Files
warmbly/.github/workflows/release.yml
T

326 lines
11 KiB
YAML

name: Release
on:
push:
tags:
- "v*.*.*"
env:
REGISTRY: ghcr.io
IMAGE_PREFIX: ghcr.io/${{ github.repository_owner }}/warmbly
jobs:
validate-tag:
name: Validate Tag
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
major: ${{ steps.version.outputs.major }}
minor: ${{ steps.version.outputs.minor }}
steps:
- name: Extract version
id: version
run: |
TAG="${{ github.ref_name }}"
VERSION="${TAG#v}"
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "major=$(echo "$VERSION" | cut -d. -f1)" >> $GITHUB_OUTPUT
echo "minor=$(echo "$VERSION" | cut -d. -f1-2)" >> $GITHUB_OUTPUT
echo "Release version: ${VERSION}"
# Go services: the Dockerfiles build on $BUILDPLATFORM and cross-compile to
# each target arch, so one amd64 runner produces both platforms without QEMU.
build-go:
name: Build ${{ matrix.service }}
needs: validate-tag
strategy:
fail-fast: false
matrix:
service: [backend, consumer, worker, forms, updater]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: deploy/docker/${{ matrix.service }}.Dockerfile
push: true
build-args: |
VERSION=${{ github.ref_name }}
COMMIT=${{ github.sha }}
BUILT_AT=${{ github.event.head_commit.timestamp }}
tags: |
${{ env.IMAGE_PREFIX }}/${{ matrix.service }}:${{ github.ref_name }}
${{ env.IMAGE_PREFIX }}/${{ matrix.service }}:v${{ needs.validate-tag.outputs.minor }}
${{ env.IMAGE_PREFIX }}/${{ matrix.service }}:v${{ needs.validate-tag.outputs.major }}
${{ env.IMAGE_PREFIX }}/${{ matrix.service }}:prod
platforms: linux/amd64,linux/arm64
cache-from: type=gha,scope=${{ matrix.service }}
cache-to: type=gha,mode=max,scope=${{ matrix.service }}
# Frontends (web dashboard, admin panel): the pnpm build runs once on the
# build platform and its static output is served from nginx, so a single
# buildx build produces both arches without a per-arch rebuild.
build-frontend:
name: Build ${{ matrix.service }}
needs: validate-tag
strategy:
fail-fast: false
matrix:
service: [web, admin]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: ./${{ matrix.service }}
file: ./${{ matrix.service }}/Dockerfile
push: true
tags: |
${{ env.IMAGE_PREFIX }}/${{ matrix.service }}:${{ github.ref_name }}
${{ env.IMAGE_PREFIX }}/${{ matrix.service }}:v${{ needs.validate-tag.outputs.minor }}
${{ env.IMAGE_PREFIX }}/${{ matrix.service }}:v${{ needs.validate-tag.outputs.major }}
${{ env.IMAGE_PREFIX }}/${{ matrix.service }}:prod
platforms: linux/amd64,linux/arm64
cache-from: type=gha,scope=${{ matrix.service }}
cache-to: type=gha,mode=max,scope=${{ matrix.service }}
# Rust (tracking) and Elixir (realtime) have no cross-compiler; an emulated
# arm64 build under QEMU runs for an hour or more. Build each arch on a
# native runner and merge the digests into one manifest (the
# Docker-documented multi-runner pattern).
build-native:
name: Build ${{ matrix.service }} (${{ matrix.platform }})
needs: validate-tag
strategy:
fail-fast: false
matrix:
service: [tracking, realtime]
platform: [linux/amd64, linux/arm64]
runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Prepare platform pair
id: prep
run: echo "pair=${PLATFORM//\//-}" >> "$GITHUB_OUTPUT"
env:
PLATFORM: ${{ matrix.platform }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
with:
context: ${{ matrix.service == 'tracking' && './tracking' || '.' }}
file: ${{ matrix.service == 'tracking' && './tracking/Dockerfile' || format('deploy/docker/{0}.Dockerfile', matrix.service) }}
platforms: ${{ matrix.platform }}
cache-from: type=gha,scope=${{ matrix.service }}-${{ steps.prep.outputs.pair }}
cache-to: type=gha,mode=max,scope=${{ matrix.service }}-${{ steps.prep.outputs.pair }}
outputs: type=image,name=${{ env.IMAGE_PREFIX }}/${{ matrix.service }},push-by-digest=true,name-canonical=true,push=true
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-${{ matrix.service }}-${{ steps.prep.outputs.pair }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge-native:
name: Merge ${{ matrix.service }} manifest
needs: [validate-tag, build-native]
strategy:
fail-fast: false
matrix:
service: [tracking, realtime]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-${{ matrix.service }}-*
merge-multiple: true
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create \
-t ${{ env.IMAGE_PREFIX }}/${{ matrix.service }}:${{ github.ref_name }} \
-t ${{ env.IMAGE_PREFIX }}/${{ matrix.service }}:v${{ needs.validate-tag.outputs.minor }} \
-t ${{ env.IMAGE_PREFIX }}/${{ matrix.service }}:v${{ needs.validate-tag.outputs.major }} \
-t ${{ env.IMAGE_PREFIX }}/${{ matrix.service }}:prod \
$(printf '${{ env.IMAGE_PREFIX }}/${{ matrix.service }}@sha256:%s ' *)
create-release:
name: Create GitHub Release
needs: [validate-tag, build-go, build-frontend, merge-native]
runs-on: ubuntu-latest
permissions:
contents: write
# The manifest step logs in to GHCR and inspects each published image;
# without read access that fails on a private package.
packages: read
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# The installer verifies what it pulled against this file, so it is what
# makes "curl | sh" checkable after the fact rather than only before it.
# One line per service, because the thing that reads it is a POSIX shell.
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Publish the image manifest
env:
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
{
printf '{\n'
printf ' "tag": "%s",\n' "$TAG"
printf ' "registry": "%s",\n' "$IMAGE_PREFIX"
printf ' "images": {\n'
first=1
for s in backend consumer worker forms updater web admin tracking realtime; do
digest=$(docker buildx imagetools inspect "$IMAGE_PREFIX/$s:$TAG" \
--format '{{json .Manifest.Digest}}' | tr -d '"')
[ "$first" = 1 ] || printf ',\n'
first=0
printf ' "%s": "%s"' "$s" "$digest"
done
printf '\n }\n}\n'
} > /tmp/images.json
cat /tmp/images.json
- name: Generate release notes
env:
GITHUB_SERVER_URL: ${{ github.server_url }}
GITHUB_REPOSITORY: ${{ github.repository }}
TAG: ${{ github.ref_name }}
run: |
.github/scripts/release-notes.sh "$TAG" > /tmp/changelog.md
cat /tmp/changelog.md
- name: Assemble release body
run: |
{
cat /tmp/changelog.md
cat <<'EOF'
## Install
```
curl -fsSL https://warmbly.com/install.sh | sh -s -- --version ${{ github.ref_name }}
```
Add `--wizard` to be asked where each store lives, what is kept and
for how long, and how it is backed up.
`images.json` below lists the manifest digest of every image in this
release. The installer checks what it pulled against it, and you can
too:
```
docker image inspect ${{ env.IMAGE_PREFIX }}/backend:${{ github.ref_name }} --format '{{index .RepoDigests 0}}'
```
## Docker Images
All images are available at `ghcr.io/${{ github.repository_owner }}/warmbly`:
| Service | Image |
|---------|-------|
| Backend | `${{ env.IMAGE_PREFIX }}/backend:${{ github.ref_name }}` |
| Consumer | `${{ env.IMAGE_PREFIX }}/consumer:${{ github.ref_name }}` |
| Worker | `${{ env.IMAGE_PREFIX }}/worker:${{ github.ref_name }}` |
| Tracking | `${{ env.IMAGE_PREFIX }}/tracking:${{ github.ref_name }}` |
| Realtime | `${{ env.IMAGE_PREFIX }}/realtime:${{ github.ref_name }}` |
| Dashboard (web) | `${{ env.IMAGE_PREFIX }}/web:${{ github.ref_name }}` |
| Admin | `${{ env.IMAGE_PREFIX }}/admin:${{ github.ref_name }}` |
| Forms | `${{ env.IMAGE_PREFIX }}/forms:${{ github.ref_name }}` |
| Updater | `${{ env.IMAGE_PREFIX }}/updater:${{ github.ref_name }}` |
## Deployment
This release will be automatically deployed to production.
EOF
} > /tmp/release-body.md
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
body_path: /tmp/release-body.md
files: /tmp/images.json
draft: false
prerelease: ${{ contains(github.ref_name, '-') }}