Release #98
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - "v*.*.*" | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| preflight: | |
| # Fail within seconds if WINGET_GITHUB_TOKEN is expired or revoked, | |
| # instead of 14 minutes into GoReleaser — after it has already created | |
| # the draft release and opened the Homebrew/Scoop PRs — which strands | |
| # the release in a half-published state (see the v0.18.2 run). The | |
| # winget publisher is the only one on a long-lived PAT: the release-bot | |
| # app token cannot open PRs on microsoft/winget-pkgs, where the app is | |
| # not installed. | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Check winget token validity and expiry | |
| env: | |
| WINGET_GITHUB_TOKEN: ${{ secrets.WINGET_GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| headers_file="$(mktemp)" | |
| status="$(curl -sS -o /dev/null -D "$headers_file" -w '%{http_code}' \ | |
| -H "Authorization: Bearer ${WINGET_GITHUB_TOKEN}" \ | |
| https://api.github.com/repos/bomly-dev/winget-pkgs)" | |
| if [ "$status" != "200" ]; then | |
| echo "::error::WINGET_GITHUB_TOKEN cannot read bomly-dev/winget-pkgs (HTTP ${status}). The PAT has likely expired or been revoked — mint a new one (public_repo scope, it must be able to open PRs on microsoft/winget-pkgs) and update the secret: gh secret set WINGET_GITHUB_TOKEN -R bomly-dev/bomly-cli" | |
| exit 1 | |
| fi | |
| expiry="$(grep -i '^github-authentication-token-expiration:' "$headers_file" | cut -d' ' -f2- | tr -d '\r' || true)" | |
| if [ -n "$expiry" ]; then | |
| expiry_epoch="$(date -u -d "$expiry" +%s)" | |
| days_left=$(( (expiry_epoch - $(date -u +%s)) / 86400 )) | |
| echo "WINGET_GITHUB_TOKEN expires ${expiry} UTC (${days_left} days from now)." | |
| if [ "$days_left" -lt 14 ]; then | |
| echo "::warning::WINGET_GITHUB_TOKEN expires in ${days_left} days (${expiry} UTC). Rotate it soon: gh secret set WINGET_GITHUB_TOKEN -R bomly-dev/bomly-cli" | |
| fi | |
| else | |
| echo "WINGET_GITHUB_TOKEN has no expiration date." | |
| fi | |
| validate: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Set up Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version-file: go.mod | |
| cache: true | |
| cache-dependency-path: | | |
| go.sum | |
| - name: Run test suite | |
| run: make test | |
| - name: Run go vet | |
| run: go vet ./... | |
| release: | |
| needs: [preflight, validate] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| permissions: | |
| contents: write | |
| packages: write | |
| id-token: write # required for cosign keyless signing | |
| outputs: | |
| hashes: ${{ steps.hash.outputs.hashes }} | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version-file: go.mod | |
| cache: true | |
| cache-dependency-path: | | |
| go.sum | |
| - name: Mint package-manager token | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| id: package-token | |
| with: | |
| client-id: ${{ vars.RELEASE_BOT_CLIENT_ID }} | |
| private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }} | |
| owner: bomly-dev | |
| repositories: | | |
| homebrew-tap | |
| scoop-bucket | |
| permission-contents: write | |
| permission-pull-requests: write | |
| - name: Mint release token | |
| # Used as GoReleaser's GITHUB_TOKEN to create the draft release on this | |
| # repo and upload its assets (archives, packages, checksums, signature). | |
| # The release stays a draft here (see .goreleaser.yaml) — actual | |
| # publishing happens in the `publish` job below, after the SLSA | |
| # provenance file is also attached. | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| id: release-token | |
| with: | |
| client-id: ${{ vars.RELEASE_BOT_CLIENT_ID }} | |
| private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }} | |
| owner: bomly-dev | |
| repositories: bomly-cli | |
| permission-contents: write | |
| - name: Install cosign | |
| uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 | |
| - name: Run GoReleaser | |
| uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 | |
| with: | |
| distribution: goreleaser | |
| version: v2.16.0 | |
| args: release --clean | |
| env: | |
| GITHUB_TOKEN: ${{ steps.release-token.outputs.token }} | |
| TAP_GITHUB_TOKEN: ${{ steps.package-token.outputs.token }} | |
| SCOOP_GITHUB_TOKEN: ${{ steps.package-token.outputs.token }} | |
| WINGET_GITHUB_TOKEN: ${{ secrets.WINGET_GITHUB_TOKEN }} | |
| - name: Generate artifact hashes for SLSA provenance | |
| id: hash | |
| run: echo "hashes=$(base64 -w0 < dist/SHA256SUMS)" >> "$GITHUB_OUTPUT" | |
| provenance: | |
| needs: release | |
| permissions: | |
| actions: read # detect the workflow that triggered this run | |
| id-token: write # sign the provenance | |
| contents: write # upload the provenance to the release | |
| # The SLSA generator MUST be pinned by a full vX.Y.Z tag, not a commit SHA or | |
| # shorter tag — slsa-verifier resolves the builder identity from this exact ref, | |
| # and SHA-pinning breaks that resolution. See .github/pinact.yaml, which excludes | |
| # this line from automated re-pinning. | |
| # | |
| # upload-assets is deliberately false: its underlying action | |
| # (softprops/action-gh-release) finds a release by tag, and GitHub's | |
| # "get release by tag" API does not return drafts. Against our still-draft | |
| # release, it can't find one — so it creates a *second*, non-draft release | |
| # for the same tag instead, which immediately and permanently marks that | |
| # tag as immutable (even if the bad release is later deleted). The | |
| # `publish` job uploads this provenance file itself, by release ID, to | |
| # avoid that entirely. | |
| uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 | |
| with: | |
| base64-subjects: ${{ needs.release.outputs.hashes }} | |
| upload-assets: false | |
| publish: | |
| needs: [release, provenance] | |
| if: startsWith(github.ref, 'refs/tags/') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Mint release token | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| id: release-token | |
| with: | |
| client-id: ${{ vars.RELEASE_BOT_CLIENT_ID }} | |
| private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }} | |
| owner: bomly-dev | |
| repositories: bomly-cli | |
| permission-contents: write | |
| - name: Download SLSA provenance | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: ${{ needs.provenance.outputs.provenance-name }} | |
| - name: Attach provenance and publish release | |
| # Uploads the provenance file directly to the draft release by ID | |
| # (not by tag — see the `provenance` job comment for why), then | |
| # finalizes the draft into a published release now that every asset | |
| # is attached. Must run last: immutable releases reject new assets | |
| # once published. | |
| # | |
| # Uses the release-bot app token, not the default GITHUB_TOKEN: GitHub | |
| # does not start new workflow runs from events triggered by the | |
| # default token, so a release published with it would never fire the | |
| # `release: published` trigger on "Release lifecycle sync" | |
| # (notify-landing-yank.yml) — and the landing-page docs sync would | |
| # silently never run. An app token is attributed to the app, so the | |
| # publish event cascades. | |
| env: | |
| GH_TOKEN: ${{ steps.release-token.outputs.token }} | |
| RELEASE_TAG: ${{ github.ref_name }} | |
| PROVENANCE_FILE: ${{ needs.provenance.outputs.provenance-name }} | |
| run: | | |
| set -euo pipefail | |
| release_id="$(gh api "repos/bomly-dev/bomly-cli/releases?per_page=100" | \ | |
| jq -r --arg tag "$RELEASE_TAG" '.[] | select(.tag_name == $tag and .draft == true) | .id' | head -n1)" | |
| if [ -z "$release_id" ]; then | |
| echo "::error::No draft release found for tag ${RELEASE_TAG}" | |
| exit 1 | |
| fi | |
| curl -fsSL \ | |
| -X POST \ | |
| -H "Authorization: Bearer ${GH_TOKEN}" \ | |
| -H "Content-Type: application/octet-stream" \ | |
| --data-binary "@${PROVENANCE_FILE}" \ | |
| "https://uploads.github.com/repos/bomly-dev/bomly-cli/releases/${release_id}/assets?name=${PROVENANCE_FILE}" \ | |
| > /dev/null | |
| gh release edit "$RELEASE_TAG" --draft=false --repo bomly-dev/bomly-cli | |
| publish-npm: | |
| # Publishes the `bomly-mcp` npm wrapper, which is the package the official | |
| # MCP Registry entry points at. | |
| # | |
| # Authentication is npm Trusted Publishing (OIDC) — there is deliberately | |
| # no npm token here, and none should ever be added. npm's own token screen | |
| # warns against long-lived automation tokens for exactly this case. | |
| # | |
| # Must run after `publish`: the wrapper's postinstall step downloads the | |
| # release archive and its checksum file from this tag, and draft-release | |
| # assets are not publicly downloadable — so publishing npm any earlier | |
| # ships a package that cannot install. | |
| # | |
| # Split from `publish-mcp-registry` so a failure in either half can be | |
| # re-run on its own. npm versions are immutable, so this job also treats | |
| # "this exact version is already published" as success rather than as an | |
| # error that would block the registry step behind it. | |
| # | |
| # Inert until Ahmed turns it on. Setup is one-time and documented in | |
| # the short version: | |
| # 1. publish bomly-mcp once by hand (trusted publishing cannot create a | |
| # package that does not exist yet) | |
| # 2. set the trusted publisher at | |
| # npmjs.com/package/bomly-mcp/access -> this repo, workflow release.yml | |
| # 3. gh variable set PUBLISH_MCP_REGISTRY -R bomly-dev/bomly-cli --body true | |
| needs: publish | |
| if: startsWith(github.ref, 'refs/tags/') && vars.PUBLISH_MCP_REGISTRY == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| # Named in npm's trusted-publisher config, which checks it as an OIDC | |
| # claim: publishing fails unless the two match. Restrict this environment | |
| # to the tag pattern `v*` so a push to any branch cannot reach npm. | |
| # | |
| # Deliberately NOT the existing `release` environment — that one is scoped | |
| # to the `main` branch, and this job runs on a tag ref, so it would be | |
| # rejected outright. | |
| environment: npm-publish | |
| permissions: | |
| contents: read | |
| id-token: write # mints the OIDC token npm exchanges for publish rights | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Ensure npm supports trusted publishing | |
| # Trusted publishing needs npm >= 11.5.1, which the runner image has | |
| # not always bundled. Upgrade only when the shipped npm is too old, so | |
| # this becomes a no-op once the image catches up. | |
| # | |
| # `sudo` is required: npm's global prefix on the runner is /usr/local, | |
| # which the `runner` user cannot write to. Without it this fails with | |
| # EACCES on /usr/local/share/man — which is exactly how the v0.21.2 | |
| # release lost its npm and registry publish. | |
| run: | | |
| set -euo pipefail | |
| required=11.5.1 | |
| current="$(npm --version)" | |
| if printf '%s\n%s\n' "$required" "$current" | sort -V -C; then | |
| echo "npm ${current} already supports trusted publishing." | |
| else | |
| echo "npm ${current} is older than ${required}; upgrading." | |
| sudo npm install -g "npm@^${required}" | |
| fi | |
| npm --version | |
| - name: Stamp the release version into npm/package.json | |
| # The tag is authoritative. Auto Version keeps the committed files in | |
| # sync too, but deriving here means a missed bump cannot publish a | |
| # wrapper that points at a release that does not exist. | |
| env: | |
| RELEASE_TAG: ${{ github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| version="${RELEASE_TAG#v}" | |
| jq --arg v "$version" '.version = $v' npm/package.json > npm/package.json.tmp | |
| mv npm/package.json.tmp npm/package.json | |
| - name: Publish bomly-mcp to npm | |
| # No `env:` and no npmrc: npm reads the OIDC token from the Actions | |
| # runtime itself. --provenance is passed explicitly even though trusted | |
| # publishing is documented to imply it, because the implicit path has | |
| # been reported to no-op. | |
| env: | |
| RELEASE_TAG: ${{ github.ref_name }} | |
| working-directory: npm | |
| run: | | |
| set -euo pipefail | |
| version="${RELEASE_TAG#v}" | |
| # Idempotent: a re-run after a partial failure must not die here on | |
| # an immutable version that is already live. | |
| if curl -fsSL "https://registry.npmjs.org/bomly-mcp/${version}" > /dev/null 2>&1; then | |
| echo "bomly-mcp@${version} is already published; skipping npm publish." | |
| exit 0 | |
| fi | |
| npm publish --provenance --access public | |
| publish-mcp-registry: | |
| # Publishes the server entry in the official MCP Registry | |
| # (https://registry.modelcontextprotocol.io). | |
| # | |
| # Registry auth is GitHub OIDC, so it needs no secret of its own; the | |
| # `io.github.bomly-dev/*` namespace is granted from this repo's identity. | |
| # | |
| # Kept separate from publish-npm so either half can be re-run alone: npm | |
| # versions are immutable, so a combined job that failed here could not be | |
| # retried without tripping over the already-published version. (It was also | |
| # the boundary that kept an npm token away from the downloaded | |
| # mcp-publisher binary; with trusted publishing there is no token left to | |
| # isolate, but the retry argument stands on its own.) | |
| needs: publish-npm | |
| if: startsWith(github.ref, 'refs/tags/') && vars.PUBLISH_MCP_REGISTRY == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read | |
| id-token: write # required for mcp-publisher's OIDC login | |
| env: | |
| # Pinned deliberately. `releases/latest` would download and execute a | |
| # moving target inside a job that holds an OIDC identity able to publish | |
| # under our registry namespace. Bump this together with the digest and | |
| # the signer identity below, after reading the release diff. | |
| MCP_PUBLISHER_VERSION: v1.8.0 | |
| MCP_PUBLISHER_SHA256: 1370446bbe74d562608e8005a6ccce02d146a661fbd78674e11cc70b9618d6cf | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Stamp the release version into server.json | |
| env: | |
| RELEASE_TAG: ${{ github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| version="${RELEASE_TAG#v}" | |
| jq --arg v "$version" '.version = $v | .packages[0].version = $v' server.json > server.json.tmp | |
| mv server.json.tmp server.json | |
| - name: Wait for the npm package to be resolvable | |
| # The registry verifies package ownership by fetching the package, so | |
| # publishing the server entry before npm's CDN has the new version | |
| # fails validation. | |
| env: | |
| RELEASE_TAG: ${{ github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| version="${RELEASE_TAG#v}" | |
| for attempt in $(seq 1 30); do | |
| if curl -fsSL "https://registry.npmjs.org/bomly-mcp/${version}" > /dev/null 2>&1; then | |
| echo "bomly-mcp@${version} is resolvable." | |
| exit 0 | |
| fi | |
| echo "Attempt ${attempt}: bomly-mcp@${version} not visible yet, waiting." | |
| sleep 10 | |
| done | |
| echo "::error::bomly-mcp@${version} did not become resolvable on npm within 5 minutes" | |
| exit 1 | |
| - name: Install cosign | |
| uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 | |
| - name: Download and verify mcp-publisher | |
| # Two independent checks before anything is extracted or executed: | |
| # the pinned SHA-256, and the Sigstore bundle proving the archive was | |
| # built by the registry's own tagged release workflow. | |
| run: | | |
| set -euo pipefail | |
| base="https://github.com/modelcontextprotocol/registry/releases/download/${MCP_PUBLISHER_VERSION}" | |
| archive="mcp-publisher_linux_amd64.tar.gz" | |
| curl -fsSL -o "${archive}" "${base}/${archive}" | |
| curl -fsSL -o "${archive}.sigstore.json" "${base}/${archive}.sigstore.json" | |
| echo "${MCP_PUBLISHER_SHA256} ${archive}" | sha256sum --check --strict | |
| cosign verify-blob "${archive}" \ | |
| --bundle "${archive}.sigstore.json" \ | |
| --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ | |
| --certificate-identity "https://github.com/modelcontextprotocol/registry/.github/workflows/release.yml@refs/tags/${MCP_PUBLISHER_VERSION}" | |
| tar xzf "${archive}" mcp-publisher | |
| - name: Publish the server entry to the MCP Registry | |
| run: | | |
| set -euo pipefail | |
| ./mcp-publisher login github-oidc | |
| ./mcp-publisher publish |