Skip to content

ci: make the EoSim Sanity Gate fail on any dependency, and give it nothing to close on - #151

Open
Kartikey1306 wants to merge 9 commits into
embeddedos-org:masterfrom
Kartikey1306:ci/eosim-sanity-gate-fails-closed
Open

Kartikey1306 wants to merge 9 commits into
embeddedos-org:masterfrom
Kartikey1306:ci/eosim-sanity-gate-fails-closed

Conversation

@Kartikey1306

@Kartikey1306 Kartikey1306 commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Closes #150

Two fixes, one change, because the first is unmergeable without the second. sanity-gate was fail-open — five needs, one checked — and the two jobs it hid, Windows Sanity and macOS Sanity, still install eosim from a release wheel URL that 404s. A fail-closed gate is only mergeable when there is nothing to close on, so the gate fix and the wheel fix land together. The wheel hunks are srpatcha's #121, carried verbatim; the two PRs merge in either order, and if #121 lands first this reduces to the gate. Evidence that the gate works came from this branch's own earlier heads, where it went red naming both jobs on the identical state master's gate had passed (runs 35204716639, 35207728484).

Stacked on #132 (4054494) — review 4054494..99e5dbd for this change: one commit, two files (.github/workflows/eosim-sanity.yml, tests/unit/test_ci_gate.py). Land #132 first. (Previous heads: bad8cb7 off master — inherited master's mypy break on every Test leg, which #132 repairs; 9866819 on #132 — gate only, red by design. This head adds the wheel fix.)

Problem

sanity-gate needs: five jobs, prints all five results, exits non-zero only when install-validate is not success, and otherwise prints ✅ All EoSim sanity checks passed.

Observed on master, not inferred: run 35181688794 (2026-09-17 04:22Z) — Windows Sanity failure, macOS Sanity failure, EoSim Sanity Gate success. Same on 09-16. The gate is the one job in this workflow a required-check policy would name, and it has been asserting more than it checked.

Change

Gate. The body iterates toJSON(needs) — the gate eos and eBoot already run — so it cannot fall out of step with needs:, and it fails on any result that is not success (including skipped) and on an empty/null payload, which is not "no failures". needs:, if: always() and the job list are unchanged. The workflow is added to its own pull_request: paths: filter so a change to it gets the run that proves it; it was schedule- and dispatch-only.

Wheel. windows-sanity and macos-sanity gain an Install EoSim from source step — git clone --depth 1 --branch v${{ env.EOSIM_VERSION }} … /tmp/EoSim && pip install -e /tmp/EoSim, the path install-validate already runs green on windows-latest and macos-latest with the same eosim list && eosim doctor after it — and the pip install "eosim @ …/eosim-1.5.0-py3-none-any.whl" line is removed. No EoSim release has ever shipped a wheel (memory of 09-01; both jobs 404 on every run). Byte-identical to #121's hunks; no wheel URL remains anywhere in the file.

Verification

Both gate bodies executed locally with this morning's needs values (install-validate success, both sanity jobs failure, the rest success):

old body                     -> rc=0  "✅ All EoSim sanity checks passed"
new body                     -> rc=1  "These jobs did not succeed:
                                          windows-sanity: failure
                                          macos-sanity: failure"
new body, one job skipped    -> rc=1
new body, RESULTS empty      -> rc=1  "gate received no results to check"
new body, RESULTS null       -> rc=1
new body, all five success   -> rc=0

Parsed structure asserted on the document, not on parse success: needs still the same five, if: always(), six jobs in the same order, needs.install-validate.result no longer referenced anywhere in the step, pull_request.paths == ['.github/workflows/eosim-sanity.yml'], schedule and workflow_dispatch retained.

This head 99e5dbd — run 35208305579: all 22 jobs succeed — 9/9 Install & Validate, 7/7 Nested Simulation, 3/3 Guest OS Install, Windows Sanity ✅, macOS Sanity ✅, and EoSim Sanity Gate ✅ with All EoSim sanity jobs succeeded. — 52 checks pass on the PR, 0 fail. Of the 100 EoSim Sanity runs in this repository's history, this is the second success and the first under a gate that could have failed: the other is #121's workflow_dispatch run 35035512098 on 09-15, whose sanity jobs also passed on the same clone path but whose gate was the fail-open one.

Earlier heads' EoSim Sanity runs — the gate proving itself by going red. Run 35204716639 at bad8cb7: 9/9 Install & Validate ✅, 7/7 Nested Simulation ✅, 3/3 Guest OS Install ✅, Windows Sanity ❌, macOS Sanity ❌ (the 404 wheel, #121's defect, unchanged here), and EoSim Sanity Gate with:

##[error]EoSim Sanity Gate failed. These jobs did not succeed:
  windows-sanity: failure
  macos-sanity: failure

Master's gate reported success on the identical five results at 04:22Z the same day (run 35181688794). The red on this PR is the gate working. Anything the path filter does not trigger is NOT RUN here.

