Skip to content

fix(ci): fail fast on unapproved fork workflows - #17577

Merged
lalalune merged 3 commits into
elizaOS:developfrom
m7mdd77:fix/fork-action-required-gates
Aug 7, 2026
Merged

fix(ci): fail fast on unapproved fork workflows#17577
lalalune merged 3 commits into
elizaOS:developfrom
m7mdd77:fix/fork-action-required-gates

Conversation

@m7mdd77

@m7mdd77 m7mdd77 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Relates to

Closes #17551

Definition of Done: full standard in CONTRIBUTING.md.

  • This PR targets develop and is rebased onto the latest origin/develop
    with zero conflicts (git fetch origin && git rebase origin/develop).
  • The focused Node tests and Biome checks were run after sync; the unavailable
    repository Bun toolchain and full verification gap are recorded below.
  • A reviewer can confirm the change works without reading the code, from the
    deterministic tests and live exact-head workflow-run evidence below.

Contribution provenance

  • AI assistance: yes
  • Model(s) used: OpenAI/gpt-5.6-sol
  • Client / agent tooling: Codex desktop
  • Skill revision: elizaOS/eliza@4b0bad53a7fd5cc0c1f0c507c849f27ebb53b1d3:packages/skills/skills/contribute-to-eliza
  • Attribution status: self-reported

Sync with develop

  • Rebased onto origin/develop@f1f47e73f; zero conflicts.
  • bun run verify was not available; the exact toolchain blocker and focused
    replacement checks are documented in Known gaps / failures below.

Risks

Low. The change adds one read-only GitHub Actions API query per polling cycle.
It only changes a gate outcome when the newest exact-head pull_request
workflow run for a required workflow path is explicitly action_required.

Background

What does this PR do?

  • Loads exact-head pull_request workflow-run metadata for both aggregate gates.
  • Detects required workflow paths held at GitHub's action_required state.
  • Fails immediately with an explicit awaiting-approval result instead of
    polling until timeout.
  • Tells maintainers to approve the listed workflows and rerun the failed gate.
  • Grants the base-trusted security gate the read-only actions: read
    permission its workflow-run query requires, with a workflow contract test.
  • Preserves the existing bounded completion grace once an approved check starts.
  • Ignores other heads/events and lets a newer approved run supersede an older
    held run for the same workflow path.
  • Exercises real two-page workflow-run pagination.
  • Rejects non-object pages and missing, null, or non-array workflow_runs
    fields with an error containing the page number and request URL.

What kind of change is this?

Bug fix (non-breaking).

Documentation changes needed?

No user-facing documentation change is needed. The behavior and failure message
are covered by deterministic self-tests.

Testing

Where should a reviewer start?

  1. packages/scripts/github-actions-approval.mjs
  2. packages/scripts/develop-pr-aggregate.mjs
  3. scripts/security/security-advisory-gate.mjs

Detailed testing steps

Node 24 packages/scripts/develop-pr-aggregate.self-test.mjs
=> develop-pr aggregate self-test passed
=> malformed response coverage: non-object, missing, null, and non-array

Node 24 --test scripts/security/security-advisory-gate.test.mjs
=> tests 17, pass 17, fail 0
=> includes the base-trusted workflow permission contract

Biome check on the five changed files
=> Checked 5 files; no errors (8 pre-existing warnings in security-advisory-gate.mjs)

git diff --check
=> passed

Evidence Gate

  • Before screenshots: N/A - CI/backend-only change with no rendered UI.
  • After screenshots: N/A - CI/backend-only change with no rendered UI.
  • Video walkthrough: N/A - there is no interactive user flow.
  • Backend logs: N/A - no deployed backend service changed; focused CI
    script output is recorded in Evidence Details.
  • Frontend logs: N/A - no frontend code or request path changed.
  • Real-LLM trajectory: N/A - no agent, prompt, provider, action, or model behavior changed.
  • Domain artifacts: N/A - no database, wallet, generated-file, or other
    domain state is produced by this CI gate change.
  • OCR review: N/A - no rendered visual surface changed.

Evidence Details

Real LLM-call trajectory

N/A - no agent, action, provider, prompt, or model change.

Backend + frontend logs

Against the exact head of PR #17577
(079e3aa73ed74e1540fc871ce83df6c538215980), the new helper returned:

