Skip to content

Migrate pipeline from CircleCI to GitHub Actions - #169

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

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

Conversation

@phelma

@phelma phelma commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Part of PP-709.

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

Verdict: COMMENT

This is a clean, plan-conformant CircleCI→GitHub Actions cutover. The five lenses (correctness, security, safety, standards, code quality) found no in-scope critical or major defects: PR-controlled inputs are consistently passed via env rather than interpolated into shell, the secret-bearing prerelease job is correctly gated to same-repo non-Dependabot PRs on pull_request (not pull_request_target), the Rakefile provisioning adds robust git-crypt-ciphertext guards, and the prerelease:publish task restores version.rb in an ensure block. An independent check of the diff against family-plan §4 found every required change present, nothing beyond the plan changed (the sole unlisted change — a .rubocop.yml BlockLength exclusion for define_repository_tasks — is a legitimate consequence of the enlarged RakeGithub block, needed for library:check to pass), and no CircleCI residue.

Cross-Cutting Themes

  • queue: max concurrency key (flagged by: correctness, safety) — Flagged as an invalid Actions schema key. This is a plan concern, not a defect: family plan §4.2 mandates queue: max verbatim and cites its GA (2026-05-07 changelog). The diff faithfully reproduces the authoritative plan YAML, so it is plan-conformant. Recorded for a human to revisit the plan if the feature name/behaviour differs from what was assumed.

Strengths

  • ✅ Untrusted PR-controlled values (title, number, head SHA) passed via env, never interpolated into run: — closes the GHA script-injection vector.
  • ✅ Prerelease job double-gated (head.repo.full_name == github.repository AND user.login != 'dependabot[bot]'); dependabot auto-merge gates on the immutable user.login, not the spoofable github.actor.
  • prerelease:publish restores version.rb and removes the built gem in an ensure block; the passphrase guard rejects git-crypt ciphertext before upload; token resolution fails fast with actionable messages.
  • ✅ Every job carries an explicit timeout-minutes; release uses git pull --ff-only to fail loudly on non-fast-forward.
  • ✅ Requires and dev-dependencies remain alphabetical; release job correctly omits documentation:update to match the repo's original release.sh.

General Findings

  • 🔵 Security: checkout@v4 in the prerelease job persists the workflow token by default; with: persist-credentials: false removes a residual token from the runner (suggestion, inline).
  • 🔵 Security: Slack notify steps interpolate job.status into the run: string; not attacker-controllable today, but passing it via env would keep the codebase uniformly free of context-into-run: interpolation (suggestion).
  • 🔵 Code Quality: the prerelease version_pattern regex is coupled to the single-quoted VERSION = '...' style and unanchored (suggestion, inline).
  • 🔵 Standards: notify guards use !cancelled() where the rake_slack reference uses always(); internally consistent and arguably an improvement — flagged only as conscious divergence (suggestion).

Plan Concerns (documented-deliberate — do not block)

  • 🟡 queue: max (correctness/safety) — plan §4.2 mandates it and cites GA.
  • 🔵 "while this pipeline beds in" comment (standards) — plan §4.4 step 4 explicitly instructs keeping this exact comment; flagging it contradicts the plan.
  • 🔵 Triplicated unlock/rubygems provisioning steps (code-quality) — the flat per-job scaffolding is a deliberate §1 decision (logic lives in ./go/rake, CI stays lean).

Noted Non-Issue (false positive)

  • The prerelease version can stack a second prerelease marker (0.8.0.pre.2.pr169.5.1) when the committed base is itself .pre.N. This is a valid Gem::Version and cannot collide with main's sequence — no defect.

Review generated by /accelerator:review-pr

Comment thread .github/workflows/pr.yaml
group: pr-prerelease-${{ github.event.pull_request.number }}
cancel-in-progress: false
steps:
- uses: actions/checkout@v4

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.

🔵 Security (suggestion)

The prerelease job runs PR-branch code after actions/checkout@v4, which by default persists the workflow GITHUB_TOKEN in .git/config on the runner. The top-level permission is contents: read so the residual token is low-value, but arbitrary same-repo PR-branch code executes in the same job that also holds ENCRYPTION_PASSPHRASE and the RubyGems credential.

Suggestion: add with: persist-credentials: false to this checkout — the job publishes via the RubyGems credential, not the checkout token.

Comment thread Rakefile
end

version_file = 'lib/rake_process_manager/version.rb'
version_pattern = /(VERSION\s*=\s*')([^']+)(')/

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 Quality (suggestion)

version_pattern = /(VERSION\s*=\s*')([^']+)(')/ matches only a single-quoted VERSION assignment and is unanchored. If version.rb is reformatted (double quotes) or a VERSION-prefixed token appears earlier, the match silently fails (caught by unless base) or captures the wrong token (not caught).

Suggestion: anchor and accept either quote style, e.g. /^(\s*VERSION\s*=\s*['"])([^'"]+)(['"])/, or add a brief comment noting the pattern assumes the current single-quoted format.

@phelma
phelma marked this pull request as ready for review July 22, 2026 19:29
@phelma
phelma merged commit 185d30c into main Jul 22, 2026
5 of 6 checks passed
@phelma
phelma deleted the gha-migration branch July 22, 2026 19:29
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