On the restacked head 9866819 — run 35207728484: the same three — Windows Sanity ❌, macOS Sanity ❌, EoSim Sanity Gate ❌ naming both — and nothing else red: CI Gate ✅ and all nine Test (Python 3.10/3.11/3.12 × macos/ubuntu/windows) legs ✅, which at bad8cb7 off master were red on master's mypy break. 49 checks pass; the 3 that fail are the finding.

The registry entry

Adding the pull_request trigger makes this a pull-request workflow in tests/unit/test_ci_gate.py's eyes, and that registry requires every such workflow to be either required (with its gate) or excused with a reason about the workflow itself — otherwise test_every_pull_request_workflow_is_accounted_for fails, which it did at bad8cb7. It is recorded as not requirable: the trigger is filtered to this one file, so on a pull request touching nothing else it reports no status, and a required check that never arrives hangs the merge rather than failing it — the reason eos records its book-build.yml the same way. The gate is fail-closed for the runs it does make. Negative control: with the entry removed the test fails again (1 failed, 10 passed); with it, 11 passed; full pytest 683 passed / 1 skipped; ruff clean.

Scope

The gate, the two wheel hunks (srpatcha's #121, verbatim), and the registry entry. Nothing else in the workflow is changed.

CI -- ebuild has been red on master since the 09-08 batch merge, and the
first failing step (ruff) has hidden the ones behind it.

Lint (ruff, all nine Test legs):
- test_build_dir_resolution.py imported shutil twice (F811).
- test_package_recipe.py lost its trailing newline (W292).
- test_ci_gate.py had `import itertools` / `import re` two hundred lines
  down (E402) -- my own embeddedos-org#103, replayed onto a file that had moved.
  These three hunks are byte-identical to embeddedos-org#122's, so either PR merging
  first leaves the other clean.

Type check and tests (never reached on master since 09-08):
- ebuild/packages/index_sync.py calls PackageRecipe.to_dict(), which embeddedos-org#111
  defined and embeddedos-org#112 -- merged five minutes later from a base without it --
  deleted in its replay. mypy names it once; pytest fails nine
  test_index_sync cases with AttributeError. The method is restored verbatim
  from embeddedos-org#111 (cc90078): it emits the `package:`/`build:` keys parse_recipe()
  reads back, which an asdict() replacement would not.

Vendored core drift:
- embeddedos-org#109 (dba3d83) edited core/eos/docs/three-way-alignment.md, a vendored copy
  pinned to eos 5544c98, so drift went 44 -> 45 and the guard failed as
  designed. Reverted to the pinned content (blob 7f9c8c1, the same bytes as
  eos:docs/three-way-alignment.md at the pin). The alignment note belongs in
  ebuild's own docs or upstream in eos, not in the snapshot.

OSSF Scorecard:
- ossf/scorecard-action@v2.4.0 pulls gcr.io/openssf/scorecard-action, and
  gcr.io now refuses the pull ("requires billing to be enabled"). v2.4.3
  pulls from ghcr.io; eos already pins it and its Scorecard job is green.

Not in this PR: EoSim Sanity's Windows/macOS legs install a wheel that has
never been published; embeddedos-org#121 (srpatcha) already replaces that with the clone
the other legs use.

Verified locally: ruff clean, yamllint clean, mypy clean over 107 files,
680 passed / 1 skipped, scripts/check_vendor_drift.py 44/44 and 46/46.
…/3.11, yamllint on Windows

Both surfaced on this branch's first CI run, once ruff let the job get past
its first step.

- ebuild/plugins/__init__.py: on Python 3.10 and 3.11 the stubs type
  entry_points() as the deprecated mapping, and its .get() wants an
  EntryPoints default, so mypy fails with arg-type. The line carried a
  '# type: ignore[attr-defined]' -- the wrong error code, so it suppressed
  nothing. Spelled out with a cast, byte-identical to embeddedos-org#122's hunk (54605f0).

- .yamllint.yml: the Windows runners check out with core.autocrlf=true, so
  every YAML file arrives as CRLF and the default new-lines: unix rule
  rejected every line. The step was added on 09-03 and had never passed on
  that leg. new-lines: platform accepts the checkout's own convention.
…e checkout's line ending

new-lines: platform was the wrong fix. The Windows runners' autocrlf turns
LF files into CRLF -- except a file that already carries a stray CR, which
git leaves alone, and auto-assign.yml had one on its last line. So under
'platform' Windows expected CRLF and got LF on that file's first line, and
the leg was red again for the opposite reason.

Pin *.yml and *.yaml to eol=lf so every OS lints the same bytes, keep
yamllint's default unix rule, and drop the stray CR.
…h a round-trip test

PackageRecipe.to_dict() was written before install_args existed and was
never taught about it, so a recipe that went through index_sync came back
from the cache with install_args empty while every other field survived.
It also returned the recipe's own list objects, so a caller that appended
to what it got back edited the recipe behind its back.

install_args is now emitted after build_args, matching the order
parse_recipe() reads them, and every list field is copied on the way out.
index_sync's entry-to-recipe mapping carries install_args too; without
that the field could not arrive from an index at all.

The new round-trip test builds a recipe with every field set and asserts
parse_recipe(safe_load(safe_dump(to_dict()))) equals it; against the
previous to_dict() it fails on install_args. A second test checks the
lists are copies, and test_index_sync gains a case that an index entry's
install_args reaches the cached YAML.
actions/checkout, ossf/scorecard-action and codeql-action/upload-sarif
were referenced by moving tags. A tag can be re-pointed; a commit cannot,
and Scorecard itself flags unpinned actions. Each is now pinned to the
commit its tag resolved to on 2026-09-14, with the tag kept in a trailing
comment, the same shape linked-issue.yml already uses.
The changelog records what the master repair changed and why. CONTRIBUTING
gains a note for Windows contributors: .gitattributes now pins YAML to LF,
but the attribute governs future checkouts and commits, not files already
in a working tree, so an existing clone needs one git add --renormalize .
(or a fresh clone) before yamllint stops seeing CRLF.
…working tree

git add --renormalize . re-applies the clean filter to the index; it never
rewrites files, so a clone with CRLF YAML still has CRLF YAML afterwards
(reproduced in a scratch clone with core.autocrlf=true: two CRs before,
two after, status clean). What re-checks the files out is
git rm --cached -r . && git reset --hard HEAD, or a fresh clone. CONTRIBUTING
and the changelog now say that. The changelog entry also lists every file
it describes and the plugins/__init__.py type-check fix it had left out, and
the to_dict() comment no longer claims to follow parse_recipe()'s order.
The review at 43b0337 found the one-line assertion pinning install_args
next to build_args and, six lines above it, recipe.py's docstring saying
key order does not matter. Both are true: order is not a correctness
property, because parse_recipe() reads every key by name, and it is a
stability property, because index_sync writes the dict as cached YAML that
humans diff. The assertion now says which of the two it is guarding and
what to do when a reordering is deliberate, so a future failure reads as
intentional rather than as a mystery.

No behaviour change. ruff clean; pytest 683 passed, 1 skipped.
@Kartikey1306
Kartikey1306 force-pushed the ci/eosim-sanity-gate-fails-closed branch from bad8cb7 to 9866819 Compare September 17, 2026 09:55
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…thing to close on

sanity-gate needs five jobs, printed all five results, exited non-zero only
when install-validate was not success, and otherwise printed "All EoSim
sanity checks passed". On 2026-09-17 (run 35181688794) it did exactly that
with Windows Sanity and macOS Sanity both red, and again on 09-16.

The gate body now iterates toJSON(needs), so it cannot fall out of step with
needs:, and fails on any result that is not success -- including skipped --
and on an empty or null payload, which is not "no failures". This is the
gate eos and eBoot already run.

A fail-closed gate is only mergeable when there is nothing to close on, so
the two failures it was hiding are fixed in the same change: windows-sanity
and macos-sanity still installed eosim from a release wheel URL that 404s --
no EoSim release has ever shipped one -- and now clone the pinned tag and
install it editable, exactly as install-validate already does on all three
runners. Those two hunks are srpatcha's embeddedos-org#121, carried verbatim so the two
pull requests merge in either order; if embeddedos-org#121 lands first this reduces to
the gate.

The workflow is added to its own pull_request paths: filter, so a change to
it gets the run that proves it. That makes it a pull-request workflow in
tests/unit/test_ci_gate.py's registry; it is recorded there as not
requirable -- the trigger is paths-filtered, so on a pull request touching
nothing else it reports no status, and a required check that never arrives
hangs the merge rather than failing it. The gate is fail-closed for the
runs it does make.

Verified by executing the two gate bodies with the 09-17 needs values
(install-validate success, both sanity jobs failure, the rest success):

  old body  -> rc=0  "All EoSim sanity checks passed"
  new body  -> rc=1  "These jobs did not succeed: windows-sanity: failure,
                      macos-sanity: failure"
  new body, one job skipped   -> rc=1
  new body, RESULTS empty     -> rc=1  "gate received no results to check"
  new body, RESULTS null      -> rc=1
  new body, all five success  -> rc=0

On this branch's earlier heads the gate proved itself by going red: bad8cb7
(run 35204716639) and 9866819 (run 35207728484) both show the two sanity
jobs failing on the wheel 404 and the gate failing naming both, where
master's gate said success on the identical state. This head is the
evidence the other way: with the wheel gone, the gate has nothing to
report.

Parsed structure asserted: gate needs unchanged (5), if: always(), job list
unchanged, no single-job check remains, no wheel URL anywhere, both sanity
jobs install from the tag clone before their test step, self-trigger
present. test_ci_gate.py 11 passed (removing the registry entry fails
test_every_pull_request_workflow_is_accounted_for); pytest 683 passed,
1 skipped; ruff clean.

Closes embeddedos-org#150
@Kartikey1306
Kartikey1306 force-pushed the ci/eosim-sanity-gate-fails-closed branch from 9866819 to 99e5dbd Compare September 17, 2026 10:01
@Kartikey1306 Kartikey1306 changed the title ci: make the EoSim Sanity Gate fail on any dependency, not one of five ci: make the EoSim Sanity Gate fail on any dependency, and give it nothing to close on Sep 17, 2026
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.

EoSim Sanity Gate reports success while four of its five dependencies can fail

2 participants