Skip to content

Migrate pipeline from CircleCI to GitHub Actions - #119

Merged
phelma merged 1 commit into
mainfrom
gha-migration
Jul 22, 2026
Merged

Migrate pipeline from CircleCI to GitHub Actions#119
phelma merged 1 commit into
mainfrom
gha-migration

Conversation

@phelma

@phelma phelma commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Cutover to GitHub Actions per the Variant A family plan (gem pilot).
Includes decommission — merging this PR completes the repo's migration.

  • main + pr workflows: check/test, prerelease, release environment gate
  • PR CI publishes a namespaced pre-release to RubyGems (see below)
  • git-crypt unlock on the runner; encrypted CI GPG key moved to .github/
  • Slack notifications via rake_slack; dependabot auto-merge job
  • Rakefile provisioning swapped to rake_github secrets/environments; rake_circle_ci dropped
  • CircleCI pipeline removed: .circleci/, scripts/ci/, the CI SSH deploy
    key pair and its keys:deploy/deploy_keys provisioning, and the stored
    CircleCI/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 release publishes to RubyGems before the version-bump commit is
    pushed — pre-existing ordering inside the untouched release logic.
  • Prerelease publishes on every push to main with no approval gate; only
    full releases are gated (environment: release).
  • Dependabot auto-merge accepts any update type that passes checks, and the
    merge does not trigger a release build — on CircleCI the merge commit
    carried [skip ci], so this matches. Updates ship with the next
    human-triggered release.
  • The release job pulls main at approval time, so a delayed approval
    publishes main as it stands then, not the SHA this run tested — parity with
    the old release.sh (which also pulled; prerelease.sh did not, so the
    prerelease job has no pull).
  • asdf_install@v1 is our own action (infrablocks/github-actions); we are
    happy tracking its major version tag.
  • Job scaffolding is repeated flat per job by design: the logic lives in the
    build system (./go/rake) and CI stays lean — it just triggers tasks and
    supplies secrets/context.
  • Gemfile.lock carries transitive major bumps — the unavoidable resolution
    of the targeted bundle lock --update, not scope creep.
  • Small library hunks may appear where the refreshed toolchain's rubocop
    autocorrects existing code (e.g. Style/ArgumentsForwarding) — required
    by the library:check verification gate, not drive-by refactoring.
  • Provisioning (pipeline:prepare) authenticates with the operator's ambient
    gh login (GITHUB_TOKEN fallback) instead of a stored PAT — a deliberate
    parity deviation; the stored token in config/secrets/github/config.yaml
    is deleted with the rest of the CircleCI-era credentials.

PR-CI prerelease publish (deliberate, permanent)

pr.yaml has a prerelease job that publishes a namespaced pre-release of
this 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 new prerelease:publish
Rakefile task), so it can never collide with main's version: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 release is 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-request does not
depend 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 CircleCI deploy key are
deferred 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.

Pipeline Task pack Family Run
migrate migrate-gem gem 2026-07-22T17-48-59-192Z

atomic-foundry-pr · foundry-pipeline: migrate · foundry-task: migrate-gem · foundry-run: 2026-07-22T17-48-59-192Z

@phelma phelma left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Code Review: #119 - Migrate pipeline from CircleCI to GitHub Actions

Verdict: COMMENT

This is a clean, plan-conformant Variant A cutover. Across six lenses the diff
holds up strongly: untrusted PR metadata is passed via env vars (never
interpolated into shells), secret-bearing jobs are correctly gated to same-repo
non-Dependabot PRs, permissions are least-privilege, the release-loop guard
([ci skip] bump commits) is preserved, and the Rakefile helpers fail fast with
actionable errors. Every substantive lens finding lands on a decision the family
plan or PR description documents as deliberate, so none block; they are retained
below as plan concerns for a human to revisit fleet-wide.

Cross-Cutting Themes

  • queue: max concurrency key (flagged by: correctness, safety) — Both
    lenses read queue: max as an unsupported concurrency key that would invalidate
    main.yaml. This is a false positive against the plan: family plan §4.2
    documents queue: max as a GA GitHub Actions feature (2026-05-07,
    github.blog/changelog/2026-05-07-github-actions-concurrency-groups-now-allow-larger-queues)
    and includes it in the authoritative YAML. The reviewing models' training
    predates that GA. Recorded as a plan concern, not a defect — worth a human
    confirming the feature is live before merge, but it does not block a
    plan-conformant diff.
  • Divergence from the rake_slack reference (flagged by: standards,
    architecture) — notify-step idiom (!cancelled() + continue-on-error vs
    always()), job name (merge-pull-request vs dependabot-auto-merge), omitted
    skip-ci-check job, and per-file check/test duplication. Each is a documented
    Variant A decision (§3 "the §4 YAML is authoritative where they differ", §4.1
    job-name note, D4 loop-guard rationale, §1 flat-scaffolding parity item).

