Skip to content

Commit eac2394

Browse files
Merge pull request #135 from CodeForPhilly/feat/release-flow
ci(release): add develop→main Release-PR automation
2 parents 9c7b5fa + 517cb14 commit eac2394

7 files changed

Lines changed: 268 additions & 1 deletion

File tree

‎.github/workflows/ci.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: CI
22

33
on:
44
push:
5-
branches: [main]
5+
branches: [main, develop]
66
pull_request:
77

88
concurrency:
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: "Container: Publish Image"
2+
3+
# Builds and pushes the runtime image to GHCR when a vX.Y.Z tag is pushed
4+
# (by release-publish.yml). Tags the image :vX.Y.Z and :latest.
5+
#
6+
# GitHub-hosted runners are amd64 and the cluster nodes are amd64, so no
7+
# --platform flag is needed here (it's only required for local Apple-silicon
8+
# builds — see docs/operations/sandbox-deploy.md).
9+
10+
on:
11+
push:
12+
tags: ["v*"]
13+
14+
permissions:
15+
contents: read
16+
packages: write
17+
18+
concurrency:
19+
group: "container-publish-${{ github.ref }}"
20+
cancel-in-progress: false
21+
22+
jobs:
23+
container-publish:
24+
name: Build and Push
25+
runs-on: ubuntu-latest
26+
steps:
27+
- uses: actions/checkout@v6
28+
29+
- name: Login to ghcr.io
30+
uses: docker/login-action@v3
31+
with:
32+
registry: ghcr.io
33+
username: ${{ github.repository_owner }}
34+
password: ${{ secrets.GITHUB_TOKEN }}
35+
36+
- name: Compute image address
37+
run: |
38+
DOCKER_IMAGE="ghcr.io/${GITHUB_REPOSITORY,,}"
39+
DOCKER_TAG="${GITHUB_REF#refs/tags/}"
40+
echo "DOCKER_IMAGE=${DOCKER_IMAGE}" >> "$GITHUB_ENV"
41+
echo "DOCKER_TAG=${DOCKER_TAG}" >> "$GITHUB_ENV"
42+
echo "Publishing ${DOCKER_IMAGE}:${DOCKER_TAG} (+ :latest)"
43+
44+
- name: Pull previous image for layer cache
45+
run: docker pull "${DOCKER_IMAGE}:latest" || true
46+
47+
- name: Build image
48+
run: |
49+
docker build \
50+
--cache-from "${DOCKER_IMAGE}:latest" \
51+
--tag "${DOCKER_IMAGE}:latest" \
52+
--tag "${DOCKER_IMAGE}:${DOCKER_TAG}" \
53+
.
54+
55+
- name: Push versioned tag
56+
run: docker push "${DOCKER_IMAGE}:${DOCKER_TAG}"
57+
58+
- name: Push latest
59+
run: docker push "${DOCKER_IMAGE}:latest"
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: "Release: Prepare PR"
2+
3+
# Pushing `develop` opens (or updates) a "Release: vX.Y.Z" PR into `main` with a
4+
# bot-generated changelog. Merging that PR publishes (see release-publish.yml).
5+
6+
on:
7+
push:
8+
branches: [develop]
9+
10+
permissions:
11+
contents: read
12+
pull-requests: write
13+
14+
concurrency:
15+
group: "release-prepare"
16+
cancel-in-progress: true
17+
18+
jobs:
19+
release-prepare:
20+
runs-on: ubuntu-latest
21+
steps:
22+
- uses: JarvusInnovations/infra-components@channels/github-actions/release-prepare/latest
23+
with:
24+
github-token: ${{ secrets.GITHUB_TOKEN }}
25+
release-branch: main
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: "Release: Publish PR"
2+
3+
# When a "Release: v*" PR into `main` is merged, this creates the `vX.Y.Z` git
4+
# tag (and GitHub release). The tag push then triggers container-publish.yml.
5+
#
6+
# Must use BOT_GITHUB_TOKEN, not GITHUB_TOKEN: a tag pushed with the default
7+
# GITHUB_TOKEN cannot trigger another workflow (container-publish), so the
8+
# image would never build.
9+
10+
on:
11+
pull_request:
12+
branches: [main]
13+
types: [closed]
14+
15+
jobs:
16+
release-publish:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- uses: JarvusInnovations/infra-components@channels/github-actions/release-publish/latest
20+
with:
21+
github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: "Release: Validate PR"
2+
3+
# Keeps the open "Release: v*" PR's title/version/changelog well-formed as it
4+
# changes. Runs on the PR into `main`.
5+
6+
on:
7+
pull_request:
8+
branches: [main]
9+
types: [opened, edited, reopened, synchronize]
10+
11+
jobs:
12+
release-validate:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: JarvusInnovations/infra-components@channels/github-actions/release-validate/latest
16+
with:
17+
github-token: ${{ secrets.GITHUB_TOKEN }}

