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
17 changes: 13 additions & 4 deletions .github/workflows/container-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,29 @@ jobs:
image_tag: "simplerisk/simplerisk:testing"
build_args: "ubuntu_version_code=noble"

simplerisk-minimal-php84:
simplerisk-minimal-php83:
name: 'Verify simplerisk/simplerisk-minimal image based on PHP 8.3 with Apache'
uses: ./.github/workflows/verify-image_rw.yml
with:
context_path: "simplerisk-minimal/"
dockerfile_path: "simplerisk-minimal/Dockerfile"
image_tag: "simplerisk/simplerisk-minimal:testing"
build_args: "php_version=8.3"
build_args: "php_version=8.3\nPREGA_BUNDLE_FALLBACK=true"

simplerisk-minimal-php85:
simplerisk-minimal-php84:
name: 'Verify simplerisk/simplerisk-minimal image based on PHP 8.4 with Apache'
uses: ./.github/workflows/verify-image_rw.yml
with:
context_path: "simplerisk-minimal/"
dockerfile_path: "simplerisk-minimal/Dockerfile"
image_tag: "simplerisk/simplerisk-minimal:testing"
build_args: "php_version=8.4"
build_args: "php_version=8.4\nPREGA_BUNDLE_FALLBACK=true"

simplerisk-minimal-php85:
name: 'Verify simplerisk/simplerisk-minimal image based on PHP 8.5 with Apache'
uses: ./.github/workflows/verify-image_rw.yml
with:
context_path: "simplerisk-minimal/"
dockerfile_path: "simplerisk-minimal/Dockerfile"
image_tag: "simplerisk/simplerisk-minimal:testing"
build_args: "php_version=8.5\nPREGA_BUNDLE_FALLBACK=true"
71 changes: 51 additions & 20 deletions .github/workflows/publish-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@ name: Publish simplerisk-minimal testing image + promote
# `sync_docker_testing` workflow), or a manual dispatch.
#
# Build: the CURRENT testing bundle from bundles-test (the built testing-branch
# code) + the database/testing schema, via `generate_dockerfile.sh testing`
# (COPYs the app from the context) — the same recipe the code-development
# `test_docker_deploy` smoke uses, but pushed multi-arch to Docker Hub.
# code) + the database/testing 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.
#
# Tags (see design 2026-07-01-testing-image-promote): an IMMUTABLE per-version
# tag `<VERSION>-testing` plus the floating `:testing` alias. The bare `<VERSION>`
# and `:latest` tags are RESERVED for the release build (master) and are NOT
# touched here — the testing and release images are different builds (testing
# bundle vs finalized public bundle), so they must not share the bare version tag.
# 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.
#
# Promote: writes SSM /simplerisk/customers/image-tag/testing = <VERSION>-testing
# 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).

Expand All @@ -43,7 +46,8 @@ env:
jobs:
publish:
runs-on: ubuntu-latest
timeout-minutes: 40
# 3 sequential multi-arch (arm64 = QEMU-emulated, slow) variant builds run here.
timeout-minutes: 120
steps:
- name: Checkout (docker@testing)
uses: actions/checkout@v6
Expand Down Expand Up @@ -92,9 +96,10 @@ jobs:
run: |
set -euo pipefail
cd simplerisk-minimal
# generate_dockerfile.sh testing -> a Dockerfile that COPYs simplerisk/
# (app) + common/simplerisk.sql (schema) from this context.
./generate_dockerfile.sh testing
# generate_dockerfile.sh "$VERSION" context -> a Dockerfile that COPYs
# simplerisk/ (app) + common/simplerisk.sql (schema) from this context,
# with a real `ENV version=$VERSION` (no literal "testing").
./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; }
Expand All @@ -111,31 +116,57 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}

- name: Build and push (multi-arch) — <VERSION>-testing + :testing
- name: Build and push php83 — <VERSION>-php83
uses: docker/build-push-action@v7
with:
context: simplerisk-minimal
file: simplerisk-minimal/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
build-args: php_version=8.3
tags: ${{ env.IMAGE_NAME }}:${{ steps.fetch.outputs.version }}-php83
cache-from: type=gha,scope=minimal-testing-php83
cache-to: type=gha,mode=max,scope=minimal-testing-php83

- name: Build and push php84 — <VERSION>-php84
uses: docker/build-push-action@v7
with:
context: simplerisk-minimal
file: simplerisk-minimal/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
build-args: php_version=8.4
tags: ${{ env.IMAGE_NAME }}:${{ steps.fetch.outputs.version }}-php84
cache-from: type=gha,scope=minimal-testing-php84
cache-to: type=gha,mode=max,scope=minimal-testing-php84

