From 4ad71807467389260e437ca547d94f98833b3a0f Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 16 Jul 2026 00:17:32 +0900 Subject: [PATCH] Create release tags explicitly before draft release creation The publish rewrite removed the git tag push that previously preceded release creation. With the active tag ruleset, the workflow token cannot create a release for a tag that does not exist yet and GitHub returns 403 Resource not accessible by integration, which broke both the native CLI and dispatcher publish paths for every new release. The privileged publish jobs now create the tag ref through the git refs API at the already-verified release commit before creating the draft release, matching the previously proven tag-first flow without adding a repository checkout. Existing tags keep the same mismatch rejection, and reruns remain idempotent because a tag pointing at the verified commit skips creation. --- .github/workflows/dispatcher-publish.yml | 6 ++++++ .github/workflows/native-cli-publish.yml | 6 ++++++ scripts/test-dispatcher-publish-workflow.sh | 6 ++++++ scripts/test-native-cli-publish-workflow.sh | 6 ++++++ 4 files changed, 24 insertions(+) diff --git a/.github/workflows/dispatcher-publish.yml b/.github/workflows/dispatcher-publish.yml index f4f181f7d..8419e755c 100644 --- a/.github/workflows/dispatcher-publish.yml +++ b/.github/workflows/dispatcher-publish.yml @@ -252,6 +252,12 @@ jobs: echo "Release tag ${RELEASE_TAG} does not match approved build commit ${GITHUB_SHA}." >&2 exit 1 fi + if [ -z "${tag_sha}" ]; then + # Why the tag is created before the release: with an active tag ruleset the + # workflow token cannot create a release for a not-yet-existing tag, while + # explicit ref creation is permitted and pins the tag to the verified commit. + gh api "repos/${GITHUB_REPOSITORY}/git/refs" -f ref="refs/tags/${RELEASE_TAG}" -f sha="${GITHUB_SHA}" + fi if gh release view "${RELEASE_TAG}" --json isDraft,targetCommitish > release-input/release-state.json 2>/dev/null; then is_draft=$(jq -r '.isDraft' release-input/release-state.json) if [ "${is_draft}" = "false" ]; then diff --git a/.github/workflows/native-cli-publish.yml b/.github/workflows/native-cli-publish.yml index 73c8d2428..08ff8ada5 100644 --- a/.github/workflows/native-cli-publish.yml +++ b/.github/workflows/native-cli-publish.yml @@ -301,6 +301,12 @@ jobs: echo "Release tag ${RELEASE_TAG} does not match approved release commit ${RELEASE_SHA}." >&2 exit 1 fi + if [ -z "${tag_sha}" ]; then + # Why the tag is created before the release: with an active tag ruleset the + # workflow token cannot create a release for a not-yet-existing tag, while + # explicit ref creation is permitted and pins the tag to the verified commit. + gh api "repos/${GITHUB_REPOSITORY}/git/refs" -f ref="refs/tags/${RELEASE_TAG}" -f sha="${RELEASE_SHA}" + fi if gh release view "${RELEASE_TAG}" --json isDraft,targetCommitish > release-input/release-state.json 2>/dev/null; then is_draft=$(jq -r '.isDraft' release-input/release-state.json) if [ "${is_draft}" = "false" ]; then diff --git a/scripts/test-dispatcher-publish-workflow.sh b/scripts/test-dispatcher-publish-workflow.sh index 782798a83..0e4de9edb 100755 --- a/scripts/test-dispatcher-publish-workflow.sh +++ b/scripts/test-dispatcher-publish-workflow.sh @@ -166,6 +166,11 @@ test_publish_rejects_manifest_and_existing_tag_mismatches() { assert_not_contains "release-input/dist/dispatcher-release" } +test_release_tag_is_created_before_the_release() { + assert_contains 'gh api "repos/${GITHUB_REPOSITORY}/git/refs" -f ref="refs/tags/${RELEASE_TAG}" -f sha="${GITHUB_SHA}"' + assert_before 'gh api "repos/${GITHUB_REPOSITORY}/git/refs"' 'gh release create "${RELEASE_TAG}"' +} + test_draft_creation_accepts_only_the_known_missing_tag_responses() { assert_contains '*"HTTP 404"*) tag_sha="" ;;' assert_contains '*"HTTP 422"*"No commit found for SHA"*|*"No commit found for SHA"*"HTTP 422"*) tag_sha="" ;;' @@ -214,6 +219,7 @@ test_checkout_free_publish_has_explicit_repository_context test_dispatcher_assets_are_attested_after_the_manifest_is_verified test_dispatcher_verifies_tagged_installers_after_draft_creation test_publish_rejects_manifest_and_existing_tag_mismatches +test_release_tag_is_created_before_the_release test_draft_creation_accepts_only_the_known_missing_tag_responses test_dispatcher_publish_rechecks_the_tag_before_publishing test_dispatcher_build_preserves_release_checks diff --git a/scripts/test-native-cli-publish-workflow.sh b/scripts/test-native-cli-publish-workflow.sh index 959187e0b..dd4ea064a 100755 --- a/scripts/test-native-cli-publish-workflow.sh +++ b/scripts/test-native-cli-publish-workflow.sh @@ -177,6 +177,11 @@ test_publish_rejects_manifest_and_existing_tag_mismatches() { assert_not_contains "release-input/dist/release" } +test_release_tag_is_created_before_the_release() { + assert_contains 'gh api "repos/${GITHUB_REPOSITORY}/git/refs" -f ref="refs/tags/${RELEASE_TAG}" -f sha="${RELEASE_SHA}"' + assert_before 'gh api "repos/${GITHUB_REPOSITORY}/git/refs"' 'gh release create "${RELEASE_TAG}"' +} + test_draft_creation_accepts_only_the_known_missing_tag_responses() { assert_contains '*"HTTP 404"*) tag_sha="" ;;' assert_contains '*"HTTP 422"*"No commit found for SHA"*|*"No commit found for SHA"*"HTTP 422"*) tag_sha="" ;;' @@ -224,6 +229,7 @@ test_publish_validates_metadata_without_checking_out_source test_checkout_free_publish_has_explicit_repository_context test_assets_are_attested_after_the_manifest_is_verified test_publish_rejects_manifest_and_existing_tag_mismatches +test_release_tag_is_created_before_the_release test_draft_creation_accepts_only_the_known_missing_tag_responses test_publish_rechecks_the_tag_and_uses_least_privilege_post_publish_permissions test_build_verifies_assets_before_writing_the_publish_input