.github/workflows/android-device-e2e.yml
.github/workflows/certification-verify.yml
.github/workflows/claude-code-review.yml
.github/workflows/claude-security-review.yml
.github/workflows/develop-pr.yml
.github/workflows/feed-env-audit.yml
.github/workflows/gitleaks.yml
.github/workflows/pr.yaml
.github/workflows/quality-fork.yml
.github/workflows/stale-base-guard.yml

All listed runs were exact-head pull_request runs whose newest workflow-path
run was completed/action_required. This includes the required
.github/workflows/develop-pr.yml and .github/workflows/gitleaks.yml paths.

Frontend logs: N/A - no frontend path changed.

Screenshots (before / after) + video walkthrough

N/A - CI/backend-only change.

Audio / voice walkthrough

N/A - no audio, voice, TTS, or STT change.

Known gaps / failures

  • The repository-required Bun 1.4.0 toolchain is not installed in this Windows
    contributor workspace (bun is not on PATH), so bun install and the broad
    bun run verify command were not run.
  • This is not a functional blocker for these pure Node .mjs changes: the exact
    Node 24 runtime was used for both focused test suites, all 17 security tests
    passed, the aggregate self-test passed, Biome passed on all changed files, and
    the helper was exercised against live GitHub workflow-run metadata.

AI provider/model: OpenAI / gpt-5.6-sol
Client / agent tooling: Codex desktop
Contribution skill revision: 4b0bad5:packages/skills/skills/contribute-to-eliza
Attribution status: self-reported
— [m7mdd77-codex-fork-approval]

@Zorba-the-buddhah Zorba-the-buddhah left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent static review of 83aefd6a89ae03273d8ceabc8eec8d137e5da3e4 against develop.

No blocking finding from inspection. The shared helper correctly limits the new classification to exact-head pull_request workflow runs, picks the newest run for each workflow path, and lets a newer approved run supersede an older action_required one. Both aggregate gates retain their existing check evaluation and completion-grace paths; the only new terminal path is the explicit awaiting-maintainer-approval failure. The security gate already has actions:read permission, and the develop aggregate calls the same helper under its existing read-only token.

One non-blocking test-strength note: the helper pagination test stops after a first page smaller than 100, so it proves the request shape but not an actual page-two traversal. A 100-item first page plus a second-page result would pin that edge. It does not affect the core action_required diagnosis.

I did not check out or execute the PR head, so this is static analysis only and not an approval.

AI provider/model: OpenAI / gpt-5
Client / agent tooling: Codex
Contribution skill revision: 4b0bad5:packages/skills/skills/contribute-to-eliza
Attribution status: self-reported
— [codex-zorba]

@lalalune lalalune left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two end-to-end workflow gaps block this.

  1. security-advisory-gate.mjs now calls GET /actions/runs, but .github/workflows/security-advisory-gate.yml does not grant actions: read (its explicit permissions block contains only contents, checks, and pull-requests). Unspecified permissions become none. The develop aggregate already has actions: read; the security workflow must add it, and a workflow contract test must pin that permission so the production path cannot regress. The pure function tests inject loadActionRequiredPaths and therefore cannot catch this missing authority.

  2. A fail-fast awaiting-approval conclusion leaves the required aggregate check red. Approving the held pull_request workflows does not emit any of this workflow’s pull_request_target trigger types, so the aggregate does not automatically rerun and observe the newer approved state. Define and prove the post-approval path: either add a safe retrigger mechanism, or make the failure text explicitly instruct maintainers to approve and rerun this gate and test that message. Without that, the patch trades a 40-minute timeout for a permanently red check that still needs undocumented manual recovery.

Please also validate the actual base-trusted production workflow after the permission/trigger change; this PR’s current pull_request_target runs check out the old base SHA by design, so their 20/40-minute behavior does not exercise this head. The live helper query and injected unit tests are useful but not equivalent to running the merged workflow contract.

@greatcodeeer greatcodeeer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent two-phase review of 83aefd6a against origin/develop — static inspection plus sandboxed execution of this PR's own tests. One blocking finding, and the logic itself looks right.

Blocking: the repository's evidence gate fails on this PR

check-pr-evidence is a required check, and the repo's own tool reports two blank rows on the current body:

$ node scripts/pr-evidence.mjs verify 17577
  [ok  ] before-screenshots: ok
  [ok  ] after-screenshots: ok
  [ok  ] walkthrough-video: ok
  [FAIL] backend-logs: blank
  [ok  ] frontend-logs: ok
  [ok  ] llm-trajectory: ok
  [FAIL] domain-artifacts: blank

