From 652c4560933dc5598e09d083ed37d5f497b7a97c Mon Sep 17 00:00:00 2001 From: "Derek Palmer (Creative)" Date: Fri, 29 May 2026 16:18:45 -0400 Subject: [PATCH] ci(npm-publish): harden GitHub Packages publish job Add a pre-publish verification step to publish-gpr that asserts the scoped name mutation took effect (@derek-palmer/codeforerunner) and the tag matches package.json version, so a failed rename or mismatch can't ship a mislabeled scoped package. Add workflow tests covering the job's packages:write/token auth (not OIDC), scoped mutation, the verification guard, and that the scoped rename does not leak into the public npmjs publish job. Closes #47 --- .github/workflows/npm-publish.yml | 16 ++++++++++++++++ tests/test_workflows_yaml.py | 27 +++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index c9e60ea..e572303 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -51,6 +51,22 @@ jobs: scope: "@derek-palmer" - name: Scope package name for GitHub Packages run: npm pkg set name="@derek-palmer/codeforerunner" + - name: Verify scoped name and tag/version parity + # Guard against a failed/partial rename or a tag/version mismatch + # shipping a mislabeled scoped package. + run: | + set -e + TAG="${GITHUB_REF#refs/tags/v}" + NAME="$(node -e "process.stdout.write(require('./package.json').name)")" + VERSION="$(node -e "process.stdout.write(require('./package.json').version)")" + if [ "$NAME" != "@derek-palmer/codeforerunner" ]; then + echo "scoped name not applied: got '$NAME'" >&2 + exit 1 + fi + if [ "$TAG" != "$VERSION" ]; then + echo "tag ($TAG) does not match package.json version ($VERSION)" >&2 + exit 1 + fi - name: Publish to GitHub Packages run: npm publish --access public env: diff --git a/tests/test_workflows_yaml.py b/tests/test_workflows_yaml.py index 30e6e85..f0c9f2f 100644 --- a/tests/test_workflows_yaml.py +++ b/tests/test_workflows_yaml.py @@ -114,6 +114,33 @@ def test_npm_publish_workflow_uses_oidc_trusted_publishing(): assert "NODE_AUTH_TOKEN" not in publish_only +def test_github_packages_publish_job_is_scoped_and_isolated(): + wf = WORKFLOWS_DIR / "npm-publish.yml" + doc = yaml.safe_load(wf.read_text()) + jobs = doc["jobs"] + + gpr = jobs.get("publish-gpr") + assert isinstance(gpr, dict), "missing publish-gpr job" + # Runs after npmjs publish, with only packages:write (token auth, not OIDC). + assert gpr.get("needs") == "publish" + assert gpr.get("permissions", {}).get("packages") == "write" + assert "id-token" not in gpr.get("permissions", {}) + + gpr_text = yaml.dump(gpr) + # Scoped name mutation and GITHUB_TOKEN auth live in this job. + assert 'name="@derek-palmer/codeforerunner"' in gpr_text + assert "NODE_AUTH_TOKEN" in gpr_text + # Before publish, the job verifies the scoped name took effect and the tag + # matches the package version, so a failed mutation can't ship mislabeled. + assert "@derek-palmer/codeforerunner" in gpr_text + assert "GITHUB_REF" in gpr_text + + # The npmjs publish job must not perform the scoped-name mutation — the + # rename must not leak into the public npmjs artifact. + publish_text = yaml.dump(jobs.get("publish")) + assert "@derek-palmer/codeforerunner" not in publish_text + + def test_docker_publish_workflow_uses_version_tag_and_ghcr(): wf = WORKFLOWS_DIR / "docker-publish.yml" doc = yaml.safe_load(wf.read_text())