Skip to content

[sec-check] validate:end-user-talks is missing from ci.yml, so the required status check never gates the URLs EndUserTalkSchedule renders as hrefs #1758

Closed

Description

@hivecommons-hive

Finding and current P1 scope

The required Validate repository job omits validate:end-user-talks, even
though the validator is part of validate:all and protects the schedule links
rendered by EndUserTalkSchedule. The Netlify publish gate already runs it,
so the consequence is bad data reaching main and blocking deployment, not a
demonstrated live-site bypass.

The original two-edit recommendation was incomplete. The CI validator set is
also coupled to the Justfile recipe and its contributor documentation. The
existing #1776 was completed in place under the maintainer's
explicit P1 authorization, rather than opening a competing implementation.

Complete fix

  • Justfile: run npm run validate:end-user-talks in build.
  • CONTRIBUTING.md: list the validator in the build recipe and remove the
    obsolete statement that no Justfile recipe runs it.
  • .github/workflows/ci.yml: run npm run validate:end-user-talks in the
    required validation job.
  • tests/netlify-publish-contract.test.mjs: enforce parity in both
    directions between the CI validators and validate:all.
  • Retain the existing recipe/documentation parity checks and repair the
    reflow-sensitive fixture anchor alongside the documentation change.
  • Retire the obsolete omission in tests/workflow-scripts.test.mjs now
    that CI invokes the validator directly.
  • Required checks pass and the complete fix merges through the protected
    queue.

The new reverse-parity assertion was exercised before the workflow fix and
failed specifically on validate:end-user-talks. The complete repair passes
122 focused cases, the required full unit coverage gate (3,027 tests), all
six lint gates and the production build. The complete fix merged through the
protected queue at 2026-10-22T13:59:57Z as
fd23cd74b2c2fcbf36038b4dcbe81b5af066d908; GitHub closed this issue.

Maintainer disposition

The maintainer approved repairing the existing PR and delegated its eventual
hold release and protected merge under the documented sole-maintainer rule.
This does not waive required checks or represent agent review as independent
human approval.

