Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 26 additions & 0 deletions .github/workflows/container-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,29 @@ jobs:
dockerfile_path: "simplerisk-minimal/Dockerfile"
image_tag: "simplerisk/simplerisk-minimal:testing"
build_args: "php_version=8.5\nPREGA_BUNDLE_FALLBACK=true"

generator_checks:
name: 'Verify the Dockerfile generators (version/source-mode decoupling)'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6

# Both generators take (version, source_mode). The RC build depends on
# `context` mode stamping a real ENV version while sourcing the app from
# the build context; the release build depends on `download` mode still
# emitting the hash-verifying downloader stage. These harnesses pin both.
- name: simplerisk-minimal generator
run: ./simplerisk-minimal/test_generate_dockerfile.sh

- name: simplerisk (full-stack) generator
run: ./simplerisk/test_generate_dockerfile.sh

- name: Fail if a generator left the committed Dockerfile modified
run: |
set -euo pipefail
if ! git diff --quiet -- simplerisk/Dockerfile simplerisk-minimal/Dockerfile; then
echo "::error::a generator harness left a committed Dockerfile modified"
git --no-pager diff -- simplerisk/Dockerfile simplerisk-minimal/Dockerfile
exit 1
fi
167 changes: 139 additions & 28 deletions .github/workflows/publish-testing.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
name: Publish simplerisk-minimal testing image + promote
name: Publish testing images + promote

# Publishes a TESTING-channel simplerisk-minimal image and promotes it into the
# dedicated-hosting (customers-cdk) account so tier=testing customers auto-update.
# Publishes the TESTING-channel SimpleRisk images and promotes the minimal one
# into the dedicated-hosting (customers-cdk) account so tier=testing customers
# auto-update.
#
# Trigger: a push to this repo's `testing` branch (code-development mirrors the
# current testing version here on each testing publish — see the code-development
Expand All @@ -11,19 +12,25 @@ name: Publish simplerisk-minimal testing image + promote
# code) + the database/master schema, via `generate_dockerfile.sh "$VERSION"
# context` (COPYs the app from the assembled context, real `ENV version`) — the
# same recipe the code-development `test_docker_deploy` smoke uses, but pushed
# multi-arch to Docker Hub.
# to Docker Hub.
#
# Tags (see design code-development docs/superpowers/specs/2026-07-10-release-image-
# promotion-design): the RC is built ONCE here from the testing bundle and later
# PROMOTED (not rebuilt) to prod. Publishes immutable per-PHP variants
# <VERSION>-php83/-php84/-php85, the bare <VERSION> (= php85, the default/main),
# and moves the floating :testing alias to it. There is NO <VERSION>-testing tag.
# The floating :latest and the prod SSM tier are moved by the separate GA promote
# (Plan 2), which retags this same digest — it never rebuilds.
# promotion-design): every release image is built ONCE here from the testing
# bundle and later PROMOTED (not rebuilt) to prod by promote-latest.yml on
# master, which retags these same digests. Nothing is rebuilt at GA.
#
# simplerisk/simplerisk-minimal <VERSION>-php83/-php84/-php85, bare <VERSION>
# (= php85, the default), and :testing
# simplerisk/simplerisk <VERSION>-jammy/-noble, bare <VERSION>
# (= noble, the default), and :testing
#
# There is NO <VERSION>-testing tag. The floating :latest and the prod SSM tier
# are moved only by the GA promote.
#
# Promote: writes SSM /simplerisk/customers/image-tag/testing = <VERSION>-php85
# in the customers account via OIDC; the image-updater Lambda there rolls every
# tier=testing service (new image + fresh extras together).
# tier=testing service (new image + fresh extras together). The full-stack image
# is not part of the managed fleet, so it has no SSM tier.

on:
push:
Expand All @@ -40,20 +47,22 @@ concurrency:

env:
IMAGE_NAME: simplerisk/simplerisk-minimal
FULL_IMAGE_NAME: simplerisk/simplerisk
AWS_REGION: us-east-1
SSM_PARAM: /simplerisk/customers/image-tag/testing

