CI: build on main (and drop the :main tag) #57

Merged
bvandeusen merged 2 commits from dev into main 2026-06-03 12:44:45 -04:00
+28 -21
View File
@@ -1,12 +1,19 @@
# CI runs first; build only proceeds if all checks pass. # CI runs first; build only proceeds if all checks pass.
# #
# Push to dev: typecheck + lint + test + build :dev + :<sha> # Push to dev: typecheck + lint + test + build :dev + :<sha>
# Tag v* (release): typecheck + lint + test + build :latest + :<sha> + :<version> # Push to main: typecheck + lint + test + build :<sha> (no moving tag)
# Tag v* (release): typecheck + lint + test + build :latest + :<version> + :<sha>
# #
# main pushes are NOT gated here: a merge to main only happens after # Both dev and main are gated AND built. dev pushes move the :dev tag; main
# dev has already passed CI, and the release tag is the sole trigger # pushes publish only the immutable :<sha> image — no :main tag, because
# for a production image. Re-running CI on the merge commit just burns # :latest (release-only) is the single production pointer and a :main alias
# runner time without changing the outcome. # would just duplicate it. Running CI on the main merge commit is intentional:
# main is validated and its :<sha> image is the rollback point. The v* release
# tag is the ONLY trigger that publishes :latest plus the immutable :<version>.
#
# Successive pushes to the SAME ref supersede each other (see concurrency
# below), so rapid pushes don't stack identical work; dev and main runs are
# independent refs and never cancel one another.
# #
# To cut a release: # To cut a release:
# Create a release via the Forgejo UI on main with a v* tag name. # Create a release via the Forgejo UI on main with a v* tag name.
@@ -16,11 +23,8 @@
# gating on branch push is already enough. # gating on branch push is already enough.
# #
# NOTE on the `if:` guards below: Forgejo Actions does not consistently # NOTE on the `if:` guards below: Forgejo Actions does not consistently
# honor `on.push.branches` as a filter — merge commits landing on main # honor `on.push.branches` as a filter, so every job repeats the ref check
# still trigger the workflow, producing redundant runs on the same SHA # explicitly — permitting dev, main, and v* tags, rejecting anything else.
# that was already gated on dev. Every job therefore repeats the ref
# check so main pushes trigger the workflow but every job skips
# immediately (no runner time, no duplicate work).
# #
# Required secrets (repo → Settings → Secrets → Actions): # Required secrets (repo → Settings → Secrets → Actions):
# REGISTRY_USER — your Forgejo username # REGISTRY_USER — your Forgejo username
@@ -29,7 +33,7 @@ name: CI & Build
on: on:
push: push:
branches: [dev] branches: [dev, main]
tags: ["v*"] tags: ["v*"]
paths: paths:
- "src/**" - "src/**"
@@ -67,8 +71,8 @@ env:
jobs: jobs:
typecheck: typecheck:
name: TypeScript typecheck name: TypeScript typecheck
# Skip on main merge-commit pushes — see workflow header comment. # Gate dev, main, and v* tags; reject any other ref (see header note).
if: github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/v') if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
runs-on: python-ci runs-on: python-ci
container: container:
image: git.fabledsword.com/bvandeusen/ci-python:3.14 image: git.fabledsword.com/bvandeusen/ci-python:3.14
@@ -92,7 +96,7 @@ jobs:
lint: lint:
name: Python lint name: Python lint
if: github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/v') if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
runs-on: python-ci runs-on: python-ci
container: container:
image: git.fabledsword.com/bvandeusen/ci-python:3.14 image: git.fabledsword.com/bvandeusen/ci-python:3.14
@@ -106,7 +110,7 @@ jobs:
test: test:
name: Python tests name: Python tests
if: github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/v') if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
runs-on: python-ci runs-on: python-ci
container: container:
image: git.fabledsword.com/bvandeusen/ci-python:3.14 image: git.fabledsword.com/bvandeusen/ci-python:3.14
@@ -138,11 +142,9 @@ jobs:
build: build:
name: Build & push image name: Build & push image
needs: [typecheck, lint, test] needs: [typecheck, lint, test]
# Build on dev branch pushes and version tag pushes only. # Build on dev, main, and v* tag pushes. dev → :dev, main → (sha only),
# Mirrors the ref guard on the gate jobs above — main merge-commit # tag → :latest + :<version>; every build also gets an immutable :<sha>.
# pushes skip here too, so no production image is ever built from a if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
# raw main push (only from the v* tag the release creates).
if: github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/v')
runs-on: python-ci runs-on: python-ci
container: container:
image: git.fabledsword.com/bvandeusen/ci-python:3.14 image: git.fabledsword.com/bvandeusen/ci-python:3.14
@@ -168,6 +170,11 @@ jobs:
refs/heads/dev) refs/heads/dev)
TAGS="$TAGS,${{ env.IMAGE }}:dev" TAGS="$TAGS,${{ env.IMAGE }}:dev"
;; ;;
refs/heads/main)
# main publishes only the immutable :<sha> image (set above) —
# no :main tag; :latest (release-only) is the production pointer.
BUILD_VERSION="main"
;;
refs/tags/*) refs/tags/*)
TAGS="$TAGS,${{ env.IMAGE }}:latest,${{ env.IMAGE }}:${{ github.ref_name }}" TAGS="$TAGS,${{ env.IMAGE }}:latest,${{ env.IMAGE }}:${{ github.ref_name }}"
BUILD_VERSION="${{ github.ref_name }}" BUILD_VERSION="${{ github.ref_name }}"