diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index e775f3a..f0699d7 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -209,6 +209,7 @@ jobs: env: DIST_TAG: ${{ needs.resolve.outputs.dist_tag }} VERSION: ${{ needs.resolve.outputs.version }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} shell: bash run: | set -euo pipefail @@ -216,7 +217,11 @@ jobs: echo "@operatorstack/create-yield@${VERSION} already exists" else file="$(jq -r '.archives[] | select(.name == "@operatorstack/create-yield") | .file' dist/release-unit/npm/npm-release.json)" - npm publish "dist/release-unit/npm/${file}" --tag "$DIST_TAG" + if [[ -n "${NPM_TOKEN:-}" ]]; then + NODE_AUTH_TOKEN="$NPM_TOKEN" npm publish "dist/release-unit/npm/${file}" --tag "$DIST_TAG" + else + npm publish "dist/release-unit/npm/${file}" --tag "$DIST_TAG" + fi fi - name: Verify complete npm release unit env: diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 1a68e3d..cea9d54 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -179,9 +179,16 @@ jobs: run: npm run test:conversion - name: Run every example fixture run: | - plan="$RUNNER_TEMP/example-release-plan.env" - node scripts/release-plan.mjs --bump auto --output "$plan" --notes "$RUNNER_TEMP/example-release-notes.md" - . "$plan" + base_tag="$(git tag --merged HEAD --list 'v[0-9]*' --sort=-v:refname | head -n1)" + test -n "$base_tag" + pending_changesets="$(git diff --name-only --diff-filter=A "$base_tag"..HEAD -- '.changeset/*.md')" + if [[ -n "$pending_changesets" ]]; then + plan="$RUNNER_TEMP/example-release-plan.env" + node scripts/release-plan.mjs --bump auto --output "$plan" --notes "$RUNNER_TEMP/example-release-notes.md" + . "$plan" + else + version="${base_tag#v}" + fi go build -ldflags "-X main.version=$version" -o "$RUNNER_TEMP/yskill" ./cmd/yskill "$RUNNER_TEMP/yskill" test examples/investigate "$RUNNER_TEMP/yskill" test examples/release-checklist diff --git a/scripts/check-release-control.mjs b/scripts/check-release-control.mjs index 5fbd3ca..0132e4c 100644 --- a/scripts/check-release-control.mjs +++ b/scripts/check-release-control.mjs @@ -302,8 +302,31 @@ export async function checkReleaseControl(root = resolve(import.meta.dirname, ". "crates.io publishing must not use a bootstrap token", ) for (const [name, text] of Object.entries(raw)) { + let credentialSurface = text + if (name === "npm-publish.yml") { + const initializerStart = text.indexOf(" - name: Publish npm initializer") + const initializerEnd = text.indexOf(" - name: Verify complete npm release unit") + expect( + initializerStart >= 0 && initializerEnd > initializerStart, + "npm bootstrap credential must be confined to the initializer step", + ) + const initializer = text.slice(initializerStart, initializerEnd) + expect( + initializer.includes("NPM_TOKEN: ${{ secrets.NPM_TOKEN }}"), + "the initializer bootstrap credential must be explicitly scoped", + ) + expect( + initializer.includes( + 'NODE_AUTH_TOKEN="$NPM_TOKEN" npm publish "dist/release-unit/npm/${file}"', + ), + "the bootstrap credential must authenticate only the verified initializer archive", + ) + credentialSurface = text.slice(0, initializerStart) + text.slice(initializerEnd) + } expect( - !/NPM_TOKEN|NODE_AUTH_TOKEN|PYPI_TOKEN|secrets\.(npm|pypi)|password:/i.test(text), + !/NPM_TOKEN|NODE_AUTH_TOKEN|PYPI_TOKEN|secrets\.(npm|pypi)|password:/i.test( + credentialSurface, + ), `${name}: long-lived registry credentials are forbidden`, ) }