- name: Build and push php85 (default) — <VERSION>-php85 + <VERSION> + :testing
uses: docker/build-push-action@v7
with:
context: simplerisk-minimal
file: simplerisk-minimal/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
build-args: php_version=8.5
tags: |
${{ env.IMAGE_NAME }}:${{ steps.fetch.outputs.version }}-testing
${{ env.IMAGE_NAME }}:${{ steps.fetch.outputs.version }}-php85
${{ env.IMAGE_NAME }}:${{ steps.fetch.outputs.version }}
${{ env.IMAGE_NAME }}:testing
cache-from: type=gha,scope=minimal-testing
cache-to: type=gha,mode=max,scope=minimal-testing
cache-from: type=gha,scope=minimal-testing-php85
cache-to: type=gha,mode=max,scope=minimal-testing-php85

- name: Configure AWS credentials (OIDC → customers account)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.IMAGE_PROMOTER_TESTING_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}

- name: Promote — SSM /image-tag/testing = <VERSION>-testing
- name: Promote — SSM /image-tag/testing = <VERSION>-php85
env:
VERSION: ${{ steps.fetch.outputs.version }}
run: |
set -euo pipefail
aws ssm put-parameter --name "$SSM_PARAM" \
--value "${VERSION}-testing" --type String --overwrite \
--value "${VERSION}-php85" --type String --overwrite \
--region "$AWS_REGION"
echo "promoted $SSM_PARAM = ${VERSION}-testing" >> "$GITHUB_STEP_SUMMARY"
echo "promoted $SSM_PARAM = ${VERSION}-php85" >> "$GITHUB_STEP_SUMMARY"
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Both images use multi-stage builds (Alpine curl downloader stage → main stage)
# Full-stack (build args: ubuntu_version_code=jammy|noble)
docker build -t simplerisk/simplerisk simplerisk/