‎docs/operations/releases.md‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Releases
2+
3+
This repo uses the Jarvus **develop→main Release-PR** flow (the
4+
`JarvusInnovations/infra-components` `release-prepare` / `release-validate` /
5+
`release-publish` composite actions). Versioned releases are cut from a
6+
changelog'd PR; merging it tags the release and publishes the container image.
7+
8+
## The flow
9+
10+
```
11+
feature branch ──▶ develop ──(push)──▶ "Release: vX.Y.Z" PR into main
12+
│ (review changelog, adjust bump)
13+
▼ (merge)
14+
tag vX.Y.Z ──▶ GHCR image :vX.Y.Z + :latest
15+
```
16+
17+
1. **Merge work into `develop`.** Feature branches PR into `develop` (CI runs on
18+
`develop` and on every PR). **Only Release PRs ever target `main`** — that's
19+
what `release-validate` enforces: it fails any PR into `main` whose title
20+
isn't `Release: vX.Y.Z`, so a stray feature-PR-into-`main` is caught. (The
21+
one exception is the very first bootstrap PR that introduces these workflows,
22+
which necessarily merges to `main` directly and trips that check once.)
23+
2. **Push `develop`.** `release-prepare.yml` opens (or updates) a
24+
**`Release: vX.Y.Z`** PR into `main` with a bot-generated `## Changelog`
25+
comment. The version is computed from the last `v*` tag + the commits since.
26+
3. **Review the Release PR.** Sort the changelog, confirm the semver bump
27+
(edit the PR title to override the version if needed — `release-validate.yml`
28+
keeps it well-formed). Use the **`release-flow`** skill for the changelog +
29+
bump conventions.
30+
4. **Merge the Release PR.** `release-publish.yml` creates the `vX.Y.Z` tag and
31+
GitHub release. The tag push triggers `container-publish.yml`, which builds
32+
and pushes `ghcr.io/codeforphilly/codeforphilly-ng:vX.Y.Z` and `:latest`.
33+
5. **Deploy.** The cluster picks up the image per
34+
[deploy.md](./deploy.md) (the published `:vX.Y.Z` / `:latest` tags replace
35+
the previously-manual `:sandbox` build for versioned releases).
36+
37+
## Prerequisites (one-time)
38+
39+
- **`BOT_GITHUB_TOKEN`** repo (or org) secret — a PAT/app token with `repo`
40+
scope. Required by `release-publish`: a tag pushed with the default
41+
`GITHUB_TOKEN` cannot trigger `container-publish`, so the image would never
42+
build.
43+
- **GHCR package write** for Actions — the first `container-publish` run creates
44+
the package; ensure `github.com/CodeForPhilly/codeforphilly-ng` →
45+
Packages grants the repo's Actions write access (the workflow uses
46+
`permissions: packages: write`).
47+
- **Branch protection on `main`** (recommended) — require a PR + green CI to
48+
merge, so releases only land via the Release PR.
49+
50+
## First release
51+
52+
There are no tags yet, so the first push to `develop` proposes **`v0.1.0`**. If
53+
you want a different baseline, edit the Release PR title before merging.
54+
55+
## Notes
56+
57+
- The manual `docker build --platform=linux/amd64 … :sandbox` path
58+
([sandbox-deploy.md](./sandbox-deploy.md)) still works for ad-hoc iteration;
59+
versioned releases now go through `container-publish` instead.
60+
- CI runners and cluster nodes are both amd64, so `container-publish` needs no
61+
`--platform` flag (that's only for local Apple-silicon builds).

‎plans/release-flow.md‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
---
2+
status: done
3+
depends: []
4+
specs: []
5+
issues: []
6+
pr: 135
7+
---
8+
9+
# Plan: stand up develop→main Release-PR automation
10+
11+
## Scope
12+
13+
Adopt the Jarvus develop→main Release-PR flow so versioned releases are cut from
14+
a changelog'd PR, and the GHCR image build (currently manual) is automated on
15+
tag. Replaces the ad-hoc `docker build/push :sandbox` step for versioned
16+
releases.
17+
18+
What ships:
19+
20+
- **Four workflows** under `.github/workflows/`:
21+
- `release-prepare.yml` — push to `develop` opens/updates a `Release: vX.Y.Z`
22+
PR into `main` with a bot changelog (`GITHUB_TOKEN`).
23+
- `release-validate.yml` — validates that PR as it changes (`GITHUB_TOKEN`).
24+
- `release-publish.yml` — on merge of that PR, tags `vX.Y.Z`
25+
(`BOT_GITHUB_TOKEN`, required so the tag can trigger the next workflow).
26+
- `container-publish.yml` — on `v*` tag, builds + pushes
27+
`ghcr.io/codeforphilly/codeforphilly-ng:vX.Y.Z` and `:latest`.
28+
- **`ci.yml`** also runs on `develop`.
29+
- **`docs/operations/releases.md`** — operator guide for the flow.
30+
- A `develop` branch created off `main`.
31+
32+
## Implements
33+
34+
No spec — release/CI tooling. Uses the `JarvusInnovations/infra-components`
35+
release-* composite actions (unpinned `channels/.../latest`), matching the
36+
reference repo (`jarvus-data-pipeline`).
37+
38+
## Approach
39+
40+
Adapt the reference workflows to this repo: single image (no sub-image, no
41+
BigQuery), `actions/checkout@v6` + `docker/login-action@v3`, no `--platform`
42+
(CI runners + cluster are both amd64). `BOT_GITHUB_TOKEN` already set as a repo
43+
secret. First release will be seeded at **v0.1.0**.
44+
45+
## Validation
46+
47+
- [x] Four workflows + `ci.yml` (now on `develop`) + `docs/operations/releases.md`
48+
shipped; YAML parsed by GitHub Actions (the workflows ran on the PR).
49+
- [x] `release-validate` behaves as designed — it ran on PR #135 and failed with
50+
`PR title must match "Release: vX.Y.Z"`, confirming the guard works (only
51+
Release PRs may target `main`). Expected on this bootstrap feature-PR.
52+
- [ ] **Activation (post-merge, operator):** create `develop`; first push opens a
53+
`Release: v0.1.0` PR; merging it tags `v0.1.0` and `container-publish`
54+
pushes the image. Deferred — see Follow-ups.
55+
56+
## Risks
57+
58+
- `container-publish`'s first run fails if GHCR package write isn't granted or
59+
`BOT_GITHUB_TOKEN` is missing — non-destructive (tag created, push fails),
60+
fixable and re-runnable.
61+
- Branch protection on `main` is a GitHub-settings change (operator action).
62+
63+
## Notes
64+
65+
- `release-validate` runs on **every** PR into `main` and fails non-Release PRs
66+
by design — that's the guard enforcing "only Release PRs target `main`; feature
67+
work goes to `develop`." Do not be alarmed by its failure on this bootstrap PR.
68+
Consequently, if branch protection on `main` requires status checks, require
69+
**`build`** (CI); requiring `release-validate` too would additionally enforce
70+
the Release-PR-only rule.
71+
- The manual `:sandbox` build path still works for ad-hoc iteration; versioned
72+
releases now flow through `container-publish`.
73+
74+
## Follow-ups
75+
76+
- **Activation (operator):** after this merges to `main`, create `develop` off
77+
`main` and push it to open the first `Release: v0.1.0` PR. (I can do this on
78+
request — held back so the first release is opened deliberately.)
79+
- **Operator (GitHub settings):** confirm `BOT_GITHUB_TOKEN` secret; grant the
80+
repo's Actions `packages: write` on the GHCR package (first publish creates
81+
it); add branch protection on `main` (require `build`).
82+
- **Deferred:** wire the cluster/GitOps to track the published `:vX.Y.Z` /
83+
`:latest` tags instead of the manual `:sandbox` push. No issue filed yet —
84+
revisit when prod GitOps lands.

0 commit comments

Comments
 (0)