Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
1b21ebe
docs: add backup & disaster recovery guide
andrewyager Jun 9, 2026
bbd1f8f
fix: harden the login flow against silent failures
andrewyager Sep 17, 2026
5f87f3b
build: migrate dependency management from pip-tools to uv
andrewyager Sep 17, 2026
e290442
build: update Node, MJML and Tailwind build toolchain
andrewyager Sep 17, 2026
9cdb6ba
fix: default the cache to the Celery Redis host
andrewyager Sep 17, 2026
6b02ab1
test: keep generated media out of the source tree
andrewyager Sep 17, 2026
1ee2ad5
ci: install uv with pipx on the runner
andrewyager Sep 17, 2026
fd67b5c
build: choose the production image registry per deployment
andrewyager Sep 17, 2026
b01fc50
ci: run CI and releases on the internal GHES
andrewyager Sep 17, 2026
c572d80
ci: pick registry and runner label from the Actions host
andrewyager Sep 17, 2026
5425783
ci: install uv with setup-uv pinned to 0.12.15
andrewyager Sep 17, 2026
4aaabcd
ci: install uv with the pinned official installer
andrewyager Sep 17, 2026
4e568bd
ci: build and test in one job
andrewyager Sep 17, 2026
db99a1c
ci: verify the uv installer checksum and install job-scoped
andrewyager Sep 17, 2026
2ebaf35
test: skip repo-root file checks inside the Docker image
andrewyager Sep 17, 2026
863b649
build: pin MJML to 4.18.0
andrewyager Sep 17, 2026
5b1f49e
build: split test and runtime image targets, pass the version at runtime
andrewyager Sep 17, 2026
e2a663f
ci: skip build and tests when the commit image already exists
andrewyager Sep 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ POSTGRES_PASSWORD=props_dev_password
# Celery / Redis
CELERY_BROKER_URL=redis://redis:6379/0
CELERY_RESULT_BACKEND=redis://redis:6379/0
CACHE_URL=redis://redis:6379/1

# Production image (prod profile). Public releases live on ghcr.io; RWTS-run
# deployments pull from the internal registry instead.
# PROPS_IMAGE=containers.github.realworld.net.au/realworldtech/props
# Pin the release. It selects the image tag and is passed to the app as
# APP_VERSION (shown in the footer and used as the Sentry release).
# PROPS_VERSION=2026.09.0

# Gunicorn workers (production)
# GUNICORN_WORKERS=4
Expand Down
17 changes: 17 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
version: 2
updates:
- package-ecosystem: uv
directory: /
schedule:
interval: weekly
groups:
python:
patterns: ["*"]
- package-ecosystem: github-actions
directory: /
schedule:
interval: monthly
- package-ecosystem: docker
directory: /
schedule:
interval: monthly
158 changes: 89 additions & 69 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,117 +1,137 @@
name: CI

# Build once, promote later. Every pull request builds the image for its head
# commit, runs the test suite against it, and publishes it as
# <registry>/realworldtech/props:sha-<commit>. Merges to develop and main only
# retag that image (see promote-develop.yml and release.yml), and a pull
# request whose head commit already has an image (develop -> main) skips the
# build and the tests: that image has been tested already.
#
# Runs unchanged on github.com and on the internal GHES. The runner label comes
# from the RUNNER_LABEL repository variable (default ubuntu-latest) and the
# registry follows the host.

on:
pull_request:
branches: [develop, main]
workflow_dispatch:

concurrency:
group: ci-${{ github.event.pull_request.number }}
group: ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read
packages: write

env:
REGISTRY: ${{ github.server_url == 'https://github.com' && 'ghcr.io' || 'containers.github.realworld.net.au' }}
IMAGE: realworldtech/props