# Minimal (build args: php_version=8.1|8.3|8.4)
# Minimal (build args: php_version=8.3|8.4|8.5)
docker build -t simplerisk/simplerisk-minimal simplerisk-minimal/
```

Expand Down Expand Up @@ -115,7 +115,7 @@ The entrypoint script handles:

### CI/CD

- **PRs** trigger `container-validation.yml`: builds all 4 variants (jammy, noble, php81, php83), runs Dockle (Dockerfile linter) and Grype (CVE scanner, severity cutoff: critical, only-fixed).
- **PRs** trigger `container-validation.yml`: builds all 4 variants (jammy, noble, php83, php84, php85), runs Dockle (Dockerfile linter) and Grype (CVE scanner, severity cutoff: critical, only-fixed).
- **Pushes** trigger separate workflows to publish to Docker Hub and GitHub Container Registry (GHCR). GHCR images are signed with Cosign/sigstore. The `simplerisk-minimal` push builds target both `linux/amd64` and `linux/arm64`.
- The reusable workflow files (`*_rw.yml`) are called by the entry-point workflows.

Expand Down
16 changes: 14 additions & 2 deletions simplerisk-minimal/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
# Dockerfile generated by script
ARG php_version=8.4
ARG php_version=8.5

FROM alpine/curl:8.12.1 AS downloader

# PREGA_BUNDLE_FALLBACK is a CI-ONLY switch, default false. Pre-GA the prod
# bundle for a new version does not exist yet (it lands in public/bundles/ only
# at GA). CI sets this true so a pre-GA build can fall back to the testing
# bundle. A released image is ALWAYS built from the prod bundle and NEVER
# silently falls back to testing bytes (even on a transient prod failure).
ARG PREGA_BUNDLE_FALLBACK=false

SHELL [ "/bin/ash", "-eo", "pipefail", "-c" ]

RUN mkdir -p /var/www && \
curl -sL https://simplerisk-downloads.s3.amazonaws.com/public/bundles/simplerisk-20260519-001.tgz | tar xz -C /var/www
if curl -fsSL https://simplerisk-downloads.s3.amazonaws.com/public/bundles/simplerisk-20260519-001.tgz -o /tmp/bundle.tgz; then true; \
elif [ "$PREGA_BUNDLE_FALLBACK" = "true" ]; then \
echo "prod bundle absent — pre-GA CI fallback to bundles-test"; \
curl -fsSL https://bundles-test.simplerisk.com/simplerisk-20260519-001.tgz -o /tmp/bundle.tgz; \
else echo "prod bundle simplerisk-20260519-001.tgz not found and PREGA_BUNDLE_FALLBACK=false" && exit 1; fi && \
tar xzf /tmp/bundle.tgz -C /var/www && rm -f /tmp/bundle.tgz

FROM php:${php_version}-apache

Expand Down
33 changes: 28 additions & 5 deletions simplerisk-minimal/generate_dockerfile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,49 @@ 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 app from build context (no downloader stage);
# `download` = COPY --from=downloader (curl the prod bundle). 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 php_version=8.4
ARG php_version=8.5

EOF

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

# PREGA_BUNDLE_FALLBACK is a CI-ONLY switch, default false. Pre-GA the prod
# bundle for a new version does not exist yet (it lands in public/bundles/ only
# at GA). CI sets this true so a pre-GA build can fall back to the testing
# bundle. A released image is ALWAYS built from the prod bundle and NEVER
# silently falls back to testing bytes (even on a transient prod failure).
ARG PREGA_BUNDLE_FALLBACK=false

SHELL [ "/bin/ash", "-eo", "pipefail", "-c" ]

RUN mkdir -p /var/www && \\
curl -sL https://simplerisk-downloads.s3.amazonaws.com/public/bundles/simplerisk-$release.tgz | tar xz -C /var/www
if curl -fsSL https://simplerisk-downloads.s3.amazonaws.com/public/bundles/simplerisk-$release.tgz -o /tmp/bundle.tgz; then true; \\
elif [ "\$PREGA_BUNDLE_FALLBACK" = "true" ]; then \\
echo "prod bundle absent — pre-GA CI fallback to bundles-test"; \\
curl -fsSL https://bundles-test.simplerisk.com/simplerisk-$release.tgz -o /tmp/bundle.tgz; \\
else echo "prod bundle simplerisk-$release.tgz not found and PREGA_BUNDLE_FALLBACK=false" && exit 1; fi && \\
tar xzf /tmp/bundle.tgz -C /var/www && rm -f /tmp/bundle.tgz

EOF
fi
Expand Down Expand Up @@ -102,7 +125,7 @@ RUN echo "0 0 * * * root /usr/sbin/logrotate /etc/logrotate.d/simplerisk.conf >
COPY common/ /
EOF
# shellcheck disable=SC2015
if [ "$release" == "testing" ]; then
if [ "$source_mode" == "context" ]; then
cat << EOF >> "${SCRIPT_LOCATION}/Dockerfile"
COPY simplerisk/ /var/www/simplerisk
COPY common/simplerisk.sql /var/www/simplerisk/simplerisk.sql
Expand Down
51 changes: 51 additions & 0 deletions simplerisk-minimal/test_generate_dockerfile.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# Regression checks for generate_dockerfile.sh version/source-mode decoupling.
set -euo pipefail
cd "$(dirname "$(readlink -f "$0")")"

# generate_dockerfile.sh hardcodes its output to the tracked Dockerfile in this
# directory; back it up and restore it on exit (pass or fail) so this checker
# never leaves the committed Dockerfile overwritten.
cp Dockerfile "/tmp/Dockerfile.bak.$$" 2>/dev/null || true
trap 'cp "/tmp/Dockerfile.bak.$$" Dockerfile 2>/dev/null || git checkout -- Dockerfile 2>/dev/null || true; rm -f "/tmp/Dockerfile.bak.$$"' EXIT

fail=0
check() { if grep -qF "$2" Dockerfile; then echo "ok: $1"; else echo "FAIL: $1 (missing: $2)"; fail=1; fi; }
absent() { if grep -qF "$2" Dockerfile; then echo "FAIL: $1 (should be absent: $2)"; fail=1; else echo "ok: $1"; fi; }

# context mode with a real version: no downloader, COPY-from-context, real ENV version, php 8.5 default
./generate_dockerfile.sh 20260709-001 context
check "context: real ENV version" "ENV version=20260709-001"
check "context: COPY app from context" "COPY simplerisk/ /var/www/simplerisk"
absent "context: no downloader stage" "FROM alpine/curl"
check "context: php default 8.5" "ARG php_version=8.5"

# download mode (explicit): downloader present, COPY-from-downloader, real ENV version
./generate_dockerfile.sh 20260709-001 download
check "download: downloader stage" "FROM alpine/curl"
check "download: COPY from downloader" "COPY --from=downloader /var/www/simplerisk /var/www/simplerisk"
check "download: real ENV version" "ENV version=20260709-001"

# back-compat: literal "testing" with no mode arg still selects context recipe
./generate_dockerfile.sh testing
absent "testing back-compat: no downloader" "FROM alpine/curl"
check "testing back-compat: COPY context" "COPY simplerisk/ /var/www/simplerisk"

# invalid source-mode is rejected
if ./generate_dockerfile.sh 20260709-001 bogus >/tmp/bogus-mode.out.$$ 2>&1; then
echo "FAIL: invalid source-mode should be rejected (exited 0)"; fail=1
else
echo "ok: invalid source-mode rejected"
fi
rm -f "/tmp/bogus-mode.out.$$"

# idempotence: re-running the same context args does not double anything
./generate_dockerfile.sh 20260709-001 context
copy_count=$(grep -cF "COPY simplerisk/ /var/www/simplerisk" Dockerfile)
if [ "$copy_count" -eq 1 ]; then
echo "ok: idempotence (single COPY simplerisk/ line after re-run)"
else
echo "FAIL: idempotence (expected 1 COPY simplerisk/ line, found $copy_count)"; fail=1
fi

exit $fail