Skip to content

[codex] use shared immutable releases for deploys - #407

Merged
corrin merged 10 commits into
mainfrom
shared-disk-space-saving
Jun 25, 2026
Merged

[codex] use shared immutable releases for deploys#407
corrin merged 10 commits into
mainfrom
shared-disk-space-saving

Conversation

@corrin

@corrin corrin commented Jun 24, 2026

Copy link
Copy Markdown
Owner

Summary

  • Switch deploys from per-instance git checkouts/builds to shared immutable /opt/docketworks/releases/<sha> directories, with each instance pointing current at the selected release.
  • Add release build helpers that archive the requested SHA, build a per-release venv and frontend/manual assets, remove node_modules, and prune unreferenced releases.
  • Update deploy, instance provisioning, rollback, service templates, nginx/static paths, build-id lookup, frontend env handling, and docs to match the shared-release layout.
  • Extend deployment template tests around release preparation, runtime units, rollback helpers, fixture secrecy, and build ID behavior.

Validation

  • ./.venv/bin/python -m pytest apps/workflow/tests/test_xero_instance_templates.py — 14 passed.
  • Pre-commit during commit passed, including OpenAPI validation, typed router generation, and frontend lint-staged.
  • Pre-push passed with mypy baseline new: 0, frontend unit tests 42 passed / 263 tests, frontend type-check, production build, typed-router build check, and workflow formatting.

Notes

  • The production frontend build still emits existing vendor warnings for rrweb-player browser externalization, pdf-vue3 direct eval, and large chunks.
  • GitHub reported existing default-branch Dependabot vulnerabilities after push; this PR does not address dependency security updates.

Summary by CodeRabbit

  • New Features
    • Deployments now build once into shared immutable releases; instances switch to an active current release for code, dependencies, and static assets.
    • Frontend config now computes UAT links at runtime (SSR-safe) and falls back to a default app name when unset.
  • Bug Fixes
    • Improved build ID resolution/validation using environment or .release-sha, with early failure on malformed values.
    • Backup/rollback and runtime services now operate against release-based current; deployment can clean up unreferenced releases and verify typed-router drift.
  • Documentation
    • Updated server setup, upgrade, and backup/rollback runbooks for the release-based workflow.
  • Tests
    • Added build ID tests and expanded deployment/release reachability + drift coverage.
  • Chores
    • Added ShellCheck pre-commit and refreshed ShellCheck configuration.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces shared immutable releases under /opt/docketworks/releases/<sha>/, updates deploy and instance flows to use current, refreshes runtime templates and backup/rollback scripts, and changes build ID resolution in Django, Vite, and frontend config.

Changes

Immutable Shared Release Model

