# Release build (ADR 0014, issue #89): pushing a semver tag `vX.Y.Z` builds # and pushes the immutable release images and publishes a Gitea release # whose notes list the changes since the previous release, with a call-out # when the release contains database migrations (the `migration` marker the # update guide promises). Deploying to Prod is a SEPARATE, manual step: # after reviewing the release, push a `prod-vX.Y.Z-` tag # (prod-deploy.yml) — that tag push is the manual approval gate, since # Gitea 1.22 has no environment approvals (revisit on 1.23+). name: Release on: push: tags: ['v*.*.*'] env: IMAGE_BASE: gitea.101010.cloud/stwaidele/dorfteich jobs: build-release: name: Build release images and notes runs-on: ubuntu-latest steps: - name: Check out repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Log in to the Gitea registry run: printf '%s' "${{ secrets.REGISTRY_TOKEN }}" | tr -d '[:space:]' | docker login gitea.101010.cloud -u fable-5 --password-stdin - name: Build and push semver images run: | TAG=${GITHUB_REF_NAME} for app in web api collab backup; do docker build -f apps/$app/Dockerfile --build-arg APP_VERSION=$TAG \ -t $IMAGE_BASE-$app:$TAG . docker push $IMAGE_BASE-$app:$TAG done # Supply-chain artefacts (issue #202): one CycloneDX SBOM per release # image, one for the pnpm workspace, plus the full license report — # attached as build artefacts of this run BEFORE the release is # published, so a red gate stops the release. Mechanics dictated by # the runner (the job talks to the HOST daemon, so bind mounts of # workspace paths resolve on the host and go nowhere): files travel # into the pinned syft container via `docker cp` (an API stream), and # images via `docker save` to a tar copied the same way — syft cannot # read a tar from stdin (not seekable). - name: Generate SBOMs run: | set -euo pipefail TAG=${GITHUB_REF_NAME} SYFT=anchore/syft:v1.33.0 mkdir -p supply-chain sbom-src cp pnpm-lock.yaml package.json sbom-src/ c=$(docker create $SYFT scan dir:/src --source-name dorfteich-workspace --source-version "$TAG" -o cyclonedx-json=/out.json) docker cp sbom-src "$c:/src" docker start -a "$c" docker cp "$c:/out.json" supply-chain/sbom-workspace-$TAG.cdx.json docker rm "$c" > /dev/null for app in web api collab backup; do docker save $IMAGE_BASE-$app:$TAG -o image.tar c=$(docker create $SYFT scan docker-archive:/image.tar --source-name dorfteich-$app --source-version "$TAG" -o cyclonedx-json=/out.json) docker cp image.tar "$c:/image.tar" docker start -a "$c" docker cp "$c:/out.json" supply-chain/sbom-image-$app-$TAG.cdx.json docker rm "$c" > /dev/null rm image.tar done ls -l supply-chain/ - name: Set up pnpm uses: pnpm/action-setup@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version-file: .node-version cache: pnpm - name: Install dependencies run: pnpm install --frozen-lockfile - name: License report and allowlist gate run: | set -euo pipefail pnpm licenses list --json > supply-chain/licenses-${GITHUB_REF_NAME}.json node scripts/check-licenses.mjs < supply-chain/licenses-${GITHUB_REF_NAME}.json - name: Attach supply-chain artefacts uses: actions/upload-artifact@v3 with: name: supply-chain-${{ github.ref_name }} path: supply-chain/ - name: Generate release notes and publish the release run: | TAG=${GITHUB_REF_NAME} PREV=$(git tag --list 'v*.*.*' --sort=-v:refname | grep -vx "$TAG" | head -n1 || true) RANGE=${PREV:+$PREV..}$TAG { echo "## Changes since ${PREV:-the beginning}" echo git log --no-merges --pretty='- %s' $RANGE echo if git diff --name-only ${PREV:-$(git hash-object -t tree /dev/null)} $TAG -- apps/api/prisma/migrations/ | grep -q .; then echo '> ⚠️ **migration** — this release applies database migrations automatically at api start. Downgrade window: one minor release (docs/self-hosting).' else echo '_No database migrations in this release._' fi } > notes.md TAG=$TAG docker run --rm -i -e TAG node:22.15.1-alpine node -e \ 'const fs=require("fs");const body=fs.readFileSync(0,"utf8");process.stdout.write(JSON.stringify({tag_name:process.env.TAG,name:process.env.TAG,body}))' \ < notes.md > release.json curl -sf -X POST \ -H "Authorization: token ${{ github.token }}" \ -H 'Content-Type: application/json' \ --data @release.json \ "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases" \ > /dev/null && echo "release $TAG published" # Operations QA (issue #90): the pre-approval gate. A human pushes the # prod-vX.Y.Z tag only after BOTH jobs of this release run are green. ops-qa: name: Release-candidate operations QA needs: build-release runs-on: ubuntu-latest timeout-minutes: 20 steps: - name: Check out repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Log in to the Gitea registry run: printf '%s' "${{ secrets.REGISTRY_TOKEN }}" | tr -d '[:space:]' | docker login gitea.101010.cloud -u fable-5 --password-stdin - name: Run update simulation, degraded readiness, and backup roundtrip run: | PREV=$(git tag --list 'v*.*.*' --sort=-v:refname | grep -vx "$GITHUB_REF_NAME" | head -n1 || true) NEXT_TAG=$GITHUB_REF_NAME PREV_TAG=$PREV IMAGE_BASE=$IMAGE_BASE \ sh deploy/release-qa.sh