Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 86 additions & 15 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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/*)
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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: |
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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() {
Expand All @@ -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
Expand Down Expand Up @@ -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 }}
Expand All @@ -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
Expand Down Expand Up @@ -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"
28 changes: 21 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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 }}
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -345,21 +351,29 @@ 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'
registry-url: 'https://registry.npmjs.org'
- 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 }}"
Expand All @@ -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 }}"
)
Expand Down
4 changes: 2 additions & 2 deletions docs/RELEASE_READINESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:

Expand Down
6 changes: 4 additions & 2 deletions docs/contributing/GIT_POLICY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Loading