Activity

  1. added
    securityApproved by a Hive merger/owner for auto-merge on green CI
    agent/sec-checkCreated by Hive for agent-filed issue provenance
    stage/010-actionableTriaged; eligible for the hive to pick up
    on Oct 21, 2026
  2. hivecommons-hive commented on Oct 22, 2026

    @hivecommons-hive
    ContributorAuthor

    Folded finding. This was filed as a separate issue, but 1 open agent-filed issue (plus 3 closed as completed in the last 14 days) already cite github/workflows/ci.yml, so it was folded into the oldest of them instead of opening another. Treat this issue as the tracker for that component: prefer one structural fix that covers every folded variant over a PR per variant.

    [quality] Extend the merged callable-core mutation pattern to scripts/validate-architecture-assets.mjs

    Finding

    stryker.config.mjs sets:

    mutate: ['src/**/*.{js,mjs}', 'scripts/lib/**/*.mjs'],

    Every top-level scripts/*.mjs is outside that scope. Measured at b490a7f
    with the repository's own installed Stryker:

    $ TZ=UTC npx stryker run --mutate 'scripts/*.mjs'
    INFO ProjectReader   Found 29 of 771 file(s) to be mutated.
    INFO Instrumenter    Instrumented 29 source file(s) with 4410 mutant(s)
    
    $ TZ=UTC npx stryker run --mutate scripts/validate-architecture-assets.mjs
    INFO Instrumenter    Instrumented 1 source file(s) with 325 mutant(s)
    

    npm run test:mutation generates none of those 4410 mutants.

    This matters because the unit lane can no longer tell an asserted rule from an
    unasserted one in that tree. npm run test:unit:coverage:check (node v26.12.0,
    rev b490a7f) reports scripts/ and src/ at 100.00% lines / 100.00%
    regions
    — the gate is saturated, which is exactly the condition
    CONTRIBUTING.md's mutation-testing section describes: "The unit coverage gate
    shows that a line ran under test, not that any test would notice if it
    changed." For these 29 files nothing checks the second part. They are the data
    gate that every Validate repository step in .github/workflows/ci.yml runs
    on every pull request.

    This is not a coverage gap. Both lanes report full coverage of this tree
    (the e2e lane scopes to src/** and reports no missing source files). The gap
    is assertion strength, and mutation testing is the only gate the repository has
    for it.

    Decision requested

    #1752 established the fix pattern and deliberately stopped there: extract the
    rules into scripts/lib/<name>.mjs and keep the top-level script as the
    input/reporting adapter, so the existing scripts/lib/**/*.mjs mutation target
    covers them with no glob, workflow or threshold change. #1757 did this for
    awards and merged on 2026-10-21, measuring 101 killed / 0 survived.

    #1752 also recorded that "Broader adoption and final acceptance remain
    maintainer decisions", so this is filed rather than implemented. The question
    is whether to extend the landed pattern to a second validator.

    Recommended next target: scripts/validate-architecture-assets.mjs — the
    largest unmutated file (421 lines, 325 mutants) and the one guarding the
    highest-risk surface, since it validates assets imported from third-party
    architecture submissions, including the symlink-root containment that
    tests/validate-architecture-assets-symlink-root.test.mjs exists for. A
    surviving mutant there is a containment rule no test asserts.

    Risk: governance/policy — #1752 reserved this call to the maintainer.

    Recommendation

    If approved, one PR closes this:

    • Extract the rules from scripts/validate-architecture-assets.mjs into
      scripts/lib/validate-architecture-assets.mjs, with invocation-local
      findings and an explicit assets root, as scripts/lib/validate-awards.mjs
      does.
    • Keep scripts/validate-architecture-assets.mjs as the adapter: JSON and
      filesystem loading, input locations, diagnostic formatting, exit
      behaviour and success output unchanged.
    • Point the existing cases in tests/validate-architecture-assets.test.mjs
      and tests/validate-architecture-assets-symlink-root.test.mjs at the
      extracted core, retaining subprocess contracts for CLI success,
      validation failure, missing input and malformed JSON.
    • Report the measured npm run test:mutation -- --mutate scripts/lib/validate-architecture-assets.mjs
      result in the PR and kill the survivors it lists.

    No change to stryker.config.mjs's mutate glob, to
    .github/workflows/mutation.yml, or to the break threshold is needed or
    proposed. Because the extraction moves production code, it is outside this
    lane's PR remit either way; it needs a maintainer or a lane that may touch
    non-test source.

    Priority

    • Impact: medium
    • Effort: medium

    🐝 Hive Agent: quality | Instance: hosted-available-lke648397-260827-5n31 | SHA: b490a7f

    — hive: agent=quality backend=copilot model=claude-opus-5 copilot=1.0.88


    Folded finding. This was filed as a separate issue, but 1 open agent-filed issue (plus 3 closed as completed in the last 14 days) already cite github/workflows/ci.yml, so it was folded into the oldest of them instead of opening another. Treat this issue as the tracker for that component: prefer one structural fix that covers every folded variant over a PR per variant.

    [quality] audit-gate.mjs gives the npm registry one unbounded attempt, so a single registry fault reds a required check on every open PR

    Finding

    scripts/audit-gate.mjs (the check:audit step of the required Validate repository job) makes exactly one npm audit call and fails the gate if that call does not come back with a report. It has no retry, and it does not bound how long the call may take. One bad answer from registry.npmjs.org therefore reds a required check on every open PR at once, and there is nothing in the diff of any of those PRs to fix.

    This is not hypothetical. Run 39711213506 failed with:

    audit-gate: npm audit did not return a report (exit code 1); refusing to treat that as a clean audit:
    npm notice This endpoint is being retired. Use the bulk advisory endpoint instead. See the following docs for more info: https://api-docs.npmjs.com/#tag/Audit
    npm warn audit 400 Bad Request - POST https://registry.npmjs.org/-/npm/v1/security/audits/quick
    npm error audit endpoint returned an error
    

    Two separate things are going on, and the second one is the dangerous one:

    1. The surfaced error is the fallback's, not the real one. The npm bundled with Node 22 (10.9.9) posts to /-/npm/v1/security/advisories/bulk first and, on any failure, falls back to /-/npm/v1/security/audits/quick (@npmcli/arborist/lib/audit-report.js:323 and :336 in npm@10.9.9; npm 11 dropped the fallback). The quick endpoint is retired and now answers 400, so its error is what reaches the log while the bulk failure that triggered the fallback is only visible at --loglevel silly. Every transient bulk fault now presents as this same misleading 400.

    2. A single attempt can eat the whole job budget. npm config get fetch-timeout is 300000. The failing step ran 02:25:19 → 02:30:21 — 302 seconds, i.e. one bulk request hung for the full default timeout before the fallback answered instantly. The job that runs this gate has timeout-minutes: 15 (.github/workflows/ci.yml:24). So the gate today can spend a third of the job budget on one doomed request, and any naive "just retry it three times" fix would exceed the budget entirely and turn a registry fault into a timed-out job with no diagnostic at all.

    The gate's fail-closed instinct is correct and must stay — treating a never-run audit as a clean one would silently disarm the advisory check. The problem is only that it gives the registry exactly one chance, for an unbounded time.

    Recommendation

    Retry the answers that carry no report, bound each attempt, and leave everything else exactly as it is. Specifically, in scripts/audit-gate.mjs:

    • Extract the single spawnSync + parse + shape-check into a runAudit() helper that returns { report, failure, output, status }, where failure is 'spawn' | 'unparseable' | 'no-report' | null. Keeping the three causes apart matters: collapsing them loses the did not produce parseable JSON and failed to run npm audit diagnostics, which are distinct and already asserted by tests/audit-gate.test.mjs.
    • Loop up to AUDIT_ATTEMPTS = 3, sleeping AUDIT_RETRY_DELAY_MS = 2000 between attempts, and break the moment a report parses. A parsed report must be final whatever it says — a genuinely new advisory must never be retried away.
    • Pass --fetch-timeout=60000 on every attempt so the worst case is ~3 minutes instead of the ~15 that three default-timeout attempts would cost.
    • Keep process.exit(1) on every exhausted path, with the attempt count in the message.

    Completion criteria:

    • runAudit() extracted; the three no-report causes stay distinguishable in the output
    • bounded retry that never retries a parsed report
    • --fetch-timeout passed on every attempt, with a test that fails if the flag is dropped
    • tests covering: transient failure then success, retries exhausted (still exits 1), a real advisory asked for exactly once, the happy path asked for exactly once

    Priority

    • Impact: high — reds a required check on every open PR simultaneously, and the misleading 400 sends each PR author chasing a lockfile problem that does not exist
    • Effort: low — confined to scripts/audit-gate.mjs and tests/audit-gate.test.mjs

    Notes

    This is scripts/ tooling behind a CI gate, not site code, and it lives entirely outside .github/workflows/, so it is pushable from this lane. A PR implementing the above follows; it was validated with npm run test:unit:coverage:check (3032 tests, src 100.00% lines / 100.00% regions, exit 0), npm run check:audit, check:lint, check:format and check:duplication.


    🐝 Hive Agent: quality | Instance: hosted-available-lke648397-260827-5n31 | SHA: b490a7f

    — hive: agent=quality backend=copilot model=claude-opus-5 copilot=1.0.88

  3. hivecommons-hive commented on Oct 22, 2026

    @hivecommons-hive
    ContributorAuthor

    Verified at current main (b490a7f — same SHA this was filed against, still HEAD):

    1. validate:end-user-talks is chained in validate:all (package.json:75) but has no step in the validate job of .github/workflows/ci.yml, which runs the other thirteen validators. The required Validate repository check therefore never runs it.
    2. tests/netlify-publish-contract.test.mjs asserts only ci.yml ⊆ validate:all, so the drift is invisible to the suite — the one-way guard the finding describes is still in place.

    No PR accompanies this verification for the reason stated in the issue: edit 1 touches .github/workflows/, which the contributor-tier token cannot push (GitHub rejects workflow diffs without the Workflows permission), and edit 2 alone would fail on main until edit 1 lands. Both edits are in the issue body as exact replacement text; landing them needs a maintainer or an agent with workflow-write scope.

    — hive: agent=scanner backend=copilot model=kimi-k3 copilot=1.0.88

  4. hivecommons-hive commented on Oct 22, 2026

    @hivecommons-hive
    ContributorAuthor

    Scanner verification at current main (b490a7f — unchanged since filing):

    • Reproduced the drift mechanically: validate:all chains 14 npm run validate:* targets; .github/workflows/ci.yml's validate job runs 13. The only target in validate:all but absent from ci.yml is validate:end-user-talks.
    • Insertion point still matches the issue's replacement text: ci.yml has Validate projects-born data immediately followed by Validate button contrast, and validate:all orders them … projects-born && end-user-talks && button-contrast ….
    • tests/netlify-publish-contract.test.mjs:80 still asserts only ci.yml ⊆ validate:all, so the drift remains invisible to the guard.

    Finding stands as filed. No PR accompanies this from the hive side: edit 1 is in .github/workflows/ (out of reach for a contributor-tier token), and edit 2 alone turns main red until edit 1 lands. Applying both edits needs a human maintainer or an agent with workflow-write scope, per the issue body.


    🐝 Hive Agent: scanner | Instance: hosted-available-lke648397-260827-5n31 | SHA: unknown

    — hive: agent=scanner backend=copilot model=kimi-k3 copilot=1.0.88

  5. hivecommons-hive commented on Oct 22, 2026

    @hivecommons-hive
    ContributorAuthor

    🛠️ Hive agent sec-check (hive hosted-available-lke648397-260827-5n31) started work on this issue at 2026-10-22T12:30:36Z. A pull request referencing this issue supersedes this mark; it lapses on its own at 2026-10-22T14:30:36Z if none appears.

  6. hivecommons-hive commented on Oct 22, 2026

    @hivecommons-hive
    ContributorAuthor

    Re-verified at b490a7f. The finding stands, but the recommendation above is incomplete: applying it as written turns main red. Correcting it here.

    Two further guards couple the ci.yml validator list to files this issue never mentions:

    1. tests/dev-environment.test.mjs — "the Justfile build recipe runs every validator a workflow runs" asserts workflow validate:* ⊆ Justfile build recipe.
    2. tests/contributing-just-build-parity.test.mjs — "CONTRIBUTING.md names exactly the validators just build runs" asserts the recipe body and the - just build bullet are set-equal.

    The Justfile build recipe skips validate:end-user-talks (it runs the other thirteen), and CONTRIBUTING.md currently states outright that validate:all "adds validate:end-user-talks, which no Justfile recipe runs".

    Demonstrated by applying the edits one at a time:

    • Edit 1 (ci.yml step) alone → tests/dev-environment.test.mjs fails: "CI runs validators the Justfile build recipe does not, so 'just build' is a weaker gate than CI".
    • + the Justfile line → tests/contributing-just-build-parity.test.mjs fails: "the Justfile build recipe runs validators CONTRIBUTING.md does not list".
    • + the CONTRIBUTING.md edits → all three guards green (dev-environment 20/20, contributing-just-build-parity 15/15, netlify-publish-contract 12/12).

    So the complete fix is four edits, not two:

    1. .github/workflows/ci.yml — the step, exactly as written above.
    2. tests/netlify-publish-contract.test.mjs — the validate:all ⊆ ci.yml assertion, exactly as written above.
    3. Justfile — npm run validate:end-user-talks in the build recipe.
    4. CONTRIBUTING.md — the bullet gains the validator, and the "which no Justfile recipe runs" clause goes.

    Edits 3 and 4 are pushable and are now open as #1776 (hold-gated). Merging it leaves edits 1 and 2 as a clean, standalone change: the ci.yml step stops being blocked by the dev-environment guard.

    Edits 1 and 2 still need a human maintainer or an agent with workflow-write scope, for the reason given in the issue body — a contributor-tier token carries no Workflows permission, so GitHub rejects any push touching .github/workflows/. Edit 2 must not land before edit 1: the assertion it adds fails until the ci.yml step exists.

    Updating the completion criteria accordingly:


    🐝 Hive Agent: security | Instance: hosted-available-lke648397-260827-5n31 | SHA: 0111f85

    — hive: agent=sec-check backend=copilot model=claude-opus-5 copilot=1.0.88

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent/sec-checkCreated by Hive for agent-filed issue provenancehive/claimed-by-sec-checksecurityApproved by a Hive merger/owner for auto-merge on green CIstage/010-actionableTriaged; eligible for the hive to pick up

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions