diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 783cf3e8..5700bddf 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -7,7 +7,7 @@ on: workflow_dispatch: inputs: release_tag: - description: Existing draft release tag to recover (for example, v1.2.0) + description: Existing draft or assetless release tag to recover (for example, v1.8.0) required: true type: string @@ -95,15 +95,23 @@ jobs: fi is_draft="$(jq -r '.[0].draft' <<<"${matching_releases}")" + asset_count="$(jq -r '.[0].assets | length' <<<"${matching_releases}")" release_ref="$(jq -r '.[0].target_commitish' <<<"${matching_releases}")" - if [[ "${is_draft}" != "true" ]]; then - echo "::error::Release ${RECOVERY_TAG} is already published" + if [[ "${is_draft}" != "true" && "${asset_count}" != "0" ]]; then + echo "::error::Published release ${RECOVERY_TAG} already has assets" exit 1 fi if [[ ! "${release_ref}" =~ ^[0-9a-f]{40}$ ]]; then - echo "::error::Draft ${RECOVERY_TAG} is not pinned to an immutable commit SHA" + echo "::error::Release ${RECOVERY_TAG} is not pinned to an immutable commit SHA" exit 1 fi + if [[ "${is_draft}" != "true" ]]; then + tag_ref="$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${RECOVERY_TAG}" --jq '.object.sha')" + if [[ "${tag_ref}" != "${release_ref}" ]]; then + echo "::error::Published release ${RECOVERY_TAG} does not match its Git tag" + exit 1 + fi + fi { echo "should_release=true" @@ -297,7 +305,7 @@ jobs: bun-version: 1.1.0 - name: Smoke test exact npm bundle on minimum runtime - run: bun run scripts/smoke-npm-bundle.ts --expected-bun=1.1.0 + run: bun run scripts/smoke-npm-bundle.ts --expected-bun=1.1.0 --scaffold-only working-directory: cli - name: Restore release build toolchain diff --git a/cli/scripts/release.test.ts b/cli/scripts/release.test.ts index 0323311c..751e3cda 100644 --- a/cli/scripts/release.test.ts +++ b/cli/scripts/release.test.ts @@ -505,7 +505,7 @@ describe("release infrastructure wiring", () => { } }); - test("gates ordered draft publication on canonical and native verification", async () => { + test("gates ordered release publication on canonical and native verification", async () => { const workflow = await readWorkflow("release-please.yml"); const releasePleaseConfig = JSON.parse( await readFile(join(repositoryRoot, "release-please-config.json"), "utf8"), @@ -519,7 +519,7 @@ describe("release infrastructure wiring", () => { expect(releasePleaseConfig.packages["."]?.draft).toBe(true); expect(workflow.on?.workflow_dispatch?.inputs?.release_tag).toEqual({ - description: "Existing draft release tag to recover (for example, v1.2.0)", + description: "Existing draft or assetless release tag to recover (for example, v1.8.0)", required: true, type: "string", }); @@ -535,7 +535,11 @@ describe("release infrastructure wiring", () => { expect(contextScript).toContain("gh api --paginate --slurp"); expect(contextScript).toContain("releases?per_page=100"); expect(contextScript).toContain("select(.tag_name == $tag)"); - expect(contextScript).toContain("already published"); + expect(contextScript).toContain("asset_count=\"$(jq -r '.[0].assets | length'"); + expect(contextScript).toContain('"${is_draft}" != "true" && "${asset_count}" != "0"'); + expect(contextScript).toContain("already has assets"); + expect(contextScript).toContain("git/ref/tags/${RECOVERY_TAG}"); + expect(contextScript).toContain('"${tag_ref}" != "${release_ref}"'); expect(contextScript).not.toContain("gh release view"); expect(contextScript).toContain("immutable commit SHA"); expect(contextScript).toContain("Release recovery must run from refs/heads/main"); @@ -589,6 +593,9 @@ describe("release infrastructure wiring", () => { ].map((name) => workflowStepIndex(publication, name)); expect(orderedSteps.every((index) => index >= 0)).toBe(true); expect(orderedSteps).toEqual([...orderedSteps].sort((left, right) => left - right)); + expect(publication.steps?.[orderedSteps[5] ?? -1]?.run).toBe( + "bun run scripts/smoke-npm-bundle.ts --expected-bun=1.1.0 --scaffold-only", + ); const publishScript = "bun run .release-orchestration/cli/scripts/publish-release.ts"; expect(publication.steps?.[orderedSteps[9] ?? -1]?.run).toBe(`${publishScript} upload-github`); expect(publication.steps?.[orderedSteps[10] ?? -1]?.run).toBe(`${publishScript} publish-npm`);