From 6a1cdc34416ee34043f58b9bf3874343fede3627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Wed, 4 Nov 2026 00:09:22 -0800 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=90=9B=20fix(release):=20grant=20writ?= =?UTF-8?q?e=20to=20release=20job?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The publish step creates the release with gh, which needs contents: write. With contents: read the v2.4.8 run failed with HTTP 403 after the Marketplace upload had already succeeded. tox-dev/jetbrains-fish runs the same job with contents: write. --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index d493edf..d3b5994 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -143,7 +143,7 @@ jobs: if: always() && needs.release-notes.result == 'success' && needs.sign.result == 'success' && needs.marketplace-check.result == 'success' && (needs.marketplace-publish.result == 'success' || (needs.marketplace-publish.result == 'skipped' && needs.marketplace-check.outputs.publish == 'false')) runs-on: ubuntu-26.04 permissions: - contents: read + contents: write environment: release steps: - name: 📩 Download release notes From 94c77a107e22c65fd036b6e3fda39c332a18e822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Wed, 4 Nov 2026 00:09:26 -0800 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=91=B7=20ci(release):=20add=20GitHub?= =?UTF-8?q?=20release=20finalizer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A tag run that accepts the Marketplace upload can still fail while publishing the GitHub release. Re-pushing the tag is not an option, and the release workflow only runs on tag push, so this dispatch publishes the release from the failed run's artifacts. It takes the tag as an input and runs from main, because a workflow dispatched on the tag would need the file to exist in the tag. Before publishing, the signed ZIP and notes are checked against the acceptance receipt the run uploaded, so the release cannot carry bytes other than those accepted by the Marketplace. --- .../workflows/finalize-github-release.yaml | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 .github/workflows/finalize-github-release.yaml diff --git a/.github/workflows/finalize-github-release.yaml b/.github/workflows/finalize-github-release.yaml new file mode 100644 index 0000000..753ebc2 --- /dev/null +++ b/.github/workflows/finalize-github-release.yaml @@ -0,0 +1,91 @@ +name: 🚀 Finalize GitHub Release +on: + workflow_dispatch: + inputs: + tag: + description: Published v-prefixed release tag whose Marketplace upload was accepted + required: true + type: string + source-run-id: + description: Tag release workflow run that accepted the Marketplace upload + required: true + type: string + acceptance-artifact-id: + description: Immutable Marketplace acceptance receipt artifact ID + required: true + type: string + signed-artifact-id: + description: Signed ZIP artifact ID from that run + required: true + type: string + notes-artifact-id: + description: Release notes artifact ID from that run + required: true + type: string +permissions: + actions: read + contents: write +concurrency: + group: release-refs/tags/${{ inputs.tag }} + cancel-in-progress: false +jobs: + finalize: + name: 🚀 Finalize GitHub Release + runs-on: ubuntu-26.04 + environment: release + steps: + - name: 🔍 Verify the original tag run + id: source + env: + GH_TOKEN: ${{ github.token }} + SOURCE_RUN: ${{ inputs.source-run-id }} + TAG: ${{ inputs.tag }} + run: | + set -euo pipefail + commit=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$SOURCE_RUN" \ + --jq '. | select(.event == "push" and .path == ".github/workflows/release.yaml" and .head_branch == env.TAG) | .head_sha') + echo "commit=$commit" >>"$GITHUB_OUTPUT" + - name: 📩 Download Marketplace acceptance + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + artifact-ids: ${{ inputs.acceptance-artifact-id }} + run-id: ${{ inputs.source-run-id }} + github-token: ${{ github.token }} + path: ${{ runner.temp }}/acceptance + - name: 📩 Download signed plugin + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + artifact-ids: ${{ inputs.signed-artifact-id }} + run-id: ${{ inputs.source-run-id }} + github-token: ${{ github.token }} + path: dist + - name: 📩 Download release notes + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + artifact-ids: ${{ inputs.notes-artifact-id }} + run-id: ${{ inputs.source-run-id }} + github-token: ${{ github.token }} + path: ${{ runner.temp }}/notes + - name: 🔍 Check downloaded files against the acceptance receipt + env: + TAG: ${{ inputs.tag }} + COMMIT: ${{ steps.source.outputs.commit }} + RECEIPT: ${{ runner.temp }}/acceptance/marketplace-acceptance.json + run: | + set -euo pipefail + test "$(jq -r .tag "$RECEIPT")" = "$TAG" + test "$(jq -r .commit "$RECEIPT")" = "$COMMIT" + read -r digest _ < <(sha256sum dist/*-signed.zip) + test "$digest" = "$(jq -r .sha256 "$RECEIPT")" + read -r digest _ < <(sha256sum "$RUNNER_TEMP/notes/release-notes.md") + test "$digest" = "$(jq -r .notesSha256 "$RECEIPT")" + - name: 🚀 Publish GitHub release + uses: tox-dev/github-workflows/publish-github-release@2c1f5e4ceda37c41eb8367b5cbeaa741aa60a566 # 6.5.2 + env: + GH_TOKEN: ${{ github.token }} + with: + tag: ${{ inputs.tag }} + commit: ${{ steps.source.outputs.commit }} + title: ${{ inputs.tag }} + notes-file: ${{ runner.temp }}/notes/release-notes.md + assets-directory: dist