Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
654d978
feat(minimal): add APACHE_MAX_REQUEST_WORKERS to cap simultaneous Apa…
jsokol Oct 27, 2026
637b8be
feat(minimal): support *_FILE variables for Docker/Kubernetes secrets
jsokol Oct 27, 2026
82589db
ci: run the *_FILE secrets test in container-validation
jsokol Oct 27, 2026
ae9003f
fix(minimal): neutralise metacharacters in credentials reaching shell…
jsokol Oct 27, 2026
edbc614
fix(minimal): act on review of APACHE_MAX_REQUEST_WORKERS
jsokol Oct 27, 2026
3a8f83e
fix(minimal): apply mail settings on MySQL 8 by not forcing a non-TLS…
jsokol Oct 27, 2026
1310858
Merge pull request #178 from simplerisk/FEATURE-apache-max-request-wo…
jsokol Oct 27, 2026
ecc7292
Merge remote-tracking branch 'origin/testing' into feat/file-env-secrets
jsokol Oct 27, 2026
bd04291
Merge pull request #179 from simplerisk/feat/file-env-secrets
jsokol Oct 27, 2026
1eeebc2
fix(minimal): use TLS flags the installed mysql client understands
jsokol Oct 28, 2026
31b9de9
Merge pull request #180 from simplerisk/FIX-mariadb-client-ssl-flags
jsokol Oct 28, 2026
ef71c96
ci(testing): build the minimal image natively per architecture
jsokol Oct 28, 2026
4b4fb01
Merge pull request #181 from simplerisk/FEATURE-native-arm64-build
jsokol Oct 28, 2026
216832d
feat(minimal): ship the AWS RDS CA bundle and verify the database ser…
jsokol Nov 2, 2026
877fa66
Merge pull request #182 from simplerisk/FEATURE-rds-ca-bundle
jsokol Nov 2, 2026
02135a9
Testing image trigger: version 20261105-001 (code-development @ 02620…
Nov 6, 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
15 changes: 15 additions & 0 deletions .github/workflows/container-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,21 @@ jobs:
- name: simplerisk (full-stack) generator
run: ./simplerisk/test_generate_dockerfile.sh

- name: simplerisk-minimal *_FILE secrets support
run: ./simplerisk-minimal/test_file_env.sh

- name: simplerisk-minimal credential escaping
run: ./simplerisk-minimal/test_escaping.sh

- name: simplerisk-minimal DB TLS flags per mysql client
run: ./simplerisk-minimal/test_db_ssl_flags.sh

- name: simplerisk-minimal vendored RDS CA bundle
run: ./simplerisk-minimal/test_rds_bundle.sh

- name: simplerisk-minimal Apache worker setting
run: ./simplerisk-minimal/test_set_apache_workers.sh

- name: Fail if a generator left the committed Dockerfile modified
run: |
set -euo pipefail
Expand Down
246 changes: 176 additions & 70 deletions .github/workflows/publish-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ name: Publish testing images + promote
# same recipe the code-development `test_docker_deploy` smoke uses, but pushed
# to Docker Hub.
#
# The minimal image is built NATIVELY per architecture: a matrix of
# {php83, php84, php85} x {amd64 on ubuntu-latest, arm64 on ubuntu-24.04-arm}
# pushes each by digest, then merge-minimal stitches the two digests of each PHP
# variant into one multi-arch tag with `docker buildx imagetools create`. The
# arm64 half used to be QEMU-emulated in one job (about 27 min per variant, 88 min
# total, 24 of every 27 minutes being the PHP extension compile); natively it is
# about the amd64 time (about 2.5 min of build work per variant). The published
# tags, their multi-arch shape (amd64 + arm64 + attestations), the Grype gate and
# the single SSM promote are unchanged.
#
# Dry run: dispatch this workflow from `testing` with dry_run=true to build,
# merge and scan under <VERSION>-nativetest-php8x tags. It moves no real tag (no
# bare <VERSION>, no :testing) and does not promote. The `testing` environment
# only admits the `testing` branch, so a dry run cannot be dispatched from a
# feature branch.
#
# Tags (see design code-development docs/superpowers/specs/2026-07-10-release-image-
# 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
Expand All @@ -36,6 +52,13 @@ on:
push:
branches: [testing]
workflow_dispatch:
inputs:
dry_run:
description: >
Build, merge and scan under <VERSION>-nativetest-php8x tags only: no bare
<VERSION>, no :testing, no SSM promote.
type: boolean
default: false

permissions:
contents: read
Expand All @@ -60,8 +83,14 @@ jobs:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.ver.outputs.version }}
# "" for a real publish, "-nativetest" for a dry run: tags are
# <VERSION><infix>-php8x, so a dry run can never collide with a real tag.
infix: ${{ steps.ver.outputs.infix }}
dry_run: ${{ steps.ver.outputs.dry_run }}
steps:
- name: Derive VERSION from the testing channel
env:
DRY_RUN: ${{ inputs.dry_run }}
id: ver
run: |
set -euo pipefail
Expand All @@ -80,14 +109,35 @@ jobs:
fi
echo "testing bundle: $BUNDLE (version $VERSION)"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
if [ "${DRY_RUN:-}" = "true" ]; then
echo "infix=-nativetest" >> "$GITHUB_OUTPUT"
echo "dry_run=true" >> "$GITHUB_OUTPUT"
echo "::notice::DRY RUN: tags will be ${VERSION}-nativetest-php8x; no real tag moves and nothing is promoted"
else
echo "infix=" >> "$GITHUB_OUTPUT"
echo "dry_run=false" >> "$GITHUB_OUTPUT"
fi

