Skip to content

Sync master into testing so the branches are identical - #165

Merged
jsokol merged 26 commits into
testingfrom
SYNC-master-into-testing
Aug 21, 2026
Merged

Sync master into testing so the branches are identical#165
jsokol merged 26 commits into
testingfrom
SYNC-master-into-testing

Conversation

@jsokol

@jsokol jsokol commented Aug 21, 2026

Copy link
Copy Markdown
Member

Description

Makes testing and master genuinely identical, so the merge base reset by #164 stays cheap instead of drifting apart again immediately.

Same commit as #164 (2c55e9a), pointed at testing instead of master. origin/testing is 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 testing gains

Deletions — the legacy rebuild path (−371 lines):
push-to-dockerhub.yml, push-to-gh-pkgs.yml, both *_rw.yml, and update_workflows.sh (plus its Makefile / gorin.toml call sites). These are inert on testing today — promote-latest is dispatch-only and the push workflows need a master push that cannot happen from testing — 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.ymlgenerator_checks + the php 8.5 job.
  • create_new_tag.yml, shellcheck.yml → the permissions: blocks from ci: declare per-workflow permissions and move registry creds to environments #161.
  • CLAUDE.md → the promote-model documentation.
  • Committed Dockerfiles → 20260820-001 (were 20260519-001). These are only consumed by container-validation and local builds; publish-testing.yml generates 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 two environment: testing lines, 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 2c55e9a regardless — 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_merge is false on this repo, so #164 merging does not close this PR.

Release Notes

None — internal change.

Manual Validation Steps

  1. After both merge, confirm the branches carry identical content:
    git fetch origin master testing
    git diff --stat origin/master origin/testing    # expect: no output
  2. git merge-base origin/master origin/testing2c55e9a, not 81cb9e2.
  3. Push a no-op commit to testingpublish-testing.yml should run exactly as it does today (resolve → publish + publish-full).
  4. Confirm no workflow on testing can publish on a master push — the four push workflows should be gone.

Type of Change

  • Build / CI configuration
  • Repository maintenance (branch convergence)

Testing Results

Everything asserted for #164 holds here, because it is the same tree:

  • Zero merge conflicts — origin/testing is an ancestor of the head commit (verified with git merge-base --is-ancestor).
  • Both generator harnesses pass and leave the committed Dockerfiles clean.
  • actionlint clean on all five remaining workflows; shellcheck -S error clean on all tracked scripts.
  • make update_version VERSION=20260820-001 → exit 0, idempotent.
  • Not exercised: an actual RC publish. publish-testing.yml only runs on a testing push, so the first post-merge RC cut is the real test. Its content here is what is already live on testing plus the two environment: testing lines.

CIA Impact Check

MAY negatively impact Confidentiality, Integrity and/or Availability.

Justification for the change

  • Merge testing into master to reset the merge base #164 reset the merge base, but with testing still holding stale copies of promote-latest.yml and 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.
  • A reader or agent on testing currently sees a promote-latest.yml that 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-00120260820-001) does not affect published images, since publish-testing.yml generates 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.yml from a testing branch 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: testing now scopes the Docker Hub credentials on both credential-using jobs, where testing's current copy has no environment scoping at all.

Additional Implementation Steps

None. After this, master and testing have identical trees and future syncs are ordinary small merges.

jsokol and others added 26 commits August 6, 2026 23:43
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
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>
@jsokol
jsokol marked this pull request as ready for review August 21, 2026 21:00
@jsokol
jsokol merged commit dae32e9 into testing Aug 21, 2026
14 checks passed
@jsokol
jsokol deleted the SYNC-master-into-testing branch August 21, 2026 21:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants