Migrate pipeline from CircleCI to GitHub Actions#157
Conversation
phelma
left a comment
There was a problem hiding this comment.
Code Review: #157 - Migrate pipeline from CircleCI to GitHub Actions
Verdict: REQUEST_CHANGES
A careful, well-hardened CircleCI→GitHub Actions cutover that faithfully follows the Variant A family plan: least-privilege permissions, env-indirection for untrusted inputs, git-crypt ciphertext guards, a release environment gate, and thorough "why" comments. One in-scope defect blocks approval: both workflows invoke ./go spec, a rake task that does not exist in this repo (the only test task is test:unit), so the required test gate fails on every push and PR and the whole pipeline stalls. Everything else the lenses raised is a concern against a decision the plan/PR documents as deliberate — kept below for a human to revisit at the plan level; none of it blocks a plan-conformant diff.
Cross-Cutting Themes
- Non-existent
spectest task (correctness, standards) — bothmain.yamlandpr.yamlrun./go spec; the Rakefile defines onlytest:unit. The plan (§2.3, §5.2) specifiestest:unit, and siblingrake_slackuses it../go specwas a pre-existing broken invocation in the oldtest.sh(thespectask was removed from the Rakefile long ago), but the migration should have adopted the plan-mandatedtest:unit, which exists and passes. As written the test job errors withDon't know how to build task 'spec'.
Strengths
- ✅ Least-privilege
permissions: contents: readwith narrow per-job escalation. - ✅ Untrusted inputs passed via
env:as$VARS, never${{ }}-interpolated intorun:. - ✅ Secret-bearing PR prerelease job guarded to same-repo, non-Dependabot PRs.
- ✅ Dependabot merge uses
--match-head-commit "$HEAD_SHA"and immutableuser.login. - ✅
read_encryption_passphraserefuses git-crypt ciphertext;resolve_github_tokenfails fast. - ✅
prerelease:publishrestoresversion.rband removes the built gem in anensureblock.
General Findings
- 🔵 code-quality (suggestion, in-scope): token/passphrase logic was extracted into top-level
resolve_github_token/read_encryption_passphrasemethods; the plan (§4.4 step 3) shows this inline and says "Do not restructure anything else". Functionally identical, cleaner, likely helpsMetrics/BlockLength— a benign deviation, noted for transparency.
Plan Concerns (documented-deliberate — do not block; for plan revisit)
- 🟡 security: the same-repo PR prerelease job hands the RubyGems token and git-crypt secrets to PR-branch code; a write-access contributor could exfiltrate them with no maintainer approval. This is the D8 pre-merge publish, documented and guarded to same-repo human PRs. (
pr.yaml:85) - 🔵 security: Dependabot auto-merge accepts any update type — D3, documented parity.
- 🔵 security: provisioning uses the operator's ambient
gh/GITHUB_TOKEN, not a stored PAT — D7, documented parity. - 🔵 safety:
./go releasepublishes to RubyGems beforegit push— a pre-existing hazard inside untouched./go releaselogic (§1, D5). (main.yaml:124) - 🔵 standards:
concurrency.queue: max— the plan §4.2 specifies it and cites the 2026-05-07 GA changelog; authoritative here. (main.yaml:54) - 🔵 standards: notify idiom (
!cancelled()+continue-on-error) differs fromrake_slack'salways()— plan §3 says the §4 YAML is authoritative. - 🔵 standards:
main.yamlomits the sibling'sskip-ci-checkjob — D4 states no loop-guard job is needed. - 🔵 code-quality:
gem buildduplicated acrosslibrary:build/prerelease:publish; repeated arg list; magic Slack channel IDs — all plan-prescribed verbatim (§4.4).
Non-issue (verified)
prerelease:publish'sensurerestore not being SIGKILL-safe self-nullifies: the runner checkout is ephemeral, so a mutatedversion.rbnever persists.
Review generated by /accelerator:review-pr
| - name: Install tools | ||
| uses: infrablocks/github-actions/asdf_install@v1 | ||
| - name: Test | ||
| run: ./go spec |
There was a problem hiding this comment.
🔴 Correctness / Standards (critical)
The Test step runs ./go spec, but this Rakefile defines no spec task — the only RSpec task is test:unit (RSpec::Core::RakeTask.new(:unit) inside namespace :test). bundle exec rake spec aborts with Don't know how to build task 'spec'. Because prerelease needs [check, test], every push to main stops here and the release path never runs.
The family plan (§2.3, §5.2) specifies test:unit, and the sibling rake_slack workflow uses it. ./go spec was a stale, already-broken invocation carried over from the old test.sh (the spec task was removed from the Rakefile long ago).
Fix: change to ./go test:unit (also in pr.yaml), or add task spec: 'test:unit' to the Rakefile.
| - name: Install tools | ||
| uses: infrablocks/github-actions/asdf_install@v1 | ||
| - name: Test | ||
| run: ./go spec |
There was a problem hiding this comment.
🔴 Correctness / Standards (critical)
Same defect as main.yaml:35: ./go spec is not a defined task (only test:unit exists). Every PR fails the test job, and both prerelease and merge-pull-request depend on [check, test, build], so PR prereleases and Dependabot auto-merge can never run.
Fix: change to ./go test:unit.
| run: ./scripts/ci/common/configure-rubygems.sh | ||
| - name: Publish prerelease | ||
| # Facts via env, never interpolated | ||
| run: ./go "prerelease:publish[$PR_NUMBER,$RUN_NUMBER,$RUN_ATTEMPT]" |
There was a problem hiding this comment.
🟡 Security (major) — plan concern, not a blocking defect
This job unlocks git-crypt and runs prerelease:publish on the PR branch's own code, guarded only by same-repo + not-Dependabot. A contributor who can push a branch could modify the gemspec or the prerelease:publish task in the same PR to exfiltrate the RubyGems push token and git-crypt secrets, with no maintainer approval.
This is the deliberate D8 pre-merge publish, documented in the plan and PR description. Flagged so a human can decide whether to gate the publish behind a reviewed environment: at the plan level — it does not block this plan-conformant diff.
| run: ./go repository:set_ci_author | ||
| - name: Bump version | ||
| run: ./go "version:bump[minor]" | ||
| - name: Release |
There was a problem hiding this comment.
🔵 Safety (minor) — plan concern
./go release (gem release --tag --push) publishes to RubyGems before the Push commits/Push tags steps. A push failure leaves a public, effectively unyankable version while main never records the bump — the next run recomputes the same version and gem push fails on a duplicate, wedging releases.
This is a pre-existing, deliberately-preserved hazard inside the untouched ./go release logic (§1, D5). No change required in this migration.
| concurrency: | ||
| group: main | ||
| cancel-in-progress: false | ||
| queue: max |
There was a problem hiding this comment.
🔵 Standards (minor) — plan concern
concurrency.queue: max is a newer Actions key not present in the sibling rake_slack workflows; if the runner's schema does not honour it, the queue-all-runs intent silently degrades.
The plan §4.2 specifies this key verbatim and cites the 2026-05-07 GA changelog, so it is authoritative here — noted only so a human can confirm the feature is live for this org.
phelma
left a comment
There was a problem hiding this comment.
Code Review: #157 — Migrate pipeline from CircleCI to GitHub Actions
Verdict: REQUEST_CHANGES
A clean, faithful Variant A cutover — task wiring is sound, secrets handling is well-hardened (PR-controlled values routed through env, secret-bearing prerelease job guarded to same-repo human PRs, immutable user.login dependabot guard, least-privilege permissions:), and the decommission is complete. One in-scope critical defect blocks approval: the release job runs ./go documentation:update, but no such task exists in the Rakefile and the deleted release.sh never invoked it — plan §4.2 says this step must be omitted when release.sh does not run it. Several lens findings target the plan's own authoritative YAML/code (queue: max, gpg path, verbatim comments) and are recorded as plan concerns, not defects.
Cross-Cutting Themes
queue: maxconcurrency key (correctness, safety) — both lenses believe it is an unsupported GitHub Actions concurrency key. Plan §4.2 specifies it verbatim, citing a GA feature dated 2026-05-07 → plan concern, not a defect. Worth a human confirming the feature shipped as cited.
Strengths
- ✅ Every
./gotask referenced resolves to a real task (exceptdocumentation:update, below);slack:notify[outcome,type]arity matches; gpg path matches rake_git_crypt's default. - ✅ PR title/number/SHA/URL routed through env vars, not
${{ }}shell interpolation; only${{ job.status }}is inline (trusted enum). - ✅ Prerelease (secret-bearing) job guarded to same-repo, non-dependabot PRs; dependabot merge guard uses immutable
pull_request.user.login. - ✅ Least-privilege
permissions:;environment: releasegate present; all jobs havetimeout-minutes;--match-head-commitguards the merge. - ✅
prerelease:publishrestoresversion.rband removes the built gem in anensureblock; requires/Gemfile stay alphabetical; helpers fail fast.
General Findings
- 🔴 Correctness (plan-conformance): release job
Update documentationstep references a non-existent task;release.shnever ran it. Breaks every release. See inline. - 🔵 Code Quality: gem name /
gem buildcommand duplicated acrosslibrary:buildandprerelease:publish(minor). - 🔵 Standards: helper
defs placed mid-file between namespaces (suggestion; a clean divergence from the plan's inline code). - 🔵 Code Quality:
prerelease:publishvalidates arg presence but not numeric shape (suggestion). - 🔵 Plan-conformance (minor):
Gemfile.lockPLATFORMS still lists operator-local darwin platforms (§4.5 asks for none) — pre-existing and unchanged by this PR.
Plan Concerns (documented-deliberate; do not block)
- 🔵
queue: maxon prerelease + release (correctness ×2, safety ×1) — plan §4.2 verbatim, cites GA 2026-05-07. - 🔵 Shared
mainconcurrency group across gated release + ungated prerelease (safety) — the plan's explicit design. - 🔵 Compound prerelease version
0.13.0.pre.2.pr<N>...(correctness) — plan §4.4 step 8 documents this shape as valid/accepted. - 🔵 gpg key at
.github/gpg.private.encroot (standards) — plan §4.3 mandates this exact path. - 🔵 pr.yaml comments exceed 80 cols (standards) — verbatim from the plan's YAML; YAML is not rubocop-linted.
- 🔵
String#subreplacement-string backslash handling (code-quality) — plan's verbatim code; inputs numeric, safe.
Review generated by /accelerator:review-pr
| - name: Bump version | ||
| run: ./go "version:bump[minor]" | ||
| - name: Release | ||
| run: ./go release |
There was a problem hiding this comment.
🔴 Correctness / plan-conformance (critical) — Release job runs a non-existent documentation:update task.
This step runs ./go documentation:update, but there is no documentation:update task anywhere in the Rakefile or the loaded gems, and the deleted scripts/ci/steps/release.sh only ran version:bump[minor] + release — it never ran documentation generation. Plan §4.2 is explicit: "include the whole Update documentation step only if the repo's release.sh runs it; omit the step otherwise." This repo's release.sh does not, so the step must be removed. As written, every release run fails here with Don't know how to build task 'documentation:update'.
Fix: delete the Update documentation step from the release job.
There was a problem hiding this comment.
This is false, ./go documentation:update does not exist.
| concurrency: | ||
| group: main | ||
| cancel-in-progress: false | ||
| queue: max |
There was a problem hiding this comment.
🔵 Plan concern (not a defect) — queue: max.
Recording for a human: the correctness and safety lenses flag queue: max as an unsupported GitHub Actions concurrency key (only group / cancel-in-progress are historically valid). However, plan §4.2 specifies queue: max verbatim and cites a GA feature dated 2026-05-07. Treated as documented-deliberate and not counted as a defect — but worth confirming the feature shipped as cited, since otherwise intermediate prerelease runs may be silently coalesced.
| # Build + push directly: `gem release` aborts on the (deliberately) | ||
| # uncommitted version rewrite. PR CI must not tag, commit, or push | ||
| # (contrast the `release` task). | ||
| sh 'gem build rake_ssh.gemspec' |
There was a problem hiding this comment.
🔵 Code Quality (minor) — Gem name / build command duplicated.
gem build rake_ssh.gemspec appears in both library:build and prerelease:publish, and rake_ssh is hardcoded again when composing gem_file. Three literals that must stay in sync on a rename. Consider a single GEM_NAME constant. Low priority.
| gem_file = "rake_ssh-#{version}.gem" | ||
| begin | ||
| File.write(version_file, | ||
| source.sub(version_pattern, "\\1#{version}\\3")) |
There was a problem hiding this comment.
🔵 Plan concern (low) — String#sub replacement string interprets backslashes.
source.sub(version_pattern, "\\1#{version}\\3") uses replacement-string semantics where a \N in version would be read as a backreference. Inputs are numeric here, so it is safe; this is the plan's verbatim code, recorded for a human. The block form (sub(version_pattern) { "#{$1}#{version}#{$3}" }) would avoid the coupling.
|
Plan §4 conformance sweep (independent of the lens review) Both directions checked — required changes present, nothing beyond the plan changed. Diff matches the Variant A plan closely. Two deviations found:
Confirmed correct against the plan: gemspec needed no edits (no The |
Cutover to GitHub Actions per the Variant A family plan (gem pilot).
Includes decommission — merging this PR completes the repo's migration.
releaseenvironment gate.github/rake_slack; dependabot auto-merge jobrake_githubsecrets/environments;rake_circle_cidropped.circleci/,scripts/ci/, the CI SSH deploykey pair and its
keys:deploy/deploy_keysprovisioning, and the storedCircleCI/GitHub API credentials (
config/secrets/{circle_ci,github}/)Deliberate decisions (not defects)
This cutover reproduces the CircleCI pipeline's behaviour, warts included;
fixing inherited hazards is post-migration work. In particular:
./go releasepublishes to RubyGems before the version-bump commit ispushed — pre-existing ordering inside the untouched release logic.
mainwith no approval gate; onlyfull releases are gated (
environment: release).merge does not trigger a release build — on CircleCI the merge commit
carried
[skip ci], so this matches. Updates ship with the nexthuman-triggered release.
releasejob pullsmainat approval time, so a delayed approvalpublishes main as it stands then, not the SHA this run tested — parity with
the old
release.sh(which also pulled;prerelease.shdid not, so theprerelease job has no pull).
asdf_install@v1is our own action (infrablocks/github-actions); we arehappy tracking its major version tag.
build system (
./go/rake) and CI stays lean — it just triggers tasks andsupplies secrets/context.
Gemfile.lockcarries transitive major bumps — the unavoidable resolutionof the targeted
bundle lock --update, not scope creep.autocorrects existing code (e.g.
Style/ArgumentsForwarding) — requiredby the
library:checkverification gate, not drive-by refactoring.pipeline:prepare) authenticates with the operator's ambientghlogin (GITHUB_TOKENfallback) instead of a stored PAT — a deliberateparity deviation; the stored token in
config/secrets/github/config.yamlis deleted with the rest of the CircleCI-era credentials.
PR-CI prerelease publish (deliberate, permanent)
pr.yamlhas aprereleasejob that publishes a namespaced pre-release ofthis gem to RubyGems from the PR branch — a permanent CI feature, not
migration-only. This is a deliberate deviation from CircleCI (which published
nothing pre-merge): it proves the publish path before merge instead of
discovering it broken on
main. The version is<committed-version>.pr<PR>.<run>.<attempt>(via the newprerelease:publishRakefile task), so it can never collide with
main'sversion:bump[pre]sequence; the task builds the gem and pushes it straight to RubyGems, then
restores
version.rb, so nothing is committed, tagged, or pushed(
gem releaseis not used — it aborts on the uncommitted version rewrite).The job is skipped for fork
and Dependabot PRs (they hold no secrets), and
merge-pull-requestdoes notdepend on it. PR pre-release versions accumulate permanently on RubyGems —
accepted.
Do not merge manually — the pipeline merges once checks are green.
Disabling the CircleCI project and deleting the
CircleCIdeploy key aredeferred to the end-of-migration sweep.
🏭 This PR was opened by Foundry, Atomic's AI software development
factory. Implementation, review, and fixes are performed by AI agents;
merges happen automatically once the review and checks gates pass.
This task migrates a Ruby gem's CI from CircleCI to GitHub Actions.
migratemigrate-gem2026-07-22T17-48-59-192Zatomic-foundry-pr · foundry-pipeline: migrate · foundry-task: migrate-gem · foundry-run: 2026-07-22T17-48-59-192Z