jobs:
lint:
name: Lint & Format
runs-on: ubuntu-latest
runs-on: ${{ vars.RUNNER_LABEL || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.13"
cache: pip
- name: Install uv 0.12.15
run: |
curl -LsSf -o "$RUNNER_TEMP/uv-install.sh" https://astral.sh/uv/0.12.15/install.sh
echo "716a1d6844740756c68770fcec2f79c2013fb9b03869a113f61e15f6f482a6a1 $RUNNER_TEMP/uv-install.sh" | sha256sum -c -
UV_INSTALL_DIR="$RUNNER_TEMP/uv" sh "$RUNNER_TEMP/uv-install.sh" --no-modify-path
echo "$RUNNER_TEMP/uv" >> "$GITHUB_PATH"

- name: Install dependencies
run: pip install black isort flake8
run: uv sync --frozen --only-group dev

- name: Check black formatting
run: black --check --line-length 79 --target-version py312 src/
run: uv run black --check src/

- name: Check isort ordering
run: isort --check --profile black --line-length 79 src/
run: uv run isort --check src/

- name: Run flake8
run: flake8 src/
run: uv run flake8 src/

- name: Audit dependencies for known vulnerabilities
run: uv run pip-audit --progress-spinner off

requirements-check:
name: Requirements in sync
runs-on: ubuntu-latest
lockfile-check:
name: Lockfile in sync
runs-on: ${{ vars.RUNNER_LABEL || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@v4

- name: Verify every package in requirements.in appears in requirements.txt
- name: Install uv 0.12.15
run: |
missing=0
while IFS= read -r line; do
line=$(echo "$line" | sed 's/#.*//' | xargs)
[ -z "$line" ] && continue
[[ "$line" == -* ]] && continue
pkg=$(echo "$line" | sed 's/\[.*\]//' | sed 's/[><=!].*//' | xargs | tr '[:upper:]' '[:lower:]')
[ -z "$pkg" ] && continue
if ! grep -qi "^${pkg}[>=<! \[]" requirements.txt && ! grep -qi "# via.*-r requirements.in" requirements.txt; then
if ! grep -qi "^${pkg}" requirements.txt; then
echo "MISSING: $pkg is in requirements.in but not in requirements.txt"
missing=1
fi
fi
done < requirements.in
if [ "$missing" -eq 1 ]; then
echo "requirements.txt is out of sync with requirements.in. Run 'pip-compile requirements.in' and commit the result."
exit 1
fi
echo "All packages from requirements.in found in requirements.txt"

build:
name: Build Docker Image
runs-on: ubuntu-latest
needs: [lint, requirements-check]
curl -LsSf -o "$RUNNER_TEMP/uv-install.sh" https://astral.sh/uv/0.12.15/install.sh
echo "716a1d6844740756c68770fcec2f79c2013fb9b03869a113f61e15f6f482a6a1 $RUNNER_TEMP/uv-install.sh" | sha256sum -c -
UV_INSTALL_DIR="$RUNNER_TEMP/uv" sh "$RUNNER_TEMP/uv-install.sh" --no-modify-path
echo "$RUNNER_TEMP/uv" >> "$GITHUB_PATH"

- name: Verify uv.lock matches pyproject.toml
run: uv lock --check

build-test:
name: Build & Test
runs-on: ${{ vars.RUNNER_LABEL || 'ubuntu-latest' }}
needs: [lint, lockfile-check]
steps:
- uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build image
# Forks cannot push to the registry; their PRs are tested only.
- name: Log in to the container registry
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Check for an image already built and tested for this commit
id: existing
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
run: |
if docker buildx imagetools inspect "$REGISTRY/$IMAGE:sha-$HEAD_SHA" >/dev/null 2>&1; then
echo "found=true" >> "$GITHUB_OUTPUT"
echo "::notice::$REGISTRY/$IMAGE:sha-$HEAD_SHA exists; build and tests skipped"
else
echo "found=false" >> "$GITHUB_OUTPUT"
fi

- name: Build test image
if: steps.existing.outputs.found != 'true'
uses: docker/build-push-action@v6
with:
context: .
target: test
push: false
load: true
tags: props:ci-test
build-args: |
APP_VERSION=ci-${{ github.sha }}
GIT_COMMIT=${{ github.sha }}
build-args: GIT_COMMIT=${{ github.event.pull_request.head.sha || github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Save image as artifact
run: docker save props:ci-test | gzip > /tmp/props-ci.tar.gz

- name: Upload image artifact
uses: actions/upload-artifact@v4
with:
name: docker-image
path: /tmp/props-ci.tar.gz
retention-days: 1

test:
name: Test
runs-on: ubuntu-latest-large
needs: build
steps:
- uses: actions/checkout@v4

- name: Download image artifact
uses: actions/download-artifact@v4
with:
name: docker-image
path: /tmp

- name: Load image
run: gunzip -c /tmp/props-ci.tar.gz | docker load

- name: Run tests
if: steps.existing.outputs.found != 'true'
run: CI_IMAGE=props:ci-test docker compose -f docker-compose.ci.yml up --exit-code-from web

- name: Clean up
if: always()
if: always() && steps.existing.outputs.found != 'true'
run: docker compose -f docker-compose.ci.yml down -v

- name: Publish runtime image for this commit
if: steps.existing.outputs.found != 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
uses: docker/build-push-action@v6
with:
context: .
target: runtime
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE }}:sha-${{ github.event.pull_request.head.sha || github.sha }}
build-args: GIT_COMMIT=${{ github.event.pull_request.head.sha || github.sha }}
cache-from: type=gha
43 changes: 0 additions & 43 deletions .github/workflows/develop-image.yml

This file was deleted.

73 changes: 73 additions & 0 deletions .github/workflows/promote-develop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: Promote to develop

# Retags the image built for the merged pull request as :develop. Nothing is
# rebuilt. Same file for github.com and GHES (see ci.yml).

on:
push:
branches: [develop]

concurrency:
group: promote-develop
cancel-in-progress: true

permissions:
contents: read
packages: write

env:
REGISTRY: ${{ github.server_url == 'https://github.com' && 'ghcr.io' || 'containers.github.realworld.net.au' }}
IMAGE: realworldtech/props

jobs:
promote:
name: Retag commit image as develop
runs-on: ${{ vars.RUNNER_LABEL || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to the container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Find the image built for this merge
id: src
run: |
# The merge commit itself was never built; the PR head (second parent)
# was. Fall back to building when no image exists for it.
set -euo pipefail
IMG="$REGISTRY/$IMAGE"
if git rev-parse -q --verify HEAD^2 >/dev/null; then SRC=$(git rev-parse HEAD^2); else SRC=$(git rev-parse HEAD); fi
echo "source commit: $SRC"
if docker buildx imagetools inspect "$IMG:sha-$SRC" >/dev/null 2>&1; then
echo "promote=$IMG:sha-$SRC" >> "$GITHUB_OUTPUT"
else
echo "::warning::no image for $SRC; building from this commit"
echo "promote=" >> "$GITHUB_OUTPUT"
fi

- name: Retag as develop
if: steps.src.outputs.promote != ''
run: docker buildx imagetools create -t "$REGISTRY/$IMAGE:develop" -t "$REGISTRY/$IMAGE:sha-${{ github.sha }}" "${{ steps.src.outputs.promote }}"

- name: Build (fallback, direct push without a tested image)
if: steps.src.outputs.promote == ''
uses: docker/build-push-action@v6
with:
context: .
target: runtime
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE }}:develop
${{ env.REGISTRY }}/${{ env.IMAGE }}:sha-${{ github.sha }}
build-args: GIT_COMMIT=${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
Loading