feat(deploy): build+push release image on every merge to main
CI / Repo hygiene (pull_request) Successful in 3s
CI / Web (lint, typecheck, build) (pull_request) Successful in 22s
CI / Migrations reversible (pull_request) Successful in 10s
CI / API (lint, types, tests) (pull_request) Successful in 1m4s

Was tag-push-or-manual-dispatch only. Adds a push:main trigger so main stays
continuously deployable without needing a version tag for every change.

Also fixes a real bug this surfaced while testing the D17 registry-TLS fix:
the old tag logic unconditionally retagged :latest on every run, including
manual test dispatches off a feature branch — one such dispatch, done while
verifying the previous commit, silently overwrote :latest with a
feature-branch build. Tag resolution now only moves :latest on an actual
main push or a version tag; a manual dispatch gets its own
manual-<timestamp>-<sha> tag and leaves :latest alone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R2ZKeWkZV7ehf7fivrAkkG
This commit is contained in:
2026-09-21 20:57:52 -04:00
co-authored by Claude Sonnet 5
parent a114a7d3d8
commit 45719f284c
2 changed files with 28 additions and 8 deletions
+23 -6
View File
@@ -2,6 +2,7 @@ name: Release image
on: on:
push: push:
branches: [main]
tags: ['v*'] tags: ['v*']
workflow_dispatch: workflow_dispatch:
@@ -36,20 +37,36 @@ jobs:
username: BBergle username: BBergle
password: ${{ secrets.REGISTRY_TOKEN }} password: ${{ secrets.REGISTRY_TOKEN }}
- name: Resolve image tag # `latest` should only ever mean "what's actually on main" (or a tagged release) — not
# whatever a manual test dispatch off some feature branch happened to build. Learned the
# hard way: a manual dispatch off this very branch, while verifying the fix above, silently
# overwrote `latest` under the old unconditional-tags logic. Building the full tag list here
# in bash (rather than a conditional expression inline in the tags: block below) means there's
# never a blank line for build-push-action to choke on when latest isn't included.
- name: Resolve image tags
id: tag id: tag
run: | run: |
IMG=registry.bbergle.com:9537/bbergle/bike-app
if [ "${{ gitea.ref_type }}" = "tag" ]; then if [ "${{ gitea.ref_type }}" = "tag" ]; then
echo "value=${{ gitea.ref_name }}" >> "$GITHUB_OUTPUT" VALUE="${{ gitea.ref_name }}"
UPDATE_LATEST=true
elif [ "${{ gitea.ref_name }}" = "main" ] && [ "${{ gitea.event_name }}" = "push" ]; then
VALUE="main-$(git rev-parse --short HEAD)"
UPDATE_LATEST=true
else else
echo "value=manual-$(date -u +%Y%m%d%H%M%S)" >> "$GITHUB_OUTPUT" VALUE="manual-$(date -u +%Y%m%d%H%M%S)-$(git rev-parse --short HEAD)"
UPDATE_LATEST=false
fi fi
{
echo "tags<<EOF"
echo "$IMG:$VALUE"
[ "$UPDATE_LATEST" = true ] && echo "$IMG:latest"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- uses: docker/build-push-action@v6 - uses: docker/build-push-action@v6
with: with:
context: . context: .
file: Dockerfile file: Dockerfile
push: true push: true
tags: | tags: ${{ steps.tag.outputs.tags }}
registry.bbergle.com:9537/bbergle/bike-app:latest
registry.bbergle.com:9537/bbergle/bike-app:${{ steps.tag.outputs.value }}
+5 -2
View File
@@ -73,8 +73,11 @@ stays editable by hand afterward regardless of what the template pre-fills.
## Publishing the image ## Publishing the image
`.gitea/workflows/release.yml` builds this Dockerfile and pushes it to the Gitea container `.gitea/workflows/release.yml` builds this Dockerfile and pushes it to the Gitea container
registry at `registry.bbergle.com:9537/bbergle/bike-app` on a `v*` tag push, or on manual registry at `registry.bbergle.com:9537/bbergle/bike-app` on every push to `main` (tagged
`workflow_dispatch`. Not `192.168.0.3:3000` (Gitea's own plain-HTTP address) directly — Docker `main-<short-sha>`, and `latest`), on a `v*` tag push (tagged with the tag name, and `latest`), or
on manual `workflow_dispatch` (tagged `manual-<timestamp>-<short-sha>` only — a manual dispatch
never moves `latest`, so testing a feature branch can't clobber what's actually deployable). Not
`192.168.0.3:3000` (Gitea's own plain-HTTP address) directly — Docker
refuses any non-localhost registry over plain HTTP by default, so `registry.bbergle.com:9537` is refuses any non-localhost registry over plain HTTP by default, so `registry.bbergle.com:9537` is
an NPMplus proxy host in front of Gitea's registry that terminates TLS with a self-signed cert. an NPMplus proxy host in front of Gitea's registry that terminates TLS with a self-signed cert.
See `docs/DECISIONS.md` D17 for the full setup (cert, NPMplus proxy host, `certs.d` trust, and the See `docs/DECISIONS.md` D17 for the full setup (cert, NPMplus proxy host, `certs.d` trust, and the