jobs:
publish:
# Resolve the version ONCE so both publish jobs stamp the same release, even
# if the testing channel rotates mid-run. Each publish job then fetches that
# exact bundle by name and verifies its hash independently.
resolve:
name: Resolve the current testing version
runs-on: ubuntu-latest
# 3 sequential multi-arch (arm64 = QEMU-emulated, slow) variant builds run here.
timeout-minutes: 120
outputs:
version: ${{ steps.ver.outputs.version }}
steps:
- name: Checkout (docker@testing)
uses: actions/checkout@v6

- name: Resolve current testing version + fetch bundle/schema
id: fetch
- name: Derive VERSION from the testing channel
id: ver
run: |
set -euo pipefail
# The testing channel keeps exactly one current bundle; list it and
Expand All @@ -70,10 +79,28 @@ jobs:
echo "::error::could not derive VERSION from bundle '$BUNDLE'"; exit 1
fi
echo "testing bundle: $BUNDLE (version $VERSION)"
curl -fsSL -o /tmp/testing-bundle.tgz "https://bundles-test.simplerisk.com/$BUNDLE"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"

publish:
name: Publish simplerisk-minimal RC images
needs: resolve
runs-on: ubuntu-latest
# 3 sequential multi-arch (arm64 = QEMU-emulated, slow) variant builds run here.
timeout-minutes: 120
steps:
- name: Checkout (docker@testing)
uses: actions/checkout@v6

- name: Fetch + verify the testing bundle and schema
env:
VERSION: ${{ needs.resolve.outputs.version }}
run: |
set -euo pipefail
curl -fsSL -o /tmp/testing-bundle.tgz \
"https://bundles-test.simplerisk.com/simplerisk-${VERSION}.tgz"
# Integrity: verify the bundle against the sha256 published in the served
# updates-test feed (publish-bundle writes the hash on the same push).
# VERSION is regex-guarded, so it is safe in the sed pattern.
# VERSION is regex-guarded upstream, so it is safe in the sed pattern.
EXPECTED_SHA=$(curl -fsSL "https://updates-test.simplerisk.com/releases.xml" \
| sed -n "/<release version=\"${VERSION}\">/,/<\/release>/p" \
| grep -oE '<bundle_sha256>[a-f0-9]{64}</bundle_sha256>' | head -1 | grep -oE '[a-f0-9]{64}')
Expand All @@ -88,11 +115,10 @@ jobs:
SQL_URL="https://raw.githubusercontent.com/simplerisk/database/master/simplerisk-en-${VERSION}.sql"
curl -fsSL -o /tmp/testing.sql "$SQL_URL" \
|| { echo "::error::release schema not found on database/master: $SQL_URL"; exit 1; }
echo "version=$VERSION" >> "$GITHUB_OUTPUT"

- name: Assemble the testing build context
env:
VERSION: ${{ steps.fetch.outputs.version }}
VERSION: ${{ needs.resolve.outputs.version }}
run: |
set -euo pipefail
cd simplerisk-minimal
Expand Down Expand Up @@ -124,7 +150,7 @@ jobs:
push: true
platforms: linux/amd64,linux/arm64
build-args: php_version=8.3
tags: ${{ env.IMAGE_NAME }}:${{ steps.fetch.outputs.version }}-php83
tags: ${{ env.IMAGE_NAME }}:${{ needs.resolve.outputs.version }}-php83
cache-from: type=gha,scope=minimal-testing-php83
cache-to: type=gha,mode=max,scope=minimal-testing-php83

Expand All @@ -136,7 +162,7 @@ jobs:
push: true
platforms: linux/amd64,linux/arm64
build-args: php_version=8.4
tags: ${{ env.IMAGE_NAME }}:${{ steps.fetch.outputs.version }}-php84
tags: ${{ env.IMAGE_NAME }}:${{ needs.resolve.outputs.version }}-php84
cache-from: type=gha,scope=minimal-testing-php84
cache-to: type=gha,mode=max,scope=minimal-testing-php84

Expand All @@ -149,8 +175,8 @@ jobs:
platforms: linux/amd64,linux/arm64
build-args: php_version=8.5
tags: |
${{ env.IMAGE_NAME }}:${{ steps.fetch.outputs.version }}-php85
${{ env.IMAGE_NAME }}:${{ steps.fetch.outputs.version }}
${{ env.IMAGE_NAME }}:${{ needs.resolve.outputs.version }}-php85
${{ env.IMAGE_NAME }}:${{ needs.resolve.outputs.version }}
${{ env.IMAGE_NAME }}:testing
cache-from: type=gha,scope=minimal-testing-php85
cache-to: type=gha,mode=max,scope=minimal-testing-php85
Expand All @@ -163,10 +189,95 @@ jobs:

- name: Promote — SSM /image-tag/testing = <VERSION>-php85
env:
VERSION: ${{ steps.fetch.outputs.version }}
VERSION: ${{ needs.resolve.outputs.version }}
run: |
set -euo pipefail
aws ssm put-parameter --name "$SSM_PARAM" \
--value "${VERSION}-php85" --type String --overwrite \
--region "$AWS_REGION"
echo "promoted $SSM_PARAM = ${VERSION}-php85" >> "$GITHUB_STEP_SUMMARY"

# The full-stack image is built here for the same build-once reason as the
# minimal one: GA promotes this digest rather than rebuilding from the prod
# bundle. It is amd64-only, matching what the release build published before.
publish-full:
name: Publish simplerisk (full-stack) RC images
needs: resolve
runs-on: ubuntu-latest
timeout-minutes: 90
permissions:
contents: read
steps:
- name: Checkout (docker@testing)
uses: actions/checkout@v6

- name: Fetch + verify the testing bundle and schema
env:
VERSION: ${{ needs.resolve.outputs.version }}
run: |
set -euo pipefail
curl -fsSL -o /tmp/testing-bundle.tgz \
"https://bundles-test.simplerisk.com/simplerisk-${VERSION}.tgz"
EXPECTED_SHA=$(curl -fsSL "https://updates-test.simplerisk.com/releases.xml" \
| sed -n "/<release version=\"${VERSION}\">/,/<\/release>/p" \
| grep -oE '<bundle_sha256>[a-f0-9]{64}</bundle_sha256>' | head -1 | grep -oE '[a-f0-9]{64}')
if ! printf '%s' "$EXPECTED_SHA" | grep -qE '^[a-f0-9]{64}$'; then
echo "::error::no bundle_sha256 for $VERSION in updates-test releases.xml"; exit 1
fi
ACTUAL_SHA=$(sha256sum /tmp/testing-bundle.tgz | cut -d' ' -f1)
if [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then
echo "::error::bundle sha256 mismatch for $VERSION (expected $EXPECTED_SHA, got $ACTUAL_SHA)"; exit 1
fi
echo "bundle sha256 verified"
SQL_URL="https://raw.githubusercontent.com/simplerisk/database/master/simplerisk-en-${VERSION}.sql"
curl -fsSL -o /tmp/testing.sql "$SQL_URL" \
|| { echo "::error::release schema not found on database/master: $SQL_URL"; exit 1; }

- name: Assemble the testing build context
env:
VERSION: ${{ needs.resolve.outputs.version }}
run: |
set -euo pipefail
cd simplerisk
# Same context recipe as the minimal image: COPY the app + schema from
# the build context, with a real `ENV version=$VERSION`.
./generate_dockerfile.sh "$VERSION" context
tar xzf /tmp/testing-bundle.tgz -C .
cp /tmp/testing.sql common/simplerisk.sql
test -d simplerisk || { echo "::error::bundle did not extract a simplerisk/ dir"; exit 1; }

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

- name: Log in to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}

- name: Build and push jammy — <VERSION>-jammy
uses: docker/build-push-action@v7
with:
context: simplerisk
file: simplerisk/Dockerfile
push: true
platforms: linux/amd64
build-args: ubuntu_version_code=jammy
tags: ${{ env.FULL_IMAGE_NAME }}:${{ needs.resolve.outputs.version }}-jammy
cache-from: type=gha,scope=full-testing-jammy
cache-to: type=gha,mode=max,scope=full-testing-jammy

- name: Build and push noble (default) — <VERSION>-noble + <VERSION> + :testing
uses: docker/build-push-action@v7
with:
context: simplerisk
file: simplerisk/Dockerfile
push: true
platforms: linux/amd64
build-args: ubuntu_version_code=noble
tags: |
${{ env.FULL_IMAGE_NAME }}:${{ needs.resolve.outputs.version }}-noble
${{ env.FULL_IMAGE_NAME }}:${{ needs.resolve.outputs.version }}
${{ env.FULL_IMAGE_NAME }}:testing
cache-from: type=gha,scope=full-testing-noble
cache-to: type=gha,mode=max,scope=full-testing-noble
4 changes: 3 additions & 1 deletion simplerisk/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ SHELL [ "/bin/ash", "-eo", "pipefail", "-c" ]

