diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0834813..07442f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,7 @@ jobs: rust_required: ${{ steps.classify.outputs.rust_required }} npm_required: ${{ steps.classify.outputs.npm_required }} workflow_required: ${{ steps.classify.outputs.workflow_required }} + claim_docs_required: ${{ steps.classify.outputs.claim_docs_required }} steps: - uses: actions/checkout@v4 with: @@ -52,6 +53,7 @@ jobs: rust_required="$force_all" npm_required="$force_all" workflow_required=false + claim_docs_required="$force_all" while IFS= read -r file; do [ -n "$file" ] || continue case "$file" in @@ -63,17 +65,38 @@ jobs: Cargo.toml|Cargo.lock|crates/*|examples/*|corpus/*|oracle/*) rust_required=true ;; - # npm package, launcher, and release helper changes require npm smoke. + # npm package and launcher changes require npm smoke. npm/*) npm_required=true ;; + # Claim-bearing docs/matrices validate public support and benchmark claims + # without forcing Rust/npm unless another changed path requires them. + docs/command-support-matrix.json|docs/command-benchmark-manifest.json|docs/TOOL_FEEDBACK.md|docs/EVALUATION_GATES.md|docs/COMMAND_SUPPORT_MATRIX.md|docs/COMMAND_BENCHMARK_MANIFEST.md|README.md) + claim_docs_required=true + ;; + # Named release/npm helpers get the smallest lane that proves their contract. + scripts/check-release-version.js|scripts/npm-publish-plan.js|scripts/npm-dist-tag-check.js|scripts/release-config.js) + npm_required=true + ;; + scripts/npm-preview-smoke.js|scripts/package-release.js|scripts/release-dry-run.sh) + rust_required=true + npm_required=true + ;; + scripts/validate-command-support-matrix.js) + claim_docs_required=true + ;; + scripts/verify.sh) + rust_required=true + npm_required=true + claim_docs_required=true + ;; scripts/*) rust_required=true npm_required=true ;; # Inert docs and decorative brand assets need only the lightweight gate. # Policy: assets/brand/** must not contain runtime/package/test inputs. - assets/brand/*|docs/*|README.md|AGENTS.md) + assets/brand/*|docs/*|AGENTS.md) ;; # Other GitHub metadata is validated by the lightweight aggregate gate. .github/*) @@ -91,6 +114,7 @@ jobs: printf 'rust_required=%s\n' "$rust_required" printf 'npm_required=%s\n' "$npm_required" printf 'workflow_required=%s\n' "$workflow_required" + printf 'claim_docs_required=%s\n' "$claim_docs_required" } >> "$GITHUB_OUTPUT" docs-check: @@ -111,6 +135,18 @@ jobs: git cat-file -e "$HEAD^{commit}" 2>/dev/null || git fetch --no-tags --depth=1 origin "$HEAD" git diff --check "$BASE" "$HEAD" + claim-docs-check: + runs-on: ubuntu-latest + needs: changes + if: ${{ needs.changes.outputs.claim_docs_required == 'true' }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + - name: Validate claim-bearing docs and matrices + run: node scripts/validate-command-support-matrix.js + workflow-check: runs-on: ubuntu-latest needs: changes @@ -126,7 +162,7 @@ jobs: shell: bash run: | set -euo pipefail - go run github.com/rhysd/actionlint/cmd/actionlint@latest .github/workflows/ci.yml + go run github.com/rhysd/actionlint/cmd/actionlint@latest .github/workflows/ci.yml .github/workflows/release.yml - name: Assert CI routing policy shell: bash run: | @@ -138,10 +174,14 @@ jobs: local rust_required="$force_all" local npm_required="$force_all" local workflow_required=false + local claim_docs_required="$force_all" while IFS= read -r file; do [ -n "$file" ] || continue case "$file" in - assets/brand/*|docs/*|README.md|AGENTS.md) + docs/command-support-matrix.json|docs/command-benchmark-manifest.json|docs/TOOL_FEEDBACK.md|docs/EVALUATION_GATES.md|docs/COMMAND_SUPPORT_MATRIX.md|docs/COMMAND_BENCHMARK_MANIFEST.md|README.md) + claim_docs_required=true + ;; + assets/brand/*|docs/*|AGENTS.md) ;; .github/workflows/*) workflow_required=true @@ -154,6 +194,21 @@ jobs: npm/*) npm_required=true ;; + scripts/check-release-version.js|scripts/npm-publish-plan.js|scripts/npm-dist-tag-check.js|scripts/release-config.js) + npm_required=true + ;; + scripts/npm-preview-smoke.js|scripts/package-release.js|scripts/release-dry-run.sh) + rust_required=true + npm_required=true + ;; + scripts/validate-command-support-matrix.js) + claim_docs_required=true + ;; + scripts/verify.sh) + rust_required=true + npm_required=true + claim_docs_required=true + ;; scripts/*) rust_required=true npm_required=true @@ -164,7 +219,7 @@ jobs: ;; esac done <<< "$changed_files" - printf 'workflow=%s rust=%s npm=%s' "$workflow_required" "$rust_required" "$npm_required" + printf 'workflow=%s claim_docs=%s rust=%s npm=%s' "$workflow_required" "$claim_docs_required" "$rust_required" "$npm_required" } assert_route() { @@ -181,14 +236,18 @@ jobs: printf 'routing assertion passed for %s: %s\n' "$name" "$actual" } - assert_route 'docs and decorative brand assets' $'assets/brand/mascot.png\ndocs/guide.md\nREADME.md' false 'workflow=false rust=false npm=false' - assert_route 'GitHub workflow edits' '.github/workflows/ci.yml' false 'workflow=true rust=false npm=false' - assert_route 'GitHub metadata edits' '.github/PULL_REQUEST_TEMPLATE.md' false 'workflow=false rust=false npm=false' - assert_route 'Rust workspace edits' 'crates/tfy-cli/src/main.rs' false 'workflow=false rust=true npm=false' - assert_route 'npm package edits' 'npm/tfy-cli/README.md' false 'workflow=false rust=false npm=true' - assert_route 'script edits' 'scripts/npm-preview-smoke.js' false 'workflow=false rust=true npm=true' - assert_route 'unknown root image edits' 'image.svg' false 'workflow=false rust=true npm=true' - assert_route 'uncertain new branch push' 'assets/brand/mascot.png' true 'workflow=false rust=true npm=true' + assert_route 'inert docs and decorative brand assets' $'assets/brand/mascot.png\ndocs/guide.md\nAGENTS.md' false 'workflow=false claim_docs=false rust=false npm=false' + assert_route 'claim-bearing docs and matrices' $'docs/COMMAND_SUPPORT_MATRIX.md\ndocs/command-support-matrix.json\nREADME.md' false 'workflow=false claim_docs=true rust=false npm=false' + assert_route 'GitHub workflow edits' '.github/workflows/ci.yml' false 'workflow=true claim_docs=false rust=false npm=false' + assert_route 'GitHub metadata edits' '.github/PULL_REQUEST_TEMPLATE.md' false 'workflow=false claim_docs=false rust=false npm=false' + assert_route 'Rust workspace edits' 'crates/tfy-cli/src/main.rs' false 'workflow=false claim_docs=false rust=true npm=false' + assert_route 'npm package edits' 'npm/tfy-cli/README.md' false 'workflow=false claim_docs=false rust=false npm=true' + assert_route 'release metadata helper edits' 'scripts/check-release-version.js' false 'workflow=false claim_docs=false rust=false npm=true' + assert_route 'claim validator edits' 'scripts/validate-command-support-matrix.js' false 'workflow=false claim_docs=true rust=false npm=false' + assert_route 'verify script edits' 'scripts/verify.sh' false 'workflow=false claim_docs=true rust=true npm=true' + assert_route 'script edits' 'scripts/npm-preview-smoke.js' false 'workflow=false claim_docs=false rust=true npm=true' + assert_route 'unknown root image edits' 'image.svg' false 'workflow=false claim_docs=false rust=true npm=true' + assert_route 'uncertain new branch push' 'assets/brand/mascot.png' true 'workflow=false claim_docs=true rust=true npm=true' rust: runs-on: ubuntu-latest @@ -231,16 +290,18 @@ jobs: ci-required: runs-on: ubuntu-latest if: ${{ always() }} - needs: [changes, docs-check, workflow-check, rust, npm-preview-smoke] + needs: [changes, docs-check, claim-docs-check, workflow-check, rust, npm-preview-smoke] steps: - name: Check required CI results shell: bash env: CHANGES_RESULT: ${{ needs.changes.result }} DOCS_CHECK_RESULT: ${{ needs.docs-check.result }} + CLAIM_DOCS_RESULT: ${{ needs.claim-docs-check.result }} WORKFLOW_RESULT: ${{ needs.workflow-check.result }} RUST_RESULT: ${{ needs.rust.result }} NPM_RESULT: ${{ needs.npm-preview-smoke.result }} + CLAIM_DOCS_REQUIRED: ${{ needs.changes.outputs.claim_docs_required }} WORKFLOW_REQUIRED: ${{ needs.changes.outputs.workflow_required }} RUST_REQUIRED: ${{ needs.changes.outputs.rust_required }} NPM_REQUIRED: ${{ needs.changes.outputs.npm_required }} @@ -256,6 +317,16 @@ jobs: exit 1 fi + if [ "$CLAIM_DOCS_REQUIRED" = "true" ]; then + if [ "$CLAIM_DOCS_RESULT" != "success" ]; then + echo "claim-docs-check required but result was $CLAIM_DOCS_RESULT" >&2 + exit 1 + fi + elif [ "$CLAIM_DOCS_RESULT" != "skipped" ]; then + echo "claim-docs-check should be skipped but result was $CLAIM_DOCS_RESULT" >&2 + exit 1 + fi + if [ "$WORKFLOW_REQUIRED" = "true" ]; then if [ "$WORKFLOW_RESULT" != "success" ]; then echo "workflow-check required but result was $WORKFLOW_RESULT" >&2 @@ -286,4 +357,4 @@ jobs: exit 1 fi - echo "ci-required passed; workflow_required=$WORKFLOW_REQUIRED rust_required=$RUST_REQUIRED npm_required=$NPM_REQUIRED" + echo "ci-required passed; claim_docs_required=$CLAIM_DOCS_REQUIRED workflow_required=$WORKFLOW_REQUIRED rust_required=$RUST_REQUIRED npm_required=$NPM_REQUIRED" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 093ce1d..62771d9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,6 +43,7 @@ jobs: prerelease: ${{ steps.metadata.outputs.prerelease }} npm_dist_tag: ${{ steps.metadata.outputs.npm_dist_tag }} target_sha: ${{ steps.ref.outputs.target_sha }} + release_tools_sha: ${{ steps.tools.outputs.release_tools_sha }} steps: - name: Resolve and validate source ref default id: defaults @@ -62,6 +63,7 @@ jobs: fi fi + # shellcheck disable=SC1083 # @{ is literal ref-syntax rejection, not shell expansion. if [[ ! "$source_ref" =~ ^(main|develop|release/[A-Za-z0-9._/-]+)$ ]] || [[ "$source_ref" == *..* ]] || [[ "$source_ref" == *//* ]] || [[ "$source_ref" == */ ]] || [[ "$source_ref" == *@{* ]]; then echo "source_ref contains unsupported characters or ref syntax" >&2 exit 1 @@ -90,6 +92,11 @@ jobs: ref: ${{ github.event.repository.default_branch }} path: release-tools + - name: Pin trusted release tooling revision + id: tools + shell: bash + run: printf 'release_tools_sha=%s\n' "$(git -C release-tools rev-parse HEAD)" >> "$GITHUB_OUTPUT" + - name: Checkout release source ref uses: actions/checkout@v4 with: @@ -166,7 +173,7 @@ jobs: - name: Checkout trusted release tooling uses: actions/checkout@v4 with: - ref: ${{ github.event.repository.default_branch }} + ref: ${{ needs.preflight.outputs.release_tools_sha }} path: release-tools - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 @@ -175,8 +182,6 @@ jobs: node-version: '20' - name: Full local gate run: ./scripts/verify.sh - - name: NPM helper tests - run: npm test --prefix npm/tfy-cli build: name: Build ${{ matrix.rust_target }} @@ -202,7 +207,7 @@ jobs: - name: Checkout trusted release tooling uses: actions/checkout@v4 with: - ref: ${{ github.event.repository.default_branch }} + ref: ${{ needs.preflight.outputs.release_tools_sha }} path: release-tools - uses: dtolnay/rust-toolchain@stable with: @@ -251,7 +256,7 @@ jobs: - name: Checkout trusted release tooling uses: actions/checkout@v4 with: - ref: ${{ github.event.repository.default_branch }} + ref: ${{ needs.preflight.outputs.release_tools_sha }} path: release-tools - uses: actions/setup-node@v4 with: @@ -302,6 +307,7 @@ jobs: npm dist-tag: ${{ needs.preflight.outputs.npm_dist_tag }} Source ref: ${{ needs.preflight.outputs.source_ref }} Target commit: ${{ needs.preflight.outputs.target_sha }} + Release tooling commit: ${{ needs.preflight.outputs.release_tools_sha }} This release provides GitHub Release archives and SHA-256 checksums for the npm installer. It does not change TFY claim boundaries: host launch support and token-saving claims still require route-bound evidence. EOF_NOTES @@ -345,6 +351,11 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ needs.preflight.outputs.target_sha }} + - name: Checkout trusted release tooling + uses: actions/checkout@v4 + with: + ref: ${{ needs.preflight.outputs.release_tools_sha }} + path: release-tools - uses: actions/setup-node@v4 with: node-version: '24' @@ -352,14 +363,17 @@ jobs: - name: Verify npm Trusted Publishing runtime run: | set -euo pipefail + # shellcheck disable=SC2016 # JavaScript template expressions must be passed literally to node. node -e 'const [major] = process.versions.node.split(".").map(Number); if (major < 24) throw new Error(`Node ${process.versions.node} is too old for npm Trusted Publishing`); console.log(`node ${process.versions.node}`);' NPM_VERSION="$(npm --version)" export NPM_VERSION + # shellcheck disable=SC2016 # JavaScript template expressions must be passed literally to node. node -e 'const version = process.env.NPM_VERSION.split(".").map(Number); const min = [11, 5, 1]; for (let i = 0; i < min.length; i += 1) { if (version[i] > min[i]) break; if (version[i] < min[i]) throw new Error(`npm ${process.env.NPM_VERSION} is too old for npm Trusted Publishing`); } console.log(`npm ${process.env.NPM_VERSION}`);' - name: Print npm publish plan run: | set -euo pipefail - node scripts/npm-publish-plan.js \ + node release-tools/scripts/npm-publish-plan.js \ + --root "$GITHUB_WORKSPACE" \ --version "${{ needs.preflight.outputs.version }}" \ --channel "${{ inputs.channel }}" \ --source-ref "${{ needs.preflight.outputs.source_ref }}" @@ -373,7 +387,7 @@ jobs: run: | set -euo pipefail args=( - node scripts/npm-dist-tag-check.js + node release-tools/scripts/npm-dist-tag-check.js --version "${{ needs.preflight.outputs.version }}" --channel "${{ inputs.channel }}" ) diff --git a/docs/RELEASE_READINESS.md b/docs/RELEASE_READINESS.md index 08530a4..f7956e7 100644 --- a/docs/RELEASE_READINESS.md +++ b/docs/RELEASE_READINESS.md @@ -54,7 +54,7 @@ Intel Mac (`darwin:x64` / `x86_64-apple-darwin`) is not provided as a prebuilt n Required local checks for the npm path: ```sh -./scripts/npm-preview-smoke.sh +node scripts/npm-preview-smoke.js ./scripts/release-dry-run.sh node scripts/npm-publish-plan.js --version 0.1.1-preview.0 --channel preview --source-ref develop node scripts/npm-publish-plan.js --version 0.1.1 --channel stable --source-ref main @@ -96,7 +96,7 @@ Metadata contract: Preview archive names intentionally omit the `-preview.N` suffix because the npm installer resolves the full tag while using base-version asset names. For example, `@ium/tfy-cli@0.1.1-preview.0` downloads from tag `v0.1.1-preview.0` and expects `tfy-0.1.1-linux-x86_64.tar.gz` plus `.sha256`. -The workflow fails closed when version metadata is inconsistent, when `source_ref` does not match the channel, when the tag or release already exists, or when any current npm-supported platform asset/checksum is missing. The supported matrix is shared with the npm installer in `npm/tfy-cli/scripts/lib/platform.js`. +The workflow fails closed when version metadata is inconsistent, when `source_ref` does not match the channel, when the tag or release already exists, or when any current npm-supported platform asset/checksum is missing. Release policy and publish-plan logic run from a trusted default-branch `release-tools` checkout pinned once during preflight, while reading immutable release-source metadata through explicit roots. The supported matrix is shared with the npm installer in `npm/tfy-cli/scripts/lib/platform.js`. Local preflight examples: diff --git a/docs/contributing/GIT_POLICY.md b/docs/contributing/GIT_POLICY.md index 3f4be5f..fec04d7 100644 --- a/docs/contributing/GIT_POLICY.md +++ b/docs/contributing/GIT_POLICY.md @@ -141,9 +141,11 @@ Not-tested: Branch protection settings were not changed through GitHub admin API ## CI branch-protection target -The CI workflow uses path-aware heavy-job skipping for docs and inert static assets. If branch protection is enabled, require the stable aggregate `ci-required` check rather than optional `rust` or `npm preview smoke (...)` jobs, because those heavy jobs intentionally skip for docs/static asset-only changes. +The CI workflow uses path-aware heavy-job skipping for inert docs and static assets. If branch protection is enabled, require the stable aggregate `ci-required` check rather than optional lane jobs such as `rust`, `claim-docs-check`, or `npm preview smoke (...)`, because those jobs intentionally skip when their lane is not required. -Do not require skipped optional heavy jobs directly unless the CI workflow is changed to make them always emit required statuses. +Claim-bearing docs and matrices, such as command-support matrix files and public claim docs, are not treated as inert docs: CI routes them through the lightweight `claim-docs-check` lane (`node scripts/validate-command-support-matrix.js`) without forcing Rust or npm heavy lanes unless another changed path requires them. + +Do not require skipped optional lane jobs directly unless the CI workflow is changed to make them always emit required statuses. `assets/brand/**` is reserved for decorative brand artwork and other non-runtime identity assets. Do not place packaged runtime assets, test fixtures, release inputs, generated config, or behavior-affecting files there; use the appropriate code/package/test directory so CI routing can require Rust or npm validation.