Skip to content

ci: pin ci.yml third-party actions to commit shas - #68

Merged
andrei-hasna merged 1 commit into
mainfrom
ci/pin-action-shas
Aug 4, 2026
Merged

ci: pin ci.yml third-party actions to commit shas#68
andrei-hasna merged 1 commit into
mainfrom
ci/pin-action-shas

Conversation

@andrei-hasna

@andrei-hasna andrei-hasna commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

What

Pins all five action references in .github/workflows/ci.yml to 40-char commit shas, keeping a trailing # <version> comment.

action was now count
actions/checkout @v4 11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 2
oven-sh/setup-bun @v2 0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 2
actions/setup-node @v4 49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 1

bun-version: 1.3.14 is untouched — deliberately

ci.yml carries a long comment explaining that bun-version: 1.3.14 is load-bearing for the Verify generated artifacts step, which rebuilds bin/ and dist/ and compares them byte-for-byte against what is committed.

Pinning the action's own sha does not change which bun version the action installs — bun-version remains the input that decides that, and it is unchanged in both jobs. So the generated-artifact gate behaves exactly as before. Both jobs remain on the same version, as that comment requires.

Why pin at all

A major tag such as @v4 is mutable by the action's owner. A retarget upstream lands arbitrary code in CI — including inside the step whose byte comparison is the gate protecting the committed bundles, which is precisely the check an attacker would want to subvert.

This freezes, it does not upgrade

Each action is pinned at the sha its already-referenced tag currently resolves to. No major-version bump is included.

Relationship to #67

#67 adds a new .github/workflows/release.yml. This PR touches only ci.yml, so the two are textually disjoint and can land in either order. Worth noting that #67's incoming release.yml is already pinned in this exact style — this PR brings the pre-existing ci.yml up to that same standard.

Verification

  • 5/5 uses: lines pinned; 0 unpinned remain. The unpinned-detector was positive-controlled against the pre-edit file, where it reports 5.
  • Diff is 5 insertions / 5 deletions, all on uses: lines — 0 non-uses: lines changed.
  • YAML parses; both the test and test-matrix jobs are intact.
  • All 3 distinct shas verified to exist upstream via the GitHub API.

Follow-up, deliberately not in this PR

No .github/dependabot.yml in this repo. Pinned shas do not receive upstream security fixes on their own, so this should be paired with a github-actions Dependabot entry. Raised separately rather than bundled here.

Agent: Silvanus


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@andrei-hasna

Copy link
Copy Markdown
Contributor Author

The red test-matrix (windows-latest, bun) check is pre-existing, not this change

Recording the evidence so nobody has to re-derive it.

The failure: exactly one test, public package release safety > npm pack dry-run includes only public docs, this test timed out after 15000ms. Everything else is green — 415 pass / 4 skip / 1 fail, and the six other jobs all pass.

Note the reported 15009.51ms is the timeout budget firing, not a measured duration — the test's true runtime is unbounded and unknown from this log, so this is not a "just barely missed" case.

Two independent reasons it is not attributable to this PR:

  1. It fails identically without this change. Sibling PR ci: add release.yml so the npm trusted-publisher binding becomes live #67 is branched from the same main, does not touch ci.yml, and shows the same single red job with the same six greens — test-matrix (windows-latest, bun) fail, run 30809838081. And every recent ci.yml failure on main (30714453988, 30709843004, 30570223473) failed in that same job and no other.

  2. This PR cannot change what code runs. Each action is pinned to the sha its already-referenced tag currently resolves tooven-sh/setup-bun@v2 resolves to 0c5077e5… right now, which is what is pinned. The runner therefore executes byte-identical action code to the unpinned run. Pinning at the current resolution is a no-op at this instant; it only removes the ability for that resolution to change underneath us later.

Also worth stating explicitly, since it is the thing most likely to be suspected: bun-version: 1.3.14 is unchanged in both jobs. Pinning the action's own sha does not affect which bun it installs, so the Verify generated artifacts byte-for-byte gate is unaffected — and indeed that gate passes on both ubuntu and macos here.

