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 }} diff --git a/docs/operations/releases.md b/docs/operations/releases.md new file mode 100644 index 0000000..f005317 --- /dev/null +++ b/docs/operations/releases.md @@ -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). diff --git a/plans/release-flow.md b/plans/release-flow.md new file mode 100644 index 0000000..294b3ae --- /dev/null +++ b/plans/release-flow.md @@ -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.