Skip to content

ci: remove the ruby dependency from the org reusable workflows#153

Merged
haasonsaas merged 2 commits into
mainfrom
ci/de-ruby-org-reusable-workflows
Jul 25, 2026
Merged

ci: remove the ruby dependency from the org reusable workflows#153
haasonsaas merged 2 commits into
mainfrom
ci/de-ruby-org-reusable-workflows

Conversation

@haasonsaas

@haasonsaas haasonsaas commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Why

Both reusable workflows in this repo used Ruby — as a YAML/JSON parser in codex-rails-check, and as the host for the review-thread guard script. Our self-hosted runner images ship Ruby 3.1.2. The Actions Runner Controller (ARC) image we are migrating to ships none, so the check dies in 7 seconds:

ruby -e 'require "yaml"; ARGV.each { |f| YAML.load_file(f); puts "ok #{f}" }' "${files[@]}"
/home/runner/_work/_temp/....sh: line 8: ruby: command not found
##[error]Process completed with exit code 127

Both images were probed directly rather than read out of a Dockerfile — a job running command -v for 95 binaries on one runner of each type. Ruby is the only tool either workflow depends on that the two images disagree about:

tool old image ARC image
ruby / gem 3.1.2 absent
python3 3.11.2 3.12.3
python3 -c 'import yaml' (PyYAML) present present
node v22.23.1 v22.23.1
node -e "require('yaml')" / js-yaml absent absent
yq absent absent
gh, git, jq, curl, tar, make, gcc, perl, openssl present present

The review-thread guard is the more serious of the two. It produces a status check that is required on a protected branch, and its only real step is ruby .../check-pr-review-threads.rb. It has not broken yet only because the calling repository's runner variable still resolves to the old fleet — the ARC fallback written into the caller is dead code. Repointing that variable, which is the whole direction of travel, would have failed the check on every PR and stopped all merges at once.

What changed

Both workflows are restored into this repo — they were removed at 0783dc1 when the org automation engine moved to a private repository — with Ruby replaced.

They have to live here rather than in the private repo, and the reason is checkable:

$ gh api repos/<private-automation-repo>/actions/permissions/access
{"access_level":"none"}

No repository outside it can call its reusable workflows. Every consumer still pins evalops/.github@<sha>, and those pins keep resolving only because the SHAs predate 0783dc1. For anyone to pin a fixed version, the shared workflows have to exist at this repo's HEAD.

Nothing newly private is published: check-pr-review-threads.rb has been in this repo's public history since d45c0bca, and the restored files carry no scripts beyond the guard itself.

Replacement: python3 + PyYAML, with the assumption made loud

PyYAML is present on both images today, verified above. "The image already has the library" is exactly the assumption that has silently broken a gate here before — a required check whose YAML-structural predicates quietly degraded to line matching for months because the parser was never installed and nothing asserted it. So the dependency is asserted rather than assumed:

  • codex-rails-check.yml opens with a preflight that exits 1 with a named ::error:: if python3 or import yaml is unavailable, then self-tests the validator against a known-bad YAML file and fails if the bad file is accepted.
  • review-thread-guard.yml preflights python3, gh and the guard script's existence, then runs the guard against two committed fixtures: a clean PR that must exit 0, and a PR with an unresolved high-severity thread that must exit 1. If the guard ever starts passing vacuously, the check goes red instead of green.

Rejected alternatives: yq (absent on both images; adds a download and a checksum to maintain), a vendored JS parser (hand-rolling YAML is how a validator ends up accepting invalid input), and actions/setup-* (a per-run install for a library both images already carry).

check-pr-review-threads.rbcheck_pr_review_threads.py

Standard library only — json, re, subprocess, argparse. Same severity ranking, same informational-summary suppression, same GraphQL documents and pagination, same annotations, same exit codes.

One deliberate behaviour change: if gh is missing, the port raises instead of returning an empty feedback list. A guard reporting "no unresolved feedback" because it could not query anything is the failure mode this PR exists to prevent.

Two other ARC hazards fixed in passing

  • codex-rails-check.yml's runner_label defaulted to a Blacksmith label. Blacksmith was removed org-wide; a job targeting a label with no runners queues forever rather than failing — no red X, no annotation, no notification. The default is now ubuntu-latest.
  • The "Run repo Ruby tests" step still needs a real Ruby. It now fails with a named ::error:: when a repo has test/**/*_test.rb and the runner has no ruby, instead of skipping. No current consumer ships a Ruby suite, so the path is inert today.