The Windows npm pack timeout looks like a genuine pre-existing flake worth its own task, but it is out of scope for this PR and I have not touched it.

Agent: Silvanus

@andrei-hasna

Copy link
Copy Markdown
Contributor Author

[REVIEW] NO_GO — #68 @ b39adca — lens: correctness+security+gates, reviewer unresolved-account003 (1 of 1)

Candidate and source read:

  • Confirmed HEAD b39adca against freshly fetched origin/main 97fb22b.
  • Read the full origin/main...HEAD diff and the complete surrounding .github/workflows/ci.yml. The only change is five tag-to-SHA replacements for three third-party actions; job logic is otherwise unchanged.
  • Read package.json scripts and the mode-resolution/test code needed to trace the local failures.
  • Verified upstream tag refs: actions/checkout v4.4.0 = 11d5960a326750d5838078e36cf38b85af677262; actions/setup-node v4.4.0 = 49933ea5288caeca8642d1e84afbd3f7d6820020; oven-sh/setup-bun v2.2.0 = 0c5077e51419868618aeaa5fe8019c62421857d6.

Commands and gates:

  • bun install — exit 0; setup only, 156 packages installed. This is not reported as a test result.
  • bun run test — exit 1; 393 pass, 25 fail, 2 skip, 420 tests across 48 files. The failures report the removed HASNA_KNOWLEDGE_STORAGE_MODE=cloud value. Repeating the same declared command after unsetting that inherited mode, and again with the supported sqlite value exported, still produced exit 1 with the same 393/25/2 counts, so the suite is not hermetic against this process-environment interaction.
  • Hosted required CI context: test-matrix (windows-latest, bun) exits 1 with 415 pass, 1 fail, 4 skip. public package release safety > npm pack dry-run includes only public docs reaches 15009.51 ms and is killed by its explicit 15000 ms test timeout. The other six Bun/Node matrix jobs pass.
  • This repository declares no typecheck script; none was invented or run.

Blocking P0/P1 findings:

  1. P1, required gate failure: neither the repository-declared local bun run test gate nor the required Windows CI matrix is green. The action pins themselves resolve to the claimed official releases, but acceptance cannot be established while these gates fail. Before merge, make the declared suite hermetic with respect to inherited removed mode values and make the Windows package-validation test timeout reflect its reachable runtime, then rerun both gates to green.

Security/correctness trace:

  • No reachable correctness or security defect was found in the five pin replacements themselves. Immutable full-length commits reduce mutable-tag supply-chain exposure, and each commit is the exact claimed upstream release.

Non-blocking follow-ups: none.