publish:
name: Publish simplerisk-minimal RC images
# One leg per {PHP variant} x {architecture}, each on a runner of that architecture, so
# nothing is emulated. Each leg pushes its image BY DIGEST (no tag) and uploads the digest;
# merge-minimal below turns the two digests of a variant into the multi-arch tag.
build-minimal:
name: Build minimal php${{ matrix.php }} ${{ matrix.arch }}
needs: resolve
environment: testing
runs-on: ubuntu-latest
# 3 sequential multi-arch (arm64 = QEMU-emulated, slow) variant builds run here.
timeout-minutes: 120
runs-on: ${{ matrix.runner }}
timeout-minutes: 60
strategy:
# One failed leg means there is no complete release; do not spend the rest.
fail-fast: true
matrix:
php: ['83', '84', '85']
arch: [amd64, arm64]
include:
- arch: amd64
runner: ubuntu-latest
- arch: arm64
runner: ubuntu-24.04-arm
steps:
- name: Checkout (docker@testing)
uses: actions/checkout@v6
Expand Down Expand Up @@ -131,9 +181,7 @@ jobs:
cp /tmp/testing.sql common/simplerisk.sql
test -d simplerisk || { echo "::error::bundle did not extract a simplerisk/ dir"; exit 1; }

- name: Set up QEMU
uses: docker/setup-qemu-action@v4

# No QEMU: this leg builds only its own architecture, on a runner of that architecture.
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

Expand All @@ -143,76 +191,134 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}

- 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 }}:${{ needs.resolve.outputs.version }}-php83
cache-from: type=gha,scope=minimal-testing-php83
cache-to: type=gha,mode=max,scope=minimal-testing-php83
- name: Derive the PHP version (8.3 from 83)
id: php
env:
PHP: ${{ matrix.php }}
run: echo "version=${PHP:0:1}.${PHP:1}" >> "$GITHUB_OUTPUT"

- name: Build and push php84 — <VERSION>-php84
- name: Build and push by digest — php${{ matrix.php }} ${{ matrix.arch }}
id: build
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 }}:${{ needs.resolve.outputs.version }}-php84
cache-from: type=gha,scope=minimal-testing-php84
cache-to: type=gha,mode=max,scope=minimal-testing-php84
platforms: linux/${{ matrix.arch }}
build-args: php_version=${{ steps.php.outputs.version }}
# No tag: the digest is merged into the tags by merge-minimal.
outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=minimal-testing-php${{ matrix.php }}-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=minimal-testing-php${{ matrix.php }}-${{ matrix.arch }}

- name: Build and push php85 (default) — <VERSION>-php85 + <VERSION> + :testing
uses: docker/build-push-action@v7
- name: Record the digest
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
set -euo pipefail
printf '%s' "$DIGEST" | grep -qE '^sha256:[a-f0-9]{64}$' \
|| { echo "::error::build produced no image digest (got '$DIGEST')"; exit 1; }
mkdir -p /tmp/digests
touch "/tmp/digests/${DIGEST#sha256:}"

