fix(hedgedoc): Seeded admin account + R2 snapshot/restore persistence#619
Merged
Conversation
Contributor
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Contributor
There was a problem hiding this comment.
Pull request overview
This PR makes the HedgeDoc stack usable by (1) introducing a seeded in-app admin account (with self-signup + anonymous edits disabled) and (2) adding full R2 snapshot/restore coverage for HedgeDoc’s Postgres DB and uploads directory, so notes and attachments survive teardown/spin-up cycles.
Changes:
- Add a new OpenTofu-generated HedgeDoc admin password, plumb it through config/Infisical, and add a
services-configurehook to idempotently create/reset the admin viamanage_users. - Add HedgeDoc to
s3_restore.standard_targets(Postgres dump + uploads rsync) and switch uploads from a named volume to a/mnt/nexus-data/...bind mount with a guardedchownprep step. - Update HedgeDoc compose + docs + unit tests/snapshots to reflect the new auth and persistence model.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tofu/stack/main.tf | Adds random_password.hedgedoc_admin for the seeded HedgeDoc admin credential. |
| tofu/stack/outputs.tf | Exposes hedgedoc_admin_password via the secrets output for downstream sync. |
| src/nexus_deploy/config.py | Extends the secrets schema to include hedgedoc_admin_password. |
| src/nexus_deploy/infisical.py | Pushes HedgeDoc admin username/password into the /hedgedoc Infisical folder. |
| src/nexus_deploy/service_env.py | Adds HedgeDoc admin email/password to rendered env and extends fail-fast guards. |
| src/nexus_deploy/services.py | Implements render_hedgedoc_hook and registers it in the hook registry. |
| src/nexus_deploy/compose_runner.py | Adds hedgedoc_uploads_prep to prep/chown the uploads bind mount directory. |
| src/nexus_deploy/s3_restore.py | Adds HedgeDoc Postgres dump + uploads rsync targets to standard persistence. |
| stacks/hedgedoc/docker-compose.yml | Disables anonymous/signup, sets private-by-default notes, converts uploads to bind mount. |
| docs/stacks/hedgedoc.md | Updates usage/auth/persistence docs for the seeded-admin + R2 persistence behavior. |
| tests/unit/test_config.py | Updates schema field-count assertion. |
| tests/unit/test_service_env.py | Adds coverage for new HedgeDoc admin guardrails and env rendering. |
| tests/unit/test_services.py | Adds coverage for the rendered HedgeDoc admin-seed hook script behavior. |
| tests/unit/test_compose_runner.py | Adds coverage for the HedgeDoc uploads prep block rendering/defaulting. |
| tests/unit/test_s3_restore.py | Adds assertions for the new HedgeDoc persistence targets. |
| tests/fixtures/secrets_full.json | Adds fixture value for hedgedoc_admin_password. |
| tests/unit/snapshots/test_config.ambr | Updates snapshot to include the new config field. |
| tests/unit/snapshots/test_infisical.ambr | Updates snapshot to include the new HedgeDoc Infisical secrets. |
stefanko-ch
added a commit
that referenced
this pull request
May 24, 2026
Copilot flagged that _render_hedgedoc rendered HEDGEDOC_ADMIN_EMAIL / HEDGEDOC_ADMIN_PASSWORD env vars into the .env file, but the Infisical-stored keys are HEDGEDOC_USERNAME / HEDGEDOC_PASSWORD — an operator grepping for the latter in deploy-error output would come up empty and stay stuck. Root cause: those env vars were dead code. The docker-compose.yml doesn't reference HEDGEDOC_ADMIN_* and the services-configure hook reads the values straight from NexusConfig / BootstrapEnv (Python-side), not from the rendered .env. They were a speculative "future container-side automation" that never materialised, and added confusion as a naming-mismatch surface. - Drop HEDGEDOC_ADMIN_EMAIL / HEDGEDOC_ADMIN_PASSWORD from the rendered .env. Keep the four-way fail-fast guard (it checks the underlying config / env values, no longer the rendered .env entries). - Rewrite the guard error message to use the Infisical key names (HEDGEDOC_PASSWORD / HEDGEDOC_USERNAME) so operators grepping the failure find the matching secret in Infisical directly. - Add explicit "(Infisical /hedgedoc)" annotation per missing key to remove the translation step entirely. - Tests updated: assert the admin keys are NOT in rendered env vars, error-message regex switched to the Infisical names. Snapshot regen for test_config.py covers the pre-existing trailing-newline issue that was masked by the previous full-suite runs.
stefanko-ch
added a commit
that referenced
this pull request
May 27, 2026
HedgeDoc was added to `standard_targets()` with both a PostgresDumpTarget (safe) AND an `hedgedoc-uploads` RsyncTarget against `/mnt/nexus-data/hedgedoc/uploads`, but the corresponding compose-file entry was missed in `_STANDARD_STOP_COMPOSE_FILES`. Snapshot rsync therefore ran against the live container, producing torn writes / half-uploaded attachments — same class of bug the Metabase stop entry was added to fix in issue #528. Added the missing stop entry. Also added `test_standard_stop_compose_files_matches_rsync_targets` as a regression guard: derives the expected stop list from `standard_targets()` rsync targets and asserts symmetry in both directions (no RsyncTarget without a stop entry, no dead stop entry without a matching consumer). Future stack additions get a hard fail at test time instead of silent production corruption. Expanded the inline rationale block on `_STANDARD_STOP_COMPOSE_FILES` to make the "rsync target ⇒ stop entry" invariant explicit and to reference the new test by name. Addresses Copilot review comment 3309024957 on PR #619.
Two coupled fixes, both required for HedgeDoc to be a useful stack:
1. Admin authentication
Previously HedgeDoc shipped with CMD_ALLOW_EMAIL_REGISTER=false
and CMD_ALLOW_ANONYMOUS=true — relying on CF Access at the edge
as the only auth gate and anonymous editing as the in-app
workflow. That misses HedgeDoc's core value (note ownership,
"My Notes" list, private notes) and surprised the operator with
a sign-in modal that couldn't accept any credentials.
- random_password.hedgedoc_admin in Tofu, surfaced via outputs.tf
and pushed to Infisical /hedgedoc as HEDGEDOC_USERNAME (admin
email) + HEDGEDOC_PASSWORD.
- service_env._render_hedgedoc renders HEDGEDOC_ADMIN_EMAIL +
HEDGEDOC_ADMIN_PASSWORD env vars and fail-fast guards all four
required secrets together.
- render_hedgedoc_hook (services.py) seeds the admin via
`node /hedgedoc/bin/manage_users --add EMAIL --pass PASS`
during services-configure, with --reset fallback for
idempotency on re-runs (matches the Superset hook's
create→reset pattern). Password piped via stdin so the
cleartext never appears in host-side `ps` argv.
- compose flips CMD_ALLOW_ANONYMOUS / ANONYMOUS_EDITS / FREEURL
to false and CMD_DEFAULT_PERMISSION to private — newly-created
notes are admin-only until explicitly shared via the per-note
permission selector.
2. Persistence
HedgeDoc was NOT in s3_restore.standard_targets, so every
teardown discarded all notes + uploads. Independently broken
regardless of auth mode.
- PostgresDumpTarget(container="hedgedoc-db", database="hedgedoc",
user="nexus-hedgedoc") added to standard_targets.
- Convert hedgedoc-uploads from a Docker named volume to a
bind-mount on /mnt/nexus-data/hedgedoc/uploads so rsync can
capture it.
- New compose_runner flag hedgedoc_uploads_prep does the
idempotent mkdir + owner-guarded chown to 10000:10000 (the
in-container hedgedoc UID, verified via `docker exec`) before
compose-up. Mirrors the existing dify_storage_prep /
metabase_storage_prep pattern.
- Matching RsyncTarget added to standard_targets so the existing
teardown→R2 / spin-up→R2 cycle preserves attachments.
Tests cover all four guards in service_env, both compose_runner flag
behaviours, hook stdin-pipe + idempotency, and the standard_targets
additions (Postgres user/db pin + rsync subpath layout). Snapshots
regenerated for test_config.py and test_infisical.py. secrets_full
fixture gains hedgedoc_admin_password.
Closes #618
Copilot flagged that _render_hedgedoc rendered HEDGEDOC_ADMIN_EMAIL / HEDGEDOC_ADMIN_PASSWORD env vars into the .env file, but the Infisical-stored keys are HEDGEDOC_USERNAME / HEDGEDOC_PASSWORD — an operator grepping for the latter in deploy-error output would come up empty and stay stuck. Root cause: those env vars were dead code. The docker-compose.yml doesn't reference HEDGEDOC_ADMIN_* and the services-configure hook reads the values straight from NexusConfig / BootstrapEnv (Python-side), not from the rendered .env. They were a speculative "future container-side automation" that never materialised, and added confusion as a naming-mismatch surface. - Drop HEDGEDOC_ADMIN_EMAIL / HEDGEDOC_ADMIN_PASSWORD from the rendered .env. Keep the four-way fail-fast guard (it checks the underlying config / env values, no longer the rendered .env entries). - Rewrite the guard error message to use the Infisical key names (HEDGEDOC_PASSWORD / HEDGEDOC_USERNAME) so operators grepping the failure find the matching secret in Infisical directly. - Add explicit "(Infisical /hedgedoc)" annotation per missing key to remove the translation step entirely. - Tests updated: assert the admin keys are NOT in rendered env vars, error-message regex switched to the Infisical names. Snapshot regen for test_config.py covers the pre-existing trailing-newline issue that was masked by the previous full-suite runs.
HedgeDoc was added to `standard_targets()` with both a PostgresDumpTarget (safe) AND an `hedgedoc-uploads` RsyncTarget against `/mnt/nexus-data/hedgedoc/uploads`, but the corresponding compose-file entry was missed in `_STANDARD_STOP_COMPOSE_FILES`. Snapshot rsync therefore ran against the live container, producing torn writes / half-uploaded attachments — same class of bug the Metabase stop entry was added to fix in issue #528. Added the missing stop entry. Also added `test_standard_stop_compose_files_matches_rsync_targets` as a regression guard: derives the expected stop list from `standard_targets()` rsync targets and asserts symmetry in both directions (no RsyncTarget without a stop entry, no dead stop entry without a matching consumer). Future stack additions get a hard fail at test time instead of silent production corruption. Expanded the inline rationale block on `_STANDARD_STOP_COMPOSE_FILES` to make the "rsync target ⇒ stop entry" invariant explicit and to reference the new test by name. Addresses Copilot review comment 3309024957 on PR #619.
59aefcc to
91e2e59
Compare
stefanko-ch
pushed a commit
that referenced
this pull request
May 28, 2026
🤖 I have created a release *beep* *boop* --- ## [0.68.0](v0.67.0...v0.68.0) (2026-05-28) ### 🚀 Features * **stacks:** Add Evidence — SQL+markdown BI for analytics engineers ([#616](#616)) ([ac9ef64](ac9ef64)) ### 🐛 Bug Fixes * **hedgedoc:** Seeded admin account + R2 snapshot/restore persistence ([#619](#619)) ([a04d529](a04d529)) * **pipeline:** Pre-create ollama-internal network when LiteLLM enabled ([#617](#617)) ([3ea36be](3ea36be)) * **stacks:** Raise Metabase JVM heap to prevent OOM ([#620](#620)) ([70bb786](70bb786)) * **stacks:** Remove hardcoded credential fallbacks (audit C1-C3) ([#623](#623)) ([3002629](3002629)) * **stacks:** Resolve host-port 8888 collision (adminer ⇔ seaweedfs) ([#624](#624)) ([6831d2c](6831d2c)) * **tofu:** Validate cloudflare_account_id, cloudflare_zone_id, domain shapes ([#622](#622)) ([47f25c6](47f25c6)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.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.
Summary
Two coupled fixes for HedgeDoc, both required for the stack to be useful:
s3_restore.standard_targets, so every teardown discarded ALL notes + uploads. Independently broken regardless of auth mode.Login without persistence would be nonsense, so both fixes land together.
Closes #618
Admin authentication
random_password.hedgedoc_adminin Tofu →outputs.tfsecrets → Infisical/hedgedocasHEDGEDOC_USERNAME(admin email) +HEDGEDOC_PASSWORD.service_env._render_hedgedocextends the fail-fast guard to require all four (session secret, DB pw, admin pw, admin email).render_hedgedoc_hook(services.py) seeds the admin vianode /hedgedoc/bin/manage_users --add EMAIL --pass PASSduring services-configure. Idempotent: try--addfirst, fall back to--resetif user exists (mirrors the Superset hook's create→reset pattern, handles snapshot-restore + Tofu password rotation).docker exec -iso the cleartext never appears in host-sidepsargv (same as the Superset hook).CMD_ALLOW_ANONYMOUS/ANONYMOUS_EDITS/FREEURLtofalseandCMD_DEFAULT_PERMISSIONtoprivate— newly-created notes are admin-only until explicitly shared via the per-note ⚙ permission selector.Persistence
PostgresDumpTarget(container="hedgedoc-db", database="hedgedoc", user="nexus-hedgedoc")added tostandard_targets.hedgedoc-uploadsconverted from a Docker named volume to a bind-mount on/mnt/nexus-data/hedgedoc/uploadssorsynccan capture it (named volumes can't bersynced without--volumes-fromgymnastics).compose_runner.hedgedoc_uploads_prepflag: idempotentmkdir+ owner-guardedchownto10000:10000(the in-containerhedgedocUID, verified viadocker exec hedgedoc id). Mirrors the existingdify_storage_prep/metabase_storage_preppattern.RsyncTarget(name="hedgedoc-uploads", s3_subpath="hedgedoc/uploads")so the existing teardown→R2 / spin-up→R2 cycle preserves attachments.Why this shape
CMD_ALLOW_EMAIL_REGISTER=trueand re-deploy; CF Access at the edge still gates entry.docker run --volumes-fromto bersynced; bind-mounts on/mnt/nexus-data/align with the Dify / Metabase / Gitea pattern thats3_restorealready handles.--addfails (exit 2 + "already exists") on second run;--resetsucceeds when the user exists. Together: same logical "ensure admin exists with this password" semantics, robust against R2 snapshot restore from a stack with a different password.Test plan
uv run pytest tests/unit/— 1552 passedrender_hedgedoc_hook: stdin pipe, password NOT indocker execargv on any line (HD-CANARY check), email via-e VAR=, both--addand--resetpaths renderedcompose_runner.hedgedoc_uploads_prep: block only when flagged, owner-guarded chown structures3_restore.standard_targets: pg-dump pin (container/db/user), rsync subpath pinpre-commit runclean (ruff format, ruff check, mypy strict, shellcheck)Manual deploy test:
Then:
https://hedgedoc.<domain>→ CF Access OTP → HedgeDoc login form should appear (NO sign-up link).ADMIN_EMAIL, password =infisical secrets get HEDGEDOC_PASSWORD --path=/hedgedoc --plain.teardown.yml→spin-up.yml.Out of scope
Files
18 files modified:
tofu/stack/main.tf,tofu/stack/outputs.tf— random_password + secret outputsrc/nexus_deploy/config.py,infisical.py— schema + Infisical foldersrc/nexus_deploy/service_env.py— extended guard + new env varssrc/nexus_deploy/services.py—render_hedgedoc_hook+ registry entrysrc/nexus_deploy/compose_runner.py—hedgedoc_uploads_prepflagsrc/nexus_deploy/s3_restore.py— pg-dump + rsync targetsstacks/hedgedoc/docker-compose.yml— auth flags, bind-mount, comment updatedocs/stacks/hedgedoc.md— auth model + persistence sections rewrittentests/unit/{test_config,test_compose_runner,test_s3_restore,test_service_env,test_services}.py— new tests + updated fixturetests/fixtures/secrets_full.json—hedgedoc_admin_passwordtests/unit/__snapshots__/{test_config,test_infisical}.ambr— regenerated