# Download the prod bundle, verify its published sha256 (md5 fallback) from the
# updates feed, then extract (fail-closed) -- then fetch the release SQL schema.
# -fsSL on the SQL fetch too: without --fail, curl writes the 404 body into
# /simplerisk.sql and the image ships an HTML error page as its schema.
COPY common/download_and_verify_bundle.sh /download_and_verify_bundle.sh
RUN PREGA_BUNDLE_FALLBACK="$PREGA_BUNDLE_FALLBACK" sh /download_and_verify_bundle.sh 20260519-001 && \
curl -sL "https://github.com/simplerisk/database/raw/master/simplerisk-$DB_LANG-20260519-001.sql" > /simplerisk.sql
curl -fsSL "https://github.com/simplerisk/database/raw/master/simplerisk-$DB_LANG-20260519-001.sql" > /simplerisk.sql

# Using Ubuntu image
FROM ubuntu:${ubuntu_version_code}
Expand Down
24 changes: 20 additions & 4 deletions simplerisk/generate_dockerfile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,33 @@ set -euo pipefail
SCRIPT_LOCATION="$(dirname "$(readlink -f "$0")")"
readonly SCRIPT_LOCATION

if [ $# -eq 1 ]; then
if [ $# -ge 1 ]; then
release=$1
else
echo "No release version provided. Aborting." && exit 1
fi
# Source mode: `context` = COPY the app from the build context (no downloader
# stage); `download` = COPY --from=downloader (curl + hash-verify the prod
# bundle). Splitting this out of $release lets the RC build stamp a real
# `ENV version=<VERSION>` while still sourcing bytes from the build context --
# the build-once half of the promote model. Default preserves back-compat:
# context when release==testing, else download.
if [ $# -ge 2 ]; then
source_mode=$2
else
if [ "$release" == "testing" ]; then source_mode="context"; else source_mode="download"; fi
fi
if [ "$source_mode" != "context" ] && [ "$source_mode" != "download" ]; then
echo "Invalid source mode '$source_mode' (expected context|download). Aborting." && exit 1
fi

cat << EOF > "${SCRIPT_LOCATION}/Dockerfile"
# Dockerfile generated by script
ARG ubuntu_version_code=noble

EOF

if [ "$release" != "testing" ]; then
if [ "$source_mode" == "download" ]; then
cat << EOF >> "${SCRIPT_LOCATION}/Dockerfile"
FROM alpine/curl:8.12.1 AS downloader

Expand All @@ -29,9 +43,11 @@ SHELL [ "/bin/ash", "-eo", "pipefail", "-c" ]

# Download the prod bundle, verify its published sha256 (md5 fallback) from the
# updates feed, then extract (fail-closed) -- then fetch the release SQL schema.
# -fsSL on the SQL fetch too: without --fail, curl writes the 404 body into
# /simplerisk.sql and the image ships an HTML error page as its schema.
COPY common/download_and_verify_bundle.sh /download_and_verify_bundle.sh
RUN PREGA_BUNDLE_FALLBACK="\$PREGA_BUNDLE_FALLBACK" sh /download_and_verify_bundle.sh $release && \\
curl -sL "https://github.com/simplerisk/database/raw/master/simplerisk-\$DB_LANG-$release.sql" > /simplerisk.sql
curl -fsSL "https://github.com/simplerisk/database/raw/master/simplerisk-\$DB_LANG-$release.sql" > /simplerisk.sql

EOF
fi
Expand Down Expand Up @@ -96,7 +112,7 @@ RUN echo "\$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c\${1:-32})" > /password
# Install common files
COPY common/ /
EOF
if [ "$release" == "testing" ]; then
if [ "$source_mode" == "context" ]; then
cat << EOF >> "${SCRIPT_LOCATION}/Dockerfile"
COPY common/simplerisk.sql /simplerisk.sql
COPY ./simplerisk/ /var/www/simplerisk
Expand Down
Loading