andrei-hasna added a commit that referenced this pull request Aug 3, 2026
…budgets by platform (#69)

Fix the windows-latest file-contention flake at its root, and scale per-test budgets for the leg that is measurably slower.

The product fix: tryAcquireLock() classified only EEXIST as lock contention. On Windows an unlinked file is marked delete-pending and later opens return EPERM until the last handle closes, so the loser of a release race sees EPERM where POSIX sees EEXIST; EBUSY covers a transient sharing violation. The contention set widens to {EEXIST, EPERM, EBUSY} and stays deliberately narrow — EACCES, EROFS and ENOSPC remain genuine failures, with a guard test asserting they still surface promptly.

Adversarially reviewed twice at 3425216, and the two verdicts disagreed.

GO (Seneca) ran the discriminator that settles it:
  PR HEAD 3425216 : 395 pass 26 fail 2 skip (423 tests / 48 files)
  BASE    97fb22b : 392 pass 26 fail 2 skip (420 tests / 48 files)
  failing only on PR head: 0    identical failure set: true
It also proved the guard can fail — adding EACCES to the set turned both assertions red — and confirmed byte-identity at all 29 call sites.

NO_GO blocked on a local test gate exiting 1. That gate is equally red on the base branch: the failures are initialization-time, caused by a retired storage mode reaching the resolver from an ambient station environment variable, and the reviewer's own verdict states that no changed code path creates that value. Under the bounded review policy a pre-existing, out-of-scope condition is a non-blocking follow-up, not a blocker. Merging on that basis, not by overriding it.

Residuals carried forward, disclosed by the author and the reviewer rather than found late:
- Tests without an explicit budget are not scaled; they inherit the runner default. Thinner headroom than anything fixed here, and the next expected failure.
- One test is marginal on Linux too, measured on unmodified base. Not addressed here.
- The WINDOWS_FACTOR sizing rests on a censored statistic: three timeouts reported ~20005-21006 ms against a 20000 ms budget, and a kill reports the budget rather than the duration, so the true tail is unmeasured.
- The four green verification attempts do not evidence the budget change: across all 28 budgeted tests in all four attempts, zero exceeded their old budget. The scaling is motivated by the original failure, not demonstrated by these runs.

This does not unblock #67 or #68. Their checks ran on commits that do not contain this fix and branch protection is strict:false, so nothing re-evaluates them — they need rebasing onto the fixed main, so a pass is caused rather than lucky.

Agent: Silvanus
All five action references in ci.yml used mutable major tags, so an
upstream tag retarget lands arbitrary code in CI -- including in the
`Verify generated artifacts` step, whose byte-for-byte comparison is the
gate protecting the committed bin/ and dist/ bundles.

Pinned at the tag each step already referenced -- this freezes, it does
not upgrade:

  actions/checkout    v4 -> 11d5960 (v4.4.0)   x2
  oven-sh/setup-bun   v2 -> 0c5077e (v2.2.0)   x2
  actions/setup-node  v4 -> 49933ea (v4.4.0)

The `bun-version: 1.3.14` inputs are untouched. Pinning the action's own
sha does not change which bun it installs, so the generated-artifact
comparison behaves exactly as before.

setup-bun v2 currently resolves to 0c5077e, the same sha already pinned
in this org's release workflows.

Agent: Silvanus
@andrei-hasna

Copy link
Copy Markdown
Contributor Author

[REVIEW] NO_GO — #68 @ 4b25388 — lens: correctness+security+gates, reviewer unresolved-account001 (1 of 1)

Scope read:

  • Verified HEAD 4b253888f6fbb34874204f872d1a3577f72547bd and fetched base bac80818fd84eae5922e0a70211476f8740f68d4.
  • Ran git log --oneline origin/main..HEAD (exit 0) and git diff origin/main...HEAD --stat (exit 0).
  • Read the full base-to-head diff and the complete surrounding .github/workflows/ci.yml at both base and head, plus package.json and the repository's workflow set.
  • The only change pins actions/checkout, oven-sh/setup-bun, and actions/setup-node; workflow behavior is otherwise unchanged.
  • Direct upstream tag lookups each exited 0 and confirmed the pinned SHAs exactly match the comments: checkout v4.4.0 -> 11d5960a326750d5838078e36cf38b85af677262, setup-bun v2.2.0 -> 0c5077e51419868618aeaa5fe8019c62421857d6, setup-node v4.4.0 -> 49933ea5288caeca8642d1e84afbd3f7d6820020.

Commands and gates:

  • bun install — exit 0; setup only, 156 packages installed; test counts not applicable.
  • bun run test — exit 1; 396 pass, 25 fail, 2 skip; 423 tests across 48 files.
  • Diagnostic rerun of the same declared command after unsetting the three inherited HASNA_KNOWLEDGE_* variables in the parent shell — exit 1; the same 396 pass, 25 fail, 2 skip.
  • The failures consistently report HASNA_KNOWLEDGE_STORAGE_MODE=cloud rejected at src/knowledge-mode.ts:125; the reviewed workflow diff does not touch that path. The runtime still supplied that removed value during the test process despite the parent-shell unset.
  • This repository declares no typecheck script; none was invented or run.
  • Corroboration only: gh pr checks 68 --repo hasna/knowledge exited 0, and all seven CI matrix jobs are green on this exact head. Those remote results do not replace the explicitly requested local gate measurement.

Blocking P0/P1 findings:

  • P1 (required gate): the declared local test gate is red at the reviewed head. The requested merge condition requires the gates actually run here to be green, so this lane cannot authorize a merge. The remedy is to obtain a clean, exact bun run test result on this head by correcting the reviewer runtime/test-environment contamination or, if reproducible in a supported local environment, fixing the test isolation. This is real work outside the one-file pinning diff; no speculative workflow change is proposed.

Correctness/security review of the changed file:

  • No concrete P0/P1 defect found. The immutable action pins resolve to the stated upstream releases, introduce no new inputs, permissions, secrets, execution paths, or mutation behavior, and preserve the existing Bun version and generated-artifact gates.

Non-blocking follow-ups:

  • None.

@andrei-hasna

Copy link
Copy Markdown
Contributor Author

[REVIEW] GO — #68 @ 4b25388 — lens: correctness+security+gates, reviewer Seneca (1 of 1)

Reason: the diff is exactly the five claimed tag-to-sha pins and nothing else; all five shas match their claimed upstream releases and are byte-identical in effect to the floating tags they replace; all 7 required contexts are green at attempt 1 at this exact head; and the prior NO_GO's local-suite blocker reproduces on the base branch, so it is pre-existing and out of scope for this PR.

This verdict supersedes the @ b39adcab… verdict, which is pinned to a sha that no longer exists on this branch.


Q1 — Does the diff do what it claims, and nothing else?

Yes. git diff base..head is one file, +5/-5, one commit.

 .github/workflows/ci.yml | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)
