Skip to content

Release

Release #4

Workflow file for this run

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"