From 96872e3790ee7f8d53db0c9928c88df601bc05b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Wed, 6 May 2026 10:32:12 +0200 Subject: [PATCH 1/7] Protect publishes with env gate --- .github/workflows/publish.yml | 56 ++++++++++++++++++++++++ .github/workflows/version-or-publish.yml | 27 ------------ 2 files changed, 56 insertions(+), 27 deletions(-) create mode 100644 .github/workflows/publish.yml delete mode 100644 .github/workflows/version-or-publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..c134d6ef --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,56 @@ +name: Publish + +on: + push: + branches: + - main + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +permissions: {} # each job should define its own permission explicitly + +jobs: + version: + name: Version + runs-on: ubuntu-latest + timeout-minutes: 20 + outputs: + hasChangesets: ${{ steps.changesets.outputs.hasChangesets }} + permissions: + contents: write # to create release (changesets/action) + issues: write # to post issue comments (changesets/action) + pull-requests: write # to create pull request (changesets/action) + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: ./.github/actions/ci-setup + + - name: Build + run: yarn build + + - name: Create or update release pull request + id: changesets + uses: ./ + with: + version: yarn bump + + publish: + name: Publish + if: needs.version.outputs.hasChangesets == 'false' + needs: version + runs-on: ubuntu-latest + environment: marketplace + timeout-minutes: 20 + permissions: + contents: write # to create release (changesets/action) + id-token: write # to use OpenID Connect token for trusted publishing (changesets/action) + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: ./.github/actions/ci-setup + + - name: Build + run: yarn build + + - name: Publish to marketplace + uses: ./ + with: + publish: yarn release diff --git a/.github/workflows/version-or-publish.yml b/.github/workflows/version-or-publish.yml deleted file mode 100644 index 4e7f070c..00000000 --- a/.github/workflows/version-or-publish.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Version or Publish - -on: - push: - branches: - - main - -concurrency: ${{ github.workflow }}-${{ github.ref }} - -jobs: - changesets: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: ./.github/actions/ci-setup - - - name: Build - run: yarn build - - - name: Create Release Pull Request or Publish - id: changesets - uses: ./ - with: - version: yarn bump - publish: yarn release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 2b594ab64edb5dd2d6472860b4059425693ad71c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Wed, 6 May 2026 11:23:05 +0200 Subject: [PATCH 2/7] tweak CODEOWNERS and release-pr --- .github/CODEOWNERS | 6 ++- .github/workflows/release-pr.yml | 65 +++++++++++++++++++++++++++++--- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 50032f21..73b552ae 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1,5 @@ -* @Andarist @emmatown +/.github/workflows/** @Andarist @emmatown +/action.yml @Andarist @emmatown +/scripts/bump.ts @Andarist @emmatown +/scripts/release.ts @Andarist @emmatown +/scripts/release-pr.ts @Andarist @emmatown diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 31d21afc..aa3fdaae 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -4,10 +4,16 @@ on: issue_comment: types: [created] +permissions: {} + jobs: release_check: if: github.repository == 'changesets/action' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/release-pr') runs-on: ubuntu-latest + permissions: + contents: read + issues: write + pull-requests: read steps: - id: report_in_progress run: | @@ -15,6 +21,18 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - id: parse_command + run: | + if [[ "$COMMENT_BODY" =~ ^/release-pr[[:space:]]+([0-9a-fA-F]{7,40})[[:space:]]*$ ]] + then + echo "requested_sha=${BASH_REMATCH[1]}" >> "$GITHUB_OUTPUT" + else + echo "Expected '/release-pr ' where is a 7-40 character commit SHA." + exit 1 + fi + env: + COMMENT_BODY: ${{ github.event.comment.body }} + - id: check_authorization run: | if [[ $AUTHOR_ASSOCIATION == 'MEMBER' || $AUTHOR_ASSOCIATION == 'OWNER' || $AUTHOR_ASSOCIATION == 'COLLABORATOR' ]] @@ -27,22 +45,56 @@ jobs: env: AUTHOR_ASSOCIATION: ${{ github.event.comment.author_association }} + - id: get_pr_head_sha + run: | + echo "pr_head_sha=$(gh pr view ${{ github.event.issue.number }} --json headRefOid --jq '.headRefOid')" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + outputs: in_progress_reaction_id: ${{ steps.report_in_progress.outputs.in_progress_reaction_id }} + requested_sha: ${{ steps.parse_command.outputs.requested_sha }} + pr_head_sha: ${{ steps.get_pr_head_sha.outputs.pr_head_sha }} release: if: github.repository == 'changesets/action' timeout-minutes: 20 runs-on: ubuntu-latest needs: release_check + permissions: + contents: write + issues: write + pull-requests: write steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: ./.github/actions/ci-setup - - name: Checkout pull request - run: gh pr checkout ${{ github.event.issue.number }} + - name: Fetch pull request head commit + run: git fetch --no-tags origin ${{ needs.release_check.outputs.pr_head_sha }} + + - name: Resolve requested SHA + id: resolve_requested_sha + run: | + resolved_sha=$(git rev-parse --verify "${REQUESTED_SHA}^{commit}") + echo "resolved_sha=$resolved_sha" >> "$GITHUB_OUTPUT" env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REQUESTED_SHA: ${{ needs.release_check.outputs.requested_sha }} + + - name: Verify requested SHA matches current PR head + run: | + if [[ "$RESOLVED_SHA" != "$PR_HEAD_SHA" ]] + then + echo "Requested SHA $RESOLVED_SHA is not the current PR head $PR_HEAD_SHA." + exit 1 + fi + env: + RESOLVED_SHA: ${{ steps.resolve_requested_sha.outputs.resolved_sha }} + PR_HEAD_SHA: ${{ needs.release_check.outputs.pr_head_sha }} + + - name: Checkout validated commit + run: git checkout --detach $RESOLVED_SHA + env: + RESOLVED_SHA: ${{ steps.resolve_requested_sha.outputs.resolved_sha }} - name: Check if Version Packages PR id: check_version_packages @@ -78,7 +130,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - run: gh pr comment ${{ github.event.issue.number }} --body "The [${{ github.repository }}@$(git rev-parse HEAD)](https://github.com/${{ github.repository }}/commit/$(git rev-parse HEAD)) release triggered by [this comment](${{ github.event.comment.url }}) has [succeeded](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})." + - run: gh pr comment ${{ github.event.issue.number }} --body "The release for \`${{ steps.resolve_requested_sha.outputs.resolved_sha }}\` triggered by [this comment](${{ github.event.comment.url }}) has [succeeded](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}). Published commit: [${{ github.repository }}@$(git rev-parse HEAD)](https://github.com/${{ github.repository }}/commit/$(git rev-parse HEAD))." env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -87,6 +139,9 @@ jobs: timeout-minutes: 2 runs-on: ubuntu-latest if: failure() && github.repository == 'changesets/action' && (needs.release_check.result == 'failure' || needs.release.result == 'failure') + permissions: + issues: write + pull-requests: write steps: - run: gh api /repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions -f content='-1' env: @@ -96,7 +151,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - run: gh pr comment ${{ github.event.issue.number }} --body "The release triggered by [this comment](${{ github.event.comment.url }}) has [failed](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})." + - run: gh pr comment ${{ github.event.issue.number }} --body "The release for \`${{ needs.release_check.outputs.requested_sha }}\` triggered by [this comment](${{ github.event.comment.url }}) has [failed](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})." env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} From 964f7c22f98508ee7fcc8e24fafb2477146bfc45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Wed, 6 May 2026 11:25:51 +0200 Subject: [PATCH 3/7] remove redundant permission --- .github/workflows/publish.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c134d6ef..d474b1ce 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -42,7 +42,6 @@ jobs: timeout-minutes: 20 permissions: contents: write # to create release (changesets/action) - id-token: write # to use OpenID Connect token for trusted publishing (changesets/action) steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: ./.github/actions/ci-setup From 24bd8dd9c0903c3fc53475e38a164ac29071617d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Wed, 6 May 2026 11:30:08 +0200 Subject: [PATCH 4/7] fix syntax --- .github/workflows/release-pr.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index aa3fdaae..af79f040 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -130,7 +130,8 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - run: gh pr comment ${{ github.event.issue.number }} --body "The release for \`${{ steps.resolve_requested_sha.outputs.resolved_sha }}\` triggered by [this comment](${{ github.event.comment.url }}) has [succeeded](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}). Published commit: [${{ github.repository }}@$(git rev-parse HEAD)](https://github.com/${{ github.repository }}/commit/$(git rev-parse HEAD))." + - run: | + gh pr comment ${{ github.event.issue.number }} --body "The release for \`${{ steps.resolve_requested_sha.outputs.resolved_sha }}\` triggered by [this comment](${{ github.event.comment.url }}) has [succeeded](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}). Published commit: [${{ github.repository }}@$(git rev-parse HEAD)](https://github.com/${{ github.repository }}/commit/$(git rev-parse HEAD))." env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -151,7 +152,8 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - run: gh pr comment ${{ github.event.issue.number }} --body "The release for \`${{ needs.release_check.outputs.requested_sha }}\` triggered by [this comment](${{ github.event.comment.url }}) has [failed](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})." + - run: | + gh pr comment ${{ github.event.issue.number }} --body "The release for \`${{ needs.release_check.outputs.requested_sha }}\` triggered by [this comment](${{ github.event.comment.url }}) has [failed](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})." env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} From c48fa3efc6111657bce2d50092feb7bdea7c2755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Thu, 7 May 2026 10:35:14 +0200 Subject: [PATCH 5/7] Use app with protection bypass for publishing --- .github/workflows/publish.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d474b1ce..30b619cb 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -43,7 +43,16 @@ jobs: permissions: contents: write # to create release (changesets/action) steps: + - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + id: app-token + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + token: ${{ steps.app-token.outputs.token }} + - uses: ./.github/actions/ci-setup - name: Build From 0444a3e7c2c4c281be4980b8ac005ec03d96f544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Thu, 7 May 2026 11:10:02 +0200 Subject: [PATCH 6/7] handle forks better --- .github/workflows/release-pr.yml | 72 +++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 24 deletions(-) diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index af79f040..35f2f5d1 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -45,16 +45,54 @@ jobs: env: AUTHOR_ASSOCIATION: ${{ github.event.comment.author_association }} - - id: get_pr_head_sha + - id: resolve_requested_sha run: | - echo "pr_head_sha=$(gh pr view ${{ github.event.issue.number }} --json headRefOid --jq '.headRefOid')" >> "$GITHUB_OUTPUT" + requested_sha=$(echo "$REQUESTED_SHA" | tr '[:upper:]' '[:lower:]') + + mapfile -t matches < <( + gh api /repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}/commits --paginate --jq '.[].sha' | + awk -v sha="$requested_sha" 'index(tolower($0), sha) == 1 { print $0 }' + ) + + if [[ ${#matches[@]} -eq 0 ]] + then + echo "Requested SHA $REQUESTED_SHA is not part of this pull request." + exit 1 + fi + + if [[ ${#matches[@]} -gt 1 ]] + then + echo "Requested SHA $REQUESTED_SHA is ambiguous for this pull request." + exit 1 + fi + + resolved_sha="${matches[0]}" + pr_head_sha=$(gh pr view ${{ github.event.issue.number }} --json headRefOid --jq '.headRefOid') + + if [[ "$resolved_sha" != "$pr_head_sha" ]] + then + echo "Requested SHA $resolved_sha is not the current PR head $pr_head_sha." + exit 1 + fi + + echo "resolved_sha=$resolved_sha" >> "$GITHUB_OUTPUT" + echo "pr_head_sha=$pr_head_sha" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REQUESTED_SHA: ${{ steps.parse_command.outputs.requested_sha }} + + - id: get_pr_head_repository + run: | + echo "head_repository=$(gh pr view ${{ github.event.issue.number }} --json headRepositoryOwner,headRepository --jq '.headRepositoryOwner.login + "/" + .headRepository.name')" >> "$GITHUB_OUTPUT" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} outputs: in_progress_reaction_id: ${{ steps.report_in_progress.outputs.in_progress_reaction_id }} requested_sha: ${{ steps.parse_command.outputs.requested_sha }} - pr_head_sha: ${{ steps.get_pr_head_sha.outputs.pr_head_sha }} + resolved_sha: ${{ steps.resolve_requested_sha.outputs.resolved_sha }} + pr_head_sha: ${{ steps.resolve_requested_sha.outputs.pr_head_sha }} + head_repository: ${{ steps.get_pr_head_repository.outputs.head_repository }} release: if: github.repository == 'changesets/action' @@ -69,32 +107,18 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: ./.github/actions/ci-setup - - name: Fetch pull request head commit - run: git fetch --no-tags origin ${{ needs.release_check.outputs.pr_head_sha }} - - - name: Resolve requested SHA - id: resolve_requested_sha + - name: Fetch validated commit from pull request head repository run: | - resolved_sha=$(git rev-parse --verify "${REQUESTED_SHA}^{commit}") - echo "resolved_sha=$resolved_sha" >> "$GITHUB_OUTPUT" - env: - REQUESTED_SHA: ${{ needs.release_check.outputs.requested_sha }} - - - name: Verify requested SHA matches current PR head - run: | - if [[ "$RESOLVED_SHA" != "$PR_HEAD_SHA" ]] - then - echo "Requested SHA $RESOLVED_SHA is not the current PR head $PR_HEAD_SHA." - exit 1 - fi + git remote add pr-head https://github.com/$HEAD_REPOSITORY.git + git fetch --no-tags pr-head $RESOLVED_SHA env: - RESOLVED_SHA: ${{ steps.resolve_requested_sha.outputs.resolved_sha }} - PR_HEAD_SHA: ${{ needs.release_check.outputs.pr_head_sha }} + HEAD_REPOSITORY: ${{ needs.release_check.outputs.head_repository }} + RESOLVED_SHA: ${{ needs.release_check.outputs.resolved_sha }} - name: Checkout validated commit run: git checkout --detach $RESOLVED_SHA env: - RESOLVED_SHA: ${{ steps.resolve_requested_sha.outputs.resolved_sha }} + RESOLVED_SHA: ${{ needs.release_check.outputs.resolved_sha }} - name: Check if Version Packages PR id: check_version_packages @@ -131,7 +155,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh pr comment ${{ github.event.issue.number }} --body "The release for \`${{ steps.resolve_requested_sha.outputs.resolved_sha }}\` triggered by [this comment](${{ github.event.comment.url }}) has [succeeded](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}). Published commit: [${{ github.repository }}@$(git rev-parse HEAD)](https://github.com/${{ github.repository }}/commit/$(git rev-parse HEAD))." + gh pr comment ${{ github.event.issue.number }} --body "The release for \`${{ needs.release_check.outputs.resolved_sha }}\` triggered by [this comment](${{ github.event.comment.url }}) has [succeeded](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}). Published commit: [${{ github.repository }}@$(git rev-parse HEAD)](https://github.com/${{ github.repository }}/commit/$(git rev-parse HEAD))." env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 5c49ca6190c7aaa7488b77087be76d67838d6931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Thu, 7 May 2026 11:12:00 +0200 Subject: [PATCH 7/7] remove redundant permission --- .github/workflows/publish.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 30b619cb..46edd6c7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -40,8 +40,6 @@ jobs: runs-on: ubuntu-latest environment: marketplace timeout-minutes: 20 - permissions: - contents: write # to create release (changesets/action) steps: - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 id: app-token