Layer / File(s) Summary
Shared paths and server setup
scripts/server/common.sh, scripts/server/server-setup.sh, scripts/server/README.md
Adds release-path helpers, provisions release/cache directories, removes shared dependency installation, and updates server setup/layout documentation for the release model.
Release helpers and build flow
scripts/server/release-utils.sh, apps/workflow/tests/test_xero_instance_templates.py
Adds release resolution, immutable build, cleanup, and compaction helpers, with tests covering release build and typed-router behavior.
Release-based deploy orchestration
scripts/server/deploy.sh, scripts/server/templates/*, docs/updating.md, scripts/server/README.md
Deploy resolves a target SHA, builds/releases it, switches instances to current, re-renders runtime services and Nginx, and documents the release-based update flow.
Instance provisioning and fixtures
scripts/server/instance.sh, docs/server_setup.md, apps/workflow/tests/test_xero_instance_templates.py
Instance creation now uses shared releases, instance-local fixtures, release-aware profiles, and updated Xero/bootstrap checks, with matching doc and test updates.
Build ID and runtime config
docketworks/settings.py, frontend/vite.config.ts, frontend/src/config/adminPages.ts, frontend/src/config/app.ts, frontend/env.d.ts, frontend/.env.example, scripts/server/dw-run.sh, apps/workflow/tests/test_build_id.py
Build IDs now come from release metadata or environment, frontend config resolves UAT URLs at runtime, and dw-run.sh runs commands from the current release.
Backup and rollback by release SHA
scripts/predeploy_backup.sh, scripts/predeploy_rollback.sh, scripts/README.md
Backup filenames now use the current release SHA and rollback restores data plus release state through a temporary database swap.
Validation, linting, and authoring support
.github/copilot-instructions.md, .pre-commit-config.yaml, .shellcheckrc, frontend/scripts/*, frontend/tests/scripts/*, scripts/*, scripts/setup_database.sh
ShellCheck, quoting, validation scripts, and authoring guidance are updated to match the new shell and release workflows.

Possibly related PRs

  • corrin/docketworks#406: Directly related — both PRs modify typed-router drift detection and deploy-time release handling, with this PR moving the check into the release build flow and removing the prior deploy-time restore strings.

Poem

🐇 I hopped through releases, neat and bright,
With current guiding every byte.
One SHA builds, then all can share,
No old checkout paths left there.
I twitch my nose at cleanup night—
Fresh builds, fresh hops, and everything right.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the change and validation, but it omits the required Jira section and the checklist template. Add the required Jira key section and the checklist block from the template, then keep the summary and validation details.
Docstring Coverage ⚠️ Warning Docstring coverage is 16.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: switching deploys to shared immutable releases.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch shared-disk-space-saving

Comment @coderabbitai help to get the list of available commands.

@corrin
corrin marked this pull request as ready for review June 24, 2026 09:36
@corrin
corrin requested a review from Copilot June 24, 2026 09:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the deployment model from per-instance mutable checkouts/builds to shared, immutable release directories under /opt/docketworks/releases/<sha>, with each instance switching a current symlink to select the running release. This reduces cross-instance coupling (deps/build artifacts) and enables consistent roll-forward/roll-back behavior keyed by release SHA.

Changes:

  • Introduces release-utils.sh to build, switch, and garbage-collect immutable releases (per-release venv + frontend/manual build).
  • Updates deploy/provision/rollback scripts plus systemd/nginx templates to run from instances/<name>/current instead of per-instance checkouts/shared venv.
  • Updates frontend + backend build-id resolution and frontend env expectations to work in the shared-release layout, with docs/tests extended accordingly.

Reviewed changes

Copilot reviewed 24 out of 25 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
scripts/server/templates/nginx-instance.conf.template Serve frontend + manual assets from instances/<name>/current/... paths.
scripts/server/templates/gunicorn-instance.service.template Run gunicorn from the release-local venv in current, set working dir to current.
scripts/server/templates/frontend-env-instance.template Removes per-instance frontend .env template (no longer rendered).
scripts/server/templates/celery-worker-instance.service.template Run celery worker from current release venv/working dir.
scripts/server/templates/celery-beat-instance.service.template Run celery beat from current release venv/working dir.
scripts/server/templates/backup-db-instance.service.template Run backup script from current release path.
scripts/server/server-setup.sh Stops building shared venv/node_modules; prepares releases and npm cache dirs.
scripts/server/release-utils.sh New helpers to build releases, switch current, and prune unreferenced releases.
scripts/server/README.md Documents shared-release architecture and deploy/cleanup behavior.
scripts/server/instance.sh Provision instances by creating current symlink and using shared releases; moves secret fixtures to instance-private .fixtures.
scripts/server/dw-run.sh Executes commands inside the current release venv/working dir while sourcing instance .env.
scripts/server/deploy.sh Deploy via --ref→SHA→shared release build, switch current, migrate, rerender units/nginx, cleanup releases.
scripts/server/common.sh Adds RELEASES_DIR and centralizes read_env_value.
scripts/README.md Updates backup/rollback descriptions to use release SHA switching.
scripts/predeploy_rollback.sh Rollback now switches current to a release SHA and restarts services accordingly.
scripts/predeploy_backup.sh Backup now stamps dumps with current release SHA instead of git checkout HEAD.
frontend/vite.config.ts Build-id resolution from env/.release-sha before git; avoid backend .env dependency in production builds.
frontend/src/config/app.ts Fallback default for APP_NAME when VITE_APP_NAME is unset.
frontend/src/config/adminPages.ts Compute UAT URL dynamically from hostname instead of VITE_UAT_URL.
frontend/env.d.ts Removes VITE_UAT_URL typing.
frontend/.env.example Removes VITE_UAT_URL example entry.
docs/updating.md Updates operator workflow docs for shared release deploys + cleanup-only mode.
docs/server_setup.md Updates provisioning docs to reflect current -> releases/<sha> layout.
docketworks/settings.py Build-id now resolves from env/.release-sha before falling back to git.
apps/workflow/tests/test_xero_instance_templates.py Extends template/script tests to cover shared-release mechanics and build-id behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +156 to +160
for instance_dir in "$INSTANCES_DIR"/*; do
[[ -d "$instance_dir" ]] || continue
if [[ -L "$instance_dir/current" && "$(readlink -f "$instance_dir/current")" == "$(release_path "$sha")" ]]; then
return 0
fi

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentionally not applying this. A broken current symlink is an invalid state, and this codebase fails fast on invalid state rather than tolerating it (ADR 0015: fix the data, don't add a read-side fallback). Wrapping readlink in || true would let cleanup silently proceed past a broken instance link, masking a real problem. If a stop is too opaque we make it clearer (a diagnostic error), never tolerant. The deploy/cleanup aborting on a broken symlink is the desired behaviour.

Comment thread scripts/server/deploy.sh Outdated
Comment on lines +145 to +150
fqdn=$(grep -oP 'server_name \K[^;]+' "$existing_conf" | head -1 | awk '{$1=$1; print}')
cert_domain=$(grep -oP 'ssl_certificate /etc/letsencrypt/live/\K[^/]+' "$existing_conf" | head -1)
if [[ -z "$fqdn" || -z "$cert_domain" ]]; then
log " ERROR: Could not extract FQDN/CERT_DOMAIN from $existing_conf"
return 1
fi
Comment thread scripts/predeploy_backup.sh Outdated
Comment on lines 35 to 39
DB_NAME=$(grep -E '^DB_NAME=' "$ENV_FILE" | cut -d= -f2)
if [[ -z "$DB_NAME" ]]; then
echo "ERROR: DB_NAME not set in $ENV_FILE" >&2
exit 1
fi

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
scripts/server/instance.sh (1)

225-241: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Harden secret fixture staging against symlinks and failed cleanup.

The instance user owns $INSTANCE_DIR, so .fixtures can be pre-created or replaced before this root script writes secrets into it. Reject symlinks, make the staging directory/files root-owned but readable by the instance user, and use a trap so failed loaddata calls do not leave JSON secrets behind.

Also applies to: 247-266, 496-508

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/server/instance.sh` around lines 225 - 241, The secret fixture
staging in the AI providers fixture generation path is vulnerable because the
instance user can pre-create or replace .fixtures before root writes sensitive
JSON, and failed loaddata runs can leave secrets behind. Update the fixture
creation flow around the ai_providers.json generation logic to reject symlinks,
ensure the staging directory and file are root-owned while still readable by the
instance user, and add a trap/cleanup path so temporary secret fixtures are
removed on failure. Apply the same hardening to the related fixture-writing
flows referenced by the same staging helpers so all secret-containing outputs
are protected consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docketworks/settings.py`:
- Around line 18-34: `read_build_id()` is returning unvalidated values from
`DOCKETWORKS_BUILD_SHA` and `.release-sha`, so add upfront validation before
returning any build ID. Update the `read_build_id` flow to verify the value is a
real commit SHA format (for example, a full hex SHA) for both the environment
variable and the file contents, and fail fast with a clear error if it is
missing or invalid. Keep the existing fallback order in `read_build_id`, but
only return values that pass validation so `/api/build-id/` never exposes
malformed IDs.

In `@scripts/predeploy_rollback.sh`:
- Around line 65-70: Handle release-resolution failures in predeploy rollback
before assigning FULL_SHA. The current FULL_SHA="$(resolve_existing_release_sha
"$HASH")" path can terminate under set -e before the friendly release-missing
message is reached; update the rollback flow in predeploy_rollback.sh so
resolve_existing_release_sha/resolve_release_ref failures are captured
explicitly and reported with the intended error handling before checking
release_complete.

In `@scripts/server/deploy.sh`:
- Around line 316-327: The migration failure handler in deploy.sh only rolls
back the release pointer in the migrate failure path, which can leave the
database partially upgraded; update the migrate handling around the dw-run.sh
python manage.py migrate call to either invoke the pre-deploy database
restore/rollback flow or avoid switching back to the previous release unless DB
state has been explicitly restored or reconciled. Use the existing rollback
helpers and the switch_instance_release logic in this block to ensure the app
and schema stay in sync before continuing.
- Around line 333-335: Skip release cleanup when any instance has failed after
switching current. In deploy.sh, the failure path around render_nginx_config and
the later cleanup logic should be linked so that FAILED_INSTANCES prevents
deleting previous_sha entries that may still be serving traffic. Update the
cleanup section near the code that removes unreferenced SHAs to either
short-circuit when FAILED_INSTANCES is non-empty or explicitly keep previous
SHAs for failed instances, using the existing FAILED_INSTANCES, previous_sha,
and current release-handling logic to locate the fix.

In `@scripts/server/instance.sh`:
- Around line 323-328: The bootstrap detection in instance.sh is incorrectly
using the presence of .env to decide whether setup is complete, which can skip
migrations and admin initialization after a failed first run. Update the logic
around IS_EXISTING and NEEDS_APP_BOOTSTRAP so it relies on an explicit success
marker written only after bootstrap completes, or checks the database
migration/bootstrap state before setting NEEDS_APP_BOOTSTRAP false. Keep the fix
localized to the existing bootstrap flow near render_instance_env and the
current .env checks.

In `@scripts/server/README.md`:
- Line 208: The README inventory entry for server-setup.sh is stale because it
still mentions venv even though dependency installation now happens in release
builds via deploy.sh. Update the server-setup.sh description in the
scripts/server/README.md inventory row to remove the venv reference and keep the
wording aligned with its current host-level convergence responsibilities.
- Line 155: The fenced block in the README is missing a language identifier,
triggering the MD040 markdown lint rule. Update the fenced code block in the
server README section to include an explicit language tag (for example, the
block containing the /opt/docketworks/ path) so the opening and closing fences
remain balanced and markdown lint passes.

In `@scripts/server/release-utils.sh`:
- Around line 68-69: The switch_instance_release flow can update current to a
release that is missing or incomplete. Add a guard in switch_instance_release
using release_complete "$sha" before the ln -sfn and mv -Tf steps so the symlink
is only replaced when the target release is valid, and keep the check close to
the existing release selection logic in release-utils.sh.
- Around line 100-142: The release build currently creates the virtualenv under
the temporary .building path in the build block, which leaves venv metadata and
console scripts pointing at the wrong location after mv to the final release
directory. Update the release assembly logic in release-utils.sh so the
virtualenv used by the poetry install and subsequent npm steps is created or
rebuilt directly under the final release path (the release_dir/current .venv
location) before touch of .complete, and ensure the build commands reference
that final venv path instead of $tmp_dir/.venv.

---

Outside diff comments:
In `@scripts/server/instance.sh`:
- Around line 225-241: The secret fixture staging in the AI providers fixture
generation path is vulnerable because the instance user can pre-create or
replace .fixtures before root writes sensitive JSON, and failed loaddata runs
can leave secrets behind. Update the fixture creation flow around the
ai_providers.json generation logic to reject symlinks, ensure the staging
directory and file are root-owned while still readable by the instance user, and
add a trap/cleanup path so temporary secret fixtures are removed on failure.
Apply the same hardening to the related fixture-writing flows referenced by the
same staging helpers so all secret-containing outputs are protected
consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7fb9895b-6aa4-409c-b0c7-1132546231c3

📥 Commits

Reviewing files that changed from the base of the PR and between 742f4b9 and 21d06fa.

📒 Files selected for processing (25)
  • apps/workflow/tests/test_xero_instance_templates.py
  • docketworks/settings.py
  • docs/server_setup.md
  • docs/updating.md
  • frontend/.env.example
  • frontend/env.d.ts
  • frontend/src/config/adminPages.ts
  • frontend/src/config/app.ts
  • frontend/vite.config.ts
  • scripts/README.md
  • scripts/predeploy_backup.sh
  • scripts/predeploy_rollback.sh
  • scripts/server/README.md
  • scripts/server/common.sh
  • scripts/server/deploy.sh
  • scripts/server/dw-run.sh
  • scripts/server/instance.sh
  • scripts/server/release-utils.sh
  • scripts/server/server-setup.sh
  • scripts/server/templates/backup-db-instance.service.template
  • scripts/server/templates/celery-beat-instance.service.template
  • scripts/server/templates/celery-worker-instance.service.template
  • scripts/server/templates/frontend-env-instance.template
  • scripts/server/templates/gunicorn-instance.service.template
  • scripts/server/templates/nginx-instance.conf.template
💤 Files with no reviewable changes (3)
  • frontend/.env.example
  • frontend/env.d.ts
  • scripts/server/templates/frontend-env-instance.template

Comment thread docketworks/settings.py Outdated
Comment thread scripts/predeploy_rollback.sh Outdated
Comment thread scripts/server/deploy.sh
Comment thread scripts/server/deploy.sh
Comment thread scripts/server/instance.sh
Comment thread scripts/server/README.md Outdated
Comment thread scripts/server/README.md Outdated
Comment thread scripts/server/release-utils.sh
Comment thread scripts/server/release-utils.sh Outdated
corrin and others added 3 commits June 25, 2026 09:44
Distil the load-bearing architectural rules and code-style gotchas from
the ADRs and CLAUDE.md into .github/copilot-instructions.md so Copilot's
chat, completions, and code review align with the codebase instead of
suggesting typical Django/Vue defaults (deprecation shims, read-side
fallbacks, |\| true tolerance patches).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a local shellcheck hook (system binary, default severity) plus a .shellcheckrc with external-sources + source-path=SCRIPTDIR so sourced libraries like scripts/server/common.sh are followed — killing the SC1091/SC2153 false positives. Then fix every remaining finding across the repo's shell scripts so they pass clean:

- quote word-splitting expansions (SC2086/SC2046), drop useless cat (SC2002), use 'if cmd' over $? (SC2181), read -r (SC2162), split declare/assign (SC2155)
- fix a real copy-paste bug in validate-migration.sh: the parseFloat() line was reporting the parseInt count
- delete dead variables (SC2034); add justified directives for intentional dynamic 'source' (SC1090) and client-side ssh expansion (SC2029)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lure

predeploy_rollback.sh restores the pre-deploy dump into a temporary database and atomically swaps it into place (rename live DB aside, promote restored DB, drop the old one) instead of dropping and restoring the live DB in place — a failed restore no longer destroys the running database. Adds a root-user check, requires DB_USER, and verifies the target release exists before prompting.

deploy.sh stops an instance's services before switching releases. On a failed migration it leaves services stopped — the DB may be partially migrated, so code-only symlink rollback is unsafe — and prints the explicit predeploy_rollback.sh command and a showmigrations hint instead of auto-reverting. READMEs and docs/updating.md document the flow.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
scripts/server/README.md (2)

208-208: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the stale venv mention.

server-setup.sh no longer owns dependency installation; the per-release virtualenv now comes from the release build. Keeping venv here misdescribes the operator flow.

Proposed fix
-| `server-setup.sh`                                   | Host-level convergence (packages, venv, SSL, shared config). Runs every deploy — see "Server Setup". |
+| `server-setup.sh`                                   | Host-level convergence (packages, SSL, shared config). Runs every deploy — see "Server Setup". |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/server/README.md` at line 208, The deployment table entry for
server-setup.sh still mentions venv, but that responsibility has moved to the
release build. Update the description in the README table row for
server-setup.sh to remove the stale venv reference and keep the wording focused
on host-level convergence items only, preserving the existing server-setup.sh
and Server Setup references for location.

155-175: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a fence language tag.

The directory tree block still lacks a language identifier, so markdownlint will keep flagging MD040. text is enough here.

Proposed fix
-```
+```text
 /opt/docketworks/
 ...
-```
+```
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/server/README.md` around lines 155 - 175, Markdown in the directory
tree block is missing a fence language tag, so update the fenced block in the
README to use a text identifier to satisfy markdownlint MD040. Locate the
directory tree snippet under the server README and change the opening fence to a
text-labeled code block while keeping the rest of the content unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@scripts/server/README.md`:
- Line 208: The deployment table entry for server-setup.sh still mentions venv,
but that responsibility has moved to the release build. Update the description
in the README table row for server-setup.sh to remove the stale venv reference
and keep the wording focused on host-level convergence items only, preserving
the existing server-setup.sh and Server Setup references for location.
- Around line 155-175: Markdown in the directory tree block is missing a fence
language tag, so update the fenced block in the README to use a text identifier
to satisfy markdownlint MD040. Locate the directory tree snippet under the
server README and change the opening fence to a text-labeled code block while
keeping the rest of the content unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 16f0c6e2-ab52-4016-86ae-979cf6ceaf76

📥 Commits

Reviewing files that changed from the base of the PR and between 401e4d0 and 990cbed.

📒 Files selected for processing (21)
  • .pre-commit-config.yaml
  • .shellcheckrc
  • docs/updating.md
  • frontend/scripts/audit-numeric-conversions.sh
  • frontend/scripts/validate-migration.sh
  • frontend/tests/scripts/backup-db.sh
  • frontend/tests/scripts/restore-db.sh
  • scripts/README.md
  • scripts/backup_db.sh
  • scripts/cleanup_backups.sh
  • scripts/predeploy_backup.sh
  • scripts/predeploy_rollback.sh
  • scripts/pull_prod_backup.sh
  • scripts/server/README.md
  • scripts/server/certbot-dreamhost-auth.sh
  • scripts/server/certbot-dreamhost-cleanup.sh
  • scripts/server/common.sh
  • scripts/server/deploy.sh
  • scripts/server/instance.sh
  • scripts/server/server-setup.sh
  • scripts/setup_database.sh
💤 Files with no reviewable changes (1)
  • scripts/predeploy_backup.sh
✅ Files skipped from review due to trivial changes (11)
  • scripts/backup_db.sh
  • scripts/cleanup_backups.sh
  • scripts/server/certbot-dreamhost-cleanup.sh
  • .shellcheckrc
  • frontend/scripts/audit-numeric-conversions.sh
  • scripts/pull_prod_backup.sh
  • scripts/setup_database.sh
  • frontend/tests/scripts/restore-db.sh
  • scripts/server/certbot-dreamhost-auth.sh
  • scripts/README.md
  • docs/updating.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • scripts/server/common.sh
  • scripts/predeploy_rollback.sh
  • scripts/server/deploy.sh
  • scripts/server/instance.sh

corrin and others added 4 commits June 25, 2026 12:32
…ling

Build each shared release directly at releases/<sha> instead of building in a .building-<sha> staging dir and mv-ing it into place. Python venvs are not relocatable: the moved venv's console-script wrappers (gunicorn, celery) kept an absolute shebang to the staging path, so systemd service start failed after every deploy. Building in place keeps the shebangs valid; .complete (written last) stays the sole completion gate, and an interrupted build leaves an incomplete, unreferenced dir the next build clears (cleanup_stale_release_builds -> cleanup_incomplete_releases). Serial deploys mean no build races, so no lock is needed.

Also: switch_instance_release refuses to point an instance at an incomplete/missing release, and resolve_existing_release_sha gates on release_complete now that incomplete dirs can exist at the canonical path; deploy.sh skips release cleanup when any instance failed (don't delete a failed instance's rollback target); render_nginx_config greps and predeploy DB_NAME/DB_USER reads no longer abort under set -e before their explicit guards run (read_env_value / || true).

Addresses Copilot and CodeRabbit findings on #407.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
instance.sh decided whether to run one-time bootstrap (migrate + setup_dev_logins) from .env existence, but .env is written early — a first run that failed after that falsely skipped bootstrap and left a half-initialised instance. Gate on a .bootstrap-complete marker written only after bootstrap succeeds. setup_dev_logins resets staff passwords, so re-running bootstrap on a live instance is destructive: an existing instance with no marker is ambiguous (pre-marker vs failed first run), so stop with guidance rather than guess. Mark established instances once with 'sudo touch <dir>/.bootstrap-complete'.

Addresses CodeRabbit finding on #407.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
read_build_id() returned any non-empty DOCKETWORKS_BUILD_SHA / .release-sha / git output into /api/build-id/; a malformed value would silently break the frontend version-check reload. Validate all three sources are a 40-char hex SHA and raise ImproperlyConfigured otherwise. Adds tests.

Addresses CodeRabbit finding on #407.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The directory-tree fence lacked a language tag (MD040); the server-setup.sh inventory row said it installs a 'venv', but the app venv is built per-release by release-utils.sh. Addresses CodeRabbit findings on #407.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
scripts/server/release-utils.sh (1)

38-39: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use release_complete for prefix-matched releases too.

The prefix path accepts any directory with .complete, bypassing the .release-sha identity check used by release_complete. A stale/corrupt prefix match can prevent fallback ref resolution and break rollback/deploy selection.

Proposed fix
-    if (( ${`#matches`[@]} == 1 )) && [[ -f "${matches[0]}/.complete" ]]; then
-        basename "${matches[0]}"
-        return 0
+    if (( ${`#matches`[@]} == 1 )); then
+        matched_sha="$(basename "${matches[0]}")"
+        if release_complete "$matched_sha"; then
+            echo "$matched_sha"
+            return 0
+        fi
     fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/server/release-utils.sh` around lines 38 - 39, The prefix-match
branch in release-utils.sh is bypassing the same release validation used by
release_complete, so update the lookup logic around the prefix match handling to
require the .release-sha identity check before accepting a directory. Reuse the
release_complete validation path or apply the same check in the matching logic
that returns basename for matches[0], so only valid completed releases are
selected and stale/corrupt prefix matches are rejected.
scripts/server/instance.sh (1)

512-524: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Remove generated fixture secrets on load failure.

With set -e, a failing dw-run.sh ... loaddata exits before the following rm -f, leaving generated AI/Xero fixture JSON on disk. Wrap each load so cleanup runs on both success and failure.

🛡️ Proposed cleanup fix
     render_ai_providers_fixture "$INSTANCE_DIR" "$INSTANCE_USER"
     log "Loading AI providers..."
     local AI_PROVIDERS_FIXTURE="$INSTANCE_DIR/.fixtures/ai_providers.json"
-    "$SCRIPT_DIR/dw-run.sh" "$INSTANCE" python manage.py shell -c \
-        "from django.core.management import call_command; from apps.workflow.models import AIProvider; print('AIProvider already configured; skipping ai_providers.json load') if AIProvider.objects.exists() else call_command('loaddata', '$AI_PROVIDERS_FIXTURE')"
+    if ! "$SCRIPT_DIR/dw-run.sh" "$INSTANCE" python manage.py shell -c \
+        "from django.core.management import call_command; from apps.workflow.models import AIProvider; print('AIProvider already configured; skipping ai_providers.json load') if AIProvider.objects.exists() else call_command('loaddata', '$AI_PROVIDERS_FIXTURE')"; then
+        rm -f "$AI_PROVIDERS_FIXTURE"
+        exit 1
+    fi
     rm -f "$AI_PROVIDERS_FIXTURE"
 
     render_xero_apps_fixture "$INSTANCE_DIR" "$INSTANCE_USER"
     log "Loading Xero apps..."
     local XERO_APPS_FIXTURE="$INSTANCE_DIR/.fixtures/xero_apps.json"
-    "$SCRIPT_DIR/dw-run.sh" "$INSTANCE" python manage.py shell -c \
-        "from django.core.management import call_command; from apps.workflow.models import XeroApp; print('XeroApp already configured; skipping xero_apps.json load') if XeroApp.objects.exists() else call_command('loaddata', '$XERO_APPS_FIXTURE')"
+    if ! "$SCRIPT_DIR/dw-run.sh" "$INSTANCE" python manage.py shell -c \
+        "from django.core.management import call_command; from apps.workflow.models import XeroApp; print('XeroApp already configured; skipping xero_apps.json load') if XeroApp.objects.exists() else call_command('loaddata', '$XERO_APPS_FIXTURE')"; then
+        rm -f "$XERO_APPS_FIXTURE"
+        exit 1
+    fi
     rm -f "$XERO_APPS_FIXTURE"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/server/instance.sh` around lines 512 - 524, The AI/Xero fixture JSON
cleanup in instance.sh can be skipped when dw-run.sh or loaddata fails because
set -e exits before the rm -f calls. Update the AI provider and Xero app load
blocks in render_ai_providers_fixture/render_xero_apps_fixture flow so each
generated fixture file is removed regardless of success or failure, for example
by wrapping the load/skip logic in a cleanup-safe construct. Keep the existing
behavior around AIProvider.objects.exists() and XeroApp.objects.exists(), but
ensure the temporary .fixtures/ai_providers.json and .fixtures/xero_apps.json
are always deleted.
🧹 Nitpick comments (1)
apps/workflow/tests/test_build_id.py (1)

32-40: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the .release-sha build-id path.

The new runtime contract reads DOCKETWORKS_BUILD_SHA, then .release-sha, then git; these tests only cover the env branch, leaving the release-directory path untested.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/workflow/tests/test_build_id.py` around lines 32 - 40, Add a test for
the `.release-sha` branch in ReadBuildIdTests so the new fallback path is
covered. Extend test_build_id.py by mocking the environment without
DOCKETWORKS_BUILD_SHA, then patching the file access used by read_build_id() to
make `.release-sha` return a valid SHA and asserting that value is returned. Use
the existing read_build_id() and ReadBuildIdTests symbols to locate the fallback
logic, and keep the current env validation tests unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docketworks/settings.py`:
- Around line 44-47: The git SHA lookup in the settings module relies on
subprocess.run with the bare "git" executable name, which can be hijacked via
PATH. Update the logic around the SHA validation helper that calls git rev-parse
HEAD to resolve the full git executable path first and then use that pinned path
in subprocess.run, keeping the rest of the _validate_sha flow unchanged.

In `@scripts/server/release-utils.sh`:
- Around line 91-97: `ensure_release` currently performs an in-place rebuild
without its own synchronization, so concurrent `instance.sh` invocations can
delete or rebuild the same release directory at the same time. Add a
release-scoped lock inside `ensure_release` itself, or make sure every caller
acquires the same lock before entering that path; use the existing `rm -rf
"$release_dir"`/build flow in `release-utils.sh` as the place to wrap with
locking.

---

Outside diff comments:
In `@scripts/server/instance.sh`:
- Around line 512-524: The AI/Xero fixture JSON cleanup in instance.sh can be
skipped when dw-run.sh or loaddata fails because set -e exits before the rm -f
calls. Update the AI provider and Xero app load blocks in
render_ai_providers_fixture/render_xero_apps_fixture flow so each generated
fixture file is removed regardless of success or failure, for example by
wrapping the load/skip logic in a cleanup-safe construct. Keep the existing
behavior around AIProvider.objects.exists() and XeroApp.objects.exists(), but
ensure the temporary .fixtures/ai_providers.json and .fixtures/xero_apps.json
are always deleted.

In `@scripts/server/release-utils.sh`:
- Around line 38-39: The prefix-match branch in release-utils.sh is bypassing
the same release validation used by release_complete, so update the lookup logic
around the prefix match handling to require the .release-sha identity check
before accepting a directory. Reuse the release_complete validation path or
apply the same check in the matching logic that returns basename for matches[0],
so only valid completed releases are selected and stale/corrupt prefix matches
are rejected.

---

Nitpick comments:
In `@apps/workflow/tests/test_build_id.py`:
- Around line 32-40: Add a test for the `.release-sha` branch in
ReadBuildIdTests so the new fallback path is covered. Extend test_build_id.py by
mocking the environment without DOCKETWORKS_BUILD_SHA, then patching the file
access used by read_build_id() to make `.release-sha` return a valid SHA and
asserting that value is returned. Use the existing read_build_id() and
ReadBuildIdTests symbols to locate the fallback logic, and keep the current env
validation tests unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: edf2b137-6d45-4157-8c69-07f772d9f8f5

📥 Commits

Reviewing files that changed from the base of the PR and between 990cbed and 54b3369.

📒 Files selected for processing (8)
  • apps/workflow/tests/test_build_id.py
  • docketworks/settings.py
  • scripts/predeploy_backup.sh
  • scripts/predeploy_rollback.sh
  • scripts/server/README.md
  • scripts/server/deploy.sh
  • scripts/server/instance.sh
  • scripts/server/release-utils.sh
🚧 Files skipped from review as they are similar to previous changes (3)
  • scripts/predeploy_backup.sh
  • scripts/predeploy_rollback.sh
  • scripts/server/README.md

Comment thread docketworks/settings.py
Comment on lines +44 to +47
return _validate_sha(
subprocess.run(
["git", "rev-parse", "HEAD"],
cwd=BASE_DIR,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Harden git invocation against PATH hijacking

At Line 46, subprocess.run uses a partial executable name ("git"). If PATH is compromised, startup could execute the wrong binary. Resolve and pin the executable path first, then invoke that path.

Suggested fix
+import shutil
+
+GIT_BIN = shutil.which("git")
+if not GIT_BIN:
+    raise ImproperlyConfigured("git executable not found in PATH")
+
 def read_build_id() -> str:
@@
     return _validate_sha(
         subprocess.run(
-            ["git", "rev-parse", "HEAD"],
+            [GIT_BIN, "rev-parse", "HEAD"],
             cwd=BASE_DIR,
             check=True,
             capture_output=True,
             text=True,
         ).stdout,
🧰 Tools
🪛 ast-grep (0.44.0)

[error] 44-50: Command coming from incoming request
Context: subprocess.run(
["git", "rev-parse", "HEAD"],
cwd=BASE_DIR,
check=True,
capture_output=True,
text=True,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)

🪛 Ruff (0.15.18)

[error] 46-46: Starting a process with a partial executable path

(S607)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docketworks/settings.py` around lines 44 - 47, The git SHA lookup in the
settings module relies on subprocess.run with the bare "git" executable name,
which can be hijacked via PATH. Update the logic around the SHA validation
helper that calls git rev-parse HEAD to resolve the full git executable path
first and then use that pinned path in subprocess.run, keeping the rest of the
_validate_sha flow unchanged.

Source: Linters/SAST tools

Comment on lines +91 to +97
# Build directly at the final path. .complete (written last) is the only
# completion gate, so an interrupted build leaves an incomplete, unreferenced
# dir that we clear here and rebuild. Building in place (not build-then-mv)
# keeps the venv's console-script shebangs valid — a moved venv is not
# relocatable. Serial deploys mean there is no concurrent build to race.
rm -rf "$release_dir"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify all ensure_release call paths are serialized.
# Expected: deploy.sh and instance.sh either share the same lock before calling
# ensure_release, or ensure_release itself has a release-scoped flock around
# rm/build/touch .complete.
rg -n -C 8 '\bensure_release\b|\.deploy\.lock|\bflock\b' \
  scripts/server/instance.sh \
  scripts/server/deploy.sh \
  scripts/server/release-utils.sh

Repository: corrin/docketworks

Length of output: 4657


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant release helper and the call sites around ensure_release
# to determine whether they share the same deployment lock or any equivalent
# serialization around release creation.
printf '\n== release-utils.sh ==\n'
sed -n '1,180p' scripts/server/release-utils.sh

printf '\n== deploy.sh (lock + ensure_release) ==\n'
sed -n '1,330p' scripts/server/deploy.sh | sed -n '1,80p;280,300p'

printf '\n== instance.sh (lock + ensure_release) ==\n'
sed -n '1,560p' scripts/server/instance.sh | sed -n '1,120p;470,520p'

Repository: corrin/docketworks

Length of output: 15589


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant release helper and the call sites around ensure_release
# to determine whether they share the same deployment lock or any equivalent
# serialization around release creation.
printf '\n== release-utils.sh ==\n'
sed -n '1,180p' scripts/server/release-utils.sh

printf '\n== deploy.sh (lock + ensure_release) ==\n'
sed -n '1,330p' scripts/server/deploy.sh | sed -n '1,80p;280,300p'

printf '\n== instance.sh (lock + ensure_release) ==\n'
sed -n '1,560p' scripts/server/instance.sh | sed -n '1,120p;470,520p'

Repository: corrin/docketworks

Length of output: 15589


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find any locking/serialization in instance.sh and how ensure_release is reached.
rg -n -C 4 '\bflock\b|\.lock\b|ensure_release\b|current release|bootstrap|deploy' scripts/server/instance.sh

Repository: corrin/docketworks

Length of output: 3878


Guard ensure_release with a release-scoped lock. deploy.sh serializes this helper with .deploy.lock, but instance.sh calls the same in-place build path without that lock. Two concurrent instance.sh runs for the same SHA can remove and rebuild the canonical release directory out from under each other. Move the lock into ensure_release or acquire it at every caller.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/server/release-utils.sh` around lines 91 - 97, `ensure_release`
currently performs an in-place rebuild without its own synchronization, so
concurrent `instance.sh` invocations can delete or rebuild the same release
directory at the same time. Add a release-scoped lock inside `ensure_release`
itself, or make sure every caller acquires the same lock before entering that
path; use the existing `rm -rf "$release_dir"`/build flow in `release-utils.sh`
as the place to wrap with locking.

…creation

Revert ca69148's .bootstrap-complete marker gate (and fail-loud branch) back to the original .env-based NEEDS_APP_BOOTSTRAP gate — the marker machinery wasn't worth the friction for a minor instance-creation retry edge case.

Instance creation called setup_dev_logins.py, which also resets ALL staff passwords — a restore-prod-to-nonprod scrub that must never be triggered by provisioning. Add an --admin-only flag that ensures the default admin exists without touching staff passwords, and use it from instance.sh. Plain invocation (full reset) is unchanged for the restore flow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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