Strengths

  • ✅ Untrusted PR metadata (number, title, URL, head SHA) passed via env vars,
    explicitly "never interpolated" — no Actions expression injection into shells.
  • ✅ Prerelease (secret-unlocking) job double-gated on same-repo + non-Dependabot;
    fork/Dependabot PRs cannot reach ENCRYPTION_PASSPHRASE or RubyGems creds.
  • ✅ Least-privilege: top-level contents: read, write scoped only where needed.
  • ✅ Release-loop guard preserved: [ci skip] bump commits + GITHUB_TOKEN push
    semantics stop main pushes re-triggering the workflow.
  • --match-head-commit TOCTOU guard and --ff-only pull on the release job.
  • read_ci_passphrase git-crypt magic-byte guard prevents uploading ciphertext
    as a secret; resolve_github_token fails fast instead of an opaque Octokit 401.
  • prerelease:publish wraps the version.rb rewrite in ensure so the tree is
    always restored and the built gem removed, even on push failure.
  • ✅ Secret-file deletions scoped correctly: only CircleCI-era credentials removed;
    RubyGems creds, GPG key, and encryption passphrase retained.

General Findings

  • 🔵 Code Quality: resolve_github_token/read_ci_passphrase are bare
    top-level defs (private methods on Object) — a conventional Rakefile shortcut;
    a mild namespace smell only if helper count grows. (In-scope suggestion.)
  • 🔵 Correctness: main.yaml prerelease git push runs on a checkout with no
    explicit ref:; actions/checkout@v4 resolves a branch for push events so this
    is fine in practice — flagged low-confidence, no change required. (In-scope suggestion.)

Plan Concerns (documented-deliberate — do not block)

  • 🔵 queue: max reported as invalid — plan §4.2 documents it as a GA feature.
  • 🔵 asdf_install@v1 mutable tag in secret-bearing jobs — §1: our own action,
    tracking the major tag is fine.
  • 🔵 Dependabot auto-merge accepts any update type — D3: no update-type filter, parity.
  • 🔵 Pending release approval blocks later prereleases — §4.2 documents this as the
    intended behaviour of the shared job-level main group.
  • 🔵 Notify idiom (!cancelled() + continue-on-error) diverges from rake_slack's
    always() — §3: the §4 YAML is authoritative.
  • 🔵 Job named merge-pull-request vs canonical dependabot-auto-merge — §4.1
    Variant A decision.
  • 🔵 skip-ci-check job omitted — D4: no custom loop-guard needed.
  • 🔵 Regex/backreference version.rb rewrite is fragile — plan §4.4 step 8 specifies
    this exact implementation.
  • 🔵 Secret/tooling setup split between rake tasks and surviving shell scripts —
    §4.7 mandates retaining and calling install-git-crypt.sh/configure-rubygems.sh.
  • 🔵 check/test duplicated across pr.yaml/main.yaml — §1 flat-scaffolding parity.
  • 🔵 build verified on PR but not main — plan §4.2 specifies main's job set
    (check/test/prerelease/release).

Plan-Conformance (independent §4 check)

Verified both directions. All required §4 changes are present and correct:
workflows (§4.1/4.2), the byte-identical GPG key move (§4.3), all Rakefile edits
(§4.4 — requires, RakeCircleCI/keys:deploy removal, token/passphrase/secrets/
environments, slack, set_ci_author, library:build, pipeline:prepare,
prerelease:publish), Gemfile/lockfile (§4.5), README (§4.6), and the full CircleCI
decommission (§4.7). The release job correctly uses version:bump[minor] and omits
documentation:update (both mirror this repo's old release.sh). rake_git is
pinned ~> 0.2.0 to avoid the 0.3.0.pre.2 prerelease — exactly per §4.5/§5
check 10. .circleci grep hits remaining are only library source/spec (an example
string and tests), which §4.8 leaves alone. Nothing beyond the plan changed.

Per-Lens Results

Correctness

Summary: Task names, arguments, and interpolation are almost entirely correct;
slack:notify arity, git_crypt:unlock, version:bump, prerelease:publish, and routing
rules all resolve. Only flagged queue: max (false positive — see above).

Security

Summary: Written with clear security awareness — env-var facts, double-gated
prerelease, least-privilege permissions, pull_request (not _target),
--match-head-commit. Residual: mutable-tag action and no update-type filter (both
plan-documented).

Safety

Summary: Release-loop guard preserved, --ff-only/--match-head-commit,
ensure-restored version rewrite, correctly-scoped secret deletions. queue: max
and approval-blocks-prerelease flagged (both plan-documented).

Standards

Summary: Closely follows the rake_slack canonical shape (alphabetical deps,
routing block, task shapes). Notify idiom, merge-job name, and omitted skip-ci-check
diverge — all plan-documented.

Code Quality

Summary: High-quality Rakefile — small well-named helpers, strong fail-fast
error handling. Minor: fragile regex version rewrite (plan-specified) and top-level
helper namespace pollution.

Architecture

Summary: Clean job graph, sound dependency direction (CI depends on rake),
well-reasoned job-level concurrency and independent dependabot path. Notes: rake/shell
logic split, cross-file check/test duplication, build-on-PR-not-main — all plan-scoped.


Review generated by /accelerator:review-pr

@phelma
phelma marked this pull request as ready for review July 22, 2026 18:16
@phelma
phelma merged commit dc2473c into main Jul 22, 2026
5 of 6 checks passed
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