chore(ci): test on windows/macos and node 22/24; pin source bytes to LF - #91
chore(ci): test on windows/macos and node 22/24; pin source bytes to LF#91PrzemekGalarowicz wants to merge 6 commits into
Conversation
ci.yml ran one cell — ubuntu-latest x node 20 — while publish.yml built the
shipped artifact on node 24, so the release ran a node no test had ever seen,
and Windows/macOS had never executed a single test. The untested surface was
the repo's riskiest class: safeJoin/toPosix path handling, the symlink fixtures
across apply-update/backup/install-capabilities, records byte-equality, and the
spawn-based self-tests.
Five cells now, fail-fast: false so a red cell yields a full failure table
instead of aborting its siblings: ubuntu-latest x {20, 22, 24} plus
windows-latest and macos-latest on 24 — 24 carries the OS legs because 24 is
what publish ships. Each cell runs the same six independent gates, unchanged.
The explicit job name pins check identity to os + node, so a future matrix key
cannot silently rename all five required checks.
.gitattributes (* text=auto eol=lf) makes checked-out bytes deterministic on
every OS — the suite now contains tests that read the repo's own tracked files.
It is a no-op renormalization today (zero CRLF tracked), and it removes at the
source the class check-action-pins.mjs had already been forced to defend
against per-scanner.
check-soft-tier.mjs is the fence under "no soft tier": five cells make "mark
the red one experimental" a standing temptation, and a check that renders as
present while gating nothing is worse than no check. It refuses any
continue-on-error key, value-blind, so false and ${{ matrix.experimental }} are
refused alongside true and the verdict stays an integer count rather than a
judgment about untrusted file content. Named residuals: it cannot see branch
protection, and it does not read shell-level swallowing such as `|| true`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 53 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThe change expands CI to five OS and Node.js cells, normalizes text files to LF, adds a no-soft-tier workflow scanner, fixes Windows blocked-path classification, and records plan, verification, regression, review, and ship results. ChangesCI matrix and cross-platform path handling
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant GitHubActions
participant MatrixRunner
participant SetupNode
participant CICommands
GitHubActions->>MatrixRunner: create five OS and Node cells
MatrixRunner->>SetupNode: select matrix Node version
SetupNode->>CICommands: run the six-gate job body
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The Windows CI cell went red on its first run with three failures sharing one
root cause. readDiskState used lstatSync(dest, { throwIfNoEntry: false }), whose
suppression covers ENOENT only — documented in that line's own comment as
relying on a parent-is-a-file raising ENOTDIR instead. That is a POSIX-only
truth: Windows raises ENOENT for the same situation, so it WAS suppressed and
the path was classified `absent`.
User-visible on Windows: `pharn status` listed such a path under Missing,
implying it could be restored, when a regular file squats on its parent and it
cannot exist; `pharn update` then planned that restore and exited 1 instead of
emitting the named skip. The "Unreadable" drift category has been POSIX-only
correct since it shipped.
The split no longer reads an errno. It walks the components below the project
root and observes what they ARE — segment-wise from the root, the same idiom
assertNoSymlinkPath already uses, so containment stays structural (safeJoin per
step) rather than a lexical prefix compare.
A symlinked parent is deliberately NOT blocking. It does not mean the path
cannot exist, and deciding it here would turn a planned write into a silent skip
and dissolve the ApplyError partial-failure contract — caught by two previously
green tests when the first draft got this wrong. Refusing to write through a
symlink stays applyWrites' job.
No test file is edited: the three failing tests already assert the correct
behavior, which is what makes this a product fix rather than a test
accommodation. The blocking branch is unreachable on POSIX, so the Windows cell
is now this fix's regression test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
regress no-regressions, verify PASS, review GREEN with 5 advisory findings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
.dev/features/ci-matrix-os-node/SHIP.md (1)
41-43: 🔒 Security & Privacy | 🔵 TrivialVerify branch protection before merge.
The workflow now names five matrix checks instead of the previous single
checkjob. If branch protection still requirescheck, merges may block. If it does not require all five new names, a merge can bypass one or more matrix cells. Confirm both protected branches require these exact checks:
ubuntu-latest / node 20ubuntu-latest / node 22ubuntu-latest / node 24windows-latest / node 24macos-latest / node 24Evidence: the supplied
.github/workflows/ci.ymlsnippet defines this name template, and this file states that repository settings are unverified.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.dev/features/ci-matrix-os-node/SHIP.md around lines 41 - 43, Verify branch-protection settings for both protected branches and update required status checks to exactly include the five matrix check names: ubuntu-latest / node 20, ubuntu-latest / node 22, ubuntu-latest / node 24, windows-latest / node 24, and macos-latest / node 24. Remove any obsolete requirement for the former check job and record the verification in the standing item.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.dev/features/ci-matrix-os-node/REVIEW.md:
- Around line 28-33: Update the R1 coverage discussion in REVIEW.md to
distinguish the existing regular-file parent regression test, which verifies
readDiskState returns unreadable without throwing, from the Windows-only
parentBlocks true branch. State that POSIX tests cover the fallback path rather
than claiming the 643 tests say nothing about R1, and retain the limitation that
the Windows branch is exercised only by the Windows CI cell.
In @.dev/floor/check-soft-tier.mjs:
- Line 75: Update SOFT_KEY_RE and the surrounding scanner to recognize explicit
YAML mapping keys written as “? continue-on-error” with the value on a following
line before “: true”. Add a regression fixture covering this multiline form and
assert that it reports a violation.
- Around line 110-116: Update the directory enumeration logic around readdirSync
so failures for existing .github/workflows and .github/actions scan roots record
an unreadable-file violation for the affected path instead of treating it as
empty. Preserve normal sorted enumeration on success, and add regression
coverage for both scan roots.
In @.dev/floor/check-soft-tier.test.mjs:
- Around line 7-9: Protect the floor workflow by adding floor to the main
protection ruleset’s required checks and enforcing the no-soft-tier scan outside
pull-request-modifiable workflow files. In
.dev/floor/check-soft-tier.test.mjs:7-9, retain the ★ test and document it as a
residual until both protections exist; update
.dev/features/ci-matrix-os-node/PLAN.md:68-77 and CHANGELOG.md:92-100 to reflect
the required floor check and external enforcement.
In @.github/workflows/ci.yml:
- Around line 13-31: Restructure the CI workflow so Format check, Lint, Markdown
lint, Typecheck, Test, and Build each run as separately named jobs rather than
steps inside the five-cell matrix job. Preserve the matrix coverage where
applicable, assign stable explicit names to every gate job, and remove the
combined check’s role as the sole required status so branch protection can
require each gate independently.
---
Nitpick comments:
In @.dev/features/ci-matrix-os-node/SHIP.md:
- Around line 41-43: Verify branch-protection settings for both protected
branches and update required status checks to exactly include the five matrix
check names: ubuntu-latest / node 20, ubuntu-latest / node 22, ubuntu-latest /
node 24, windows-latest / node 24, and macos-latest / node 24. Remove any
obsolete requirement for the former check job and record the verification in the
standing item.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 8f5e3d73-e85f-4f30-b09b-1bcb2d3e67ca
📒 Files selected for processing (18)
.dev/features/ci-matrix-os-node/GRILL.md.dev/features/ci-matrix-os-node/PLAN.md.dev/features/ci-matrix-os-node/REGRESSION.md.dev/features/ci-matrix-os-node/REVIEW.md.dev/features/ci-matrix-os-node/SHIP.md.dev/features/ci-matrix-os-node/VERIFY.md.dev/features/ci-matrix-os-node/regression-report.json.dev/features/ci-matrix-os-node/verify-report.json.dev/floor/check-soft-tier.mjs.dev/floor/check-soft-tier.test.mjs.gitattributes.github/workflows/ci.yml.pharn/pharn-dev-regress/base-results.json.pharn/pharn-dev-regress/head-results.json.pharn/pharn-dev-verify/results.json.pharn/writes-scope.jsonCHANGELOG.mdsrc/lib/apply-update.ts
Run check-soft-tier.mjs directly in floor.yml, fail closed on unreadable scan roots, and document the ★ live-repo tests as residual until branch protection requires floor. Co-authored-by: Cursor <cursoragent@cursor.com>
Pin the matrix once via a YAML anchor so every gate job fans out identically, with explicit per-gate check names for branch protection. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
.dev/features/ci-matrix-os-node/PLAN.md (3)
119-123: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winUse precise P5 terminology.
Line 121 says the new logic never performs classification. The soft-tier regex classifies each scanned line as a violation or non-violation. Replace this with a narrower claim, such as “does not classify values or infer semantics.”
Otherwise, the determinism audit contradicts the described key-match behavior. The supplied plan describes the regex as the violation decision.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.dev/features/ci-matrix-os-node/PLAN.md around lines 119 - 123, Revise the P5 determinism audit in the “Determinism audit (P5)” section to avoid claiming that the logic never performs classification. State narrowly that it does not classify values or infer semantics, while preserving the description of check-soft-tier.mjs using the regex key-match to decide whether each scanned line is a violation.
113-117: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick winNarrow the trust-boundary claim.
check-soft-tier.mjsreads and regex-matches workflow content from the checked-out PR. Treat these files as untrusted data. The scanner does not execute workflow text, fetch remote artifacts, or make network calls. Remove the claim that “no decision reads their content.”
on: pull_requeststill executes fork-controlled code throughnpm ciandnpm run. Scope the guarantee to privileged access: fork runs havecontents: read, use no repository secrets, and setpersist-credentials: falseon each checkout.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.dev/features/ci-matrix-os-node/PLAN.md around lines 113 - 117, Revise the “Trust audit (P2)” section to treat checked-out workflow files scanned by check-soft-tier.mjs as untrusted data, removing the claim that no decision reads their content. State only that the scanner does not execute workflow text, fetch remote artifacts, or make network calls, and scope the pull_request guarantee to privileged access: contents: read, no repository secrets, and persist-credentials: false on every checkout.
46-67: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winUpdate the proposed
ci.ymlshape to match the independent gate jobs.Lines 47-67 still document one
checkjob with six steps. Document the six matrix jobs (format-check,lint,markdown-lint,typecheck,test, andbuild) with explicit gate-and-cellname:values. The current workflow emits 30 distinct gate/cell jobs; the plan must not describe the obsolete combined-job shape.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.dev/features/ci-matrix-os-node/PLAN.md around lines 46 - 67, Update the proposed ci.yml shape to document six independent matrix jobs named format-check, lint, markdown-lint, typecheck, test, and build rather than one combined check job. Give each job explicit name values that identify both its gate and matrix cell, and preserve the matrix OS/node combinations and required install gating so the plan reflects all 30 emitted gate/cell jobs.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.dev/features/ci-matrix-os-node/PLAN.md:
- Around line 74-76: Update the enforcement statement in the plan to describe
the main protection ruleset’s floor requirement as intended rather than active
enforcement. At HALT 2, record that the active ruleset currently has no required
status checks, including floor, while preserving the direct floor invocation and
residual scope.
In @.github/workflows/ci.yml:
- Around line 11-20: Move the x-ci-matrix anchor from the workflow root into
format-check.strategy, replacing that job’s existing matrix alias while
preserving the fail-fast and include entries. Keep strategy: *ci-matrix
unchanged in the other five jobs.
---
Outside diff comments:
In @.dev/features/ci-matrix-os-node/PLAN.md:
- Around line 119-123: Revise the P5 determinism audit in the “Determinism audit
(P5)” section to avoid claiming that the logic never performs classification.
State narrowly that it does not classify values or infer semantics, while
preserving the description of check-soft-tier.mjs using the regex key-match to
decide whether each scanned line is a violation.
- Around line 113-117: Revise the “Trust audit (P2)” section to treat
checked-out workflow files scanned by check-soft-tier.mjs as untrusted data,
removing the claim that no decision reads their content. State only that the
scanner does not execute workflow text, fetch remote artifacts, or make network
calls, and scope the pull_request guarantee to privileged access: contents:
read, no repository secrets, and persist-credentials: false on every checkout.
- Around line 46-67: Update the proposed ci.yml shape to document six
independent matrix jobs named format-check, lint, markdown-lint, typecheck,
test, and build rather than one combined check job. Give each job explicit name
values that identify both its gate and matrix cell, and preserve the matrix
OS/node combinations and required install gating so the plan reflects all 30
emitted gate/cell jobs.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: f5f3dec7-06c9-4e95-bdf7-b08da03c34f7
📒 Files selected for processing (6)
.dev/features/ci-matrix-os-node/PLAN.md.dev/floor/check-soft-tier.mjs.dev/floor/check-soft-tier.test.mjs.github/workflows/ci.yml.github/workflows/floor.ymlCHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (2)
- CHANGELOG.md
- .dev/floor/check-soft-tier.mjs
| - **Enforcement (outside the scanned workflow surface):** `floor.yml` runs `node .dev/floor/check-soft-tier.mjs .` directly (not only via its test file), and the `main protection` ruleset requires the `floor` status check. The `★ LIVE REPO-CONSISTENCY` block in `check-soft-tier.test.mjs` is retained as a third anti-vacuity residual on top of both wires. | ||
| - **Named residuals** (stated, never claimed closed): | ||
| - **R1 — other required checks are still a REPO-SETTINGS property.** This gate guarantees no workflow *asks* to be soft, and `floor` itself is now required on `main`; a *different* job left out of the required list (e.g. a matrix cell) remains invisible to this scanner. The operational note for those names still goes to the human by hand. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
gh api --paginate repos/pharn-dev/pharn-cli/rulesets \
--jq '.[] | select(.name == "main protection") |
{name, enforcement, required_checks: [
.rules[]? |
select(.type == "required_status_checks") |
.parameters.required_status_checks[]?.context
]}'Repository: pharn-dev/pharn-cli
Length of output: 228
🏁 Script executed:
#!/bin/bash
set -euo pipefail
gh api repos/pharn-dev/pharn-cli/rulesets \
--jq '.[] | select(.name == "main protection") |
{id, name, enforcement, bypass_actors, rules}'Repository: pharn-dev/pharn-cli
Length of output: 255
Correct the floor enforcement claim.
The active main protection ruleset has no required status checks, so it does not require floor. Describe floor as intended enforcement and record the actual required-check configuration at HALT 2.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.dev/features/ci-matrix-os-node/PLAN.md around lines 74 - 76, Update the
enforcement statement in the plan to describe the main protection ruleset’s
floor requirement as intended rather than active enforcement. At HALT 2, record
that the active ruleset currently has no required status checks, including
floor, while preserving the direct floor invocation and residual scope.
| # Shared 5-cell matrix — pinned once so every gate job fans out identically. | ||
| x-ci-matrix: &ci-matrix | ||
| fail-fast: false | ||
| matrix: | ||
| include: | ||
| - { os: ubuntu-latest, node: 20 } | ||
| - { os: ubuntu-latest, node: 22 } | ||
| - { os: ubuntu-latest, node: 24 } | ||
| - { os: windows-latest, node: 24 } | ||
| - { os: macos-latest, node: 24 } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
if command -v actionlint >/dev/null 2>&1; then
actionlint .github/workflows/ci.yml
else
echo "Install actionlint, then validate .github/workflows/ci.yml."
exit 1
fiRepository: pharn-dev/pharn-cli
Length of output: 406
🏁 Script executed:
#!/bin/bash
set -euo pipefail
cat -n .github/workflows/ci.yml
printf '\nWorkflow files:\n'
git ls-files '.github/workflows/*'
printf '\nMatrix and gate references:\n'
rg -n '^(x-ci-matrix|jobs:| [A-Za-z0-9_-]+:| strategy:| needs:)|matrix\.(os|node)|coverage|markdown|build|typecheck|lint|format' .github/workflows/ci.ymlRepository: pharn-dev/pharn-cli
Length of output: 6492
🌐 Web query:
GitHub Actions workflow YAML anchors aliases supported job strategy workflow root unexpected key documentation
💡 Result:
GitHub Actions supports basic YAML anchors (&) and aliases (*) within workflow files, enabling the reuse of specific configuration blocks [1][2]. However, this support is limited and does not include the full YAML specification—specifically, YAML merge keys (<<:) are not supported [3]. Key considerations regarding this support include: - Basic Reuse: You can use anchors and aliases to deduplicate repetitive configurations such as environment variables, entire job definitions, or step sequences [2][3]. - Absence of Merge Keys: GitHub Actions does not support the YAML merge key (<<:), which is often used in other CI/CD platforms to extend or override base configurations [3]. If you attempt to use merge keys, the workflow will fail validation [3]. - Error Messages: If you encounter an unexpected key or validation error related to anchors, ensure that you are not attempting to use merge keys [3]. While some older community discussions or legacy documentation may suggest that anchors are unsupported entirely, they are now officially supported for direct aliasing [1][2]. Errors in modern workflows regarding anchors are typically due to syntax that violates the supported subset (e.g., attempting a merge) rather than the presence of anchors themselves [3]. - Recommended Alternatives: For complex configuration sharing, such as needing to extend or override base settings, GitHub recommends using official Actions features instead of relying on YAML-level primitives. These include: - Reusable workflows for sharing entire jobs or complex logic [2][4]. - Composite actions for sharing sequences of steps [4][5]. - Matrices for dynamic job generation [4]. For authoritative guidance, refer to the official documentation on reusing workflow configurations [2][6].
Citations:
- 1: https://github.blog/changelog/2025-09-18-actions-yaml-anchors-and-non-public-workflow-templates/
- 2: https://docs.github.com/en/enterprise-cloud@latest/actions/reference/workflows-and-actions/reusing-workflow-configurations
- 3: https://frenck.dev/github-actions-yaml-anchors-aliases-merge-keys/
- 4: https://latchkey.dev/learn/github-actions/gha-anchor-alias-not-supported
- 5: https://latchkey.dev/learn/github-actions/github-actions-yaml-anchors-not-supported
- 6: https://docs.github.com/en/actions/concepts/workflows-and-actions/reusing-workflow-configurations
🏁 Script executed:
#!/bin/bash
set -euo pipefail
if ! command -v actionlint >/dev/null 2>&1; then
echo "actionlint is not available"
exit 1
fi
echo "actionlint version:"
actionlint -version || true
python3 - <<'PY' | actionlint -
from pathlib import Path
lines = Path(".github/workflows/ci.yml").read_text().splitlines()
out = []
i = 0
while i < len(lines):
if i == 10: # Remove the root-level matrix comment and x-ci-matrix block.
i = 20
continue
if i == 22: # Add the anchor at format-check.strategy.
out.extend([
" strategy: &ci-matrix",
" fail-fast: false",
" matrix:",
" include:",
" - { os: ubuntu-latest, node: 20 }",
" - { os: ubuntu-latest, node: 22 }",
" - { os: ubuntu-latest, node: 24 }",
" - { os: windows-latest, node: 24 }",
" - { os: macos-latest, node: 24 }",
])
if i == 27: # Remove format-check's old alias line.
i += 1
continue
out.append(lines[i])
i += 1
print("\n".join(out))
PYRepository: pharn-dev/pharn-cli
Length of output: 413
🏁 Script executed:
#!/bin/bash
set -euo pipefail
if ! command -v actionlint >/dev/null 2>&1; then
echo "actionlint is not available"
exit 1
fi
python3 - <<'PY' >/tmp/ci-corrected.yml
from pathlib import Path
src = Path(".github/workflows/ci.yml").read_text().splitlines()
out = []
i = 0
while i < len(src):
# Remove lines 11-20 from the current file.
if 10 <= i <= 19:
i += 1
continue
# Insert the anchor before format-check.name.
if i == 23:
out.extend([
" strategy: &ci-matrix",
" fail-fast: false",
" matrix:",
" include:",
" - { os: ubuntu-latest, node: 20 }",
" - { os: ubuntu-latest, node: 22 }",
" - { os: ubuntu-latest, node: 24 }",
" - { os: windows-latest, node: 24 }",
" - { os: macos-latest, node: 24 }",
])
# Remove format-check's existing strategy alias.
if i == 27:
i += 1
continue
out.append(src[i])
i += 1
print("\n".join(out))
PY
echo "Corrected workflow excerpt:"
nl -ba /tmp/ci-corrected.yml | sed -n '8,38p'
echo
actionlint /tmp/ci-corrected.ymlRepository: pharn-dev/pharn-cli
Length of output: 226
🏁 Script executed:
#!/bin/bash
set -euo pipefail
if ! command -v actionlint >/dev/null 2>&1; then
echo "actionlint is not available"
exit 1
fi
python3 - <<'PY' >/tmp/ci-corrected.yml
from pathlib import Path
text = Path(".github/workflows/ci.yml").read_text()
old_block = """# Shared 5-cell matrix — pinned once so every gate job fans out identically.
x-ci-matrix: &ci-matrix
fail-fast: false
matrix:
include:
- { os: ubuntu-latest, node: 20 }
- { os: ubuntu-latest, node: 22 }
- { os: ubuntu-latest, node: 24 }
- { os: windows-latest, node: 24 }
- { os: macos-latest, node: 24 }
"""
assert old_block in text
text = text.replace(old_block, "", 1)
needle = """ format-check:
# Explicit name pins check identity to gate + cell so branch protection can
"""
replacement = """ format-check:
strategy: &ci-matrix
fail-fast: false
matrix:
include:
- { os: ubuntu-latest, node: 20 }
- { os: ubuntu-latest, node: 22 }
- { os: ubuntu-latest, node: 24 }
- { os: windows-latest, node: 24 }
- { os: macos-latest, node: 24 }
# Explicit name pins check identity to gate + cell so branch protection can
"""
assert needle in text
text = text.replace(needle, replacement, 1)
old_strategy = " strategy: *ci-matrix\n"
assert text.count(old_strategy) == 6
text = text.replace(old_strategy, "", 1) # The first occurrence is format-check.
print(text, end="")
PY
echo "Corrected workflow excerpt:"
sed -n '1,45p' /tmp/ci-corrected.yml
echo
actionlint /tmp/ci-corrected.ymlRepository: pharn-dev/pharn-cli
Length of output: 1490
Move the matrix anchor to a supported workflow node.
GitHub Actions rejects x-ci-matrix at the workflow root. Move the matrix to format-check.strategy, replace its existing alias, and keep strategy: *ci-matrix in the other five jobs.
🧰 Tools
🪛 actionlint (1.7.12)
[error] 12-12: unexpected key "x-ci-matrix" for "workflow" section. expected one of "concurrency", "defaults", "env", "jobs", "name", "on", "permissions", "run-name"
(syntax-check)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/ci.yml around lines 11 - 20, Move the x-ci-matrix anchor
from the workflow root into format-check.strategy, replacing that job’s existing
matrix alias while preserving the fail-fast and include entries. Keep strategy:
*ci-matrix unchanged in the other five jobs.
Source: Linters/SAST tools
Probe PR. The ground truth for a CI matrix lives on the runners, not in the repo — so this is opened as a draft to read the actual cells before deciding the final shape.
What changed
.github/workflows/ci.ymlfail-fast: false.gitattributes* text=auto eol=lf.dev/floor/check-soft-tier.mjs+.test.mjscontinue-on-error, anywhereThe matrix:
ubuntu-latest× {20, 22, 24} +windows-latestandmacos-latest× 24.The OS-axis risk (fs, symlinks, paths, spawn) is node-version-independent and the node-axis risk (API drift) is OS-independent, so the cross-terms don't earn 9 cells. Node 24 carries the OS legs because 24 is what
publish.ymlships — the release has been running a Node no test had ever seen.Each cell runs the same six independent gates, unchanged, behind the existing
always() && steps.install.outcome == 'success'guard. Both action digests are reused byte-for-byte withpersist-credentials: false.The explicit job
name:is load-bearing, not decoration: without it GitHub derives each check's name from every matrix key, so adding a key later would silently rename all five required checks.Why
.gitattributesThe suite now contains tests that read the repo's own tracked bytes. This is a no-op renormalization today — zero CRLF in tracked files, and
git add --renormalize .rewrites zero pre-existing files — so it costs nothing and removes the class structurally.It is not hypothetical:
check-action-pins.mjs:57-64already carries aLINE_SPLIT_REwritten specifically because "core.autocrlfon Windows produces CRLF on checkout, this repo has no.gitattributes" — a fail-open that made everyuses:in a CRLF file invisible. This removes that root cause at the source instead of per-scanner.Why the soft-tier gate
Five cells make "just mark the red one
continue-on-error" a standing temptation. A yellow cell renders as present, gates nothing, and reads to every future contributor as covered. The rule existed as convention; this is the fence.Value-blind by design:
falseand${{ matrix.experimental }}are refused alongsidetrue, so the verdict staysviolations.length > 0— an integer test — instead of depending on an untrusted file's value.Named residuals, not claimed closed: it cannot see branch protection (R1), and it does not read shell-level swallowing like
|| true(R2).This sandbox cannot see repo settings, so the required-checks state is unverified. Both branches, verbatim:
checkjob (plausible — verify in Settings → Branches): this PR renames the checks to the five per-cell names, and the required list must be updated by hand, or merges either lose the gate or wedge on a check that no longer exists.Scope
M7 is the measurement arm of the node-version-policy ticket — these matrix results are its evidence base.
floor.yml'slts/*is that ticket's business and is not touched here, nor arepublish.yml,engines, or node/runner pinning.Two independent concerns land together (a measurement and an enforcement) — noted so a future bisect sees the coupling.
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Tests