release: v4.11.0 (v5.0 model-engineering lane Phases 1–4 — 6 skills, … #21
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 | |
| # Triggered when a version tag is pushed (e.g., v4.7.0). | |
| # Builds the classroom installer ZIPs (with an injected, verified provenance.json), attests their | |
| # build provenance, verifies they are consumable by the self-updater, creates a GitHub Release with | |
| # the ZIPs attached, and publishes the npm package (idempotently, with npm provenance) so the | |
| # `npx medsci-skills@latest install` channel never drifts behind the GitHub release. Zenodo | |
| # automatically archives the GitHub Release if the integration is enabled at | |
| # https://zenodo.org/account/settings/github/ | |
| # | |
| # The npm publish step runs only when the `NPM_TOKEN` repo secret is set (a granular/automation token | |
| # scoped to medsci-skills with publish rights + 2FA bypass), and skips if that version is already on | |
| # npm, so re-running a tag is safe. It runs AFTER the GitHub Release so an npm hiccup never blocks it. | |
| # | |
| # Supply-chain posture (see SECURITY.md "Release integrity & revocation"): the self-updater verifies | |
| # each download's sha256 against the github.com API digest, which detects transport/asset tampering | |
| # only — it does NOT defend against a compromised publisher account. Defenses added here: | |
| # * a protected `release` environment (configure a required reviewer in repo Settings → Environments | |
| # so a human approves before any release is published); | |
| # * least-privilege token scopes; | |
| # * a version-consistency gate (CITATION == package.json == manifest == the pushed tag); | |
| # * build-provenance attestation of the ZIP artifacts; | |
| # * a post-build check that each ZIP round-trips through the updater's own safe-extract + provenance | |
| # validation, so a release can never ship a ZIP the updater would refuse. | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| # Least privilege: contents:write to publish the release; id-token + attestations for the build | |
| # provenance attestation. Nothing else. | |
| permissions: | |
| contents: write | |
| id-token: write | |
| attestations: write | |
| jobs: | |
| release: | |
| runs-on: ubuntu-latest | |
| # Human gate: add a required reviewer to the `release` environment (repo Settings → Environments) | |
| # so publishing a release requires explicit approval. Harmless before that setting exists. | |
| environment: release | |
| env: | |
| # 'true' only when the secret exists, so the npm steps are skipped on forks / before setup. | |
| HAS_NPM_TOKEN: ${{ secrets.NPM_TOKEN != '' }} | |
| steps: | |
| - name: Checkout repository (with full history for tag annotation) | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.11" | |
| - name: Version-consistency gate (CITATION == package.json == manifest == pushed tag) | |
| run: | | |
| set -euo pipefail | |
| # CITATION == package.json == manifest (three-source version agreement)... | |
| python3 scripts/check_version_consistency.py | |
| # ...and the tracked distribution_files.json inventory still matches the on-disk tree, so | |
| # the inventory the ZIP bundles (which the updater trusts) is anchored to the real source. | |
| python3 scripts/gen_distribution_manifest.py --check | |
| TAG="${GITHUB_REF_NAME}" | |
| VER="${TAG#v}" | |
| MAN="$(python3 -c 'import json;print(json.load(open("metadata/distribution_manifest.json"))["version"])')" | |
| if [ "$VER" != "$MAN" ]; then | |
| echo "::error::pushed tag ${TAG} (version ${VER}) != distribution_manifest version ${MAN}. Bump CITATION.cff/package.json/manifest before tagging." >&2 | |
| exit 1 | |
| fi | |
| echo "version gate OK: ${TAG} == manifest ${MAN}" | |
| - name: Build classroom release ZIPs (with verified provenance.json) | |
| run: | | |
| set -euo pipefail | |
| BUILT_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)" | |
| python3 scripts/build_classroom_release.py \ | |
| --tag "${GITHUB_REF_NAME}" \ | |
| --git-sha "${GITHUB_SHA}" \ | |
| --built-at "${BUILT_AT}" | |
| - name: Verify ZIPs are consumable by the self-updater (provenance + safe-extract round-trip) | |
| run: | | |
| set -euo pipefail | |
| for zip in dist/medsci-skills-classroom-*.zip; do | |
| python3 scripts/check_release_zip.py --zip "$zip" \ | |
| --expect-tag "${GITHUB_REF_NAME}" --require-provenance | |
| done | |
| - name: List built artifacts | |
| run: | | |
| ls -lh dist/ | |
| echo "---" | |
| echo "ZIP contents preview:" | |
| for zip in dist/*.zip; do | |
| echo "## ${zip}" | |
| unzip -l "${zip}" | head -20 | |
| done | |
| - name: Attest build provenance of the release ZIPs | |
| uses: actions/attest-build-provenance@v2 | |
| with: | |
| subject-path: "dist/medsci-skills-classroom-*.zip" | |
| - name: Extract release notes from CHANGELOG.md | |
| id: notes | |
| run: | | |
| TAG="${GITHUB_REF_NAME}" | |
| # Tags are "vX.Y.Z" but CHANGELOG headers are "## [X.Y.Z]" (no leading v). | |
| # Strip the v so the section is actually found (else notes fall back). | |
| VER="${TAG#v}" | |
| # Extract the section for this version from CHANGELOG.md (between this version header and the next version header) | |
| if [ -f CHANGELOG.md ]; then | |
| awk -v ver="$VER" ' | |
| $0 ~ "^## \\[?"ver"\\]?" { found=1; next } | |
| found && /^## / { exit } | |
| found { print } | |
| ' CHANGELOG.md > /tmp/release_notes.md | |
| else | |
| echo "Release ${TAG}" > /tmp/release_notes.md | |
| fi | |
| # Fallback if CHANGELOG section was empty | |
| if [ ! -s /tmp/release_notes.md ]; then | |
| echo "Release ${TAG}" > /tmp/release_notes.md | |
| echo "" >> /tmp/release_notes.md | |
| echo "See [CHANGELOG.md](CHANGELOG.md) for details." >> /tmp/release_notes.md | |
| fi | |
| echo "Release notes preview:" | |
| cat /tmp/release_notes.md | |
| - name: Create GitHub Release with classroom ZIP attached | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # Publish the SAME classroom artifacts that were verified (line ~75) and attested (line ~97); | |
| # the glob is identical so a release can only ship a ZIP that round-tripped + was attested. | |
| # Nothing modifies dist/ between those steps and this upload, so the API sha256 the updater | |
| # later checks covers exactly the verified bytes. | |
| run: | | |
| TAG="${GITHUB_REF_NAME}" | |
| gh release create "$TAG" \ | |
| --title "MedSci Skills ${TAG}" \ | |
| --notes-file /tmp/release_notes.md \ | |
| dist/medsci-skills-classroom-*.zip | |
| # --- npm publish (last, gated on NPM_TOKEN, idempotent) so a npm issue never blocks the release --- | |
| - name: Set up Node for npm publish | |
| if: env.HAS_NPM_TOKEN == 'true' | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| registry-url: "https://registry.npmjs.org" | |
| - name: Publish to npm (idempotent; npm provenance via OIDC) | |
| if: env.HAS_NPM_TOKEN == 'true' | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| VER="${GITHUB_REF_NAME#v}" | |
| # Idempotent: re-running a tag must not fail on "version already published". | |
| if npm view "medsci-skills@${VER}" version >/dev/null 2>&1; then | |
| echo "medsci-skills@${VER} is already on npm — skipping publish." | |
| exit 0 | |
| fi | |
| # --provenance links the published package to this GitHub Actions build via OIDC | |
| # (uses the id-token permission already granted above). | |
| npm publish --provenance --access public | |
| echo "Published medsci-skills@${VER} to npm." |