Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: CI

on:
push:
branches: [main]
branches: [main, develop]
pull_request:

concurrency:
Expand Down
59 changes: 59 additions & 0 deletions .github/workflows/container-publish.yml
Original file line number Diff line number Diff line change
@@ -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"
25 changes: 25 additions & 0 deletions .github/workflows/release-prepare.yml
Original file line number Diff line number Diff line change
@@ -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
21 changes: 21 additions & 0 deletions .github/workflows/release-publish.yml
Original file line number Diff line number Diff line change
@@ -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 }}
17 changes: 17 additions & 0 deletions .github/workflows/release-validate.yml
Original file line number Diff line number Diff line change
@@ -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 }}
61 changes: 61 additions & 0 deletions docs/operations/releases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# 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). **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.
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).
84 changes: 84 additions & 0 deletions plans/release-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
status: done
depends: []
specs: []
issues: []
pr: 135
---

# 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

- [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).

## 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.
Loading