diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml index ad9c8df3..936e0793 100644 --- a/.github/workflows/build-and-publish.yml +++ b/.github/workflows/build-and-publish.yml @@ -1,5 +1,5 @@ -# Builds + publishes container images to ghcr.io with `:latest` and -# `:` tags. Cluster rollout is handled out-of-band: Keel polls +# Builds + publishes container images to ghcr.io with `:latest`, +# `:`, and release tags. Cluster rollout is handled out-of-band: Keel polls # ghcr every 2 minutes and rolls the matching Deployment when the # `:latest` digest changes, while Flux reconciles manifest changes # from `main`. Neither is this workflow's responsibility — the name @@ -9,6 +9,7 @@ name: Build & Publish on: push: branches: [main] + tags: ['v*.*.*'] # Manual trigger for the cases where a push event never fires the # workflow (rare GitHub oddity — observed once with the `#308` # merge: workflow silently absent for that SHA) or where a merge @@ -52,7 +53,7 @@ jobs: - uses: dorny/paths-filter@v4.0.1 id: filter with: - base: ${{ github.event.before }} + base: ${{ github.ref_type == 'tag' && github.sha || github.event.before }} filters: | workflow: - '.github/workflows/build-and-publish.yml' @@ -136,11 +137,8 @@ jobs: # # Multi-arch is gated on `main` only. The arm64 leg builds under # QEMU emulation and roughly doubles the UI build time; we don't - # want to pay that on every reconcile / dispatch / future trigger - # that someone might add. Today the workflow's `on:` only fires - # for push to main, but this guard makes the intent explicit and - # forward-safe — if you add workflow_dispatch later, feature - # branches still build amd64-only. + # want to pay that on every reconcile, dispatch, or release-tag + # trigger; feature branches and tags still build amd64-only. AMD64=linux/amd64 if [[ "${{ github.ref }}" == "refs/heads/main" && "${{ github.event_name }}" == "push" ]]; then UI_PLATFORMS=linux/amd64,linux/arm64 @@ -150,8 +148,12 @@ jobs: fi # `force_all=true` on workflow_dispatch overrides every - # per-service filter so all images rebuild in one go. + # per-service filter so all images rebuild in one go. Release tags + # also rebuild every service so each image gets the version tag. FORCE_ALL="${{ github.event.inputs.force_all || 'false' }}" + if [[ "$GITHUB_REF_TYPE" == "tag" && "$GITHUB_REF_NAME" == v*.*.* ]]; then + FORCE_ALL=true + fi [[ "$(any "$FORCE_ALL" "$KOTLIN_SHARED" "$FILTER_AUTH_API")" == "true" ]] && append auth-api services/auth-api/Dockerfile "$AMD64" [[ "$(any "$FORCE_ALL" "$KOTLIN_SHARED" "$FILTER_ASSISTANT_API")" == "true" ]] && append assistant-api services/assistant-api/Dockerfile "$AMD64" @@ -197,6 +199,14 @@ jobs: - uses: actions/checkout@v6.0.2 - name: Set image prefix (lowercase) run: echo "IMAGE_PREFIX=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV" + - name: Compute release version + id: release-version + run: | + if [[ "$GITHUB_REF_TYPE" == "tag" && "$GITHUB_REF_NAME" == v*.*.* ]]; then + echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + else + echo "version=" >> "$GITHUB_OUTPUT" + fi # QEMU is only needed when a service builds for a non-runner arch # (the UIs build linux/arm64 alongside amd64). No-op for the # amd64-only JVM services. @@ -225,5 +235,6 @@ jobs: tags: | ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/${{ matrix.service.name }}:${{ github.sha }} ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/${{ matrix.service.name }}:latest + ${{ steps.release-version.outputs.version && format('{0}/{1}/{2}:{3}', env.REGISTRY, env.IMAGE_PREFIX, matrix.service.name, steps.release-version.outputs.version) || '' }} cache-from: type=gha,scope=${{ matrix.service.name }} cache-to: type=gha,mode=max,scope=${{ matrix.service.name }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..613855a9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,23 @@ +name: Release + +on: + push: + branches: [main] + +permissions: + contents: write + pull-requests: write + +concurrency: + group: release-please + cancel-in-progress: false + +jobs: + release: + name: Release Please + runs-on: ubuntu-latest + steps: + - uses: googleapis/release-please-action@v4 + with: + config-file: release-please-config.json + manifest-file: .release-please-manifest.json diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 00000000..466df71c --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.1.0" +} diff --git a/docs/runbooks/versioned-deploy.md b/docs/runbooks/versioned-deploy.md new file mode 100644 index 00000000..a928e092 --- /dev/null +++ b/docs/runbooks/versioned-deploy.md @@ -0,0 +1,36 @@ +# Versioned Deploy Model + +Production deploys are driven by explicit version pins in Git, not by pushes to +`main` or mutable `:latest` tags. + +## Target Model + +- `main` is the integration branch. Merging to `main` does not by itself define + the production version. +- release-please creates release tags in the form `vX.Y.Z`. +- `build-and-publish` publishes in-house container images with version tags that + match the release tag. +- A release PR bumps explicit image tags under `platform/cluster/flux/**` from + the previous version to the new version. +- Flux deploys the commit that pins those image tags. +- Rollback is a Git revert of the release PR that bumped the image tags. + +## Deploying a Version + +To deploy a specific release, reconcile the commit that pins the desired +`vX.Y.Z` image tags in `platform/cluster/flux/**`. The deployed state is the +Flux-applied Git state, so the image tags in the cluster manifests are the +source of truth. + +## Rollback + +Rollback is performed by reverting the tag-bump commit or PR. Flux reconciles +the reverted manifests and returns the affected workloads to the previously +pinned image versions. + +## Keel Removal Sequencing + +Keel-based `:latest` auto-rolls are removed from the in-house app deploy path in +a separate, sequenced PR. That Flux/Keel change must land only after the first +versioned in-house images have been published, so every workload can be pinned +to an existing version tag before `:latest` automation is removed. diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 00000000..a422ca91 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,10 @@ +{ + "packages": { + ".": { + "release-type": "simple", + "bump-minor-pre-major": true, + "changelog-path": "CHANGELOG.md", + "include-component-in-tag": false + } + } +} diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000..b21816ab --- /dev/null +++ b/renovate.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended", ":semanticCommits", ":dependencyDashboard"], + "rangeStrategy": "pin", + "labels": ["dependencies"], + "minimumReleaseAge": "3 days", + "packageRules": [ + { + "description": "Group all ExtraToast shared artifacts so a coherent platform bump lands in one PR.", + "matchPackagePatterns": ["^dev\\.extratoast", "^@extratoast/"], + "matchDepNames": ["ExtraToast/github-workflows"], + "groupName": "extratoast shared artifacts", + "labels": ["dependencies", "extratoast-platform"] + }, + { + "description": "Pin GitHub Actions to immutable refs.", + "matchManagers": ["github-actions"], + "pinDigests": true + } + ], + "lockFileMaintenance": { "enabled": true } +}