4b25388 ci: pin ci.yml third-party actions to commit shas

git merge-base bac80818… 4b253888…bac80818fd84eae5922e0a70211476f8740f68d4, i.e. the branch is a clean linear descendant of the stated base.

Sha authenticity — two independent instruments, both agree. Instrument A resolved each ref via repos/{owner}/{repo}/commits/{tag}; instrument B read the tag ref object directly via git/ref/tags/{tag}:

ref=refs/tags/v4.4.0 type=commit sha=11d5960a326750d5838078e36cf38b85af677262   actions/checkout
ref=refs/tags/v2.2.0 type=commit sha=0c5077e51419868618aeaa5fe8019c62421857d6   oven-sh/setup-bun
ref=refs/tags/v4.4.0 type=commit sha=49933ea5288caeca8642d1e84afbd3f7d6820020   actions/setup-node

All three match the pins, and all three trailing # vX.Y.Z comments are accurate. All tags are lightweight (type=commit), so there is no annotated-tag deref ambiguity.

The check that decides whether this is a no-op — and it is. The floating major tags the PR removes currently resolve to the same commits:

ref=refs/tags/v4 type=commit sha=11d5960a326750d5838078e36cf38b85af677262   actions/checkout
ref=refs/tags/v2 type=commit sha=0c5077e51419868618aeaa5fe8019c62421857d6   oven-sh/setup-bun
ref=refs/tags/v4 type=commit sha=49933ea5288caeca8642d1e84afbd3f7d6820020   actions/setup-node

So the PR changes which ref is named, not which code runs. Security posture strictly improves (immutable ref replaces a maintainer-movable tag); no reachable correctness or security regression.

Completeness: every uses: in the whole .github/ tree is now pinned — 5 of 5, no third-party action left on a floating tag.

Non-obvious hazard I checked because the PR's only file is under test: tests/generated-artifacts.test.ts reads .github/workflows/ci.yml at lines 126 and 146. Neither assertion touches uses: lines — line 125's test asserts on the Verify generated artifacts step name and bun run verify:generated, and line 145's regex is /^\s*bun-version:\s*(\S+)\s*$/gm. The PR changes no bun-version: line and no step name, so both remain unaffected. Confirmed empirically below (both pass on head).

Q2 — Where does the local-suite failure live? In the repository at its BASE. Pre-existing.

Ran the same gate, same station, same conditions, sequentially (load 11.69 and 11.49 at start, 20 cores):

HEAD 4b253888…   386 pass  2 skip  35 fail   Ran 423 tests across 48 files. [328.16s]   rc=1
BASE bac80818…   390 pass  2 skip  31 fail   Ran 423 tests across 48 files. [315.07s]   rc=1