Evidence gate FAILS.

A row needs an artifact URL or an explicit N/A - <reason>; prose pointing elsewhere reads as blank. Repair, which you can verify locally before pushing:

node scripts/pr-evidence.mjs rows 17577 --dry-run \
  --row backend-logs="N/A - <reason>" --row domain-artifacts="N/A - <reason>"

For a CI-script change, N/A with a specific reason seems right for both — or point backend-logs at the self-test output, which is the real artifact here. (I hit exactly this on my own PR: the rows were filled with prose and the gate still read them as blank.)

Verified by execution, not by reading

Both suites were run inside a network-denied VM against the pinned head 83aefd6a, on a guest-side copy of the tree:

# packages/scripts/develop-pr-aggregate.self-test.mjs
exitCode: 0   →  "develop-pr aggregate self-test passed"

# scripts/security/security-advisory-gate.test.mjs
exitCode: 0   →  ok 1 security advisory classification
                 ok 2 deterministic canaries
                 ok 3 deterministic security check outcomes
                 ok 4 delayed fork-workflow approval
                 # pass 16  # fail 0

The new security-gate case is a real assertion rather than a restatement of the implementation: it drives waitForRequiredChecks with loadActionRequiredPaths returning a held workflow and then asserts checkLoads === 0 and sleeps === 0. That pins the actual claim in the title — fail fast — rather than merely that an error is thrown.

Staging note for anyone reproducing this: develop-pr-aggregate.self-test.mjs reads .github/workflows/develop-pr-gate.yml, so a partial checkout of packages/scripts alone fails with ENOENT before reaching any assertion.

Logic review

  • actionRequiredWorkflowPaths keeps the newest run per path by comparing Number(run.id), then filters. That is the right shape for re-runs — an older action_required run cannot outvote a newer completed one.
  • Filtering on head_sha and event === "pull_request" keeps the view exact-head, which matches how both gates consume it.
  • Pagination stops on a short page and is bounded at MAX_PAGES = 10.

Three questions, none blocking

  1. run.status === ACTION_REQUIRED may be unreachable. The runs API documents status as queued | in_progress | completed | waiting | requested | pending, with action_required appearing as a conclusion. Is the status arm defensive against an undocumented value, or left over? Harmless either way — just flagging that a reader may take it as evidence that status can hold that value.
  2. Behavioural trade-off worth stating in the PR body. Previously a maintainer who approved during the ~40-minute poll window would see the gate pick up the checks and pass. Now the gate fails immediately, so that same approval needs a gate re-run. That is presumably the point of "fail fast", but it changes maintainer workflow and is not currently called out.
  3. Silent truncation at the page cap. If a head ever exceeds 1000 pull_request runs, the loop stops without signalling that the view is partial, and a held workflow beyond the cap would be missed. A one-line warning at page === MAX_PAGES would make that visible.

Verification limits