Verification

Every number below comes from a command that was run, not from reading the diff.

  • 190/190 differential cases between the Python port and the Ruby original — 19 payload fixtures × 5 severities × both --include-outdated settings — byte-identical stdout, stderr and exit code, plus matching exit 2 on an invalid --min-severity.
  • 83/83 on codex-rails-check: every step extracted from the YAML and executed against synthetic repositories, covering each step's pass and fail path, plus a differential against the Ruby original across 11 repo variants × 5 steps. The Python version fails on exactly the inputs the Ruby version failed on.
  • 12/12 on the guard workflow, including four sabotage cases. Replacing the guard with a script that always exits 0 — the silent-degradation failure mode — is caught by the self-test: review-thread guard self-test failed: a blocking fixture was reported clean. This gate is not verifying anything. Removing python3, gh, or the script itself each fails the preflight with the missing tool named.
  • actionlint (which also runs shellcheck over every run: block) is clean on both workflows.

Both workflows were then exercised end-to-end on real ARC runners from the consumer-side PR, including a deliberate breakage that turned codex-rails-check red with a precise parse error and a revert that turned it green again.

Which version this restores

codex-rails-check.yml here is the ec91f80b step set, which is what most consumers call. Two consumers pin the later 7c84584, which adds six steps. Those are not carried forward, deliberately:

  • Five of them shell out to helper Ruby scripts that were deleted from this repo at 0783dc1 and now live only in the private automation repo. Restoring steps that invoke deleted scripts would ship something broken by construction.
  • The sixth falls back to go run github.com/rhysd/actionlint/..., and go is absent on the ARC image — porting it as-is would trade a Ruby break for a Go break.

Those two consumers should stay on 7c84584 until someone decides whether those org-only steps belong in the shared workflow at all.

Follow-up required — this PR alone changes almost nothing

Consumers pin by SHA, so until the pins move they keep running the Ruby version. Two things worth knowing before merging:

  1. One consumer pins @main, and main has had no codex-rails-check.yml since 0783dc1. Its check has not run since May — the workflow reference is unresolvable, so there is nothing to fail. That repo needs no pin bump; merging this restores its check.
  2. review-thread-guard callers pass guard_ref as well as the uses: SHA. Both must move to this merge commit, or the job checks out a tree with no check_pr_review_threads.py and fails its own preflight.

Consumer pin inventory and the bump PR: evalops/maestro-internal#3168.

agent-authorship-label.yml also runs Ruby and is still pinned by three consumers. Not fixed here: it is a separate reusable workflow with its own helper script, and it is not currently routed to ARC.

Codex Rails failed on ARC in 7s with `ruby: command not found`. The Hetzner
runner image ships Ruby 3.1.2; the ARC image ships none. Both workflows used
Ruby only as a YAML/JSON parser and as the host for the review-thread guard.

Restores codex-rails-check.yml and review-thread-guard.yml into this repo
(removed at 0783dc1 when the engine moved to .github-private) with Ruby
replaced by python3 + PyYAML, and ports check-pr-review-threads.rb to
check_pr_review_threads.py using the standard library only.

PyYAML is present on both runner images, verified by probing them rather than
by reading a Dockerfile. Because "the image has the library" is exactly the
assumption that silently broke the deploy cost gate, both workflows now assert
their toolchain up front and self-test against fixtures that must fail. A guard
that stops verifying goes red instead of green.

Also drops the dead `blacksmith-4vcpu-ubuntu-2404` runner default, which would
queue forever rather than fail, and makes the Ruby test step fail loudly
instead of skipping when a repo has a Ruby suite and the runner has no ruby.

The Python port matches the Ruby original on 190 differential cases across 19
payload fixtures, 5 severities and both --include-outdated settings.

Consumers pin this repo by SHA and must bump both `uses:` and `guard_ref`.
The header comments I added in 5721334 listed the organisation's private
consumer repositories by name, and named a specific private repo's protected
branch, inside a repository that is public. That is the same disclosure the
automation move at 0783dc1 was meant to avoid.

Reworded to describe the constraint without the inventory. No behaviour change;
comments only. Landed as a new commit rather than an amend, since 5721334 is
already pushed.
@haasonsaas
haasonsaas merged commit 94ecd0d into main Jul 25, 2026
3 checks passed
@haasonsaas
haasonsaas deleted the ci/de-ruby-org-reusable-workflows branch July 25, 2026 23:36
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