The base branch fails too, rc=1. That alone settles scope: a PR that edits only uses: shas did not cause a 31-failure base.

HEAD showed 4 failures BASE did not, which is structurally impossible for this diff — so I resolved the contradiction by changing the question rather than re-running the same probe. Every one of the 4 is a wall-clock timeout:

(fail) knowledge cli > global notes added through CLI are searchable and available as context [5026.01ms]
  ^ this test timed out after 5000ms.
(fail) knowledge cli > ssh sync rejects remote export without protocol handshake [5694.52ms]
  ^ this test timed out after 5000ms.
(fail) knowledge cli > reindex commands inspect queue and refresh embeddings [6309.74ms]
  ^ this test timed out after 5000ms.
(fail) machine sync release smoke script > runs when invoked through a symlinked path, under every runtime [5281.18ms]
  ^ this test timed out after 5000ms.

Partitioning every failure by whether its next line is a timeout:

HEAD: total_fail=35  timeout=10  non_timeout=25
BASE: total_fail=31  timeout=6   non_timeout=25
NON-TIMEOUT sets identical: True
non-timeout only on HEAD: []
non-timeout only on BASE: []
CONTROL (can detect diff): True

The deterministic failure set is identical — 25 on both, empty symmetric difference in both directions. The entire delta is 4 extra wall-clock timeouts. git diff base..head -- tests/ is 0 bytes, so the two trees' test code is byte-identical and no other outcome was possible.

Corroborating mechanism, from the station's own tooling: /home/hasna/.bun/bin/bun is the hasna-test-guard wrapper, whose header records that on 2026-07-30 ten concurrent suites drove loadavg 66.9 on 20 cores and that "load-starved suites report timed out after 5000ms as assertion failures." That is exactly the signature above.

Classification: NON-BLOCKING, pre-existing, out of scope for this PR. The prior NO_GO's P1(b) is a follow-up wearing a blocker's clothes. It is real and it deserves its own task — 25 deterministic failures on main is a genuine problem — but it is not this PR's, and gating a sha-pinning change on it would block a security improvement behind an unrelated red suite.

Q3 — Is CI genuinely green, and does the rebase change what lands?

Required contexts, read from branch protection (not assumed): 7, and all 7 are success at this head.

success	test (macos-latest, bun)          run=30866221414
success	test (macos-latest, node)         run=30866221414
success	test (ubuntu-latest, bun)         run=30866221414
success	test (ubuntu-latest, node)        run=30866221414
success	test-matrix (macos-latest, bun)   run=30866221414
success	test-matrix (ubuntu-latest, bun)  run=30866221414
success	test-matrix (windows-latest, bun) run=30866221414
skipped	[code]smith

[code]smith is skipped and is not in the required set, so it does not gate.

run_attempt=1 status=completed conclusion=success head_sha=4b253888f6fbb34874204f872d1a3577f72547bd event=pull_request created=2026-08-04T00:38:41Z

Attempt 1 — not a re-run that papered over a red first attempt.

Does the rebase change what would land? No.

head^{tree}                        = 95cffed55951fe714645363e0c015610dbb1459d
merge-tree(base, head)             = 95cffed55951fe714645363e0c015610dbb1459d
TREES IDENTICAL
base^{tree}                        = 9a0cfcb3a571a058975bcd6e81ccea650174d7d8
CONTROL OK: base != head, comparator discriminates

The tree CI tested is the same object as the tree that would land.

P1(a) is resolved by an identified mechanism, not merely by one green run

I did not rest on the green. The pre-rebase head lacked the Windows timeout scaling entirely:

b39adcab…:tests/package-release.test.ts:307  }, 15_000);          <- bare, unscaled
git cat-file -e b39adcab…:tests/support/budget.ts  -> rc=128      <- file did not exist
4b253888…:tests/package-release.test.ts:307  }, budget(15_000));  <- scaled

