name: Release run-name: ${{ github.event_name == 'workflow_dispatch' && inputs.release_operation != 'none' && format('Release {0} {1} ({2})', inputs.release_operation, inputs.release_channel, inputs.release_bump) || github.event_name == 'workflow_dispatch' && inputs.governance_preflight_nonce != '' && format('Release governance preflight {0}', inputs.governance_preflight_nonce) || github.event_name == 'workflow_dispatch' && inputs.recover_release_version != '' && format('Release recovery {0} at {1} {2}', inputs.recover_release_version, inputs.recover_release_commit, inputs.recover_release_nonce) || github.event_name == 'workflow_dispatch' && inputs.repair_npm_version != '' && format('Release npm repair {0}', inputs.repair_npm_version) || github.event_name == 'workflow_dispatch' && inputs.repair_gitee_version != '' && format('Release Gitee repair {0}', inputs.repair_gitee_version) || github.event_name == 'workflow_dispatch' && inputs.repair_oss_version != '' && format('Release OSS repair {0}', inputs.repair_oss_version) || github.workflow }} on: workflow_dispatch: inputs: release_operation: description: "Plan the next version or publish it from the current default-branch commit" required: true default: none type: choice options: - none - plan - publish release_channel: description: "Release channel (used only when release_operation is plan/publish)" required: true default: beta type: choice options: - beta - stable release_bump: description: "New beta core bump; ignored while continuing an open beta line" required: true default: patch type: choice options: - patch - minor - major repair_npm_version: description: "Recover npm for a public immutable release without moving latest/beta" required: false type: string repair_gitee_version: description: "Mirror an existing public immutable release's assets to Gitee" required: false type: string repair_oss_version: description: "Mirror an existing public immutable release's assets to OSS" required: false type: string governance_preflight_commit: description: "Verify release governance for this exact default-branch commit without publishing" required: false type: string governance_preflight_nonce: description: "Unique identifier for one release governance preflight" required: false type: string recover_release_version: description: "Recover one existing failed release tag (vX.Y.Z or vX.Y.Z-beta.N)" required: false type: string recover_release_tag_object: description: "Expected 40-character annotated tag object SHA" required: false type: string recover_release_commit: description: "Expected 40-character peeled release commit SHA" required: false type: string recover_failed_run_id: description: "Failed exact-tag Release workflow run ID" required: false type: string recover_failed_run_attempt: description: "Failed attempt number within the exact-tag Release workflow run" required: false type: string recover_release_nonce: description: "Unique identifier for this machine-verified recovery request" required: false type: string recover_release_confirmation: description: "Type the exact release version again to authorize recovery" required: false type: string permissions: contents: read # Queue governance checks, tag releases, protected recoveries, and channel # repairs behind one publication lock. The local entrypoint also refuses to tag # while a Release run is active. concurrency: group: dws-release-publication queue: max jobs: dispatch-contract: if: ${{ github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest timeout-minutes: 2 outputs: mode: ${{ steps.mode.outputs.mode }} steps: - name: Select exactly one dispatch mode id: mode env: REPAIR_NPM_VERSION: ${{ inputs.repair_npm_version }} REPAIR_GITEE_VERSION: ${{ inputs.repair_gitee_version }} REPAIR_OSS_VERSION: ${{ inputs.repair_oss_version }} GOVERNANCE_COMMIT: ${{ inputs.governance_preflight_commit }} GOVERNANCE_NONCE: ${{ inputs.governance_preflight_nonce }} RECOVER_VERSION: ${{ inputs.recover_release_version }} RECOVER_TAG_OBJECT: ${{ inputs.recover_release_tag_object }} RECOVER_COMMIT: ${{ inputs.recover_release_commit }} RECOVER_FAILED_RUN_ID: ${{ inputs.recover_failed_run_id }} RECOVER_FAILED_RUN_ATTEMPT: ${{ inputs.recover_failed_run_attempt }} RECOVER_NONCE: ${{ inputs.recover_release_nonce }} RECOVER_CONFIRMATION: ${{ inputs.recover_release_confirmation }} RELEASE_OPERATION: ${{ inputs.release_operation }} RELEASE_CHANNEL: ${{ inputs.release_channel }} RELEASE_BUMP: ${{ inputs.release_bump }} run: | set -eu release_flow=0 npm_repair=0 gitee_repair=0 oss_repair=0 governance=0 recovery=0 if test -n "$RELEASE_OPERATION" && test "$RELEASE_OPERATION" != none; then release_flow=1 fi test -z "$REPAIR_NPM_VERSION" || npm_repair=1 test -z "$REPAIR_GITEE_VERSION" || gitee_repair=1 test -z "$REPAIR_OSS_VERSION" || oss_repair=1 if test -n "$GOVERNANCE_COMMIT" || test -n "$GOVERNANCE_NONCE"; then governance=1; fi if test -n "$RECOVER_VERSION" || test -n "$RECOVER_TAG_OBJECT" || \ test -n "$RECOVER_COMMIT" || test -n "$RECOVER_FAILED_RUN_ID" || \ test -n "$RECOVER_FAILED_RUN_ATTEMPT" || \ test -n "$RECOVER_NONCE" || \ test -n "$RECOVER_CONFIRMATION"; then recovery=1; fi test $((release_flow + npm_repair + gitee_repair + oss_repair + governance + recovery)) -eq 1 || { echo "workflow_dispatch must select exactly one release mode" >&2 exit 1 } if test "$release_flow" -eq 1; then case "$RELEASE_CHANNEL" in beta|stable) ;; *) echo "release_channel must be beta or stable" >&2 exit 1 esac case "$RELEASE_BUMP" in patch|minor|major) ;; *) echo "release_bump must be patch, minor, or major" >&2 exit 1 esac if test "$RELEASE_OPERATION" = plan; then echo "mode=plan_release" >> "$GITHUB_OUTPUT" elif test "$RELEASE_OPERATION" = publish; then echo "mode=create_release" >> "$GITHUB_OUTPUT" else echo "release_operation must be none, plan, or publish" >&2 exit 1 fi elif test "$npm_repair" -eq 1; then echo "mode=repair_npm" >> "$GITHUB_OUTPUT" elif test "$gitee_repair" -eq 1; then echo "mode=repair_gitee" >> "$GITHUB_OUTPUT" elif test "$oss_repair" -eq 1; then echo "mode=repair_oss" >> "$GITHUB_OUTPUT" elif test "$governance" -eq 1; then if test -z "$GOVERNANCE_COMMIT" || test -z "$GOVERNANCE_NONCE"; then echo "governance preflight requires both commit and nonce" >&2 exit 1 fi echo "mode=governance_preflight" >> "$GITHUB_OUTPUT" else if test -z "$RECOVER_VERSION" || test -z "$RECOVER_TAG_OBJECT" || \ test -z "$RECOVER_COMMIT" || test -z "$RECOVER_FAILED_RUN_ID" || \ test -z "$RECOVER_FAILED_RUN_ATTEMPT" || \ test -z "$RECOVER_NONCE" || test -z "$RECOVER_CONFIRMATION"; then echo "release recovery requires every recovery input" >&2 exit 1 fi test "$RECOVER_CONFIRMATION" = "$RECOVER_VERSION" || { echo "release recovery confirmation must equal the exact version" >&2 exit 1 } printf '%s\n' "$RECOVER_COMMIT" | grep -Eq '^[0-9a-f]{40}$' || { echo "recover_release_commit must be a full lowercase commit SHA" >&2 exit 1 } printf '%s\n' "$RECOVER_TAG_OBJECT" | grep -Eq '^[0-9a-f]{40}$' || { echo "recover_release_tag_object must be a full lowercase tag object SHA" >&2 exit 1 } printf '%s\n' "$RECOVER_FAILED_RUN_ID" | grep -Eq '^[1-9][0-9]*$' || { echo "recover_failed_run_id must be a positive run ID" >&2 exit 1 } printf '%s\n' "$RECOVER_FAILED_RUN_ATTEMPT" | grep -Eq '^[1-9][0-9]*$' || { echo "recover_failed_run_attempt must be a positive attempt number" >&2 exit 1 } printf '%s\n' "$RECOVER_NONCE" | grep -Eq "^${RECOVER_COMMIT}-[0-9]+-[0-9]+$" || { echo "recover_release_nonce must be bound to the release commit" >&2 exit 1 } echo "mode=recover_release" >> "$GITHUB_OUTPUT" fi - name: Authorize repository release requester if: ${{ steps.mode.outputs.mode == 'plan_release' || steps.mode.outputs.mode == 'create_release' }} uses: actions/github-script@v7 env: EXPECTED_REPOSITORY: DingTalk-Real-AI/dingtalk-workspace-cli REQUESTED_BY: ${{ github.actor }} TRIGGERED_BY: ${{ github.triggering_actor }} with: script: | const expectedRepository = process.env.EXPECTED_REPOSITORY; if (context.payload.repository.full_name !== expectedRepository) { core.setFailed(`release requests are restricted to ${expectedRepository}`); return; } const actors = new Set([ process.env.REQUESTED_BY, process.env.TRIGGERED_BY, ]); for (const username of actors) { if (!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(username || "")) { core.setFailed("release request contains an invalid GitHub actor"); return; } let permission; try { const response = await github.rest.repos.getCollaboratorPermissionLevel({ ...context.repo, username, }); permission = response.data.permission; } catch (error) { if (error.status === 404) { core.setFailed(`${username} is not an authorized repository collaborator`); return; } throw error; } // GitHub maps maintain to the legacy write permission here. if (!["write", "admin"].includes(permission)) { core.setFailed( `${username} needs repository write, maintain, or admin access to release`, ); return; } } governance-preflight: name: Release governance preflight needs: dispatch-contract if: ${{ needs.dispatch-contract.outputs.mode == 'governance_preflight' || needs.dispatch-contract.outputs.mode == 'create_release' }} runs-on: ubuntu-latest timeout-minutes: 5 permissions: checks: read contents: read steps: - name: Validate trusted governance preflight context env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} EXPECTED_REPOSITORY: DingTalk-Real-AI/dingtalk-workspace-cli DISPATCH_MODE: ${{ needs.dispatch-contract.outputs.mode }} PREFLIGHT_COMMIT: ${{ needs.dispatch-contract.outputs.mode == 'create_release' && github.sha || inputs.governance_preflight_commit }} PREFLIGHT_NONCE: ${{ inputs.governance_preflight_nonce }} REPAIR_NPM_VERSION: ${{ inputs.repair_npm_version }} run: | set -eu test "$GITHUB_REPOSITORY" = "$EXPECTED_REPOSITORY" || { echo "governance preflight is restricted to $EXPECTED_REPOSITORY" >&2 exit 1 } test "$GITHUB_REF_NAME" = "$DEFAULT_BRANCH" || { echo "governance preflight must run from the default branch $DEFAULT_BRANCH" >&2 exit 1 } test -z "$REPAIR_NPM_VERSION" || { echo "governance preflight cannot be combined with npm repair" >&2 exit 1 } case "$DISPATCH_MODE" in governance_preflight) if test -z "$PREFLIGHT_COMMIT" || test -z "$PREFLIGHT_NONCE"; then echo "governance_preflight_commit and governance_preflight_nonce are both required" >&2 exit 1 fi ;; create_release) test -z "$PREFLIGHT_NONCE" || { echo "cloud release cannot be combined with a governance nonce" >&2 exit 1 } ;; *) echo "unsupported governance preflight mode: $DISPATCH_MODE" >&2 exit 1 ;; esac case "$PREFLIGHT_COMMIT" in *[!0-9a-f]*) echo "governance_preflight_commit must be a lowercase commit SHA" >&2 exit 1 ;; esac test "${#PREFLIGHT_COMMIT}" -eq 40 || { echo "governance_preflight_commit must be a full 40-character commit SHA" >&2 exit 1 } test "$PREFLIGHT_COMMIT" = "$GITHUB_SHA" || { echo "governance preflight commit must equal the dispatched default-branch commit" >&2 exit 1 } - name: Require successful Code Admission contexts on the preflight commit uses: actions/github-script@v7 env: PREFLIGHT_COMMIT: ${{ needs.dispatch-contract.outputs.mode == 'create_release' && github.sha || inputs.governance_preflight_commit }} with: script: | const { owner, repo } = context.repo; const sha = process.env.PREFLIGHT_COMMIT; const requiredContexts = [ "Lint", "Test", "Coverage", "Policy", "Edition", "Interface Integrity", "AI Behavior", "CLI Smoke", "Mock MCP", ]; const runs = await github.paginate(github.rest.checks.listForRef, { owner, repo, ref: sha, filter: "latest", per_page: 100, }); const latestByName = new Map(); for (const run of runs) { if (run.head_sha !== sha || !requiredContexts.includes(run.name)) { continue; } const current = latestByName.get(run.name); if (!current || run.id > current.id) { latestByName.set(run.name, run); } } const missing = requiredContexts.filter((name) => !latestByName.has(name)); const nonSuccess = requiredContexts.flatMap((name) => { const run = latestByName.get(name); if (!run || run.conclusion === "success") { return []; } return [`${name}=${run.conclusion || run.status || "unknown"}`]; }); if (missing.length > 0 || nonSuccess.length > 0) { core.setFailed( `Code Admission contexts are not all successful for ${sha}; ` + `missing: ${missing.length > 0 ? missing.join(", ") : "none"}; ` + `non-success: ${nonSuccess.length > 0 ? nonSuccess.join(", ") : "none"}`, ); } - name: Check out trusted preflight tooling uses: actions/checkout@v4 with: ref: ${{ needs.dispatch-contract.outputs.mode == 'create_release' && github.sha || inputs.governance_preflight_commit }} persist-credentials: false fetch-depth: 0 - name: Require release governance token env: RELEASE_GOVERNANCE_TOKEN: ${{ secrets.RELEASE_GOVERNANCE_TOKEN }} run: | test -n "$RELEASE_GOVERNANCE_TOKEN" || { echo "RELEASE_GOVERNANCE_TOKEN with repository Administration read permission is required" >&2 exit 1 } - name: Require immutable releases governance uses: actions/github-script@v7 with: # GITHUB_TOKEN cannot read repository administration settings. This # dedicated credential needs only repository Administration: read. github-token: ${{ secrets.RELEASE_GOVERNANCE_TOKEN }} script: | const { owner, repo } = context.repo; const response = await github.request( "GET /repos/{owner}/{repo}/immutable-releases", { owner, repo, headers: { "X-GitHub-Api-Version": "2026-03-10" }, }, ); if (response.data.enabled !== true) { core.setFailed("Immutable releases must be enabled before publishing"); } release-contract: needs: [dispatch-contract, governance-preflight, release-plan, seal-release] if: ${{ !cancelled() && (github.event_name == 'push' || (needs.dispatch-contract.result == 'success' && needs.dispatch-contract.outputs.mode == 'recover_release') || (needs.dispatch-contract.result == 'success' && needs.dispatch-contract.outputs.mode == 'create_release' && needs.governance-preflight.result == 'success' && needs.release-plan.result == 'success' && needs.seal-release.result == 'success')) }} runs-on: ubuntu-latest timeout-minutes: 20 permissions: actions: read checks: read contents: read outputs: release_version: ${{ steps.target.outputs.release_version }} release_commit: ${{ steps.target.outputs.release_commit }} release_tag_object: ${{ steps.target.outputs.release_tag_object }} oss_mirror: ${{ steps.target.outputs.oss_mirror }} is_recovery: ${{ steps.target.outputs.is_recovery }} failed_run_id: ${{ steps.target.outputs.failed_run_id }} channel: ${{ steps.metadata.outputs.channel }} previous_stable: ${{ steps.metadata.outputs.previous_stable }} previous_stable_commit: ${{ steps.metadata.outputs.previous_stable_commit }} from_beta: ${{ steps.metadata.outputs.from_beta }} from_beta_commit: ${{ steps.metadata.outputs.from_beta_commit }} steps: - name: Resolve and verify exact release target id: target uses: actions/github-script@v7 env: RECOVER_VERSION: ${{ inputs.recover_release_version }} RECOVER_TAG_OBJECT: ${{ inputs.recover_release_tag_object }} RECOVER_COMMIT: ${{ inputs.recover_release_commit }} RECOVER_FAILED_RUN_ID: ${{ inputs.recover_failed_run_id }} RECOVER_FAILED_RUN_ATTEMPT: ${{ inputs.recover_failed_run_attempt }} DISPATCH_MODE: ${{ needs.dispatch-contract.outputs.mode }} SEALED_VERSION: ${{ needs.seal-release.outputs.release_version }} SEALED_COMMIT: ${{ needs.seal-release.outputs.release_commit }} SEALED_TAG_OBJECT: ${{ needs.seal-release.outputs.release_tag_object }} with: script: | const { owner, repo } = context.repo; const dispatchMode = process.env.DISPATCH_MODE; const isRecovery = context.eventName === "workflow_dispatch" && dispatchMode === "recover_release"; const isCreate = context.eventName === "workflow_dispatch" && dispatchMode === "create_release"; let version = context.ref.replace("refs/tags/", ""); let commit = context.sha; let expectedTagObject = ""; let failedRunId = ""; let failedRunAttempt = ""; if (context.eventName === "workflow_dispatch" && !isRecovery && !isCreate) { core.setFailed(`unsupported publication dispatch mode: ${dispatchMode}`); return; } if (isRecovery || isCreate) { if ( context.payload.repository.full_name !== "DingTalk-Real-AI/dingtalk-workspace-cli" || context.ref !== `refs/heads/${context.payload.repository.default_branch}` ) { core.setFailed("release recovery must be dispatched from the official default branch"); return; } version = isCreate ? process.env.SEALED_VERSION : process.env.RECOVER_VERSION; commit = isCreate ? process.env.SEALED_COMMIT : process.env.RECOVER_COMMIT; expectedTagObject = isCreate ? process.env.SEALED_TAG_OBJECT : process.env.RECOVER_TAG_OBJECT; if (isRecovery) { failedRunId = process.env.RECOVER_FAILED_RUN_ID; failedRunAttempt = process.env.RECOVER_FAILED_RUN_ATTEMPT; } if (!/^v(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-beta\.[1-9]\d*)?$/.test(version)) { core.setFailed(`invalid dispatched release version: ${version}`); return; } if (!/^[0-9a-f]{40}$/.test(commit) || !/^[0-9a-f]{40}$/.test(expectedTagObject)) { core.setFailed("dispatched tag object and commit must be full lowercase SHA-1 values"); return; } if (isRecovery && !/^[1-9]\d*$/.test(failedRunId)) { core.setFailed("recover_failed_run_id must be a positive workflow run ID"); return; } if (isRecovery && !/^[1-9]\d*$/.test(failedRunAttempt)) { core.setFailed("recover_failed_run_attempt must be a positive run attempt"); return; } } const ref = await github.rest.git.getRef({ owner, repo, ref: `tags/${version}` }); if (ref.data.object.type !== "tag") { core.setFailed(`${version} is not an annotated tag`); return; } const tagObject = ref.data.object.sha; const tag = await github.rest.git.getTag({ owner, repo, tag_sha: tagObject }); if (tag.data.tag !== version || tag.data.object.type !== "commit") { core.setFailed(`${version} does not resolve to one annotated commit tag`); return; } if (tag.data.object.sha !== commit) { core.setFailed(`${version} resolves to ${tag.data.object.sha}, expected ${commit}`); return; } if ((isRecovery || isCreate) && tagObject !== expectedTagObject) { core.setFailed(`${version} tag object is ${tagObject}, expected ${expectedTagObject}`); return; } const tagFields = new Map(); for (const line of tag.data.message.split(/\r?\n/)) { const separator = line.indexOf(": "); if (separator < 1) continue; const key = line.slice(0, separator); if (tagFields.has(key)) { core.setFailed(`${version} annotated tag contains duplicate ${key} metadata`); return; } tagFields.set(key, line.slice(separator + 2)); } const ossMirror = tagFields.has("OSS-Mirror") ? tagFields.get("OSS-Mirror") : "enabled"; if (!["enabled", "deferred"].includes(ossMirror)) { core.setFailed(`${version} contains invalid OSS-Mirror metadata`); return; } const cloudOnlyKeys = [ "Release-Run", "Release-Run-Attempt", "Requested-By", "Requested-By-ID", "Sealed-Commit", "Workflow-Commit", "Allocation-Fingerprint", ]; const cloudKeys = ["Channel", ...cloudOnlyKeys]; const hasAnyCloudMetadata = cloudOnlyKeys.some((key) => tagFields.has(key)); const isCloudSeal = cloudKeys.every((key) => tagFields.has(key)); if (hasAnyCloudMetadata && !isCloudSeal) { core.setFailed(`${version} contains incomplete cloud seal metadata`); return; } if (isCloudSeal) { const expectedChannel = version.includes("-beta.") ? "prerelease" : "stable"; const actorId = String(context.payload.sender?.id || ""); const sealedRunAttempt = Number(tagFields.get("Release-Run-Attempt")); const currentRunAttempt = Number(process.env.GITHUB_RUN_ATTEMPT); if ( tagFields.get("Channel") !== expectedChannel || tagFields.get("Sealed-Commit") !== commit || tagFields.get("Workflow-Commit") !== commit || !/^[0-9a-f]{64}$/.test(tagFields.get("Allocation-Fingerprint")) || !/^[1-9]\d*$/.test(tagFields.get("Release-Run")) || !/^[1-9]\d*$/.test(tagFields.get("Release-Run-Attempt")) || !/^[1-9]\d*$/.test(tagFields.get("Requested-By-ID")) ) { core.setFailed(`${version} cloud seal metadata does not match its tag authority`); return; } if (isCreate && ( tagFields.get("Release-Run") !== String(context.runId) || !Number.isSafeInteger(sealedRunAttempt) || !Number.isSafeInteger(currentRunAttempt) || sealedRunAttempt < 1 || sealedRunAttempt > currentRunAttempt || tagFields.get("Requested-By") !== context.actor || tagFields.get("Requested-By-ID") !== actorId )) { core.setFailed(`${version} cloud seal is not bound to this release request`); return; } } else if (isCreate) { core.setFailed(`${version} is missing cloud seal metadata`); return; } if (isRecovery || isCreate) { const defaultBranch = context.payload.repository.default_branch; const branch = await github.rest.repos.getBranch({ owner, repo, branch: defaultBranch }); const comparison = await github.rest.repos.compareCommitsWithBasehead({ owner, repo, basehead: `${commit}...${branch.data.commit.sha}`, }); if (!["ahead", "identical"].includes(comparison.data.status)) { core.setFailed(`${version} commit is not contained in ${defaultBranch}`); return; } } if (isRecovery) { const failedRun = await github.request( "GET /repos/{owner}/{repo}/actions/runs/{run_id}/attempts/{attempt_number}", { owner, repo, run_id: Number(failedRunId), attempt_number: Number(failedRunAttempt), }, ); const run = failedRun.data; const failedByCloud = isCloudSeal; const expectedBranch = failedByCloud ? context.payload.repository.default_branch : version; const expectedEvent = failedByCloud ? "workflow_dispatch" : "push"; if ( run.id !== Number(failedRunId) || run.run_attempt !== Number(failedRunAttempt) || run.repository.full_name !== `${owner}/${repo}` || run.path !== ".github/workflows/release.yml" || run.event !== expectedEvent || run.status !== "completed" || !["failure", "cancelled", "timed_out", "startup_failure", "stale"].includes(run.conclusion) || run.head_branch !== expectedBranch || run.head_sha !== commit ) { core.setFailed( `run ${failedRunId} attempt ${failedRunAttempt} is not the failed sealed Release attempt for ${version}`, ); return; } if (failedByCloud && ( tagFields.get("Release-Run") !== failedRunId || tagFields.get("Release-Run-Attempt") !== failedRunAttempt || tagFields.get("Requested-By") !== String(run.actor?.login || "") || tagFields.get("Requested-By-ID") !== String(run.actor?.id || "") )) { core.setFailed( `run ${failedRunId} attempt ${failedRunAttempt} is not bound by the cloud seal for ${version}`, ); return; } try { const release = await github.rest.repos.getReleaseByTag({ owner, repo, tag: version }); if (!release.data.draft) { core.setFailed(`${version} already has a public release; use a channel repair instead`); return; } } catch (error) { if (error.status !== 404) throw error; } } core.setOutput("release_version", version); core.setOutput("release_commit", commit); core.setOutput("release_tag_object", tagObject); core.setOutput("oss_mirror", ossMirror); core.setOutput("is_recovery", isRecovery ? "true" : "false"); core.setOutput("failed_run_id", failedRunId); # create_release already ran this exact source contract and its delivered # baselines in release-plan. The seal binds that plan's commit and complete # tag-ref fingerprint; the target step above then verifies the resulting # annotated tag object and cloud metadata. Tag pushes and recoveries have # no same-run candidate proof, so they retain the full independent path. - name: Check out repository if: ${{ needs.dispatch-contract.outputs.mode != 'create_release' }} uses: actions/checkout@v4 with: ref: ${{ steps.target.outputs.release_commit }} persist-credentials: false fetch-depth: 0 - name: Check out trusted release tooling if: ${{ needs.dispatch-contract.outputs.mode != 'create_release' }} uses: actions/checkout@v4 with: ref: ${{ github.sha }} path: tmp/trusted-release-tooling persist-credentials: false - name: Fetch release branch and official tags if: ${{ needs.dispatch-contract.outputs.mode != 'create_release' }} run: | git fetch --force origin \ '+refs/heads/main:refs/remotes/origin/main' git fetch --force --no-tags \ https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git \ '+refs/tags/v*:refs/tags/v*' \ '+refs/tags/withdrawn/v*:refs/tags/withdrawn/v*' - name: Validate release contract if: ${{ needs.dispatch-contract.outputs.mode != 'create_release' }} id: contract env: RELEASE_VERSION: ${{ steps.target.outputs.release_version }} run: | set -eu . ./scripts/release/release-lib.sh channel="$(release_channel_for_version "$RELEASE_VERSION")" ./scripts/release/release-contract.sh \ --channel "$channel" \ --version "$RELEASE_VERSION" \ --context ci \ --remote origin \ --branch main \ --notes-output "$RUNNER_TEMP/release-notes.md" \ --metadata-output "$GITHUB_OUTPUT" - name: Verify remote annotated tag authority if: ${{ needs.dispatch-contract.outputs.mode != 'create_release' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_VERSION: ${{ steps.target.outputs.release_version }} RELEASE_COMMIT: ${{ steps.target.outputs.release_commit }} RELEASE_TAG_OBJECT: ${{ steps.target.outputs.release_tag_object }} run: | "$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/verify-github-tag-authority.sh" \ "$RELEASE_VERSION" "$RELEASE_COMMIT" "$RELEASE_TAG_OBJECT" - name: Require successful Code Admission contexts on the sealed commit uses: actions/github-script@v7 env: RELEASE_COMMIT: ${{ steps.target.outputs.release_commit }} with: script: | const { owner, repo } = context.repo; const sha = process.env.RELEASE_COMMIT; const requiredContexts = [ "Lint", "Test", "Coverage", "Policy", "Edition", "Interface Integrity", "AI Behavior", "CLI Smoke", "Mock MCP", ]; const runs = await github.paginate(github.rest.checks.listForRef, { owner, repo, ref: sha, filter: "latest", per_page: 100, }); const latestByName = new Map(); for (const run of runs) { if (run.head_sha !== sha || !requiredContexts.includes(run.name)) { continue; } const current = latestByName.get(run.name); if (!current || run.id > current.id) { latestByName.set(run.name, run); } } const missing = requiredContexts.filter((name) => !latestByName.has(name)); const nonSuccess = requiredContexts.flatMap((name) => { const run = latestByName.get(name); if (!run || run.conclusion === "success") { return []; } return [`${name}=${run.conclusion || run.status || "unknown"}`]; }); if (missing.length > 0 || nonSuccess.length > 0) { core.setFailed( `Code Admission contexts are not all successful for ${sha}; ` + `missing: ${missing.length > 0 ? missing.join(", ") : "none"}; ` + `non-success: ${nonSuccess.length > 0 ? nonSuccess.join(", ") : "none"}`, ); } - name: Require delivered previous stable baseline if: ${{ github.event_name == 'push' || needs.dispatch-contract.outputs.mode == 'recover_release' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PREVIOUS_STABLE: ${{ steps.contract.outputs.previous_stable }} PREVIOUS_STABLE_COMMIT: ${{ steps.contract.outputs.previous_stable_commit }} run: | "$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/verify-delivered-stable.sh" \ "$PREVIOUS_STABLE" "$PREVIOUS_STABLE_COMMIT" - name: Require release governance token env: RELEASE_GOVERNANCE_TOKEN: ${{ secrets.RELEASE_GOVERNANCE_TOKEN }} run: | test -n "$RELEASE_GOVERNANCE_TOKEN" || { echo "RELEASE_GOVERNANCE_TOKEN with repository Administration read permission is required" >&2 exit 1 } - name: Require immutable releases governance uses: actions/github-script@v7 with: # GITHUB_TOKEN cannot read repository administration settings. This # dedicated credential needs only repository Administration: read. github-token: ${{ secrets.RELEASE_GOVERNANCE_TOKEN }} script: | const { owner, repo } = context.repo; const response = await github.request( "GET /repos/{owner}/{repo}/immutable-releases", { owner, repo, headers: { "X-GitHub-Api-Version": "2026-03-10" }, }, ); if (response.data.enabled !== true) { core.setFailed("Immutable releases must be enabled before publishing"); } - name: Require successful beta delivery before stable promotion if: ${{ steps.contract.outputs.channel == 'stable' && (github.event_name == 'push' || needs.dispatch-contract.outputs.mode == 'recover_release') }} uses: actions/github-script@v7 env: FROM_BETA: ${{ steps.contract.outputs.from_beta }} FROM_BETA_COMMIT: ${{ steps.contract.outputs.from_beta_commit }} with: script: | const { owner, repo } = context.repo; const expectedAssets = new Set([ "dws-darwin-amd64.tar.gz", "dws-darwin-arm64.tar.gz", "dws-linux-amd64.tar.gz", "dws-linux-arm64.tar.gz", "dws-windows-amd64.zip", "dws-windows-arm64.zip", "dws-skills.zip", "checksums.txt", ]); const { data: release } = await github.rest.repos.getReleaseByTag({ owner, repo, tag: process.env.FROM_BETA, }); if (release.draft || !release.prerelease || !release.immutable) { core.setFailed(`${process.env.FROM_BETA} is not a published immutable prerelease`); return; } const assetNames = release.assets.map((asset) => asset.name); const names = new Set(assetNames); const missing = [...expectedAssets].filter((name) => !names.has(name)); const extra = assetNames.filter((name) => !expectedAssets.has(name)); if ( missing.length > 0 || extra.length > 0 || assetNames.length !== expectedAssets.size ) { core.setFailed( `beta release asset set differs: missing=${missing.join(",")} extra=${extra.join(",")}`, ); return; } - name: Verify beta Release workflow delivery if: ${{ steps.contract.outputs.channel == 'stable' && (github.event_name == 'push' || needs.dispatch-contract.outputs.mode == 'recover_release') }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} FROM_BETA: ${{ steps.contract.outputs.from_beta }} FROM_BETA_COMMIT: ${{ steps.contract.outputs.from_beta_commit }} run: | "$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/verify-release-workflow-delivery.sh" \ "$FROM_BETA" "$FROM_BETA_COMMIT" - name: Resolve validated release metadata id: metadata env: DISPATCH_MODE: ${{ needs.dispatch-contract.outputs.mode }} PLAN_CHANNEL: ${{ needs.release-plan.outputs.channel }} PLAN_PREVIOUS_STABLE: ${{ needs.release-plan.outputs.previous_stable }} PLAN_PREVIOUS_STABLE_COMMIT: ${{ needs.release-plan.outputs.previous_stable_commit }} PLAN_FROM_BETA: ${{ needs.release-plan.outputs.from_beta }} PLAN_FROM_BETA_COMMIT: ${{ needs.release-plan.outputs.from_beta_commit }} CONTRACT_CHANNEL: ${{ steps.contract.outputs.channel }} CONTRACT_PREVIOUS_STABLE: ${{ steps.contract.outputs.previous_stable }} CONTRACT_PREVIOUS_STABLE_COMMIT: ${{ steps.contract.outputs.previous_stable_commit }} CONTRACT_FROM_BETA: ${{ steps.contract.outputs.from_beta }} CONTRACT_FROM_BETA_COMMIT: ${{ steps.contract.outputs.from_beta_commit }} run: | set -eu if test "$DISPATCH_MODE" = create_release; then channel="$PLAN_CHANNEL" previous_stable="$PLAN_PREVIOUS_STABLE" previous_stable_commit="$PLAN_PREVIOUS_STABLE_COMMIT" from_beta="$PLAN_FROM_BETA" from_beta_commit="$PLAN_FROM_BETA_COMMIT" else channel="$CONTRACT_CHANNEL" previous_stable="$CONTRACT_PREVIOUS_STABLE" previous_stable_commit="$CONTRACT_PREVIOUS_STABLE_COMMIT" from_beta="$CONTRACT_FROM_BETA" from_beta_commit="$CONTRACT_FROM_BETA_COMMIT" fi test -n "$channel" test -n "$previous_stable" test -n "$previous_stable_commit" case "$channel" in prerelease) test -z "$from_beta" test -z "$from_beta_commit" ;; stable) test -n "$from_beta" test -n "$from_beta_commit" ;; *) echo "unsupported validated release channel: $channel" >&2 exit 2 ;; esac { printf 'channel=%s\n' "$channel" printf 'previous_stable=%s\n' "$previous_stable" printf 'previous_stable_commit=%s\n' "$previous_stable_commit" printf 'from_beta=%s\n' "$from_beta" printf 'from_beta_commit=%s\n' "$from_beta_commit" } >> "$GITHUB_OUTPUT" release: name: Build signed release artifacts if: ${{ !cancelled() && needs.release-contract.result == 'success' }} needs: [release-contract] runs-on: ubuntu-latest timeout-minutes: 60 permissions: contents: read env: RELEASE_VERSION: ${{ needs.release-contract.outputs.release_version }} RELEASE_COMMIT: ${{ needs.release-contract.outputs.release_commit }} RELEASE_TAG_OBJECT: ${{ needs.release-contract.outputs.release_tag_object }} steps: - name: Check out repository uses: actions/checkout@v4 with: ref: ${{ needs.release-contract.outputs.release_commit }} persist-credentials: false fetch-depth: 0 - name: Check out trusted release tooling uses: actions/checkout@v4 with: ref: ${{ github.sha }} path: tmp/trusted-release-tooling persist-credentials: false - name: Fetch release branch and official tags run: | git fetch --force origin \ '+refs/heads/main:refs/remotes/origin/main' git fetch --force --no-tags \ https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git \ '+refs/tags/v*:refs/tags/v*' \ '+refs/tags/withdrawn/v*:refs/tags/withdrawn/v*' - name: Verify sealed tag before executing release source env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | "$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/verify-github-tag-authority.sh" \ "$RELEASE_VERSION" "$RELEASE_COMMIT" "$RELEASE_TAG_OBJECT" - name: Revalidate contract and render release notes run: | ./scripts/release/release-contract.sh \ --channel "${{ needs.release-contract.outputs.channel }}" \ --version "$RELEASE_VERSION" \ --context ci \ --remote origin \ --branch main \ --notes-output "$RUNNER_TEMP/release-notes.md" - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Install rcodesign (sign darwin binaries from Linux) run: | set -euo pipefail RCS_VERSION="0.29.0" RCS_ARCHIVE_SHA256="dbe85cedd8ee4217b64e9a0e4c2aef92ab8bcaaa41f20bde99781ff02e600002" curl -fsSL -o /tmp/rcodesign.tar.gz \ "https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F${RCS_VERSION}/apple-codesign-${RCS_VERSION}-x86_64-unknown-linux-musl.tar.gz" printf '%s %s\n' "$RCS_ARCHIVE_SHA256" /tmp/rcodesign.tar.gz \ | sha256sum --check --strict - mkdir -p /tmp/rcodesign tar -xzf /tmp/rcodesign.tar.gz -C /tmp/rcodesign --strip-components=1 sudo install -m 0755 /tmp/rcodesign/rcodesign /usr/local/bin/rcodesign rcodesign --version - name: Prepare Apple Developer ID certificate env: APPLE_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_CERTIFICATE_P12_BASE64 }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} run: | set -euo pipefail if [ -z "${APPLE_CERTIFICATE_P12_BASE64:-}" ] || [ -z "${APPLE_CERTIFICATE_PASSWORD:-}" ]; then if [ "$GITHUB_REPOSITORY_OWNER" = "DingTalk-Real-AI" ]; then echo "APPLE_CERTIFICATE_P12_BASE64 and APPLE_CERTIFICATE_PASSWORD are required for official releases" >&2 exit 1 fi echo "Developer ID secrets are unavailable; fork release will use ad-hoc signing." exit 0 fi umask 077 certificate_path="$RUNNER_TEMP/dws-developer-id.p12" password_path="$RUNNER_TEMP/dws-developer-id-password" printf '%s' "$APPLE_CERTIFICATE_P12_BASE64" | base64 --decode > "$certificate_path" printf '%s' "$APPLE_CERTIFICATE_PASSWORD" > "$password_path" # Fail before packaging if the secret is corrupt or the password is wrong. # The exported P12 may use legacy PKCS#12 ciphers; OpenSSL 3 requires # -legacy to validate those containers even though rcodesign can read them. openssl pkcs12 -legacy -in "$certificate_path" -passin "file:$password_path" -noout echo "DWS_APPLE_CERTIFICATE_P12=$certificate_path" >> "$GITHUB_ENV" echo "DWS_APPLE_CERTIFICATE_PASSWORD_FILE=$password_path" >> "$GITHUB_ENV" - name: Require a clean sealed source before GoReleaser run: | dirty="$(git status --porcelain --untracked-files=all)" test -z "$dirty" || { echo "sealed release source became dirty before GoReleaser:" >&2 printf '%s\n' "$dirty" >&2 exit 1 } - name: Build release artifacts without publishing uses: goreleaser/goreleaser-action@v6 with: version: "v2.16.0" args: release --clean --skip=publish --release-notes=${{ runner.temp }}/release-notes.md env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GORELEASER_CURRENT_TAG: ${{ needs.release-contract.outputs.release_version }} GORELEASER_PREVIOUS_TAG: ${{ needs.release-contract.outputs.previous_stable }} - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: "20" registry-url: "https://registry.npmjs.org" - name: Install archive tooling run: | if command -v zip >/dev/null 2>&1 && command -v unzip >/dev/null 2>&1; then exit 0 fi sudo apt-get update sudo apt-get install -y zip unzip - name: Post-release packaging run: ./scripts/release/post-goreleaser.sh env: DWS_PACKAGE_VERSION: ${{ needs.release-contract.outputs.release_version }} DWS_RELEASE_BASE_URL: https://github.com/${{ github.repository }}/releases/download/${{ needs.release-contract.outputs.release_version }} DWS_REQUIRE_DEVELOPER_ID_SIGNING: ${{ github.repository_owner == 'DingTalk-Real-AI' }} - name: Remove Apple Developer ID certificate if: ${{ always() }} run: | rm -f "$RUNNER_TEMP/dws-developer-id.p12" rm -f "$RUNNER_TEMP/dws-developer-id-password" - name: Verify final release artifacts run: ./scripts/release/verify-release-artifacts.sh "$RELEASE_VERSION" - name: Verify npm package before sealing GitHub Release run: ./scripts/release/verify-package-managers.sh --npm-only --expected-version "$RELEASE_VERSION" - name: Reverify remote tag before sealing GitHub Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | "$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/verify-github-tag-authority.sh" \ "$RELEASE_VERSION" "$RELEASE_COMMIT" "$RELEASE_TAG_OBJECT" - name: Preserve finalized distribution files uses: actions/upload-artifact@v4 with: name: finalized-release-dist path: dist/ if-no-files-found: error retention-days: 1 verify-darwin-signatures: name: Verify Apple Developer ID signatures if: ${{ !cancelled() && needs.release-contract.result == 'success' && needs.release.result == 'success' }} needs: [release-contract, release] runs-on: macos-latest timeout-minutes: 10 permissions: contents: read steps: - name: Restore finalized distribution files uses: actions/download-artifact@v4 with: name: finalized-release-dist path: dist - name: Verify finalized Darwin signatures with Apple codesign run: | set -euo pipefail for arch in amd64 arm64; do archive="dist/dws-darwin-${arch}.tar.gz" stage="$RUNNER_TEMP/verify-darwin-${arch}" mkdir -p "$stage" tar -xzf "$archive" -C "$stage" test -f "$stage/dws" codesign --verify --strict --verbose=4 "$stage/dws" codesign -dvvv "$stage/dws" done publish-release: name: Publish immutable GitHub Release if: ${{ !cancelled() && needs.release-contract.result == 'success' && needs.release-validation.result == 'success' && needs.release.result == 'success' && needs.verify-darwin-signatures.result == 'success' }} needs: [release-contract, release-validation, release, verify-darwin-signatures] runs-on: ubuntu-latest timeout-minutes: 30 permissions: checks: write contents: write env: RELEASE_VERSION: ${{ needs.release-contract.outputs.release_version }} RELEASE_COMMIT: ${{ needs.release-contract.outputs.release_commit }} RELEASE_TAG_OBJECT: ${{ needs.release-contract.outputs.release_tag_object }} IS_RECOVERY: ${{ needs.release-contract.outputs.is_recovery }} steps: - name: Check out sealed release source uses: actions/checkout@v4 with: ref: ${{ needs.release-contract.outputs.release_commit }} persist-credentials: false fetch-depth: 0 - name: Check out trusted release tooling uses: actions/checkout@v4 with: ref: ${{ github.sha }} path: tmp/trusted-release-tooling persist-credentials: false - name: Fetch release branch and official tags run: | git fetch --force origin \ '+refs/heads/main:refs/remotes/origin/main' git fetch --force --no-tags \ https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git \ '+refs/tags/v*:refs/tags/v*' \ '+refs/tags/withdrawn/v*:refs/tags/withdrawn/v*' - name: Verify sealed tag before executing publication source env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | "$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/verify-github-tag-authority.sh" \ "$RELEASE_VERSION" "$RELEASE_COMMIT" "$RELEASE_TAG_OBJECT" - name: Revalidate contract and render release notes run: | ./scripts/release/release-contract.sh \ --channel "${{ needs.release-contract.outputs.channel }}" \ --version "$RELEASE_VERSION" \ --context ci \ --remote origin \ --branch main \ --notes-output "$RUNNER_TEMP/release-notes.md" - name: Bind recovery publication to this workflow run if: ${{ needs.release-contract.outputs.is_recovery == 'true' }} run: | printf '\n\n' \ "$GITHUB_RUN_ID" "$RELEASE_TAG_OBJECT" "$RELEASE_COMMIT" \ >> "$RUNNER_TEMP/release-notes.md" - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: "20" registry-url: "https://registry.npmjs.org" - name: Install archive tooling run: sudo apt-get update && sudo apt-get install -y zip unzip - name: Restore finalized distribution files uses: actions/download-artifact@v4 with: name: finalized-release-dist path: dist - name: Verify finalized release artifacts before publication run: ./scripts/release/verify-release-artifacts.sh "$RELEASE_VERSION" - name: Verify npm package before sealing GitHub Release run: ./scripts/release/verify-package-managers.sh --npm-only --expected-version "$RELEASE_VERSION" - name: Reverify remote tag before sealing GitHub Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | "$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/verify-github-tag-authority.sh" \ "$RELEASE_VERSION" "$RELEASE_COMMIT" "$RELEASE_TAG_OBJECT" - name: Publish or reuse immutable GitHub Release id: publish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_CHANNEL: ${{ needs.release-contract.outputs.channel }} run: | set -eu expected_prerelease=false prerelease_flag="" if [ "$RELEASE_CHANNEL" = "prerelease" ]; then expected_prerelease=true prerelease_flag="--prerelease" fi release_id="" if gh release view "$RELEASE_VERSION" >/dev/null 2>&1; then release_id="$( gh release view "$RELEASE_VERSION" \ --json databaseId \ --jq .databaseId )" printf '%s\n' "$release_id" | grep -Eq '^[1-9][0-9]*$' || { echo "Existing GitHub Release has an invalid database ID: $release_id" >&2 exit 1 } resolved_tag="$( gh api -H 'X-GitHub-Api-Version: 2026-03-10' \ "repos/$GITHUB_REPOSITORY/releases/$release_id" \ --jq .tag_name )" test "$resolved_tag" = "$RELEASE_VERSION" || { echo "GitHub Release ID $release_id targets $resolved_tag, not $RELEASE_VERSION." >&2 exit 1 } is_draft="$( gh api -H 'X-GitHub-Api-Version: 2026-03-10' \ "repos/$GITHUB_REPOSITORY/releases/$release_id" \ --jq .draft )" is_prerelease="$( gh api -H 'X-GitHub-Api-Version: 2026-03-10' \ "repos/$GITHUB_REPOSITORY/releases/$release_id" \ --jq .prerelease )" if [ "$is_prerelease" != "$expected_prerelease" ]; then echo "Existing release channel does not match $RELEASE_CHANNEL." >&2 exit 1 fi if [ "$is_draft" = "false" ]; then is_immutable="$( gh api -H 'X-GitHub-Api-Version: 2026-03-10' \ "repos/$GITHUB_REPOSITORY/releases/$release_id" \ --jq .immutable )" if [ "$is_immutable" != "true" ]; then echo "Published release is mutable; refusing to reuse its assets." >&2 exit 1 fi remote_body="$( gh api -H 'X-GitHub-Api-Version: 2026-03-10' \ "repos/$GITHUB_REPOSITORY/releases/$release_id" \ --jq .body )" local_body="$(cat "$RUNNER_TEMP/release-notes.md")" if [ "$remote_body" != "$local_body" ]; then echo "Published release notes differ from the sealed CHANGELOG." >&2 exit 1 fi if [ "$IS_RECOVERY" = "true" ]; then recovery_marker="" case "$remote_body" in *"$recovery_marker"*) ;; *) echo "Public release is not bound to this exact recovery run." >&2 exit 1 ;; esac fi published_dir="$RUNNER_TEMP/published-release" mkdir -p "$published_dir" DWS_GITHUB_RELEASE_ID="$release_id" \ "$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/download-github-release-assets.sh" \ "$RELEASE_VERSION" "$published_dir" DWS_PACKAGE_DIST_DIR="$published_dir" \ ./scripts/release/verify-release-artifacts.sh "$RELEASE_VERSION" for asset in "$published_dir"/dws-* "$published_dir"/checksums.txt; do if [ "$IS_RECOVERY" = "true" ] && ! cmp -s "$asset" "dist/$(basename "$asset")"; then echo "Public recovery asset differs from this run's sealed artifact: $(basename "$asset")" >&2 exit 1 fi cp "$asset" dist/ done DWS_PACKAGE_DIST_DIR="$GITHUB_WORKSPACE/dist" \ DWS_PACKAGE_SOURCE_ROOT="$GITHUB_WORKSPACE" \ ./scripts/release/stage-npm-package.sh "$RELEASE_VERSION" echo "release_id=$release_id" >> "$GITHUB_OUTPUT" echo "Published GitHub Release is immutable and verified; reusing its exact assets." exit 0 fi else # shellcheck disable=SC2086 gh release create "$RELEASE_VERSION" \ --verify-tag \ --draft \ --title "$RELEASE_VERSION" \ --notes-file "$RUNNER_TEMP/release-notes.md" \ $prerelease_flag release_id="$( gh release view "$RELEASE_VERSION" \ --json databaseId \ --jq .databaseId )" fi printf '%s\n' "$release_id" | grep -Eq '^[1-9][0-9]*$' || { echo "Draft GitHub Release has an invalid database ID: $release_id" >&2 exit 1 } resolved_tag="$( gh api -H 'X-GitHub-Api-Version: 2026-03-10' \ "repos/$GITHUB_REPOSITORY/releases/$release_id" \ --jq .tag_name )" test "$resolved_tag" = "$RELEASE_VERSION" || { echo "Draft GitHub Release ID $release_id targets $resolved_tag, not $RELEASE_VERSION." >&2 exit 1 } gh api --method PATCH \ -H 'X-GitHub-Api-Version: 2026-03-10' \ "repos/$GITHUB_REPOSITORY/releases/$release_id" \ -f tag_name="$RELEASE_VERSION" \ -f target_commitish="$RELEASE_COMMIT" \ -f name="$RELEASE_VERSION" \ -f body="$(cat "$RUNNER_TEMP/release-notes.md")" \ -F draft=true \ -F prerelease="$expected_prerelease" \ >/dev/null draft_state="$( gh api -H 'X-GitHub-Api-Version: 2026-03-10' \ "repos/$GITHUB_REPOSITORY/releases/$release_id" \ --jq '[.tag_name, .draft, .prerelease] | @tsv' )" test "$draft_state" = "$(printf '%s\ttrue\t%s' "$RELEASE_VERSION" "$expected_prerelease")" || { echo "Draft GitHub Release identity or channel changed before upload." >&2 exit 1 } draft_body="$( gh api -H 'X-GitHub-Api-Version: 2026-03-10' \ "repos/$GITHUB_REPOSITORY/releases/$release_id" \ --jq .body )" local_body="$(cat "$RUNNER_TEMP/release-notes.md")" test "$draft_body" = "$local_body" || { echo "Draft GitHub Release notes differ from the sealed CHANGELOG." >&2 exit 1 } if [ "$IS_RECOVERY" = "true" ]; then recovery_marker="" case "$draft_body" in *"$recovery_marker"*) ;; *) echo "Draft GitHub Release is not bound to this exact recovery run." >&2 exit 1 ;; esac fi gh release upload "$RELEASE_VERSION" \ dist/dws-*.tar.gz \ dist/dws-windows-*.zip \ dist/dws-skills.zip \ dist/checksums.txt \ --clobber uploaded_release_id="$( gh release view "$RELEASE_VERSION" \ --json databaseId \ --jq .databaseId )" test "$uploaded_release_id" = "$release_id" || { echo "GitHub Release identity changed during upload: $release_id -> $uploaded_release_id" >&2 exit 1 } DWS_GITHUB_RELEASE_ID="$release_id" \ "$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/verify-github-release-assets.sh" \ "$RELEASE_VERSION" sealed_upload="$RUNNER_TEMP/sealed-upload" rm -rf "$sealed_upload" mkdir -p "$sealed_upload" DWS_GITHUB_RELEASE_ID="$release_id" \ "$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/download-github-release-assets.sh" \ "$RELEASE_VERSION" "$sealed_upload" DWS_PACKAGE_DIST_DIR="$sealed_upload" \ ./scripts/release/verify-release-artifacts.sh "$RELEASE_VERSION" for remote_asset in "$sealed_upload"/dws-* "$sealed_upload"/checksums.txt; do local_asset="dist/$(basename "$remote_asset")" cmp -s "$local_asset" "$remote_asset" || { echo "Uploaded GitHub asset differs from local sealed bytes: $(basename "$remote_asset")" >&2 exit 1 } done gh api --method PATCH \ -H 'X-GitHub-Api-Version: 2026-03-10' \ "repos/$GITHUB_REPOSITORY/releases/$release_id" \ -f name="$RELEASE_VERSION" \ -f body="$(cat "$RUNNER_TEMP/release-notes.md")" \ -F prerelease="$expected_prerelease" \ -F draft=false \ >/dev/null echo "release_id=$release_id" >> "$GITHUB_OUTPUT" - name: Require immutable published GitHub Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_ID: ${{ steps.publish.outputs.release_id }} RELEASE_CHANNEL: ${{ needs.release-contract.outputs.channel }} run: | set -eu expected_prerelease=false if [ "$RELEASE_CHANNEL" = "prerelease" ]; then expected_prerelease=true; fi printf '%s\n' "$RELEASE_ID" | grep -Eq '^[1-9][0-9]*$' || { echo "Published GitHub Release has an invalid database ID: $RELEASE_ID" >&2 exit 1 } release_state="$( gh api -H 'X-GitHub-Api-Version: 2026-03-10' \ "repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID" \ --jq '[.tag_name, .draft, .prerelease, .immutable] | @tsv' )" test "$release_state" = "$(printf '%s\tfalse\t%s\ttrue' "$RELEASE_VERSION" "$expected_prerelease")" || { echo "GitHub Release ID $RELEASE_ID is not the exact public immutable $RELEASE_VERSION release." >&2 exit 1 } immutable_body="$( gh api -H 'X-GitHub-Api-Version: 2026-03-10' \ "repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID" \ --jq .body )" local_body="$(cat "$RUNNER_TEMP/release-notes.md")" test "$immutable_body" = "$local_body" || { echo "Immutable GitHub Release notes differ from the sealed CHANGELOG." >&2 exit 1 } if [ "$IS_RECOVERY" = "true" ]; then recovery_marker="" case "$immutable_body" in *"$recovery_marker"*) ;; *) echo "Immutable GitHub Release is not bound to this exact recovery run." >&2 exit 1 ;; esac fi DWS_GITHUB_RELEASE_ID="$RELEASE_ID" \ "$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/verify-github-release-assets.sh" \ "$RELEASE_VERSION" immutable_dir="$RUNNER_TEMP/immutable-release" rm -rf "$immutable_dir" mkdir -p "$immutable_dir" DWS_GITHUB_RELEASE_ID="$RELEASE_ID" \ "$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/download-github-release-assets.sh" \ "$RELEASE_VERSION" "$immutable_dir" DWS_PACKAGE_DIST_DIR="$immutable_dir" \ ./scripts/release/verify-release-artifacts.sh "$RELEASE_VERSION" for immutable_asset in "$immutable_dir"/dws-* "$immutable_dir"/checksums.txt; do sealed_asset="dist/$(basename "$immutable_asset")" cmp -s "$sealed_asset" "$immutable_asset" || { echo "Immutable GitHub asset differs from this run's sealed bytes: $(basename "$immutable_asset")" >&2 exit 1 } done "$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/verify-github-tag-authority.sh" \ "$RELEASE_VERSION" "$RELEASE_COMMIT" "$RELEASE_TAG_OBJECT" - name: Publish stable Homebrew formula id: homebrew-stable if: ${{ github.repository_owner == 'DingTalk-Real-AI' && needs.release-contract.outputs.channel == 'stable' }} run: DWS_PUBLISH_OUTPUT="$GITHUB_OUTPUT" "$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/publish-homebrew-formula.sh" env: DWS_FORMULA_SOURCE: ${{ github.workspace }}/dist/homebrew/dingtalk-workspace-cli.rb DWS_TAP_REPO_URL: https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git DWS_TAP_BRANCH: main DWS_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_PR_TOKEN }} DWS_TAP_COMMIT_MESSAGE: "chore: update formula for ${{ needs.release-contract.outputs.release_version }} [skip ci]" DWS_GIT_NAME: github-actions[bot] DWS_GIT_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com - name: Publish beta Homebrew formula id: homebrew-beta if: ${{ github.repository_owner == 'DingTalk-Real-AI' && needs.release-contract.outputs.channel == 'prerelease' }} run: DWS_PUBLISH_OUTPUT="$GITHUB_OUTPUT" "$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/publish-homebrew-formula.sh" env: DWS_FORMULA_SOURCE: ${{ github.workspace }}/dist/homebrew/dingtalk-workspace-cli-beta.rb DWS_TAP_FORMULA_PATH: Formula/dingtalk-workspace-cli-beta.rb DWS_TAP_REPO_URL: https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git DWS_TAP_BRANCH: main DWS_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_PR_TOKEN }} DWS_TAP_COMMIT_MESSAGE: "chore: update beta formula for ${{ needs.release-contract.outputs.release_version }} [skip ci]" DWS_GIT_NAME: github-actions[bot] DWS_GIT_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com - name: Seal Formula-only Code Admission contexts if: ${{ github.repository_owner == 'DingTalk-Real-AI' }} uses: actions/github-script@v7 env: FORMULA_CHANGED: ${{ steps.homebrew-stable.outputs.formula_changed || steps.homebrew-beta.outputs.formula_changed }} FORMULA_COMMIT: ${{ steps.homebrew-stable.outputs.published_commit || steps.homebrew-beta.outputs.published_commit }} RELEASE_CHANNEL: ${{ needs.release-contract.outputs.channel }} with: script: | const fs = require("fs"); const { owner, repo } = context.repo; const commit = process.env.FORMULA_COMMIT; const formulaChanged = process.env.FORMULA_CHANGED === "true"; const channel = process.env.RELEASE_CHANNEL; const version = process.env.RELEASE_VERSION; const formulaPath = channel === "stable" ? "Formula/dingtalk-workspace-cli.rb" : "Formula/dingtalk-workspace-cli-beta.rb"; const sourcePath = channel === "stable" ? "dist/homebrew/dingtalk-workspace-cli.rb" : "dist/homebrew/dingtalk-workspace-cli-beta.rb"; const expectedMessage = channel === "stable" ? `chore: update formula for ${version} [skip ci]` : `chore: update beta formula for ${version} [skip ci]`; if (!/^[0-9a-f]{40}$/.test(commit)) { core.setFailed("Homebrew publisher did not return one full commit SHA"); return; } const commitResponse = await github.rest.repos.getCommit({ owner, repo, ref: commit, per_page: 100, }); const files = commitResponse.data.files || []; const requiredContexts = [ "Lint", "Test", "Coverage", "Policy", "Edition", "Interface Integrity", "AI Behavior", "CLI Smoke", "Mock MCP", ]; const exactFormulaCommit = commitResponse.data.sha === commit && commitResponse.data.parents.length === 1 && commitResponse.data.commit.message === expectedMessage && commitResponse.data.author?.login === "github-actions[bot]" && commitResponse.data.committer?.login === "github-actions[bot]" && files.length === 1 && files[0].filename === formulaPath && ["added", "modified"].includes(files[0].status); if (!exactFormulaCommit) { if (formulaChanged) { core.setFailed( `${commit} is not the exact github-actions Formula-only commit for ${version}`, ); } else { core.info("Formula was already current; no Formula-only check sealing is needed."); } return; } const parent = commitResponse.data.parents[0].sha; const parentRuns = await github.paginate(github.rest.checks.listForRef, { owner, repo, ref: parent, filter: "latest", per_page: 100, }); const latestParentRunByName = new Map(); for (const run of parentRuns) { if ( run.head_sha !== parent || run.app?.slug !== "github-actions" || !requiredContexts.includes(run.name) ) { continue; } const current = latestParentRunByName.get(run.name); if (!current || run.id > current.id) { latestParentRunByName.set(run.name, run); } } const invalidParentContexts = requiredContexts.filter((name) => { const run = latestParentRunByName.get(name); return !run || run.conclusion !== "success"; }); if (invalidParentContexts.length > 0) { core.setFailed( `Formula commit parent ${parent} lacks successful Code Admission contexts: ` + invalidParentContexts.join(", "), ); return; } const remoteFormula = await github.rest.repos.getContent({ owner, repo, path: formulaPath, ref: commit, }); if ( Array.isArray(remoteFormula.data) || remoteFormula.data.type !== "file" || remoteFormula.data.encoding !== "base64" ) { core.setFailed(`${formulaPath} is not a regular base64 GitHub blob at ${commit}`); return; } const expectedFormula = fs.readFileSync(sourcePath); const actualFormula = Buffer.from( remoteFormula.data.content.replace(/\s/g, ""), "base64", ); if (!actualFormula.equals(expectedFormula)) { core.setFailed(`${formulaPath} at ${commit} differs from the verified release Formula`); return; } const branch = await github.rest.repos.getBranch({ owner, repo, branch: context.payload.repository.default_branch, }); const comparison = await github.rest.repos.compareCommitsWithBasehead({ owner, repo, basehead: `${commit}...${branch.data.commit.sha}`, }); if (!["ahead", "identical"].includes(comparison.data.status)) { core.setFailed(`${commit} is no longer contained in the default branch`); return; } for (const name of requiredContexts) { await github.rest.checks.create({ owner, repo, name, head_sha: commit, status: "completed", conclusion: "success", output: { title: "Verified Formula-only release commit", summary: `${formulaPath} was generated from immutable ${version} assets, ` + "validated byte-for-byte, and was the commit's only changed file.", }, }); } - name: Reverify exact immutable npm package run: ./scripts/release/verify-package-managers.sh --npm-only --expected-version "$RELEASE_VERSION" publish-channels: name: Publish npm and mirrors if: ${{ !cancelled() && needs.release-contract.result == 'success' && needs.publish-release.result == 'success' }} needs: [release-contract, publish-release] runs-on: ubuntu-latest timeout-minutes: 30 permissions: contents: read env: RELEASE_VERSION: ${{ needs.release-contract.outputs.release_version }} RELEASE_COMMIT: ${{ needs.release-contract.outputs.release_commit }} RELEASE_TAG_OBJECT: ${{ needs.release-contract.outputs.release_tag_object }} steps: - name: Check out sealed release source uses: actions/checkout@v4 with: ref: ${{ needs.release-contract.outputs.release_commit }} persist-credentials: false fetch-depth: 0 - name: Check out trusted release tooling uses: actions/checkout@v4 with: ref: ${{ github.sha }} path: tmp/trusted-release-tooling persist-credentials: false - name: Fetch release branch and official tags run: | git fetch --force origin \ '+refs/heads/main:refs/remotes/origin/main' git fetch --force --no-tags \ https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git \ '+refs/tags/v*:refs/tags/v*' \ '+refs/tags/withdrawn/v*:refs/tags/withdrawn/v*' - name: Verify sealed tag before executing channel source env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | "$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/verify-github-tag-authority.sh" \ "$RELEASE_VERSION" "$RELEASE_COMMIT" "$RELEASE_TAG_OBJECT" - name: Revalidate sealed contract run: | ./scripts/release/release-contract.sh \ --channel "${{ needs.release-contract.outputs.channel }}" \ --version "$RELEASE_VERSION" \ --context ci \ --remote origin \ --branch main - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: "20" registry-url: "https://registry.npmjs.org" - name: Install archive tooling run: sudo apt-get update && sudo apt-get install -y zip unzip - name: Download and verify immutable GitHub Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eu release_state="$( gh api -H 'X-GitHub-Api-Version: 2026-03-10' \ "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_VERSION" \ --jq '[.draft, .immutable] | @tsv' )" test "$release_state" = "$(printf 'false\ttrue')" || { echo "GitHub Release must be public and immutable before channel publication." >&2 exit 1 } ./scripts/release/verify-github-release-assets.sh "$RELEASE_VERSION" "$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/verify-github-tag-authority.sh" \ "$RELEASE_VERSION" "$RELEASE_COMMIT" "$RELEASE_TAG_OBJECT" mkdir -p dist gh release download "$RELEASE_VERSION" \ --dir dist \ --pattern 'dws-*' \ --pattern 'checksums.txt' \ --clobber ./scripts/release/verify-release-artifacts.sh "$RELEASE_VERSION" DWS_PACKAGE_SOURCE_ROOT="$GITHUB_WORKSPACE" \ DWS_PACKAGE_DIST_DIR="$GITHUB_WORKSPACE/dist" \ ./scripts/release/stage-npm-package.sh "$RELEASE_VERSION" - name: Verify immutable npm package without publication credentials run: | ./scripts/release/verify-package-managers.sh \ --npm-only \ --expected-version "$RELEASE_VERSION" - name: Inspect npm channel state id: npm-state if: ${{ github.repository_owner == 'DingTalk-Real-AI' }} env: RELEASE_CHANNEL: ${{ needs.release-contract.outputs.channel }} run: | set -eu . ./scripts/release/release-lib.sh semver="${RELEASE_VERSION#v}" npm_tag=latest if [ "$RELEASE_CHANNEL" = "prerelease" ]; then npm_tag=beta; fi current="$(npm view dingtalk-workspace-cli "dist-tags.$npm_tag")" local_integrity="$( ./scripts/release/pack-npm-package.sh \ "$GITHUB_WORKSPACE/dist/npm/dingtalk-workspace-cli" \ "$RUNNER_TEMP/dingtalk-workspace-cli.tgz" )" version_error="$RUNNER_TEMP/npm-version-error.log" if existing="$(npm view "dingtalk-workspace-cli@$semver" version 2>"$version_error")"; then if [ "$existing" != "$semver" ]; then echo "npm returned unexpected version $existing for $semver" >&2 exit 1 fi elif grep -Eq 'E404|404 Not Found' "$version_error"; then existing="" else cat "$version_error" >&2 exit 1 fi if [ -n "$existing" ]; then remote_integrity="$(npm view "dingtalk-workspace-cli@$semver" dist.integrity)" if [ -z "$remote_integrity" ] || [ "$remote_integrity" != "$local_integrity" ]; then echo "npm $semver integrity differs from the sealed package." >&2 echo "local: $local_integrity" >&2 echo "remote: $remote_integrity" >&2 exit 1 fi advance_channel=false if [ "$current" = "$semver" ]; then : elif [ -n "$current" ] && release_version_is_greater "v$current" "$RELEASE_VERSION"; then echo "npm $npm_tag already points to newer v$current; leaving it unchanged." elif [ -z "$current" ] || release_version_is_greater "$RELEASE_VERSION" "v$current"; then advance_channel=true else echo "Cannot reconcile npm $npm_tag=$current with $RELEASE_VERSION" >&2 exit 1 fi { echo "published=true" echo "advance_channel=$advance_channel" echo "npm_tag=$npm_tag" echo "semver=$semver" } >> "$GITHUB_OUTPUT" exit 0 fi if [ -n "$current" ] && ! release_version_is_greater "$RELEASE_VERSION" "v$current"; then echo "$RELEASE_VERSION would move npm $npm_tag backwards from v$current" >&2 exit 1 fi { echo "published=false" echo "advance_channel=false" echo "npm_tag=$npm_tag" echo "semver=$semver" } >> "$GITHUB_OUTPUT" - name: Publish missing version to npm channel if: ${{ github.repository_owner == 'DingTalk-Real-AI' && steps.npm-state.outputs.published != 'true' }} env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_TAG: ${{ steps.npm-state.outputs.npm_tag }} SEMVER: ${{ steps.npm-state.outputs.semver }} run: | set -eu . ./scripts/release/release-lib.sh current="$(npm view dingtalk-workspace-cli "dist-tags.$NPM_TAG")" if [ -n "$current" ] && [ "$current" != "$SEMVER" ] && ! release_version_is_greater "v$SEMVER" "v$current"; then echo "Refusing to move npm $NPM_TAG backwards from v$current to v$SEMVER" >&2 exit 1 fi npm publish "$RUNNER_TEMP/dingtalk-workspace-cli.tgz" \ --access public \ --tag "$NPM_TAG" \ --ignore-scripts \ --registry=https://registry.npmjs.org - name: Advance npm channel for an existing exact version if: ${{ github.repository_owner == 'DingTalk-Real-AI' && steps.npm-state.outputs.advance_channel == 'true' }} env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_TAG: ${{ steps.npm-state.outputs.npm_tag }} SEMVER: ${{ steps.npm-state.outputs.semver }} run: | set -eu . ./scripts/release/release-lib.sh current="$(npm view dingtalk-workspace-cli "dist-tags.$NPM_TAG")" if [ "$current" = "$SEMVER" ]; then exit 0; fi if [ -n "$current" ] && release_version_is_greater "v$current" "v$SEMVER"; then echo "npm $NPM_TAG advanced to newer v$current; leaving it unchanged." exit 0 fi if [ -n "$current" ] && ! release_version_is_greater "v$SEMVER" "v$current"; then echo "Refusing unsafe npm $NPM_TAG transition from v$current to v$SEMVER" >&2 exit 1 fi npm dist-tag add "dingtalk-workspace-cli@$SEMVER" "$NPM_TAG" \ --registry=https://registry.npmjs.org - name: Verify npm channel delivery if: ${{ github.repository_owner == 'DingTalk-Real-AI' }} env: NPM_TAG: ${{ steps.npm-state.outputs.npm_tag }} SEMVER: ${{ steps.npm-state.outputs.semver }} run: | set -eu . ./scripts/release/release-lib.sh attempt=1 max_attempts=60 while :; do delivered="$(npm view dingtalk-workspace-cli "dist-tags.$NPM_TAG" \ --registry=https://registry.npmjs.org --prefer-online)" if [ "$delivered" = "$SEMVER" ]; then exit 0; fi if release_version_is_greater "v$delivered" "v$SEMVER"; then echo "npm $NPM_TAG already supersedes this historical rerun at v$delivered." exit 0 fi release_version_is_greater "v$SEMVER" "v$delivered" || { echo "npm $NPM_TAG=$delivered cannot be reconciled with v$SEMVER" >&2 exit 1 } if [ "$attempt" -ge "$max_attempts" ]; then echo "npm $NPM_TAG still reports older v$delivered after $attempt attempts; expected v$SEMVER" >&2 exit 1 fi echo "npm $NPM_TAG still reports older v$delivered; waiting for registry propagation ($attempt/$max_attempts)." # incremental backoff: 5s for first 12 attempts (1 min), then 10s if [ "$attempt" -lt 12 ]; then sleep 5 else sleep 10 fi attempt=$((attempt + 1)) done - name: Sync release artifacts to China OSS mirror if: ${{ needs.release-contract.outputs.oss_mirror == 'enabled' }} run: ./scripts/release/sync-to-oss.sh env: VERSION: ${{ needs.release-contract.outputs.release_version }} DWS_RELEASE_CHANNEL: ${{ needs.release-contract.outputs.channel }} OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }} OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }} OSS_ENDPOINT: ${{ secrets.OSS_ENDPOINT }} OSS_BUCKET: ${{ secrets.OSS_BUCKET }} OSS_PREFIX: ${{ secrets.OSS_PREFIX }} DWS_REQUIRE_OSS: "1" mirror-gitee-release: name: Mirror immutable release to Gitee if: ${{ !cancelled() && vars.ENABLE_GITEE_UPLOAD_FALLBACK == 'true' && needs.release-contract.result == 'success' && needs.release.result == 'success' && needs.publish-channels.result == 'success' }} needs: [release-contract, release, publish-channels] runs-on: ubuntu-latest timeout-minutes: 360 permissions: contents: read env: RELEASE_VERSION: ${{ needs.release-contract.outputs.release_version }} RELEASE_COMMIT: ${{ needs.release-contract.outputs.release_commit }} RELEASE_TAG_OBJECT: ${{ needs.release-contract.outputs.release_tag_object }} steps: - name: Check out sealed release source uses: actions/checkout@v4 timeout-minutes: 5 with: ref: ${{ needs.release-contract.outputs.release_commit }} persist-credentials: false - name: Check out trusted release tooling uses: actions/checkout@v4 timeout-minutes: 5 with: ref: ${{ github.sha }} path: tmp/trusted-release-tooling persist-credentials: false - name: Fetch and verify sealed release tag timeout-minutes: 5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | git fetch --force --no-tags \ https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git \ '+refs/tags/v*:refs/tags/v*' \ '+refs/tags/withdrawn/v*:refs/tags/withdrawn/v*' "$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/verify-github-tag-authority.sh" \ "$RELEASE_VERSION" "$RELEASE_COMMIT" "$RELEASE_TAG_OBJECT" - name: Restore finalized distribution files uses: actions/download-artifact@v4 timeout-minutes: 10 with: name: finalized-release-dist path: dist - name: Mirror release to Gitee (China) timeout-minutes: 318 run: ./scripts/release/sync-to-gitee.sh env: VERSION: ${{ needs.release-contract.outputs.release_version }} GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }} GITEE_USER: ${{ secrets.GITEE_USER }} GITEE_REPO: ${{ secrets.GITEE_REPO }} DWS_REQUIRE_GITEE: "1" repair-npm: needs: dispatch-contract if: ${{ needs.dispatch-contract.outputs.mode == 'repair_npm' && github.ref_name == github.event.repository.default_branch && github.repository_owner == 'DingTalk-Real-AI' }} runs-on: ubuntu-latest timeout-minutes: 20 permissions: actions: read contents: read steps: - name: Check out trusted release tooling uses: actions/checkout@v4 with: ref: ${{ github.sha }} path: tooling persist-credentials: false - name: Validate repair version id: version working-directory: tooling env: VERSION: ${{ inputs.repair_npm_version }} run: | set -eu . ./scripts/release/release-lib.sh if ! release_is_stable_version "$VERSION" && ! release_is_prerelease_version "$VERSION"; then echo "Invalid release version: $VERSION" >&2 exit 2 fi echo "semver=${VERSION#v}" >> "$GITHUB_OUTPUT" - name: Verify immutable release authority id: authority uses: actions/github-script@v7 env: VERSION: ${{ inputs.repair_npm_version }} with: script: | const { owner, repo } = context.repo; const version = process.env.VERSION; try { await github.rest.git.getRef({ owner, repo, ref: `tags/withdrawn/${version}`, }); core.setFailed(`${version} was withdrawn and cannot be repaired`); return; } catch (error) { if (error.status !== 404) throw error; } const ref = await github.rest.git.getRef({ owner, repo, ref: `tags/${version}`, }); if (ref.data.object.type !== "tag") { core.setFailed(`${version} is not an annotated tag`); return; } const tag = await github.rest.git.getTag({ owner, repo, tag_sha: ref.data.object.sha, }); if (tag.data.tag !== version || tag.data.object.type !== "commit") { core.setFailed(`${version} does not resolve to one annotated commit tag`); return; } const commitSha = tag.data.object.sha; const defaultBranch = context.payload.repository.default_branch; const branch = await github.rest.repos.getBranch({ owner, repo, branch: defaultBranch, }); const comparison = await github.rest.repos.compareCommitsWithBasehead({ owner, repo, basehead: `${commitSha}...${branch.data.commit.sha}`, }); if (!["ahead", "identical"].includes(comparison.data.status)) { core.setFailed(`${version} is not contained in ${defaultBranch}`); return; } const release = await github.rest.repos.getReleaseByTag({ owner, repo, tag: version, }); if ( release.data.tag_name !== version || release.data.draft || !release.data.immutable ) { core.setFailed(`${version} is not a public immutable GitHub Release`); return; } const expectedAssets = new Set([ "dws-darwin-amd64.tar.gz", "dws-darwin-arm64.tar.gz", "dws-linux-amd64.tar.gz", "dws-linux-arm64.tar.gz", "dws-windows-amd64.zip", "dws-windows-arm64.zip", "dws-skills.zip", "checksums.txt", ]); const assetNames = release.data.assets.map((asset) => asset.name); if ( assetNames.length !== expectedAssets.size || assetNames.some((name) => !expectedAssets.has(name)) || new Set(assetNames).size !== expectedAssets.size ) { core.setFailed(`${version} does not contain the exact release asset set`); return; } core.setOutput("commit_sha", commitSha); - name: Require successful Release workflow delivery working-directory: tooling env: VERSION: ${{ inputs.repair_npm_version }} RELEASE_COMMIT: ${{ steps.authority.outputs.commit_sha }} DWS_RELEASE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | ./scripts/release/verify-release-workflow-delivery.sh --npm-repair \ "$VERSION" "$RELEASE_COMMIT" - name: Check out target release source uses: actions/checkout@v4 with: ref: ${{ steps.authority.outputs.commit_sha }} path: release-source persist-credentials: false - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: "20" registry-url: "https://registry.npmjs.org" - name: Download and verify GitHub release assets env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION: ${{ inputs.repair_npm_version }} run: | set -eu mkdir -p dist gh release download "$VERSION" \ --repo "$GITHUB_REPOSITORY" \ --dir dist \ --pattern 'dws-*' \ --pattern 'checksums.txt' \ --clobber DWS_PACKAGE_DIST_DIR="$GITHUB_WORKSPACE/dist" \ "$GITHUB_WORKSPACE/tooling/scripts/release/verify-release-artifacts.sh" "$VERSION" - name: Stage and verify target npm package env: VERSION: ${{ inputs.repair_npm_version }} run: | set -eu DWS_PACKAGE_SOURCE_ROOT="$GITHUB_WORKSPACE/release-source" \ DWS_PACKAGE_DIST_DIR="$GITHUB_WORKSPACE/dist" \ "$GITHUB_WORKSPACE/tooling/scripts/release/stage-npm-package.sh" "$VERSION" DWS_PACKAGE_DIST_DIR="$GITHUB_WORKSPACE/dist" \ "$GITHUB_WORKSPACE/tooling/scripts/release/verify-package-managers.sh" \ --npm-only \ --expected-version "$VERSION" \ --skip-skill-targets - name: Inspect npm version id: npm env: SEMVER: ${{ steps.version.outputs.semver }} run: | set -eu local_integrity="$( "$GITHUB_WORKSPACE/tooling/scripts/release/pack-npm-package.sh" \ "$GITHUB_WORKSPACE/dist/npm/dingtalk-workspace-cli" \ "$RUNNER_TEMP/dingtalk-workspace-cli-repair.tgz" )" version_error="$RUNNER_TEMP/npm-version-error.log" if existing="$(npm view "dingtalk-workspace-cli@$SEMVER" version 2>"$version_error")"; then if [ "$existing" != "$SEMVER" ]; then echo "npm returned unexpected version $existing for $SEMVER" >&2 exit 1 fi elif grep -Eq 'E404|404 Not Found' "$version_error"; then existing="" else cat "$version_error" >&2 exit 1 fi if [ "$existing" = "$SEMVER" ]; then remote_integrity="$(npm view "dingtalk-workspace-cli@$SEMVER" dist.integrity)" if [ -z "$remote_integrity" ] || [ "$remote_integrity" != "$local_integrity" ]; then echo "npm $SEMVER integrity differs from the immutable release package." >&2 echo "local: $local_integrity" >&2 echo "remote: $remote_integrity" >&2 exit 1 fi echo "published=true" >> "$GITHUB_OUTPUT" else echo "published=false" >> "$GITHUB_OUTPUT" fi - name: Backfill exact version to npm if: ${{ steps.npm.outputs.published != 'true' }} run: npm publish "$RUNNER_TEMP/dingtalk-workspace-cli-repair.tgz" --access public --tag backfill --ignore-scripts --registry=https://registry.npmjs.org env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} release-delivery-gate: name: Release delivery gate if: ${{ !cancelled() }} needs: - dispatch-contract - governance-preflight - release-contract - release-validation - release - verify-darwin-signatures - publish-release - publish-channels - mirror-gitee-release - repair-npm - repair-channel - release-plan - seal-release runs-on: ubuntu-latest timeout-minutes: 2 permissions: {} steps: - name: Require the selected release mode to finish env: EVENT_NAME: ${{ github.event_name }} DISPATCH_MODE: ${{ needs.dispatch-contract.outputs.mode }} GITEE_FALLBACK_ENABLED: ${{ vars.ENABLE_GITEE_UPLOAD_FALLBACK }} DISPATCH_RESULT: ${{ needs.dispatch-contract.result }} GOVERNANCE_PREFLIGHT_RESULT: ${{ needs.governance-preflight.result }} RELEASE_CONTRACT_RESULT: ${{ needs.release-contract.result }} RELEASE_VALIDATION_RESULT: ${{ needs.release-validation.result }} RELEASE_RESULT: ${{ needs.release.result }} DARWIN_SIGNATURE_RESULT: ${{ needs.verify-darwin-signatures.result }} PUBLISH_RELEASE_RESULT: ${{ needs.publish-release.result }} PUBLISH_CHANNELS_RESULT: ${{ needs.publish-channels.result }} MIRROR_GITEE_RESULT: ${{ needs.mirror-gitee-release.result }} REPAIR_NPM_RESULT: ${{ needs.repair-npm.result }} REPAIR_CHANNEL_RESULT: ${{ needs.repair-channel.result }} RELEASE_PLAN_RESULT: ${{ needs.release-plan.result }} SEAL_RELEASE_RESULT: ${{ needs.seal-release.result }} run: | set -eu require_result() { job="$1" actual="$2" expected="$3" test "$actual" = "$expected" || { echo "$job result is $actual, expected $expected" >&2 exit 1 } } require_publication() { require_result release-contract "$RELEASE_CONTRACT_RESULT" success require_result release-validation "$RELEASE_VALIDATION_RESULT" success require_result release "$RELEASE_RESULT" success require_result verify-darwin-signatures "$DARWIN_SIGNATURE_RESULT" success require_result publish-release "$PUBLISH_RELEASE_RESULT" success require_result publish-channels "$PUBLISH_CHANNELS_RESULT" success if test "$GITEE_FALLBACK_ENABLED" = true; then require_result mirror-gitee-release "$MIRROR_GITEE_RESULT" success else require_result mirror-gitee-release "$MIRROR_GITEE_RESULT" skipped fi } require_cloud_jobs_skipped() { require_result release-plan "$RELEASE_PLAN_RESULT" skipped require_result seal-release "$SEAL_RELEASE_RESULT" skipped } case "$EVENT_NAME:$DISPATCH_MODE" in push:) require_result dispatch-contract "$DISPATCH_RESULT" skipped require_result governance-preflight "$GOVERNANCE_PREFLIGHT_RESULT" skipped require_result repair-npm "$REPAIR_NPM_RESULT" skipped require_result repair-channel "$REPAIR_CHANNEL_RESULT" skipped require_cloud_jobs_skipped require_publication ;; workflow_dispatch:create_release) require_result dispatch-contract "$DISPATCH_RESULT" success require_result governance-preflight "$GOVERNANCE_PREFLIGHT_RESULT" success require_result release-plan "$RELEASE_PLAN_RESULT" success require_result seal-release "$SEAL_RELEASE_RESULT" success require_result repair-npm "$REPAIR_NPM_RESULT" skipped require_result repair-channel "$REPAIR_CHANNEL_RESULT" skipped require_publication ;; workflow_dispatch:plan_release) require_result dispatch-contract "$DISPATCH_RESULT" success require_result release-plan "$RELEASE_PLAN_RESULT" success require_result seal-release "$SEAL_RELEASE_RESULT" skipped require_result governance-preflight "$GOVERNANCE_PREFLIGHT_RESULT" skipped require_result release-contract "$RELEASE_CONTRACT_RESULT" skipped require_result release-validation "$RELEASE_VALIDATION_RESULT" skipped require_result release "$RELEASE_RESULT" skipped require_result verify-darwin-signatures "$DARWIN_SIGNATURE_RESULT" skipped require_result publish-release "$PUBLISH_RELEASE_RESULT" skipped require_result publish-channels "$PUBLISH_CHANNELS_RESULT" skipped require_result mirror-gitee-release "$MIRROR_GITEE_RESULT" skipped require_result repair-npm "$REPAIR_NPM_RESULT" skipped require_result repair-channel "$REPAIR_CHANNEL_RESULT" skipped ;; workflow_dispatch:recover_release) require_result dispatch-contract "$DISPATCH_RESULT" success require_result governance-preflight "$GOVERNANCE_PREFLIGHT_RESULT" skipped require_result repair-npm "$REPAIR_NPM_RESULT" skipped require_result repair-channel "$REPAIR_CHANNEL_RESULT" skipped require_cloud_jobs_skipped require_publication ;; workflow_dispatch:governance_preflight) require_result dispatch-contract "$DISPATCH_RESULT" success require_result governance-preflight "$GOVERNANCE_PREFLIGHT_RESULT" success require_result release-contract "$RELEASE_CONTRACT_RESULT" skipped require_result release-validation "$RELEASE_VALIDATION_RESULT" skipped require_result release "$RELEASE_RESULT" skipped require_result verify-darwin-signatures "$DARWIN_SIGNATURE_RESULT" skipped require_result publish-release "$PUBLISH_RELEASE_RESULT" skipped require_result publish-channels "$PUBLISH_CHANNELS_RESULT" skipped require_result mirror-gitee-release "$MIRROR_GITEE_RESULT" skipped require_result repair-npm "$REPAIR_NPM_RESULT" skipped require_result repair-channel "$REPAIR_CHANNEL_RESULT" skipped require_cloud_jobs_skipped ;; workflow_dispatch:repair_npm) require_result dispatch-contract "$DISPATCH_RESULT" success require_result repair-npm "$REPAIR_NPM_RESULT" success require_result governance-preflight "$GOVERNANCE_PREFLIGHT_RESULT" skipped require_result release-contract "$RELEASE_CONTRACT_RESULT" skipped require_result release-validation "$RELEASE_VALIDATION_RESULT" skipped require_result release "$RELEASE_RESULT" skipped require_result verify-darwin-signatures "$DARWIN_SIGNATURE_RESULT" skipped require_result publish-release "$PUBLISH_RELEASE_RESULT" skipped require_result publish-channels "$PUBLISH_CHANNELS_RESULT" skipped require_result mirror-gitee-release "$MIRROR_GITEE_RESULT" skipped require_result repair-channel "$REPAIR_CHANNEL_RESULT" skipped require_cloud_jobs_skipped ;; workflow_dispatch:repair_gitee) require_result dispatch-contract "$DISPATCH_RESULT" success require_result repair-channel "$REPAIR_CHANNEL_RESULT" success require_result governance-preflight "$GOVERNANCE_PREFLIGHT_RESULT" skipped require_result release-contract "$RELEASE_CONTRACT_RESULT" skipped require_result release-validation "$RELEASE_VALIDATION_RESULT" skipped require_result release "$RELEASE_RESULT" skipped require_result verify-darwin-signatures "$DARWIN_SIGNATURE_RESULT" skipped require_result publish-release "$PUBLISH_RELEASE_RESULT" skipped require_result publish-channels "$PUBLISH_CHANNELS_RESULT" skipped require_result mirror-gitee-release "$MIRROR_GITEE_RESULT" skipped require_result repair-npm "$REPAIR_NPM_RESULT" skipped require_cloud_jobs_skipped ;; workflow_dispatch:repair_oss) require_result dispatch-contract "$DISPATCH_RESULT" success require_result repair-channel "$REPAIR_CHANNEL_RESULT" success require_result governance-preflight "$GOVERNANCE_PREFLIGHT_RESULT" skipped require_result release-contract "$RELEASE_CONTRACT_RESULT" skipped require_result release-validation "$RELEASE_VALIDATION_RESULT" skipped require_result release "$RELEASE_RESULT" skipped require_result verify-darwin-signatures "$DARWIN_SIGNATURE_RESULT" skipped require_result publish-release "$PUBLISH_RELEASE_RESULT" skipped require_result publish-channels "$PUBLISH_CHANNELS_RESULT" skipped require_result mirror-gitee-release "$MIRROR_GITEE_RESULT" skipped require_result repair-npm "$REPAIR_NPM_RESULT" skipped require_cloud_jobs_skipped ;; *) echo "unsupported release mode: event=$EVENT_NAME mode=$DISPATCH_MODE" >&2 exit 1 ;; esac # Repair one downstream channel from an existing public immutable release on # demand. The push-triggered channel jobs depend on the original run's # finalized dist artifact, so a tag that was already published cannot reuse # it. This dispatch path re-derives the asset set from the immutable GitHub # Release itself. The release tag, delivery provenance, and asset checksums # remain the sealed authority boundary. repair-channel: name: Repair an existing immutable release channel needs: dispatch-contract if: ${{ !cancelled() && needs.dispatch-contract.result == 'success' && (needs.dispatch-contract.outputs.mode == 'repair_gitee' || needs.dispatch-contract.outputs.mode == 'repair_oss') && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.repository == 'DingTalk-Real-AI/dingtalk-workspace-cli' }} runs-on: ubuntu-latest timeout-minutes: 360 permissions: actions: read contents: read steps: - name: Check out trusted release tooling uses: actions/checkout@v4 timeout-minutes: 5 with: ref: ${{ github.sha }} path: tooling persist-credentials: false fetch-depth: 0 - name: Validate repair version timeout-minutes: 2 working-directory: tooling env: VERSION: ${{ inputs.repair_gitee_version || inputs.repair_oss_version }} run: | set -eu . ./scripts/release/release-lib.sh if ! release_is_stable_version "$VERSION" && ! release_is_prerelease_version "$VERSION"; then echo "Invalid release version: $VERSION" >&2 exit 2 fi - name: Verify immutable release authority id: authority uses: actions/github-script@v7 timeout-minutes: 5 env: VERSION: ${{ inputs.repair_gitee_version || inputs.repair_oss_version }} with: script: | const { owner, repo } = context.repo; const version = process.env.VERSION; try { await github.rest.git.getRef({ owner, repo, ref: `tags/withdrawn/${version}`, }); core.setFailed(`${version} was withdrawn and cannot be repaired`); return; } catch (error) { if (error.status !== 404) throw error; } const ref = await github.rest.git.getRef({ owner, repo, ref: `tags/${version}`, }); if (ref.data.object.type !== "tag") { core.setFailed(`${version} is not an annotated tag`); return; } const tag = await github.rest.git.getTag({ owner, repo, tag_sha: ref.data.object.sha, }); if (tag.data.tag !== version || tag.data.object.type !== "commit") { core.setFailed(`${version} does not resolve to one annotated commit tag`); return; } const tagFields = new Map(); for (const line of tag.data.message.split(/\r?\n/)) { const separator = line.indexOf(": "); if (separator < 1) continue; const key = line.slice(0, separator); if (tagFields.has(key)) { core.setFailed(`${version} annotated tag contains duplicate ${key} metadata`); return; } tagFields.set(key, line.slice(separator + 2)); } const ossMirror = tagFields.has("OSS-Mirror") ? tagFields.get("OSS-Mirror") : "enabled"; if (!["enabled", "deferred"].includes(ossMirror)) { core.setFailed(`${version} contains invalid OSS-Mirror metadata`); return; } const commitSha = tag.data.object.sha; const defaultBranch = context.payload.repository.default_branch; const branch = await github.rest.repos.getBranch({ owner, repo, branch: defaultBranch, }); const comparison = await github.rest.repos.compareCommitsWithBasehead({ owner, repo, basehead: `${commitSha}...${branch.data.commit.sha}`, }); if (!["ahead", "identical"].includes(comparison.data.status)) { core.setFailed(`${version} is not contained in ${defaultBranch}`); return; } const release = await github.rest.repos.getReleaseByTag({ owner, repo, tag: version, }); if ( release.data.tag_name !== version || release.data.draft || !release.data.immutable ) { core.setFailed(`${version} is not a public immutable GitHub Release`); return; } const expectedPrerelease = version.includes("-beta."); if (release.data.prerelease !== expectedPrerelease) { core.setFailed(`${version} prerelease metadata does not match its version`); return; } const expectedAssets = new Set([ "dws-darwin-amd64.tar.gz", "dws-darwin-arm64.tar.gz", "dws-linux-amd64.tar.gz", "dws-linux-arm64.tar.gz", "dws-windows-amd64.zip", "dws-windows-arm64.zip", "dws-skills.zip", "checksums.txt", ]); const assetNames = release.data.assets.map((asset) => asset.name); if ( assetNames.length !== expectedAssets.size || assetNames.some((name) => !expectedAssets.has(name)) || new Set(assetNames).size !== expectedAssets.size ) { core.setFailed(`${version} does not contain the exact release asset set`); return; } core.setOutput("commit_sha", commitSha); core.setOutput("tag_object", ref.data.object.sha); core.setOutput("oss_mirror", ossMirror); - name: Require sealed OSS policy for repair if: ${{ needs.dispatch-contract.outputs.mode == 'repair_oss' }} env: OSS_MIRROR: ${{ steps.authority.outputs.oss_mirror }} run: | test "$OSS_MIRROR" = enabled || { echo "OSS repair is unavailable because this immutable release deferred the OSS channel." >&2 exit 1 } - name: Check out sealed release source uses: actions/checkout@v4 timeout-minutes: 5 with: ref: ${{ steps.authority.outputs.commit_sha }} path: release-source persist-credentials: false - name: Fetch and verify sealed release tag timeout-minutes: 5 working-directory: tooling env: VERSION: ${{ inputs.repair_gitee_version || inputs.repair_oss_version }} RELEASE_COMMIT: ${{ steps.authority.outputs.commit_sha }} RELEASE_TAG_OBJECT: ${{ steps.authority.outputs.tag_object }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eu git fetch --force --no-tags \ https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git \ '+refs/tags/v*:refs/tags/v*' \ '+refs/tags/withdrawn/v*:refs/tags/withdrawn/v*' ./scripts/release/verify-github-tag-authority.sh \ "$VERSION" "$RELEASE_COMMIT" "$RELEASE_TAG_OBJECT" - name: Require successful Release workflow delivery timeout-minutes: 5 working-directory: tooling env: VERSION: ${{ inputs.repair_gitee_version || inputs.repair_oss_version }} RELEASE_COMMIT: ${{ steps.authority.outputs.commit_sha }} REPAIR_MODE: ${{ needs.dispatch-contract.outputs.mode }} DWS_RELEASE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | case "$REPAIR_MODE" in repair_gitee) target=gitee ;; repair_oss) target=oss ;; *) echo "Unsupported channel repair mode: $REPAIR_MODE" >&2; exit 2 ;; esac ./scripts/release/verify-release-workflow-delivery.sh \ --channel-repair "$target" "$VERSION" "$RELEASE_COMMIT" - name: Download and verify immutable GitHub Release assets timeout-minutes: 10 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION: ${{ inputs.repair_gitee_version || inputs.repair_oss_version }} run: | set -eu mkdir -p dist gh release download "$VERSION" \ --repo "$GITHUB_REPOSITORY" \ --dir dist \ --pattern 'dws-*' \ --pattern 'checksums.txt' \ --clobber DWS_PACKAGE_DIST_DIR="$GITHUB_WORKSPACE/dist" \ "$GITHUB_WORKSPACE/tooling/scripts/release/verify-release-artifacts.sh" "$VERSION" - name: Mirror release to Gitee (China) if: ${{ needs.dispatch-contract.outputs.mode == 'repair_gitee' }} timeout-minutes: 318 working-directory: tooling run: | "$GITHUB_WORKSPACE/tooling/scripts/release/sync-to-gitee.sh" env: VERSION: ${{ inputs.repair_gitee_version || inputs.repair_oss_version }} DIST_DIR: ${{ github.workspace }}/dist GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }} GITEE_USER: ${{ secrets.GITEE_USER }} GITEE_REPO: ${{ secrets.GITEE_REPO }} DWS_REQUIRE_GITEE: "1" - name: Mirror release to OSS (China) if: ${{ needs.dispatch-contract.outputs.mode == 'repair_oss' }} timeout-minutes: 30 working-directory: release-source run: | "$GITHUB_WORKSPACE/tooling/scripts/release/sync-to-oss.sh" env: VERSION: ${{ inputs.repair_gitee_version || inputs.repair_oss_version }} DIST_DIR: ${{ github.workspace }}/dist OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }} OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }} OSS_ENDPOINT: ${{ secrets.OSS_ENDPOINT }} OSS_BUCKET: ${{ secrets.OSS_BUCKET }} OSS_PREFIX: ${{ secrets.OSS_PREFIX }} DWS_REQUIRE_OSS: "1" # These checks remain publication blockers, but they do not need to serialize # artifact compilation. Each matrix leg gets an isolated runner workspace so the # module-tidy check and E2E build cache cannot interfere with one another. release-validation: name: Validate sealed release (${{ matrix.check }}) if: ${{ !cancelled() && needs.release-contract.result == 'success' }} needs: [release-contract] runs-on: ubuntu-latest timeout-minutes: 15 permissions: contents: read strategy: fail-fast: false matrix: check: - automation - compatibility - e2e steps: - name: Check out sealed release source uses: actions/checkout@v4 with: ref: ${{ needs.release-contract.outputs.release_commit }} persist-credentials: false fetch-depth: 0 - name: Check out trusted release tooling uses: actions/checkout@v4 with: ref: ${{ github.sha }} path: tmp/trusted-release-tooling persist-credentials: false - name: Fetch and verify sealed release tag env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_VERSION: ${{ needs.release-contract.outputs.release_version }} RELEASE_COMMIT: ${{ needs.release-contract.outputs.release_commit }} RELEASE_TAG_OBJECT: ${{ needs.release-contract.outputs.release_tag_object }} run: | set -eu git fetch --force --no-tags \ https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git \ '+refs/tags/v*:refs/tags/v*' \ '+refs/tags/withdrawn/v*:refs/tags/withdrawn/v*' "$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/verify-github-tag-authority.sh" \ "$RELEASE_VERSION" "$RELEASE_COMMIT" "$RELEASE_TAG_OBJECT" # The sealed candidate tag is intentionally visible while its GitHub # authority is checked above. Compatibility must instead discover the # previous delivered stable tag, so hide only this verified candidate # from this isolated runner's local tag namespace. - name: Prepare delivered-stable compatibility ref view if: ${{ matrix.check == 'compatibility' }} env: RELEASE_VERSION: ${{ needs.release-contract.outputs.release_version }} RELEASE_COMMIT: ${{ needs.release-contract.outputs.release_commit }} RELEASE_TAG_OBJECT: ${{ needs.release-contract.outputs.release_tag_object }} PREVIOUS_STABLE: ${{ needs.release-contract.outputs.previous_stable }} PREVIOUS_STABLE_COMMIT: ${{ needs.release-contract.outputs.previous_stable_commit }} run: | set -eu test -n "$RELEASE_VERSION" test -n "$RELEASE_COMMIT" test -n "$RELEASE_TAG_OBJECT" test -n "$PREVIOUS_STABLE" test -n "$PREVIOUS_STABLE_COMMIT" test "$RELEASE_VERSION" != "$PREVIOUS_STABLE" test "$(git rev-parse HEAD)" = "$RELEASE_COMMIT" test "$(git rev-parse --verify "refs/tags/${RELEASE_VERSION}")" = "$RELEASE_TAG_OBJECT" test "$(git rev-parse --verify "refs/tags/${RELEASE_VERSION}^{commit}")" = "$RELEASE_COMMIT" test "$(git rev-parse --verify "${PREVIOUS_STABLE}^{commit}")" = "$PREVIOUS_STABLE_COMMIT" git update-ref -d "refs/tags/${RELEASE_VERSION}" "$RELEASE_TAG_OBJECT" if git show-ref --verify --quiet "refs/tags/${RELEASE_VERSION}"; then echo "sealed candidate tag is still visible to compatibility baseline discovery" >&2 exit 2 fi test "$(git rev-parse HEAD)" = "$RELEASE_COMMIT" test "$(git rev-parse --verify "${PREVIOUS_STABLE}^{commit}")" = "$PREVIOUS_STABLE_COMMIT" - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Run isolated release validation env: VALIDATION_CHECK: ${{ matrix.check }} PREVIOUS_STABLE: ${{ needs.release-contract.outputs.previous_stable }} run: | set -eu case "$VALIDATION_CHECK" in automation) go test -v -count=1 -timeout=5m ./test/scripts/... -run '^TestRelease' go mod tidy git diff --exit-code -- go.mod go.sum ;; compatibility) test -n "$PREVIOUS_STABLE" ./scripts/policy/check-command-compatibility.sh \ --base-ref HEAD \ --stable-ref "$PREVIOUS_STABLE" ;; e2e) bash scripts/dev/test-multi-profile-e2e.sh ;; *) echo "unsupported release validation check: $VALIDATION_CHECK" >&2 exit 2 ;; esac release-plan: name: Plan next cloud release needs: [dispatch-contract] if: ${{ !cancelled() && needs.dispatch-contract.result == 'success' && (needs.dispatch-contract.outputs.mode == 'plan_release' || needs.dispatch-contract.outputs.mode == 'create_release') }} runs-on: ubuntu-latest timeout-minutes: 20 permissions: actions: read contents: read outputs: release_version: ${{ steps.allocate.outputs.release_version }} release_commit: ${{ steps.allocate.outputs.release_commit }} channel: ${{ steps.allocate.outputs.channel }} from_beta: ${{ steps.allocate.outputs.from_beta }} base: ${{ steps.allocate.outputs.base }} refs_fingerprint: ${{ steps.allocate.outputs.refs_fingerprint }} oss_mirror: ${{ steps.allocate.outputs.oss_mirror }} previous_stable: ${{ steps.candidate.outputs.previous_stable }} previous_stable_commit: ${{ steps.candidate.outputs.previous_stable_commit }} from_beta_commit: ${{ steps.candidate.outputs.from_beta_commit }} steps: - name: Validate official default-branch release request env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} EXPECTED_REPOSITORY: DingTalk-Real-AI/dingtalk-workspace-cli run: | set -eu test "$GITHUB_REPOSITORY" = "$EXPECTED_REPOSITORY" || { echo "cloud releases are restricted to $EXPECTED_REPOSITORY" >&2 exit 1 } test "$GITHUB_REF" = "refs/heads/$DEFAULT_BRANCH" || { echo "cloud releases must be dispatched from $DEFAULT_BRANCH" >&2 exit 1 } printf '%s\n' "$GITHUB_SHA" | grep -Eq '^[0-9a-f]{40}$' || { echo "cloud release commit must be a full lowercase SHA" >&2 exit 1 } - name: Check out exact default-branch commit uses: actions/checkout@v4 with: ref: ${{ github.sha }} persist-credentials: false fetch-depth: 0 - name: Fetch current main and all allocated versions run: | set -eu git fetch --force origin \ '+refs/heads/main:refs/remotes/origin/main' git fetch --force --no-tags \ https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git \ '+refs/tags/v*:refs/tags/v*' \ '+refs/tags/withdrawn/v*:refs/tags/withdrawn/v*' test "$(git rev-parse HEAD)" = "$GITHUB_SHA" || { echo "checked-out release commit differs from the dispatched commit" >&2 exit 1 } test "$(git rev-parse refs/remotes/origin/main)" = "$GITHUB_SHA" || { echo "main moved after this release was dispatched; run it again from current main" >&2 exit 1 } - name: Allocate the next version id: allocate env: REQUESTED_CHANNEL: ${{ inputs.release_channel }} REQUESTED_BUMP: ${{ inputs.release_bump }} OSS_MIRROR: ${{ vars.ENABLE_OSS_MIRROR == 'true' && 'enabled' || 'deferred' }} run: | set -eu allocation="$RUNNER_TEMP/next-release-version" ./scripts/release/next-release-version.sh \ --channel "$REQUESTED_CHANNEL" \ --bump "$REQUESTED_BUMP" > "$allocation" for key in release_version from_beta channel base; do count="$(grep -c "^$key=" "$allocation")" test "$count" -eq 1 || { echo "allocator returned $count values for $key" >&2 exit 1 } value="$(sed -n "s/^$key=//p" "$allocation")" printf '%s=%s\n' "$key" "$value" >> "$GITHUB_OUTPUT" done refs_unsorted="$RUNNER_TEMP/release-refs.unsorted" refs_manifest="$RUNNER_TEMP/release-refs" git for-each-ref --format='%(refname)=%(objectname)' \ 'refs/tags/v*' 'refs/tags/withdrawn/v*' > "$refs_unsorted" LC_ALL=C sort "$refs_unsorted" > "$refs_manifest" test -s "$refs_manifest" || { echo "release ref manifest is empty after fetching allocated tags" >&2 exit 1 } refs_fingerprint="$(sha256sum "$refs_manifest" | awk '{print $1}')" echo "release_commit=$GITHUB_SHA" >> "$GITHUB_OUTPUT" echo "refs_fingerprint=$refs_fingerprint" >> "$GITHUB_OUTPUT" case "$OSS_MIRROR" in enabled|deferred) ;; *) exit 1 ;; esac echo "oss_mirror=$OSS_MIRROR" >> "$GITHUB_OUTPUT" release_version="$(sed -n 's/^release_version=//p' "$allocation")" from_beta="$(sed -n 's/^from_beta=//p' "$allocation")" channel="$(sed -n 's/^channel=//p' "$allocation")" { echo "## Cloud release plan" echo echo "- Version: \`$release_version\`" echo "- Channel: \`$channel\`" echo "- Commit: \`$GITHUB_SHA\`" echo "- OSS mirror: \`$OSS_MIRROR\`" if test -n "$from_beta"; then echo "- Promotes beta: \`$from_beta\`" fi if test "${{ needs.dispatch-contract.outputs.mode }}" = plan_release; then echo echo "Plan only: no tag or package was created. Render pending \`.changes/*.md\` fragments into the exact \`CHANGELOG.md\` section, merge the release-seal PR to main, then run publish." fi } >> "$GITHUB_STEP_SUMMARY" - name: Validate the candidate release contract before sealing id: candidate if: ${{ needs.dispatch-contract.outputs.mode == 'create_release' }} env: RELEASE_VERSION: ${{ steps.allocate.outputs.release_version }} RELEASE_CHANNEL: ${{ steps.allocate.outputs.channel }} FROM_BETA: ${{ steps.allocate.outputs.from_beta }} OSS_MIRROR: ${{ steps.allocate.outputs.oss_mirror }} run: | set -eu git config user.name "dws release cloud seal" git config user.email "dws-release@users.noreply.github.com" { printf 'Release %s\n\n' "$RELEASE_VERSION" printf 'Channel: %s\n' "$RELEASE_CHANNEL" test -z "$FROM_BETA" || printf 'From-Beta: %s\n' "$FROM_BETA" printf 'OSS-Mirror: %s\n' "$OSS_MIRROR" } > "$RUNNER_TEMP/candidate-tag-message" git tag -a "$RELEASE_VERSION" HEAD -F "$RUNNER_TEMP/candidate-tag-message" set -- \ --channel "$RELEASE_CHANNEL" \ --version "$RELEASE_VERSION" \ --context ci \ --remote origin \ --branch main \ --notes-output "$RUNNER_TEMP/release-notes.md" \ --metadata-output "$RUNNER_TEMP/release-contract-output" if test -n "$FROM_BETA"; then set -- "$@" --from-beta "$FROM_BETA" fi ./scripts/release/release-contract.sh "$@" for key in previous_stable previous_stable_commit from_beta_commit; do count="$(grep -c "^$key=" "$RUNNER_TEMP/release-contract-output")" test "$count" -eq 1 || { echo "release contract returned $count values for $key" >&2 exit 1 } value="$(sed -n "s/^$key=//p" "$RUNNER_TEMP/release-contract-output")" printf '%s=%s\n' "$key" "$value" >> "$GITHUB_OUTPUT" done - name: Require delivered previous stable baseline before sealing if: ${{ needs.dispatch-contract.outputs.mode == 'create_release' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} DWS_RELEASE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eu previous_stable="$(sed -n 's/^previous_stable=//p' "$RUNNER_TEMP/release-contract-output")" previous_commit="$(sed -n 's/^previous_stable_commit=//p' "$RUNNER_TEMP/release-contract-output")" if test -n "$previous_stable"; then ./scripts/release/verify-delivered-stable.sh "$previous_stable" "$previous_commit" fi - name: Require delivered beta before sealing stable if: ${{ needs.dispatch-contract.outputs.mode == 'create_release' && steps.allocate.outputs.channel == 'stable' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} DWS_RELEASE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} FROM_BETA: ${{ steps.allocate.outputs.from_beta }} run: | set -eu from_beta_commit="$(git rev-parse "$FROM_BETA^{commit}")" release_state="$( gh api -H 'Accept: application/vnd.github+json' \ "repos/$GITHUB_REPOSITORY/releases/tags/$FROM_BETA" \ --jq '[.tag_name, .draft, .prerelease, .immutable] | @tsv' )" test "$release_state" = "$(printf '%s\tfalse\ttrue\ttrue' "$FROM_BETA")" || { echo "$FROM_BETA is not a published immutable prerelease" >&2 exit 1 } ./scripts/release/verify-github-release-assets.sh "$FROM_BETA" ./scripts/release/verify-release-workflow-delivery.sh \ "$FROM_BETA" "$from_beta_commit" seal-release: name: Seal cloud release tag needs: [dispatch-contract, governance-preflight, release-plan] if: ${{ !cancelled() && needs.dispatch-contract.result == 'success' && needs.dispatch-contract.outputs.mode == 'create_release' && needs.governance-preflight.result == 'success' && needs.release-plan.result == 'success' }} environment: name: ${{ inputs.release_channel == 'stable' && 'release-stable' || 'release-beta' }} runs-on: ubuntu-latest timeout-minutes: 5 permissions: actions: read contents: write outputs: release_version: ${{ steps.seal.outputs.release_version }} release_commit: ${{ steps.seal.outputs.release_commit }} release_tag_object: ${{ steps.seal.outputs.release_tag_object }} steps: - name: Verify release environment governance before sealing uses: actions/github-script@v7 env: RELEASE_ENVIRONMENT: ${{ inputs.release_channel == 'stable' && 'release-stable' || 'release-beta' }} with: script: | const { owner, repo } = context.repo; const environmentName = process.env.RELEASE_ENVIRONMENT; const expectedRepository = "DingTalk-Real-AI/dingtalk-workspace-cli"; if ( context.payload.repository.full_name !== expectedRepository || !["release-beta", "release-stable"].includes(environmentName) ) { core.setFailed("release environment validation is not bound to an official channel"); return; } const response = await github.request( "GET /repos/{owner}/{repo}/environments/{environment_name}", { owner, repo, environment_name: environmentName }, ); const environment = response.data; const branchPolicy = environment.deployment_branch_policy; const protectionRules = environment.protection_rules || []; const reviewerRules = protectionRules.filter( (rule) => rule.type === "required_reviewers", ); const waitRules = protectionRules.filter( (rule) => rule.type === "wait_timer", ); if ( environment.name !== environmentName || environment.can_admins_bypass !== false || branchPolicy?.protected_branches !== true || branchPolicy?.custom_branch_policies !== false || waitRules.length !== 0 ) { core.setFailed(`${environmentName} does not match the required release policy`); return; } if (environmentName === "release-beta") { if (reviewerRules.length !== 0) { core.setFailed("release-beta must not require a manual reviewer"); } return; } if ( reviewerRules.length !== 1 || reviewerRules[0].prevent_self_review !== true || !Array.isArray(reviewerRules[0].reviewers) || reviewerRules[0].reviewers.length === 0 ) { core.setFailed( "release-stable requires a non-self reviewing administrator", ); return; } for (const reviewer of reviewerRules[0].reviewers) { if ( reviewer.type !== "User" || !reviewer.reviewer?.login ) { core.setFailed("release-stable reviewers must be repository administrators"); return; } const permission = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username: reviewer.reviewer.login, }); if (permission.data.permission !== "admin") { core.setFailed( `${reviewer.reviewer.login} is no longer a repository administrator`, ); return; } } - name: Create one immutable annotated release tag id: seal uses: actions/github-script@v7 env: RELEASE_VERSION: ${{ needs.release-plan.outputs.release_version }} RELEASE_COMMIT: ${{ needs.release-plan.outputs.release_commit }} RELEASE_CHANNEL: ${{ needs.release-plan.outputs.channel }} FROM_BETA: ${{ needs.release-plan.outputs.from_beta }} REFS_FINGERPRINT: ${{ needs.release-plan.outputs.refs_fingerprint }} OSS_MIRROR: ${{ needs.release-plan.outputs.oss_mirror }} with: script: | const crypto = require("crypto"); const { owner, repo } = context.repo; const expectedRepository = "DingTalk-Real-AI/dingtalk-workspace-cli"; const defaultBranch = context.payload.repository.default_branch; const version = process.env.RELEASE_VERSION; const commit = process.env.RELEASE_COMMIT; const channel = process.env.RELEASE_CHANNEL; const fromBeta = process.env.FROM_BETA; const expectedFingerprint = process.env.REFS_FINGERPRINT; const ossMirror = process.env.OSS_MIRROR; if ( context.payload.repository.full_name !== expectedRepository || context.ref !== `refs/heads/${defaultBranch}` || context.sha !== commit ) { core.setFailed("cloud release seal is not bound to the official dispatched default-branch commit"); return; } if ( !/^v(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-beta\.[1-9]\d*)?$/.test(version) || !/^[0-9a-f]{40}$/.test(commit) || !/^[0-9a-f]{64}$/.test(expectedFingerprint) || !["enabled", "deferred"].includes(ossMirror) ) { core.setFailed("cloud release plan contains an invalid sealed identity"); return; } if ( (channel === "stable" && !fromBeta) || (channel === "prerelease" && fromBeta) ) { core.setFailed("cloud release channel and beta promotion baseline disagree"); return; } const branch = await github.rest.repos.getBranch({ owner, repo, branch: defaultBranch, }); const currentBranchCommit = branch.data.commit.sha; const branchMoved = branch.data.commit.sha !== commit; const currentAttempt = Number(process.env.GITHUB_RUN_ATTEMPT); const requestedById = String(context.payload.sender?.id || ""); if (!Number.isSafeInteger(currentAttempt) || currentAttempt < 1 || !/^[1-9]\d*$/.test(requestedById)) { core.setFailed("cloud release request contains an invalid run attempt or requester identity"); return; } const messageForAttempt = (attempt) => [ `Release ${version}`, "", `Channel: ${channel}`, ...(fromBeta ? [`From-Beta: ${fromBeta}`] : []), `OSS-Mirror: ${ossMirror}`, `Release-Run: ${context.runId}`, `Release-Run-Attempt: ${attempt}`, `Requested-By: ${context.actor}`, `Requested-By-ID: ${context.payload.sender?.id || ""}`, `Sealed-Commit: ${commit}`, `Workflow-Commit: ${context.sha}`, `Allocation-Fingerprint: ${expectedFingerprint}`, ].join("\n"); const retryablePostWriteError = (error) => error.status === 404 || error.status === 429 || ( Number.isInteger(error.status) && error.status >= 500 && error.status < 600 ); const postWriteRetryDelaysMs = [0, 1000, 2000, 4000, 8000, 16000]; const sleep = (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)); const readAfterWrite = async (description, operation) => { let lastError; for (const delayMs of postWriteRetryDelaysMs) { if (delayMs > 0) { core.info(`${description} is not visible yet; retrying in ${delayMs}ms`); await sleep(delayMs); } try { return await operation(); } catch (error) { lastError = error; if (!retryablePostWriteError(error)) { throw error; } } } throw lastError; }; const verifyExactTag = async (refResponse, expectedTagObject = "") => { if (refResponse.data.object.type !== "tag") { throw new Error(`${version} does not point to an annotated tag object`); } const tagObject = refResponse.data.object.sha; if (expectedTagObject && tagObject !== expectedTagObject) { throw new Error( `${version} points to ${tagObject}, expected new tag object ${expectedTagObject}`, ); } const tagResponse = await readAfterWrite( `${version} annotated tag object`, () => github.rest.git.getTag({ owner, repo, tag_sha: tagObject }), ); const attemptMatches = [ ...tagResponse.data.message.matchAll(/^Release-Run-Attempt: ([1-9]\d*)$/gm), ]; if (attemptMatches.length !== 1) { throw new Error(`${version} has invalid Release-Run-Attempt metadata`); } const sealedAttempt = Number(attemptMatches[0][1]); if ( !Number.isSafeInteger(sealedAttempt) || sealedAttempt < 1 || sealedAttempt > currentAttempt || tagResponse.data.tag !== version || tagResponse.data.object.type !== "commit" || tagResponse.data.object.sha !== commit || tagResponse.data.message !== messageForAttempt(sealedAttempt) ) { throw new Error( `${version} existing annotated tag is not the exact seal from this workflow run`, ); } return tagObject; }; const refs = []; for (const prefix of ["tags/v", "tags/withdrawn/v"]) { const matching = await github.paginate(github.rest.git.listMatchingRefs, { owner, repo, ref: prefix, per_page: 100, }); for (const ref of matching) { // A failed-jobs rerun keeps the successful release-plan output. // Ignore only this run's candidate ref while recomputing that // pre-write fingerprint; verify its complete authority below. if (ref.ref === `refs/tags/${version}`) { continue; } refs.push(`${ref.ref}=${ref.object.sha}`); } } refs.sort(); const actualFingerprint = crypto .createHash("sha256") .update(refs.length > 0 ? `${refs.join("\n")}\n` : "") .digest("hex"); if (actualFingerprint !== expectedFingerprint) { core.setFailed("allocated release refs changed after planning; run the release again"); return; } let existingRef = null; try { existingRef = await github.rest.git.getRef({ owner, repo, ref: `tags/${version}`, }); } catch (error) { if (error.status !== 404) throw error; } try { await github.rest.git.getRef({ owner, repo, ref: `tags/withdrawn/${version}`, }); core.setFailed(`release version was withdrawn and cannot be reused: ${version}`); return; } catch (error) { if (error.status !== 404) throw error; } if (branchMoved) { if (!existingRef) { core.setFailed( `${defaultBranch} moved to ${currentBranchCommit} before the tag was created; run the release again`, ); return; } const comparison = await github.rest.repos.compareCommitsWithBasehead({ owner, repo, basehead: `${commit}...${currentBranchCommit}`, }); if (!["ahead", "identical"].includes(comparison.data.status)) { core.setFailed( `${version} sealed commit is no longer contained in ${defaultBranch}`, ); return; } } let sealedTagObject = ""; let reusedExistingSeal = false; if (existingRef) { sealedTagObject = await verifyExactTag(existingRef); reusedExistingSeal = true; core.info( `Reusing exact ${version} seal from run ${context.runId}; continuing failed jobs without recovery approval`, ); } else { const message = messageForAttempt(currentAttempt); const tag = await github.rest.git.createTag({ owner, repo, tag: version, message, object: commit, type: "commit", }); if ( tag.data.tag !== version || tag.data.object.type !== "commit" || tag.data.object.sha !== commit || tag.data.message !== message ) { core.setFailed(`${version} annotated tag creation response differs from the seal`); return; } let createRefResponse = null; let createRefError = null; for (const delayMs of postWriteRetryDelaysMs) { if (delayMs > 0) { core.info( `${version} ref creation is not confirmed; retrying in ${delayMs}ms`, ); await sleep(delayMs); } try { createRefResponse = await github.rest.git.createRef({ owner, repo, ref: `refs/tags/${version}`, sha: tag.data.sha, }); break; } catch (error) { createRefError = error; if ( error.status !== 422 && error.status !== 429 && !(Number.isInteger(error.status) && error.status >= 500 && error.status < 600) ) { throw error; } try { const observedRef = await github.rest.git.getRef({ owner, repo, ref: `tags/${version}`, }); await verifyExactTag(observedRef, tag.data.sha); createRefResponse = observedRef; break; } catch (observeError) { if (!retryablePostWriteError(observeError)) { throw observeError; } } } } if (!createRefResponse) { throw createRefError; } if ( createRefResponse.data.object.type !== "tag" || createRefResponse.data.object.sha !== tag.data.sha ) { core.setFailed(`${version} ref creation response differs from the new tag object`); return; } const sealedRef = await readAfterWrite( `${version} tag ref`, () => github.rest.git.getRef({ owner, repo, ref: `tags/${version}` }), ); sealedTagObject = await verifyExactTag(sealedRef, tag.data.sha); } core.setOutput("release_version", version); core.setOutput("release_commit", commit); core.setOutput("release_tag_object", sealedTagObject); await core.summary .addHeading("Cloud release sealed") .addRaw( `\`${version}\` → \`${commit}\` (run ${context.runId}` + `${reusedExistingSeal ? ", exact seal reused" : ""})`, ) .write();