Skip to content

Migrate pipeline from CircleCI to GitHub Actions#106

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

Migrate pipeline from CircleCI to GitHub Actions#106
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-23T15-01-57-210Z

atomic-foundry-pr · foundry-pipeline: migrate · foundry-task: migrate-gem · foundry-run: 2026-07-23T15-01-57-210Z

@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: #106 - Migrate pipeline from CircleCI to GitHub Actions

Verdict: COMMENT

This is a clean, plan-conformant CircleCI → GitHub Actions cutover with notably strong security hygiene (all untrusted PR-controlled values passed via env: rather than interpolated into run:, least-privilege permissions:, a correct fork/Dependabot guard on the prerelease job, and explicit git-crypt-ciphertext guards in provisioning). Across five lenses (correctness, security, safety, standards, code quality) no in-scope critical or major defects were found; the substantive findings are documented deliberate decisions from the family plan (plan concerns) plus two small in-scope defence-in-depth nits. The diff faithfully implements every §4 change and nothing beyond the plan was altered.

Strengths

  • ✅ Untrusted PR data (title, number, head SHA, URL) is passed exclusively via env: and referenced as shell variables — closing the classic Actions script-injection vector.
  • ✅ The prerelease job guard (head.repo.full_name == github.repository && user.login != 'dependabot[bot]') correctly excludes fork and Dependabot PRs and uses the immutable user.login, not the spoofable github.actor.
  • ✅ Least-privilege permissions: top-level contents: read with per-job elevation only where needed.
  • ✅ Provisioning refuses to upload the passphrase if the file is missing or still git-crypt ciphertext (\x00GITCRYPT), and resolves the GitHub token fail-fast from ambient auth rather than a checked-in PAT.
  • prerelease:publish validates all three args and restores version.rb in an ensure block; the release job uses git pull --ff-only to fail loud.
  • ✅ Full decommission verified: deleted secrets/scripts orphan nothing still referenced; retained rubygems credentials, passphrase and GPG keys remain in use.

General Findings

  • 🔵 Security (minor): scripts/ci/common/configure-rubygems.sh no longer runs git crypt unlock (unlock is now an explicit earlier workflow step) but still blindly copies config/secrets/rubygems/credentials to ~/.gem/credentials with no ciphertext guard — unlike the Rakefile passphrase handling this PR hardens. Not exploitable today (all jobs unlock first); defence-in-depth only. Consider mirroring the \x00GITCRYPT guard.
  • 🔵 Security (suggestion): the Slack notify steps interpolate ${{ job.status }} into run: — safe here (GitHub-controlled enum) but inconsistent with the "never interpolate into run:" convention the rest of the diff follows. Could pass via env: for uniformity.

Plan Concerns (documented-deliberate; non-blocking — for the human to revisit the plan)

  • 🔵 queue: max in the concurrency: blocks (correctness lens rated critical). Flagged as an unknown key that would make Actions reject the workflow. However this is prescribed verbatim by plan §4.2, which cites a GitHub changelog (2026-05-07: concurrency groups allow larger queues). The diff reproduces the plan's authoritative YAML exactly, so this is a plan concern, not a defect of the diff. Worth a human double-check that queue: max is a currently-valid concurrency key before merge — an invalid key would fail the whole main pipeline — but any fix belongs in the plan, fleet-wide.
  • 🔵 Provisioning block embeds procedural logic (code-quality). The RakeGithub block inlines token resolution + git-crypt guard, needing a # rubocop:disable Metrics/BlockLength. Extracting helpers is the natural quality move but the plan prescribes this inline code and says "do not restructure anything else in the Rakefile".
  • 🔵 Duplicated argument-name list in prerelease:publish (code-quality) — reproduced verbatim from plan §4.4 step 8.
  • 🔵 Slack routing inline-comment spacing (standards) — one space vs two, reproduced exactly from the plan's template text.

False Positives (verified, counted nowhere)

  • GPG key path discovery (safety): concern that .github/gpg.private.enc might not be found by the unlock task. Verified against rake_git_crypt 0.4.0: encrypted_key_path defaults to .github/gpg.private.enc — exactly the new location. Correct as-is.

Review generated by /accelerator:review-pr

concurrency:
group: main
cancel-in-progress: false
queue: max

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.

🔵 Plan concern / verify (correctness) — The concurrency: block uses queue: max (also in the release job). The correctness lens flagged this as an unknown key that would make Actions reject the workflow — historically concurrency: only accepts group and cancel-in-progress. This is prescribed verbatim by the family plan (§4.2, citing a 2026-05-07 GitHub changelog adding larger queues), so it is treated as a documented deliberate decision rather than a defect of this diff. Flagging for a human to confirm queue: max is a currently-valid concurrency option before merge — if it is not, the entire main pipeline would fail to run. Any change belongs in the plan (fleet-wide), not this PR.

Comment thread Rakefile
end

RakeGithub.define_repository_tasks(
RakeGithub.define_repository_tasks( # rubocop:disable Metrics/BlockLength

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.

🔵 Plan concern (code-quality) — The RakeGithub.define_repository_tasks block now inlines token resolution and the git-crypt passphrase guard, which pushed it over the length threshold and required the # rubocop:disable Metrics/BlockLength. Extracting resolve_github_token / read_ci_passphrase helpers would make the routines independently testable and likely remove the suppression — but the plan prescribes this inline code and instructs "do not restructure anything else in the Rakefile", so this is a plan concern for a future fleet-wide revisit, not a change for this PR.

@phelma
phelma marked this pull request as ready for review July 23, 2026 15:30
@phelma
phelma merged commit 46c2d84 into main Jul 23, 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