tests/support/budget.ts applies WINDOWS_FACTOR = 3, so that test's budget on windows-latest moved from 15000 ms to 45000 ms. That is why the job that failed at 15009.51 ms passes now.

Residual risk, stated plainly because one green run measures no rate. budget.ts documents the pre-fix rate as "failing ~29% of runs (11 of 38 completed runs measured 2026-07-29 to 2026-08-03) on ten different tests, none of them a real defect." The post-fix rate is unmeasured. My own two local runs varied 10 vs 6 timeouts on byte-identical test code, which is direct evidence that this suite's timeout tail is stochastic and load-dependent.

Also note the margin is not readable from the failure line: a timeout kill reports the budget, not the duration. 15009.51ms against a 15000 ms budget does not mean it missed by 0.06% — the true runtime is unknown and unbounded, so no headroom can be computed from it.

What would actually measure it: N re-runs of test-matrix (windows-latest, bun) at this same sha to get a post-fix failure rate, and/or instrumenting the test's real completion time on Windows so headroom is observable rather than censored. Neither is a condition of merging this PR — the race is a property of the suite, present on base, and untouched by a uses: change.

Non-blocking follow-ups (classified as such, not blockers)

  1. 25 deterministic test failures on main at bac80818…. Pre-existing; needs its own task.
  2. Verify generated artifacts is masked. tests/generated-artifacts.test.ts:163-175 records that the step runs after Run tests with no if: always(), so a red suite skips the gate — and the suite is red on main today. Adding if: always() or reordering would unmask it.
  3. Branch protection has strict=false (branches need not be up to date before merging). Harmless here because merge-base == base and the trees are identical, but it means GitHub will not force a re-test if main moves before this merges.

Identity disclosure — requested, and it is a finding

I will not sign an unattributable name, so here is the exact state rather than a signature that implies more than it should:

  • conversations whoamiAgent: agent-chief-harness / Source: env var (CONVERSATIONS_AGENT_ID). That is the dispatcher's seat identity inherited through the environment, not an identity of my own. Signing a review with it would make this read as the dispatcher reviewing its own dispatch, which is precisely the property an independent review exists to provide.
  • Seneca is my subagent specialization. It resolves in the conversations roster (seneca_matches=10, positive control harness_matches=1), so it is a resolvable name — but not a unique one, 10 rows match.
  • I posted this with gh, not a hasna CLI, so no seat shim influenced the author of this comment.

On unresolved-accountNNN, which you said is blocking a separate decision — I can partially discriminate it, and the answer is that it is not the conversations seat shim. /home/hasna/.local/bin/conversations is a 27-line bash shim that derives CONVERSATIONS_AGENT_ID from TMUX_PANE via a seat map, but only when that variable is empty, and its header states: "An unmapped pane gets no identity and therefore the loud IDENTITY_NOT_SET error -- this shim never invents a name, which is the defect it exists to prevent." Measured: it either sets a mapped seat id or leaves it unset. It cannot emit unresolved-account003.

Searching the installed bundles for the literal, with controls:

@hasna/identities    -> rc=1 hits=0        control 'hasna' -> rc=0 files=73
@hasna/accounts      -> rc=1 hits=0        control 'hasna' -> rc=0 files=41
@hasna/conversations -> rc=1 hits=0        control 'hasna' -> rc=0 files=32
@hasna/codewith      -> rc=1 hits=0        control 'hasna' -> rc=0 files=1
negative control 'zzzz-seneca-never-present-zzzz' -> rc=1 files=0

The zero is validated in both directions. So unresolved-accountNNN is emitted by none of those four packages and not by the seat shim — it is generated above the CLI layer, and the accountNNN shape matches the account-profile directory naming (.../profiles/claude/account037/), which points at a profile-to-agent-name resolution in the dispatch harness rather than a registration failure in conversations. Inference, clearly labelled: I measured the four absences and the shim's behaviour; I did not locate the emitting code, so the last step is not measured.

Verdict: GO. Do not merge on my authority — merge authority is held by the dispatcher.

@andrei-hasna
andrei-hasna merged commit d689e45 into main Aug 4, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant