Sync master into testing so the branches are identical - #165
Merged
Conversation
Lets a public demo instance restrict uploads, password change and language
change through configuration rather than by patching core files. That
matters because the hosted platform runs ONE shared image for every
customer — an instance with patched files cannot run on it, which is why
demo.simplerisk.com is currently off on its own.
Deliberately NOT a placeholder in config.sample.php. This is a
hosting-platform concern, not a self-hosted option, so the define is
APPENDED only when asked for and stays invisible to everyone else.
config.sample.php has no closing "?>", so an appended define lands inside
PHP.
ONLY an explicitly true value activates it (true/1/yes/on, case-insensitive).
This is deliberate, not defensive noise: PHP treats the STRING 'false' as
truthy, so writing define('DEMO_MODE', 'false') would put EVERY customer
into demo mode. The constant's PRESENCE is the signal — core checks
defined('DEMO_MODE'), not its value — and anything not clearly true is
logged and ignored.
The grep guard is for the fallback path in set_config: when
config.sample.php is absent the existing config.php is reused rather than
regenerated, so an unguarded append would redefine the constant on every
boot.
Verified across two boots: exactly one define for true/TRUE/1/yes/on, none
for unset/false/0/no/arbitrary strings, and no duplication when config.php
already carries it.
Minimal image only. The non-minimal image's set_config is an older shape
(hardcoded values, run-once guard) and the hosted platform uses minimal.
Core gating — the defined('DEMO_MODE') checks — is a separate change owned
by the core repo. No config.sample.php change is needed there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…up connections The privileged setup/delete MySQL client in the simplerisk-minimal entrypoint now honors DB_SSL_ENABLED. Set to exactly "true", it appends --ssl-mode=REQUIRED --enable-cleartext-plugin to the db_setup()/delete_db() mysql calls — for databases that require the cleartext auth plugin to be sent over TLS (e.g. when DB_SETUP_PASS is a short-lived token rather than a static password). Defaults off (fail-closed): any other value or unset preserves today's plaintext-capable connection, so published images are unaffected for existing consumers. Documented in README.md and CLAUDE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e; add apt upgrade The CI gate is 'grype --fail-on critical --only-fixed'. On feature/db-ssl-enabled it failed on 4 FIXABLE criticals — CVE-2026-11856/-10536/-8927/-8924 — all against a binary Grype labels 'curl 8.4.24'. That string is the PHP interpreter's own version (PHP/8.4.24, embedded in php/libphp.so/curl.so), misclassified as curl. The real curl is the Debian package (8.14.1-2+deb13u4) and is patched. The testing branch already suppresses these exact 4 CVEs in .grype.yaml; this ports that block (scoped to package name=curl type=binary so a genuine curl finding still surfaces). Also add 'apt-get -y upgrade' to the minimal generator (regenerated Dockerfile) so base Debian packages pick up security point-releases at build time, matching testing. No effect on the current scan (the pulled base is already current) but prevents CVE drift between base-image rebuilds. Evidence (amd64): gate FAIL->PASS. Raw criticals unchanged at 60 (all but the 4 FPs are wont-fix/not-fixed base debt, invisible to --only-fixed). PHP stays 8.4.24; all extensions load; container boots. Remaining fixable Highs are app-bundle deps (twig/phpspreadsheet/simplesamlphp/guzzle/symfony/...) fixed by rebuilding the S3 bundle, out of scope for this repo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…testing
code-development be81d416f9 ("write DB schemas to database/master, drop the
database/testing branch") made master the single source for generated installer
SQL. This workflow was last touched 2026-07-01, ten days earlier, and still
fetched database/testing.
The 20260709-001 RC was cut before that change, so database/testing still held
its SQL and this step passed. 20260811-001 is the first RC cut after it, and the
step failed with a 404 on
database/testing/simplerisk-en-20260811-001.sql — blocking the testing image
publish and the tier=testing SSM promotion. The bundle fetch and its sha256
verification were unaffected.
Also retitles the error so a future failure names the branch it actually
searched rather than saying "testing schema".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An instance on a pre-release image compares itself against the production
updates endpoint, which only advertises the current GA release — so a
canary running 20260811-001 reports a phantom version problem against an
updates service still publishing 20260519-001. The same applies to
registration: a rebuilt canary would register itself into the production
licensing database.
Adds optional SIMPLERISK_{SERVICES,UPDATES,PING,BUNDLES,LICENSING}_URL,
appended to config.php only when supplied. Same shape as DEMO_MODE: no
placeholder in config.sample.php, so an instance that says nothing keeps
the code's built-in production defaults, and self-hosted users see nothing
new.
All five are supported deliberately, not for completeness. LICENSING_URL
superseded SERVICES_URL and PING_URL when registration and ping merged into
the licensing service in the July 2026 release — but the platform runs
different images per release channel, and the `latest` channel currently
points at 20260519-001, which reads the legacy pair. Writing whichever are
supplied keeps one customer config correct across both. Every consumer is
defined()-guarded with a production fallback, so a constant an image does
not know about is simply ignored.
Values are validated rather than trusted: http(s) only, and no quote or
backslash, since they are interpolated into a single-quoted PHP string.
Rejections are logged rather than silent. The grep guard makes it
idempotent for the persisted-volume path where config.php is reused rather
than regenerated.
Verified: valid URLs written once, a non-URL and a quote-injection attempt
both refused, second pass a no-op.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Allow the service endpoint URLs to be overridden per instance
An Apache CustomLog inside a vhost REPLACES the one inherited from the server config. default-ssl.conf declared only ssl_request_log, so conf-enabled/other-vhosts-access-log.conf never applied to :443 -- and since the ALB speaks only to :443, the :80 vhost's access.log stayed empty too. The result was no HTTP access logging anywhere for a dedicated-hosting customer. ssl_request_log was the only record of a request, and it is a poor one: its %h is the load balancer's private address rather than the caller, and its format carries no status code, referer, or user agent. Nothing shipped it off the container either -- it is a real file on the ephemeral /var/log volume, so it died with the task. Found while verifying demo's migration: a probe request could not be located in CloudWatch at all. The log-tailer sidecar appears to cover this (it tails /var/log/apache2/access.log) but cannot -- that path is a symlink to /dev/stdout in the php:apache base image, and /dev/stdout is a write end, so the tail reads nothing. The sidecar entry looked like coverage while providing none. Adds a combined-format CustomLog to access.log, which reaches the container's stdout and therefore the awslogs driver. Client identity comes from X-Forwarded-For, since %h is the load balancer. The whole header is logged deliberately: the ALB appends the true peer as the last element, so only the last one is trustworthy, and keeping the chain visible beats hiding a forged prefix behind a single value. ssl_request_log is kept for the TLS protocol/cipher detail the combined format does not carry. Verified on a built image: `apache2ctl -t` reports Syntax OK, and requests through the vhost emit, on stdout, 203.0.113.9 ... "GET /?abuse-probe HTTP/1.1" 200 4031 "-" "curl/8.7.1" 198.51.100.4 ... "GET /nonexistent-page HTTP/1.1" 404 298 "-" "curl/8.7.1" including the 404 the previous configuration could not record.
Restore HTTP access logging on the minimal image's :443 vhost
The image build fetched the app bundle with a bare `curl -sL .../public/bundles/simplerisk-<version>.tgz | tar xz`. Two problems. 1. public/bundles/ is the GA path, written only by propagate_release_bundle at the testing -> master cut. But bump_downstream_versions opens the docker update-<version> PR at the TESTING cut, from base master -- so the version it pins has no prod bundle yet and container-validation fails on every release. Observed on update-20260709-001, update-20260811-001 and update-20260820-001. 2. Without --fail, curl streams the S3 error document into tar, so the failure surfaces as a tar exit code rather than an HTTP status. The real cause is invisible in the log, which is how this got misread as expected behaviour. The fix already exists on the testing branch and is ported here. PR #146 attempted this in July and was correctly closed: it added the ARG, the COPY and the generator changes but never added the script itself, so it would have failed at COPY. common/download_and_verify_bundle.sh (identical in both build contexts, taken verbatim from testing) downloads the prod bundle, resolves its published sha256 (md5 fallback) from the prod updates feed, and verifies before extracting. Fail-closed by default: a missing bundle, a missing feed hash or a mismatch aborts the build, so a swapped S3 object cannot be baked into a published image. PREGA_BUNDLE_FALLBACK=true -- set ONLY by container-validation -- allows a pre-GA build to fall back to bundles-test without verification, warning loudly, because a release has no published hash before GA. Ported surgically rather than copied. The testing branch's versions of these files also carry a PHP default bump (8.4 -> 8.5), a new source_mode generator parameter, an added php85 validation job and a trigger change -- all unrelated, none included here. Verified after patching: php_version is still 8.4, there are no source_mode references, and the container-validation diff is exactly four build_args lines. Patched the GENERATORS and re-ran them rather than editing the Dockerfiles: the Dockerfiles are generated, and make update_version regenerates them on every version bump, so a hand-edit would be silently overwritten. Also fixed the SQL fetch in the full-stack image, which had the same missing --fail: `curl -sL ... > /simplerisk.sql` writes a 404 body to disk, so the image would ship an HTML error page as its database schema. Verified by running the script in the real alpine/curl:8.12.1 downloader image across all three paths: fail-closed on a pre-GA version without the flag (exit 1), pre-GA fallback to bundles-test with the flag (exit 0, warned, extracted), and the verified-prod path against a GA version that exists. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Verified bundle download with a pre-GA fallback on master
SimpleRisk 20260820-001 Release
master still ran the pre-redesign promote: SSM-only, auto-fired on a master push, and its own header pointed at push-to-dockerhub to build the image. That is what let today's GA write /image-tag/latest = 20260820-001 while the image build for that version had already failed -- production was promoted onto a tag that the workflow believed did not exist. Bring master onto the build-once-promote model: - manual workflow_dispatch only, so GA is a deliberate gate rather than a side effect of a branch merge; - retag DockerHub :latest to the existing RC digest via buildx imagetools create, for simplerisk-minimal (<V>-php85) and simplerisk (<V>-noble). No rebuild, so the bytes validated in testing are the bytes that ship; - a currency guard per image: refuse to promote a version whose digest is not the one :testing currently points at, so a stale committed Dockerfile version cannot push an old-but-existing release to prod; - mirror the promoted digests into GHCR, cosign-signed, so ghcr <V> and dockerhub <V> are finally the same bytes rather than two builds sharing a name. skip_full_image covers the transition: releases cut before the full-stack RC build landed have no simplerisk/simplerisk RC digest to promote. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
These are the workflows that failed the 20260820-001 GA. They rebuild from the prod S3 bundle on a master push, but the release PR merges ~17 minutes before the bundle propagation uploads it, so download_and_verify_bundle.sh fail-closed on a bare 403 (S3 returns 403, not 404, for a missing object under an anonymous-list-denied bucket -- it reads as a permissions error). Rebuilding at GA is also what the promote model exists to remove: it shipped bytes to production that were never the bytes validated in testing, and gave GHCR a separate build under the same version tag. Drop the `push: master` trigger and the simplerisk-minimal jobs from both. promote-latest.yml now owns GA for both registries. The jammy/noble jobs stay dispatchable as a transitional escape hatch for releases with no full-stack RC digest; both headers say when they can be deleted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The CI/CD section still said pushes publish to Docker Hub and GHCR, which is now wrong in both halves: nothing publishes on a master push, and GHCR is a mirror of the promoted digest rather than its own build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cut GA over to promoting the RC digest instead of rebuilding
…onments
Prep so the repo-level default GITHUB_TOKEN can be set to read-only.
- Every workflow now declares an explicit `permissions:` block.
create_new_tag.yml is the only one that needs contents: write (it tags
and pushes tags); the rest are contents: read.
- push-to-gh-pkgs.yml grants packages + id-token write at the caller
level: a reusable workflow can only downgrade the caller's token, never
escalate, so a bare contents:read caller would break the callee's ghcr
push and cosign sign.
- The jobs that use the Docker Hub credentials now declare an
environment, so those credentials become environment-scoped with a
deployment-branch policy rather than repo-wide:
publish-testing.yml -> environment: testing (branch: testing)
promote-latest.yml -> environment: release (branch: master)
push-to-dockerhub_rw.yml -> environment: release (branch: master)
This matches how the AWS OIDC roles are already scoped by ref.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d-environments ci: declare per-workflow permissions and move registry creds to environments
Their stated deletion condition is met: the first post-cutover RC published simplerisk/simplerisk 20260820-001-jammy/-noble, and GA promoted both images from those digests (run 32507524464). There is no longer a release path that needs a rebuild, so the escape hatch has nothing left to catch. Both were already dispatch-only and unreferenced outside their own files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
push-to-dockerhub_rw.yml and push-to-gh-pkgs_rw.yml had exactly two callers between them -- the entry points removed in the previous commit. Nothing else in the repo references either file, so they are dead code rather than reusable building blocks. Split from the previous commit so it can be dropped independently if you would rather keep the generic build-and-push blocks around. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…on pin Deleting the push workflows would have broken `make update_version`, not just left dead code. update_workflows.sh loops over `.github/workflows/push*`; with no matches bash leaves the literal glob, sed fails on the nonexistent path, and `set -e` exits 1 -- taking down step 1 of the release bump. Verified: the loop exits 1 against the post-deletion tree. The script only ever patched `version:` pins in those four workflows, and despite its name update_stack_and_workflows.sh touches only stack.yml, so nothing else needs it. The version now lives solely in the two generated Dockerfiles (`ENV version=`), which is where promote-latest.yml and create_new_tag.yml already read it. Removed from the Makefile and from gorin.toml -- the latter generates the Makefile, so leaving it would resurrect the call on the next `make generate_makefile`. CLAUDE.md's release-step list updated to match. Verified: `make update_version VERSION=20260820-001` succeeds and is idempotent (no Dockerfile churn). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-workflows Delete the legacy rebuild workflows and their vestigial version-bump script
The generator layer had drifted: testing gained the (version, source_mode) split in July, master never did. That is what made generator_checks a testing-only job and left master unable to produce a context-sourced image with a real ENV version. Takes testing's four generator files verbatim -- both generate_dockerfile.sh and both test_generate_dockerfile.sh -- so the layer is now byte-identical across branches rather than approximately similar. The minimal generator's default php moves 8.4 -> 8.5 as part of that, because the decoupling and the default bump landed in the same upstream commit and the minimal harness asserts 8.5. This corrects stale config rather than changing what ships: every released image is the testing-built php85 one, so master's committed 8.4 default only ever affected container-validation and local `docker build`. Committed Dockerfiles regenerated via `make update_version VERSION=20260820-001`. The only substantive change is that php default; the rest is comment punctuation. The -fsSL guard on the schema fetch survives the regeneration (asserted by the full-stack harness). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-key skew - generator_checks runs both harnesses on PRs and fails if either leaves a committed Dockerfile modified. The generators are load-bearing for the release path but nothing pinned them on master. - Adds the php 8.5 variant, which is the minimal image's default and the one GA actually promotes, yet was unvalidated here. - Fixes a name/key skew that made the matrix misleading: job simplerisk-minimal-php84 built php 8.3 and was labelled "PHP 8.3", and simplerisk-minimal-php85 built 8.4. Keys, labels and build args now agree. CLAUDE.md's CI/CD bullet still claimed jammy/noble/php81/php83; corrected along with the stale php_version build-arg list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Converge the Dockerfile generators with testing and pin them in CI
First-ever testing -> master merge. The base was still 81cb9e2 (PR #144, July 2026): every shared change since had landed as a separate cherry-pick on each branch, so git saw both sides as independently modified and the branches could never be compared cheaply. This resets that. Nine conflicts, resolved deliberately rather than by picking a side: Kept master's (testing carries the pre-cutover release path): - push-to-dockerhub.yml stays DELETED. testing's copy still has the `push: master` trigger that rebuilt from the prod bundle and broke the 20260820-001 GA. The other three push workflows and update_workflows.sh auto-resolved as deletes. - promote-latest.yml -- master's build-once GA promote (both images, GHCR mirror, currency guard) over testing's old minimal-only copy. - Both Dockerfiles. testing's carry ENV version=20260519-001; taking them would have reverted the GA version. Regenerated at 20260820-001 instead of hand-merging, since they are generated artifacts. - .grype.yaml -- comment-only difference; master's wording is version-agnostic and stays correct now the minimal default is php 8.5. Ignore lists verified identical. Took testing's (strict supersets, verified: zero master-only lines): - publish-testing.yml -- the live RC publisher with the resolve/publish-full jobs. master's copy was the old dormant one. Re-applied `environment: testing` from #161, which testing predates, and extended it to publish-full -- that job uses the same Docker Hub credentials. - entrypoint.sh + README.md -- the DB_UPGRADE headless schema-upgrade feature, whose db-upgrade.php auto-merged in. CLAUDE.md resolved as a union: testing's DB_UPGRADE row, master's promote-model CI/CD bullets and php-default note. Verified on the result: no workflow publishes on a master push (create_new_tag is the only master-push trigger, and it only tags); both generator harnesses pass and leave the committed Dockerfiles clean; actionlint clean; shellcheck clean at CI severity; make update_version idempotent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Makes
testingandmastergenuinely identical, so the merge base reset by #164 stays cheap instead of drifting apart again immediately.Same commit as #164 (
2c55e9a), pointed attestinginstead ofmaster.origin/testingis already an ancestor of it, so this merges with zero conflicts — no second round of conflict resolution, and no chance of resolving something differently here than in #164.What
testinggainsDeletions — the legacy rebuild path (−371 lines):
push-to-dockerhub.yml,push-to-gh-pkgs.yml, both*_rw.yml, andupdate_workflows.sh(plus itsMakefile/gorin.tomlcall sites). These are inert ontestingtoday —promote-latestis dispatch-only and the push workflows need amasterpush that cannot happen fromtesting— but leaving them means every future sync re-diffs them.Updates:
promote-latest.yml→ the build-once GA promote (both images, GHCR mirror, currency guard,environment: release).testing's copy was the old minimal-only version.container-validation.yml→generator_checks+ the php 8.5 job.create_new_tag.yml,shellcheck.yml→ thepermissions:blocks from ci: declare per-workflow permissions and move registry creds to environments #161.CLAUDE.md→ the promote-model documentation.20260820-001(were20260519-001). These are only consumed bycontainer-validationand local builds;publish-testing.ymlgenerates its own at build time, so this changes nothing about what the RC publishes.What this does NOT change
publish-testing.yml's behaviour is untouched — the only delta is the twoenvironment: testinglines, which #164 added to the merged copy and which mirror what #161 already validated. The RC publish flow, its tags, and its SSM promotion are byte-for-byte what is running today.Ordering
Important
Merge #164 first, then this. Either order technically works — both PRs merge the same commit, so the merge base ends up at
2c55e9aregardless — but #164 first keeps the history readable.Unlike #164, this PR is safe to squash (there is no merge base to preserve here); a merge commit is still preferable for consistency.
delete_branch_on_mergeisfalseon this repo, so #164 merging does not close this PR.Release Notes
None — internal change.
Manual Validation Steps
git fetch origin master testing git diff --stat origin/master origin/testing # expect: no outputgit merge-base origin/master origin/testing→2c55e9a, not81cb9e2.testing→publish-testing.ymlshould run exactly as it does today (resolve → publish + publish-full).testingcan publish on amasterpush — the four push workflows should be gone.Type of Change
Testing Results
Everything asserted for #164 holds here, because it is the same tree:
origin/testingis an ancestor of the head commit (verified withgit merge-base --is-ancestor).actionlintclean on all five remaining workflows;shellcheck -S errorclean on all tracked scripts.make update_version VERSION=20260820-001→ exit 0, idempotent.publish-testing.ymlonly runs on atestingpush, so the first post-merge RC cut is the real test. Its content here is what is already live ontestingplus the twoenvironment: testinglines.CIA Impact Check
MAY negatively impact Confidentiality, Integrity and/or Availability.
Justification for the change
testingstill holding stale copies ofpromote-latest.ymland four deleted workflows, the branches would begin re-diverging on the very next comparison — and the next sync would again require hand-resolving the release-critical files, which is where a silent revert can creep in.testingcurrently sees apromote-latest.ymlthat describes the removed rebuild model.Potential impact on Confidentiality, Integrity and/or Availability
Integrity. The RC publish path is what protects the release, and it is unchanged apart from credential scoping. The committed Dockerfile version bump on
testing(20260519-001→20260820-001) does not affect published images, sincepublish-testing.ymlgenerates its Dockerfile at build time from the resolved bundle version.Availability (deploy-window). The first RC cut after this merge is the first to run
publish-testing.ymlfrom atestingbranch carrying these deletions. The deleted workflows are not referenced by it — verified by grep — but the RC cut is the point to watch.Confidentiality. Net positive:
environment: testingnow scopes the Docker Hub credentials on both credential-using jobs, wheretesting's current copy has no environment scoping at all.Additional Implementation Steps
None. After this,
masterandtestinghave identical trees and future syncs are ordinary small merges.