From fbece77a1597f1486fa85b5dc717a458f1ba5974 Mon Sep 17 00:00:00 2001 From: Chris Alfano Date: Fri, 26 Jun 2026 10:27:42 -0400 Subject: [PATCH 1/4] chore(plans): open release-flow Co-Authored-By: Claude Opus 4.8 (1M context) --- plans/release-flow.md | 62 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 plans/release-flow.md diff --git a/plans/release-flow.md b/plans/release-flow.md new file mode 100644 index 0000000..feb85d4 --- /dev/null +++ b/plans/release-flow.md @@ -0,0 +1,62 @@ +--- +status: in-progress +depends: [] +specs: [] +issues: [] +pr: +--- + +# Plan: stand up develop→main Release-PR automation + +## Scope + +Adopt the Jarvus develop→main Release-PR flow so versioned releases are cut from +a changelog'd PR, and the GHCR image build (currently manual) is automated on +tag. Replaces the ad-hoc `docker build/push :sandbox` step for versioned +releases. + +What ships: + +- **Four workflows** under `.github/workflows/`: + - `release-prepare.yml` — push to `develop` opens/updates a `Release: vX.Y.Z` + PR into `main` with a bot changelog (`GITHUB_TOKEN`). + - `release-validate.yml` — validates that PR as it changes (`GITHUB_TOKEN`). + - `release-publish.yml` — on merge of that PR, tags `vX.Y.Z` + (`BOT_GITHUB_TOKEN`, required so the tag can trigger the next workflow). + - `container-publish.yml` — on `v*` tag, builds + pushes + `ghcr.io/codeforphilly/codeforphilly-ng:vX.Y.Z` and `:latest`. +- **`ci.yml`** also runs on `develop`. +- **`docs/operations/releases.md`** — operator guide for the flow. +- A `develop` branch created off `main`. + +## Implements + +No spec — release/CI tooling. Uses the `JarvusInnovations/infra-components` +release-* composite actions (unpinned `channels/.../latest`), matching the +reference repo (`jarvus-data-pipeline`). + +## Approach + +Adapt the reference workflows to this repo: single image (no sub-image, no +BigQuery), `actions/checkout@v6` + `docker/login-action@v3`, no `--platform` +(CI runners + cluster are both amd64). `BOT_GITHUB_TOKEN` already set as a repo +secret. First release will be seeded at **v0.1.0**. + +## Validation + +- [ ] `develop` branch exists on origin. +- [ ] Pushing `develop` opens a `Release: v0.1.0` PR into `main` with a changelog. +- [ ] Merging that PR tags `v0.1.0` and `container-publish` pushes the image to GHCR. +- [ ] YAML is valid (lint / Actions parses it). + +## Risks + +- `container-publish`'s first run fails if GHCR package write isn't granted or + `BOT_GITHUB_TOKEN` is missing — non-destructive (tag created, push fails), + fixable and re-runnable. +- Branch protection on `main` is a GitHub-settings change (operator action), + not in this repo. + +## Notes + +## Follow-ups From 2990bb559d56178711f7eca3f6a31bb9b70a1a0a Mon Sep 17 00:00:00 2001 From: Chris Alfano Date: Fri, 26 Jun 2026 10:27:42 -0400 Subject: [PATCH 2/4] =?UTF-8?q?ci(release):=20add=20develop=E2=86=92main?= =?UTF-8?q?=20Release-PR=20automation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adopt the Jarvus develop→main release flow. Pushing `develop` opens a "Release: vX.Y.Z" PR into main with a bot changelog (release-prepare); merging it tags vX.Y.Z (release-publish, via BOT_GITHUB_TOKEN so the tag can trigger downstream), which fires container-publish to build + push the GHCR image (:vX.Y.Z + :latest) — automating the previously-manual docker build. release-validate keeps the Release PR well-formed. ci.yml now also runs on develop. Workflows adapted from the jarvus-data-pipeline reference: single image (no sub-image/BigQuery), checkout@v6 + login-action@v3, no --platform (CI + cluster are amd64). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 2 +- .github/workflows/container-publish.yml | 59 +++++++++++++++++++++++++ .github/workflows/release-prepare.yml | 25 +++++++++++ .github/workflows/release-publish.yml | 21 +++++++++ .github/workflows/release-validate.yml | 17 +++++++ 5 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/container-publish.yml create mode 100644 .github/workflows/release-prepare.yml create mode 100644 .github/workflows/release-publish.yml create mode 100644 .github/workflows/release-validate.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b262edb..88ffd70 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [main] + branches: [main, develop] pull_request: concurrency: diff --git a/.github/workflows/container-publish.yml b/.github/workflows/container-publish.yml new file mode 100644 index 0000000..92b62d7 --- /dev/null +++ b/.github/workflows/container-publish.yml @@ -0,0 +1,59 @@ +name: "Container: Publish Image" + +# Builds and pushes the runtime image to GHCR when a vX.Y.Z tag is pushed +# (by release-publish.yml). Tags the image :vX.Y.Z and :latest. +# +# GitHub-hosted runners are amd64 and the cluster nodes are amd64, so no +# --platform flag is needed here (it's only required for local Apple-silicon +# builds — see docs/operations/sandbox-deploy.md). + +on: + push: + tags: ["v*"] + +permissions: + contents: read + packages: write + +concurrency: + group: "container-publish-${{ github.ref }}" + cancel-in-progress: false + +jobs: + container-publish: + name: Build and Push + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Login to ghcr.io + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Compute image address + run: | + DOCKER_IMAGE="ghcr.io/${GITHUB_REPOSITORY,,}" + DOCKER_TAG="${GITHUB_REF#refs/tags/}" + echo "DOCKER_IMAGE=${DOCKER_IMAGE}" >> "$GITHUB_ENV" + echo "DOCKER_TAG=${DOCKER_TAG}" >> "$GITHUB_ENV" + echo "Publishing ${DOCKER_IMAGE}:${DOCKER_TAG} (+ :latest)" + + - name: Pull previous image for layer cache + run: docker pull "${DOCKER_IMAGE}:latest" || true + + - name: Build image + run: | + docker build \ + --cache-from "${DOCKER_IMAGE}:latest" \ + --tag "${DOCKER_IMAGE}:latest" \ + --tag "${DOCKER_IMAGE}:${DOCKER_TAG}" \ + . + + - name: Push versioned tag + run: docker push "${DOCKER_IMAGE}:${DOCKER_TAG}" + + - name: Push latest + run: docker push "${DOCKER_IMAGE}:latest" diff --git a/.github/workflows/release-prepare.yml b/.github/workflows/release-prepare.yml new file mode 100644 index 0000000..5c99d54 --- /dev/null +++ b/.github/workflows/release-prepare.yml @@ -0,0 +1,25 @@ +name: "Release: Prepare PR" + +# Pushing `develop` opens (or updates) a "Release: vX.Y.Z" PR into `main` with a +# bot-generated changelog. Merging that PR publishes (see release-publish.yml). + +on: + push: + branches: [develop] + +permissions: + contents: read + pull-requests: write + +concurrency: + group: "release-prepare" + cancel-in-progress: true + +jobs: + release-prepare: + runs-on: ubuntu-latest + steps: + - uses: JarvusInnovations/infra-components@channels/github-actions/release-prepare/latest + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + release-branch: main diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml new file mode 100644 index 0000000..ae9655f --- /dev/null +++ b/.github/workflows/release-publish.yml @@ -0,0 +1,21 @@ +name: "Release: Publish PR" + +# When a "Release: v*" PR into `main` is merged, this creates the `vX.Y.Z` git +# tag (and GitHub release). The tag push then triggers container-publish.yml. +# +# Must use BOT_GITHUB_TOKEN, not GITHUB_TOKEN: a tag pushed with the default +# GITHUB_TOKEN cannot trigger another workflow (container-publish), so the +# image would never build. + +on: + pull_request: + branches: [main] + types: [closed] + +jobs: + release-publish: + runs-on: ubuntu-latest + steps: + - uses: JarvusInnovations/infra-components@channels/github-actions/release-publish/latest + with: + github-token: ${{ secrets.BOT_GITHUB_TOKEN }} diff --git a/.github/workflows/release-validate.yml b/.github/workflows/release-validate.yml new file mode 100644 index 0000000..fabdd5c --- /dev/null +++ b/.github/workflows/release-validate.yml @@ -0,0 +1,17 @@ +name: "Release: Validate PR" + +# Keeps the open "Release: v*" PR's title/version/changelog well-formed as it +# changes. Runs on the PR into `main`. + +on: + pull_request: + branches: [main] + types: [opened, edited, reopened, synchronize] + +jobs: + release-validate: + runs-on: ubuntu-latest + steps: + - uses: JarvusInnovations/infra-components@channels/github-actions/release-validate/latest + with: + github-token: ${{ secrets.GITHUB_TOKEN }} From b66d64876aa14f4a1121cc4fa78d1fd11c3fb420 Mon Sep 17 00:00:00 2001 From: Chris Alfano Date: Fri, 26 Jun 2026 10:27:42 -0400 Subject: [PATCH 3/4] =?UTF-8?q?docs(ops):=20document=20the=20develop?= =?UTF-8?q?=E2=86=92main=20release=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/operations/releases.md | 57 +++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 docs/operations/releases.md diff --git a/docs/operations/releases.md b/docs/operations/releases.md new file mode 100644 index 0000000..9996d54 --- /dev/null +++ b/docs/operations/releases.md @@ -0,0 +1,57 @@ +# Releases + +This repo uses the Jarvus **develop→main Release-PR** flow (the +`JarvusInnovations/infra-components` `release-prepare` / `release-validate` / +`release-publish` composite actions). Versioned releases are cut from a +changelog'd PR; merging it tags the release and publishes the container image. + +## The flow + +``` +feature branch ──▶ develop ──(push)──▶ "Release: vX.Y.Z" PR into main + │ (review changelog, adjust bump) + ▼ (merge) + tag vX.Y.Z ──▶ GHCR image :vX.Y.Z + :latest +``` + +1. **Merge work into `develop`.** Feature branches PR into `develop` (CI runs on + `develop` and on every PR). +2. **Push `develop`.** `release-prepare.yml` opens (or updates) a + **`Release: vX.Y.Z`** PR into `main` with a bot-generated `## Changelog` + comment. The version is computed from the last `v*` tag + the commits since. +3. **Review the Release PR.** Sort the changelog, confirm the semver bump + (edit the PR title to override the version if needed — `release-validate.yml` + keeps it well-formed). Use the **`release-flow`** skill for the changelog + + bump conventions. +4. **Merge the Release PR.** `release-publish.yml` creates the `vX.Y.Z` tag and + GitHub release. The tag push triggers `container-publish.yml`, which builds + and pushes `ghcr.io/codeforphilly/codeforphilly-ng:vX.Y.Z` and `:latest`. +5. **Deploy.** The cluster picks up the image per + [deploy.md](./deploy.md) (the published `:vX.Y.Z` / `:latest` tags replace + the previously-manual `:sandbox` build for versioned releases). + +## Prerequisites (one-time) + +- **`BOT_GITHUB_TOKEN`** repo (or org) secret — a PAT/app token with `repo` + scope. Required by `release-publish`: a tag pushed with the default + `GITHUB_TOKEN` cannot trigger `container-publish`, so the image would never + build. +- **GHCR package write** for Actions — the first `container-publish` run creates + the package; ensure `github.com/CodeForPhilly/codeforphilly-ng` → + Packages grants the repo's Actions write access (the workflow uses + `permissions: packages: write`). +- **Branch protection on `main`** (recommended) — require a PR + green CI to + merge, so releases only land via the Release PR. + +## First release + +There are no tags yet, so the first push to `develop` proposes **`v0.1.0`**. If +you want a different baseline, edit the Release PR title before merging. + +## Notes + +- The manual `docker build --platform=linux/amd64 … :sandbox` path + ([sandbox-deploy.md](./sandbox-deploy.md)) still works for ad-hoc iteration; + versioned releases now go through `container-publish` instead. +- CI runners and cluster nodes are both amd64, so `container-publish` needs no + `--platform` flag (that's only for local Apple-silicon builds). From 517cb14e1a920a0ef4bb07c3bd2c29a51c93f822 Mon Sep 17 00:00:00 2001 From: Chris Alfano Date: Fri, 26 Jun 2026 10:30:57 -0400 Subject: [PATCH 4/4] chore(plans): mark release-flow done (PR #135) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also clarify in releases.md that release-validate guards main (only Release PRs target main; feature work goes to develop) — its failure on the bootstrap PR is expected. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/operations/releases.md | 6 +++++- plans/release-flow.md | 38 +++++++++++++++++++++++++++++-------- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/docs/operations/releases.md b/docs/operations/releases.md index 9996d54..f005317 100644 --- a/docs/operations/releases.md +++ b/docs/operations/releases.md @@ -15,7 +15,11 @@ feature branch ──▶ develop ──(push)──▶ "Release: vX.Y.Z" PR into ``` 1. **Merge work into `develop`.** Feature branches PR into `develop` (CI runs on - `develop` and on every PR). + `develop` and on every PR). **Only Release PRs ever target `main`** — that's + what `release-validate` enforces: it fails any PR into `main` whose title + isn't `Release: vX.Y.Z`, so a stray feature-PR-into-`main` is caught. (The + one exception is the very first bootstrap PR that introduces these workflows, + which necessarily merges to `main` directly and trips that check once.) 2. **Push `develop`.** `release-prepare.yml` opens (or updates) a **`Release: vX.Y.Z`** PR into `main` with a bot-generated `## Changelog` comment. The version is computed from the last `v*` tag + the commits since. diff --git a/plans/release-flow.md b/plans/release-flow.md index feb85d4..294b3ae 100644 --- a/plans/release-flow.md +++ b/plans/release-flow.md @@ -1,9 +1,9 @@ --- -status: in-progress +status: done depends: [] specs: [] issues: [] -pr: +pr: 135 --- # Plan: stand up develop→main Release-PR automation @@ -44,19 +44,41 @@ secret. First release will be seeded at **v0.1.0**. ## Validation -- [ ] `develop` branch exists on origin. -- [ ] Pushing `develop` opens a `Release: v0.1.0` PR into `main` with a changelog. -- [ ] Merging that PR tags `v0.1.0` and `container-publish` pushes the image to GHCR. -- [ ] YAML is valid (lint / Actions parses it). +- [x] Four workflows + `ci.yml` (now on `develop`) + `docs/operations/releases.md` + shipped; YAML parsed by GitHub Actions (the workflows ran on the PR). +- [x] `release-validate` behaves as designed — it ran on PR #135 and failed with + `PR title must match "Release: vX.Y.Z"`, confirming the guard works (only + Release PRs may target `main`). Expected on this bootstrap feature-PR. +- [ ] **Activation (post-merge, operator):** create `develop`; first push opens a + `Release: v0.1.0` PR; merging it tags `v0.1.0` and `container-publish` + pushes the image. Deferred — see Follow-ups. ## Risks - `container-publish`'s first run fails if GHCR package write isn't granted or `BOT_GITHUB_TOKEN` is missing — non-destructive (tag created, push fails), fixable and re-runnable. -- Branch protection on `main` is a GitHub-settings change (operator action), - not in this repo. +- Branch protection on `main` is a GitHub-settings change (operator action). ## Notes +- `release-validate` runs on **every** PR into `main` and fails non-Release PRs + by design — that's the guard enforcing "only Release PRs target `main`; feature + work goes to `develop`." Do not be alarmed by its failure on this bootstrap PR. + Consequently, if branch protection on `main` requires status checks, require + **`build`** (CI); requiring `release-validate` too would additionally enforce + the Release-PR-only rule. +- The manual `:sandbox` build path still works for ad-hoc iteration; versioned + releases now flow through `container-publish`. + ## Follow-ups + +- **Activation (operator):** after this merges to `main`, create `develop` off + `main` and push it to open the first `Release: v0.1.0` PR. (I can do this on + request — held back so the first release is opened deliberately.) +- **Operator (GitHub settings):** confirm `BOT_GITHUB_TOKEN` secret; grant the + repo's Actions `packages: write` on the GHCR package (first publish creates + it); add branch protection on `main` (require `build`). +- **Deferred:** wire the cluster/GitOps to track the published `:vX.Y.Z` / + `:latest` tags instead of the manual `:sandbox` push. No issue filed yet — + revisit when prod GitOps lands.