- name: Upload the digest
uses: actions/upload-artifact@v4
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 }}:${{ 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

# Scan the images exactly as published, BEFORE the SSM promote below rolls them
# out to tier=testing customers. Each scan is continue-on-error so all three
# reports are produced and uploaded; the gate step then fails the job (and so
# skips the promote) if any of them hit a critical finding.
- name: Scan php83 (published)
id: scan83
continue-on-error: true
uses: ./.github/actions/grype-scan
name: digests-php${{ matrix.php }}-${{ matrix.arch }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1

# Stitch the amd64 and arm64 digests of each PHP variant into the published tags.
merge-minimal:
name: Merge minimal php${{ matrix.php }} (amd64 + arm64)
needs: [resolve, build-minimal]
environment: testing
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: true
matrix:
php: ['83', '84', '85']
steps:
- name: Download the digests
uses: actions/download-artifact@v4
with:
image: ${{ env.IMAGE_NAME }}:${{ needs.resolve.outputs.version }}-php83
label: published-minimal-php83
- name: Scan php84 (published)
id: scan84
continue-on-error: true
uses: ./.github/actions/grype-scan
path: /tmp/digests
pattern: digests-php${{ matrix.php }}-*
merge-multiple: true

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

- name: Log in to Docker Hub
uses: docker/login-action@v4
with:
image: ${{ env.IMAGE_NAME }}:${{ needs.resolve.outputs.version }}-php84
label: published-minimal-php84
- name: Scan php85 (published)
id: scan85
continue-on-error: true
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}

- name: Create the multi-arch tags
env:
VERSION: ${{ needs.resolve.outputs.version }}
INFIX: ${{ needs.resolve.outputs.infix }}
PHP: ${{ matrix.php }}
run: |
set -euo pipefail
cd /tmp/digests
mapfile -t digests < <(ls)
if [ "${#digests[@]}" -ne 2 ]; then
echo "::error::php${PHP}: expected 2 digests (amd64, arm64), found ${#digests[@]}: ${digests[*]:-none}"; exit 1
fi
refs=()
for d in "${digests[@]}"; do refs+=("${IMAGE_NAME}@sha256:${d}"); done

# <VERSION>-php8x always; php85 (the default) also carries the bare <VERSION> and
# :testing. A dry run (INFIX=-nativetest) never touches the bare tag or :testing.
tags=("${IMAGE_NAME}:${VERSION}${INFIX}-php${PHP}")
if [ "$PHP" = "85" ] && [ -z "$INFIX" ]; then
tags+=("${IMAGE_NAME}:${VERSION}" "${IMAGE_NAME}:testing")
fi
args=(); for t in "${tags[@]}"; do args+=(-t "$t"); done
docker buildx imagetools create "${args[@]}" "${refs[@]}"

# Do not publish a half-built release: every tag must carry BOTH architectures.
for t in "${tags[@]}"; do
plats=$(docker buildx imagetools inspect "$t" --format '{{json .Manifest}}' \
| jq -r '[.manifests[].platform | select(.os == "linux") | "\(.os)/\(.architecture)"] | unique | join(",")')
echo "$t -> $plats"
[ "$plats" = "linux/amd64,linux/arm64" ] \
|| { echo "::error::$t is missing an architecture (got '$plats')"; exit 1; }
done

# Scan the images exactly as published, BEFORE the SSM promote rolls them out to
# tier=testing customers. Every leg uploads its report; any critical finding fails the
# leg, and promote needs all three legs, so a critical finding blocks the promote.
scan-minimal:
name: Scan minimal php${{ matrix.php }} (published)
needs: [resolve, merge-minimal]
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
php: ['83', '84', '85']
steps:
- name: Checkout (docker@testing)
uses: actions/checkout@v6
- name: Scan php${{ matrix.php }} (published)
uses: ./.github/actions/grype-scan
with:
image: ${{ env.IMAGE_NAME }}:${{ needs.resolve.outputs.version }}-php85
label: published-minimal-php85
- name: Block the promote on critical findings
if: ${{ steps.scan83.outcome == 'failure' || steps.scan84.outcome == 'failure' || steps.scan85.outcome == 'failure' }}
run: |
echo "::error::Grype found critical vulnerabilities in the published images; not promoting to the testing tier. See the scan steps above."
exit 1
image: ${{ env.IMAGE_NAME }}:${{ needs.resolve.outputs.version }}${{ needs.resolve.outputs.infix }}-php${{ matrix.php }}
label: published-minimal-php${{ matrix.php }}

promote:
name: Promote — SSM /image-tag/testing
needs: [resolve, scan-minimal]
# A dry run publishes under -nativetest tags only; nothing may roll out to tier=testing.
if: ${{ needs.resolve.outputs.dry_run != 'true' }}
environment: testing
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Configure AWS credentials (OIDC → customers account)
uses: aws-actions/configure-aws-credentials@v4
with:
Expand Down Expand Up @@ -340,7 +446,7 @@ jobs:
# always(): a critical finding fails a publish job (and blocks the promote), and
# that is exactly when the report matters.
if: ${{ always() }}
needs: [publish, publish-full]
needs: [scan-minimal, publish-full]
runs-on: ubuntu-latest
permissions:
contents: read
Expand All @@ -353,8 +459,8 @@ jobs:
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
context: ${{ github.workflow }} · ${{ github.ref_name }}
results: |
published-minimal-php83=${{ needs.publish.result }}
published-minimal-php84=${{ needs.publish.result }}
published-minimal-php85=${{ needs.publish.result }}
published-minimal-php83=${{ needs.scan-minimal.result }}
published-minimal-php84=${{ needs.scan-minimal.result }}
published-minimal-php85=${{ needs.scan-minimal.result }}
published-full-jammy=${{ needs.publish-full.result }}
published-full-noble=${{ needs.publish-full.result }}
Loading
Loading