Merge testing into master to reset the merge base - #164
Merged
Conversation
…d recipe (supersedes #146)
…ng; drop -testing; promote php85
… checker, raise publish-testing timeout, doc/comment fixes
Build-once RC publish: immutable php83/84/85 + bare + :testing (Plan 1)
…); manual dispatch; php85 SSM
… the RC digest instead)
…C) version; fix step name
GA promote (no rebuild): retag :latest to the RC digest + manual dispatch (Plan 2)
Adds a DB_UPGRADE entrypoint mode (automatic / automatic-only) that runs SimpleRisk's core release-by-release schema upgrade (run_database_upgrade_structured in includes/upgrade.php) headlessly via /db-upgrade.php against an already-installed database, using the app DB user (SIMPLERISK_DB_*) — no privileged user, no HTTP context, no credentials beyond the DB connection. Emits the structured per-release JSON result to the log; automatic-only exits with the upgrade status so a one-shot Job fails on a failed upgrade. The upgrade logic is core-only (run_database_upgrade_structured + the 97 per-release functions live in includes/upgrade.php; upgrade.php requires only core includes/), so the mode needs only the base image + the app DB env. No Dockerfile change: common/ is already COPYed to / (db-upgrade.php -> /). Twin of the DB_SETUP mode. DB_UPGRADE documented in simplerisk-minimal/README.md and CLAUDE.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
simplerisk-minimal: headless DB_UPGRADE mode for release-triggered schema upgrades
The pinned php:${php_version}-apache base ships apache2/curl at its own
build-time versions, and the Dockerfile only `install`s specific packages — so
base packages accumulate upstream-fixed CVEs. Grype (--fail-on critical
--only-fixed) flags criticals on every minimal build: curl
(CVE-2026-11856/10536/8927/8924, fixed 8.21.0) and apache2
(CVE-2026-29167/42535, fixed 2.4.68). Add `apt-get -y upgrade` after
`apt-get update` so the Debian security fixes are pulled; regenerate the
Dockerfile from the generator.
Targets testing (features land here, then promote to master). Full/Ubuntu
image currently passes Grype and is untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Grype's binary classifier reads the PHP interpreter's version string (PHP 8.3.32, embedded in /usr/local/bin/php, libphp.so, and extensions) as a "curl" binary at 8.3.32 and flags curl CVEs (CVE-2026-11856/10536/8927/8924, fixed 8.21.0). The real curl is the Debian package, patched to 8.14.1-2+deb13u4 and correctly not flagged. Scope the ignore to binary-type curl so the deb curl stays scanned. With the apt-get upgrade (apache2 -> 2.4.68) this clears the minimal Grype gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Security review flagged the previous `package: {name: curl, type: binary}` rule
as over-broad — it would silently suppress any future binary-curl finding. Narrow
it to the four specific PHP-version-misidentified CVEs (CVE-2026-11856/10536/8927/
8924) so a genuine curl vulnerability still surfaces.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both gates only triggered on pull_request -> master, so feature PRs to testing (where features land before promotion) were never validated. Add testing to the branch filter so the gates cover the branch features actually target. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Patch base-image CVEs in the minimal image (apt upgrade)
Reconciles the bundle-integrity feature (#141) onto testing. A single common/download_and_verify_bundle.sh downloads the prod bundle, resolves its bundle_sha256 (md5 fallback) from the updates feed, verifies fail-closed, then extracts — so a swapped S3 object aborts the build instead of being baked into a published image. Both image generators COPY + run it. Reconciled with testing's PREGA_BUNDLE_FALLBACK: a RELEASED build is always verified against the prod bundle; the pre-GA CI fallback to bundles-test skips verification (the release has no published hash yet) and warns loudly — never for a released image. The full image keeps its separate release-SQL download. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fication Verify the release bundle hash before extracting (both images)
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>
…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>
…-testing Allow the service endpoint URLs to be overridden per instance (testing)
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.
…ting Restore HTTP access logging on the minimal image's :443 vhost (testing)
simplerisk/generate_dockerfile.sh branched its recipe on the literal string "testing", so a context-sourced build could only ever stamp `ENV version=testing`. That is the one thing standing between the full-stack image and the build-once-promote model: the RC has to carry a real version to be promotable at GA. Add the same (version, source_mode) split simplerisk-minimal already has: `context` COPYs the app from the build context, `download` emits the hash-verifying downloader stage. The default preserves back-compat exactly -- a bare "testing" still selects context, a bare version still selects download. Also restore --fail on the schema fetch (present on master, lost on testing). Without it curl writes the 404 body into /simplerisk.sql and the image ships an HTML error page as its schema. Adds test_generate_dockerfile.sh mirroring the minimal harness: 14 checks over both modes, both back-compat paths, invalid-mode rejection and idempotence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nimal GA promotes digests rather than rebuilding, so every image it promotes has to exist as an RC. simplerisk/simplerisk had no RC build at all -- it was only ever built on master from the prod bundle, which is exactly the rebuild the promote model removes. Add a publish-full job tagging <VERSION>-jammy/-noble, bare <VERSION> (= noble, the default) and :testing, amd64-only to match what the release build published before. No SSM tier: the full-stack image is not part of the managed fleet. Hoist version resolution into a shared resolve job so both publish jobs stamp the same release even if the testing channel rotates mid-run, and have each fetch that exact bundle by name rather than re-listing the channel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The generators are now load-bearing for the release path -- context mode has to stamp a real ENV version, download mode has to keep the hash-verifying downloader stage -- but neither harness was wired into CI. Run both on PRs, and fail if either leaves a committed Dockerfile modified. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…testing Build the full-stack image once at RC time, so GA can promote it
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.
Caution
Merge this with "Create a merge commit". Do NOT squash or rebase.
The entire point is the second parent — squashing produces the same files but leaves the merge base at
81cb9e2, so the nexttesting → mastercomparison conflicts exactly as badly as this one did. The repo permits squash merges, hence the warning.Description
The first-ever
testing → mastermerge. The merge base was still81cb9e2(PR #144, July 2026): every shared change in the four months 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 diffed or merged cheaply.After this, the merge base is
af02e01(currenttestingtip), and future syncs are ordinary small merges.Nine conflicts, resolved deliberately rather than by picking a side.
Kept master's —
testingstill carries the pre-cutover release pathpush-to-dockerhub.ymltesting's copy still has thepush: mastertrigger that rebuilt from the prod bundle and broke the 20260820-001 GA. The other three push workflows andupdate_workflows.shauto-resolved as deletes.promote-latest.ymltesting's old minimal-only copy.simplerisk/Dockerfile,simplerisk-minimal/Dockerfiletesting's carryENV version=20260519-001— taking them would have reverted the GA version. Regenerated at20260820-001rather than hand-merged, since they're generated artifacts..grype.yamlTook testing's — strict supersets, verified zero master-only lines
publish-testing.ymlresolve/publish-fulljobs; master's copy was the old dormant one. Re-appliedenvironment: testingfrom #161, whichtestingpredates — and extended it topublish-full, which uses the same Docker Hub credentials.entrypoint.sh,README.mdDB_UPGRADEheadless schema-upgrade feature, whosedb-upgrade.phpauto-merged in.CLAUDE.mdresolved as a union: testing'sDB_UPGRADErow, master's promote-model CI/CD bullets and php-default note.Release Notes
None — internal change. Brings the
DB_UPGRADEheadless schema-upgrade mode to themasterbranch's image sources; it was already live ontesting.Manual Validation Steps
masterpush —create_new_tag.ymlshould be the onlypush: mastertrigger, and it only tags.push-to-dockerhub.yml,push-to-gh-pkgs.yml, both_rw.yml,update_workflows.sh.grep '^ENV version=' simplerisk/Dockerfile simplerisk-minimal/Dockerfile→ both20260820-001.grep -c 'environment: testing' .github/workflows/publish-testing.yml→2.git merge-base origin/master origin/testing→af02e01, not81cb9e2.testingshould behave identically — this PR does not changetesting.Type of Change
Testing Results
generator_checksguard).actionlintclean on all five remaining workflows.shellcheck -S error(CI's gate) clean on all tracked scripts.make update_version VERSION=20260820-001→ exit 0, idempotent.container-validation.ymlauto-merged correctly: gained testing'sbranches: [master, testing]while keeping master'sgenerator_checksand php 8.5 jobs.publish-testing.ymlonly runs on atestingpush, so its merged form onmasteris dormant and untested here. It is byte-identical to what is already running ontestingapart from the twoenvironment: testinglines, which mirror what ci: declare per-workflow permissions and move registry creds to environments #161 already validated on master's copy.CIA Impact Check
MAY negatively impact Confidentiality, Integrity and/or Availability.
Justification for the change
masterandtestinghad drifted on real content (DB_UPGRADE, grype scoping, container-validation triggers) with no mechanism forcing reconciliation.Potential impact on Confidentiality, Integrity and/or Availability
Integrity (the main risk, mitigated). A careless resolution here would have reverted the GA version to
20260519-001, restored the auto-rebuild trigger, or downgradedpromote-latest.yml— each silently. Every one of those was checked explicitly and is listed above with its resolution; the verification steps assert them on the merged tree. No release-path behaviour changes:promote-latest.ymlis byte-identical to what is onmastertoday.Availability (low). The image sources on
mastergain theDB_UPGRADEcode path, but it is inert unlessDB_UPGRADEis set, and it has been running ontestingsince before the last RC.container-validationnow also triggers ontesting-targeted PRs, which only adds coverage.Confidentiality. Net positive —
environment: testingnow scopes the Docker Hub credentials on both credential-using jobs inpublish-testing.yml, wheretesting's copy had no environment scoping at all.Additional Implementation Steps
No schema change, no customer action.
Follow-up worth considering:
testingstill holds stale copies ofpromote-latest.ymland the four deleted push workflows. They are inert there (promote-latestis dispatch-only; the push workflows would need amasterpush, which cannot happen ontesting), but amaster → testingsync would make the branches genuinely identical and keep the next merge trivial.