Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 0 additions & 176 deletions .circleci/config.yml

This file was deleted.

File renamed without changes.
178 changes: 178 additions & 0 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
name: Main
on:
push:
branches:
- main

permissions:
contents: read

# Concurrency is JOB-level, on prerelease + release only (shared group), NOT
# workflow-level: a run waiting on the release approval counts as the group's
# in-progress occupant, so a workflow-level group would freeze ALL main CI
# (check/test/prerelease of later pushes) for up to the 30-day approval
# window. The approval itself lives on the slot-free release-gate job, so a
# pending approval holds no slot. queue: max keeps every queued publish (the
# default keeps only the newest pending) so close-together merges serialise
# FIFO on tag operations instead of cancelling each other.
# 'queue: max' — GA 2026-05-07: https://github.blog/changelog/2026-05-07-github-actions-concurrency-groups-now-allow-larger-queues/
# Gate/slot split pattern: https://github.com/orgs/community/discussions/17401

jobs:
skip-ci-check:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
should_skip_ci: ${{ steps.skip_ci_check.outputs.should_skip_ci }}
steps:
- id: skip_ci_check
env:
HEAD_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
run: |
if [[ "$HEAD_COMMIT_MESSAGE" == *"[no ci]"* ]] \
|| [[ "$HEAD_COMMIT_MESSAGE" == *"[skip ci]"* ]] \
|| [[ "$HEAD_COMMIT_MESSAGE" == *"[ci skip]"* ]]; then
echo "should_skip_ci=true" >> "$GITHUB_OUTPUT"
else
echo "should_skip_ci=false" >> "$GITHUB_OUTPUT"
fi

check:
needs: [skip-ci-check]
if: needs.skip-ci-check.outputs.should_skip_ci != 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Install tools
uses: infrablocks/github-actions/asdf_install@v1
- name: Check
run: ./go test:code:check
- name: Notify Slack
if: ${{ !cancelled() }}
continue-on-error: true
run: ./go "slack:notify[${{ job.status }}]"
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

test:
needs: [skip-ci-check]
if: needs.skip-ci-check.outputs.should_skip_ci != 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install tools
uses: infrablocks/github-actions/asdf_install@v1
- name: Ensure docker-compose is available
# Pinned + checksum-verified: this runs as root on runners that later
# hold secrets; the checksum is from the release's own .sha256 asset.
run: |
if ! command -v docker-compose >/dev/null 2>&1; then
sudo curl -fsSL \
"https://github.com/docker/compose/releases/download/v5.3.1/docker-compose-linux-x86_64" \
-o /usr/local/bin/docker-compose
echo "f9ebc6ebdb19d769b793c245a736caaeb198c62587f13b25c660c13b4987f959 /usr/local/bin/docker-compose" \
| sha256sum -c -
sudo chmod +x /usr/local/bin/docker-compose
Comment on lines +73 to +79

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

🔵 Security / Safety — plan concern (§4.1/§4.2 prescribe this verbatim)

To be clear up front: the pin itself checks out. I verified docker/compose v5.3.1 exists and that the release's published .sha256 asset reads exactly f9ebc6ebdb19d769b793c245a736caaeb198c62587f13b25c660c13b4987f959 — the comment's provenance claim is accurate. This note is purely about ordering.

curl writes directly to /usr/local/bin/docker-compose and only then pipes the digest to sha256sum -c -. Verify-after-install inverts the safe idiom: the unverified artefact occupies the final, sudo-owned install location on PATH during the verification window.

Impact: low in practice — chmod +x never runs on a failed verification and runners are ephemeral — but any future reordering, retry, or added PATH consumer between the two commands turns a checksum failure into an executed artefact. This step runs as root on runners that later hold ENCRYPTION_PASSPHRASE.

Suggestion: download → verify → install, fleet-wide in the family plan (the same block is in pr.yaml):

tmp=$(mktemp)
curl -fsSL "$URL" -o "$tmp"
echo "$SHA  $tmp" | sha256sum -c -
sudo install -m 0755 "$tmp" /usr/local/bin/docker-compose

fi
- name: Test
run: ./go test:integration
- name: Notify Slack
if: ${{ !cancelled() }}
continue-on-error: true
run: ./go "slack:notify[${{ job.status }}]"
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

prerelease:
needs: [check, test]
runs-on: ubuntu-latest
timeout-minutes: 30
concurrency:
group: main-tags
cancel-in-progress: false
queue: max
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install tools
uses: infrablocks/github-actions/asdf_install@v1
- name: Install secrets tools
run: sudo apt-get update && sudo apt-get install -y git-crypt gnupg
- name: Unlock git-crypt
run: ./go git_crypt:unlock_with_encrypted_gpg_key
env:
ENCRYPTION_PASSPHRASE: ${{ secrets.ENCRYPTION_PASSPHRASE }}
- name: Set CI git author
run: ./go repository:set_ci_author
- name: Prerelease
run: ./go "version:bump[rc]" && ./go image:publish
- name: Notify Slack of release hold
continue-on-error: true
run: ./go "slack:notify[success,on_hold]"
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
- name: Notify Slack
if: ${{ !cancelled() }}
continue-on-error: true
run: ./go "slack:notify[${{ job.status }}]"
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

release-gate:
# Approval-only job: carries the environment but NO concurrency group, so
# a pending approval never occupies the main-tags slot. Multiple pushes
# can await approval independently.
needs: [prerelease]
runs-on: ubuntu-latest
timeout-minutes: 10
environment: release
steps:
- name: Release approved
run: "true"

release:
needs: [release-gate]
runs-on: ubuntu-latest
timeout-minutes: 30
concurrency:
group: main-tags
cancel-in-progress: false
queue: max
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
- name: Pull latest main
# Approval can land long after the run starts; release publishes main
# as of approval time, not the tested SHA — parity with the old
# release.sh, which also pulled. prerelease.sh never pulled, so the
# prerelease job deliberately has no pull.
run: git pull
Comment on lines +155 to +160

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

🔵 Correctness — plan concern (§4.2 prescribes this step and its comment verbatim)

This git pull is a guaranteed no-op. The preceding actions/checkout@v4 already uses ref: main with fetch-depth: 0, and it runs after the approval gate and after the job acquires the main-tags slot — so it already fetches main as of approval time. Nothing can land between the checkout and the pull. The behaviour the comment describes ("release publishes main as of approval time, not the tested SHA") is delivered by ref: main, not by the pull; the old release.sh needed the pull only because CircleCI had checked out the triggering SHA.

Impact: no behavioural difference, so nothing is broken. It is dead code adding a small failure surface, and it runs before Set CI git author — in the old pipeline configure-git.sh ran before release.sh's pull, so if a pull ever did need a merge commit it would die with "unable to auto-detect email address" where CircleCI would not. Unreachable today (local main is always equal to or behind origin/main, so the pull fast-forwards at worst).

Suggestion: for the family plan, either drop the step (ref: main already achieves the stated parity) or move Set CI git author above it to match the ordering of the pipeline it reproduces.

- name: Install tools
uses: infrablocks/github-actions/asdf_install@v1
- name: Install secrets tools
run: sudo apt-get update && sudo apt-get install -y git-crypt gnupg
- name: Unlock git-crypt
run: ./go git_crypt:unlock_with_encrypted_gpg_key
env:
ENCRYPTION_PASSPHRASE: ${{ secrets.ENCRYPTION_PASSPHRASE }}
- name: Set CI git author
run: ./go repository:set_ci_author
- name: Release
run: ./go version:release && ./go image:publish
- name: Notify Slack
if: ${{ !cancelled() }}
continue-on-error: true
run: ./go "slack:notify[${{ job.status }}]"
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
Loading
Loading