Repository navigation
Release #4
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 | |
| # The only workflow that can publish to npm. Run it manually from main with an | |
| # existing, immutable release tag. It rebuilds and tests that tag, packs one | |
| # tarball, runs the package gate on it, and STAGES exactly that tarball via npm | |
| # trusted publishing (OIDC, with provenance) under a non-latest dist-tag. | |
| # | |
| # Staged versions are not public until a package owner approves them with 2FA | |
| # (`npm stage approve`). After approval, run the "Verify release" workflow. | |
| # Promotion to "latest" is a separate owner step; see RELEASING.md. | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: Existing release tag to publish (for example v1.1.0) | |
| required: true | |
| type: string | |
| dist_tag: | |
| description: npm dist-tag for the new version (never latest) | |
| required: true | |
| default: next | |
| type: choice | |
| options: [next] | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: npm-release | |
| cancel-in-progress: false | |
| env: | |
| PACKAGE: '@programcomputer/nasa-mcp-server' | |
| jobs: | |
| build: | |
| name: verify tag, test and pack | |
| if: github.repository == 'ProgramComputer/NASA-MCP-server' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| outputs: | |
| version: ${{ steps.meta.outputs.version }} | |
| sha: ${{ steps.meta.outputs.sha }} | |
| tarball: ${{ steps.pack.outputs.tarball }} | |
| integrity: ${{ steps.pack.outputs.integrity }} | |
| steps: | |
| - name: Validate inputs | |
| env: | |
| TAG: ${{ inputs.tag }} | |
| REF: ${{ github.ref }} | |
| run: | | |
| [[ "$REF" == "refs/heads/main" ]] || { echo "::error::Run this workflow from main (got $REF)"; exit 1; } | |
| [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]] || { echo "::error::tag must look like v1.2.3"; exit 1; } | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: refs/tags/${{ inputs.tag }} | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Verify tag, commit and version | |
| id: meta | |
| env: | |
| TAG: ${{ inputs.tag }} | |
| run: | | |
| set -euo pipefail | |
| sha=$(git rev-parse "refs/tags/$TAG^{commit}") | |
| [[ "$(git cat-file -t "refs/tags/$TAG")" == "tag" ]] || { echo "::error::$TAG must be an annotated tag"; exit 1; } | |
| git fetch --no-tags origin main | |
| git merge-base --is-ancestor "$sha" origin/main || { echo "::error::$TAG ($sha) is not on main"; exit 1; } | |
| version="${TAG#v}" | |
| [[ "$(node -p "require('./package.json').version")" == "$version" ]] || { echo "::error::package.json version does not match $TAG"; exit 1; } | |
| [[ "$(node -p "require('./package-lock.json').version")" == "$version" ]] || { echo "::error::package-lock.json version does not match $TAG"; exit 1; } | |
| grep -q "^## $version\$" CHANGELOG.md || { echo "::error::CHANGELOG.md has no '## $version' section"; exit 1; } | |
| echo "version=$version" >> "$GITHUB_OUTPUT" | |
| echo "sha=$sha" >> "$GITHUB_OUTPUT" | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: '24.x' | |
| - name: Version must not exist on npm | |
| env: | |
| VERSION: ${{ steps.meta.outputs.version }} | |
| run: | | |
| if npm view "$PACKAGE@$VERSION" version >/dev/null 2>&1; then | |
| echo "::error::$PACKAGE@$VERSION already exists on npm; published versions are never replaced"; exit 1 | |
| fi | |
| - run: npm ci | |
| - run: npm run lint | |
| - run: npm run typecheck | |
| - run: npm run clean && npm run build | |
| - run: npm run docs:check | |
| - run: npm test | |
| - name: Pack | |
| id: pack | |
| run: | | |
| set -euo pipefail | |
| npm pack --json > pack.json | |
| echo "tarball=$(node -p "require('./pack.json')[0].filename")" >> "$GITHUB_OUTPUT" | |
| echo "integrity=$(node -p "require('./pack.json')[0].integrity")" >> "$GITHUB_OUTPUT" | |
| - name: Package gate on the exact tarball | |
| env: | |
| TARBALL: ${{ steps.pack.outputs.tarball }} | |
| INTEGRITY: ${{ steps.pack.outputs.integrity }} | |
| run: node scripts/package-smoke.mjs --tarball "$TARBALL" --expect-integrity "$INTEGRITY" --report package-smoke-report.json | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: release-tarball | |
| path: | | |
| ${{ steps.pack.outputs.tarball }} | |
| pack.json | |
| package-smoke-report.json | |
| if-no-files-found: error | |
| retention-days: 90 | |
| stage: | |
| name: stage on npm (${{ inputs.dist_tag }}) | |
| needs: build | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| environment: npm-release | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: release-tarball | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: '24.x' | |
| registry-url: https://registry.npmjs.org | |
| - name: Check tooling and artifact identity | |
| env: | |
| TARBALL: ${{ needs.build.outputs.tarball }} | |
| INTEGRITY: ${{ needs.build.outputs.integrity }} | |
| VERSION: ${{ needs.build.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| node -e "const [a,b]=process.versions.node.split('.').map(Number);if(a<22||(a===22&&b<14))process.exit(1)" | |
| npm_version=$(npm --version) | |
| node -e "const [a,b,c]='$npm_version'.split('.').map(Number);if(a<11||(a===11&&(b<5||(b===5&&c<1))))process.exit(1)" || { echo "::error::npm >= 11.5.1 is required for trusted publishing (have $npm_version)"; exit 1; } | |
| npm stage --help >/dev/null 2>&1 || { echo "::error::npm $npm_version has no 'npm stage' command"; exit 1; } | |
| actual="sha512-$(openssl dgst -sha512 -binary "$TARBALL" | base64 -w0)" | |
| [[ "$actual" == "$INTEGRITY" ]] || { echo "::error::tarball integrity changed between build and stage"; exit 1; } | |
| if npm view "$PACKAGE@$VERSION" version >/dev/null 2>&1; then | |
| echo "::error::$PACKAGE@$VERSION already exists; not republishing"; exit 1 | |
| fi | |
| - name: Stage the tested tarball (trusted publishing, provenance) | |
| env: | |
| TARBALL: ${{ needs.build.outputs.tarball }} | |
| DIST_TAG: ${{ inputs.dist_tag }} | |
| run: npm stage publish "./$TARBALL" --tag "$DIST_TAG" --access public --provenance 2>&1 | tee stage-output.txt | |
| - name: Next steps | |
| env: | |
| VERSION: ${{ needs.build.outputs.version }} | |
| INTEGRITY: ${{ needs.build.outputs.integrity }} | |
| SHA: ${{ needs.build.outputs.sha }} | |
| TAG: ${{ inputs.tag }} | |
| DIST_TAG: ${{ inputs.dist_tag }} | |
| run: | | |
| { | |
| echo "## $PACKAGE@$VERSION staged (not public yet)" | |
| echo | |
| echo "- Release tag: \`$TAG\` (commit \`$SHA\`)" | |
| echo "- Tested tarball integrity: \`$INTEGRITY\`" | |
| echo "- dist-tag applied on approval: \`$DIST_TAG\`" | |
| echo | |
| echo '```' | |
| cat stage-output.txt | |
| echo '```' | |
| echo | |
| echo "1. Owner: \`npx -y npm@11 stage list $PACKAGE\`, then \`npx -y npm@11 stage approve <stage-id>\` (2FA)." | |
| echo "2. Run **Verify release** with tag \`$TAG\` and integrity \`$INTEGRITY\`." | |
| echo "3. Owner, after verification: \`npm dist-tag add $PACKAGE@$VERSION latest\`." | |
| } >> "$GITHUB_STEP_SUMMARY" |