Skip to content

Migrate pipeline from CircleCI to GitHub Actions#118

Open
phelma wants to merge 1 commit into
mainfrom
gha-migration
Open

Migrate pipeline from CircleCI to GitHub Actions#118
phelma wants to merge 1 commit into
mainfrom
gha-migration

Conversation

@phelma

@phelma phelma commented Jul 14, 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: loop-guard, check/test, prerelease, release environment gate
  • 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.

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.

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

Verdict: COMMENT

This PR is a faithful, plan-conformant implementation of the Variant A gem migration plan (§4). Independent conformance checking (both directions) found every required change present and correctly parameterised, and nothing changed beyond the plan. All five lenses (correctness, security, safety, standards, code-quality) ran clean of in-scope defects: every substantive finding either targets something the plan or PR description documents as a deliberate decision (the plan's exact YAML, D3, the §1 parity list) — recorded below as plan concerns for a human to revisit the plan, not as defects of the diff — or is a false positive disproven by the D3 token mechanism. Verdict is COMMENT (not APPROVE) solely to draw a human's eye to the queue: max concern before merge; it does not request changes to this diff.

Plan-conformance check (independent of lenses)

  • §4.1/§4.2 workflows match the plan's authoritative YAML. The release job correctly parameterises to version:bump[minor] and omits documentation:update, mirroring this repo's old release.sh exactly as §4.2 requires.
  • §4.3 GPG key git mv is byte-identical; the repo's own unlock-task default is already .github/gpg.private.enc, so the moved key resolves (verified).
  • §4.4 Rakefile: all edits present. §4.5: dev deps live in the Gemfile here (gemspec is runtime-only), so the Gemfile edit is the correct locus; lock drops rake_circle_ci/rake_ssh, adds rake_slack 0.3.0 / rake_github 0.17.0.
  • §4.6 README updated, no badge (correct). §4.7 decommission complete; git-crypt secrets retained.

No plan violations found.

Cross-cutting theme (plan concern)

  • queue: max is not a valid GitHub Actions concurrency key (correctness, safety, standards, code-quality). Native concurrency supports only group + cancel-in-progress; the "keeps every queued run" semantics are not achievable. This is verbatim from the plan's §4.2 YAML (plan lines 292–295) → plan concern, not a defect of this diff, but the highest-value item to fix in the plan fleet-wide.

False positive (counted nowhere)

  • "Dependabot merge lacks [skip ci] → unintended prerelease" (correctness, major) — disproven. The merge runs with GH_TOKEN: secrets.GITHUB_TOKEN, and GITHUB_TOKEN-authored pushes do not trigger new push runs (D3), so main.yaml never starts. No skip-ci marker needed.

Strengths

  • ✅ Faithful, complete plan implementation with correct per-repo parameterisation.
  • github_token resolution fails fast with an actionable message.
  • ✅ Least-privilege token model (top-level contents: read, per-job elevation).
  • ✅ Skip-ci check avoids the ${{ github.event... }} shell-injection sink (env var + quoted [[ ]]).
  • ✅ git-crypt unlock / ENCRYPTION_PASSPHRASE confined to main.yaml, never exposed to PR runs.
  • ✅ Slack notify steps are continue-on-error + !cancelled().

General findings (all plan concerns — do not block this diff)

  • 🟡 Security/Safety: Dependabot auto-merge accepts any update type (D3 / §1 parity — no semver filter by design).
  • 🔵 Security/Safety: release publishes post-approval main HEAD, not the tested SHA (§1 parity with old release.sh).
  • 🟡 Safety: RubyGems publish-before-push inside untouched ./go release (D5 / §1 parity).
  • 🔵 Security: SLACK_BOT_TOKEN in scope for PR jobs running PR-supplied ./go code (inherent to the plan's flat notify-per-job YAML).
  • 🔵 Code Quality: RubyGems credential setup inlined/duplicated across prerelease+release (verbatim plan §4.2 YAML; extracting would deviate).
  • 🔵 Safety: No post-publish smoke test (beyond the plan's specified shape).
  • 🔵 Standards: .yaml vs retired .yml — plan-mandated filenames; conformant.

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 — not a defect of this diff.

The concurrency mapping only accepts group and cancel-in-progress; queue is not part of the GitHub Actions schema, and the "keeps every queued run" guarantee the comment describes is not achievable via native concurrency (at most one running + one pending per group). Depending on parser strictness the key is either silently ignored (serialisation via group still works; lossless queuing does not) or rejected.

This YAML is reproduced verbatim from the family plan's authoritative §4.2 block (plan lines 292–295), so the diff is plan-conformant and this is not an implementation defect. Flagging it as the highest-value item for a human to correct in the plan before fleet-wide rollout: drop queue: max and reword the comment, or implement lossless queuing outside native concurrency.

Comment thread .github/workflows/pr.yaml
continue-on-error: true
run: ./go "slack:notify[${{ job.status }}]"
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

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 — not a defect of this diff.

The check/test/build PR jobs execute ./go/Rakefile code from the PR head while SLACK_BOT_TOKEN is injected into the notify step. Same-repo and dependabot branches receive secrets, so a crafted PR that edits the slack:notify task could read the token (fork PRs are protected). This is inherent to the plan's mandated flat notify-per-job structure; changing it would deviate from the plan. Recorded for the plan owners (e.g. run PR notifications in a workflow_run job that does not execute PR code).

Comment thread .github/workflows/pr.yaml
pull-requests: write
steps:
- name: Merge pull request
run: gh pr merge --merge "$PR_URL"

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 — not a defect of this diff.

The merge-pull-request job auto-merges any dependabot[bot] PR once checks pass, with no semver-type filter. This is documented deliberate in D3 and the §1 parity list ("No update-type filter, also parity; post-migration hardening may add one"). Recorded for the plan owners; consider gating on dependabot/fetch-metadata update-type in post-migration hardening.

False-positive note (counted nowhere): a correctness finding claimed this merge triggers an unintended prerelease because the merge commit lacks [skip ci]. That is disproven — the merge runs with GH_TOKEN: secrets.GITHUB_TOKEN, and GITHUB_TOKEN-authored pushes do not trigger new push workflow runs (D3), so main.yaml never starts. No skip-ci marker is needed.

@phelma phelma marked this pull request as ready for review July 14, 2026 14:16
Copilot AI review requested due to automatic review settings July 14, 2026 14:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Migrates the repository’s CI/CD from CircleCI to GitHub Actions, including removal of CircleCI-specific scripts/config and updating the build/provisioning tooling to use GitHub Actions-compatible tasks and secrets.

Changes:

  • Added GitHub Actions workflows for PR validation and main publishing (prerelease + gated release).
  • Removed CircleCI pipeline configuration and associated scripts/ci/** helper scripts.
  • Updated Ruby tooling and provisioning: switched from CircleCI-era tasks to rake_github + Slack notifications, and refreshed gem dependencies.

Reviewed changes

Copilot reviewed 19 out of 25 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
scripts/ci/steps/test.sh Removed CircleCI test step wrapper (now handled by GitHub Actions).
scripts/ci/steps/release.sh Removed CircleCI release step wrapper (now handled by GitHub Actions).
scripts/ci/steps/prerelease.sh Removed CircleCI prerelease step wrapper (now handled by GitHub Actions).
scripts/ci/steps/merge-pull-request.sh Removed CircleCI dependabot merge helper (replaced with gh pr merge job).
scripts/ci/steps/build.sh Removed CircleCI build step wrapper (now handled by GitHub Actions).
scripts/ci/common/install-slack-deps.sh Removed CircleCI apt-based Slack deps install (not used in Actions).
scripts/ci/common/install-gpg-key.sh Removed CircleCI GPG key install/decrypt script (replaced by rake task + .github/ encrypted key).
scripts/ci/common/install-git-crypt.sh Removed CircleCI git-crypt install script (Actions installs via apt).
scripts/ci/common/install-asdf.sh Removed CircleCI asdf installer script (replaced by asdf_install@v1 action).
scripts/ci/common/install-asdf-dependencies.sh Removed CircleCI asdf install script (handled by asdf_install@v1).
scripts/ci/common/configure-rubygems.sh Removed CircleCI RubyGems credential setup script (Actions configures directly).
scripts/ci/common/configure-git.sh Removed CircleCI git author config script (replaced by repository:set_ci_author).
scripts/ci/common/configure-asdf.sh Removed CircleCI asdf plugin config script (handled by asdf_install@v1).
README.md Updated documentation to reflect CI key management in GitHub Actions (.github/gpg.private.enc).
Rakefile Dropped CircleCI/SSH provisioning, added GitHub secrets/environments provisioning, Slack notifications, and library:build.
Gemfile.lock Updated dependency set (removed CircleCI/SSH gems, added Slack support, upgraded other dependencies).
Gemfile Removed rake_circle_ci/rake_ssh, added rake_slack.
.github/workflows/pr.yaml Added PR workflow (check/test/build + dependabot auto-merge).
.github/workflows/main.yaml Added main-branch workflow (check/test + prerelease + environment-gated release).
.github/gpg.private.enc Added encrypted GPG private key artifact for CI git-crypt unlock flow.
.circleci/config.yml Removed CircleCI configuration (migration/decommission).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .github/workflows/pr.yaml
Comment on lines +62 to +66
- name: Merge pull request
run: gh pr merge --merge "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Comment thread .github/workflows/pr.yaml
Comment on lines +17 to +22
- name: Notify Slack
if: ${{ !cancelled() }}
continue-on-error: true
run: ./go "slack:notify[${{ job.status }}]"
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
Comment thread .github/workflows/pr.yaml
Comment on lines +32 to +37
- name: Notify Slack
if: ${{ !cancelled() }}
continue-on-error: true
run: ./go "slack:notify[${{ job.status }}]"
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
Comment thread .github/workflows/pr.yaml
Comment on lines +47 to +52
- name: Notify Slack
if: ${{ !cancelled() }}
continue-on-error: true
run: ./go "slack:notify[${{ job.status }}]"
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
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.

2 participants