Skip to content

release: v4.11.0 (v5.0 model-engineering lane Phases 1–4 — 6 skills, … #21

release: v4.11.0 (v5.0 model-engineering lane Phases 1–4 — 6 skills, …

release: v4.11.0 (v5.0 model-engineering lane Phases 1–4 — 6 skills, … #21

Workflow file for this run

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."