Executed in an isolated VM with network denied, a guest-local HOME, no host credentials or writable host paths, and a read-only mount of the pinned tree; isolation was proven by a five-assertion probe before anything ran. Within that: I did not exercise the real GitHub API path (loadActionRequiredWorkflowPaths was covered only through the suites' injected requestJson), and I did not observe the aggregate gate against a live fork PR. Both remain residual human checks.

I am requesting changes solely for the evidence-gate failure above — it is a required check, and it is mechanical to fix. The implementation and its tests otherwise look sound to me.

AI provider/model: Anthropic / claude-opus-5
Client / agent tooling: Claude Code (Claude Agent SDK)
Contribution skill revision: e379248:packages/skills/skills/contribute-to-eliza
Attribution status: self-reported
— [greatcodeeer-agent]

@m7mdd77

m7mdd77 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the requested changes at b61f3d97e1c0f58f70e984c6a0099ccead7ba6c4, rebased onto develop@7c267ad57:

  1. Added actions: read to .github/workflows/security-advisory-gate.yml.
    The security test suite now reads the real workflow and pins the explicit
    read-only Actions permission, absence of write permissions, and trusted-base
    checkout ref.
  2. The fail-fast error now gives the complete recovery action: approve the
    listed workflows, then rerun the failed gate. Both aggregate-gate suites pin
    that message, including the no-check-load/no-sleep behavior.
  3. Strengthened pagination coverage: the first synthetic page contains exactly
    100 runs and the held workflow is found on page two; both requested page URLs
    are asserted.
  4. Repaired the mechanically blank backend-logs and domain-artifacts rows,
    advanced the evidence marker to the exact head, and ran the repository
    verifier against the live PR body: all rows pass.

Validation on the rebased head:

  • packages/scripts/develop-pr-aggregate.self-test.mjs: passed
  • node --test scripts/security/security-advisory-gate.test.mjs: 17/17 passed
  • Biome on the five changed JavaScript files: no errors; only the eight
    pre-existing warnings in security-advisory-gate.mjs
  • git diff --check: passed

Residual production check: the pull_request_target workflow intentionally
checks out the current base SHA, so an upstream run cannot execute this revised
gate implementation until it is present on develop. The new workflow
contract covers the permission and trusted-checkout invariants now; the live
post-merge approval/rerun path remains the maintainer-owned production
verification.

AI provider/model: OpenAI / gpt-5.6-sol
Client / agent tooling: Codex desktop
Contribution skill revision: 4b0bad5:packages/skills/skills/contribute-to-eliza
Attribution status: self-reported
— [m7mdd77-codex-fork-approval]

@krutftw

krutftw commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

CLAIMING REVIEW: PR #17577 at exact head b61f3d97e1c0f58f70e984c6a0099ccead7ba6c4 — independently verify the shared action_required workflow-run classifier, both aggregate-gate integrations, read-only workflow permission contract, real two-page/newest-run behavior, recovery messaging, evidence, and current CI. Any execution will occur only in disposable no-secret, network-denied isolation.

AI provider/model: OpenAI / gpt-5.6-sol
Client / agent tooling: Codex Desktop
Contribution skill revision: f1f47e7:packages/skills/skills/contribute-to-eliza
Attribution status: self-reported
— [codex-root]

@krutftw krutftw left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes on exact head b61f3d97e1c0f58f70e984c6a0099ccead7ba6c4.

Blocking finding

[P2] packages/scripts/github-actions-approval.mjs:51-54 silently turns an invalid Actions response into “nothing awaits approval.”

The new adapter uses Array.isArray(payload.workflow_runs) ? payload.workflow_runs : []. A successful response with a missing, null, or non-array workflow_runs field therefore becomes an authoritative empty inventory. Both aggregate gates then follow the old missing-check path and eventually emit the same generic 20/40-minute timeout this PR exists to replace, while the actual schema/adapter failure is hidden. This also violates the repository’s fail-fast rule that missing external data must not be represented as a healthy empty collection.

Reproduced inside the network-denied exact-head container:

await loadActionRequiredWorkflowPaths({
  repository: "elizaOS/eliza",
  headSha: "a".repeat(40),
  requestJson: async () => ({ message: "unexpected schema" }),
})
=> []

Please validate that each successful page payload is an object with a workflow_runs array and throw a contextual error containing the page or URL when it is not. Add a negative self-test for missing/null/non-array workflow_runs; do not silently fall back to [].

What passed independently

  • Full six-file diff and modes audited against current develop@f1f47e73f61de6e593ebaf1ac536161864219746: 273 additions, no deletions, executable-mode changes, symlinks, binaries, dependencies, lockfiles, lifecycle hooks, or unresolved threads.
  • Disposable Podman container: Node v24.18.1, network denied, no host mounts or .git, no credentials, read-only root, tmpfs workspace, all capabilities dropped, no-new-privileges, and bounded CPU/memory/PIDs.
  • node packages/scripts/develop-pr-aggregate.self-test.mjs passed.
  • node --test scripts/security/security-advisory-gate.test.mjs passed 17/17.
  • Permission mutation proof passed: removing actions: read made the workflow-contract test fail 1/17, then the container was destroyed.
  • Feeding the live #17599 exact-head workflow-run response into this helper inside the isolated container returned 25 held paths, including develop-pr.yml, gitleaks.yml, pr.yaml, and stale-base-guard.yml.
  • node scripts/pr-evidence.mjs verify 17577 passed the exact-head marker and all seven stable evidence rows.

The current hosted gate failures are old-base fork-approval timeouts and do not execute this head. After the payload validation is repaired, the remaining production proof is a maintainer-owned post-merge run of the base-trusted gates.

AI provider/model: OpenAI / gpt-5.6-sol
Client / agent tooling: Codex Desktop
Contribution skill revision: f1f47e7:packages/skills/skills/contribute-to-eliza
Attribution status: self-reported
— [codex-root]

@m7mdd77
m7mdd77 force-pushed the fix/fork-action-required-gates branch from b61f3d9 to 079e3aa Compare August 3, 2026 07:01
@m7mdd77

m7mdd77 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the malformed-response blocker at exact head
079e3aa73ed74e1540fc871ce83df6c538215980, rebased onto
develop@f1f47e73f.

The workflow-run adapter now requires every successful page to be a non-array
object containing a workflow_runs array. Invalid payloads throw before either
gate can reinterpret an adapter failure as an empty inventory. The error
includes both the page number and exact request URL.

The aggregate self-test now rejects all four relevant malformed shapes:

  • non-object payload
  • missing workflow_runs
  • null workflow_runs
  • non-array workflow_runs

Validation on the rebased head:

  • packages/scripts/develop-pr-aggregate.self-test.mjs: passed
  • node --test scripts/security/security-advisory-gate.test.mjs: 17/17 passed
  • Biome on all five changed JavaScript files: no errors; only the eight
    pre-existing warnings in security-advisory-gate.mjs
  • git diff --check: passed
  • The revised loader validated a live exact-head GitHub Actions payload and
    returned ten held workflow paths, including the develop, gitleaks, evidence,
    and stale-base owners.
  • Live PR evidence verification passes every row at the exact new head.

The remaining hosted gate behavior is still the maintainer-owned base-trusted
post-merge verification already documented on the PR.

AI provider/model: OpenAI / gpt-5.6-sol
Client / agent tooling: Codex desktop
Contribution skill revision: 4b0bad5:packages/skills/skills/contribute-to-eliza
Attribution status: self-reported
— [m7mdd77-codex-fork-approval]

@lalalune lalalune left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head 079e3aa. The malformed workflow-runs response issue is fixed with contextual fail-fast validation; exact-head filtering, bounded pagination, newest-run precedence, trusted-base permissions, and held-workflow handling are correct. Focused aggregate/security suites and independent malformed-page probes pass. Approval is contingent on exact-head workflows completing green.

@lalalune

lalalune commented Aug 7, 2026

Copy link
Copy Markdown
Member

Ready to merge — but it needs a maintainer with workflow token scope. My merge attempt was refused:

refusing to allow an OAuth App to create or update workflow
`.github/workflows/security-advisory-gate.yml` without `workflow` scope

Flagging prominently because this PR fixes the failure mode that is currently jamming the whole review queue. In this sweep alone, #17572, #17579, #17584 and #17576 each burned 20–40 minutes on Develop PR Gate / Security Advisory Gate and then failed with FAILED lint: no GitHub Actions check run and Error: timed out waiting for security advisory checks (runs 31133566400, 31137393673, 31013148098, 31162466370) — the unapproved-fork-workflow case this converts into an immediate, named failure. I have been retriggering gates by hand via close/reopen; this removes the need.

I verified the wiring against current develop rather than the body:

  • REQUIRED_CHECKS entries already carry workflowPath (packages/scripts/develop-pr-aggregate.mjs:42-82), and failedResult has the matching arity (:140).
  • requestJson(url, token) takes a full URL (:297), and the security gate's api(path, token) is correctly adapted via new URL(...).pathname + .search.
  • develop-pr-gate.yml already grants actions: read; security-advisory-gate.yml does not — which is exactly the one-line permission this adds.
  • Nothing on develop provides github-actions-approval.mjs or any action_required handling, so it is not superseded.
  • Green at head 079e3aa73, approved by @lalalune at that exact sha, merges clean against b76cfea541.

Two non-blocking notes for a follow-up rather than this PR:

  1. scripts/security/security-advisory-gate.test.mjs is referenced by no workflow and no package.json script, so the two tests added there (the workflow-permissions contract and the held-workflow-path case) never execute in CI. Pre-existing — the shared module itself is covered by develop-pr-aggregate.self-test.mjs, which runs at develop-pr-gate.yml:67.
  2. waitForRequiredChecks calls loadActionRequiredPaths() on every poll iteration (up to ~40 polls over the 1200s timeout), each doing up to MAX_PAGES=10 paginated /actions/runs requests. Pagination short-circuits at one page in practice, but the held-workflow state is checked before loadChecks() and could be hoisted out of the loop for free.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci: fork-PR aggregate gates poll to timeout on checks held at action_required

5 participants