From 586400e56b388873ecfcd8c67fc494a88dd73e31 Mon Sep 17 00:00:00 2001 From: Md Nurnobi Date: Mon, 17 Aug 2026 18:14:38 -0700 Subject: [PATCH 1/4] docs(phase-004): finalize RC4 forensic baseline --- .github/workflows/ci.yml | 4 +- CHANGELOG.md | 14 + README.md | 6 +- VERSION | 2 +- design/DESIGN_MANIFEST.json | 2 +- docs/BUILD.md | 4 +- docs/DEPENDENCY_REVIEW.md | 6 +- docs/FORENSIC_AUDIT_REPORT.md | 323 ++++++++---------- docs/FORENSIC_FILE_INVENTORY.json | 127 +++---- docs/PERFORMANCE_REVIEW.md | 2 +- docs/PUBLISHING.md | 16 +- docs/RELEASE_NOTES_1.3.0.md | 15 +- docs/RELEASE_PROCESS.md | 2 +- docs/TEST_REPORT.md | 230 ++++++------- documents/ADMIN_GUIDE.md | 2 +- documents/BASELINE.md | 51 ++- documents/DOCUMENTATION_MANIFEST.json | 88 ++--- documents/DOCUMENTATION_POLICY.md | 2 +- documents/HOW_TO_USE.md | 2 +- documents/README.md | 35 +- documents/USER_MANUAL.md | 2 +- .../design/ACCESSIBILITY_SPECIFICATION.md | 2 +- documents/design/COMPONENT_MATRIX.md | 2 +- documents/design/FUTURE_UI_ROADMAP.md | 2 +- documents/design/IMPLEMENTATION_STATUS.md | 4 +- documents/design/RESPONSIVE_SPECIFICATION.md | 2 +- documents/design/SCREEN_CATALOG.md | 2 +- documents/design/UI_FOUNDATION.md | 2 +- documents/phases/PHASE-000-BASELINE.md | 2 +- .../PHASE-001-UI-DESIGN-INTAKE-BASELINE.md | 2 +- ...RED-UI-FOUNDATION-AND-APPLICATION-SHELL.md | 2 +- ...COVERY-AND-INBOUND-DELIVERY-RELIABILITY.md | 2 +- ...ASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md | 72 ++++ mailbox-app/CHANGELOG.md | 5 + mailbox-app/pyproject.toml | 2 +- 35 files changed, 568 insertions(+), 470 deletions(-) create mode 100644 documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 721655a..b7ebd81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -125,5 +125,5 @@ jobs: - name: Release verification run: | python scripts/verify_release.py \ - dist/mailstack-1.3.0-rc.4-source.zip \ - --checksum dist/mailstack-1.3.0-rc.4-source.zip.sha256 + dist/mailstack-1.3.0-rc.5-source.zip \ + --checksum dist/mailstack-1.3.0-rc.5-source.zip.sha256 diff --git a/CHANGELOG.md b/CHANGELOG.md index ce849b0..93d8404 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable repository-level changes are recorded here. Application history before the open-source conversion remains in `mailbox-app/CHANGELOG.md`. +## 1.3.0-rc.5 — Unreleased + +### Documentation and forensic baseline + +- Finalized the published RC4 evidence in the forensic and test reports using the successful `main`, tag-CI, and release-artifact workflow results instead of the pre-release RC3/RC4-pending wording. +- Established `MAILSTACK-1.3.0-RC4-OFFICIAL-SOURCE-BASELINE-001` as the current official source baseline, anchored to tag `v1.3.0-rc.4`, commit `896dbcc2ed1f38d9c618bf0b712efe5923f92e56`, tree `0d845b3d975949894c24581e6834aff7b33c30b4`, and deterministic source SHA-256 `58f06adea7c813e9861799d20e392441367bf64f6513d6e0634455d2011d4eac`. +- Preserved the earlier RC1 documentation baseline as historical provenance instead of treating its source commit as the current release anchor. +- Added the PHASE-004 release, upgrade, and operational-reliability record; PHASE-004A changes documentation, release metadata, and generated manifests only. + +### Compatibility + +- No application runtime, database schema, migration, route, authorization, UI, mail-flow, installer behavior, deployment template, or service configuration changes are introduced by PHASE-004A. +- `v1.3.0-rc.4` and its published source identity remain immutable; `1.3.0-rc.5` is the next development-candidate version and is not yet a published release. + ## Unreleased — MailStack repository bootstrap ### Changed diff --git a/README.md b/README.md index b0390d7..bffa23f 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ It combines **Postfix**, **Dovecot LMTP**, **Django**, **MariaDB**, **Maildir**, **Gunicorn**, and **Nginx** in a hardened single-node reference deployment. MailStack provides a private browser-based inbox, administrator-managed users, object-level mailbox access, safe email rendering, protected attachments, live inbox updates, operational audit logs, backup and restore tooling, a public website, and an isolated contact service. -> **Release status:** `v1.3.0-rc.4` remains a release candidate. Clean Ubuntu 24.04 installation and real external SMTP/LMTP delivery have been exercised during the PHASE-003 hardening campaign; backup/restore, restart-recovery, legal, and final release-owner acceptance still gate stable promotion. +> **Release status:** `v1.3.0-rc.4` is the latest published release candidate. The repository development version is `1.3.0-rc.5` for PHASE-004 reliability work. PHASE-003 staging validated real external SMTP/LMTP delivery after the accepted fixes; an exact RC4 clean-host reinstall remains deferred until a fresh VPS is available. Backup/restore, restart-recovery, legal, and final release-owner acceptance still gate stable promotion. ## Why MailStack @@ -211,8 +211,8 @@ Build and verify a deterministic source release: ```bash python scripts/build_release.py --root . python scripts/verify_release.py \ - dist/mailstack-1.3.0-rc.4-source.zip \ - --checksum dist/mailstack-1.3.0-rc.4-source.zip.sha256 + dist/mailstack-1.3.0-rc.5-source.zip \ + --checksum dist/mailstack-1.3.0-rc.5-source.zip.sha256 ``` ## Security diff --git a/VERSION b/VERSION index 9b388ed..d4f0697 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.0-rc.4 +1.3.0-rc.5 diff --git a/design/DESIGN_MANIFEST.json b/design/DESIGN_MANIFEST.json index 811dcce..0a9aa00 100644 --- a/design/DESIGN_MANIFEST.json +++ b/design/DESIGN_MANIFEST.json @@ -561,7 +561,7 @@ "unsupported_features_inactive": true }, "project": "MailStack", - "release_version": "1.3.0-rc.4", + "release_version": "1.3.0-rc.5", "schema_version": 1, "source_archive": { "archive_crc": "PASS", diff --git a/docs/BUILD.md b/docs/BUILD.md index c5ddcee..58eac53 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -20,8 +20,8 @@ python scripts/forensic_audit.py --root . --full ```bash python scripts/build_release.py --root . python scripts/verify_release.py \ - dist/mailstack-1.3.0-rc.4-source.zip \ - --checksum dist/mailstack-1.3.0-rc.4-source.zip.sha256 + dist/mailstack-1.3.0-rc.5-source.zip \ + --checksum dist/mailstack-1.3.0-rc.5-source.zip.sha256 ``` The builder normalizes archive timestamps, preserves executable permissions, writes a source manifest, excludes generated/runtime artifacts and emits a SHA-256 checksum. diff --git a/docs/DEPENDENCY_REVIEW.md b/docs/DEPENDENCY_REVIEW.md index 68defab..cfbd181 100644 --- a/docs/DEPENDENCY_REVIEW.md +++ b/docs/DEPENDENCY_REVIEW.md @@ -30,5 +30,7 @@ GitHub Actions run `32053931714` on PHASE-003 RC2 passed the structural and repo blocking `pip-audit` step found CVE-2026-71491, CVE-2026-59894, CVE-2026-59893, and CVE-2026-54284 in `sqlparse==0.5.5`. RC3 introduced and RC4 preserves `sqlparse==0.6.0`, the upstream security release fixing those findings. Django 5.2.16 permits `sqlparse>=0.3.1`, and sqlparse 0.6.0 supports Python 3.10+, including -MailStack's required Python 3.12 runtime. `pip check` and the network-enabled blocking `pip-audit` -gate remain mandatory for RC4 qualification; no advisory is ignored or suppressed. +MailStack's required Python 3.12 runtime. RC4 subsequently passed `pip check` and the blocking +network-enabled `pip-audit` gate in the qualified branch/main/tag workflows with no known +vulnerabilities reported. The same gates remain mandatory for RC5 and later changes; no advisory is +ignored or suppressed. diff --git a/docs/FORENSIC_AUDIT_REPORT.md b/docs/FORENSIC_AUDIT_REPORT.md index d05dbec..7f407bc 100644 --- a/docs/FORENSIC_AUDIT_REPORT.md +++ b/docs/FORENSIC_AUDIT_REPORT.md @@ -1,184 +1,159 @@ -# Forensic audit report — MailStack 1.3.0 RC3 +# Forensic audit report — MailStack 1.3.0 RC5 development baseline -**PHASE-003 audit date:** 2026-08-17 -**Release version:** `1.3.0-rc.4` +**PHASE-004A audit date:** 2026-08-17 +**Repository development version:** `1.3.0-rc.5` +**Latest published release candidate:** `v1.3.0-rc.4` **Target runtime:** Ubuntu Server 24.04 LTS and CPython 3.12 -**Release classification:** locally structurally qualified release candidate; dependency-backed RC3 CI requalification pending +**Current classification:** RC4 release qualification finalized; RC5 PHASE-004A CI requalification pending ## Executive disposition | Gate | Status | |---|---| -| Baseline feature preservation | PASS | -| Repository structure and public documentation | PASS | -| User manual, how-to, admin guide and phase-document baseline | PASS | -| UI design intake, immutable-source manifest and design contract tests | PASS in GitHub Actions run `30165905840` | -| PHASE-002 shared UI foundation structural contracts | PASS locally — 8 tests | -| PHASE-002 local Django, coverage and mailbox lint qualification | PASS — 195 passed, 1 skipped, 94.99% coverage | -| Deterministic documentation index, manifest and change policy | PASS | -| Complete AGPL-3.0 license text | PASS | -| Source secret/private-data safety gate | PASS | -| Python, JSON, YAML and shell syntax | PASS | -| Django application tests and coverage | PASS | -| Ruff and Bandit | PASS for mailbox application; standalone contact-service gates added to final verifier and CI | -| Django checks and migration drift | PASS | -| Public contact-service tests | PASS | -| Installer and operations contract tests | PASS | -| Template rendering and placeholder validation | PASS | -| Forensic file/symbol inventory | PASS | -| Deterministic release ZIP, manifest and checksum | PENDING RC4 CI; deterministic local build/verification required before patch handoff | -| Online dependency advisory query | PENDING RC4 rerun — RC2 run `32053931714` failed on sqlparse 0.5.5; RC3/RC4 pin 0.6.0 | -| Clean Ubuntu 24.04 RC3 full-stack acceptance | PENDING exact-RC3 clean VPS requalification | -| Real inbound SMTP/LMTP acceptance | PASS in staging after equivalent PHASE-003 LMTP hotfix; exact-RC3 clean requalification pending | -| Copyright ownership/license confirmation | PENDING release owner | - -**OPEN_SOURCE_RELEASE_CANDIDATE:** PENDING RC4 CI -**PRODUCTION_ACCEPTANCE:** PENDING - -## Audited scope - -The audit covers the complete maintained repository: Django application code, migrations, templates, static assets, tests, Postfix/Dovecot/MariaDB/Nginx/systemd templates, public website, contact service, installer, backup/restore/health scripts, CI workflows, release tooling, governance documents and repository metadata. - -The deterministic machine-readable inventory at `docs/FORENSIC_FILE_INVENTORY.json` records every maintained file except itself and generated build/cache artifacts. It includes SHA-256, byte size, text-line count and Python/shell symbol information. - -## Architecture - -- Django 5.2.16 team mailbox application -- Gunicorn application service over a Unix socket -- MariaDB application schema and virtual-mail schema -- receive-only Postfix virtual mailbox delivery -- Dovecot LMTP delivery to Maildir -- durable Maildir ingestion worker -- safe MIME/HTML processing and protected attachment storage -- Nginx TLS reverse proxy and static/protected-file serving -- static public website and isolated rate-limited contact service -- systemd confinement -- backup, restore, rollback, health, verification, CI and deterministic release tooling - -## Feature preservation - -No functional application, migration, template, test, public-site or contact-service baseline file was deleted. The prior root `SOURCE_MANIFEST.sha256` was a generated snapshot and is deliberately regenerated inside every release archive rather than maintained as stale source metadata. - -Preserved behavior includes: - -- administrator and ordinary-user authentication -- administrator-managed user lifecycle -- object-scoped mailbox memberships -- mailbox create, enable, disable and soft-delete -- reserved postmaster/abuse handling -- receive-only Postfix/Dovecot delivery -- Maildir ingestion, duplicate protection and restart safety -- MIME parsing, HTML sanitization and attachment authorization -- search, pagination, counters and read/unread state -- live inbox updates -- security audit logging and health/readiness routes -- public website and protected contact workflow -- backup/restore/rollback and legacy `vibmail.my` compatibility - -See `FEATURE_MATRIX.md` for the feature-by-feature verification record. - -## Defects and release gaps remediated - -1. Fixed-domain assumptions were generalized while retaining the legacy defaults. -2. Production settings now fail closed for invalid hostnames, paths, origins, secrets and SQL identifiers. -3. Source and release scanners block credentials, private keys, databases, Maildir, attachments, logs and archives. -4. MariaDB privileges and `SQL SECURITY INVOKER` views use least-privilege access. -5. Postfix has no mailbox-secret, write or DDL access. -6. Dovecot remains LMTP-only under the fixed virtual-mail identity. -7. Installer validation rejects hostname collisions and malformed arguments. -8. Backup/restore checksums, archive safety and exact prior service-state restoration were strengthened. -9. Repository documentation, governance, SEO metadata, community templates and release automation were completed. -10. The complete AGPL-3.0 license text and licensing rationale were added. -11. A deterministic file/symbol inventory and documentation consistency gate were added. -12. Local audit virtual environments are ignored without weakening generated-artifact release blocking. -13. A root `documents/` baseline now provides maintained user, administrator and phase documentation. -14. Deterministic synchronization, draft blocking and diff-based CI policy prevent maintained feature changes from merging without substantive documentation. -15. The complete 25-image UI and logo archive is preserved with stable IDs, SHA-256 hashes, PNG structural validation, scope classification, and CI enforcement. -16. PHASE-002 adds the frozen runtime design tokens, responsive authenticated and sign-in shells, local SVG assets, accessible navigation behavior, and focused UI contract gates without changing page business logic. -17. Cross-platform verification now closes contact-service SQLite handles deterministically, preserves POSIX-only permission assertions, and subjects the standalone contact service to Ruff and Bandit in both the full forensic gate and CI. -18. RC3 updates the vulnerable transitive sqlparse 0.5.5 lock to upstream 0.6.0 after the blocking PHASE-003 CI advisory scan identified four 2026 CVEs; no advisory suppression or application behavior change is introduced. - -## Automated evidence - -- Last completed pre-RC3 Django suite: **195 passed, 1 capability-based skip, 0 failed**; RC3 full rerun pending -- Application coverage: **94.99%**; minimum: **85%** -- Ruff: **PASS** -- Bandit: **PASS** -- Django system check: **PASS** -- Migration drift: **none** -- Contact-service test program: **PASS**, including deterministic connection-close regression coverage -- Deployment templates rendered: **13**, unresolved tokens: **0** -- Installer plans: **2 valid and 9 invalid cases**, all passed -- Backup/restore/health contracts: **PASS** -- Shell syntax: **13 files PASS** -- Source forensic gate: **PASS with zero blocking findings** -- Engineering documentation gate: **PASS** -- User-document synchronization, manifest and policy tests: **PASS** -- Python environment consistency (`pip check`): **PASS** - -The authoritative repository qualification is GitHub Actions run `30133728843` on Ubuntu 24.04 with Python 3.12 at commit `1e1737edea2e6c922265a15d8584b56671820c65`. Local isolated audit environments remain useful for non-networked structural checks but do not replace CI. +| Official RC4 source baseline identity | PASS | +| RC4 `main` CI run `32071701530` | PASS | +| RC4 tag CI run `32072699991` | PASS | +| RC4 release-artifact workflow `32072699830` | PASS | +| RC4 dependency vulnerability audit | PASS — no known vulnerabilities reported | +| RC4 Django application suite | PASS — 198 passed, 0 failed | +| RC4 application coverage | PASS — 95.00%, minimum 85% | +| RC4 Ruff and Bandit | PASS | +| RC4 Django checks and migration drift | PASS | +| RC4 contact-service tests, Ruff, and Bandit | PASS | +| RC4 installer and operations contracts | PASS | +| RC4 full forensic audit | PASS — zero blocking findings | +| RC4 deterministic release build and verification | PASS | +| RC4 deterministic source SHA-256 | `58f06adea7c813e9861799d20e392441367bf64f6513d6e0634455d2011d4eac` | +| PHASE-003 staging external SMTP/LMTP path | PASS after accepted fixes | +| Exact RC4 clean-host reinstall | DEFERRED until a fresh VPS is available | +| Real backup/restore acceptance | PENDING | +| Restart/reboot recovery acceptance | PENDING | +| Final ownership/license review | PENDING release owner | +| PHASE-004A structural/local repository gates | PASS | +| PHASE-004A GitHub CI | PENDING after branch push | + +**OFFICIAL_RC4_SOURCE_BASELINE:** PASS +**RC4_OPEN_SOURCE_RELEASE_CANDIDATE:** PASS +**PHASE_004A_LOCAL_QUALIFICATION:** PASS +**RC5_DEVELOPMENT_REQUALIFICATION:** PENDING GITHUB CI +**STABLE_PRODUCTION_ACCEPTANCE:** PENDING + +## Official RC4 baseline identity + +The official frozen source baseline is `MAILSTACK-1.3.0-RC4-OFFICIAL-SOURCE-BASELINE-001`: + +- release: `1.3.0-rc.4` +- tag: `v1.3.0-rc.4` +- commit: `896dbcc2ed1f38d9c618bf0b712efe5923f92e56` +- Git tree: `0d845b3d975949894c24581e6834aff7b33c30b4` +- deterministic archive: `mailstack-1.3.0-rc.4-source.zip` +- archive SHA-256: `58f06adea7c813e9861799d20e392441367bf64f6513d6e0634455d2011d4eac` +- verified archive members: 405 +- verified manifest members: 404 + +The prior `MAILSTACK-1.3.0-RC1-DOCS-BASELINE-001` remains historical documentation provenance; it is +not the current canonical source anchor. + +## RC4 authoritative automated evidence + +The post-merge `main` workflow run `32071701530` completed successfully at commit +`896dbcc2ed1f38d9c618bf0b712efe5923f92e56`. Its blocking quality-and-security job passed source +safety, documentation and design integrity, shared UI contracts, forensic inventory, deployment +templates, installer contracts, operations contracts, dependency vulnerability audit, Ruff, +Bandit, Django tests and coverage, contact-service tests/lint/security checks, Django checks, shell +syntax, full forensic audit, deterministic release build, and release verification. + +The release tag triggered two further authoritative workflows on the same source commit: + +- tag CI run `32072699991`: **PASS** +- release-artifact workflow `32072699830`: **PASS** + +The release-artifact workflow used Ubuntu 24.04 and Python 3.12, reran the full forensic gate, built +the deterministic source archive, verified the checksum/manifest, and uploaded the verified GitHub +Actions artifact. + +## RC4 test and coverage result + +The authoritative released-RC4 application suite collected and passed 198 tests with zero failures. +Total application coverage was 95.00 percent against the repository's 85 percent minimum. The +network-enabled dependency advisory audit reported no known vulnerabilities for the locked RC4 +runtime, including `sqlparse==0.6.0`. + +Historical PHASE-002 local Windows evidence of 195 passed tests, one capability-based symbolic-link +skip, and 94.99 percent coverage remains valid as historical evidence only; it is no longer the +current release-qualification result. + +## Architecture and feature preservation + +The audited baseline preserves: + +- administrator and ordinary-user authentication; +- administrator-managed user lifecycle; +- object-scoped mailbox memberships; +- mailbox create, enable, disable, and soft-delete behavior; +- reserved postmaster/abuse handling; +- receive-only Postfix virtual-recipient validation; +- Dovecot LMTP delivery to Maildir; +- durable Maildir ingestion, duplicate protection, and restart safety; +- MIME parsing, sanitized HTML, and protected attachment authorization; +- search, pagination, counters, read/unread state, and live inbox updates; +- security audit logging and readiness/health routes; +- public website and isolated contact workflow; +- backup/restore/rollback tooling and legacy `vibmail.my` compatibility. + +No SMTP submission, IMAP, POP3, public registration, outbound reply/forward/send, campaigns, or +multi-node operation is introduced. + +## PHASE-003 reliability evidence + +PHASE-003 corrected the reproduced installer and inbound-delivery defects without adding a database +migration or changing application product scope. The accepted source includes protection of the +host-wide `/var/log` mode, sanitized least-privilege installer execution, provisioning lock-path +preparation, explicit repair preservation, early root-only initial-admin credential persistence, +Dovecot static-userdb `allow_all_users=yes` for Postfix-validated recipients, live-safe one-shot +Maildir verification, narrow production MariaDB warning qualification, and SSH session-resilience +guidance. + +The staging campaign demonstrated real Gmail delivery through Postfix and Dovecot LMTP into Maildir, +queue drain, ingestion, and web-inbox visibility after the accepted fixes. The exact final RC4 +archive was not reinstalled on a new clean host before publication; that clean-host acceptance is +explicitly deferred rather than being claimed. + +## PHASE-004A audit boundary + +PHASE-004A starts the repository development identity `1.3.0-rc.5` and is documentation/metadata +only. It finalizes RC4 evidence, establishes the official RC4 source baseline, adds the PHASE-004 +record, synchronizes active managed-document version metadata, updates current build/release examples, +and regenerates deterministic documentation/design/forensic manifests. + +PHASE-004A does not change application runtime logic, models, migrations, URLs, authorization, +templates, CSS, JavaScript, mail flow, ingestion behavior, installer behavior, deployment templates, +service definitions, database schema, DNS/TLS configuration, or the existing VPS. ## Security review -Verified controls include root-only generated secrets, strict configuration validation, Argon2 password hashing, CSRF and secure-cookie controls, login throttling, object-level mailbox authorization, safe HTML sanitization, protected attachments, receive-only SMTP, no public registration, no IMAP/POP3/submission in the reference deployment, MariaDB least privilege, systemd sandboxing, safe archive extraction, checksum verification and fail-closed CI/release gates. - -The historical pre-PHASE-003 `pip-audit` gate passed in GitHub Actions run `30133728843`. -For the current PHASE-003 branch, run `32053931714` failed at the blocking advisory step because -`sqlparse==0.5.5` was newly reported for CVE-2026-71491, CVE-2026-59894, CVE-2026-59893, and -CVE-2026-54284. RC3 pins upstream sqlparse 0.6.0, which contains those security fixes. The RC3 -dependency audit, `pip check`, and all downstream CI gates remain mandatory; no vulnerability is -ignored or waived. - -## Performance review - -Web requests and Maildir ingestion remain separate services. Gunicorn worker/thread counts are bounded, database connections are reused, mailbox/message fields are indexed, list views are paginated, live-update scopes are bounded, attachments remain file-backed and Nginx serves static/protected files. Backup/restore performs controlled service quiescing and restores the exact prior active state. - -## GitHub/open-source readiness - -The repository includes an SEO-oriented README, logo asset, release/download/license/platform/language/community badges, GitHub topics and description guidance, full license, notice, security policy, contribution guide, code of conduct, support policy, roadmap, issue/PR templates, CODEOWNERS, Dependabot, CI, release workflow, installation/build/development/operations/security documentation and Vib Tools ecosystem links. - - -## PHASE-002 audit boundary - -The phase changes only the shared base template, shared static foundation assets and JavaScript, -focused tests, the contact-service connection-lifecycle helper, CI/audit tooling, generated -inventories, and required documentation. It contains no model, migration, URL, form, permission, -mail-flow, ingestion schema, deployment-template, or package change. Local dependency-backed tests -and coverage passed; the final overwrite verifier must pass before commit, and GitHub Actions must -pass before remote qualification. - -## PHASE-003 audit boundary - -PHASE-003 changes only installer/recovery behavior, the Dovecot LMTP static-userdb template, -one-shot ingestion verification semantics, narrowly qualified production MariaDB warnings, focused -management-command/test contracts, release metadata, and required documentation. It adds no database -migration, dependency, UI page, URL, permission model, outbound mail path, or data transformation. - -The structural forensic gate passes with zero blocking findings after regenerating the deterministic -file inventory. RC4 additionally centralizes verified Bash runtime discovery for repository-level -installer, operations, and forensic tooling: Windows prefers Git for Windows Bash over the WSL -launcher, while Linux keeps the system Bash contract. This prevents an unavailable WSL/Docker -Desktop backing disk from being reported as multiple MailStack shell syntax defects. A subsequent -Windows run proved Git Bash selection was correct but exposed a second host-compatibility edge: the -Windows Python installation provided `python` but no Git-Bash-visible `python3` command. The RC4 -audit harness now uses a process-local `BASH_ENV` bridge to map `python3` to the exact interpreter -running the test harness on Windows only; `install.sh` and Ubuntu's production `python3` behavior -remain unchanged. Dependency-free documentation, design, UI-foundation, template, installer, -operations, Python compile, and shell-syntax gates pass locally. Dependency-backed -Django/Ruff/Bandit/coverage and full-forensic qualification remain blocking in GitHub Actions because -the local artifact builder does not contain or have network access to the pinned development -environment. - -## External acceptance gates - -Before stable promotion: - -1. Confirm source ownership and third-party license compatibility. -2. Preserve the successful GitHub CI baseline and require every subsequent release commit to pass all blocking gates. -3. Install on a clean isolated Ubuntu Server 24.04 VPS. -4. Verify DNS, MX, PTR/rDNS, TLS, firewall, unknown-recipient rejection, LMTP delivery, ingestion, authorization, contact delivery, backup, restore and restart recovery. -5. Publish `v1.3.0-rc.4` only after its blocking CI gates pass; promote to stable only after the remaining acceptance gates pass. - -## Final classification - -The repository remains a **MailStack 1.3.0 RC4 release candidate**, not yet a proven stable production release. PHASE-003 source qualification is complete only when its blocking GitHub Actions run passes. +The RC4 source remains pinned to Django 5.2.16, Python 3.12, and `sqlparse==0.6.0`. The blocking +online `pip-audit` gate passed after RC3 replaced the vulnerable `sqlparse==0.5.5` pin. No advisory +suppression was added for those findings. Existing security boundaries remain: Argon2 password +hashing, CSRF and secure-cookie controls, login throttling, object-level mailbox authorization, +sanitized HTML, protected attachments, receive-only SMTP, no public registration, MariaDB least +privilege, systemd confinement, archive safety, checksum verification, and fail-closed release gates. + +## External acceptance gates before stable promotion + +1. Preserve a passing GitHub CI baseline for every subsequent release commit. +2. Perform an exact-source clean installation on an isolated Ubuntu Server 24.04 VPS when a fresh + test host is available. +3. Validate DNS/MX/PTR/rDNS/TLS/firewall and unknown-recipient rejection on that clean host. +4. Perform a real backup/restore acceptance exercise with data-integrity verification. +5. Verify restart/reboot recovery of MariaDB, Postfix, Dovecot, Gunicorn, ingestion, Nginx, and the + contact service. +6. Confirm copyright ownership and third-party license compatibility. +7. Obtain final release-owner acceptance before promoting `1.3.0` stable. + +## Final disposition + +The published `v1.3.0-rc.4` source is a qualified release candidate and is the official frozen source +baseline for PHASE-004. PHASE-004A corrects the documentation evidence around that baseline but does +not retroactively modify or retag RC4. The working repository version `1.3.0-rc.5` remains a +development candidate until its branch GitHub CI passes. diff --git a/docs/FORENSIC_FILE_INVENTORY.json b/docs/FORENSIC_FILE_INVENTORY.json index 30aed4f..5237c03 100644 --- a/docs/FORENSIC_FILE_INVENTORY.json +++ b/docs/FORENSIC_FILE_INVENTORY.json @@ -74,7 +74,7 @@ "kind": "text", "lines": 129, "path": ".github/workflows/ci.yml", - "sha256": "0b719b1125e6cd730da533e1bc3d729e00b067a8f0707c7c2837ada2d3fd8b81", + "sha256": "bdab8313c3732e0652637a1048e04872629c5cf99cf49263720cd39a3ab2f926", "size_bytes": 4106 }, { @@ -93,10 +93,10 @@ }, { "kind": "text", - "lines": 96, + "lines": 110, "path": "CHANGELOG.md", - "sha256": "fa3285b026126a1d98da9cef4817015c86decf0793b8205bf4357ad8b6773f10", - "size_bytes": 8261 + "sha256": "53e1e7a9429533729f30bef2986b75976f77578a7efeb39f3ffa66e2bc93475d", + "size_bytes": 9547 }, { "kind": "text", @@ -137,8 +137,8 @@ "kind": "text", "lines": 314, "path": "README.md", - "sha256": "55aea5c0a8828fa26d10913c82a3fc16a78c2ee3bf44c00bbb7aff1bd68a7719", - "size_bytes": 13033 + "sha256": "0b42d27779799a7e9fc5daba7d0dfb867f2f89feffb19eff39fad6e08df9d72b", + "size_bytes": 13170 }, { "kind": "text", @@ -165,7 +165,7 @@ "kind": "text", "lines": 1, "path": "VERSION", - "sha256": "87892d5c7aa3d86de1ccd4742825eb8ff6f9c6350133351dd01a6007e1163e87", + "sha256": "055c3f7a1baed813c28024ff2509c55fb90e27cd93799cc933395edd5c950a44", "size_bytes": 11 }, { @@ -277,7 +277,7 @@ "kind": "text", "lines": 581, "path": "design/DESIGN_MANIFEST.json", - "sha256": "353323c27340d064b3226a94ad5dd6286d5c3efad3001b2fbd77f55601a420aa", + "sha256": "5b3de8f4ca8e3edd890eaef30c257465e981e5de597d3c757b8ae42c8cb23781", "size_bytes": 21143 }, { @@ -469,7 +469,7 @@ "kind": "text", "lines": 31, "path": "docs/BUILD.md", - "sha256": "04fdbccd2bb6484d38cfe49d4df429454c5309178f7c297b22bf3b8de3a852bf", + "sha256": "0f0ed0d791a2ddefa4a83f9be173bc42912bf34b7d496a02a4223af83204bff2", "size_bytes": 804 }, { @@ -488,10 +488,10 @@ }, { "kind": "text", - "lines": 34, + "lines": 36, "path": "docs/DEPENDENCY_REVIEW.md", - "sha256": "2a9e2eeb011457c9ca9711e9422febd731c006aafc22c1760c17d173fdb1d2c6", - "size_bytes": 2184 + "sha256": "5d9fad2c726291bc979299fb6b5e88b843136fa911ed1dd409b372db2357e313", + "size_bytes": 2310 }, { "kind": "text", @@ -537,10 +537,10 @@ }, { "kind": "text", - "lines": 184, + "lines": 159, "path": "docs/FORENSIC_AUDIT_REPORT.md", - "sha256": "d02d831ed01c89a80badec752f7fed92a4a23e934d51270c49f864f30cd082bd", - "size_bytes": 12561 + "sha256": "fe17e0c0784a127f6166f3383176e0cedd33fb803728bb68b67a2548e20bc4b4", + "size_bytes": 8353 }, { "kind": "text", @@ -602,8 +602,8 @@ "kind": "text", "lines": 55, "path": "docs/PERFORMANCE_REVIEW.md", - "sha256": "6f3d980f1f24cf43bfb8231b51fc4e2522c6336f7259782bed30901be557f590", - "size_bytes": 3397 + "sha256": "eac64eb35cf2d08f0d3f6b72cae1be30172c9f7c294acb1899772b3658a7fb6f", + "size_bytes": 3434 }, { "kind": "text", @@ -623,8 +623,8 @@ "kind": "text", "lines": 61, "path": "docs/PUBLISHING.md", - "sha256": "d8e887b3cf2af8c3688c71d9bb535f90911d354dfc31d09121e4b1031dbdfa19", - "size_bytes": 2279 + "sha256": "f3bd3a629334ad7f916932b64c231091b2623ba7a04ae1340447249056c85446", + "size_bytes": 2289 }, { "kind": "text", @@ -642,16 +642,16 @@ }, { "kind": "text", - "lines": 92, + "lines": 99, "path": "docs/RELEASE_NOTES_1.3.0.md", - "sha256": "43626199bdc76f4b30c9b57b9630687df3b76307db2e45e8af711b39e7a78f25", - "size_bytes": 6059 + "sha256": "34ad6fc04b4e6691385c3a6dbac1e2eaf8e15caf0b5d78f0ea817589dfb13e87", + "size_bytes": 6568 }, { "kind": "text", "lines": 14, "path": "docs/RELEASE_PROCESS.md", - "sha256": "e6ab1c94eecadb55d6cdf6eb040ed312ace2f74d5818f4bbadb1df3f55eb9196", + "sha256": "f744e5e890d84a8b2786899f62f72cca838f4969870c1cc415367245368a2b44", "size_bytes": 973 }, { @@ -670,10 +670,10 @@ }, { "kind": "text", - "lines": 142, + "lines": 124, "path": "docs/TEST_REPORT.md", - "sha256": "881bf725b8338853a82726d7627feedbfaefa21e4bd88e79590cb84247abeda1", - "size_bytes": 8779 + "sha256": "fce6543f37edcaa9fc390eeea85ecd2a70b084f5647ac6ecebd8ac740b15f769", + "size_bytes": 6066 }, { "kind": "text", @@ -700,128 +700,135 @@ "kind": "text", "lines": 68, "path": "documents/ADMIN_GUIDE.md", - "sha256": "dd61ab74c2b54cb6e637d5097c789a44c792feba592a591a5d1348a587625f4f", + "sha256": "9dea23e3e918f8357ea968891e18a5b0b3b24326515519bb1ced91c0395cf9c6", "size_bytes": 3855 }, { "kind": "text", - "lines": 44, + "lines": 65, "path": "documents/BASELINE.md", - "sha256": "5a8adbcc64d6ea48c11dbdb04f238caa51b621475b8df3993e4138717195b236", - "size_bytes": 2050 + "sha256": "377fc784d744848fc173219a1884b66e7282e81ac6da37fe25171625c1890108", + "size_bytes": 3408 }, { "kind": "text", - "lines": 209, + "lines": 221, "path": "documents/DOCUMENTATION_MANIFEST.json", - "sha256": "c6cc10f50dbc600cf1f08a49b473b8aa456367bd30b8b1d6cab2b84da8263fbb", - "size_bytes": 7880 + "sha256": "4979d782922f229efebb53163a67a02cc7749efd47cba70425045ba7b56cf59d", + "size_bytes": 8429 }, { "kind": "text", "lines": 76, "path": "documents/DOCUMENTATION_POLICY.md", - "sha256": "7786aef25b75254c38870b61329b9ca8ccdf367244ef8a97a0f13f692697879e", + "sha256": "bc0459b0bbc920759e50ba41a3786f866c43e900fe2ef0a1672f8b955b4c796f", "size_bytes": 3189 }, { "kind": "text", "lines": 76, "path": "documents/HOW_TO_USE.md", - "sha256": "0dd3e9d1d352ddb152180af3858afbec7f89b0aeb6b5a39881455e0ecd661655", + "sha256": "eafe95fd3d3246716c5ef320c12ee127df5cdd502e1c2080fd9b5cafee1b3dcc", "size_bytes": 3253 }, { "kind": "text", - "lines": 53, + "lines": 54, "path": "documents/README.md", - "sha256": "fd28dd001bd954ae9d635fe695beb799e6f40ce8990e977e5449c804352175b4", - "size_bytes": 3693 + "sha256": "aa67fe16b45310aa8e992e3d23b1b4cb1309fe32dfe72f3716dac5d7fb29995d", + "size_bytes": 3873 }, { "kind": "text", "lines": 82, "path": "documents/USER_MANUAL.md", - "sha256": "374ee5a683de0098b59dc39b25efdf6a04efdd7e33873517792160093f6fb6f2", + "sha256": "6d65ffe731c2f3725ce45705c49a438514aad60fe13a5e747812ff7d3082238e", "size_bytes": 4218 }, { "kind": "text", "lines": 46, "path": "documents/design/ACCESSIBILITY_SPECIFICATION.md", - "sha256": "8f31425f3cdec0165a4d0e6246ea68a770a898aed919ae01ac39da082326c11b", + "sha256": "aabae514afafe1186fa69f04fadd35423356cfe8d02922531e3713c7567b88d8", "size_bytes": 2187 }, { "kind": "text", "lines": 55, "path": "documents/design/COMPONENT_MATRIX.md", - "sha256": "b3c29ce563f1e794a22e60868dc47afd1efda6c20c27b56a05638e9288311b81", + "sha256": "b38667400ae202d75e2b95dd81bbdde8907b263cb8718eaa205c50878d91480f", "size_bytes": 3465 }, { "kind": "text", "lines": 52, "path": "documents/design/FUTURE_UI_ROADMAP.md", - "sha256": "c08173fb7454572a326b2aeccb3706531113fbd4c635239242e283630bc8d2e5", + "sha256": "7523c559d870025b1d1a9fd9127e603130bb96e87acfd2126fb6ee1d6025d1d6", "size_bytes": 1567 }, { "kind": "text", "lines": 48, "path": "documents/design/IMPLEMENTATION_STATUS.md", - "sha256": "61ed02ae43021e603a2fdc2b9cf0b91414a213b2bec14c4917b8cf0da44b6e02", - "size_bytes": 2516 + "sha256": "a0e25cf557e479a375e402b00e2518377e60572482d023031574da528d1775e4", + "size_bytes": 2518 }, { "kind": "text", "lines": 51, "path": "documents/design/RESPONSIVE_SPECIFICATION.md", - "sha256": "fcc959dd904bece5f2b481b5f5e77ec8af349136c3d750b56bc41ac1547e0280", + "sha256": "fb5168baac39e08c40c7ba30ad0c675e49c7a7be57fe1c6df0966f048bafa992", "size_bytes": 2027 }, { "kind": "text", "lines": 62, "path": "documents/design/SCREEN_CATALOG.md", - "sha256": "36db5fa837255ef66f98edc25cfeb599d704a079071338739e5b14965584f9f9", + "sha256": "54c7e6d838074cefc19b0626f84fcc3f965de42de8a04e7c6ac185a149d73b5c", "size_bytes": 3292 }, { "kind": "text", "lines": 89, "path": "documents/design/UI_FOUNDATION.md", - "sha256": "0b3184ec7e1f9133f0f80360698d2abf04ecf9cc9df6cdfb58350aa07d77ecc2", + "sha256": "c98a9d3d7f982a604392bebc4ebedb3bdbf92ac691638424032ac4cc4cb5bc48", "size_bytes": 3758 }, { "kind": "text", "lines": 59, "path": "documents/phases/PHASE-000-BASELINE.md", - "sha256": "6970ddf63b8c2aa45f1d6f8c62244f25b4aaf09f00922acfa83947a5184e0129", + "sha256": "9320a939456c39acf5a59c85d008010d205e3ebd6fc83ca07e0201b28e53693d", "size_bytes": 2543 }, { "kind": "text", "lines": 68, "path": "documents/phases/PHASE-001-UI-DESIGN-INTAKE-BASELINE.md", - "sha256": "204a4a66e2017081242d15b15d01481dddcde6cdcd269cda43f477e0d7ae0f01", + "sha256": "4e86d967000db5a21e870d762b220236de6139a67eedb25c4ea259328c891310", "size_bytes": 2770 }, { "kind": "text", "lines": 98, "path": "documents/phases/PHASE-002-SHARED-UI-FOUNDATION-AND-APPLICATION-SHELL.md", - "sha256": "450c51bf90048366e91615aecefb366247fa0b3a13a9eea7a1039a3ca3e4718d", + "sha256": "fff6f07ae9cb661d6689ad679d9c2be1ffa97229990b5690142f6cf9ee39d2b8", "size_bytes": 5493 }, { "kind": "text", "lines": 86, "path": "documents/phases/PHASE-003-INSTALLATION-RECOVERY-AND-INBOUND-DELIVERY-RELIABILITY.md", - "sha256": "b2e0b9e88d6410d20e916ea420b1151fb9d014dc253177dc5a71b2274b47b822", + "sha256": "e656dfe0d583d318fe803c89988cc636f8f60673f12e88ab865c90193027ec7c", "size_bytes": 5055 }, + { + "kind": "text", + "lines": 72, + "path": "documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md", + "sha256": "3bc0ea393a303e7a4a05b85c146539095a5d471d2d537da1a7d31290d74e7bbe", + "size_bytes": 3565 + }, { "kind": "text", "lines": 684, @@ -867,10 +874,10 @@ }, { "kind": "text", - "lines": 76, + "lines": 81, "path": "mailbox-app/CHANGELOG.md", - "sha256": "c95cbde106aec016141e31d445e4f0b0f091be7a8b66f894d47812ea98ad222e", - "size_bytes": 4864 + "sha256": "7268a8a324e36a8c3e6792db622891950e3a5fc225a0f0f3c0269f9cecf98e06", + "size_bytes": 5156 }, { "kind": "text", @@ -3161,7 +3168,7 @@ "kind": "text", "lines": 80, "path": "mailbox-app/pyproject.toml", - "sha256": "44bd9c76b1cd6a108c3d217871936ff428e55fa2876b41f2f72e34ed8a99b1cd", + "sha256": "6fe0affefedbb4b2ec29ea757236f2f2a55564c85409031da666fbc03334beea", "size_bytes": 2411 }, { @@ -5090,7 +5097,7 @@ "size_bytes": 6930 } ], - "release_version": "1.3.0-rc.4", + "release_version": "1.3.0-rc.5", "schema_version": 1, "scope": "All maintained repository files excluding generated archives, caches, build output and this inventory file itself.", "self_entry": { @@ -5099,15 +5106,15 @@ }, "summary": { "binary_files": 25, - "files": 403, + "files": 404, "python_classes": 73, "python_files": 144, "python_functions": 405, "python_methods": 61, "shell_files": 13, "shell_functions": 31, - "text_files": 378, - "total_bytes": 31655177, - "total_text_lines": 26603 + "text_files": 379, + "total_bytes": 31656307, + "total_text_lines": 26694 } } diff --git a/docs/PERFORMANCE_REVIEW.md b/docs/PERFORMANCE_REVIEW.md index a460a30..f390f2f 100644 --- a/docs/PERFORMANCE_REVIEW.md +++ b/docs/PERFORMANCE_REVIEW.md @@ -34,7 +34,7 @@ ## Performance gate result -No regression was observed in the local Windows suite; 195 tests passed, one capability-based symbolic-link case was skipped, and the suite completed in approximately 7.4 seconds with 94.99 percent coverage. No production load benchmark was executed. Before high-volume use, benchmark realistic message size, attachment mix, mailbox count, concurrency, disk latency, and MariaDB buffer settings on target hardware. +The historical PHASE-002 local Windows suite completed 195 tests with one capability-based symbolic-link skip and 94.99 percent coverage. The authoritative released RC4 Ubuntu CI suite later passed all 198 tests with 95.00 percent coverage. Neither result is a production load benchmark. Before high-volume use, benchmark realistic message size, attachment mix, mailbox count, concurrency, disk latency, and MariaDB buffer settings on target hardware. ## Release packaging diff --git a/docs/PUBLISHING.md b/docs/PUBLISHING.md index e163221..a67b4d5 100644 --- a/docs/PUBLISHING.md +++ b/docs/PUBLISHING.md @@ -12,10 +12,10 @@ This repository is prepared for public hosting on GitHub, GitLab, Codeberg, or a 4. Build and verify the deterministic release archive: ```bash - python scripts/build_release.py --root . --version 1.3.0-rc.4 + python scripts/build_release.py --root . --version 1.3.0-rc.5 python scripts/verify_release.py \ - dist/mailstack-1.3.0-rc.4-source.zip \ - --checksum dist/mailstack-1.3.0-rc.4-source.zip.sha256 + dist/mailstack-1.3.0-rc.5-source.zip \ + --checksum dist/mailstack-1.3.0-rc.5-source.zip.sha256 ``` 5. Complete the clean Ubuntu 24.04 acceptance checklist before promoting a release candidate to production-ready. @@ -27,7 +27,7 @@ From the audited repository root: ```bash git init git add . -git commit -m "Release MailStack 1.3.0-rc.4" +git commit -m "Prepare MailStack 1.3.0-rc.5 candidate" git branch -M main git remote add origin git push -u origin main @@ -36,14 +36,14 @@ git push -u origin main Create an annotated release-candidate tag only after CI passes: ```bash -git tag -a v1.3.0-rc.4 -m "MailStack 1.3.0 RC4" -git push origin v1.3.0-rc.4 +git tag -a v1.3.0-rc.5 -m "MailStack 1.3.0 RC5" +git push origin v1.3.0-rc.5 ``` Attach these files to the forge release: -- `mailstack-1.3.0-rc.4-source.zip` -- `mailstack-1.3.0-rc.4-source.zip.sha256` +- `mailstack-1.3.0-rc.5-source.zip` +- `mailstack-1.3.0-rc.5-source.zip.sha256` - `docs/RELEASE_NOTES_1.3.0.md` - `docs/FORENSIC_AUDIT_REPORT.md` diff --git a/docs/RELEASE_NOTES_1.3.0.md b/docs/RELEASE_NOTES_1.3.0.md index 97ac9c4..b4d1074 100644 --- a/docs/RELEASE_NOTES_1.3.0.md +++ b/docs/RELEASE_NOTES_1.3.0.md @@ -79,10 +79,17 @@ Postfix queue, ingested, and displayed by the application. ## Release qualification -RC4 must pass the full documentation, installer, operations, template, Ruff, Bandit, Django, -coverage, forensic, deterministic-release, and CI gates. Clean Ubuntu 24.04 installation and real -external SMTP/LMTP acceptance are required. Stable promotion remains blocked until backup/restore, -restart-recovery, final security/legal, and release-owner acceptance are complete. +RC4 passed the full documentation, installer, operations, template, dependency-audit, Ruff, Bandit, +Django, coverage, forensic, deterministic-release, and release-verification gates. Post-merge +`main` CI run `32071701530`, tag CI run `32072699991`, and release-artifact workflow `32072699830` +all completed successfully. The application suite passed 198 tests with 95.00 percent coverage, and +the deterministic source archive SHA-256 is +`58f06adea7c813e9861799d20e392441367bf64f6513d6e0634455d2011d4eac`. + +The PHASE-003 staging campaign validated real external SMTP-to-LMTP delivery after the accepted +fixes. An exact RC4 clean-host reinstall was not repeated before publication and remains deferred +until a fresh VPS is available. Stable promotion remains blocked on backup/restore acceptance, +restart/reboot recovery, final ownership/license review, and release-owner acceptance. ## RC1 foundation preserved diff --git a/docs/RELEASE_PROCESS.md b/docs/RELEASE_PROCESS.md index e0c962e..791ded9 100644 --- a/docs/RELEASE_PROCESS.md +++ b/docs/RELEASE_PROCESS.md @@ -7,7 +7,7 @@ 5. Run the online dependency audit in CI. 6. Test a clean installation on an isolated Ubuntu Server 24.04 LTS VPS. 7. Verify external inbound SMTP, login, authorization boundaries, live updates, safe HTML, and attachment downloads. -8. Build with `python scripts/build_release.py --root . --version 1.3.0-rc.4`. +8. Build with `python scripts/build_release.py --root . --version 1.3.0-rc.5`. 9. Verify the ZIP and checksum with `python scripts/verify_release.py`. 10. Publish the source ZIP, checksum, release notes, user-document manifest, and exact Git commit/tag. diff --git a/docs/TEST_REPORT.md b/docs/TEST_REPORT.md index 618e3b4..1cb3ba9 100644 --- a/docs/TEST_REPORT.md +++ b/docs/TEST_REPORT.md @@ -1,142 +1,124 @@ -# Test report — MailStack 1.3.0 RC3 +# Test report — MailStack 1.3.0 RC5 development baseline -**PHASE-003 verification date:** 2026-08-17 +**PHASE-004A verification date:** 2026-08-17 +**Latest published release candidate:** `v1.3.0-rc.4` +**Repository development version:** `1.3.0-rc.5` -## Last completed dependency-backed application suite (pre-RC3) +## Authoritative released-RC4 application suite + +The final released-RC4 qualification ran on Ubuntu 24.04 with Python 3.12 at commit +`896dbcc2ed1f38d9c618bf0b712efe5923f92e56`. ```text -Collected: 196 -Passed: 195 -Skipped: 1 — Windows symbolic-link capability unavailable +Collected: 198 +Passed: 198 Failed: 0 -Coverage: 94.99% +Coverage: 95.00% Required coverage: 85% ``` -RC3 adds one focused deployment-security regression contract that requires both -`requirements/locked.txt` and `requirements/constraints.txt` to pin `sqlparse==0.6.0` and reject -the vulnerable 0.5.5 pin. The full RC3 collection/pass count is intentionally not claimed until the -dependency-backed GitHub Actions rerun completes. - -Covered areas include authentication, user management, mailbox membership isolation, mailbox lifecycle, Postfix contract behavior, Maildir provisioning/ingestion, MIME parsing, HTML sanitization, attachment storage and authorization, duplicate handling, counters, live updates, reliability, security controls, responsive routes, deterministic user-document synchronization, draft blocking and feature-document policy enforcement. +The suite covers authentication, user management, mailbox membership isolation, mailbox lifecycle, +Postfix recipient contracts, Maildir provisioning/ingestion, MIME parsing, HTML sanitization, +attachment storage and authorization, duplicate handling, counters, live updates, reliability, +security controls, responsive routes, documentation synchronization, draft blocking, and feature +document policy enforcement. -## Additional automated gates +## RC4 authoritative workflow evidence -| Gate | Result | +| Workflow / gate | Result | |---|---| -| Mailbox application Ruff | PASS | -| Mailbox application Bandit with repository policy | PASS | -| Django system check (test settings) | PASS | -| Django production `check --deploy` | PASS | -| Migration drift | PASS — no changes detected | -| Contact service tests | PASS — deterministic SQLite close and temporary-file cleanup verified | -| Python compileall | PASS | -| Shell `bash -n` for all shell scripts | PASS | -| Deployment templates | PASS — 13 rendered | +| Post-merge `main` CI run `32071701530` | PASS | +| Tag CI run `32072699991` | PASS | +| Release-artifact workflow `32072699830` | PASS | +| Source safety audit | PASS | +| Documentation and metadata validation | PASS | +| Managed user-document synchronization | PASS | +| Documentation system tests | PASS | +| UI design intake integrity | PASS | +| UI design tests | PASS | +| Shared UI foundation contracts | PASS | +| Feature documentation policy | PASS | +| Forensic file inventory | PASS | +| Deployment template validation | PASS | | Installer contract | PASS — 2 valid, 9 invalid plans | -| Backup/restore/health contract | PASS | -| Nginx final configuration syntax | PASS | -| Nginx bootstrap configuration syntax | PASS | -| systemd unit syntax | PASS | -| `pip check` | PASS | -| Source safety scan | PASS after generated files are removed | -| Deterministic ZIP/manifest/checksum verification | PASS in GitHub CI | -| Documentation and forensic inventory gates | PASS | -| User-document synchronization and manifest gate | PASS | -| Documentation change-policy contract tests | PASS | -| UI design manifest synchronization and PNG integrity gate | PASS | -| UI design contract tests | PASS | -| Shared UI foundation dependency-free contracts | PASS — 8 tests | -| Shared shell Django functional tests | PASS — 7 focused tests | -| Standalone contact-service Ruff/Bandit | Enforced by the final verifier, full forensic gate, and CI | -| Locked dependency advisory audit | PENDING RC4 requalification — RC2 run `32053931714` found four sqlparse 0.5.5 vulnerabilities; RC3/RC4 pin 0.6.0 | -| Canonical stored-entry release packaging and ZIP metadata verification | PASS | - -## Runtime qualification - -The last fully completed pre-PHASE-003 dependency-backed qualification remains GitHub Actions run -`30133728843` on Ubuntu 24.04 with Python 3.12 at commit -`1e1737edea2e6c922265a15d8584b56671820c65`. That historical evidence does not qualify RC4. - -For the PHASE-003 branch, GitHub Actions run `32053931714` at commit -`7a800eb9f0b6d0e3fa347f4d7f05b5ad88a5e18f` passed source safety, documentation, design, -forensic inventory, deployment-template, installer, and operations gates. It then failed at the -blocking dependency vulnerability audit, so Ruff, Bandit, Django tests/coverage, Django checks, -full forensic audit, and deterministic release steps were correctly skipped. +| Backup/restore/health operations contracts | PASS | +| Dependency vulnerability audit | PASS — no known vulnerabilities reported | +| Ruff | PASS | +| Bandit | PASS | +| Django application tests and coverage | PASS — 198 / 198, 95.00% | +| Contact service tests | PASS | +| Contact service Ruff | PASS | +| Contact service Bandit | PASS | +| Django system checks | PASS | +| Migration drift | PASS — no changes detected | +| Shell syntax | PASS | +| Full forensic gate | PASS — zero blocking findings | +| Deterministic release build | PASS | +| Release verification | PASS | + +The deterministic RC4 release archive is `mailstack-1.3.0-rc.4-source.zip` with SHA-256 +`58f06adea7c813e9861799d20e392441367bf64f6513d6e0634455d2011d4eac`. Release verification reported +405 archive members and 404 manifest members. ## Dependency advisory qualification -The failed RC2 `pip-audit` gate reported four vulnerabilities in `sqlparse==0.5.5`: -CVE-2026-71491, CVE-2026-59894, CVE-2026-59893, and CVE-2026-54284. Upstream sqlparse 0.6.0 is the -security release containing fixes for those issues. RC3 introduced and RC4 preserves `sqlparse==0.6.0` in both -`requirements/locked.txt` and `requirements/constraints.txt`. No advisory suppression is added. -RC4 must pass `pip check`, the network-enabled `pip-audit` gate, and every subsequent CI stage before -qualification. - -## RC4 Windows Bash-runtime qualification - -A Windows CMD run of the RC3 candidate passed documentation, managed-document, design, shared UI, -forensic inventory, deployment-template, and `git diff --check` gates. `test_installer.py`, -`test_operations.py`, and the shell portions of `forensic_audit.py` then failed before executing any -MailStack shell logic because the generic `bash` executable resolved to the WSL launcher. WSL failed -to attach Docker Desktop's `ext4.vhdx` with `E_ACCESSDENIED`. The forensic report's 16 findings were -therefore cascading manifestations of one local shell-runtime failure, not 16 independent source -defects. - -RC4 centralizes Bash discovery in `scripts/shell_runtime.py`. Windows prefers a probed Git for -Windows Bash runtime, supports `BASH_EXECUTABLE`, and falls back only to candidates that pass a -non-mutating startup probe. Installer plans and shell syntax checks use repository-relative POSIX -paths so the same contracts remain valid on Linux and Git Bash. A follow-up Windows qualification -showed that Git Bash correctly launched but did not expose a `python3` command even though the host -provided `python`. The RC4 harness now supplies a process-local `BASH_ENV` bridge that maps -installer-only `python3` calls to the exact Python interpreter running the test process. The bridge -is regression-tested and does not alter the production installer, Ubuntu's native `python3` contract, -or the developer machine. +The earlier RC2 run `32053931714` stopped at the blocking advisory audit because +`sqlparse==0.5.5` was reported for CVE-2026-71491, CVE-2026-59894, CVE-2026-59893, and +CVE-2026-54284. RC3 introduced and RC4 preserved `sqlparse==0.6.0` in the production lock and +constraints. RC4 then passed the online dependency audit with no known vulnerabilities reported. +No advisory suppression was added for those four findings. + +## Historical Windows qualification context + +Before final RC4 CI, Windows validation exposed two host-tooling issues rather than application +regressions: generic `bash` resolved to a failing WSL launcher, and Git Bash did not expose a +`python3` command even though the host Python was available as `python`. RC4 centralized probed Bash +runtime discovery and added a process-local Windows audit bridge mapping installer-only `python3` +invocations to the exact harness interpreter. The Ubuntu production installer and its native +`python3` contract were not changed. + +The older PHASE-002 Windows dependency-backed suite recorded 195 passes, one capability-based +symbolic-link skip, and 94.99 percent coverage. Those figures remain historical local evidence; the +198-pass, 95.00-percent Ubuntu RC4 workflow is the current authoritative release result. ## PHASE-003 installation/recovery reliability qualification -Dependency-free local qualification for 1.3.0-rc.4 passes the documentation index/manifest gate, -documentation contract tests, design manifest and PNG integrity tests, shared UI foundation -contracts, deployment-template rendering, installer contracts, operations contracts, Python compile, -shell syntax, forensic inventory generation/check, and the structural forensic audit. The installer -contract specifically protects global `/var/log`, sanitized `vmail` command execution, provisioning -runtime directories, early credential persistence, explicit repair idempotency, the Dovecot static -userdb LMTP setting, and the existing MariaDB collation qualification. - -The current artifact-building environment does not contain the repository's pinned Django/Ruff/Bandit -dependencies and cannot download them, so the dependency-backed Django/coverage/lint/full-forensic -gates are delegated to the mandatory Ubuntu 24.04 GitHub Actions run for the PHASE-003 commit. This -does not waive those gates. - -The live staging campaign that motivated PHASE-003 demonstrated real external Gmail delivery through -Postfix and Dovecot LMTP into Maildir, queue drain after the static-userdb correction, ingestion, and -web-inbox visibility. A final clean installation from the exact RC4 source remains a release-candidate -acceptance requirement before stable promotion. - -## Manual acceptance still required - -On an isolated Ubuntu 24.04 VPS, verify installation, TLS issuance/renewal, external SMTP reception, Postfix lookup rejection for unknown/disabled recipients, Dovecot LMTP delivery, Maildir ingestion, login and authorization isolation, live updates, safe HTML, attachment downloads, contact delivery, backup, restore, and restart recovery. - -## PHASE-002 qualification status - -The shared shell passed eight dependency-free local contract tests covering required assets, frozen -tokens, responsive breakpoints, current-route-only navigation, template control-flow balance, SVG -integrity, preserved JavaScript runtime markers, and unsafe construct blocking. `node --check` also -passed for `mailbox-app/static/js/app.js`. - -Seven focused Django shell tests passed. The complete local Django suite collected 196 tests: 195 -passed and one symbolic-link test was skipped because the Windows test environment does not expose -the required capability. Coverage remained 94.99 percent. Ruff, Bandit, Django checks, migration -drift, contact-service behavior, dependency consistency, installer, operations, template, design, -documentation, and structural forensic gates passed in the recorded local workflow. - -The Windows cleanup failure was traced to Python's SQLite connection context semantics: transaction -context exit did not close the file handle. `_connection()` now owns and closes the handle in a -`finally` block, and the contact test explicitly verifies the closed state. The POSIX `0700` -Maildir assertion now remains active only on POSIX runtimes. Standalone contact-service Ruff and -Bandit checks are included in both the final verifier and GitHub Actions so this analysis scope -cannot regress silently. - -Final qualification still requires the PHASE-002 commit's GitHub Actions run. The last authoritative -pre-phase baseline remains GitHub Actions run `30165905840` at commit -`a4b5f40d85c0db1d278490af218f1a6040d40218`. +Installer contracts protect the global `/var/log` mode, sanitized `vmail` command execution, +provisioning runtime directories, immediate credential persistence, explicit repair idempotency, +the Dovecot static-userdb LMTP setting, and narrow MariaDB warning qualification. Ingestion tests +protect exclusive locking for real workers while allowing one-shot dry-run verification beside the +live worker without heartbeat mutation. + +The live staging campaign demonstrated real external Gmail delivery through Postfix and Dovecot +LMTP into Maildir, queue drain, ingestion, and browser visibility after the accepted source fixes. +An exact RC4 clean-host reinstall remains deferred until a fresh test VPS is available. + +## PHASE-004A verification scope + +PHASE-004A changes documentation, version/release metadata, and generated manifests only. The +assembled local candidate passed the following dependency-free/structural gates: + +| PHASE-004A local gate | Result | +|---|---| +| Documentation validation | PASS — 46 required files, 59 local links checked | +| Managed-document synchronization/check | PASS — 17 documents, 5 phases | +| Documentation tests | PASS — 4 contracts | +| Design manifest integrity | PASS — 25 source images | +| Design tests | PASS — 4 contracts | +| Shared UI foundation contracts | PASS — 8 contracts | +| Deployment template validation | PASS — 13 templates | +| Installer contracts | PASS — 2 valid, 9 invalid plans | +| Operations contracts | PASS — 4 scripts | +| Forensic inventory check | PASS — 404 maintained entries | +| Structural forensic audit | PASS — 405 files scanned, 144 Python, 13 shell, zero blocking findings | + +The final assembled delta is additionally checked with `git diff --check` before handoff. +Dependency-backed GitHub CI remains the final authority for the RC5 development candidate. Until +that branch workflow passes, PHASE-004A is locally qualified only and `1.3.0-rc.5` must not be +published as a release candidate. + +## Manual acceptance still outstanding before stable `1.3.0` + +A future isolated Ubuntu 24.04 acceptance campaign must cover the exact-source clean install, +TLS/DNS/mail routing, unknown-recipient rejection, real inbound delivery, authorization isolation, +contact delivery, backup, restore, restart/reboot recovery, and final release-owner/legal review. diff --git a/documents/ADMIN_GUIDE.md b/documents/ADMIN_GUIDE.md index c10c57c..7400140 100644 --- a/documents/ADMIN_GUIDE.md +++ b/documents/ADMIN_GUIDE.md @@ -4,7 +4,7 @@ title: MailStack Administrator Guide document_type: admin-guide audience: mailstack-administrators status: active -version: 1.3.0-rc.4 +version: 1.3.0-rc.5 last_reviewed: 2026-08-17 --- diff --git a/documents/BASELINE.md b/documents/BASELINE.md index bf71019..4624a12 100644 --- a/documents/BASELINE.md +++ b/documents/BASELINE.md @@ -4,26 +4,41 @@ title: MailStack Baseline document_type: baseline audience: maintainers-and-operators status: active -version: 1.3.0-rc.4 +version: 1.3.0-rc.5 last_reviewed: 2026-08-17 -baseline_id: MAILSTACK-1.3.0-RC1-DOCS-BASELINE-001 -source_commit: 1e1737edea2e6c922265a15d8584b56671820c65 +baseline_id: MAILSTACK-1.3.0-RC4-OFFICIAL-SOURCE-BASELINE-001 +source_commit: 896dbcc2ed1f38d9c618bf0b712efe5923f92e56 --- # MailStack baseline ## Baseline identity -This documentation baseline is anchored to MailStack `1.3.0-rc.1` and the CI-qualified, -clean-clone-qualified source commit `1e1737edea2e6c922265a15d8584b56671820c65`. The baseline -identifier is `MAILSTACK-1.3.0-RC1-DOCS-BASELINE-001`. +The current official source baseline is `MAILSTACK-1.3.0-RC4-OFFICIAL-SOURCE-BASELINE-001`. It is +anchored to published tag `v1.3.0-rc.4`, commit +`896dbcc2ed1f38d9c618bf0b712efe5923f92e56`, and Git tree +`0d845b3d975949894c24581e6834aff7b33c30b4`. The deterministic source archive is +`mailstack-1.3.0-rc.4-source.zip` with SHA-256 +`58f06adea7c813e9861799d20e392441367bf64f6513d6e0634455d2011d4eac`. + +The earlier `MAILSTACK-1.3.0-RC1-DOCS-BASELINE-001`, anchored to commit +`1e1737edea2e6c922265a15d8584b56671820c65`, remains historical documentation provenance. It no +longer represents the current canonical source anchor. The repository development version is +`1.3.0-rc.5`; that identity begins PHASE-004 and does not rewrite the frozen RC4 tag or archive. ## Qualification status -The baseline has passed source safety, dependency vulnerability, Ruff, Bandit, automated test, -coverage, Django, shell syntax, full forensic, deterministic release-build, release-verification, -and clean-clone gates. Clean Ubuntu 24.04 VPS installation, real inbound SMTP/LMTP delivery, -backup/restore acceptance, and stable release promotion remain pending. +The RC4 baseline passed source safety, documentation and design gates, installer and operations +contracts, dependency vulnerability audit, Ruff, Bandit, Django checks, migration-drift checks, 198 +application tests at 95.00 percent coverage, full forensic audit, deterministic release build, and +release verification. The post-merge `main` CI run `32071701530`, tag CI run `32072699991`, and +release-artifact workflow `32072699830` all completed successfully. + +PHASE-003 staging also demonstrated real external Gmail delivery through Postfix and Dovecot LMTP +into Maildir, ingestion, and browser visibility after the accepted fixes. An exact RC4 clean-host +reinstall is deferred until a fresh VPS is available. Backup/restore acceptance, restart/reboot +recovery, final ownership/license review, and stable promotion remain outstanding operational or +human gates. ## Preserved architecture @@ -32,13 +47,19 @@ shared inbox, MariaDB data contracts, Nginx and Gunicorn deployment, systemd ser contact service, backup/restore tooling, and established legacy runtime identifiers. It does not add SMTP submission, IMAP, POP3, public registration, outbound campaigns, or multi-node operation. +PHASE-004A changes documentation, version/release metadata, and generated manifests only. It does +not alter application runtime behavior, database schema, migrations, routes, permissions, UI, mail +flow, installer behavior, deployment templates, or service configuration. + ## Change control -The design-governance layer is anchored to commit `068097056cecdd18f39fd304d579563b7b43c491` through -`MAILSTACK-UI-DESIGN-INTAKE-001` and `MAILSTACK-UI-FOUNDATION-001`. This does not replace the runtime source anchor above. +The design-governance layer remains anchored to commit +`068097056cecdd18f39fd304d579563b7b43c491` through `MAILSTACK-UI-DESIGN-INTAKE-001` and +`MAILSTACK-UI-FOUNDATION-001`; it does not replace the current source anchor above. Every future maintained phase must add or update a phase record under `documents/phases/`, update the affected user-facing guide when behavior changes, update `CHANGELOG.md`, synchronize the -documentation index and manifest, and pass the documentation policy in CI. Existing features, -data contracts, authorization boundaries, and deployment compatibility remain protected unless a -separately approved migration explicitly changes them. +documentation/design/forensic manifests required by repository policy, and pass the blocking CI +gates. Existing features, data contracts, authorization boundaries, receive-only scope, and +deployment compatibility remain protected unless a separately approved migration explicitly +changes them. diff --git a/documents/DOCUMENTATION_MANIFEST.json b/documents/DOCUMENTATION_MANIFEST.json index 3f0686d..faeb5ad 100644 --- a/documents/DOCUMENTATION_MANIFEST.json +++ b/documents/DOCUMENTATION_MANIFEST.json @@ -1,6 +1,6 @@ { - "baseline_id": "MAILSTACK-1.3.0-RC1-DOCS-BASELINE-001", - "baseline_source_commit": "1e1737edea2e6c922265a15d8584b56671820c65", + "baseline_id": "MAILSTACK-1.3.0-RC4-OFFICIAL-SOURCE-BASELINE-001", + "baseline_source_commit": "896dbcc2ed1f38d9c618bf0b712efe5923f92e56", "documents": [ { "audience": "mailstack-administrators", @@ -9,10 +9,10 @@ "last_reviewed": "2026-08-17", "path": "documents/ADMIN_GUIDE.md", "phase_id": null, - "sha256": "dd61ab74c2b54cb6e637d5097c789a44c792feba592a591a5d1348a587625f4f", + "sha256": "9dea23e3e918f8357ea968891e18a5b0b3b24326515519bb1ced91c0395cf9c6", "status": "active", "title": "MailStack Administrator Guide", - "version": "1.3.0-rc.4" + "version": "1.3.0-rc.5" }, { "audience": "maintainers-and-operators", @@ -21,10 +21,10 @@ "last_reviewed": "2026-08-17", "path": "documents/BASELINE.md", "phase_id": null, - "sha256": "5a8adbcc64d6ea48c11dbdb04f238caa51b621475b8df3993e4138717195b236", + "sha256": "377fc784d744848fc173219a1884b66e7282e81ac6da37fe25171625c1890108", "status": "active", "title": "MailStack Baseline", - "version": "1.3.0-rc.4" + "version": "1.3.0-rc.5" }, { "audience": "contributors-and-maintainers", @@ -33,10 +33,10 @@ "last_reviewed": "2026-08-17", "path": "documents/DOCUMENTATION_POLICY.md", "phase_id": null, - "sha256": "7786aef25b75254c38870b61329b9ca8ccdf367244ef8a97a0f13f692697879e", + "sha256": "bc0459b0bbc920759e50ba41a3786f866c43e900fe2ef0a1672f8b955b4c796f", "status": "active", "title": "Documentation Policy", - "version": "1.3.0-rc.4" + "version": "1.3.0-rc.5" }, { "audience": "mailbox-users-and-administrators", @@ -45,10 +45,10 @@ "last_reviewed": "2026-08-17", "path": "documents/HOW_TO_USE.md", "phase_id": null, - "sha256": "0dd3e9d1d352ddb152180af3858afbec7f89b0aeb6b5a39881455e0ecd661655", + "sha256": "eafe95fd3d3246716c5ef320c12ee127df5cdd502e1c2080fd9b5cafee1b3dcc", "status": "active", "title": "How to Use MailStack", - "version": "1.3.0-rc.4" + "version": "1.3.0-rc.5" }, { "audience": "mailbox-users", @@ -57,10 +57,10 @@ "last_reviewed": "2026-08-17", "path": "documents/USER_MANUAL.md", "phase_id": null, - "sha256": "374ee5a683de0098b59dc39b25efdf6a04efdd7e33873517792160093f6fb6f2", + "sha256": "6d65ffe731c2f3725ce45705c49a438514aad60fe13a5e747812ff7d3082238e", "status": "active", "title": "MailStack User Manual", - "version": "1.3.0-rc.4" + "version": "1.3.0-rc.5" }, { "audience": "designers-developers-and-maintainers", @@ -69,10 +69,10 @@ "last_reviewed": "2026-08-17", "path": "documents/design/ACCESSIBILITY_SPECIFICATION.md", "phase_id": null, - "sha256": "8f31425f3cdec0165a4d0e6246ea68a770a898aed919ae01ac39da082326c11b", + "sha256": "aabae514afafe1186fa69f04fadd35423356cfe8d02922531e3713c7567b88d8", "status": "active", "title": "MailStack Accessibility Specification", - "version": "1.3.0-rc.4" + "version": "1.3.0-rc.5" }, { "audience": "designers-developers-and-maintainers", @@ -81,10 +81,10 @@ "last_reviewed": "2026-08-17", "path": "documents/design/COMPONENT_MATRIX.md", "phase_id": null, - "sha256": "b3c29ce563f1e794a22e60868dc47afd1efda6c20c27b56a05638e9288311b81", + "sha256": "b38667400ae202d75e2b95dd81bbdde8907b263cb8718eaa205c50878d91480f", "status": "active", "title": "MailStack UI Component Matrix", - "version": "1.3.0-rc.4" + "version": "1.3.0-rc.5" }, { "audience": "designers-developers-and-maintainers", @@ -93,10 +93,10 @@ "last_reviewed": "2026-08-17", "path": "documents/design/FUTURE_UI_ROADMAP.md", "phase_id": null, - "sha256": "c08173fb7454572a326b2aeccb3706531113fbd4c635239242e283630bc8d2e5", + "sha256": "7523c559d870025b1d1a9fd9127e603130bb96e87acfd2126fb6ee1d6025d1d6", "status": "active", "title": "MailStack Future UI Roadmap", - "version": "1.3.0-rc.4" + "version": "1.3.0-rc.5" }, { "audience": "designers-developers-and-maintainers", @@ -105,10 +105,10 @@ "last_reviewed": "2026-08-17", "path": "documents/design/IMPLEMENTATION_STATUS.md", "phase_id": null, - "sha256": "61ed02ae43021e603a2fdc2b9cf0b91414a213b2bec14c4917b8cf0da44b6e02", + "sha256": "a0e25cf557e479a375e402b00e2518377e60572482d023031574da528d1775e4", "status": "active", "title": "MailStack UI Implementation Status", - "version": "1.3.0-rc.4" + "version": "1.3.0-rc.5" }, { "audience": "designers-developers-and-maintainers", @@ -117,10 +117,10 @@ "last_reviewed": "2026-08-17", "path": "documents/design/RESPONSIVE_SPECIFICATION.md", "phase_id": null, - "sha256": "fcc959dd904bece5f2b481b5f5e77ec8af349136c3d750b56bc41ac1547e0280", + "sha256": "fb5168baac39e08c40c7ba30ad0c675e49c7a7be57fe1c6df0966f048bafa992", "status": "active", "title": "MailStack Responsive Specification", - "version": "1.3.0-rc.4" + "version": "1.3.0-rc.5" }, { "audience": "designers-developers-and-maintainers", @@ -129,10 +129,10 @@ "last_reviewed": "2026-08-17", "path": "documents/design/SCREEN_CATALOG.md", "phase_id": null, - "sha256": "36db5fa837255ef66f98edc25cfeb599d704a079071338739e5b14965584f9f9", + "sha256": "54c7e6d838074cefc19b0626f84fcc3f965de42de8a04e7c6ac185a149d73b5c", "status": "active", "title": "MailStack UI Screen Catalog", - "version": "1.3.0-rc.4" + "version": "1.3.0-rc.5" }, { "audience": "designers-developers-and-maintainers", @@ -141,10 +141,10 @@ "last_reviewed": "2026-08-17", "path": "documents/design/UI_FOUNDATION.md", "phase_id": null, - "sha256": "0b3184ec7e1f9133f0f80360698d2abf04ecf9cc9df6cdfb58350aa07d77ecc2", + "sha256": "c98a9d3d7f982a604392bebc4ebedb3bdbf92ac691638424032ac4cc4cb5bc48", "status": "active", "title": "MailStack UI Foundation", - "version": "1.3.0-rc.4" + "version": "1.3.0-rc.5" }, { "audience": "users-operators-and-maintainers", @@ -153,10 +153,10 @@ "last_reviewed": "2026-08-17", "path": "documents/phases/PHASE-000-BASELINE.md", "phase_id": "PHASE-000", - "sha256": "6970ddf63b8c2aa45f1d6f8c62244f25b4aaf09f00922acfa83947a5184e0129", + "sha256": "9320a939456c39acf5a59c85d008010d205e3ebd6fc83ca07e0201b28e53693d", "status": "active", "title": "Documentation and Feature Baseline", - "version": "1.3.0-rc.4" + "version": "1.3.0-rc.5" }, { "audience": "users-operators-designers-and-maintainers", @@ -165,10 +165,10 @@ "last_reviewed": "2026-08-17", "path": "documents/phases/PHASE-001-UI-DESIGN-INTAKE-BASELINE.md", "phase_id": "PHASE-001", - "sha256": "204a4a66e2017081242d15b15d01481dddcde6cdcd269cda43f477e0d7ae0f01", + "sha256": "4e86d967000db5a21e870d762b220236de6139a67eedb25c4ea259328c891310", "status": "active", "title": "UI Design Intake Baseline", - "version": "1.3.0-rc.4" + "version": "1.3.0-rc.5" }, { "audience": "users-operators-designers-and-maintainers", @@ -177,10 +177,10 @@ "last_reviewed": "2026-08-17", "path": "documents/phases/PHASE-002-SHARED-UI-FOUNDATION-AND-APPLICATION-SHELL.md", "phase_id": "PHASE-002", - "sha256": "450c51bf90048366e91615aecefb366247fa0b3a13a9eea7a1039a3ca3e4718d", + "sha256": "fff6f07ae9cb661d6689ad679d9c2be1ffa97229990b5690142f6cf9ee39d2b8", "status": "active", "title": "Shared UI Foundation and Application Shell", - "version": "1.3.0-rc.4" + "version": "1.3.0-rc.5" }, { "audience": "users-operators-and-maintainers", @@ -189,21 +189,33 @@ "last_reviewed": "2026-08-17", "path": "documents/phases/PHASE-003-INSTALLATION-RECOVERY-AND-INBOUND-DELIVERY-RELIABILITY.md", "phase_id": "PHASE-003", - "sha256": "b2e0b9e88d6410d20e916ea420b1151fb9d014dc253177dc5a71b2274b47b822", + "sha256": "e656dfe0d583d318fe803c89988cc636f8f60673f12e88ab865c90193027ec7c", "status": "active", "title": "Installation, Recovery and Inbound Delivery Reliability", - "version": "1.3.0-rc.4" + "version": "1.3.0-rc.5" + }, + { + "audience": "users-operators-and-maintainers", + "document_id": "phase-004-release-upgrade-and-operational-reliability", + "document_type": "phase", + "last_reviewed": "2026-08-17", + "path": "documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md", + "phase_id": "PHASE-004", + "sha256": "3bc0ea393a303e7a4a05b85c146539095a5d471d2d537da1a7d31290d74e7bbe", + "status": "active", + "title": "Release, Upgrade and Operational Reliability", + "version": "1.3.0-rc.5" } ], "project": "MailStack", - "release_version": "1.3.0-rc.4", + "release_version": "1.3.0-rc.5", "schema_version": 1, "scope": "Managed user, administrator, how-to, policy, baseline, and phase Markdown documents.", "summary": { - "active": 16, - "documents": 16, + "active": 17, + "documents": 17, "draft": 0, "historical": 0, - "phases": 4 + "phases": 5 } } diff --git a/documents/DOCUMENTATION_POLICY.md b/documents/DOCUMENTATION_POLICY.md index 577ccf6..57d9f52 100644 --- a/documents/DOCUMENTATION_POLICY.md +++ b/documents/DOCUMENTATION_POLICY.md @@ -4,7 +4,7 @@ title: Documentation Policy document_type: documentation-policy audience: contributors-and-maintainers status: active -version: 1.3.0-rc.4 +version: 1.3.0-rc.5 last_reviewed: 2026-08-17 --- diff --git a/documents/HOW_TO_USE.md b/documents/HOW_TO_USE.md index c7f8cee..6e4106c 100644 --- a/documents/HOW_TO_USE.md +++ b/documents/HOW_TO_USE.md @@ -4,7 +4,7 @@ title: How to Use MailStack document_type: how-to audience: mailbox-users-and-administrators status: active -version: 1.3.0-rc.4 +version: 1.3.0-rc.5 last_reviewed: 2026-08-17 --- diff --git a/documents/README.md b/documents/README.md index 0e3747e..0f5e52f 100644 --- a/documents/README.md +++ b/documents/README.md @@ -8,7 +8,7 @@ relevant user manual, how-to guide, or administrator guide. ```bash python scripts/manage_documents.py new-phase \ - --phase-id PHASE-004 \ + --phase-id PHASE-005 \ --title "Feature title" \ --summary "What the phase changes for users" @@ -27,22 +27,23 @@ required documentation update. | Document | Type | Audience | Status | Version | |---|---|---|---|---| -| [MailStack Administrator Guide](ADMIN_GUIDE.md) | Admin Guide | mailstack-administrators | active | 1.3.0-rc.4 | -| [MailStack Baseline](BASELINE.md) | Baseline | maintainers-and-operators | active | 1.3.0-rc.4 | -| [MailStack Accessibility Specification](design/ACCESSIBILITY_SPECIFICATION.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.4 | -| [MailStack Future UI Roadmap](design/FUTURE_UI_ROADMAP.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.4 | -| [MailStack Responsive Specification](design/RESPONSIVE_SPECIFICATION.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.4 | -| [MailStack UI Component Matrix](design/COMPONENT_MATRIX.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.4 | -| [MailStack UI Foundation](design/UI_FOUNDATION.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.4 | -| [MailStack UI Implementation Status](design/IMPLEMENTATION_STATUS.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.4 | -| [MailStack UI Screen Catalog](design/SCREEN_CATALOG.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.4 | -| [Documentation Policy](DOCUMENTATION_POLICY.md) | Documentation Policy | contributors-and-maintainers | active | 1.3.0-rc.4 | -| [How to Use MailStack](HOW_TO_USE.md) | How To | mailbox-users-and-administrators | active | 1.3.0-rc.4 | -| [Documentation and Feature Baseline](phases/PHASE-000-BASELINE.md) | Phase | users-operators-and-maintainers | active | 1.3.0-rc.4 | -| [Installation, Recovery and Inbound Delivery Reliability](phases/PHASE-003-INSTALLATION-RECOVERY-AND-INBOUND-DELIVERY-RELIABILITY.md) | Phase | users-operators-and-maintainers | active | 1.3.0-rc.4 | -| [Shared UI Foundation and Application Shell](phases/PHASE-002-SHARED-UI-FOUNDATION-AND-APPLICATION-SHELL.md) | Phase | users-operators-designers-and-maintainers | active | 1.3.0-rc.4 | -| [UI Design Intake Baseline](phases/PHASE-001-UI-DESIGN-INTAKE-BASELINE.md) | Phase | users-operators-designers-and-maintainers | active | 1.3.0-rc.4 | -| [MailStack User Manual](USER_MANUAL.md) | User Manual | mailbox-users | active | 1.3.0-rc.4 | +| [MailStack Administrator Guide](ADMIN_GUIDE.md) | Admin Guide | mailstack-administrators | active | 1.3.0-rc.5 | +| [MailStack Baseline](BASELINE.md) | Baseline | maintainers-and-operators | active | 1.3.0-rc.5 | +| [MailStack Accessibility Specification](design/ACCESSIBILITY_SPECIFICATION.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.5 | +| [MailStack Future UI Roadmap](design/FUTURE_UI_ROADMAP.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.5 | +| [MailStack Responsive Specification](design/RESPONSIVE_SPECIFICATION.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.5 | +| [MailStack UI Component Matrix](design/COMPONENT_MATRIX.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.5 | +| [MailStack UI Foundation](design/UI_FOUNDATION.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.5 | +| [MailStack UI Implementation Status](design/IMPLEMENTATION_STATUS.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.5 | +| [MailStack UI Screen Catalog](design/SCREEN_CATALOG.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.5 | +| [Documentation Policy](DOCUMENTATION_POLICY.md) | Documentation Policy | contributors-and-maintainers | active | 1.3.0-rc.5 | +| [How to Use MailStack](HOW_TO_USE.md) | How To | mailbox-users-and-administrators | active | 1.3.0-rc.5 | +| [Documentation and Feature Baseline](phases/PHASE-000-BASELINE.md) | Phase | users-operators-and-maintainers | active | 1.3.0-rc.5 | +| [Installation, Recovery and Inbound Delivery Reliability](phases/PHASE-003-INSTALLATION-RECOVERY-AND-INBOUND-DELIVERY-RELIABILITY.md) | Phase | users-operators-and-maintainers | active | 1.3.0-rc.5 | +| [Release, Upgrade and Operational Reliability](phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md) | Phase | users-operators-and-maintainers | active | 1.3.0-rc.5 | +| [Shared UI Foundation and Application Shell](phases/PHASE-002-SHARED-UI-FOUNDATION-AND-APPLICATION-SHELL.md) | Phase | users-operators-designers-and-maintainers | active | 1.3.0-rc.5 | +| [UI Design Intake Baseline](phases/PHASE-001-UI-DESIGN-INTAKE-BASELINE.md) | Phase | users-operators-designers-and-maintainers | active | 1.3.0-rc.5 | +| [MailStack User Manual](USER_MANUAL.md) | User Manual | mailbox-users | active | 1.3.0-rc.5 | ## Scope boundary diff --git a/documents/USER_MANUAL.md b/documents/USER_MANUAL.md index 673f044..f6180f5 100644 --- a/documents/USER_MANUAL.md +++ b/documents/USER_MANUAL.md @@ -4,7 +4,7 @@ title: MailStack User Manual document_type: user-manual audience: mailbox-users status: active -version: 1.3.0-rc.4 +version: 1.3.0-rc.5 last_reviewed: 2026-08-17 --- diff --git a/documents/design/ACCESSIBILITY_SPECIFICATION.md b/documents/design/ACCESSIBILITY_SPECIFICATION.md index 20ea3a6..0426bd1 100644 --- a/documents/design/ACCESSIBILITY_SPECIFICATION.md +++ b/documents/design/ACCESSIBILITY_SPECIFICATION.md @@ -4,7 +4,7 @@ title: MailStack Accessibility Specification document_type: design-reference audience: designers-developers-and-maintainers status: active -version: 1.3.0-rc.4 +version: 1.3.0-rc.5 last_reviewed: 2026-08-17 --- diff --git a/documents/design/COMPONENT_MATRIX.md b/documents/design/COMPONENT_MATRIX.md index 4298da5..1704ad3 100644 --- a/documents/design/COMPONENT_MATRIX.md +++ b/documents/design/COMPONENT_MATRIX.md @@ -4,7 +4,7 @@ title: MailStack UI Component Matrix document_type: design-reference audience: designers-developers-and-maintainers status: active -version: 1.3.0-rc.4 +version: 1.3.0-rc.5 last_reviewed: 2026-08-17 --- diff --git a/documents/design/FUTURE_UI_ROADMAP.md b/documents/design/FUTURE_UI_ROADMAP.md index e0a83a7..3946d17 100644 --- a/documents/design/FUTURE_UI_ROADMAP.md +++ b/documents/design/FUTURE_UI_ROADMAP.md @@ -4,7 +4,7 @@ title: MailStack Future UI Roadmap document_type: design-reference audience: designers-developers-and-maintainers status: active -version: 1.3.0-rc.4 +version: 1.3.0-rc.5 last_reviewed: 2026-08-17 --- diff --git a/documents/design/IMPLEMENTATION_STATUS.md b/documents/design/IMPLEMENTATION_STATUS.md index ae75755..1e54a22 100644 --- a/documents/design/IMPLEMENTATION_STATUS.md +++ b/documents/design/IMPLEMENTATION_STATUS.md @@ -4,7 +4,7 @@ title: MailStack UI Implementation Status document_type: design-reference audience: designers-developers-and-maintainers status: active -version: 1.3.0-rc.4 +version: 1.3.0-rc.5 last_reviewed: 2026-08-17 --- @@ -33,7 +33,7 @@ phase records, CI evidence, or staging acceptance. | Unauthenticated sign-in shell | Implemented locally | Local branding and private-navigation isolation | | Local logo and icon assets | Implemented locally | Canonical logo copy and validated SVG sprite | | Responsive shell | Implemented locally | Desktop collapse, tablet/mobile drawer, compact mobile shell | -| Dependency-backed local qualification | Verified locally | 195 passed, 1 capability-based skip, 94.99% coverage; CI pending commit | +| Released RC4 dependency-backed qualification | Verified in GitHub CI | 198 passed, 0 failed, 95.00% coverage; main and tag CI passed | | Existing page redesign | Not started | One page per subsequent patch | | Secure message-reader redesign | Not started | Dedicated security-focused phase required | | Future feature implementation | Not started | Separate architecture phases required | diff --git a/documents/design/RESPONSIVE_SPECIFICATION.md b/documents/design/RESPONSIVE_SPECIFICATION.md index c87b628..437a204 100644 --- a/documents/design/RESPONSIVE_SPECIFICATION.md +++ b/documents/design/RESPONSIVE_SPECIFICATION.md @@ -4,7 +4,7 @@ title: MailStack Responsive Specification document_type: design-reference audience: designers-developers-and-maintainers status: active -version: 1.3.0-rc.4 +version: 1.3.0-rc.5 last_reviewed: 2026-08-17 --- diff --git a/documents/design/SCREEN_CATALOG.md b/documents/design/SCREEN_CATALOG.md index f698e33..65cbbb9 100644 --- a/documents/design/SCREEN_CATALOG.md +++ b/documents/design/SCREEN_CATALOG.md @@ -4,7 +4,7 @@ title: MailStack UI Screen Catalog document_type: design-reference audience: designers-developers-and-maintainers status: active -version: 1.3.0-rc.4 +version: 1.3.0-rc.5 last_reviewed: 2026-08-17 --- diff --git a/documents/design/UI_FOUNDATION.md b/documents/design/UI_FOUNDATION.md index c5f31c0..c9711e0 100644 --- a/documents/design/UI_FOUNDATION.md +++ b/documents/design/UI_FOUNDATION.md @@ -4,7 +4,7 @@ title: MailStack UI Foundation document_type: design-reference audience: designers-developers-and-maintainers status: active -version: 1.3.0-rc.4 +version: 1.3.0-rc.5 last_reviewed: 2026-08-17 --- diff --git a/documents/phases/PHASE-000-BASELINE.md b/documents/phases/PHASE-000-BASELINE.md index e790b28..b9f00ff 100644 --- a/documents/phases/PHASE-000-BASELINE.md +++ b/documents/phases/PHASE-000-BASELINE.md @@ -4,7 +4,7 @@ title: Documentation and Feature Baseline document_type: phase audience: users-operators-and-maintainers status: active -version: 1.3.0-rc.4 +version: 1.3.0-rc.5 last_reviewed: 2026-08-17 phase_id: PHASE-000 --- diff --git a/documents/phases/PHASE-001-UI-DESIGN-INTAKE-BASELINE.md b/documents/phases/PHASE-001-UI-DESIGN-INTAKE-BASELINE.md index 411eecf..51de382 100644 --- a/documents/phases/PHASE-001-UI-DESIGN-INTAKE-BASELINE.md +++ b/documents/phases/PHASE-001-UI-DESIGN-INTAKE-BASELINE.md @@ -4,7 +4,7 @@ title: UI Design Intake Baseline document_type: phase audience: users-operators-designers-and-maintainers status: active -version: 1.3.0-rc.4 +version: 1.3.0-rc.5 last_reviewed: 2026-08-17 phase_id: PHASE-001 --- diff --git a/documents/phases/PHASE-002-SHARED-UI-FOUNDATION-AND-APPLICATION-SHELL.md b/documents/phases/PHASE-002-SHARED-UI-FOUNDATION-AND-APPLICATION-SHELL.md index e7320b4..1898ff1 100644 --- a/documents/phases/PHASE-002-SHARED-UI-FOUNDATION-AND-APPLICATION-SHELL.md +++ b/documents/phases/PHASE-002-SHARED-UI-FOUNDATION-AND-APPLICATION-SHELL.md @@ -4,7 +4,7 @@ title: Shared UI Foundation and Application Shell document_type: phase audience: users-operators-designers-and-maintainers status: active -version: 1.3.0-rc.4 +version: 1.3.0-rc.5 last_reviewed: 2026-08-17 phase_id: PHASE-002 --- diff --git a/documents/phases/PHASE-003-INSTALLATION-RECOVERY-AND-INBOUND-DELIVERY-RELIABILITY.md b/documents/phases/PHASE-003-INSTALLATION-RECOVERY-AND-INBOUND-DELIVERY-RELIABILITY.md index 3e1428c..ca2a24a 100644 --- a/documents/phases/PHASE-003-INSTALLATION-RECOVERY-AND-INBOUND-DELIVERY-RELIABILITY.md +++ b/documents/phases/PHASE-003-INSTALLATION-RECOVERY-AND-INBOUND-DELIVERY-RELIABILITY.md @@ -4,7 +4,7 @@ title: Installation, Recovery and Inbound Delivery Reliability document_type: phase audience: users-operators-and-maintainers status: active -version: 1.3.0-rc.4 +version: 1.3.0-rc.5 last_reviewed: 2026-08-17 phase_id: PHASE-003 --- diff --git a/documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md b/documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md new file mode 100644 index 0000000..85098bf --- /dev/null +++ b/documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md @@ -0,0 +1,72 @@ +--- +document_id: phase-004-release-upgrade-and-operational-reliability +title: Release, Upgrade and Operational Reliability +document_type: phase +audience: users-operators-and-maintainers +status: active +version: 1.3.0-rc.5 +last_reviewed: 2026-08-17 +phase_id: PHASE-004 +--- + +# PHASE-004: Release, upgrade and operational reliability + +## Objective + +Make post-RC4 release handling, upgrades, rollback, and operational acceptance reproducible without +weakening the receive-only architecture or disturbing an already working MailStack deployment. +PHASE-004A finalizes the forensic/documentation baseline before later PHASE-004 subphases change +release automation or upgrade tooling. + +## Scope + +PHASE-004A is restricted to forensic evidence finalization, baseline identity, version/release +metadata synchronization, this phase record, and deterministic documentation/design/forensic +manifests. It records the completed RC4 qualification using the successful `main`, tag-CI, and +release-artifact workflows and replaces stale pre-RC4-publish wording. + +Later approved PHASE-004 subphases may address automatic tag-to-GitHub-Release publication, a +controlled existing-server upgrade/rollback mechanism, and operational backup/restore and reboot +acceptance. Those runtime/tooling changes are not implemented by PHASE-004A. + +## User-facing changes + +There is no application UI or mailbox behavior change in PHASE-004A. Maintainers and operators gain +a single explicit official source baseline for RC4 and corrected evidence that distinguishes +published RC4 qualification from the new RC5 development identity. + +## How to use + +Treat `mailstack-1.3.0-rc.4-source.zip`, SHA-256 +`58f06adea7c813e9861799d20e392441367bf64f6513d6e0634455d2011d4eac`, tag `v1.3.0-rc.4`, and commit +`896dbcc2ed1f38d9c618bf0b712efe5923f92e56` as the immutable official source baseline. Start +PHASE-004 work from that source identity. The working repository version is `1.3.0-rc.5`; do not +retag or rewrite RC4. + +## Compatibility + +PHASE-004A introduces no model, migration, URL, permission, template, CSS, JavaScript, mail-flow, +ingestion, installer, deployment-template, service, database, DNS, TLS, or existing-VPS change. +The `v1.3.0-rc.4` tag and deterministic RC4 source archive remain unchanged. There is no data or +configuration migration and no rollback operation is required for this documentation-only delta. + +## Verification + +The RC4 evidence recorded by this phase is anchored to `main` CI run `32071701530`, tag CI run +`32072699991`, and release-artifact workflow `32072699830`. RC4 passed 198 tests with 95.00 percent +coverage, the dependency vulnerability audit, Ruff, Bandit, installer and operations contracts, +full forensic audit with zero blocking findings, deterministic source build, and release +verification. + +PHASE-004A itself must pass documentation synchronization/tests, design-manifest integrity, +forensic-inventory regeneration/check, template validation, installer/operations regression +contracts, `git diff --check`, structural forensic audit, and final GitHub CI before it is considered +fully qualified. + +## Documentation impact + +This subphase updates the root and application changelogs, current build/release examples, README +status wording, dependency/performance evidence, forensic and test reports, the canonical baseline, +UI implementation-status evidence, active managed-document version metadata, generated +documentation/design/forensic manifests, and this PHASE-004 record. No user workflow guide requires +behavioral changes because application behavior is unchanged. diff --git a/mailbox-app/CHANGELOG.md b/mailbox-app/CHANGELOG.md index c7a5ba6..6f8e064 100644 --- a/mailbox-app/CHANGELOG.md +++ b/mailbox-app/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 1.3.0-rc.5 — Forensic/docs baseline finalization (unreleased) + +- Finalized RC4 qualification evidence and the official source-baseline record for PHASE-004A. +- No application model, migration, route, UI, mailbox, ingestion, LMTP, authorization, dependency, or runtime behavior changed. + ## 1.3.0-rc.4 — Cross-platform audit tooling maintenance - Added portable Bash runtime discovery for repository-level installer, operations, and forensic audit tooling on Windows while preserving Linux CI behavior. diff --git a/mailbox-app/pyproject.toml b/mailbox-app/pyproject.toml index 73ec3cd..b3b3266 100644 --- a/mailbox-app/pyproject.toml +++ b/mailbox-app/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "mailstack" -version = "1.3.0rc4" +version = "1.3.0rc5" description = "MailStack self-hosted receive-only mail server and shared team inbox" authors = [{ name = "Vib Tools" }] maintainers = [{ name = "Vib Tools" }] From ee90764335f8724727cea86e0af035c049c79e62 Mon Sep 17 00:00:00 2001 From: Md Nurnobi Date: Mon, 17 Aug 2026 19:52:47 -0700 Subject: [PATCH 2/4] ci(release): automate verified GitHub publication --- .github/workflows/ci.yml | 3 + .github/workflows/release.yml | 92 +++++- CHANGELOG.md | 9 +- docs/BUILD.md | 10 + docs/FORENSIC_AUDIT_REPORT.md | 25 +- docs/FORENSIC_FILE_INVENTORY.json | 157 +++++++--- docs/PUBLISHING.md | 14 +- docs/RELEASE_NOTES_1.3.0.md | 10 + docs/RELEASE_PROCESS.md | 10 +- docs/TEST_REPORT.md | 22 +- documents/DOCUMENTATION_MANIFEST.json | 2 +- ...ASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md | 69 +++-- scripts/forensic_audit.py | 3 + scripts/release_gate.py | 281 ++++++++++++++++++ scripts/test_release_workflow.py | 223 ++++++++++++++ 15 files changed, 844 insertions(+), 86 deletions(-) create mode 100644 scripts/release_gate.py create mode 100644 scripts/test_release_workflow.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7ebd81..06c52dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,6 +79,9 @@ jobs: - name: Backup and operations contract tests run: python scripts/test_operations.py + - name: Release workflow contract tests + run: python scripts/test_release_workflow.py + - name: Dependency vulnerability audit run: python -m pip_audit --no-deps -r mailbox-app/requirements/locked.txt --progress-spinner off diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0aeaf66..988528f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,15 +6,22 @@ name: Release artifacts tags: - "v*" -permissions: - contents: read +permissions: {} jobs: build-verified-source: runs-on: ubuntu-24.04 timeout-minutes: 30 + permissions: + contents: read + outputs: + version: ${{ steps.identity.outputs.version }} + tag: ${{ steps.identity.outputs.tag }} + prerelease: ${{ steps.identity.outputs.prerelease }} steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - uses: actions/setup-python@v5 with: python-version: "3.12" @@ -25,7 +32,21 @@ jobs: sudo apt-get update sudo apt-get install -y --no-install-recommends default-libmysqlclient-dev build-essential pkg-config - name: Development dependencies - run: python -m pip install --disable-pip-version-check -r mailbox-app/requirements/development.txt + run: | + python -m pip install --disable-pip-version-check -r mailbox-app/requirements/development.txt + python -m pip check + - name: Validate release identity + id: identity + run: | + python scripts/release_gate.py \ + --root . \ + --event-name "$GITHUB_EVENT_NAME" \ + --ref-type "$GITHUB_REF_TYPE" \ + --ref-name "$GITHUB_REF_NAME" \ + --sha "$GITHUB_SHA" \ + --github-output "$GITHUB_OUTPUT" + - name: Release workflow contract tests + run: python scripts/test_release_workflow.py - name: Full forensic gate run: python scripts/forensic_audit.py --root . --full - name: Build source archive @@ -38,10 +59,73 @@ jobs: - name: Upload verified artifacts uses: actions/upload-artifact@v4 with: - name: mailstack-source-${{ github.ref_name }} + name: mailstack-source-${{ github.run_id }} path: | dist/*.zip dist/*.sha256 docs/RELEASE_NOTES_1.3.0.md docs/FORENSIC_AUDIT_REPORT.md if-no-files-found: error + + publish-github-release: + if: github.event_name == 'push' && github.ref_type == 'tag' + needs: build-verified-source + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + actions: read + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Prove publication eligibility + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + python scripts/release_gate.py \ + --root . \ + --event-name "$GITHUB_EVENT_NAME" \ + --ref-type "$GITHUB_REF_TYPE" \ + --ref-name "$GITHUB_REF_NAME" \ + --sha "$GITHUB_SHA" \ + --repository "$GITHUB_REPOSITORY" \ + --default-branch "main" \ + --remote + - name: Download verified build artifact + env: + GH_TOKEN: ${{ github.token }} + run: | + mkdir -p release-artifacts + gh run download "$GITHUB_RUN_ID" \ + --repo "$GITHUB_REPOSITORY" \ + --name "mailstack-source-${GITHUB_RUN_ID}" \ + --dir release-artifacts + - name: Publish immutable-intent GitHub Release + env: + GH_TOKEN: ${{ github.token }} + RELEASE_VERSION: ${{ needs.build-verified-source.outputs.version }} + RELEASE_TAG: ${{ needs.build-verified-source.outputs.tag }} + RELEASE_PRERELEASE: ${{ needs.build-verified-source.outputs.prerelease }} + run: | + ZIP="release-artifacts/dist/mailstack-${RELEASE_VERSION}-source.zip" + SHA="release-artifacts/dist/mailstack-${RELEASE_VERSION}-source.zip.sha256" + test -f "$ZIP" + test -f "$SHA" + + args=( + release create "$RELEASE_TAG" + "$ZIP" + "$SHA" + --repo "$GITHUB_REPOSITORY" + --verify-tag + --target "$GITHUB_SHA" + --title "MailStack ${RELEASE_VERSION}" + --notes-file docs/RELEASE_NOTES_1.3.0.md + ) + if [[ "$RELEASE_PRERELEASE" == "true" ]]; then + args+=(--prerelease --latest=false) + else + args+=(--latest) + fi + gh "${args[@]}" diff --git a/CHANGELOG.md b/CHANGELOG.md index 93d8404..e52debb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,16 @@ All notable repository-level changes are recorded here. Application history befo - Preserved the earlier RC1 documentation baseline as historical provenance instead of treating its source commit as the current release anchor. - Added the PHASE-004 release, upgrade, and operational-reliability record; PHASE-004A changes documentation, release metadata, and generated manifests only. +### Release automation + +- Added a fail-closed release gate that requires tag/version/package-version agreement, the exact current `main` head, successful `main` push CI for the tagged SHA, and absence of an existing GitHub Release before publication. +- Split tag release handling into a read-only verified-build job and a write-scoped publication job; manual `workflow_dispatch` remains validation/build-only. +- Automated GitHub Release creation with deterministic source ZIP and SHA-256 assets, RC pre-release classification, stable latest classification, and overwrite/clobber prevention. +- Added focused release-workflow contract tests and made them blocking in CI and the forensic audit. + ### Compatibility -- No application runtime, database schema, migration, route, authorization, UI, mail-flow, installer behavior, deployment template, or service configuration changes are introduced by PHASE-004A. +- No application runtime, database schema, migration, route, authorization, UI, mail-flow, installer behavior, deployment template, or service configuration changes are introduced by PHASE-004A or PHASE-004B. - `v1.3.0-rc.4` and its published source identity remain immutable; `1.3.0-rc.5` is the next development-candidate version and is not yet a published release. ## Unreleased — MailStack repository bootstrap diff --git a/docs/BUILD.md b/docs/BUILD.md index 58eac53..8c0b781 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -26,6 +26,16 @@ python scripts/verify_release.py \ The builder normalizes archive timestamps, preserves executable permissions, writes a source manifest, excludes generated/runtime artifacts and emits a SHA-256 checksum. +## Automated GitHub publication + +The deterministic builder remains the canonical artifact producer. After the intended release commit +is merged to `main` and exact-SHA `main` CI passes, push a matching `v` tag. The release +workflow re-runs the release gate/full forensic/build/verification path, stores the verified Actions +artifact, and publishes the GitHub Release with the ZIP and `.sha256` asset. + +Manual workflow dispatch validates/builds only. Tag/version/package mismatches, non-current-main tags, +missing successful `main` CI, and pre-existing releases fail closed. + ## Stable promotion Do not change the version to `1.3.0` until every required human gate in `docs/PUBLIC_RELEASE_CHECKLIST.md` passes. diff --git a/docs/FORENSIC_AUDIT_REPORT.md b/docs/FORENSIC_AUDIT_REPORT.md index 7f407bc..8849f59 100644 --- a/docs/FORENSIC_AUDIT_REPORT.md +++ b/docs/FORENSIC_AUDIT_REPORT.md @@ -1,10 +1,10 @@ # Forensic audit report — MailStack 1.3.0 RC5 development baseline -**PHASE-004A audit date:** 2026-08-17 +**PHASE-004B audit date:** 2026-08-17 **Repository development version:** `1.3.0-rc.5` **Latest published release candidate:** `v1.3.0-rc.4` **Target runtime:** Ubuntu Server 24.04 LTS and CPython 3.12 -**Current classification:** RC4 release qualification finalized; RC5 PHASE-004A CI requalification pending +**Current classification:** RC4 release qualification finalized; PHASE-004A branch CI passed; RC5 PHASE-004B requalification pending ## Executive disposition @@ -30,12 +30,17 @@ | Restart/reboot recovery acceptance | PENDING | | Final ownership/license review | PENDING release owner | | PHASE-004A structural/local repository gates | PASS | -| PHASE-004A GitHub CI | PENDING after branch push | +| PHASE-004A branch GitHub CI run `32087558399` | PASS | +| PHASE-004B release automation contracts | PASS locally — 7 focused contracts | +| PHASE-004B structural forensic audit | PASS locally — 407 files, 146 Python, 13 shell, zero blocking findings | +| PHASE-004B GitHub branch CI | PENDING after delta apply/push | **OFFICIAL_RC4_SOURCE_BASELINE:** PASS **RC4_OPEN_SOURCE_RELEASE_CANDIDATE:** PASS -**PHASE_004A_LOCAL_QUALIFICATION:** PASS -**RC5_DEVELOPMENT_REQUALIFICATION:** PENDING GITHUB CI +**PHASE_004A_REMOTE_QUALIFICATION:** PASS — branch CI `32087558399` +**PHASE_004B_LOCAL_QUALIFICATION:** PASS +**PHASE_004B_REMOTE_QUALIFICATION:** PENDING GITHUB CI +**RC5_DEVELOPMENT_REQUALIFICATION:** PENDING PHASE-004B GITHUB CI **STABLE_PRODUCTION_ACCEPTANCE:** PENDING ## Official RC4 baseline identity @@ -130,6 +135,16 @@ PHASE-004A does not change application runtime logic, models, migrations, URLs, templates, CSS, JavaScript, mail flow, ingestion behavior, installer behavior, deployment templates, service definitions, database schema, DNS/TLS configuration, or the existing VPS. +## PHASE-004B release automation boundary + +PHASE-004B changes repository release automation only. It adds a fail-closed Python release gate and +focused contract tests, updates CI/full-forensic enforcement, and splits tag handling into a read-only +verified-build job plus a tag-only publication job with `actions: read` and `contents: write`. +Publication requires matching VERSION/package/tag identity, the exact current `main` head, successful +`main` push CI for the tagged SHA, and proof that no release already exists. Deterministic ZIP/SHA +assets are published without `--clobber` or release editing. Manual dispatch remains build-only. +No application/runtime/deployment/VPS behavior is changed. + ## Security review The RC4 source remains pinned to Django 5.2.16, Python 3.12, and `sqlparse==0.6.0`. The blocking diff --git a/docs/FORENSIC_FILE_INVENTORY.json b/docs/FORENSIC_FILE_INVENTORY.json index 5237c03..db8be28 100644 --- a/docs/FORENSIC_FILE_INVENTORY.json +++ b/docs/FORENSIC_FILE_INVENTORY.json @@ -72,17 +72,17 @@ }, { "kind": "text", - "lines": 129, + "lines": 132, "path": ".github/workflows/ci.yml", - "sha256": "bdab8313c3732e0652637a1048e04872629c5cf99cf49263720cd39a3ab2f926", - "size_bytes": 4106 + "sha256": "96046dd5a995af85fdda2987727eb18687affd1d16e652eddf43bd4f32d77177", + "size_bytes": 4206 }, { "kind": "text", - "lines": 47, + "lines": 131, "path": ".github/workflows/release.yml", - "sha256": "0f46b6eee13247008c11468f7c83ba9751b92332919af8e11053f1223e4c1ef4", - "size_bytes": 1510 + "sha256": "f6c150f7310b184272c5fe22fd1fe13f78c4e655d42413625861b29fae33ade3", + "size_bytes": 4418 }, { "kind": "text", @@ -93,10 +93,10 @@ }, { "kind": "text", - "lines": 110, + "lines": 117, "path": "CHANGELOG.md", - "sha256": "53e1e7a9429533729f30bef2986b75976f77578a7efeb39f3ffa66e2bc93475d", - "size_bytes": 9547 + "sha256": "089e1b03250a3cfd78f0f304daf646fbb3a4336b9d112b997c79b46c8b883a2a", + "size_bytes": 10254 }, { "kind": "text", @@ -467,10 +467,10 @@ }, { "kind": "text", - "lines": 31, + "lines": 41, "path": "docs/BUILD.md", - "sha256": "0f0ed0d791a2ddefa4a83f9be173bc42912bf34b7d496a02a4223af83204bff2", - "size_bytes": 804 + "sha256": "ec3e1eabd1e1d5bbe3bfe3852c31b7d605d199fa969596999e0dfed223de1ad9", + "size_bytes": 1388 }, { "kind": "text", @@ -537,10 +537,10 @@ }, { "kind": "text", - "lines": 159, + "lines": 174, "path": "docs/FORENSIC_AUDIT_REPORT.md", - "sha256": "fe17e0c0784a127f6166f3383176e0cedd33fb803728bb68b67a2548e20bc4b4", - "size_bytes": 8353 + "sha256": "0b890c9ad3ee692d1883621e57f5dadb09e7d0184c4d7a66007ff58faaf86039", + "size_bytes": 9483 }, { "kind": "text", @@ -621,10 +621,10 @@ }, { "kind": "text", - "lines": 61, + "lines": 67, "path": "docs/PUBLISHING.md", - "sha256": "f3bd3a629334ad7f916932b64c231091b2623ba7a04ae1340447249056c85446", - "size_bytes": 2289 + "sha256": "4080361c928c3de1d7fae531cad4c8d73e166916efbfdf1febd52b447dd4c7c8", + "size_bytes": 2926 }, { "kind": "text", @@ -642,17 +642,17 @@ }, { "kind": "text", - "lines": 99, + "lines": 109, "path": "docs/RELEASE_NOTES_1.3.0.md", - "sha256": "34ad6fc04b4e6691385c3a6dbac1e2eaf8e15caf0b5d78f0ea817589dfb13e87", - "size_bytes": 6568 + "sha256": "ff942f681e2b4f87c0d90c0c7ca8f40cfc3d0ef6c3151298a72308b117b402f4", + "size_bytes": 7312 }, { "kind": "text", - "lines": 14, + "lines": 16, "path": "docs/RELEASE_PROCESS.md", - "sha256": "f744e5e890d84a8b2786899f62f72cca838f4969870c1cc415367245368a2b44", - "size_bytes": 973 + "sha256": "52b80cda87ff2477c062f3ad88346abbab4d5e0b6907382ae58468a90ede0658", + "size_bytes": 1601 }, { "kind": "text", @@ -670,10 +670,10 @@ }, { "kind": "text", - "lines": 124, + "lines": 144, "path": "docs/TEST_REPORT.md", - "sha256": "fce6543f37edcaa9fc390eeea85ecd2a70b084f5647ac6ecebd8ac740b15f769", - "size_bytes": 6066 + "sha256": "589c7fbc9d8149a00abfeccabefd6c55966c2ecd802d32bfb4492437fa6b9358", + "size_bytes": 7306 }, { "kind": "text", @@ -714,7 +714,7 @@ "kind": "text", "lines": 221, "path": "documents/DOCUMENTATION_MANIFEST.json", - "sha256": "4979d782922f229efebb53163a67a02cc7749efd47cba70425045ba7b56cf59d", + "sha256": "89d85be3dbbbedfe41cc1c6a9f67ae73da0ce4699b95cbc3287148b7010a31ed", "size_bytes": 8429 }, { @@ -824,10 +824,10 @@ }, { "kind": "text", - "lines": 72, + "lines": 91, "path": "documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md", - "sha256": "3bc0ea393a303e7a4a05b85c146539095a5d471d2d537da1a7d31290d74e7bbe", - "size_bytes": 3565 + "sha256": "a5205df42de3faca80f7107405704dd2f79200b9340d66d493f814eb70d5d6b3", + "size_bytes": 5265 }, { "kind": "text", @@ -4691,7 +4691,7 @@ }, { "kind": "text", - "lines": 397, + "lines": 400, "path": "scripts/forensic_audit.py", "python": { "classes": [], @@ -4717,8 +4717,8 @@ ], "methods": [] }, - "sha256": "3615ebd13d33786631d50b025240f4fca85005eee84e27183450c1958ed7ffbf", - "size_bytes": 14391 + "sha256": "5d50f62a3d041a98d9420a739c4d05dafe61739e0228c340a7920ecfa34f45c3", + "size_bytes": 14567 }, { "kind": "text", @@ -4835,6 +4835,50 @@ "sha256": "65177a9ca5603dc99f3303b841d4127ea671dd45a87f3ccd5df81e6b4723da85", "size_bytes": 20080 }, + { + "kind": "text", + "lines": 281, + "path": "scripts/release_gate.py", + "python": { + "classes": [ + "ReleaseGateError", + "ReleaseIdentity" + ], + "functions": [ + "api_request_json", + "git_output", + "main", + "normalize_package_version", + "payload_has_successful_main_ci", + "read_package_version", + "require_exact_main_head", + "require_release_absent", + "require_release_lookup_absent_status", + "require_successful_main_ci", + "validate_local_identity", + "write_outputs" + ], + "imports": [ + "__future__", + "argparse", + "dataclasses", + "json", + "os", + "pathlib", + "re", + "subprocess", + "sys", + "tomllib", + "typing", + "urllib.error", + "urllib.parse", + "urllib.request" + ], + "methods": [] + }, + "sha256": "8097e778918c80224d048331132c22a00026358284d1378a6d1887c56080ab11", + "size_bytes": 10040 + }, { "kind": "text", "lines": 48, @@ -5018,6 +5062,37 @@ "sha256": "5f1917f0b6ecd769c4caecf0a7bb5cff8ae13b847d2f097ecbfa49630d9ed685", "size_bytes": 4332 }, + { + "kind": "text", + "lines": 223, + "path": "scripts/test_release_workflow.py", + "python": { + "classes": [], + "functions": [ + "fake_git_output", + "load_module", + "main", + "make_root", + "test_exact_main_head_guard", + "test_existing_release_status_fails_closed", + "test_package_version_mismatch_fails", + "test_successful_main_ci_payload_contract", + "test_tag_identity_and_manual_mode", + "test_version_normalization", + "test_workflow_contract" + ], + "imports": [ + "__future__", + "importlib.util", + "pathlib", + "sys", + "tempfile" + ], + "methods": [] + }, + "sha256": "a0555c8b9819f8e51bed877d8686c2e49ddeba8793ef5ee1613aa63d06804704", + "size_bytes": 6531 + }, { "kind": "text", "lines": 215, @@ -5106,15 +5181,15 @@ }, "summary": { "binary_files": 25, - "files": 404, - "python_classes": 73, - "python_files": 144, - "python_functions": 405, + "files": 406, + "python_classes": 75, + "python_files": 146, + "python_functions": 428, "python_methods": 61, "shell_files": 13, "shell_functions": 31, - "text_files": 379, - "total_bytes": 31656307, - "total_text_lines": 26694 + "text_files": 381, + "total_bytes": 31683432, + "total_text_lines": 27377 } } diff --git a/docs/PUBLISHING.md b/docs/PUBLISHING.md index a67b4d5..40db65d 100644 --- a/docs/PUBLISHING.md +++ b/docs/PUBLISHING.md @@ -33,19 +33,25 @@ git remote add origin git push -u origin main ``` -Create an annotated release-candidate tag only after CI passes: +Create an annotated release-candidate tag only after the intended release commit is the current +`main` head and the exact `main` SHA has a successful push CI run: ```bash git tag -a v1.3.0-rc.5 -m "MailStack 1.3.0 RC5" git push origin v1.3.0-rc.5 ``` -Attach these files to the forge release: +The tag push triggers `.github/workflows/release.yml`. It fails closed unless the tag matches +`VERSION`/`project.version`, points at the exact current `main` head, has successful `main` CI, and +has no existing GitHub Release. The workflow rebuilds and verifies the deterministic source archive, +keeps a GitHub Actions artifact, and automatically creates the GitHub Release with: - `mailstack-1.3.0-rc.5-source.zip` - `mailstack-1.3.0-rc.5-source.zip.sha256` -- `docs/RELEASE_NOTES_1.3.0.md` -- `docs/FORENSIC_AUDIT_REPORT.md` + +RC tags are published as pre-releases and are explicitly not marked latest; stable tags are normal +latest releases. Manual `workflow_dispatch` is validation/build-only and cannot publish. Existing +releases are never automatically edited, clobbered, or overwritten. ## Repository settings diff --git a/docs/RELEASE_NOTES_1.3.0.md b/docs/RELEASE_NOTES_1.3.0.md index b4d1074..2e7db65 100644 --- a/docs/RELEASE_NOTES_1.3.0.md +++ b/docs/RELEASE_NOTES_1.3.0.md @@ -10,6 +10,16 @@ invoke a broken WSL `bash.exe` launcher when Git Bash is available. Existing Mai features, UI/UX, routes, data model, receive-only scope, runtime dependencies, and deployment identifiers remain preserved. +## RC5 development: automated release publication + +PHASE-004B adds repository release automation for the next candidate without changing MailStack +runtime behavior. A legitimate tag push must match repository/package version identity, point at the +exact current `main` head, have successful `main` push CI for that SHA, and have no pre-existing +GitHub Release. The read-only build job performs release identity checks, full forensic validation, +deterministic source build, checksum verification, and Actions artifact upload. A separate tag-only +write-scoped job re-proves remote eligibility and publishes the verified ZIP/SHA assets. RC tags are +pre-releases and stable tags are normal/latest releases. Manual dispatch remains non-publishing. + ## Local Windows audit fix in RC4 A Windows CMD qualification run showed documentation, design, UI-foundation, inventory, and template diff --git a/docs/RELEASE_PROCESS.md b/docs/RELEASE_PROCESS.md index 791ded9..d154cb3 100644 --- a/docs/RELEASE_PROCESS.md +++ b/docs/RELEASE_PROCESS.md @@ -7,8 +7,10 @@ 5. Run the online dependency audit in CI. 6. Test a clean installation on an isolated Ubuntu Server 24.04 LTS VPS. 7. Verify external inbound SMTP, login, authorization boundaries, live updates, safe HTML, and attachment downloads. -8. Build with `python scripts/build_release.py --root . --version 1.3.0-rc.5`. -9. Verify the ZIP and checksum with `python scripts/verify_release.py`. -10. Publish the source ZIP, checksum, release notes, user-document manifest, and exact Git commit/tag. +8. Build with `python scripts/build_release.py --root . --version 1.3.0-rc.5` and verify the ZIP/checksum with `python scripts/verify_release.py`. +9. Merge the intended release commit to `main` and require a successful `main` push CI run for the exact SHA. +10. Create the matching `v` tag on the current `main` head and push that tag. Do not create a release manually first. +11. The tag workflow validates tag/version/package identity, exact `main` head, successful `main` CI, and release absence; it then builds/verifies the deterministic archive and creates the GitHub Release with ZIP/SHA assets. +12. Verify the published release classification and attached checksum. RC versions must be pre-releases; stable versions are normal/latest releases. -A release is not promoted from release candidate to production-ready until the clean-VPS acceptance test passes. +`workflow_dispatch` is intentionally build/validation-only. Release automation never edits or clobbers an existing release. A release is not promoted from release candidate to production-ready until the clean-VPS acceptance test passes. diff --git a/docs/TEST_REPORT.md b/docs/TEST_REPORT.md index 1cb3ba9..9c6c575 100644 --- a/docs/TEST_REPORT.md +++ b/docs/TEST_REPORT.md @@ -1,6 +1,6 @@ # Test report — MailStack 1.3.0 RC5 development baseline -**PHASE-004A verification date:** 2026-08-17 +**PHASE-004B verification date:** 2026-08-17 **Latest published release candidate:** `v1.3.0-rc.4` **Repository development version:** `1.3.0-rc.5` @@ -60,6 +60,26 @@ The deterministic RC4 release archive is `mailstack-1.3.0-rc.4-source.zip` with `58f06adea7c813e9861799d20e392441367bf64f6513d6e0634455d2011d4eac`. Release verification reported 405 archive members and 404 manifest members. +## PHASE-004A branch CI closure + +GitHub Actions run `32087558399` passed on exact commit +`586400e56b388873ecfcd8c67fc494a88dd73e31` using Ubuntu 24.04 and Python 3.12.13. The clean CI +environment installed the repository's exact development pins, reported no broken requirements or +known dependency vulnerabilities, passed all 198 Django tests at 95.00 percent coverage, and passed +all documentation, design, installer, operations, forensic, release-build, and release-verification +gates. + +## PHASE-004B release automation qualification + +PHASE-004B adds seven focused release-workflow contracts covering version normalization, RC/stable +classification, tag identity, manual-dispatch non-publication, package-version mismatch rejection, +successful-main-CI evidence matching, existing-release fail-close behavior, exact-main-head guarding, +and required workflow protections. The assembled candidate passes all seven contracts locally. They +are blocking in the main CI workflow and are also invoked by the forensic audit. The structural +forensic audit passes with 407 files scanned, 146 Python files, 13 shell files, and zero blocking +findings. GitHub branch CI remains the final authority. No fake public tag/release is created for +testing. + ## Dependency advisory qualification The earlier RC2 run `32053931714` stopped at the blocking advisory audit because diff --git a/documents/DOCUMENTATION_MANIFEST.json b/documents/DOCUMENTATION_MANIFEST.json index faeb5ad..0876881 100644 --- a/documents/DOCUMENTATION_MANIFEST.json +++ b/documents/DOCUMENTATION_MANIFEST.json @@ -201,7 +201,7 @@ "last_reviewed": "2026-08-17", "path": "documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md", "phase_id": "PHASE-004", - "sha256": "3bc0ea393a303e7a4a05b85c146539095a5d471d2d537da1a7d31290d74e7bbe", + "sha256": "a5205df42de3faca80f7107405704dd2f79200b9340d66d493f814eb70d5d6b3", "status": "active", "title": "Release, Upgrade and Operational Reliability", "version": "1.3.0-rc.5" diff --git a/documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md b/documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md index 85098bf..dca4044 100644 --- a/documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md +++ b/documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md @@ -15,40 +15,53 @@ phase_id: PHASE-004 Make post-RC4 release handling, upgrades, rollback, and operational acceptance reproducible without weakening the receive-only architecture or disturbing an already working MailStack deployment. -PHASE-004A finalizes the forensic/documentation baseline before later PHASE-004 subphases change -release automation or upgrade tooling. +PHASE-004A finalized the forensic/documentation baseline. PHASE-004B adds fail-closed automatic +tag-to-GitHub-Release publication while keeping upgrade/runtime changes for later approved subphases. ## Scope PHASE-004A is restricted to forensic evidence finalization, baseline identity, version/release metadata synchronization, this phase record, and deterministic documentation/design/forensic manifests. It records the completed RC4 qualification using the successful `main`, tag-CI, and -release-artifact workflows and replaces stale pre-RC4-publish wording. +release-artifact workflows and replaces stale pre-RC4-publish wording. Its branch CI run +`32087558399` passed on exact commit `586400e56b388873ecfcd8c67fc494a88dd73e31`. -Later approved PHASE-004 subphases may address automatic tag-to-GitHub-Release publication, a -controlled existing-server upgrade/rollback mechanism, and operational backup/restore and reboot -acceptance. Those runtime/tooling changes are not implemented by PHASE-004A. +PHASE-004B is restricted to release automation and its tests/documentation. A tag push must match +`VERSION` and the Python package version, point at the exact current `main` head, have a successful +`main` push CI run for that SHA, and have no existing GitHub Release for the tag. The verified build +job has read-only repository access. A separate tag-only publication job receives `actions: read` and +`contents: write`, downloads the verified workflow artifact, and creates the GitHub Release with the +deterministic ZIP and checksum. Manual workflow dispatch remains build/validation only. + +Later approved PHASE-004 subphases may address a controlled existing-server upgrade/rollback +mechanism and operational backup/restore and reboot acceptance. Those runtime changes are not +implemented by PHASE-004A or PHASE-004B. ## User-facing changes -There is no application UI or mailbox behavior change in PHASE-004A. Maintainers and operators gain -a single explicit official source baseline for RC4 and corrected evidence that distinguishes -published RC4 qualification from the new RC5 development identity. +There is no application UI or mailbox behavior change in PHASE-004A or PHASE-004B. Maintainers and +operators gain a single explicit official source baseline for RC4, corrected RC4 qualification +evidence, and a tag-driven release workflow that removes the need to upload deterministic release +assets manually from a workstation. ## How to use Treat `mailstack-1.3.0-rc.4-source.zip`, SHA-256 `58f06adea7c813e9861799d20e392441367bf64f6513d6e0634455d2011d4eac`, tag `v1.3.0-rc.4`, and commit -`896dbcc2ed1f38d9c618bf0b712efe5923f92e56` as the immutable official source baseline. Start -PHASE-004 work from that source identity. The working repository version is `1.3.0-rc.5`; do not -retag or rewrite RC4. +`896dbcc2ed1f38d9c618bf0b712efe5923f92e56` as the immutable official source baseline. The working +repository version remains `1.3.0-rc.5`; do not retag or rewrite RC4. For future releases, merge the +release commit to `main`, require successful `main` CI, create the matching `v` tag at the +current `main` head, and push the tag. The release workflow then builds/verifies the deterministic +source archive and publishes the GitHub Release automatically. `workflow_dispatch` validates and +builds artifacts but never publishes a release. ## Compatibility -PHASE-004A introduces no model, migration, URL, permission, template, CSS, JavaScript, mail-flow, -ingestion, installer, deployment-template, service, database, DNS, TLS, or existing-VPS change. -The `v1.3.0-rc.4` tag and deterministic RC4 source archive remain unchanged. There is no data or -configuration migration and no rollback operation is required for this documentation-only delta. +PHASE-004A and PHASE-004B introduce no model, migration, URL, application permission, template, CSS, +JavaScript, mail-flow, ingestion, installer, deployment-template, service, database, DNS, TLS, or +existing-VPS change. PHASE-004B changes only repository release automation, audit tooling, focused +contract tests, and required documentation/manifests. The `v1.3.0-rc.4` tag and deterministic RC4 +source archive remain unchanged. No data/configuration migration or VPS rollback is required. ## Verification @@ -58,15 +71,21 @@ coverage, the dependency vulnerability audit, Ruff, Bandit, installer and operat full forensic audit with zero blocking findings, deterministic source build, and release verification. -PHASE-004A itself must pass documentation synchronization/tests, design-manifest integrity, -forensic-inventory regeneration/check, template validation, installer/operations regression -contracts, `git diff --check`, structural forensic audit, and final GitHub CI before it is considered -fully qualified. +PHASE-004A branch CI run `32087558399` passed all blocking gates, including 198 Django tests at +95.00 percent coverage, dependency audit, full forensic audit, deterministic RC5 source build, and +release verification. + +PHASE-004B must pass its release-workflow contract tests, documentation/design/inventory gates, +installer/operations regression contracts, dependency/security/application CI, full forensic audit, +and deterministic release build/verification before merge. Actual publication is intentionally not +tested by creating a fake public tag/release; the first legitimate post-merge release tag is the +end-to-end publication acceptance event. ## Documentation impact -This subphase updates the root and application changelogs, current build/release examples, README -status wording, dependency/performance evidence, forensic and test reports, the canonical baseline, -UI implementation-status evidence, active managed-document version metadata, generated -documentation/design/forensic manifests, and this PHASE-004 record. No user workflow guide requires -behavioral changes because application behavior is unchanged. +PHASE-004A updated the root/application changelogs, build/release examples, README status wording, +dependency/performance evidence, forensic/test reports, canonical baseline, UI implementation +status, active managed-document version metadata, generated manifests, and this phase record. +PHASE-004B updates the release workflow, release gate/contract tests, root changelog, release/build/ +publishing documentation, release notes, forensic/test evidence, generated forensic/documentation +manifests, and this phase record. No application user workflow guide requires a behavioral change. diff --git a/scripts/forensic_audit.py b/scripts/forensic_audit.py index 051350e..0b73b9a 100644 --- a/scripts/forensic_audit.py +++ b/scripts/forensic_audit.py @@ -39,6 +39,8 @@ "docs/FORENSIC_FILE_INVENTORY.json", "scripts/test_installer.py", "scripts/test_operations.py", + "scripts/release_gate.py", + "scripts/test_release_workflow.py", "scripts/shell_runtime.py", "scripts/validate_templates.py", "docs/FEATURE_MATRIX.md", @@ -317,6 +319,7 @@ def main() -> int: ([sys.executable, str(root / "scripts/validate_templates.py")], "TEMPLATE_VALIDATION"), ([sys.executable, str(root / "scripts/test_installer.py")], "INSTALLER_CONTRACT"), ([sys.executable, str(root / "scripts/test_operations.py")], "OPERATIONS_CONTRACT"), + ([sys.executable, str(root / "scripts/test_release_workflow.py")], "RELEASE_WORKFLOW_CONTRACT"), ): code, output = run(command, root) if code: diff --git a/scripts/release_gate.py b/scripts/release_gate.py new file mode 100644 index 0000000..ca5552d --- /dev/null +++ b/scripts/release_gate.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python3 +"""Fail-closed release eligibility checks for MailStack tag publication.""" +from __future__ import annotations + +import argparse +import json +import os +import re +import subprocess +import sys +import tomllib +import urllib.error +import urllib.parse +import urllib.request +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +VERSION_PATTERN = re.compile(r"^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-rc\.(?P0|[1-9]\d*))?$") +SHA_PATTERN = re.compile(r"^[0-9a-f]{40}$") +API_VERSION = "2022-11-28" + + +class ReleaseGateError(RuntimeError): + """Raised when release publication eligibility cannot be proven.""" + + +@dataclass(frozen=True) +class ReleaseIdentity: + version: str + tag: str + package_version: str + prerelease: bool + publish: bool + + +def normalize_package_version(version: str) -> str: + match = VERSION_PATTERN.fullmatch(version) + if not match: + raise ReleaseGateError(f"unsupported VERSION format: {version!r}") + base = f"{match.group('major')}.{match.group('minor')}.{match.group('patch')}" + rc = match.group("rc") + return f"{base}rc{rc}" if rc is not None else base + + +def read_package_version(root: Path) -> str: + pyproject = root / "mailbox-app" / "pyproject.toml" + try: + payload = tomllib.loads(pyproject.read_text(encoding="utf-8")) + return str(payload["project"]["version"]) + except (OSError, KeyError, TypeError, tomllib.TOMLDecodeError) as exc: + raise ReleaseGateError(f"unable to read project.version from {pyproject}: {exc}") from exc + + +def validate_local_identity( + root: Path, + *, + event_name: str, + ref_type: str, + ref_name: str, + sha: str, +) -> ReleaseIdentity: + version = (root / "VERSION").read_text(encoding="utf-8").strip() + expected_package = normalize_package_version(version) + package_version = read_package_version(root) + if package_version != expected_package: + raise ReleaseGateError( + f"package version mismatch: VERSION={version!r}, project.version={package_version!r}, expected={expected_package!r}" + ) + + prerelease = VERSION_PATTERN.fullmatch(version).group("rc") is not None # type: ignore[union-attr] + expected_tag = f"v{version}" + publish = event_name == "push" + if publish: + if ref_type != "tag": + raise ReleaseGateError(f"release publication requires a tag push, got ref_type={ref_type!r}") + if ref_name != expected_tag: + raise ReleaseGateError(f"tag/version mismatch: tag={ref_name!r}, expected={expected_tag!r}") + if not SHA_PATTERN.fullmatch(sha): + raise ReleaseGateError(f"invalid release SHA: {sha!r}") + elif event_name != "workflow_dispatch": + raise ReleaseGateError(f"unsupported release workflow event: {event_name!r}") + + return ReleaseIdentity( + version=version, + tag=expected_tag, + package_version=package_version, + prerelease=prerelease, + publish=publish, + ) + + +def git_output(root: Path, *arguments: str) -> str: + completed = subprocess.run( + ["git", *arguments], + cwd=root, + text=True, + capture_output=True, + ) + if completed.returncode: + raise ReleaseGateError((completed.stdout + completed.stderr).strip() or "git command failed") + return completed.stdout.strip() + + +def require_exact_main_head(root: Path, sha: str, default_branch: str) -> None: + ref = f"refs/remotes/origin/{default_branch}" + git_output( + root, + "fetch", + "--no-tags", + "--prune", + "origin", + f"+refs/heads/{default_branch}:{ref}", + ) + main_sha = git_output(root, "rev-parse", ref) + if main_sha != sha: + raise ReleaseGateError( + f"tagged SHA is not the current {default_branch} head: tagged={sha}, {default_branch}={main_sha}" + ) + + +def api_request_json(url: str, token: str) -> tuple[int, Any]: + request = urllib.request.Request( + url, + headers={ + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {token}", + "X-GitHub-Api-Version": API_VERSION, + "User-Agent": "MailStack-release-gate", + }, + ) + try: + with urllib.request.urlopen(request, timeout=30) as response: + body = response.read() + return response.status, json.loads(body) if body else None + except urllib.error.HTTPError as exc: + body = exc.read() + payload: Any = None + if body: + try: + payload = json.loads(body) + except json.JSONDecodeError: + payload = None + return exc.code, payload + except urllib.error.URLError as exc: + raise ReleaseGateError(f"GitHub API request failed: {exc.reason}") from exc + + +def require_release_lookup_absent_status(status: int, tag: str) -> None: + if status == 404: + return + if status == 200: + raise ReleaseGateError(f"GitHub Release already exists for {tag}; refusing overwrite") + raise ReleaseGateError(f"unable to prove release absence for {tag}; GitHub API status={status}") + + +def require_release_absent(repository: str, tag: str, token: str, *, api_base: str) -> None: + encoded_tag = urllib.parse.quote(tag, safe="") + status, _ = api_request_json( + f"{api_base}/repos/{repository}/releases/tags/{encoded_tag}", + token, + ) + require_release_lookup_absent_status(status, tag) + + +def payload_has_successful_main_ci(payload: Any, *, sha: str, default_branch: str) -> bool: + if not isinstance(payload, dict): + return False + runs = payload.get("workflow_runs") + if not isinstance(runs, list): + return False + return any( + isinstance(run, dict) + and run.get("head_sha") == sha + and run.get("head_branch") == default_branch + and run.get("event") == "push" + and run.get("status") == "completed" + and run.get("conclusion") == "success" + for run in runs + ) + + +def require_successful_main_ci( + repository: str, + sha: str, + default_branch: str, + token: str, + *, + api_base: str, +) -> None: + params = urllib.parse.urlencode( + { + "branch": default_branch, + "event": "push", + "status": "completed", + "head_sha": sha, + "per_page": 100, + } + ) + status, payload = api_request_json( + f"{api_base}/repos/{repository}/actions/workflows/ci.yml/runs?{params}", + token, + ) + if status != 200: + raise ReleaseGateError(f"unable to query main CI evidence; GitHub API status={status}") + if not payload_has_successful_main_ci(payload, sha=sha, default_branch=default_branch): + raise ReleaseGateError(f"no successful main push CI found for exact release SHA {sha}") + + +def write_outputs(path: Path | None, identity: ReleaseIdentity) -> None: + if path is None: + return + values = { + "version": identity.version, + "tag": identity.tag, + "prerelease": "true" if identity.prerelease else "false", + "publish": "true" if identity.publish else "false", + "title": f"MailStack {identity.version}", + } + with path.open("a", encoding="utf-8", newline="\n") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1]) + parser.add_argument("--event-name", default=os.getenv("GITHUB_EVENT_NAME", "workflow_dispatch")) + parser.add_argument("--ref-type", default=os.getenv("GITHUB_REF_TYPE", "branch")) + parser.add_argument("--ref-name", default=os.getenv("GITHUB_REF_NAME", "")) + parser.add_argument("--sha", default=os.getenv("GITHUB_SHA", "")) + parser.add_argument("--repository", default=os.getenv("GITHUB_REPOSITORY", "")) + parser.add_argument("--default-branch", default="main") + parser.add_argument("--github-output", type=Path) + parser.add_argument("--remote", action="store_true") + parser.add_argument("--api-base", default=os.getenv("GITHUB_API_URL", "https://api.github.com")) + args = parser.parse_args() + + root = args.root.resolve() + try: + identity = validate_local_identity( + root, + event_name=args.event_name, + ref_type=args.ref_type, + ref_name=args.ref_name, + sha=args.sha, + ) + if args.remote: + if not identity.publish: + raise ReleaseGateError("remote publication gate is valid only for a tag push") + if not args.repository: + raise ReleaseGateError("GITHUB_REPOSITORY is required for remote publication checks") + token = os.getenv("GITHUB_TOKEN", "") + if not token: + raise ReleaseGateError("GITHUB_TOKEN is required for remote publication checks") + require_exact_main_head(root, args.sha, args.default_branch) + require_release_absent(args.repository, identity.tag, token, api_base=args.api_base.rstrip("/")) + require_successful_main_ci( + args.repository, + args.sha, + args.default_branch, + token, + api_base=args.api_base.rstrip("/"), + ) + + write_outputs(args.github_output, identity) + print(f"RELEASE_VERSION={identity.version}") + print(f"RELEASE_TAG={identity.tag}") + print(f"RELEASE_PRERELEASE={'true' if identity.prerelease else 'false'}") + print(f"RELEASE_MODE={'PUBLISH' if identity.publish else 'VALIDATE_ONLY'}") + print(f"RELEASE_REMOTE_CHECK={'PASS' if args.remote else 'NOT_REQUESTED'}") + print("RELEASE_GATE=PASS") + return 0 + except (OSError, ReleaseGateError) as exc: + print(f"RELEASE_GATE_FINDING={exc}") + print("RELEASE_GATE=FAIL") + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/test_release_workflow.py b/scripts/test_release_workflow.py new file mode 100644 index 0000000..e1b73de --- /dev/null +++ b/scripts/test_release_workflow.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +"""Contract tests for fail-closed tag-to-GitHub-Release automation.""" +from __future__ import annotations + +import importlib.util +import sys +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def load_module(name: str, path: Path): + spec = importlib.util.spec_from_file_location(name, path) + if spec is None or spec.loader is None: + raise RuntimeError(f"unable to load {path}") + module = importlib.util.module_from_spec(spec) + sys.modules[name] = module + spec.loader.exec_module(module) + return module + + +GATE = load_module("mailstack_release_gate", ROOT / "scripts/release_gate.py") + + +def make_root(version: str, package_version: str) -> Path: + temporary = tempfile.TemporaryDirectory(prefix="mailstack-release-gate-test-") + roots.append(temporary) + root = Path(temporary.name) + (root / "mailbox-app").mkdir() + (root / "VERSION").write_text(version + "\n", encoding="utf-8", newline="\n") + (root / "mailbox-app/pyproject.toml").write_text( + f'[project]\nname = "mailstack"\nversion = "{package_version}"\n', + encoding="utf-8", + newline="\n", + ) + return root + + +roots: list[tempfile.TemporaryDirectory[str]] = [] + + +def test_version_normalization() -> None: + assert GATE.normalize_package_version("1.3.0-rc.5") == "1.3.0rc5" + assert GATE.normalize_package_version("1.3.0") == "1.3.0" + try: + GATE.normalize_package_version("1.3") + except GATE.ReleaseGateError: + pass + else: + raise AssertionError("unsupported VERSION must fail closed") + + +def test_tag_identity_and_manual_mode() -> None: + root = make_root("1.3.0-rc.5", "1.3.0rc5") + identity = GATE.validate_local_identity( + root, + event_name="push", + ref_type="tag", + ref_name="v1.3.0-rc.5", + sha="a" * 40, + ) + assert identity.publish is True + assert identity.prerelease is True + assert identity.tag == "v1.3.0-rc.5" + + manual = GATE.validate_local_identity( + root, + event_name="workflow_dispatch", + ref_type="branch", + ref_name="main", + sha="b" * 40, + ) + assert manual.publish is False + + stable_root = make_root("1.3.0", "1.3.0") + stable = GATE.validate_local_identity( + stable_root, + event_name="push", + ref_type="tag", + ref_name="v1.3.0", + sha="e" * 40, + ) + assert stable.prerelease is False + + try: + GATE.validate_local_identity( + root, + event_name="push", + ref_type="tag", + ref_name="v1.3.0-rc.4", + sha="a" * 40, + ) + except GATE.ReleaseGateError: + pass + else: + raise AssertionError("tag/version mismatch must fail closed") + + +def test_package_version_mismatch_fails() -> None: + root = make_root("1.3.0-rc.5", "1.3.0rc4") + try: + GATE.validate_local_identity( + root, + event_name="workflow_dispatch", + ref_type="branch", + ref_name="main", + sha="c" * 40, + ) + except GATE.ReleaseGateError: + pass + else: + raise AssertionError("VERSION/project.version mismatch must fail closed") + + +def test_successful_main_ci_payload_contract() -> None: + sha = "d" * 40 + payload = { + "workflow_runs": [ + { + "head_sha": sha, + "head_branch": "main", + "event": "push", + "status": "completed", + "conclusion": "success", + } + ] + } + assert GATE.payload_has_successful_main_ci(payload, sha=sha, default_branch="main") + payload["workflow_runs"][0]["head_branch"] = "feature" + assert not GATE.payload_has_successful_main_ci(payload, sha=sha, default_branch="main") + + + +def test_existing_release_status_fails_closed() -> None: + GATE.require_release_lookup_absent_status(404, "v1.3.0-rc.5") + for status in (200, 500): + try: + GATE.require_release_lookup_absent_status(status, "v1.3.0-rc.5") + except GATE.ReleaseGateError: + pass + else: + raise AssertionError(f"release lookup status {status} must fail closed") + + +def test_exact_main_head_guard() -> None: + original = GATE.git_output + matching = "f" * 40 + + def fake_git_output(root: Path, *arguments: str) -> str: + if arguments and arguments[0] == "fetch": + return "" + if arguments[:2] == ("rev-parse", "refs/remotes/origin/main"): + return matching + raise AssertionError(arguments) + + GATE.git_output = fake_git_output + try: + GATE.require_exact_main_head(ROOT, matching, "main") + try: + GATE.require_exact_main_head(ROOT, "0" * 40, "main") + except GATE.ReleaseGateError: + pass + else: + raise AssertionError("non-main tag target must fail closed") + finally: + GATE.git_output = original + +def test_workflow_contract() -> None: + text = (ROOT / ".github/workflows/release.yml").read_text(encoding="utf-8") + required = ( + 'workflow_dispatch:', + 'tags:', + '- "v*"', + 'build-verified-source:', + 'publish-github-release:', + 'needs: build-verified-source', + "contents: read", + "actions: read", + "contents: write", + "python scripts/release_gate.py", + "--remote", + 'release create "$RELEASE_TAG"', + 'gh "${args[@]}"', + "--verify-tag", + "--prerelease", + "--latest=false", + "--latest", + 'dist/*.zip', + 'dist/*.sha256', + ) + for marker in required: + assert marker in text, marker + assert "--clobber" not in text + assert "gh release edit" not in text + assert "github.event_name == 'push'" in text + assert "github.ref_type == 'tag'" in text + + +def main() -> int: + tests = ( + test_version_normalization, + test_tag_identity_and_manual_mode, + test_package_version_mismatch_fails, + test_successful_main_ci_payload_contract, + test_existing_release_status_fails_closed, + test_exact_main_head_guard, + test_workflow_contract, + ) + try: + for test in tests: + test() + print(f"PASS={test.__name__}") + finally: + for temporary in roots: + temporary.cleanup() + print(f"RELEASE_WORKFLOW_TESTS={len(tests)}") + print("RELEASE_WORKFLOW_TEST_SUITE=PASS") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 47e62bb6c0acd0216fb261f47f85959655b489e0 Mon Sep 17 00:00:00 2001 From: Md Nurnobi Date: Mon, 17 Aug 2026 20:59:20 -0700 Subject: [PATCH 3/4] ops(upgrade): add verified server upgrade and rollback --- .github/workflows/ci.yml | 3 + CHANGELOG.md | 10 +- docs/BACKUP_RESTORE.md | 14 + docs/FORENSIC_AUDIT_REPORT.md | 48 ++- docs/FORENSIC_FILE_INVENTORY.json | 181 ++++++--- docs/OPERATIONS.md | 26 ++ docs/RELEASE_NOTES_1.3.0.md | 8 + docs/TEST_REPORT.md | 52 ++- docs/UPGRADE.md | 167 ++++++++- documents/DOCUMENTATION_MANIFEST.json | 2 +- ...ASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md | 73 +++- mailbox-app/CHANGELOG.md | 6 + mailbox-app/scripts/rollback_upgrade.sh | 159 ++++++++ mailbox-app/scripts/upgrade.sh | 348 ++++++++++++++++++ mailbox-app/scripts/verify_upgrade_archive.py | 313 ++++++++++++++++ scripts/forensic_audit.py | 5 + scripts/test_upgrade.py | 229 ++++++++++++ 17 files changed, 1550 insertions(+), 94 deletions(-) create mode 100755 mailbox-app/scripts/rollback_upgrade.sh create mode 100755 mailbox-app/scripts/upgrade.sh create mode 100644 mailbox-app/scripts/verify_upgrade_archive.py create mode 100644 scripts/test_upgrade.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 06c52dc..19c6a91 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,6 +82,9 @@ jobs: - name: Release workflow contract tests run: python scripts/test_release_workflow.py + - name: Upgrade and rollback contract tests + run: python scripts/test_upgrade.py + - name: Dependency vulnerability audit run: python -m pip_audit --no-deps -r mailbox-app/requirements/locked.txt --progress-spinner off diff --git a/CHANGELOG.md b/CHANGELOG.md index e52debb..34e36ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,9 +18,17 @@ All notable repository-level changes are recorded here. Application history befo - Automated GitHub Release creation with deterministic source ZIP and SHA-256 assets, RC pre-release classification, stable latest classification, and overwrite/clobber prevention. - Added focused release-workflow contract tests and made them blocking in CI and the forensic audit. +### Existing-server upgrade and rollback + +- Added a generic fail-closed existing-server upgrade driver that requires a deterministic source ZIP plus matching SHA-256, validates canonical archive/manifest/version integrity, stages outside the live tree, and rejects downgrade, same-version, removed-migration, or modified-migration targets. +- Added explicit migration acknowledgement, a non-blocking runtime lock, a verified pre-mutation consistent data backup, source/runtime rollback snapshots, application/public-site staged replacement, dependency convergence, post-upgrade contract checks, and installation-marker provenance. +- Preserved Postfix and Dovecot during the source mutation window after the consistent backup has completed, allowing accepted inbound mail to accumulate safely in Maildir while ingestion is paused. +- Added migration-aware fail-closed recovery: automatic source/runtime rollback is allowed only when no new schema migration has begun; migration-capable failures require reviewed schema/data reconciliation instead of an automatic database restore that could discard newly accepted mail. +- Added focused non-destructive upgrade/archive/rollback contract tests and made them blocking in CI and the full forensic audit. + ### Compatibility -- No application runtime, database schema, migration, route, authorization, UI, mail-flow, installer behavior, deployment template, or service configuration changes are introduced by PHASE-004A or PHASE-004B. +- PHASE-004C changes maintained operational tooling only; it does not add a database migration, application route, authorization/UI/mail-flow behavior, installer behavior, deployment-template rewrite, DNS/TLS change, or automatic host-configuration migration. - `v1.3.0-rc.4` and its published source identity remain immutable; `1.3.0-rc.5` is the next development-candidate version and is not yet a published release. ## Unreleased — MailStack repository bootstrap diff --git a/docs/BACKUP_RESTORE.md b/docs/BACKUP_RESTORE.md index 612743e..33d9a59 100644 --- a/docs/BACKUP_RESTORE.md +++ b/docs/BACKUP_RESTORE.md @@ -41,3 +41,17 @@ The built-in application backup intentionally does not duplicate the release arc - an encrypted off-host copy of the built-in backup. Backups contain credentials, user email, attachments, and message content. Restrict access, encrypt at rest and in transit, verify checksums after transfer, and test restoration on an isolated host. + +## Upgrade rollback snapshots + +PHASE-004C upgrades create a source/runtime rollback snapshot under +`/var/backups/vibmail/upgrades/` and nest a normal consistent data backup beneath that snapshot before +any source mutation. The snapshot contains the pre-upgrade application tree, previous public-site +pointer, installation marker when present, metadata, and SHA-256 checksums. The nested data backup +retains its own `SHA256SUMS` contract. + +The upgrade rollback command intentionally does **not** restore MariaDB or Maildir automatically. +When an upgrade introduces new migrations, schema/data rollback requires a reviewed reconciliation +plan because restoring a pre-upgrade database/Maildir snapshot can discard or desynchronize mail +accepted after the backup. Use `restore.sh` only when that coordinated recovery decision has been +made and the post-backup mail window has been accounted for. diff --git a/docs/FORENSIC_AUDIT_REPORT.md b/docs/FORENSIC_AUDIT_REPORT.md index 8849f59..d1f03b0 100644 --- a/docs/FORENSIC_AUDIT_REPORT.md +++ b/docs/FORENSIC_AUDIT_REPORT.md @@ -1,10 +1,10 @@ # Forensic audit report — MailStack 1.3.0 RC5 development baseline -**PHASE-004B audit date:** 2026-08-17 +**PHASE-004C audit date:** 2026-08-17 **Repository development version:** `1.3.0-rc.5` **Latest published release candidate:** `v1.3.0-rc.4` **Target runtime:** Ubuntu Server 24.04 LTS and CPython 3.12 -**Current classification:** RC4 release qualification finalized; PHASE-004A branch CI passed; RC5 PHASE-004B requalification pending +**Current classification:** RC4 official baseline preserved; PHASE-004A/004B branch qualification passed; PHASE-004C locally source-qualified and pending GitHub requalification ## Executive disposition @@ -33,14 +33,20 @@ | PHASE-004A branch GitHub CI run `32087558399` | PASS | | PHASE-004B release automation contracts | PASS locally — 7 focused contracts | | PHASE-004B structural forensic audit | PASS locally — 407 files, 146 Python, 13 shell, zero blocking findings | -| PHASE-004B GitHub branch CI | PENDING after delta apply/push | +| PHASE-004B GitHub branch CI run `32093468669` | PASS — exact commit `ee90764335f8724727cea86e0af035c049c79e62` | +| PHASE-004C upgrade/archive/rollback contracts | PASS locally | +| PHASE-004C structural forensic audit | PASS locally — 411 files, 148 Python, 15 shell, zero blocking findings | +| PHASE-004C live existing-server upgrade | NOT EXECUTED — reserved for PHASE-004D | +| PHASE-004C GitHub branch CI | PENDING after delta apply/push | **OFFICIAL_RC4_SOURCE_BASELINE:** PASS **RC4_OPEN_SOURCE_RELEASE_CANDIDATE:** PASS **PHASE_004A_REMOTE_QUALIFICATION:** PASS — branch CI `32087558399` **PHASE_004B_LOCAL_QUALIFICATION:** PASS -**PHASE_004B_REMOTE_QUALIFICATION:** PENDING GITHUB CI -**RC5_DEVELOPMENT_REQUALIFICATION:** PENDING PHASE-004B GITHUB CI +**PHASE_004B_REMOTE_QUALIFICATION:** PASS — branch CI `32093468669` +**PHASE_004C_LOCAL_QUALIFICATION:** PASS +**PHASE_004C_REMOTE_QUALIFICATION:** PENDING GITHUB CI +**RC5_DEVELOPMENT_REQUALIFICATION:** PENDING PHASE-004C GITHUB CI **STABLE_PRODUCTION_ACCEPTANCE:** PENDING ## Official RC4 baseline identity @@ -143,7 +149,32 @@ verified-build job plus a tag-only publication job with `actions: read` and `con Publication requires matching VERSION/package/tag identity, the exact current `main` head, successful `main` push CI for the tagged SHA, and proof that no release already exists. Deterministic ZIP/SHA assets are published without `--clobber` or release editing. Manual dispatch remains build-only. -No application/runtime/deployment/VPS behavior is changed. +GitHub branch CI run `32093468669` passed this boundary on exact commit +`ee90764335f8724727cea86e0af035c049c79e62`, including 198 application tests at 95.00 percent coverage, +seven release-workflow contracts, zero-blocking full forensic audit, and deterministic source +build/verification with SHA-256 `fdfff6c1e4ec409d950e3d612be1feab1ac7987f8d436c3ef3c0fc6ee1865bb5`. + +## PHASE-004C existing-server upgrade/rollback boundary + +PHASE-004C adds operational source/runtime tooling without changing Django application behavior, +schema, Postfix/Dovecot routing, deployment templates, DNS, TLS, or the existing VPS. The generic +upgrader requires both deterministic release assets, validates canonical ZIP/source-manifest and +version identity, rejects same/downgrade and migration-history rewrites, acquires a runtime lock, +creates a source snapshot plus the maintained consistent data backup before mutation, stages source +outside the live tree, converges application/public-site dependencies, and runs post-upgrade +verification. New migration files require explicit acknowledgement. + +After the pre-upgrade consistent backup returns, the source mutation window stops Gunicorn, ingestion, +and the contact worker while leaving Postfix and Dovecot active so accepted messages can accumulate +in Maildir. For a no-new-migration target, source/runtime failure recovery can restore the prior app +and public-site pointer. Once a migration-capable upgrade begins schema mutation, automatic source or +database rollback is refused; the coordinated backup/snapshot are reported for reviewed reconciliation. +The standalone rollback command never restores MariaDB or Maildir implicitly. + +The PHASE-004C local structural audit passed 411 files, 148 Python files, 15 shell files, zero blocking +findings, and the focused upgrade/archive/rollback contract suite. No live VPS upgrade is claimed; +PHASE-004D is the real existing-server acceptance boundary. GitHub CI requalification remains pending +after the PHASE-004C delta is applied and pushed. ## Security review @@ -170,5 +201,6 @@ privilege, systemd confinement, archive safety, checksum verification, and fail- The published `v1.3.0-rc.4` source is a qualified release candidate and is the official frozen source baseline for PHASE-004. PHASE-004A corrects the documentation evidence around that baseline but does -not retroactively modify or retag RC4. The working repository version `1.3.0-rc.5` remains a -development candidate until its branch GitHub CI passes. +not retroactively modify or retag RC4. The working repository version `1.3.0-rc.5` remains a development candidate. PHASE-004B branch CI +has passed, while PHASE-004C still requires dependency-backed GitHub CI and no live upgrade is claimed +until the separately approved PHASE-004D acceptance campaign. diff --git a/docs/FORENSIC_FILE_INVENTORY.json b/docs/FORENSIC_FILE_INVENTORY.json index db8be28..526f325 100644 --- a/docs/FORENSIC_FILE_INVENTORY.json +++ b/docs/FORENSIC_FILE_INVENTORY.json @@ -72,10 +72,10 @@ }, { "kind": "text", - "lines": 132, + "lines": 135, "path": ".github/workflows/ci.yml", - "sha256": "96046dd5a995af85fdda2987727eb18687affd1d16e652eddf43bd4f32d77177", - "size_bytes": 4206 + "sha256": "29e68ada609952c8d3d2c958b6298f1fb5ed5149bf5a2c8d72498b429d6aa11a", + "size_bytes": 4301 }, { "kind": "text", @@ -93,10 +93,10 @@ }, { "kind": "text", - "lines": 117, + "lines": 125, "path": "CHANGELOG.md", - "sha256": "089e1b03250a3cfd78f0f304daf646fbb3a4336b9d112b997c79b46c8b883a2a", - "size_bytes": 10254 + "sha256": "77076ab4fa3602cd3770c6208a82386e1594616a13d9f8b5bf690c10f4878f49", + "size_bytes": 11553 }, { "kind": "text", @@ -453,10 +453,10 @@ }, { "kind": "text", - "lines": 43, + "lines": 57, "path": "docs/BACKUP_RESTORE.md", - "sha256": "48f16eca276d263122acaabfb6f4b1ee26519b968670643effaaa17aa61f75fc", - "size_bytes": 2097 + "sha256": "bee325fd49907c223f1c7031c570319fafd2c5728f70ff131d8f6ad3a03a9ff5", + "size_bytes": 2990 }, { "kind": "text", @@ -537,10 +537,10 @@ }, { "kind": "text", - "lines": 174, + "lines": 206, "path": "docs/FORENSIC_AUDIT_REPORT.md", - "sha256": "0b890c9ad3ee692d1883621e57f5dadb09e7d0184c4d7a66007ff58faaf86039", - "size_bytes": 9483 + "sha256": "02abfd11d1ed31948bc8249d496ba042ff9f2268de7109c99a987656dd53e10f", + "size_bytes": 12139 }, { "kind": "text", @@ -593,10 +593,10 @@ }, { "kind": "text", - "lines": 51, + "lines": 77, "path": "docs/OPERATIONS.md", - "sha256": "25d347928a3b5ad7884632b19f3221160d4b1dffe867361831e126cd88eae660", - "size_bytes": 1772 + "sha256": "1264661dafb912103cdb1921c518bdb152b60dbda115037d8ebccd27a87726d4", + "size_bytes": 2719 }, { "kind": "text", @@ -642,10 +642,10 @@ }, { "kind": "text", - "lines": 109, + "lines": 117, "path": "docs/RELEASE_NOTES_1.3.0.md", - "sha256": "ff942f681e2b4f87c0d90c0c7ca8f40cfc3d0ef6c3151298a72308b117b402f4", - "size_bytes": 7312 + "sha256": "341b3656b21ccbcf7fb11b1f537d418df995d950e09c400d228abdafd2acde2e", + "size_bytes": 8304 }, { "kind": "text", @@ -670,10 +670,10 @@ }, { "kind": "text", - "lines": 144, + "lines": 176, "path": "docs/TEST_REPORT.md", - "sha256": "589c7fbc9d8149a00abfeccabefd6c55966c2ecd802d32bfb4492437fa6b9358", - "size_bytes": 7306 + "sha256": "89bb9ba2d85ba7fcd0f19926527ece7da5aeda3e2bd68f29b2b9e47c1edbf140", + "size_bytes": 9468 }, { "kind": "text", @@ -691,10 +691,10 @@ }, { "kind": "text", - "lines": 12, + "lines": 155, "path": "docs/UPGRADE.md", - "sha256": "b5dcb27cdd26d5730f65f09df15b4544865ed504ce68d80346e0c931031c4949", - "size_bytes": 848 + "sha256": "5b70ed1fc640b7e723f2fc551285971791d6d6b80a2e56e401e0d12d19fdd3dc", + "size_bytes": 7025 }, { "kind": "text", @@ -714,7 +714,7 @@ "kind": "text", "lines": 221, "path": "documents/DOCUMENTATION_MANIFEST.json", - "sha256": "89d85be3dbbbedfe41cc1c6a9f67ae73da0ce4699b95cbc3287148b7010a31ed", + "sha256": "cc14ac4f645f9fcadefc0e5847856919abbc92d91e60774a7622794da0e2a980", "size_bytes": 8429 }, { @@ -824,10 +824,10 @@ }, { "kind": "text", - "lines": 91, + "lines": 126, "path": "documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md", - "sha256": "a5205df42de3faca80f7107405704dd2f79200b9340d66d493f814eb70d5d6b3", - "size_bytes": 5265 + "sha256": "b3cef810cf4dfa826f079546b795c27dcb9ab89068f40bada5b048726e7eefb4", + "size_bytes": 8284 }, { "kind": "text", @@ -874,10 +874,10 @@ }, { "kind": "text", - "lines": 81, + "lines": 87, "path": "mailbox-app/CHANGELOG.md", - "sha256": "7268a8a324e36a8c3e6792db622891950e3a5fc225a0f0f3c0269f9cecf98e06", - "size_bytes": 5156 + "sha256": "73caea1e6b61cc5224811f79cac5f705115bbb49c360b9ec4084c42022e9a3d0", + "size_bytes": 5757 }, { "kind": "text", @@ -3299,6 +3299,34 @@ ], "size_bytes": 2687 }, + { + "kind": "text", + "lines": 159, + "path": "mailbox-app/scripts/rollback_upgrade.sh", + "sha256": "ce43c49d90cbd70765075b8a0ccde9d1676cefeb134ba63df3f9e1eb817ea40d", + "shell_functions": [ + "die", + "run_app", + "usage", + "validate_tar_archive" + ], + "size_bytes": 6050 + }, + { + "kind": "text", + "lines": 348, + "path": "mailbox-app/scripts/upgrade.sh", + "sha256": "8df2094eaf3fba06baa1197b82e33350c27ab92edbc6bdee0aada6a888782915", + "shell_functions": [ + "cleanup_stage", + "die", + "restore_pre_upgrade_source", + "rollback_on_failure", + "run_app", + "usage" + ], + "size_bytes": 15624 + }, { "kind": "text", "lines": 82, @@ -3342,6 +3370,43 @@ "sha256": "787c840030e1f44d77f4cfff7d9d62efd0733fe92a63b1213719c02c94d49e23", "size_bytes": 2682 }, + { + "kind": "text", + "lines": 313, + "path": "mailbox-app/scripts/verify_upgrade_archive.py", + "python": { + "classes": [ + "UpgradeArchiveError" + ], + "functions": [ + "compare_migrations", + "main", + "migration_map_from_archive", + "migration_map_from_current", + "normalized_version", + "package_from_release", + "read_checksum", + "read_current_version", + "safe_extract", + "sha256", + "sha256_bytes", + "verify_archive" + ], + "imports": [ + "__future__", + "argparse", + "hashlib", + "pathlib", + "re", + "shutil", + "tomllib", + "zipfile" + ], + "methods": [] + }, + "sha256": "395fbbe44bbe3dc7cd1412be5f11504f99e9f0552a2a8d6fc5d6b822ca46d70d", + "size_bytes": 13714 + }, { "kind": "text", "lines": 78, @@ -4691,7 +4756,7 @@ }, { "kind": "text", - "lines": 400, + "lines": 405, "path": "scripts/forensic_audit.py", "python": { "classes": [], @@ -4717,8 +4782,8 @@ ], "methods": [] }, - "sha256": "5d50f62a3d041a98d9420a739c4d05dafe61739e0228c340a7920ecfa34f45c3", - "size_bytes": 14567 + "sha256": "907b47435ba9e9284e98fe24feecf36d0b4153a7fc8b9dee8b3be9232a51bee5", + "size_bytes": 14832 }, { "kind": "text", @@ -5122,6 +5187,36 @@ "sha256": "c77bf4bbdacc4848a0de4a271df9b8978b61b7b4da77737b4f6c2f3cc3e29eb8", "size_bytes": 7417 }, + { + "kind": "text", + "lines": 229, + "path": "scripts/test_upgrade.py", + "python": { + "classes": [], + "functions": [ + "add_member", + "build_fixture_archive", + "load_verifier", + "main", + "read_shell", + "require" + ], + "imports": [ + "__future__", + "hashlib", + "importlib.util", + "pathlib", + "shell_runtime", + "subprocess", + "sys", + "tempfile", + "zipfile" + ], + "methods": [] + }, + "sha256": "c65f0768ccdb219621bde802db63e16faef8716f4f41e63f3a212d1dfab4ef30", + "size_bytes": 9665 + }, { "kind": "text", "lines": 167, @@ -5181,15 +5276,15 @@ }, "summary": { "binary_files": 25, - "files": 406, - "python_classes": 75, - "python_files": 146, - "python_functions": 428, + "files": 410, + "python_classes": 76, + "python_files": 148, + "python_functions": 446, "python_methods": 61, - "shell_files": 13, - "shell_functions": 31, - "text_files": 381, - "total_bytes": 31683432, - "total_text_lines": 27377 + "shell_files": 15, + "shell_functions": 41, + "text_files": 385, + "total_bytes": 31747591, + "total_text_lines": 28738 } } diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 5d730b4..3d88c35 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -49,3 +49,29 @@ sudo /opt/vibmail/app/scripts/restore.sh \ See `docs/BACKUP_RESTORE.md` before restoring production data. + +## Controlled existing-server upgrade + +PHASE-004C provides the generic source/runtime upgrade and rollback mechanism. It is source-qualified +only until the separate PHASE-004D real-VPS acceptance campaign. Review `docs/UPGRADE.md` before any +live use. + +```bash +sudo /opt/vibmail/app/scripts/upgrade.sh \ + --archive /root/releases/mailstack-X.Y.Z-source.zip \ + --checksum /root/releases/mailstack-X.Y.Z-source.zip.sha256 \ + --confirm-upgrade +``` + +New migration files require `--allow-migrations` after review. Successful upgrades print the exact +rollback snapshot and nested consistent data-backup paths. For a no-schema-change source/runtime +rollback: + +```bash +sudo /opt/vibmail/app/scripts/rollback_upgrade.sh \ + --snapshot /var/backups/vibmail/upgrades/TIMESTAMP-from-X-to-Y \ + --confirm-rollback +``` + +Do not treat that command as a database rollback. See `docs/UPGRADE.md` and +`docs/BACKUP_RESTORE.md` for migration-aware recovery rules. diff --git a/docs/RELEASE_NOTES_1.3.0.md b/docs/RELEASE_NOTES_1.3.0.md index 2e7db65..60240bb 100644 --- a/docs/RELEASE_NOTES_1.3.0.md +++ b/docs/RELEASE_NOTES_1.3.0.md @@ -107,3 +107,11 @@ RC1 established the configurable Ubuntu 24.04 installer, MariaDB/Postfix/Dovecot templates, reproducible source packaging, public governance/security documentation, Django 5.2.16 security pin, protected user/documentation baseline, UI design intake, and shared application shell. RC4 does not replace or redesign those foundations; it preserves the RC2 operational hardening and RC3 dependency-security fix while hardening only cross-platform repository audit execution. + +## Controlled upgrade tooling + +- PHASE-004C adds a fail-closed existing-server source/runtime upgrade driver requiring the deterministic source ZIP and matching SHA-256. +- The upgrader verifies canonical ZIP metadata and source manifest integrity, rejects same/downgrade and migration-history rewrites, creates a consistent data backup plus rollback snapshot before mutation, and stages application/public-site source outside the live tree. +- New migration files require explicit acknowledgement. A failure after schema mutation begins refuses automatic source rollback and reports the coordinated backup/snapshot for reviewed reconciliation. +- Postfix and Dovecot remain active during the source mutation window after the pre-upgrade consistent backup returns; Gunicorn, ingestion, and the contact worker are the application services stopped for replacement. +- This release contains the mechanism and automated contracts only. The first real existing-VPS upgrade is deferred to PHASE-004D. diff --git a/docs/TEST_REPORT.md b/docs/TEST_REPORT.md index 9c6c575..11a5749 100644 --- a/docs/TEST_REPORT.md +++ b/docs/TEST_REPORT.md @@ -1,6 +1,6 @@ # Test report — MailStack 1.3.0 RC5 development baseline -**PHASE-004B verification date:** 2026-08-17 +**PHASE-004C verification date:** 2026-08-17 **Latest published release candidate:** `v1.3.0-rc.4` **Repository development version:** `1.3.0-rc.5` @@ -74,11 +74,13 @@ gates. PHASE-004B adds seven focused release-workflow contracts covering version normalization, RC/stable classification, tag identity, manual-dispatch non-publication, package-version mismatch rejection, successful-main-CI evidence matching, existing-release fail-close behavior, exact-main-head guarding, -and required workflow protections. The assembled candidate passes all seven contracts locally. They -are blocking in the main CI workflow and are also invoked by the forensic audit. The structural -forensic audit passes with 407 files scanned, 146 Python files, 13 shell files, and zero blocking -findings. GitHub branch CI remains the final authority. No fake public tag/release is created for -testing. +and required workflow protections. GitHub branch CI run `32093468669` passed on exact commit +`ee90764335f8724727cea86e0af035c049c79e62` using Ubuntu 24.04 and Python 3.12.13. The run passed all +seven release-workflow contracts, the dependency vulnerability audit, Ruff/Bandit, all 198 Django +tests at 95.00 percent coverage, full forensic audit with 407 files/146 Python/13 shell files and zero +blocking findings, and deterministic RC5 source build/verification. The deterministic archive SHA-256 +was `fdfff6c1e4ec409d950e3d612be1feab1ac7987f8d436c3ef3c0fc6ee1865bb5`. No fake public tag/release +was created for testing; legitimate post-merge tag publication remains the end-to-end release event. ## Dependency advisory qualification @@ -132,10 +134,40 @@ assembled local candidate passed the following dependency-free/structural gates: | Forensic inventory check | PASS — 404 maintained entries | | Structural forensic audit | PASS — 405 files scanned, 144 Python, 13 shell, zero blocking findings | -The final assembled delta is additionally checked with `git diff --check` before handoff. -Dependency-backed GitHub CI remains the final authority for the RC5 development candidate. Until -that branch workflow passes, PHASE-004A is locally qualified only and `1.3.0-rc.5` must not be -published as a release candidate. +The final assembled PHASE-004A delta was additionally checked with `git diff --check`; its branch CI +closure is recorded above. `1.3.0-rc.5` remains a development candidate until the complete intended +RC5 line is merged, requalified on `main`, tagged, and published through the release workflow. + +## PHASE-004C upgrade/rollback tooling qualification + +PHASE-004C adds three maintained operational components: the generic `upgrade.sh` driver, +`rollback_upgrade.sh`, and `verify_upgrade_archive.py`, plus a focused non-destructive contract suite. +The local structural candidate passed deterministic archive/checksum fixture verification, canonical +source-manifest checks, version ordering, migration-delta detection, bad-checksum fail-close behavior, +Bash syntax checks, runtime-lock contracts, pre-mutation backup/rollback requirements, inbound-service +continuity assertions, and migration-aware rollback refusal. + +| PHASE-004C local gate | Result | +|---|---| +| Upgrade/archive/rollback contracts | PASS | +| Documentation validation | PASS — 46 required files, 59 local links checked | +| Managed-document synchronization/check | PASS — 17 documents, 5 phases | +| Documentation tests | PASS — 4 contracts | +| Design manifest integrity | PASS — 25 source images | +| Design tests | PASS — 4 contracts | +| Shared UI foundation contracts | PASS — 8 contracts | +| Deployment template validation | PASS — 13 templates | +| Installer contracts | PASS — 2 valid, 9 invalid plans | +| Existing operations contracts | PASS — 4 scripts | +| Release-workflow contracts | PASS — 7 contracts | +| Forensic inventory | PASS — 410 maintained entries plus the inventory file | +| Structural forensic audit | PASS — 411 files, 148 Python, 15 shell, zero blocking findings | + +PHASE-004C intentionally does not claim a live server upgrade. It changes operational tooling and +documentation, not Django application business logic or schema. Dependency/security/application +regression and the deterministic release build must still pass in the user's isolated Python 3.12 +validation and GitHub CI after the delta is applied. PHASE-004D owns the first real existing-VPS +upgrade acceptance. ## Manual acceptance still outstanding before stable `1.3.0` diff --git a/docs/UPGRADE.md b/docs/UPGRADE.md index 115b5c1..f979a75 100644 --- a/docs/UPGRADE.md +++ b/docs/UPGRADE.md @@ -1,12 +1,155 @@ -# Upgrade policy - -1. Back up both MariaDB schemas, `/var/vmail`, attachments, environment files, mail configuration, Nginx configuration, systemd units, and certificates. -2. Verify the backup checksum on another machine. -3. Review release notes and migrations. -4. Stage the new source outside `/opt/vibmail/app`. -5. Run tests, source-safety audit, and release verification. -6. Use the versioned application upgrade scripts or a reviewed maintenance procedure; the clean-install path must not be used to overwrite an unreviewed production stack. -7. Run migration, Postfix-view, Maildir, health, login, authorization, live-update, attachment, and real inbound-mail acceptance tests. -8. Roll back immediately if a mandatory gate fails. - -Legacy v1.2.1 upgrade and rollback documents remain under `mailbox-app/docs/` for existing MailStack deployments. +# Upgrade policy and controlled upgrade tool + +MailStack clean installation and existing-server upgrade are separate operations. Do **not** rerun +`install.sh` over an existing production deployment to move between releases. + +PHASE-004C adds a fail-closed source/runtime upgrade mechanism for an already installed MailStack +server. The tool changes application/public-site source and Python dependencies only. It deliberately +preserves `/etc/vibmail`, Postfix, Dovecot, Nginx, systemd, TLS, Maildir, MariaDB data, and existing +host configuration unless a future separately reviewed migration explicitly changes those contracts. + +> PHASE-004C provides and source-qualifies the mechanism. The first real existing-VPS execution is a +> separate PHASE-004D acceptance activity and is not claimed by this document. + +## Required release inputs + +Use only the deterministic release assets published for the target version: + +```text +mailstack-X.Y.Z-source.zip +mailstack-X.Y.Z-source.zip.sha256 +``` + +Both files are mandatory. The upgrade verifier checks the SHA-256 filename binding, ZIP integrity, +canonical archive metadata, the complete `SOURCE_MANIFEST.sha256`, version/package-version agreement, +and the current-to-target version direction before extracting any source. + +The target must be newer than the installed version. Same-version reinstall and downgrade are +rejected by the generic upgrade path. + +## Pre-upgrade preparation + +1. Review the target release notes and any migration notes. +2. Confirm MariaDB, Postfix, Dovecot, Nginx, Gunicorn, ingestion, and the public contact service are healthy. +3. Copy the deterministic ZIP and checksum to a root-only location on the server. +4. Keep an independent off-host backup and the currently installed release artifact available. +5. Run the command in a resilient `tmux` or `screen` session during an approved maintenance window. + +The current application verifier is executed before any mutation. Configuration syntax checks for +Postfix, Dovecot, and Nginx must also pass. + +## Upgrade command + +From an installed release that contains the PHASE-004C tooling: + +```bash +sudo /opt/vibmail/app/scripts/upgrade.sh \ + --archive /root/releases/mailstack-X.Y.Z-source.zip \ + --checksum /root/releases/mailstack-X.Y.Z-source.zip.sha256 \ + --confirm-upgrade +``` + +If the verified target introduces new Django migration files, the command stops before mutation and +requires explicit review/acknowledgement: + +```bash +sudo /opt/vibmail/app/scripts/upgrade.sh \ + --archive /root/releases/mailstack-X.Y.Z-source.zip \ + --checksum /root/releases/mailstack-X.Y.Z-source.zip.sha256 \ + --allow-migrations \ + --confirm-upgrade +``` + +Existing migration files may not be removed or modified by the target archive. Either condition is a +blocking integrity failure. + +## What the upgrade does + +The tool acquires the non-blocking `/run/lock/vibmail-upgrade.lock`, verifies the current deployment, +verifies and stages the target release, creates a rollback snapshot under +`/var/backups/vibmail/upgrades/`, and runs the maintained consistent backup command **before** source +mutation. + +The consistent data backup may briefly stop mail-facing services in order to capture the existing +backup contract. After that backup completes and the original service state is restored, the source +mutation window stops only Gunicorn, ingestion, and the public contact worker. Postfix and Dovecot +remain active so accepted inbound messages can continue to land in Maildir while ingestion is paused. + +The tool then: + +- atomically stages verified source outside `/opt/vibmail/app`; +- replaces application source while preserving runtime/environment state outside the source tree; +- converges the existing Python virtual environment to the target production requirements and runs + `pip check`; +- applies approved migrations and validates schema, Postfix, Maildir, counters, static assets, and + Django deployment checks; +- creates a new versioned public-site release and switches the `current` symlink only after rendering + and dependency installation succeed; +- updates `/etc/vibmail/installation.json` with previous/target version and verified source SHA-256; +- validates Postfix, Dovecot, and Nginx configuration without rewriting those configurations; +- restarts the application/contact workers and runs the maintained application verifier plus local + HTTPS acceptance checks. + +Success prints `MAILSTACK_UPGRADE=PASS`, the source SHA-256, the rollback snapshot path, and the +coordinated data-backup path. + +## Failure and rollback behavior + +For an upgrade with **no new migrations**, a failure after source mutation triggers automatic +application/runtime rollback from the pre-upgrade snapshot and restores the previous public-site +pointer. The previous release requirements are reinstalled before services are restarted. + +If a migration-capable upgrade fails after schema mutation begins, automatic source rollback is +intentionally refused. The tool prints: + +```text +UPGRADE_ROLLBACK=MANUAL_SCHEMA_RECONCILIATION_REQUIRED +``` + +and identifies both the source snapshot and coordinated data backup. This avoids silently pairing +old source with an unproven forward database schema or restoring a database snapshot that could +discard mail accepted after the backup. + +For a reviewed source/runtime rollback when no schema change occurred: + +```bash +sudo /opt/vibmail/app/scripts/rollback_upgrade.sh \ + --snapshot /var/backups/vibmail/upgrades/TIMESTAMP-from-X-to-Y \ + --confirm-rollback +``` + +A snapshot that records new migrations fails closed unless `--accept-forward-schema` is explicitly +provided after review. Even with that acknowledgement, the rollback command never restores MariaDB +or Maildir automatically. A true schema/data rollback must use the coordinated backup and the +reviewed restore/reconciliation procedure in `docs/BACKUP_RESTORE.md`. + +## Preserved host contracts + +The generic PHASE-004C upgrade path does not rewrite: + +- `/etc/vibmail` secrets or environment files; +- Postfix or Dovecot configuration; +- Nginx configuration; +- systemd units; +- Let's Encrypt certificates or renewal hooks; +- `/var/vmail` Maildir data; +- MariaDB databases except for explicitly approved Django migrations; +- DNS, MX, PTR/rDNS, firewall, provider, or n8n configuration. + +A release that requires one of those changes needs a separately reviewed migration procedure rather +than silently extending this generic source/runtime upgrader. + +## Post-upgrade acceptance + +After a real upgrade, retain the snapshot until all acceptance checks complete. At minimum verify: + +1. all seven MailStack-related services are active; +2. login and authorization boundaries; +3. mailbox list/message rendering and attachments; +4. Postfix unknown-recipient rejection; +5. real external inbound SMTP → LMTP → Maildir → ingestion → browser visibility; +6. public contact delivery; +7. backup creation after the upgrade; +8. restart/reboot recovery. + +The first real existing-server execution of this PHASE-004C mechanism belongs to PHASE-004D. diff --git a/documents/DOCUMENTATION_MANIFEST.json b/documents/DOCUMENTATION_MANIFEST.json index 0876881..dc77bd4 100644 --- a/documents/DOCUMENTATION_MANIFEST.json +++ b/documents/DOCUMENTATION_MANIFEST.json @@ -201,7 +201,7 @@ "last_reviewed": "2026-08-17", "path": "documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md", "phase_id": "PHASE-004", - "sha256": "a5205df42de3faca80f7107405704dd2f79200b9340d66d493f814eb70d5d6b3", + "sha256": "b3cef810cf4dfa826f079546b795c27dcb9ab89068f40bada5b048726e7eefb4", "status": "active", "title": "Release, Upgrade and Operational Reliability", "version": "1.3.0-rc.5" diff --git a/documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md b/documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md index dca4044..2c2a6f3 100644 --- a/documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md +++ b/documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md @@ -16,7 +16,8 @@ phase_id: PHASE-004 Make post-RC4 release handling, upgrades, rollback, and operational acceptance reproducible without weakening the receive-only architecture or disturbing an already working MailStack deployment. PHASE-004A finalized the forensic/documentation baseline. PHASE-004B adds fail-closed automatic -tag-to-GitHub-Release publication while keeping upgrade/runtime changes for later approved subphases. +tag-to-GitHub-Release publication. PHASE-004C adds the separately approved generic existing-server +source/runtime upgrade and rollback mechanism while leaving real VPS execution for PHASE-004D. ## Scope @@ -33,16 +34,34 @@ job has read-only repository access. A separate tag-only publication job receive `contents: write`, downloads the verified workflow artifact, and creates the GitHub Release with the deterministic ZIP and checksum. Manual workflow dispatch remains build/validation only. -Later approved PHASE-004 subphases may address a controlled existing-server upgrade/rollback -mechanism and operational backup/restore and reboot acceptance. Those runtime changes are not -implemented by PHASE-004A or PHASE-004B. +PHASE-004C is restricted to the controlled existing-server upgrade/rollback mechanism, its +non-destructive contracts, and required documentation/forensic integration. The upgrader requires a +deterministic target source ZIP and matching SHA-256, verifies canonical archive/source-manifest and +version/package identity, rejects same-version/downgrade and migration-history rewrites, acquires a +non-blocking runtime lock, and creates both a source/runtime rollback snapshot and the maintained +consistent data backup before mutation. New migration files require explicit `--allow-migrations` +acknowledgement. After that backup completes, the application mutation window leaves Postfix and +Dovecot active while Gunicorn, ingestion, and the contact worker are stopped, allowing accepted mail +to accumulate in Maildir. Application/public-site source and Python dependencies are staged and +verified without rewriting host mail/web/TLS/systemd configuration. + +For a no-new-migration target, a failure after mutation can automatically restore the prior +application/runtime source and public-site pointer. If a migration-capable upgrade fails after schema +mutation begins, automatic source/database rollback is refused and the tool reports the coordinated +data backup and source snapshot for reviewed reconciliation. The standalone rollback command never +restores MariaDB or Maildir implicitly and requires explicit forward-schema acknowledgement when its +snapshot records new migrations. + +PHASE-004C does not execute this mechanism on the existing production/staging VPS. The first real +controlled existing-server upgrade is reserved for PHASE-004D. Backup/restore and reboot acceptance +remain later PHASE-004 operational gates. ## User-facing changes -There is no application UI or mailbox behavior change in PHASE-004A or PHASE-004B. Maintainers and -operators gain a single explicit official source baseline for RC4, corrected RC4 qualification -evidence, and a tag-driven release workflow that removes the need to upload deterministic release -assets manually from a workstation. +There is no application UI or mailbox behavior change in PHASE-004A, PHASE-004B, or PHASE-004C. +Maintainers and operators gain a single explicit official source baseline for RC4, corrected RC4 +qualification evidence, tag-driven release publication, and a reviewed source/runtime upgrade and +rollback tool that fails closed around archive integrity, migration risk, and recovery provenance. ## How to use @@ -55,13 +74,21 @@ current `main` head, and push the tag. The release workflow then builds/verifies source archive and publishes the GitHub Release automatically. `workflow_dispatch` validates and builds artifacts but never publishes a release. +For an existing-server source/runtime upgrade, provide both published deterministic assets to +`/opt/vibmail/app/scripts/upgrade.sh`: `mailstack-X.Y.Z-source.zip` and its matching `.sha256`. The +target must be newer than the installed version. New migration files require explicit review and +`--allow-migrations`; modified or removed historical migration files are rejected. Successful runs +print the exact rollback snapshot and nested consistent-data-backup paths. See `docs/UPGRADE.md` for +the migration-aware rollback rules. PHASE-004D, not PHASE-004C, owns the first real VPS execution. + ## Compatibility -PHASE-004A and PHASE-004B introduce no model, migration, URL, application permission, template, CSS, -JavaScript, mail-flow, ingestion, installer, deployment-template, service, database, DNS, TLS, or -existing-VPS change. PHASE-004B changes only repository release automation, audit tooling, focused -contract tests, and required documentation/manifests. The `v1.3.0-rc.4` tag and deterministic RC4 -source archive remain unchanged. No data/configuration migration or VPS rollback is required. +PHASE-004A and PHASE-004B introduce no application/runtime change. PHASE-004C changes operational +source/runtime upgrade tooling only: it adds no application model or migration, URL, permission, UI, +mail-flow behavior, installer behavior, deployment-template rewrite, database schema change, DNS, TLS, +or live existing-VPS mutation. Postfix, Dovecot, Nginx, systemd, `/etc/vibmail`, certificates, Maildir, +and MariaDB data remain outside generic source replacement except for explicitly acknowledged future +Django migrations. The `v1.3.0-rc.4` tag and deterministic RC4 source archive remain unchanged. ## Verification @@ -75,11 +102,16 @@ PHASE-004A branch CI run `32087558399` passed all blocking gates, including 198 95.00 percent coverage, dependency audit, full forensic audit, deterministic RC5 source build, and release verification. -PHASE-004B must pass its release-workflow contract tests, documentation/design/inventory gates, -installer/operations regression contracts, dependency/security/application CI, full forensic audit, -and deterministic release build/verification before merge. Actual publication is intentionally not -tested by creating a fake public tag/release; the first legitimate post-merge release tag is the -end-to-end publication acceptance event. +PHASE-004B branch CI run `32093468669` passed on exact commit +`ee90764335f8724727cea86e0af035c049c79e62`, including seven release-workflow contracts, 198 Django +tests at 95.00 percent coverage, dependency/security gates, full forensic audit with zero blocking +findings, and deterministic source build/verification. Actual publication remains intentionally +reserved for a legitimate post-merge release tag rather than a fake public release. + +PHASE-004C must pass its upgrade/archive/rollback contracts, existing installer/operations/release +contracts, documentation/design/inventory gates, dependency/security/application CI, full forensic +audit, and deterministic release build/verification. Its automated tests are non-destructive and do +not claim a real host upgrade; PHASE-004D is the live acceptance boundary. ## Documentation impact @@ -88,4 +120,7 @@ dependency/performance evidence, forensic/test reports, canonical baseline, UI i status, active managed-document version metadata, generated manifests, and this phase record. PHASE-004B updates the release workflow, release gate/contract tests, root changelog, release/build/ publishing documentation, release notes, forensic/test evidence, generated forensic/documentation -manifests, and this phase record. No application user workflow guide requires a behavioral change. +manifests, and this phase record. PHASE-004C updates operational upgrade/rollback scripts, a target +archive verifier, focused upgrade contracts, CI/full-forensic enforcement, upgrade/backup/operations +documentation, changelogs/release notes, forensic/test evidence, generated manifests/inventory, and +this phase record. No application UI/user workflow guide requires a behavioral change. diff --git a/mailbox-app/CHANGELOG.md b/mailbox-app/CHANGELOG.md index 6f8e064..027fa9d 100644 --- a/mailbox-app/CHANGELOG.md +++ b/mailbox-app/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.3.0-rc.5 — Unreleased + +- PHASE-004C controlled existing-server upgrade tooling now verifies deterministic release ZIP/SHA assets, creates a coordinated data backup and source rollback snapshot before mutation, stages application/public-site source, converges Python dependencies, gates migrations explicitly, preserves mail-facing services during the source mutation window, and performs post-upgrade verification. +- Added a reviewed source/runtime rollback command that verifies snapshot checksums and refuses implicit database/Maildir restoration or unacknowledged forward-schema rollback. + + ## 1.3.0-rc.5 — Forensic/docs baseline finalization (unreleased) - Finalized RC4 qualification evidence and the official source-baseline record for PHASE-004A. diff --git a/mailbox-app/scripts/rollback_upgrade.sh b/mailbox-app/scripts/rollback_upgrade.sh new file mode 100755 index 0000000..1b40a6f --- /dev/null +++ b/mailbox-app/scripts/rollback_upgrade.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +IFS=$'\n\t' +umask 077 + +usage() { + cat >&2 <<'EOF' +Usage: + sudo /opt/vibmail/app/scripts/rollback_upgrade.sh \ + --snapshot /var/backups/vibmail/upgrades/TIMESTAMP-from-X-to-Y \ + [--accept-forward-schema] \ + --confirm-rollback + +This restores application/runtime source and the previous public-site pointer. +It never restores MariaDB or Maildir automatically. If the upgrade introduced migrations, +rollback fails closed unless --accept-forward-schema is explicitly supplied after review. +EOF + exit 2 +} + +die() { printf 'ROLLBACK_FINDING=%s\n' "$*" >&2; exit 1; } +validate_tar_archive() { + /usr/bin/python3 - "$1" <<'PY' +import pathlib +import sys +import tarfile +archive = pathlib.Path(sys.argv[1]) +with tarfile.open(archive, "r:gz") as handle: + for member in handle.getmembers(): + path = pathlib.PurePosixPath(member.name) + if path.is_absolute() or ".." in path.parts or member.issym() or member.islnk() or member.isdev() or member.isfifo(): + raise SystemExit(f"unsafe rollback archive member: {member.name}") +PY +} + +[[ ${EUID:-$(id -u)} -eq 0 ]] || die "run as root" +SNAPSHOT="" +ACCEPT_FORWARD_SCHEMA=0 +CONFIRM=0 +while (($#)); do + case "$1" in + --snapshot) SNAPSHOT=${2:?}; shift 2 ;; + --accept-forward-schema) ACCEPT_FORWARD_SCHEMA=1; shift ;; + --confirm-rollback) CONFIRM=1; shift ;; + -h|--help) usage ;; + *) die "unknown option: $1" ;; + esac +done +[[ -n "$SNAPSHOT" && $CONFIRM -eq 1 ]] || usage +SNAPSHOT=$(realpath -e -- "$SNAPSHOT") +case "$SNAPSHOT" in + /var/backups/vibmail/upgrades/*) ;; + *) die "snapshot path is outside /var/backups/vibmail/upgrades" ;; +esac + +APP_ROOT=${APP_ROOT:-/opt/vibmail/app} +VENV=${VENV:-/opt/vibmail/venv} +PUBLIC_ROOT=${PUBLIC_ROOT:-/opt/vibmail-public-site} +STATIC_ROOT=${STATIC_ROOT:-/var/lib/vibmail/static} +ENV_FILE=${VIBMAIL_ENV_FILE:-/etc/vibmail/vibmail.env} +MARKER_FILE=${MARKER_FILE:-/etc/vibmail/installation.json} +LOCK_FILE=${UPGRADE_LOCK_FILE:-/run/lock/vibmail-upgrade.lock} +STAGING_ROOT=${STAGING_ROOT:-/opt/vibmail-upgrades} +for command_name in python3 flock rsync tar sha256sum systemctl realpath postfix doveconf nginx; do + command -v "$command_name" >/dev/null 2>&1 || die "required command is missing: $command_name" +done +for required in application.tar.gz public-current.txt UPGRADE_METADATA.json SHA256SUMS; do + [[ -f "$SNAPSHOT/$required" ]] || die "snapshot is incomplete: $required is missing" +done +(cd "$SNAPSHOT" && sha256sum --check SHA256SUMS) +validate_tar_archive "$SNAPSHOT/application.tar.gz" + +NEW_MIGRATIONS=$(python3 - "$SNAPSHOT/UPGRADE_METADATA.json" <<'PY' +import json, pathlib, sys +payload = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8")) +print(int(payload.get("new_migrations", 0))) +PY +) +DATA_BACKUP=$(python3 - "$SNAPSHOT/UPGRADE_METADATA.json" <<'PY' +import json, pathlib, sys +payload = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8")) +print(payload.get("data_backup", "")) +PY +) +[[ "$NEW_MIGRATIONS" =~ ^[0-9]+$ ]] || die "snapshot migration metadata is invalid" +if (( NEW_MIGRATIONS > 0 && ACCEPT_FORWARD_SCHEMA == 0 )); then + die "snapshot belongs to an upgrade with new migrations; review coordinated data restore/reconciliation or explicitly accept the forward schema" +fi + +install -d -o root -g root -m 0755 "$(dirname -- "$LOCK_FILE")" "$STAGING_ROOT" +exec 9>"$LOCK_FILE" +flock -n 9 || die "another MailStack upgrade/rollback is already running" +[[ -r "$ENV_FILE" ]] || die "environment file is missing: $ENV_FILE" +# shellcheck disable=SC1090 +set -a +source "$ENV_FILE" +set +a +export VIBMAIL_ENV_FILE="$ENV_FILE" +export DJANGO_SETTINGS_MODULE=config.settings.production +run_app() { + runuser -u vmail -- env -i \ + PATH="$VENV/bin:/usr/local/bin:/usr/bin:/bin" \ + HOME=/var/vmail USER=vmail LOGNAME=vmail \ + VIBMAIL_ENV_FILE="$ENV_FILE" DJANGO_SETTINGS_MODULE=config.settings.production \ + "$@" +} + +PREVIOUS_PUBLIC=$(cat "$SNAPSHOT/public-current.txt") +[[ "$PREVIOUS_PUBLIC" == "$PUBLIC_ROOT/releases/"* && -d "$PREVIOUS_PUBLIC" ]] \ + || die "snapshot public-site pointer is invalid" +TEMP=$(mktemp -d "$STAGING_ROOT/rollback.XXXXXX") +trap 'rm -rf -- "$TEMP"' EXIT +systemctl stop vibmail-public-contact.service vibmail-ingestion.service vibmail-gunicorn.service + +tar --no-same-owner -xzf "$SNAPSHOT/application.tar.gz" -C "$TEMP" +[[ -f "$TEMP/manage.py" && -f "$TEMP/pyproject.toml" ]] || die "snapshot application archive is invalid" +rsync -a --delete-delay "$TEMP/" "$APP_ROOT/" +chown -R root:vmail "$APP_ROOT" +find "$APP_ROOT" -type d -exec chmod 0750 {} + +find "$APP_ROOT" -type f -exec chmod 0640 {} + +find "$APP_ROOT/scripts" -type f -name '*.sh' -exec chmod 0750 {} + +PIP_DISABLE_PIP_VERSION_CHECK=1 PIP_NO_INPUT=1 \ + "$VENV/bin/pip" install --requirement "$APP_ROOT/requirements/production.txt" +"$VENV/bin/python" -m pip check +chown -R root:vmail "$VENV" +chmod -R g+rX,o-rwx "$VENV" + +if [[ -f "$SNAPSHOT/installation.json" ]]; then + cp -a -- "$SNAPSHOT/installation.json" "$MARKER_FILE" + chown root:root "$MARKER_FILE" + chmod 0600 "$MARKER_FILE" +fi +ln -sfn "$PREVIOUS_PUBLIC" "$PUBLIC_ROOT/current" +if [[ -n ${PUBLIC_HOSTNAME:-} ]]; then + ln -sfn "$PUBLIC_ROOT/current/site" "/var/www/$PUBLIC_HOSTNAME/current" +fi + +cd "$APP_ROOT" +run_app "$VENV/bin/python" manage.py collectstatic --noinput +chown -R vmail:www-data "$STATIC_ROOT" +find "$STATIC_ROOT" -type d -exec chmod 0755 {} + +find "$STATIC_ROOT" -type f -exec chmod 0644 {} + +run_app "$VENV/bin/python" manage.py check --deploy +postfix check +doveconf -n >/dev/null +nginx -t +systemctl daemon-reload +systemctl start vibmail-gunicorn.service vibmail-ingestion.service vibmail-public-contact.service +"$APP_ROOT/scripts/verify_application.sh" + +printf 'MAILSTACK_ROLLBACK=PASS\n' +printf 'ROLLBACK_SNAPSHOT=%s\n' "$SNAPSHOT" +printf 'ROLLBACK_DATABASE_ACTION=NOT_PERFORMED\n' +printf 'ROLLBACK_DATA_BACKUP=%s\n' "$DATA_BACKUP" +if (( NEW_MIGRATIONS > 0 )); then + printf 'ROLLBACK_SCHEMA=FORWARD_SCHEMA_EXPLICITLY_ACCEPTED\n' +else + printf 'ROLLBACK_SCHEMA=UNCHANGED\n' +fi diff --git a/mailbox-app/scripts/upgrade.sh b/mailbox-app/scripts/upgrade.sh new file mode 100755 index 0000000..42b2412 --- /dev/null +++ b/mailbox-app/scripts/upgrade.sh @@ -0,0 +1,348 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +IFS=$'\n\t' +umask 077 + +usage() { + cat >&2 <<'EOF' +Usage: + sudo /opt/vibmail/app/scripts/upgrade.sh \ + --archive /absolute/path/mailstack-X.Y.Z-source.zip \ + --checksum /absolute/path/mailstack-X.Y.Z-source.zip.sha256 \ + [--allow-migrations] \ + --confirm-upgrade + +The deterministic source ZIP and matching SHA-256 file are both mandatory. +New Django migration files require the explicit --allow-migrations acknowledgement. +EOF + exit 2 +} + +die() { printf 'UPGRADE_FINDING=%s\n' "$*" >&2; exit 1; } + +[[ ${EUID:-$(id -u)} -eq 0 ]] || die "run as root" + +ARCHIVE="" +CHECKSUM="" +ALLOW_MIGRATIONS=0 +CONFIRM=0 +while (($#)); do + case "$1" in + --archive) ARCHIVE=${2:?}; shift 2 ;; + --checksum) CHECKSUM=${2:?}; shift 2 ;; + --allow-migrations) ALLOW_MIGRATIONS=1; shift ;; + --confirm-upgrade) CONFIRM=1; shift ;; + -h|--help) usage ;; + *) die "unknown option: $1" ;; + esac +done +[[ -n "$ARCHIVE" && -n "$CHECKSUM" && $CONFIRM -eq 1 ]] || usage + +ARCHIVE=$(realpath -e -- "$ARCHIVE") +CHECKSUM=$(realpath -e -- "$CHECKSUM") +[[ -f "$ARCHIVE" && ! -L "$ARCHIVE" ]] || die "target archive must be a regular non-symlink file" +[[ -f "$CHECKSUM" && ! -L "$CHECKSUM" ]] || die "checksum must be a regular non-symlink file" + +APP_ROOT=${APP_ROOT:-/opt/vibmail/app} +VENV=${VENV:-/opt/vibmail/venv} +PUBLIC_ROOT=${PUBLIC_ROOT:-/opt/vibmail-public-site} +STATIC_ROOT=${STATIC_ROOT:-/var/lib/vibmail/static} +ENV_FILE=${VIBMAIL_ENV_FILE:-/etc/vibmail/vibmail.env} +MARKER_FILE=${MARKER_FILE:-/etc/vibmail/installation.json} +LOCK_FILE=${UPGRADE_LOCK_FILE:-/run/lock/vibmail-upgrade.lock} +UPGRADE_ROOT=${UPGRADE_ROOT:-/var/backups/vibmail/upgrades} +STAGING_ROOT=${STAGING_ROOT:-/opt/vibmail-upgrades} +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P) +VERIFY_HELPER="$SCRIPT_DIR/verify_upgrade_archive.py" + +for command_name in python3 python3.12 flock rsync tar sha256sum systemctl curl nginx postfix realpath readlink; do + command -v "$command_name" >/dev/null 2>&1 || die "required command is missing: $command_name" +done +command -v doveconf >/dev/null 2>&1 || die "required command is missing: doveconf" +[[ -f "$VERIFY_HELPER" ]] || die "upgrade archive verifier is missing: $VERIFY_HELPER" +[[ -f "$APP_ROOT/manage.py" && -f "$APP_ROOT/pyproject.toml" ]] || die "current MailStack application is incomplete" +[[ -x "$VENV/bin/python" && -x "$VENV/bin/pip" ]] || die "MailStack virtual environment is incomplete" +[[ -r "$ENV_FILE" ]] || die "environment file is missing: $ENV_FILE" +[[ -x "$APP_ROOT/scripts/backup.sh" && -x "$APP_ROOT/scripts/verify_application.sh" ]] \ + || die "current application is missing backup/verification tooling" +id -u vmail >/dev/null 2>&1 || die "required vmail user is missing" + +install -d -o root -g root -m 0755 "$(dirname -- "$LOCK_FILE")" +exec 9>"$LOCK_FILE" +flock -n 9 || die "another MailStack upgrade is already running" + +# shellcheck disable=SC1090 +set -a +source "$ENV_FILE" +set +a +MAIL_DOMAIN=${MAIL_DOMAIN:?MAIL_DOMAIN is missing from the environment file} +APP_HOSTNAME=${APP_HOSTNAME:?APP_HOSTNAME is missing from the environment file} +MAIL_HOSTNAME=${MAIL_HOSTNAME:?MAIL_HOSTNAME is missing from the environment file} +PUBLIC_HOSTNAME=${PUBLIC_HOSTNAME:?PUBLIC_HOSTNAME is missing from the environment file} +export VIBMAIL_ENV_FILE="$ENV_FILE" +export DJANGO_SETTINGS_MODULE=config.settings.production + +run_app() { + runuser -u vmail -- env -i \ + PATH="$VENV/bin:/usr/local/bin:/usr/bin:/bin" \ + HOME=/var/vmail USER=vmail LOGNAME=vmail \ + VIBMAIL_ENV_FILE="$ENV_FILE" \ + DJANGO_SETTINGS_MODULE=config.settings.production \ + "$@" +} + +for service in mariadb postfix dovecot nginx vibmail-gunicorn vibmail-ingestion vibmail-public-contact; do + systemctl is-active --quiet "$service.service" || die "required service is not active before upgrade: $service" +done +postfix check +DOVECOT_CONFIG=$(doveconf -n) +[[ -n "$DOVECOT_CONFIG" ]] || die "doveconf returned no effective configuration" +nginx -t +"$APP_ROOT/scripts/verify_application.sh" + +install -d -o root -g root -m 0700 "$UPGRADE_ROOT" "$STAGING_ROOT" +STAGE_PARENT=$(mktemp -d "$STAGING_ROOT/stage.XXXXXX") +cleanup_stage() { rm -rf -- "$STAGE_PARENT"; } +trap cleanup_stage EXIT + +VERIFY_OUTPUT=$( + python3 "$VERIFY_HELPER" \ + --archive "$ARCHIVE" \ + --checksum "$CHECKSUM" \ + --current-app "$APP_ROOT" \ + --extract-to "$STAGE_PARENT" +) || { printf '%s\n' "$VERIFY_OUTPUT" >&2; die "target release archive verification failed"; } +printf '%s\n' "$VERIFY_OUTPUT" +CURRENT_VERSION=$(printf '%s\n' "$VERIFY_OUTPUT" | sed -n 's/^CURRENT_VERSION=//p') +TARGET_VERSION=$(printf '%s\n' "$VERIFY_OUTPUT" | sed -n 's/^TARGET_VERSION=//p') +TARGET_ROOT=$(printf '%s\n' "$VERIFY_OUTPUT" | sed -n 's/^TARGET_ROOT=//p') +TARGET_SHA256=$(printf '%s\n' "$VERIFY_OUTPUT" | sed -n 's/^UPGRADE_ARCHIVE_SHA256=//p') +NEW_MIGRATIONS=$(printf '%s\n' "$VERIFY_OUTPUT" | sed -n 's/^NEW_MIGRATIONS=//p') +[[ "$CURRENT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]] || die "invalid current version returned by verifier" +[[ "$TARGET_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]] || die "invalid target version returned by verifier" +[[ "$TARGET_SHA256" =~ ^[0-9a-f]{64}$ ]] || die "invalid target checksum returned by verifier" +[[ "$NEW_MIGRATIONS" =~ ^[0-9]+$ ]] || die "invalid migration count returned by verifier" +[[ -d "$TARGET_ROOT/mailbox-app" && -d "$TARGET_ROOT/public-site" ]] || die "verified release staging is incomplete" +if (( NEW_MIGRATIONS > 0 && ALLOW_MIGRATIONS == 0 )); then + die "target contains $NEW_MIGRATIONS new Django migration file(s); rerun only after review with --allow-migrations" +fi + +STAMP=$(date -u +%Y%m%dT%H%M%SZ) +ROLLBACK_ROOT="$UPGRADE_ROOT/${STAMP}-from-${CURRENT_VERSION}-to-${TARGET_VERSION}" +APP_ARCHIVE="$ROLLBACK_ROOT/application.tar.gz" +DATA_ROOT="$ROLLBACK_ROOT/data" +PUBLIC_POINTER="$ROLLBACK_ROOT/public-current.txt" +MIGRATION_RISK=0 +MUTATION_STARTED=0 +UPGRADE_COMPLETE=0 + +install -d -o root -g root -m 0700 "$ROLLBACK_ROOT" "$DATA_ROOT" +tar --one-file-system -C "$APP_ROOT" -czf "$APP_ARCHIVE" . +if [[ -f "$MARKER_FILE" ]]; then + cp -a -- "$MARKER_FILE" "$ROLLBACK_ROOT/installation.json" +fi +CURRENT_PUBLIC=$(readlink -f -- "$PUBLIC_ROOT/current" 2>/dev/null || true) +[[ -n "$CURRENT_PUBLIC" && -d "$CURRENT_PUBLIC" ]] || die "current public-site release symlink is missing or invalid" +printf '%s\n' "$CURRENT_PUBLIC" > "$PUBLIC_POINTER" + +BACKUP_OUTPUT=$(BACKUP_ROOT="$DATA_ROOT" "$APP_ROOT/scripts/backup.sh") || { + printf '%s\n' "$BACKUP_OUTPUT" >&2 + die "pre-upgrade consistent data backup failed" +} +printf '%s\n' "$BACKUP_OUTPUT" +DATA_BACKUP=$(find "$DATA_ROOT" -mindepth 1 -maxdepth 1 -type d -print -quit) +[[ -n "$DATA_BACKUP" && -f "$DATA_BACKUP/SHA256SUMS" ]] || die "pre-upgrade data backup was not created as expected" +(cd "$DATA_BACKUP" && sha256sum --check SHA256SUMS >/dev/null) + +python3 - "$ROLLBACK_ROOT/UPGRADE_METADATA.json" "$CURRENT_VERSION" "$TARGET_VERSION" \ + "$TARGET_SHA256" "$NEW_MIGRATIONS" "$DATA_BACKUP" <<'PY' +import json +import pathlib +import sys +from datetime import datetime, timezone + +path = pathlib.Path(sys.argv[1]) +data = { + "format_version": 1, + "created_at_utc": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + "current_version": sys.argv[2], + "target_version": sys.argv[3], + "target_archive_sha256": sys.argv[4], + "new_migrations": int(sys.argv[5]), + "data_backup": sys.argv[6], +} +path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8") +path.chmod(0o600) +PY +( + cd "$ROLLBACK_ROOT" + files=(application.tar.gz public-current.txt UPGRADE_METADATA.json) + [[ -f installation.json ]] && files+=(installation.json) + sha256sum "${files[@]}" > SHA256SUMS + sha256sum --check SHA256SUMS >/dev/null +) + +restore_pre_upgrade_source() { + local temp + temp=$(mktemp -d "$STAGING_ROOT/rollback.XXXXXX") + /usr/bin/python3 - "$APP_ARCHIVE" <<'PY' +import pathlib +import sys +import tarfile +archive = pathlib.Path(sys.argv[1]) +with tarfile.open(archive, "r:gz") as handle: + for member in handle.getmembers(): + path = pathlib.PurePosixPath(member.name) + if path.is_absolute() or ".." in path.parts or member.issym() or member.islnk() or member.isdev() or member.isfifo(): + raise SystemExit(f"unsafe rollback archive member: {member.name}") +PY + tar --no-same-owner -xzf "$APP_ARCHIVE" -C "$temp" + rsync -a --delete-delay "$temp/" "$APP_ROOT/" + rm -rf -- "$temp" + chown -R root:vmail "$APP_ROOT" + find "$APP_ROOT" -type d -exec chmod 0750 {} + + find "$APP_ROOT" -type f -exec chmod 0640 {} + + find "$APP_ROOT/scripts" -type f -name '*.sh' -exec chmod 0750 {} + + PIP_DISABLE_PIP_VERSION_CHECK=1 PIP_NO_INPUT=1 \ + "$VENV/bin/pip" install --requirement "$APP_ROOT/requirements/production.txt" + "$VENV/bin/python" -m pip check + chown -R root:vmail "$VENV" + chmod -R g+rX,o-rwx "$VENV" + if [[ -f "$ROLLBACK_ROOT/installation.json" ]]; then + cp -a -- "$ROLLBACK_ROOT/installation.json" "$MARKER_FILE" + chown root:root "$MARKER_FILE" + chmod 0600 "$MARKER_FILE" + fi + ln -sfn "$CURRENT_PUBLIC" "$PUBLIC_ROOT/current" + run_app "$VENV/bin/python" "$APP_ROOT/manage.py" collectstatic --noinput + chown -R vmail:www-data "$STATIC_ROOT" + find "$STATIC_ROOT" -type d -exec chmod 0755 {} + + find "$STATIC_ROOT" -type f -exec chmod 0644 {} + +} + +rollback_on_failure() { + local status=${1:-$?} + trap - ERR INT TERM + set +e + printf 'UPGRADE_FAILURE=detected\n' >&2 + systemctl stop vibmail-public-contact.service vibmail-ingestion.service vibmail-gunicorn.service >/dev/null 2>&1 || true + if (( MUTATION_STARTED == 1 && MIGRATION_RISK == 1 )); then + printf 'UPGRADE_ROLLBACK=MANUAL_SCHEMA_RECONCILIATION_REQUIRED\n' >&2 + printf 'UPGRADE_ROLLBACK_SNAPSHOT=%s\n' "$ROLLBACK_ROOT" >&2 + printf 'UPGRADE_DATA_BACKUP=%s\n' "$DATA_BACKUP" >&2 + printf 'A migration-capable upgrade failed after schema mutation began; automatic source rollback is refused to avoid an unproven source/schema pairing.\n' >&2 + elif (( MUTATION_STARTED == 1 )); then + printf 'UPGRADE_ROLLBACK=AUTO_SOURCE_RUNTIME\n' >&2 + restore_pre_upgrade_source || true + systemctl start vibmail-gunicorn.service vibmail-ingestion.service vibmail-public-contact.service >/dev/null 2>&1 || true + "$APP_ROOT/scripts/verify_application.sh" >/dev/null 2>&1 || true + printf 'UPGRADE_ROLLBACK_SNAPSHOT=%s\n' "$ROLLBACK_ROOT" >&2 + fi + exit "$status" +} +trap rollback_on_failure ERR INT TERM + +# The consistent data backup above may briefly stop SMTP/LMTP. From this point onward, +# Postfix and Dovecot remain active while the web/ingestion workers are upgraded. +MUTATION_STARTED=1 +systemctl stop vibmail-public-contact.service vibmail-ingestion.service vibmail-gunicorn.service +systemctl is-active --quiet postfix.service || { printf 'UPGRADE_FINDING=Postfix stopped unexpectedly before application mutation\n' >&2; rollback_on_failure 1; } +systemctl is-active --quiet dovecot.service || { printf 'UPGRADE_FINDING=Dovecot stopped unexpectedly before application mutation\n' >&2; rollback_on_failure 1; } + +rsync -a --delete-delay \ + --exclude='.env' --exclude='.venv' --exclude='__pycache__' --exclude='*.pyc' \ + --exclude='.pytest_cache' --exclude='.ruff_cache' --exclude='.coverage' --exclude='htmlcov' \ + --exclude='coverage.json' --exclude='coverage.xml' --exclude='staticfiles' --exclude='.runtime' \ + "$TARGET_ROOT/mailbox-app/" "$APP_ROOT/" +chown -R root:vmail "$APP_ROOT" +find "$APP_ROOT" -type d -exec chmod 0750 {} + +find "$APP_ROOT" -type f -exec chmod 0640 {} + +find "$APP_ROOT/scripts" -type f -name '*.sh' -exec chmod 0750 {} + + +PIP_DISABLE_PIP_VERSION_CHECK=1 PIP_NO_INPUT=1 \ + "$VENV/bin/pip" install --requirement "$APP_ROOT/requirements/production.txt" +"$VENV/bin/python" -m pip check +chown -R root:vmail "$VENV" +chmod -R g+rX,o-rwx "$VENV" +runuser -u vmail -- "$VENV/bin/python" -c 'import django.core, MySQLdb' + +cd "$APP_ROOT" +if (( NEW_MIGRATIONS > 0 )); then + MIGRATION_RISK=1 +fi +run_app "$VENV/bin/python" manage.py migrate --noinput +run_app "$VENV/bin/python" manage.py verify_mailserver_schema +run_app "$VENV/bin/python" manage.py sync_mailserver_mailboxes --strict +run_app "$VENV/bin/python" manage.py update_mailbox_counters +run_app "$VENV/bin/python" manage.py verify_mail_storage +run_app "$VENV/bin/python" manage.py verify_postfix_contract +run_app "$VENV/bin/python" manage.py collectstatic --noinput +chown -R vmail:www-data "$STATIC_ROOT" +find "$STATIC_ROOT" -type d -exec chmod 0755 {} + +find "$STATIC_ROOT" -type f -exec chmod 0644 {} + +run_app "$VENV/bin/python" manage.py check --deploy + +PUBLIC_RELEASE="$PUBLIC_ROOT/releases/${STAMP}-${TARGET_VERSION}" +[[ ! -e "$PUBLIC_RELEASE" ]] || { printf 'UPGRADE_FINDING=target public release directory already exists: %s\n' "$PUBLIC_RELEASE" >&2; rollback_on_failure 1; } +install -d -o root -g root -m 0755 "$PUBLIC_RELEASE" +rsync -a --delete --exclude='__pycache__' --exclude='*.pyc' "$TARGET_ROOT/public-site/" "$PUBLIC_RELEASE/" +python3 "$TARGET_ROOT/scripts/render_public_site.py" \ + "$PUBLIC_RELEASE/site-template" "$PUBLIC_RELEASE/site" \ + --public-hostname "$PUBLIC_HOSTNAME" --app-hostname "$APP_HOSTNAME" \ + --mail-hostname "$MAIL_HOSTNAME" --mail-domain "$MAIL_DOMAIN" +if [[ ! -x "$PUBLIC_ROOT/venv/bin/python" ]]; then + python3.12 -m venv "$PUBLIC_ROOT/venv" +fi +PIP_DISABLE_PIP_VERSION_CHECK=1 PIP_NO_INPUT=1 \ + "$PUBLIC_ROOT/venv/bin/pip" install --requirement "$PUBLIC_RELEASE/requirements.txt" +"$PUBLIC_ROOT/venv/bin/python" -m pip check +chmod -R a+rX "$PUBLIC_ROOT/venv" "$PUBLIC_RELEASE" +ln -sfn "$PUBLIC_RELEASE" "$PUBLIC_ROOT/current" +ln -sfn "$PUBLIC_ROOT/current/site" "/var/www/$PUBLIC_HOSTNAME/current" + +python3 - "$MARKER_FILE" "$CURRENT_VERSION" "$TARGET_VERSION" "$TARGET_SHA256" <<'PY' +import json +import pathlib +import sys +from datetime import datetime, timezone +path = pathlib.Path(sys.argv[1]) +data = {} +if path.is_file(): + data = json.loads(path.read_text(encoding="utf-8")) +data.update({ + "product": data.get("product", "MailStack"), + "previous_version": sys.argv[2], + "version": sys.argv[3], + "upgraded_at_utc": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + "source_sha256": sys.argv[4], +}) +path.parent.mkdir(parents=True, exist_ok=True) +path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8") +path.chmod(0o600) +PY +chown root:root "$MARKER_FILE" +chmod 0600 "$MARKER_FILE" + +postfix check +doveconf -n >/dev/null +nginx -t +systemctl daemon-reload +systemctl start vibmail-gunicorn.service vibmail-ingestion.service vibmail-public-contact.service +for service in mariadb postfix dovecot nginx vibmail-gunicorn vibmail-ingestion vibmail-public-contact; do + systemctl is-active --quiet "$service.service" || { printf 'UPGRADE_FINDING=service is not active after upgrade: %s\n' "$service" >&2; rollback_on_failure 1; } +done +"$APP_ROOT/scripts/verify_application.sh" +curl --fail --silent --show-error --max-time 20 \ + --resolve "$APP_HOSTNAME:443:127.0.0.1" "https://$APP_HOSTNAME/accounts/login/" >/dev/null +curl --fail --silent --show-error --max-time 20 \ + --resolve "$PUBLIC_HOSTNAME:443:127.0.0.1" "https://$PUBLIC_HOSTNAME/" >/dev/null + +UPGRADE_COMPLETE=1 +trap - ERR INT TERM +printf 'MAILSTACK_UPGRADE=PASS\n' +printf 'UPGRADE_FROM=%s\n' "$CURRENT_VERSION" +printf 'UPGRADE_TO=%s\n' "$TARGET_VERSION" +printf 'UPGRADE_ARCHIVE_SHA256=%s\n' "$TARGET_SHA256" +printf 'UPGRADE_ROLLBACK_SNAPSHOT=%s\n' "$ROLLBACK_ROOT" +printf 'UPGRADE_DATA_BACKUP=%s\n' "$DATA_BACKUP" +printf 'INBOUND_CONTINUITY=POSTFIX_DOVECOT_LEFT_ACTIVE_DURING_SOURCE_MUTATION\n' diff --git a/mailbox-app/scripts/verify_upgrade_archive.py b/mailbox-app/scripts/verify_upgrade_archive.py new file mode 100644 index 0000000..bbb27c7 --- /dev/null +++ b/mailbox-app/scripts/verify_upgrade_archive.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python3 +"""Verify and safely stage a deterministic MailStack source archive for upgrade.""" +from __future__ import annotations + +import argparse +import hashlib +import re +import shutil +import tomllib +import zipfile +from pathlib import Path, PurePosixPath + +VERSION_PATTERN = re.compile( + r"^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)" + r"(?:-rc\.(?P0|[1-9]\d*))?$" +) +PACKAGE_PATTERN = re.compile( + r"^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)" + r"(?:rc(?P0|[1-9]\d*))?$" +) +MANIFEST_LINE = re.compile(r"^([0-9a-f]{64}) (.+)$") +CANONICAL_ZIP_TIMESTAMP = (2026, 1, 1, 0, 0, 0) +CANONICAL_ZIP_CREATE_SYSTEM = 3 +CANONICAL_ZIP_VERSION = 20 +BLOCKED_NAMES = {".env", ".coverage", "id_rsa", "id_ed25519", "credentials.json"} +BLOCKED_SUFFIXES = {".pem", ".key", ".p12", ".pfx", ".sqlite", ".sqlite3", ".db", ".log", ".bak"} +REQUIRED_MEMBERS = { + "VERSION", + "mailbox-app/manage.py", + "mailbox-app/pyproject.toml", + "mailbox-app/requirements/production.txt", + "mailbox-app/scripts/verify_application.sh", + "mailbox-app/scripts/upgrade.sh", + "mailbox-app/scripts/rollback_upgrade.sh", + "public-site/requirements.txt", + "public-site/site-template/index.html", + "scripts/render_public_site.py", +} + + +class UpgradeArchiveError(RuntimeError): + """Raised when a target release cannot be proven safe for staging.""" + + +def sha256_bytes(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def sha256(path: Path) -> str: + hasher = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + hasher.update(chunk) + return hasher.hexdigest() + + +def normalized_version(value: str, *, package: bool = False) -> tuple[str, tuple[int, int, int, int, int]]: + pattern = PACKAGE_PATTERN if package else VERSION_PATTERN + match = pattern.fullmatch(value.strip()) + if not match: + kind = "package version" if package else "VERSION" + raise UpgradeArchiveError(f"unsupported {kind}: {value!r}") + major, minor, patch = (int(match.group(name)) for name in ("major", "minor", "patch")) + rc_text = match.group("rc") + stable_rank = 1 if rc_text is None else 0 + rc = 0 if rc_text is None else int(rc_text) + canonical = f"{major}.{minor}.{patch}" + (f"-rc.{rc}" if rc_text is not None else "") + return canonical, (major, minor, patch, stable_rank, rc) + + +def package_from_release(version: str) -> str: + match = VERSION_PATTERN.fullmatch(version) + if not match: + raise UpgradeArchiveError(f"unsupported VERSION: {version!r}") + base = f"{match.group('major')}.{match.group('minor')}.{match.group('patch')}" + rc = match.group("rc") + return f"{base}rc{rc}" if rc is not None else base + + +def read_current_version(app_root: Path) -> str: + pyproject = app_root / "pyproject.toml" + if not pyproject.is_file(): + raise UpgradeArchiveError(f"current application pyproject is missing: {pyproject}") + try: + package_version = str(tomllib.loads(pyproject.read_text(encoding="utf-8"))["project"]["version"]) + except (OSError, KeyError, TypeError, tomllib.TOMLDecodeError) as exc: + raise UpgradeArchiveError(f"unable to read current project.version: {exc}") from exc + canonical, _ = normalized_version(package_version, package=True) + return canonical + + +def read_checksum(checksum_path: Path, archive: Path) -> str: + fields = checksum_path.read_text(encoding="utf-8").split() + if len(fields) != 2: + raise UpgradeArchiveError("checksum file must contain exactly one SHA-256 and filename") + expected, filename = fields + if not re.fullmatch(r"[0-9a-fA-F]{64}", expected): + raise UpgradeArchiveError("checksum file does not contain a valid SHA-256") + if filename.lstrip("*") != archive.name: + raise UpgradeArchiveError("checksum filename does not match target archive") + return expected.lower() + + +def verify_archive(archive_path: Path, checksum_path: Path) -> tuple[zipfile.ZipFile, str, str, dict[str, str]]: + expected = read_checksum(checksum_path, archive_path) + actual = sha256(archive_path) + if actual != expected: + raise UpgradeArchiveError(f"archive checksum mismatch: expected {expected}, got {actual}") + + archive = zipfile.ZipFile(archive_path) + bad = archive.testzip() + if bad is not None: + archive.close() + raise UpgradeArchiveError(f"ZIP integrity failure: {bad}") + if archive.comment: + archive.close() + raise UpgradeArchiveError("release archive comment must be empty") + + infos = archive.infolist() + names = [info.filename for info in infos] + if not names or len(names) != len(set(names)): + archive.close() + raise UpgradeArchiveError("release ZIP is empty or contains duplicate members") + top_levels = {PurePosixPath(name).parts[0] for name in names} + if len(top_levels) != 1: + archive.close() + raise UpgradeArchiveError("release must contain exactly one top-level directory") + top_level = next(iter(top_levels)) + prefix = top_level + "/" + + for info in infos: + pure = PurePosixPath(info.filename) + if pure.is_absolute() or ".." in pure.parts or "" in pure.parts: + archive.close() + raise UpgradeArchiveError(f"unsafe ZIP member: {info.filename}") + if pure.name in BLOCKED_NAMES or pure.suffix.lower() in BLOCKED_SUFFIXES: + archive.close() + raise UpgradeArchiveError(f"blocked file in release: {info.filename}") + if info.is_dir(): + continue + if info.date_time != CANONICAL_ZIP_TIMESTAMP: + archive.close() + raise UpgradeArchiveError(f"non-canonical ZIP timestamp: {info.filename}") + if info.create_system != CANONICAL_ZIP_CREATE_SYSTEM: + archive.close() + raise UpgradeArchiveError(f"non-canonical ZIP host metadata: {info.filename}") + if ( + info.create_version != CANONICAL_ZIP_VERSION + or info.extract_version != CANONICAL_ZIP_VERSION + or info.reserved != 0 + or info.flag_bits != 0 + or info.volume != 0 + or info.internal_attr != 0 + or info.compress_type != zipfile.ZIP_STORED + or info.extra + or info.comment + ): + archive.close() + raise UpgradeArchiveError(f"non-canonical ZIP member metadata: {info.filename}") + expected_mode = 0o755 if pure.name == "install.sh" or pure.suffix == ".sh" else 0o644 + actual_mode = (info.external_attr >> 16) & 0o777 + if actual_mode != expected_mode: + archive.close() + raise UpgradeArchiveError(f"non-canonical ZIP mode: {info.filename}: {oct(actual_mode)}") + + manifest_name = prefix + "SOURCE_MANIFEST.sha256" + if manifest_name not in names: + archive.close() + raise UpgradeArchiveError("source manifest missing") + expected_members: dict[str, str] = {} + manifest_text = archive.read(manifest_name).decode("utf-8") + for number, line in enumerate(manifest_text.splitlines(), start=1): + match = MANIFEST_LINE.fullmatch(line) + if not match: + archive.close() + raise UpgradeArchiveError(f"invalid source-manifest line {number}") + expected_hash, relative = match.groups() + pure = PurePosixPath(relative) + if pure.is_absolute() or ".." in pure.parts or relative == "SOURCE_MANIFEST.sha256": + archive.close() + raise UpgradeArchiveError(f"unsafe source-manifest path: {relative}") + member = prefix + relative + if member in expected_members: + archive.close() + raise UpgradeArchiveError(f"duplicate source-manifest path: {relative}") + expected_members[member] = expected_hash + + actual_members = {name for name in names if not name.endswith("/") and name != manifest_name} + if set(expected_members) != actual_members: + archive.close() + raise UpgradeArchiveError("source manifest does not match archive member set") + for member, expected_hash in expected_members.items(): + if sha256_bytes(archive.read(member)) != expected_hash: + archive.close() + raise UpgradeArchiveError(f"source-manifest hash mismatch: {member.removeprefix(prefix)}") + + missing = sorted(relative for relative in REQUIRED_MEMBERS if prefix + relative not in actual_members) + if missing: + archive.close() + raise UpgradeArchiveError(f"release is missing upgrade-required members: {missing}") + return archive, prefix, actual, expected_members + + +def migration_map_from_current(app_root: Path) -> dict[str, str]: + result: dict[str, str] = {} + apps_root = app_root / "apps" + if not apps_root.is_dir(): + raise UpgradeArchiveError(f"current application apps directory is missing: {apps_root}") + for path in sorted(apps_root.glob("*/migrations/[0-9]*.py")): + relative = path.relative_to(app_root).as_posix() + result[relative] = sha256(path) + return result + + +def migration_map_from_archive(archive: zipfile.ZipFile, prefix: str) -> dict[str, str]: + marker = prefix + "mailbox-app/" + result: dict[str, str] = {} + for info in archive.infolist(): + name = info.filename + if not name.startswith(marker) or info.is_dir(): + continue + relative = name.removeprefix(marker) + pure = PurePosixPath(relative) + if len(pure.parts) == 4 and pure.parts[0] == "apps" and pure.parts[2] == "migrations": + if re.fullmatch(r"[0-9].*\.py", pure.name): + result[relative] = sha256_bytes(archive.read(name)) + return result + + +def compare_migrations(current: dict[str, str], target: dict[str, str]) -> tuple[list[str], list[str], list[str]]: + added = sorted(set(target) - set(current)) + removed = sorted(set(current) - set(target)) + modified = sorted(path for path in set(current) & set(target) if current[path] != target[path]) + return added, removed, modified + + +def safe_extract(archive: zipfile.ZipFile, destination: Path) -> Path: + destination.mkdir(parents=True, exist_ok=True) + if any(destination.iterdir()): + raise UpgradeArchiveError(f"extract destination is not empty: {destination}") + top_level = PurePosixPath(archive.infolist()[0].filename).parts[0] + for info in archive.infolist(): + if info.is_dir(): + continue + target = destination.joinpath(*PurePosixPath(info.filename).parts) + target.parent.mkdir(parents=True, exist_ok=True) + with archive.open(info) as source, target.open("wb") as output: + shutil.copyfileobj(source, output) + mode = (info.external_attr >> 16) & 0o777 + target.chmod(mode) + return destination / top_level + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--archive", type=Path, required=True) + parser.add_argument("--checksum", type=Path, required=True) + parser.add_argument("--current-app", type=Path, required=True) + parser.add_argument("--extract-to", type=Path, required=True) + args = parser.parse_args() + + archive_path = args.archive.resolve(strict=True) + checksum_path = args.checksum.resolve(strict=True) + app_root = args.current_app.resolve(strict=True) + destination = args.extract_to.resolve() + + try: + current_version = read_current_version(app_root) + _, current_order = normalized_version(current_version) + archive, prefix, archive_sha, _ = verify_archive(archive_path, checksum_path) + try: + version_text = archive.read(prefix + "VERSION").decode("utf-8").strip() + target_version, target_order = normalized_version(version_text) + pyproject = tomllib.loads(archive.read(prefix + "mailbox-app/pyproject.toml").decode("utf-8")) + package_version = str(pyproject["project"]["version"]) + if package_version != package_from_release(target_version): + raise UpgradeArchiveError( + f"target package version mismatch: VERSION={target_version}, project.version={package_version}" + ) + if target_order <= current_order: + raise UpgradeArchiveError( + f"target version must be newer than current version: current={current_version}, target={target_version}" + ) + + current_migrations = migration_map_from_current(app_root) + target_migrations = migration_map_from_archive(archive, prefix) + added, removed, modified = compare_migrations(current_migrations, target_migrations) + if removed: + raise UpgradeArchiveError(f"target removes existing migration files: {removed}") + if modified: + raise UpgradeArchiveError(f"target modifies existing migration files: {modified}") + + target_root = safe_extract(archive, destination) + finally: + archive.close() + + print(f"CURRENT_VERSION={current_version}") + print(f"TARGET_VERSION={target_version}") + print(f"TARGET_ROOT={target_root}") + print(f"UPGRADE_ARCHIVE_SHA256={archive_sha}") + print(f"NEW_MIGRATIONS={len(added)}") + for migration in added: + print(f"NEW_MIGRATION={migration}") + print("UPGRADE_ARCHIVE_VERIFY=PASS") + return 0 + except (OSError, KeyError, TypeError, UnicodeDecodeError, zipfile.BadZipFile, UpgradeArchiveError) as exc: + print(f"UPGRADE_ARCHIVE_FINDING={exc}") + print("UPGRADE_ARCHIVE_VERIFY=FAIL") + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/forensic_audit.py b/scripts/forensic_audit.py index 0b73b9a..ab73447 100644 --- a/scripts/forensic_audit.py +++ b/scripts/forensic_audit.py @@ -41,6 +41,10 @@ "scripts/test_operations.py", "scripts/release_gate.py", "scripts/test_release_workflow.py", + "scripts/test_upgrade.py", + "mailbox-app/scripts/upgrade.sh", + "mailbox-app/scripts/rollback_upgrade.sh", + "mailbox-app/scripts/verify_upgrade_archive.py", "scripts/shell_runtime.py", "scripts/validate_templates.py", "docs/FEATURE_MATRIX.md", @@ -320,6 +324,7 @@ def main() -> int: ([sys.executable, str(root / "scripts/test_installer.py")], "INSTALLER_CONTRACT"), ([sys.executable, str(root / "scripts/test_operations.py")], "OPERATIONS_CONTRACT"), ([sys.executable, str(root / "scripts/test_release_workflow.py")], "RELEASE_WORKFLOW_CONTRACT"), + ([sys.executable, str(root / "scripts/test_upgrade.py")], "UPGRADE_ROLLBACK_CONTRACT"), ): code, output = run(command, root) if code: diff --git a/scripts/test_upgrade.py b/scripts/test_upgrade.py new file mode 100644 index 0000000..5737247 --- /dev/null +++ b/scripts/test_upgrade.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +"""Non-destructive contract tests for the generic MailStack upgrade/rollback mechanism.""" +from __future__ import annotations + +import hashlib +import importlib.util +import subprocess +import sys +import tempfile +import zipfile +from pathlib import Path + +from shell_runtime import bash_syntax_command, resolve_bash + +ROOT = Path(__file__).resolve().parents[1] +APP_SCRIPTS = ROOT / "mailbox-app/scripts" +CANONICAL_TIME = (2026, 1, 1, 0, 0, 0) + + +def require(condition: bool, message: str) -> None: + if not condition: + raise AssertionError(message) + + +def read_shell(name: str) -> str: + path = APP_SCRIPTS / name + require(path.is_file(), f"missing upgrade script: {name}") + result = subprocess.run(bash_syntax_command(path, cwd=ROOT), capture_output=True, text=True) + require(result.returncode == 0, result.stdout + result.stderr) + return path.read_text(encoding="utf-8") + + +def load_verifier(): + path = APP_SCRIPTS / "verify_upgrade_archive.py" + require(path.is_file(), "missing upgrade archive verifier") + spec = importlib.util.spec_from_file_location("mailstack_verify_upgrade_archive", path) + require(spec is not None and spec.loader is not None, "unable to load upgrade verifier") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def add_member(archive: zipfile.ZipFile, name: str, data: bytes) -> None: + info = zipfile.ZipInfo(name, date_time=CANONICAL_TIME) + info.create_system = 3 + info.create_version = 20 + info.extract_version = 20 + info.reserved = 0 + info.flag_bits = 0 + info.volume = 0 + info.internal_attr = 0 + info.compress_type = zipfile.ZIP_STORED + info.extra = b"" + info.comment = b"" + mode = 0o755 if Path(name).suffix == ".sh" or Path(name).name == "install.sh" else 0o644 + info.external_attr = mode << 16 + archive.writestr(info, data) + + +def build_fixture_archive(temp: Path) -> tuple[Path, Path, Path]: + current = temp / "current-app" + (current / "apps/core/migrations").mkdir(parents=True) + (current / "pyproject.toml").write_text('[project]\nname="mailstack"\nversion="1.3.0rc5"\n', encoding="utf-8") + migration_v1 = b"# migration one\n" + (current / "apps/core/migrations/0001_initial.py").write_bytes(migration_v1) + + prefix = "mailstack-1.3.0-rc.6/" + files: dict[str, bytes] = { + "VERSION": b"1.3.0-rc.6\n", + "mailbox-app/manage.py": b"#!/usr/bin/env python3\n", + "mailbox-app/pyproject.toml": b'[project]\nname="mailstack"\nversion="1.3.0rc6"\n', + "mailbox-app/requirements/production.txt": b"Django==5.2.16\n", + "mailbox-app/scripts/verify_application.sh": b"#!/usr/bin/env bash\nset -Eeuo pipefail\n", + "mailbox-app/scripts/upgrade.sh": b"#!/usr/bin/env bash\nset -Eeuo pipefail\n", + "mailbox-app/scripts/rollback_upgrade.sh": b"#!/usr/bin/env bash\nset -Eeuo pipefail\n", + "mailbox-app/apps/core/migrations/0001_initial.py": migration_v1, + "mailbox-app/apps/core/migrations/0002_upgrade.py": b"# migration two\n", + "public-site/requirements.txt": b"\n", + "public-site/site-template/index.html": b"\n", + "scripts/render_public_site.py": b"#!/usr/bin/env python3\n", + } + manifest = "".join( + f"{hashlib.sha256(data).hexdigest()} {name}\n" for name, data in sorted(files.items()) + ).encode("utf-8") + archive_path = temp / "mailstack-1.3.0-rc.6-source.zip" + with zipfile.ZipFile(archive_path, "w", compression=zipfile.ZIP_STORED) as archive: + archive.comment = b"" + for name, data in sorted(files.items()): + add_member(archive, prefix + name, data) + add_member(archive, prefix + "SOURCE_MANIFEST.sha256", manifest) + checksum = temp / (archive_path.name + ".sha256") + checksum.write_text( + f"{hashlib.sha256(archive_path.read_bytes()).hexdigest()} {archive_path.name}\n", + encoding="utf-8", + ) + return current, archive_path, checksum + + +def main() -> int: + print(f"BASH_RUNTIME={resolve_bash()}") + upgrade = read_shell("upgrade.sh") + rollback = read_shell("rollback_upgrade.sh") + verifier_text = (APP_SCRIPTS / "verify_upgrade_archive.py").read_text(encoding="utf-8") + module = load_verifier() + + for name, text in {"upgrade.sh": upgrade, "rollback_upgrade.sh": rollback}.items(): + require("set -Eeuo pipefail" in text, f"{name} is not fail-closed") + require("flock -n" in text, f"{name} does not use a non-blocking runtime lock") + + for fragment in ( + "--archive", + "--checksum", + "--confirm-upgrade", + "--allow-migrations", + "verify_upgrade_archive.py", + 'BACKUP_ROOT="$DATA_ROOT"', + '"$APP_ROOT/scripts/backup.sh"', + "sha256sum --check SHA256SUMS", + "MIGRATION_RISK=1", + "MANUAL_SCHEMA_RECONCILIATION_REQUIRED", + "rsync -a --delete-delay", + '"$VENV/bin/pip" install --requirement', + "manage.py migrate --noinput", + "manage.py verify_mailserver_schema", + "manage.py verify_mail_storage", + "manage.py verify_postfix_contract", + "manage.py collectstatic --noinput", + "INBOUND_CONTINUITY=POSTFIX_DOVECOT_LEFT_ACTIVE_DURING_SOURCE_MUTATION", + "systemctl stop vibmail-public-contact.service vibmail-ingestion.service vibmail-gunicorn.service", + 'systemctl is-active --quiet postfix.service', + 'systemctl is-active --quiet dovecot.service', + "MAILSTACK_UPGRADE=PASS", + ): + require(fragment in upgrade, f"upgrade contract missing: {fragment}") + require("systemctl stop postfix" not in upgrade, "upgrade mutation must not directly stop Postfix") + require("systemctl stop dovecot" not in upgrade, "upgrade mutation must not directly stop Dovecot") + + for fragment in ( + "/var/backups/vibmail/upgrades/*", + "sha256sum --check SHA256SUMS", + "validate_tar_archive", + "--accept-forward-schema", + "ROLLBACK_DATABASE_ACTION=NOT_PERFORMED", + "MAILSTACK_ROLLBACK=PASS", + '"$VENV/bin/pip" install --requirement', + "manage.py collectstatic --noinput", + '"$APP_ROOT/scripts/verify_application.sh"', + ): + require(fragment in rollback, f"rollback contract missing: {fragment}") + + for fragment in ( + "SOURCE_MANIFEST.sha256", + "CANONICAL_ZIP_TIMESTAMP", + "checksum filename does not match target archive", + "target version must be newer than current version", + "target removes existing migration files", + "target modifies existing migration files", + "source-manifest hash mismatch", + "safe_extract", + ): + require(fragment in verifier_text, f"upgrade archive verification contract missing: {fragment}") + + stable, stable_order = module.normalized_version("1.3.0") + rc, rc_order = module.normalized_version("1.3.0-rc.5") + require(stable == "1.3.0" and rc == "1.3.0-rc.5", "version normalization failed") + require(stable_order > rc_order, "stable release must sort after RC of the same base version") + added, removed, modified = module.compare_migrations( + {"apps/core/migrations/0001.py": "a"}, + {"apps/core/migrations/0001.py": "a", "apps/core/migrations/0002.py": "b"}, + ) + require(added == ["apps/core/migrations/0002.py"] and not removed and not modified, "migration comparison failed") + + with tempfile.TemporaryDirectory(prefix="mailstack-upgrade-test-") as temp_name: + temp = Path(temp_name) + current, archive, checksum = build_fixture_archive(temp) + extract_to = temp / "stage" + result = subprocess.run( + [ + sys.executable, + str(APP_SCRIPTS / "verify_upgrade_archive.py"), + "--archive", + str(archive), + "--checksum", + str(checksum), + "--current-app", + str(current), + "--extract-to", + str(extract_to), + ], + cwd=ROOT, + capture_output=True, + text=True, + ) + require(result.returncode == 0, result.stdout + result.stderr) + require("CURRENT_VERSION=1.3.0-rc.5" in result.stdout, "fixture current version was not detected") + require("TARGET_VERSION=1.3.0-rc.6" in result.stdout, "fixture target version was not detected") + require("NEW_MIGRATIONS=1" in result.stdout, "fixture migration delta was not detected") + require("UPGRADE_ARCHIVE_VERIFY=PASS" in result.stdout, "fixture archive did not verify") + require((extract_to / "mailstack-1.3.0-rc.6/mailbox-app/manage.py").is_file(), "verified archive was not staged") + + checksum.write_text("0" * 64 + f" {archive.name}\n", encoding="utf-8") + bad = subprocess.run( + [ + sys.executable, + str(APP_SCRIPTS / "verify_upgrade_archive.py"), + "--archive", + str(archive), + "--checksum", + str(checksum), + "--current-app", + str(current), + "--extract-to", + str(temp / "bad-stage"), + ], + cwd=ROOT, + capture_output=True, + text=True, + ) + require(bad.returncode != 0 and "UPGRADE_ARCHIVE_VERIFY=FAIL" in bad.stdout, "bad checksum did not fail closed") + + print("UPGRADE_ARCHIVE_CONTRACT=PASS") + print("UPGRADE_MIGRATION_GATE=PASS") + print("UPGRADE_ROLLBACK_CONTRACT=PASS") + print("UPGRADE_TESTS=PASS") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 81f762f79ca99a486d21d592f1203aab711322bf Mon Sep 17 00:00:00 2001 From: Md Nurnobi Date: Tue, 18 Aug 2026 00:55:24 -0700 Subject: [PATCH 4/4] release: prepare v1.3.1 --- .github/workflows/ci.yml | 4 +- .github/workflows/release.yml | 4 +- CHANGELOG.md | 10 +- README.md | 6 +- VERSION | 2 +- design/DESIGN_MANIFEST.json | 2 +- docs/BUILD.md | 6 +- docs/FORENSIC_AUDIT_REPORT.md | 15 +- docs/FORENSIC_FILE_INVENTORY.json | 173 +++++++++--------- docs/PUBLISHING.md | 20 +- docs/RELEASE_NOTES_1.3.1.md | 45 +++++ docs/RELEASE_PROCESS.md | 4 +- docs/TEST_REPORT.md | 28 ++- documents/ADMIN_GUIDE.md | 2 +- documents/BASELINE.md | 17 +- documents/DOCUMENTATION_MANIFEST.json | 70 +++---- documents/DOCUMENTATION_POLICY.md | 2 +- documents/HOW_TO_USE.md | 2 +- documents/README.md | 34 ++-- documents/USER_MANUAL.md | 2 +- .../design/ACCESSIBILITY_SPECIFICATION.md | 2 +- documents/design/COMPONENT_MATRIX.md | 2 +- documents/design/FUTURE_UI_ROADMAP.md | 2 +- documents/design/IMPLEMENTATION_STATUS.md | 2 +- documents/design/RESPONSIVE_SPECIFICATION.md | 2 +- documents/design/SCREEN_CATALOG.md | 2 +- documents/design/UI_FOUNDATION.md | 2 +- documents/phases/PHASE-000-BASELINE.md | 2 +- .../PHASE-001-UI-DESIGN-INTAKE-BASELINE.md | 2 +- ...RED-UI-FOUNDATION-AND-APPLICATION-SHELL.md | 2 +- ...COVERY-AND-INBOUND-DELIVERY-RELIABILITY.md | 2 +- ...ASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md | 20 +- mailbox-app/CHANGELOG.md | 3 +- mailbox-app/pyproject.toml | 2 +- mailbox-app/scripts/verify_upgrade_archive.py | 26 ++- scripts/forensic_audit.py | 1 + scripts/test_release_workflow.py | 1 + 37 files changed, 319 insertions(+), 204 deletions(-) create mode 100644 docs/RELEASE_NOTES_1.3.1.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 19c6a91..bc84542 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -131,5 +131,5 @@ jobs: - name: Release verification run: | python scripts/verify_release.py \ - dist/mailstack-1.3.0-rc.5-source.zip \ - --checksum dist/mailstack-1.3.0-rc.5-source.zip.sha256 + dist/mailstack-1.3.1-source.zip \ + --checksum dist/mailstack-1.3.1-source.zip.sha256 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 988528f..a7d7a19 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,7 +63,7 @@ jobs: path: | dist/*.zip dist/*.sha256 - docs/RELEASE_NOTES_1.3.0.md + docs/RELEASE_NOTES_1.3.1.md docs/FORENSIC_AUDIT_REPORT.md if-no-files-found: error @@ -121,7 +121,7 @@ jobs: --verify-tag --target "$GITHUB_SHA" --title "MailStack ${RELEASE_VERSION}" - --notes-file docs/RELEASE_NOTES_1.3.0.md + --notes-file docs/RELEASE_NOTES_1.3.1.md ) if [[ "$RELEASE_PRERELEASE" == "true" ]]; then args+=(--prerelease --latest=false) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34e36ef..e4acec5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable repository-level changes are recorded here. Application history before the open-source conversion remains in `mailbox-app/CHANGELOG.md`. -## 1.3.0-rc.5 — Unreleased +## 1.3.1 — Unpublished source baseline ### Documentation and forensic baseline @@ -26,10 +26,16 @@ All notable repository-level changes are recorded here. Application history befo - Added migration-aware fail-closed recovery: automatic source/runtime rollback is allowed only when no new schema migration has begun; migration-capable failures require reviewed schema/data reconciliation instead of an automatic database restore that could discard newly accepted mail. - Added focused non-destructive upgrade/archive/rollback contract tests and made them blocking in CI and the full forensic audit. +### Verification and CI correction + +- Corrected the five PHASE-004C Ruff findings in `verify_upgrade_archive.py` without changing archive, migration, upgrade, rollback, service, or data semantics. +- Recorded GitHub Actions run `32097491341` as a failed qualification attempt: source safety, documentation, upgrade/rollback contracts, and dependency audit passed before Ruff stopped the workflow; downstream runtime/release gates were therefore not executed in that run. +- Marked the next source-baseline identity as `1.3.1`, synchronized deterministic build/release examples, and moved automated publication to version-matched `docs/RELEASE_NOTES_1.3.1.md`. The version mark is not itself a production-readiness or GitHub-release claim. + ### Compatibility - PHASE-004C changes maintained operational tooling only; it does not add a database migration, application route, authorization/UI/mail-flow behavior, installer behavior, deployment-template rewrite, DNS/TLS change, or automatic host-configuration migration. -- `v1.3.0-rc.4` and its published source identity remain immutable; `1.3.0-rc.5` is the next development-candidate version and is not yet a published release. +- `v1.3.0-rc.4` and its published source identity remain immutable. `1.3.1` is the next frozen source-baseline version mark and remains unpublished until its GitHub/main CI and operational release gates are accepted. ## Unreleased — MailStack repository bootstrap diff --git a/README.md b/README.md index bffa23f..a57c239 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ It combines **Postfix**, **Dovecot LMTP**, **Django**, **MariaDB**, **Maildir**, **Gunicorn**, and **Nginx** in a hardened single-node reference deployment. MailStack provides a private browser-based inbox, administrator-managed users, object-level mailbox access, safe email rendering, protected attachments, live inbox updates, operational audit logs, backup and restore tooling, a public website, and an isolated contact service. -> **Release status:** `v1.3.0-rc.4` is the latest published release candidate. The repository development version is `1.3.0-rc.5` for PHASE-004 reliability work. PHASE-003 staging validated real external SMTP/LMTP delivery after the accepted fixes; an exact RC4 clean-host reinstall remains deferred until a fresh VPS is available. Backup/restore, restart-recovery, legal, and final release-owner acceptance still gate stable promotion. +> **Release status:** `v1.3.0-rc.4` remains the latest published release candidate. The repository source baseline is now marked `1.3.1` for the PHASE-004C verification/fix freeze; this version is not yet a published GitHub release or a production-readiness claim. PHASE-003 staging validated real external SMTP/LMTP delivery after the accepted fixes; backup/restore, restart/reboot, clean-host, legal, and final release-owner acceptance remain separate operational gates. ## Why MailStack @@ -211,8 +211,8 @@ Build and verify a deterministic source release: ```bash python scripts/build_release.py --root . python scripts/verify_release.py \ - dist/mailstack-1.3.0-rc.5-source.zip \ - --checksum dist/mailstack-1.3.0-rc.5-source.zip.sha256 + dist/mailstack-1.3.1-source.zip \ + --checksum dist/mailstack-1.3.1-source.zip.sha256 ``` ## Security diff --git a/VERSION b/VERSION index d4f0697..3a3cd8c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.0-rc.5 +1.3.1 diff --git a/design/DESIGN_MANIFEST.json b/design/DESIGN_MANIFEST.json index 0a9aa00..4ca94b8 100644 --- a/design/DESIGN_MANIFEST.json +++ b/design/DESIGN_MANIFEST.json @@ -561,7 +561,7 @@ "unsupported_features_inactive": true }, "project": "MailStack", - "release_version": "1.3.0-rc.5", + "release_version": "1.3.1", "schema_version": 1, "source_archive": { "archive_crc": "PASS", diff --git a/docs/BUILD.md b/docs/BUILD.md index 8c0b781..91d1e81 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -20,8 +20,8 @@ python scripts/forensic_audit.py --root . --full ```bash python scripts/build_release.py --root . python scripts/verify_release.py \ - dist/mailstack-1.3.0-rc.5-source.zip \ - --checksum dist/mailstack-1.3.0-rc.5-source.zip.sha256 + dist/mailstack-1.3.1-source.zip \ + --checksum dist/mailstack-1.3.1-source.zip.sha256 ``` The builder normalizes archive timestamps, preserves executable permissions, writes a source manifest, excludes generated/runtime artifacts and emits a SHA-256 checksum. @@ -38,4 +38,4 @@ missing successful `main` CI, and pre-existing releases fail closed. ## Stable promotion -Do not change the version to `1.3.0` until every required human gate in `docs/PUBLIC_RELEASE_CHECKLIST.md` passes. +The `1.3.1` source baseline is an unpublished version mark. Do not create or push a stable release tag until every required human/operational gate in `docs/PUBLIC_RELEASE_CHECKLIST.md` passes. diff --git a/docs/FORENSIC_AUDIT_REPORT.md b/docs/FORENSIC_AUDIT_REPORT.md index d1f03b0..5006505 100644 --- a/docs/FORENSIC_AUDIT_REPORT.md +++ b/docs/FORENSIC_AUDIT_REPORT.md @@ -1,10 +1,10 @@ -# Forensic audit report — MailStack 1.3.0 RC5 development baseline +# Forensic audit report — MailStack 1.3.1 source baseline verification **PHASE-004C audit date:** 2026-08-17 -**Repository development version:** `1.3.0-rc.5` +**Repository source-baseline version:** `1.3.1` **Latest published release candidate:** `v1.3.0-rc.4` **Target runtime:** Ubuntu Server 24.04 LTS and CPython 3.12 -**Current classification:** RC4 official baseline preserved; PHASE-004A/004B branch qualification passed; PHASE-004C locally source-qualified and pending GitHub requalification +**Current classification:** 1.3.1 owner-marked source-baseline correction locally structurally qualified; GitHub requalification and live PHASE-004D acceptance remain pending ## Executive disposition @@ -201,6 +201,9 @@ privilege, systemd confinement, archive safety, checksum verification, and fail- The published `v1.3.0-rc.4` source is a qualified release candidate and is the official frozen source baseline for PHASE-004. PHASE-004A corrects the documentation evidence around that baseline but does -not retroactively modify or retag RC4. The working repository version `1.3.0-rc.5` remains a development candidate. PHASE-004B branch CI -has passed, while PHASE-004C still requires dependency-backed GitHub CI and no live upgrade is claimed -until the separately approved PHASE-004D acceptance campaign. +not retroactively modify or retag RC4. The owner-marked working source baseline is `1.3.1` and remains unpublished. PHASE-004B branch CI +has passed. PHASE-004C GitHub Actions run `32097491341` reached Ruff after all earlier gates passed, +then failed on five style findings in the new archive verifier; downstream runtime/release gates were +skipped. This scoped correction removes only those Ruff findings and synchronizes the requested 1.3.1 +baseline/release metadata. A fresh GitHub CI run is required before remote qualification, and no live +upgrade is claimed until the separately approved PHASE-004D acceptance campaign. diff --git a/docs/FORENSIC_FILE_INVENTORY.json b/docs/FORENSIC_FILE_INVENTORY.json index 526f325..3c5d568 100644 --- a/docs/FORENSIC_FILE_INVENTORY.json +++ b/docs/FORENSIC_FILE_INVENTORY.json @@ -74,14 +74,14 @@ "kind": "text", "lines": 135, "path": ".github/workflows/ci.yml", - "sha256": "29e68ada609952c8d3d2c958b6298f1fb5ed5149bf5a2c8d72498b429d6aa11a", - "size_bytes": 4301 + "sha256": "3622f22f4aa5fa5d1bf89b17d7807cc6f837ec92d70229dbc35b284820419c71", + "size_bytes": 4291 }, { "kind": "text", "lines": 131, "path": ".github/workflows/release.yml", - "sha256": "f6c150f7310b184272c5fe22fd1fe13f78c4e655d42413625861b29fae33ade3", + "sha256": "a6543e8464e093e05d083f2d82566d9e209480778cf2689e30441d491955b53e", "size_bytes": 4418 }, { @@ -93,10 +93,10 @@ }, { "kind": "text", - "lines": 125, + "lines": 131, "path": "CHANGELOG.md", - "sha256": "77076ab4fa3602cd3770c6208a82386e1594616a13d9f8b5bf690c10f4878f49", - "size_bytes": 11553 + "sha256": "f2bcc49e9eeccf027add792cb3b10b2a2f579e13eedbf5163dc7d11b1b1c5d31", + "size_bytes": 12361 }, { "kind": "text", @@ -137,8 +137,8 @@ "kind": "text", "lines": 314, "path": "README.md", - "sha256": "0b42d27779799a7e9fc5daba7d0dfb867f2f89feffb19eff39fad6e08df9d72b", - "size_bytes": 13170 + "sha256": "2b1c0efbecd82fe86822628a72a2db26c454758016d9c086633d1ac2a0db1980", + "size_bytes": 13196 }, { "kind": "text", @@ -165,8 +165,8 @@ "kind": "text", "lines": 1, "path": "VERSION", - "sha256": "055c3f7a1baed813c28024ff2509c55fb90e27cd93799cc933395edd5c950a44", - "size_bytes": 11 + "sha256": "10e2e117b01523891ca271a615ae36e9d5e52348fd7d642f66727602c1b8dcbe", + "size_bytes": 6 }, { "kind": "text", @@ -277,8 +277,8 @@ "kind": "text", "lines": 581, "path": "design/DESIGN_MANIFEST.json", - "sha256": "5b3de8f4ca8e3edd890eaef30c257465e981e5de597d3c757b8ae42c8cb23781", - "size_bytes": 21143 + "sha256": "7192a7c8fd128c90fc58ad1609d21a1853ce0654d3f28a98ce30aa7727e4f069", + "size_bytes": 21138 }, { "kind": "text", @@ -469,8 +469,8 @@ "kind": "text", "lines": 41, "path": "docs/BUILD.md", - "sha256": "ec3e1eabd1e1d5bbe3bfe3852c31b7d605d199fa969596999e0dfed223de1ad9", - "size_bytes": 1388 + "sha256": "76a2e43a20c3d2e9f0de047c21802d43cf68671eb127489f53b982eaf9780748", + "size_bytes": 1456 }, { "kind": "text", @@ -537,10 +537,10 @@ }, { "kind": "text", - "lines": 206, + "lines": 209, "path": "docs/FORENSIC_AUDIT_REPORT.md", - "sha256": "02abfd11d1ed31948bc8249d496ba042ff9f2268de7109c99a987656dd53e10f", - "size_bytes": 12139 + "sha256": "71bf39fbd6550b6e6607281e2f82f411e0b4b03614e4a1754043edf7e17f7f0a", + "size_bytes": 12463 }, { "kind": "text", @@ -623,8 +623,8 @@ "kind": "text", "lines": 67, "path": "docs/PUBLISHING.md", - "sha256": "4080361c928c3de1d7fae531cad4c8d73e166916efbfdf1febd52b447dd4c7c8", - "size_bytes": 2926 + "sha256": "2ac720ae114b5d0dacf01330cd3fba4f8bab412af1aecb6742c75e4f33e7b90c", + "size_bytes": 3002 }, { "kind": "text", @@ -647,12 +647,19 @@ "sha256": "341b3656b21ccbcf7fb11b1f537d418df995d950e09c400d228abdafd2acde2e", "size_bytes": 8304 }, + { + "kind": "text", + "lines": 45, + "path": "docs/RELEASE_NOTES_1.3.1.md", + "sha256": "eb92579ccc92d1d49513d458e80f4b6349f4451524d5994b11b36a3fb36bf79e", + "size_bytes": 2841 + }, { "kind": "text", "lines": 16, "path": "docs/RELEASE_PROCESS.md", - "sha256": "52b80cda87ff2477c062f3ad88346abbab4d5e0b6907382ae58468a90ede0658", - "size_bytes": 1601 + "sha256": "40091dbd439861d5028c72d9d9b668f15dd71c8f48ddc3a7d9635a950dcc09db", + "size_bytes": 1619 }, { "kind": "text", @@ -670,10 +677,10 @@ }, { "kind": "text", - "lines": 176, + "lines": 194, "path": "docs/TEST_REPORT.md", - "sha256": "89bb9ba2d85ba7fcd0f19926527ece7da5aeda3e2bd68f29b2b9e47c1edbf140", - "size_bytes": 9468 + "sha256": "9fc597cae2aa5be46cc12e39ca3458e68f84060d1adfdee381352c77dad6f082", + "size_bytes": 10711 }, { "kind": "text", @@ -700,134 +707,134 @@ "kind": "text", "lines": 68, "path": "documents/ADMIN_GUIDE.md", - "sha256": "9dea23e3e918f8357ea968891e18a5b0b3b24326515519bb1ced91c0395cf9c6", - "size_bytes": 3855 + "sha256": "018a50bca6cc47ef51adda6104dc77a306aee87abeee7d9410f7f3f13e3182f3", + "size_bytes": 3850 }, { "kind": "text", - "lines": 65, + "lines": 70, "path": "documents/BASELINE.md", - "sha256": "377fc784d744848fc173219a1884b66e7282e81ac6da37fe25171625c1890108", - "size_bytes": 3408 + "sha256": "5343fed8ab585b7f19341b7727e8dadb0279d3f3ee06dfb06992dd687e3fbeb8", + "size_bytes": 3847 }, { "kind": "text", "lines": 221, "path": "documents/DOCUMENTATION_MANIFEST.json", - "sha256": "cc14ac4f645f9fcadefc0e5847856919abbc92d91e60774a7622794da0e2a980", - "size_bytes": 8429 + "sha256": "dd4967dcc579af807a16aaee228d8380d70bca7ed6e2944072f6ba15a5407795", + "size_bytes": 8339 }, { "kind": "text", "lines": 76, "path": "documents/DOCUMENTATION_POLICY.md", - "sha256": "bc0459b0bbc920759e50ba41a3786f866c43e900fe2ef0a1672f8b955b4c796f", - "size_bytes": 3189 + "sha256": "c8910661b15e941c6e2db493973f6e3bdaeba64ad8554eb227d1245c8c20c9a5", + "size_bytes": 3184 }, { "kind": "text", "lines": 76, "path": "documents/HOW_TO_USE.md", - "sha256": "eafe95fd3d3246716c5ef320c12ee127df5cdd502e1c2080fd9b5cafee1b3dcc", - "size_bytes": 3253 + "sha256": "364f048cd26bd0f564f7a47c26b4624942981bbdf748d9ba7c28ceba705b65a5", + "size_bytes": 3248 }, { "kind": "text", "lines": 54, "path": "documents/README.md", - "sha256": "aa67fe16b45310aa8e992e3d23b1b4cb1309fe32dfe72f3716dac5d7fb29995d", - "size_bytes": 3873 + "sha256": "570e8b3d5c078a70fc346c9bb15d83adac28316f968944c71920e7212affa794", + "size_bytes": 3788 }, { "kind": "text", "lines": 82, "path": "documents/USER_MANUAL.md", - "sha256": "6d65ffe731c2f3725ce45705c49a438514aad60fe13a5e747812ff7d3082238e", - "size_bytes": 4218 + "sha256": "778de1cec3ef89171fd2768064940c21c5f0d8f444e5376e4b43123cb3a58f13", + "size_bytes": 4213 }, { "kind": "text", "lines": 46, "path": "documents/design/ACCESSIBILITY_SPECIFICATION.md", - "sha256": "aabae514afafe1186fa69f04fadd35423356cfe8d02922531e3713c7567b88d8", - "size_bytes": 2187 + "sha256": "c9224cc17caca417a5eb0d363b7b472e250890edef028b01edc7d58cec753aa3", + "size_bytes": 2182 }, { "kind": "text", "lines": 55, "path": "documents/design/COMPONENT_MATRIX.md", - "sha256": "b38667400ae202d75e2b95dd81bbdde8907b263cb8718eaa205c50878d91480f", - "size_bytes": 3465 + "sha256": "957a04577dc899fd9f457b59e029a1b4d50b6ab5c6089f6a415339cd20cbe5e5", + "size_bytes": 3460 }, { "kind": "text", "lines": 52, "path": "documents/design/FUTURE_UI_ROADMAP.md", - "sha256": "7523c559d870025b1d1a9fd9127e603130bb96e87acfd2126fb6ee1d6025d1d6", - "size_bytes": 1567 + "sha256": "078989742c6aab70840e48ccf887c472f538079e48a73811eb48b0506d9a413a", + "size_bytes": 1562 }, { "kind": "text", "lines": 48, "path": "documents/design/IMPLEMENTATION_STATUS.md", - "sha256": "a0e25cf557e479a375e402b00e2518377e60572482d023031574da528d1775e4", - "size_bytes": 2518 + "sha256": "ad807f84993961013133f0c8e136c4fc5d636fd6051e352e6fb7d01b43e1be9f", + "size_bytes": 2513 }, { "kind": "text", "lines": 51, "path": "documents/design/RESPONSIVE_SPECIFICATION.md", - "sha256": "fb5168baac39e08c40c7ba30ad0c675e49c7a7be57fe1c6df0966f048bafa992", - "size_bytes": 2027 + "sha256": "b540f41863ec20c34871d1b0abf39517a40a2ec1a183128d8ce321e9d7a8867a", + "size_bytes": 2022 }, { "kind": "text", "lines": 62, "path": "documents/design/SCREEN_CATALOG.md", - "sha256": "54c7e6d838074cefc19b0626f84fcc3f965de42de8a04e7c6ac185a149d73b5c", - "size_bytes": 3292 + "sha256": "0f45bcdddea7a40c6bcaa753bcc5b8d7c49a2d66f383003c87244ae865c007a6", + "size_bytes": 3287 }, { "kind": "text", "lines": 89, "path": "documents/design/UI_FOUNDATION.md", - "sha256": "c98a9d3d7f982a604392bebc4ebedb3bdbf92ac691638424032ac4cc4cb5bc48", - "size_bytes": 3758 + "sha256": "38b97a9c193ad8a7a5b21d031535afa72b3e5fc3b9f6772abe37c1379dc0c459", + "size_bytes": 3753 }, { "kind": "text", "lines": 59, "path": "documents/phases/PHASE-000-BASELINE.md", - "sha256": "9320a939456c39acf5a59c85d008010d205e3ebd6fc83ca07e0201b28e53693d", - "size_bytes": 2543 + "sha256": "9c04cd5594cd07cc487f47fe7e6fd751775ba202232b84db3385e959f1ceba57", + "size_bytes": 2538 }, { "kind": "text", "lines": 68, "path": "documents/phases/PHASE-001-UI-DESIGN-INTAKE-BASELINE.md", - "sha256": "4e86d967000db5a21e870d762b220236de6139a67eedb25c4ea259328c891310", - "size_bytes": 2770 + "sha256": "b30ca3041469b56b1c3256cab99d10b12f3d00241043cb449d466ecbde352154", + "size_bytes": 2765 }, { "kind": "text", "lines": 98, "path": "documents/phases/PHASE-002-SHARED-UI-FOUNDATION-AND-APPLICATION-SHELL.md", - "sha256": "fff6f07ae9cb661d6689ad679d9c2be1ffa97229990b5690142f6cf9ee39d2b8", - "size_bytes": 5493 + "sha256": "a1e2f224910ad0eea4d43f87f0734f3ecbd394e804497b7e6cc3c8a5ec2567dc", + "size_bytes": 5488 }, { "kind": "text", "lines": 86, "path": "documents/phases/PHASE-003-INSTALLATION-RECOVERY-AND-INBOUND-DELIVERY-RELIABILITY.md", - "sha256": "e656dfe0d583d318fe803c89988cc636f8f60673f12e88ab865c90193027ec7c", - "size_bytes": 5055 + "sha256": "2b63990b712eeab3c7e28f948385a3485667a4397a4c9f0b4a731e72165393df", + "size_bytes": 5050 }, { "kind": "text", - "lines": 126, + "lines": 142, "path": "documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md", - "sha256": "b3cef810cf4dfa826f079546b795c27dcb9ab89068f40bada5b048726e7eefb4", - "size_bytes": 8284 + "sha256": "964c57e99da6bc7b63e5ee577129cf7aa45770e9bdc4c70e630bfac502dba828", + "size_bytes": 9368 }, { "kind": "text", @@ -874,10 +881,10 @@ }, { "kind": "text", - "lines": 87, + "lines": 88, "path": "mailbox-app/CHANGELOG.md", - "sha256": "73caea1e6b61cc5224811f79cac5f705115bbb49c360b9ec4084c42022e9a3d0", - "size_bytes": 5757 + "sha256": "c47ddb63ffb221477ce9d4901e14ad67a26870a4fbf1ffc2bc2514b5d1da6637", + "size_bytes": 5891 }, { "kind": "text", @@ -3168,8 +3175,8 @@ "kind": "text", "lines": 80, "path": "mailbox-app/pyproject.toml", - "sha256": "6fe0affefedbb4b2ec29ea757236f2f2a55564c85409031da666fbc03334beea", - "size_bytes": 2411 + "sha256": "adfb3bb65672b6c0723c80a73def340a586f4dff623a6f4b41dab44053fc9723", + "size_bytes": 2408 }, { "kind": "text", @@ -3372,7 +3379,7 @@ }, { "kind": "text", - "lines": 313, + "lines": 325, "path": "mailbox-app/scripts/verify_upgrade_archive.py", "python": { "classes": [ @@ -3404,8 +3411,8 @@ ], "methods": [] }, - "sha256": "395fbbe44bbe3dc7cd1412be5f11504f99e9f0552a2a8d6fc5d6b822ca46d70d", - "size_bytes": 13714 + "sha256": "61158e40f0b7acb3ae47131c98975659bc155bb10c7324c9f5cc240003bff077", + "size_bytes": 13826 }, { "kind": "text", @@ -4756,7 +4763,7 @@ }, { "kind": "text", - "lines": 405, + "lines": 406, "path": "scripts/forensic_audit.py", "python": { "classes": [], @@ -4782,8 +4789,8 @@ ], "methods": [] }, - "sha256": "907b47435ba9e9284e98fe24feecf36d0b4153a7fc8b9dee8b3be9232a51bee5", - "size_bytes": 14832 + "sha256": "5238686caff6906b9a69417df0cd0eb66a942d440fc13ceb9d80cc608bb9fc1e", + "size_bytes": 14867 }, { "kind": "text", @@ -5129,7 +5136,7 @@ }, { "kind": "text", - "lines": 223, + "lines": 224, "path": "scripts/test_release_workflow.py", "python": { "classes": [], @@ -5155,8 +5162,8 @@ ], "methods": [] }, - "sha256": "a0555c8b9819f8e51bed877d8686c2e49ddeba8793ef5ee1613aa63d06804704", - "size_bytes": 6531 + "sha256": "2796bcdb10d427c00d4bf778ab6281883f6cb7c479f2b430e15a072e96bffeee", + "size_bytes": 6570 }, { "kind": "text", @@ -5267,7 +5274,7 @@ "size_bytes": 6930 } ], - "release_version": "1.3.0-rc.5", + "release_version": "1.3.1", "schema_version": 1, "scope": "All maintained repository files excluding generated archives, caches, build output and this inventory file itself.", "self_entry": { @@ -5276,15 +5283,15 @@ }, "summary": { "binary_files": 25, - "files": 410, + "files": 411, "python_classes": 76, "python_files": 148, "python_functions": 446, "python_methods": 61, "shell_files": 15, "shell_functions": 41, - "text_files": 385, - "total_bytes": 31747591, - "total_text_lines": 28738 + "text_files": 386, + "total_bytes": 31754565, + "total_text_lines": 28846 } } diff --git a/docs/PUBLISHING.md b/docs/PUBLISHING.md index 40db65d..f839193 100644 --- a/docs/PUBLISHING.md +++ b/docs/PUBLISHING.md @@ -12,10 +12,10 @@ This repository is prepared for public hosting on GitHub, GitLab, Codeberg, or a 4. Build and verify the deterministic release archive: ```bash - python scripts/build_release.py --root . --version 1.3.0-rc.5 + python scripts/build_release.py --root . --version 1.3.1 python scripts/verify_release.py \ - dist/mailstack-1.3.0-rc.5-source.zip \ - --checksum dist/mailstack-1.3.0-rc.5-source.zip.sha256 + dist/mailstack-1.3.1-source.zip \ + --checksum dist/mailstack-1.3.1-source.zip.sha256 ``` 5. Complete the clean Ubuntu 24.04 acceptance checklist before promoting a release candidate to production-ready. @@ -27,18 +27,18 @@ From the audited repository root: ```bash git init git add . -git commit -m "Prepare MailStack 1.3.0-rc.5 candidate" +git commit -m "Prepare MailStack 1.3.1 candidate" git branch -M main git remote add origin git push -u origin main ``` -Create an annotated release-candidate tag only after the intended release commit is the current +Create the annotated stable-looking tag only after the intended release commit is the current `main` head and the exact `main` SHA has a successful push CI run: ```bash -git tag -a v1.3.0-rc.5 -m "MailStack 1.3.0 RC5" -git push origin v1.3.0-rc.5 +git tag -a v1.3.1 -m "MailStack 1.3.1" +git push origin v1.3.1 ``` The tag push triggers `.github/workflows/release.yml`. It fails closed unless the tag matches @@ -46,11 +46,11 @@ The tag push triggers `.github/workflows/release.yml`. It fails closed unless th has no existing GitHub Release. The workflow rebuilds and verifies the deterministic source archive, keeps a GitHub Actions artifact, and automatically creates the GitHub Release with: -- `mailstack-1.3.0-rc.5-source.zip` -- `mailstack-1.3.0-rc.5-source.zip.sha256` +- `mailstack-1.3.1-source.zip` +- `mailstack-1.3.1-source.zip.sha256` RC tags are published as pre-releases and are explicitly not marked latest; stable tags are normal -latest releases. Manual `workflow_dispatch` is validation/build-only and cannot publish. Existing +latest releases. The `1.3.1` baseline must not be tagged/published until the outstanding operational and release-owner gates are accepted. Manual `workflow_dispatch` is validation/build-only and cannot publish. Existing releases are never automatically edited, clobbered, or overwritten. ## Repository settings diff --git a/docs/RELEASE_NOTES_1.3.1.md b/docs/RELEASE_NOTES_1.3.1.md new file mode 100644 index 0000000..65b4647 --- /dev/null +++ b/docs/RELEASE_NOTES_1.3.1.md @@ -0,0 +1,45 @@ +# MailStack 1.3.1 source-baseline notes + +## Scope + +MailStack 1.3.1 is the owner-requested source-baseline mark that consolidates PHASE-004A forensic/ +documentation finalization, PHASE-004B fail-closed tag-to-GitHub-Release automation, and PHASE-004C +controlled existing-server source/runtime upgrade and rollback tooling. It does not introduce a new +mailbox/UI feature, outbound mail capability, database model/migration, route, authorization change, +Postfix/Dovecot routing change, installer rewrite, DNS/TLS change, or live VPS mutation. + +## PHASE-004C CI correction + +GitHub Actions run `32097491341` tested PHASE-004C commit +`47e62bb6c0acd0216fb261f47f85959655b489e0`. Source safety, documentation/design/UI contracts, +forensic inventory, deployment/installer/operations/release-workflow contracts, the PHASE-004C +upgrade/archive/rollback contracts, and the dependency vulnerability audit all passed. Ruff then +reported four E501 line-length findings and one SIM102 nested-`if` finding in the new upgrade archive +verifier. The 1.3.1 correction changes only statement/control-expression layout for those five +findings; the archive integrity, version ordering, migration detection, safe extraction, error text, +and fail-closed semantics are preserved. + +## Upgrade/rollback mechanism + +The PHASE-004C upgrader requires a deterministic source ZIP and matching SHA-256, verifies canonical +ZIP/source-manifest integrity, rejects same-version/downgrade and historical migration rewrites, +requires explicit acknowledgement for new migrations, acquires a non-blocking runtime lock, and +creates both a source/runtime snapshot and the maintained consistent data backup before mutation. +After that backup, Postfix and Dovecot remain active while Gunicorn, ingestion, and the contact worker +are replaced. No-new-migration failures can restore source/runtime automatically. Migration-capable +failures after schema mutation begins require reviewed reconciliation; MariaDB and Maildir are never +silently restored by the standalone rollback command. + +## Release automation + +The PHASE-004B release workflow remains fail-closed. A publishing tag must match `VERSION` and +`project.version`, point to the exact current `main` head, have successful exact-SHA `main` push CI, +and have no existing GitHub Release. Manual workflow dispatch remains validation/build-only. + +## Qualification boundary + +The `1.3.1` version mark and deterministic baseline ZIP are not, by themselves, a production-ready +or published stable-release claim. A fresh GitHub CI run for the correction commit is required before +remote qualification. The first real existing-VPS use of the PHASE-004C mechanism remains PHASE-004D, +and clean-host, backup/restore, restart/reboot, legal/ownership, and final release-owner acceptance +remain separate gates before publication as production-ready. diff --git a/docs/RELEASE_PROCESS.md b/docs/RELEASE_PROCESS.md index d154cb3..2d4b99d 100644 --- a/docs/RELEASE_PROCESS.md +++ b/docs/RELEASE_PROCESS.md @@ -7,10 +7,10 @@ 5. Run the online dependency audit in CI. 6. Test a clean installation on an isolated Ubuntu Server 24.04 LTS VPS. 7. Verify external inbound SMTP, login, authorization boundaries, live updates, safe HTML, and attachment downloads. -8. Build with `python scripts/build_release.py --root . --version 1.3.0-rc.5` and verify the ZIP/checksum with `python scripts/verify_release.py`. +8. Build with `python scripts/build_release.py --root . --version 1.3.1` and verify the ZIP/checksum with `python scripts/verify_release.py`. 9. Merge the intended release commit to `main` and require a successful `main` push CI run for the exact SHA. 10. Create the matching `v` tag on the current `main` head and push that tag. Do not create a release manually first. 11. The tag workflow validates tag/version/package identity, exact `main` head, successful `main` CI, and release absence; it then builds/verifies the deterministic archive and creates the GitHub Release with ZIP/SHA assets. 12. Verify the published release classification and attached checksum. RC versions must be pre-releases; stable versions are normal/latest releases. -`workflow_dispatch` is intentionally build/validation-only. Release automation never edits or clobbers an existing release. A release is not promoted from release candidate to production-ready until the clean-VPS acceptance test passes. +`workflow_dispatch` is intentionally build/validation-only. Release automation never edits or clobbers an existing release. A version-marked source baseline is not a production-ready release until the clean-VPS and remaining operational acceptance gates pass. diff --git a/docs/TEST_REPORT.md b/docs/TEST_REPORT.md index 11a5749..b89e5fc 100644 --- a/docs/TEST_REPORT.md +++ b/docs/TEST_REPORT.md @@ -1,8 +1,8 @@ -# Test report — MailStack 1.3.0 RC5 development baseline +# Test report — MailStack 1.3.1 source baseline verification **PHASE-004C verification date:** 2026-08-17 **Latest published release candidate:** `v1.3.0-rc.4` -**Repository development version:** `1.3.0-rc.5` +**Repository source-baseline version:** `1.3.1` ## Authoritative released-RC4 application suite @@ -135,8 +135,8 @@ assembled local candidate passed the following dependency-free/structural gates: | Structural forensic audit | PASS — 405 files scanned, 144 Python, 13 shell, zero blocking findings | The final assembled PHASE-004A delta was additionally checked with `git diff --check`; its branch CI -closure is recorded above. `1.3.0-rc.5` remains a development candidate until the complete intended -RC5 line is merged, requalified on `main`, tagged, and published through the release workflow. +closure is recorded above. The `1.3.0-rc.5` identity was the PHASE-004 development candidate at that +time; the current owner-requested source-baseline mark is `1.3.1`. ## PHASE-004C upgrade/rollback tooling qualification @@ -169,7 +169,25 @@ regression and the deterministic release build must still pass in the user's iso validation and GitHub CI after the delta is applied. PHASE-004D owns the first real existing-VPS upgrade acceptance. -## Manual acceptance still outstanding before stable `1.3.0` +## PHASE-004C GitHub CI failure and scoped correction + +GitHub Actions run `32097491341` executed exact PHASE-004C commit +`47e62bb6c0acd0216fb261f47f85959655b489e0`. The run passed setup, dependency installation, source +safety, documentation/design/UI contracts, documentation policy, forensic inventory, deployment +templates, installer/operations/release-workflow contracts, all PHASE-004C upgrade/rollback contracts, +and the dependency vulnerability audit. Ruff then reported exactly five findings, all in +`mailbox-app/scripts/verify_upgrade_archive.py`: four line-length findings and one nested-`if` +SIM102 finding. Because CI is fail-closed, Bandit, Django tests/coverage, contact tests, Django +checks, shell syntax, the full forensic gate, deterministic build, and release verification were +skipped in that run. + +The `1.3.1` correction changes only the Ruff representation of those verifier statements plus the +version/release/baseline metadata required by the owner's explicit baseline mark. It does not change +the verifier's conditions, error semantics, archive validation, migration comparison, or extraction +logic. A new GitHub Actions run on the correction commit remains required before remote qualification +or release publication. + +## Manual acceptance still outstanding before publishing `1.3.1` as production-ready A future isolated Ubuntu 24.04 acceptance campaign must cover the exact-source clean install, TLS/DNS/mail routing, unknown-recipient rejection, real inbound delivery, authorization isolation, diff --git a/documents/ADMIN_GUIDE.md b/documents/ADMIN_GUIDE.md index 7400140..4be16bb 100644 --- a/documents/ADMIN_GUIDE.md +++ b/documents/ADMIN_GUIDE.md @@ -4,7 +4,7 @@ title: MailStack Administrator Guide document_type: admin-guide audience: mailstack-administrators status: active -version: 1.3.0-rc.5 +version: 1.3.1 last_reviewed: 2026-08-17 --- diff --git a/documents/BASELINE.md b/documents/BASELINE.md index 4624a12..132aaf2 100644 --- a/documents/BASELINE.md +++ b/documents/BASELINE.md @@ -4,7 +4,7 @@ title: MailStack Baseline document_type: baseline audience: maintainers-and-operators status: active -version: 1.3.0-rc.5 +version: 1.3.1 last_reviewed: 2026-08-17 baseline_id: MAILSTACK-1.3.0-RC4-OFFICIAL-SOURCE-BASELINE-001 source_commit: 896dbcc2ed1f38d9c618bf0b712efe5923f92e56 @@ -23,8 +23,11 @@ anchored to published tag `v1.3.0-rc.4`, commit The earlier `MAILSTACK-1.3.0-RC1-DOCS-BASELINE-001`, anchored to commit `1e1737edea2e6c922265a15d8584b56671820c65`, remains historical documentation provenance. It no -longer represents the current canonical source anchor. The repository development version is -`1.3.0-rc.5`; that identity begins PHASE-004 and does not rewrite the frozen RC4 tag or archive. +longer represents the current canonical source anchor. For the current verification/fix campaign, +the project owner designated `MailStack_v1.3.0-rc.5_Baseline.zip` as the working input freeze. The +resulting source is version-marked `1.3.1`; its baseline ZIP/checksum are frozen outside this document +after deterministic build verification. This does not rewrite the published RC4 tag/archive or claim +a published stable release. ## Qualification status @@ -47,9 +50,11 @@ shared inbox, MariaDB data contracts, Nginx and Gunicorn deployment, systemd ser contact service, backup/restore tooling, and established legacy runtime identifiers. It does not add SMTP submission, IMAP, POP3, public registration, outbound campaigns, or multi-node operation. -PHASE-004A changes documentation, version/release metadata, and generated manifests only. It does -not alter application runtime behavior, database schema, migrations, routes, permissions, UI, mail -flow, installer behavior, deployment templates, or service configuration. +PHASE-004A changes documentation/version metadata only, PHASE-004B adds release automation, and +PHASE-004C adds controlled upgrade/rollback tooling. The 1.3.1 correction is limited to the five Ruff +findings in the PHASE-004C archive verifier plus version/release/baseline metadata synchronization. +It does not alter application business logic, database schema/migrations, routes, permissions, UI, +mail flow, installer behavior, deployment templates, DNS/TLS, or the live VPS. ## Change control diff --git a/documents/DOCUMENTATION_MANIFEST.json b/documents/DOCUMENTATION_MANIFEST.json index dc77bd4..a82cf87 100644 --- a/documents/DOCUMENTATION_MANIFEST.json +++ b/documents/DOCUMENTATION_MANIFEST.json @@ -9,10 +9,10 @@ "last_reviewed": "2026-08-17", "path": "documents/ADMIN_GUIDE.md", "phase_id": null, - "sha256": "9dea23e3e918f8357ea968891e18a5b0b3b24326515519bb1ced91c0395cf9c6", + "sha256": "018a50bca6cc47ef51adda6104dc77a306aee87abeee7d9410f7f3f13e3182f3", "status": "active", "title": "MailStack Administrator Guide", - "version": "1.3.0-rc.5" + "version": "1.3.1" }, { "audience": "maintainers-and-operators", @@ -21,10 +21,10 @@ "last_reviewed": "2026-08-17", "path": "documents/BASELINE.md", "phase_id": null, - "sha256": "377fc784d744848fc173219a1884b66e7282e81ac6da37fe25171625c1890108", + "sha256": "5343fed8ab585b7f19341b7727e8dadb0279d3f3ee06dfb06992dd687e3fbeb8", "status": "active", "title": "MailStack Baseline", - "version": "1.3.0-rc.5" + "version": "1.3.1" }, { "audience": "contributors-and-maintainers", @@ -33,10 +33,10 @@ "last_reviewed": "2026-08-17", "path": "documents/DOCUMENTATION_POLICY.md", "phase_id": null, - "sha256": "bc0459b0bbc920759e50ba41a3786f866c43e900fe2ef0a1672f8b955b4c796f", + "sha256": "c8910661b15e941c6e2db493973f6e3bdaeba64ad8554eb227d1245c8c20c9a5", "status": "active", "title": "Documentation Policy", - "version": "1.3.0-rc.5" + "version": "1.3.1" }, { "audience": "mailbox-users-and-administrators", @@ -45,10 +45,10 @@ "last_reviewed": "2026-08-17", "path": "documents/HOW_TO_USE.md", "phase_id": null, - "sha256": "eafe95fd3d3246716c5ef320c12ee127df5cdd502e1c2080fd9b5cafee1b3dcc", + "sha256": "364f048cd26bd0f564f7a47c26b4624942981bbdf748d9ba7c28ceba705b65a5", "status": "active", "title": "How to Use MailStack", - "version": "1.3.0-rc.5" + "version": "1.3.1" }, { "audience": "mailbox-users", @@ -57,10 +57,10 @@ "last_reviewed": "2026-08-17", "path": "documents/USER_MANUAL.md", "phase_id": null, - "sha256": "6d65ffe731c2f3725ce45705c49a438514aad60fe13a5e747812ff7d3082238e", + "sha256": "778de1cec3ef89171fd2768064940c21c5f0d8f444e5376e4b43123cb3a58f13", "status": "active", "title": "MailStack User Manual", - "version": "1.3.0-rc.5" + "version": "1.3.1" }, { "audience": "designers-developers-and-maintainers", @@ -69,10 +69,10 @@ "last_reviewed": "2026-08-17", "path": "documents/design/ACCESSIBILITY_SPECIFICATION.md", "phase_id": null, - "sha256": "aabae514afafe1186fa69f04fadd35423356cfe8d02922531e3713c7567b88d8", + "sha256": "c9224cc17caca417a5eb0d363b7b472e250890edef028b01edc7d58cec753aa3", "status": "active", "title": "MailStack Accessibility Specification", - "version": "1.3.0-rc.5" + "version": "1.3.1" }, { "audience": "designers-developers-and-maintainers", @@ -81,10 +81,10 @@ "last_reviewed": "2026-08-17", "path": "documents/design/COMPONENT_MATRIX.md", "phase_id": null, - "sha256": "b38667400ae202d75e2b95dd81bbdde8907b263cb8718eaa205c50878d91480f", + "sha256": "957a04577dc899fd9f457b59e029a1b4d50b6ab5c6089f6a415339cd20cbe5e5", "status": "active", "title": "MailStack UI Component Matrix", - "version": "1.3.0-rc.5" + "version": "1.3.1" }, { "audience": "designers-developers-and-maintainers", @@ -93,10 +93,10 @@ "last_reviewed": "2026-08-17", "path": "documents/design/FUTURE_UI_ROADMAP.md", "phase_id": null, - "sha256": "7523c559d870025b1d1a9fd9127e603130bb96e87acfd2126fb6ee1d6025d1d6", + "sha256": "078989742c6aab70840e48ccf887c472f538079e48a73811eb48b0506d9a413a", "status": "active", "title": "MailStack Future UI Roadmap", - "version": "1.3.0-rc.5" + "version": "1.3.1" }, { "audience": "designers-developers-and-maintainers", @@ -105,10 +105,10 @@ "last_reviewed": "2026-08-17", "path": "documents/design/IMPLEMENTATION_STATUS.md", "phase_id": null, - "sha256": "a0e25cf557e479a375e402b00e2518377e60572482d023031574da528d1775e4", + "sha256": "ad807f84993961013133f0c8e136c4fc5d636fd6051e352e6fb7d01b43e1be9f", "status": "active", "title": "MailStack UI Implementation Status", - "version": "1.3.0-rc.5" + "version": "1.3.1" }, { "audience": "designers-developers-and-maintainers", @@ -117,10 +117,10 @@ "last_reviewed": "2026-08-17", "path": "documents/design/RESPONSIVE_SPECIFICATION.md", "phase_id": null, - "sha256": "fb5168baac39e08c40c7ba30ad0c675e49c7a7be57fe1c6df0966f048bafa992", + "sha256": "b540f41863ec20c34871d1b0abf39517a40a2ec1a183128d8ce321e9d7a8867a", "status": "active", "title": "MailStack Responsive Specification", - "version": "1.3.0-rc.5" + "version": "1.3.1" }, { "audience": "designers-developers-and-maintainers", @@ -129,10 +129,10 @@ "last_reviewed": "2026-08-17", "path": "documents/design/SCREEN_CATALOG.md", "phase_id": null, - "sha256": "54c7e6d838074cefc19b0626f84fcc3f965de42de8a04e7c6ac185a149d73b5c", + "sha256": "0f45bcdddea7a40c6bcaa753bcc5b8d7c49a2d66f383003c87244ae865c007a6", "status": "active", "title": "MailStack UI Screen Catalog", - "version": "1.3.0-rc.5" + "version": "1.3.1" }, { "audience": "designers-developers-and-maintainers", @@ -141,10 +141,10 @@ "last_reviewed": "2026-08-17", "path": "documents/design/UI_FOUNDATION.md", "phase_id": null, - "sha256": "c98a9d3d7f982a604392bebc4ebedb3bdbf92ac691638424032ac4cc4cb5bc48", + "sha256": "38b97a9c193ad8a7a5b21d031535afa72b3e5fc3b9f6772abe37c1379dc0c459", "status": "active", "title": "MailStack UI Foundation", - "version": "1.3.0-rc.5" + "version": "1.3.1" }, { "audience": "users-operators-and-maintainers", @@ -153,10 +153,10 @@ "last_reviewed": "2026-08-17", "path": "documents/phases/PHASE-000-BASELINE.md", "phase_id": "PHASE-000", - "sha256": "9320a939456c39acf5a59c85d008010d205e3ebd6fc83ca07e0201b28e53693d", + "sha256": "9c04cd5594cd07cc487f47fe7e6fd751775ba202232b84db3385e959f1ceba57", "status": "active", "title": "Documentation and Feature Baseline", - "version": "1.3.0-rc.5" + "version": "1.3.1" }, { "audience": "users-operators-designers-and-maintainers", @@ -165,10 +165,10 @@ "last_reviewed": "2026-08-17", "path": "documents/phases/PHASE-001-UI-DESIGN-INTAKE-BASELINE.md", "phase_id": "PHASE-001", - "sha256": "4e86d967000db5a21e870d762b220236de6139a67eedb25c4ea259328c891310", + "sha256": "b30ca3041469b56b1c3256cab99d10b12f3d00241043cb449d466ecbde352154", "status": "active", "title": "UI Design Intake Baseline", - "version": "1.3.0-rc.5" + "version": "1.3.1" }, { "audience": "users-operators-designers-and-maintainers", @@ -177,10 +177,10 @@ "last_reviewed": "2026-08-17", "path": "documents/phases/PHASE-002-SHARED-UI-FOUNDATION-AND-APPLICATION-SHELL.md", "phase_id": "PHASE-002", - "sha256": "fff6f07ae9cb661d6689ad679d9c2be1ffa97229990b5690142f6cf9ee39d2b8", + "sha256": "a1e2f224910ad0eea4d43f87f0734f3ecbd394e804497b7e6cc3c8a5ec2567dc", "status": "active", "title": "Shared UI Foundation and Application Shell", - "version": "1.3.0-rc.5" + "version": "1.3.1" }, { "audience": "users-operators-and-maintainers", @@ -189,10 +189,10 @@ "last_reviewed": "2026-08-17", "path": "documents/phases/PHASE-003-INSTALLATION-RECOVERY-AND-INBOUND-DELIVERY-RELIABILITY.md", "phase_id": "PHASE-003", - "sha256": "e656dfe0d583d318fe803c89988cc636f8f60673f12e88ab865c90193027ec7c", + "sha256": "2b63990b712eeab3c7e28f948385a3485667a4397a4c9f0b4a731e72165393df", "status": "active", "title": "Installation, Recovery and Inbound Delivery Reliability", - "version": "1.3.0-rc.5" + "version": "1.3.1" }, { "audience": "users-operators-and-maintainers", @@ -201,14 +201,14 @@ "last_reviewed": "2026-08-17", "path": "documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md", "phase_id": "PHASE-004", - "sha256": "b3cef810cf4dfa826f079546b795c27dcb9ab89068f40bada5b048726e7eefb4", + "sha256": "964c57e99da6bc7b63e5ee577129cf7aa45770e9bdc4c70e630bfac502dba828", "status": "active", "title": "Release, Upgrade and Operational Reliability", - "version": "1.3.0-rc.5" + "version": "1.3.1" } ], "project": "MailStack", - "release_version": "1.3.0-rc.5", + "release_version": "1.3.1", "schema_version": 1, "scope": "Managed user, administrator, how-to, policy, baseline, and phase Markdown documents.", "summary": { diff --git a/documents/DOCUMENTATION_POLICY.md b/documents/DOCUMENTATION_POLICY.md index 57d9f52..563aa16 100644 --- a/documents/DOCUMENTATION_POLICY.md +++ b/documents/DOCUMENTATION_POLICY.md @@ -4,7 +4,7 @@ title: Documentation Policy document_type: documentation-policy audience: contributors-and-maintainers status: active -version: 1.3.0-rc.5 +version: 1.3.1 last_reviewed: 2026-08-17 --- diff --git a/documents/HOW_TO_USE.md b/documents/HOW_TO_USE.md index 6e4106c..2e12f27 100644 --- a/documents/HOW_TO_USE.md +++ b/documents/HOW_TO_USE.md @@ -4,7 +4,7 @@ title: How to Use MailStack document_type: how-to audience: mailbox-users-and-administrators status: active -version: 1.3.0-rc.5 +version: 1.3.1 last_reviewed: 2026-08-17 --- diff --git a/documents/README.md b/documents/README.md index 0f5e52f..542b751 100644 --- a/documents/README.md +++ b/documents/README.md @@ -27,23 +27,23 @@ required documentation update. | Document | Type | Audience | Status | Version | |---|---|---|---|---| -| [MailStack Administrator Guide](ADMIN_GUIDE.md) | Admin Guide | mailstack-administrators | active | 1.3.0-rc.5 | -| [MailStack Baseline](BASELINE.md) | Baseline | maintainers-and-operators | active | 1.3.0-rc.5 | -| [MailStack Accessibility Specification](design/ACCESSIBILITY_SPECIFICATION.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.5 | -| [MailStack Future UI Roadmap](design/FUTURE_UI_ROADMAP.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.5 | -| [MailStack Responsive Specification](design/RESPONSIVE_SPECIFICATION.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.5 | -| [MailStack UI Component Matrix](design/COMPONENT_MATRIX.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.5 | -| [MailStack UI Foundation](design/UI_FOUNDATION.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.5 | -| [MailStack UI Implementation Status](design/IMPLEMENTATION_STATUS.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.5 | -| [MailStack UI Screen Catalog](design/SCREEN_CATALOG.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.5 | -| [Documentation Policy](DOCUMENTATION_POLICY.md) | Documentation Policy | contributors-and-maintainers | active | 1.3.0-rc.5 | -| [How to Use MailStack](HOW_TO_USE.md) | How To | mailbox-users-and-administrators | active | 1.3.0-rc.5 | -| [Documentation and Feature Baseline](phases/PHASE-000-BASELINE.md) | Phase | users-operators-and-maintainers | active | 1.3.0-rc.5 | -| [Installation, Recovery and Inbound Delivery Reliability](phases/PHASE-003-INSTALLATION-RECOVERY-AND-INBOUND-DELIVERY-RELIABILITY.md) | Phase | users-operators-and-maintainers | active | 1.3.0-rc.5 | -| [Release, Upgrade and Operational Reliability](phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md) | Phase | users-operators-and-maintainers | active | 1.3.0-rc.5 | -| [Shared UI Foundation and Application Shell](phases/PHASE-002-SHARED-UI-FOUNDATION-AND-APPLICATION-SHELL.md) | Phase | users-operators-designers-and-maintainers | active | 1.3.0-rc.5 | -| [UI Design Intake Baseline](phases/PHASE-001-UI-DESIGN-INTAKE-BASELINE.md) | Phase | users-operators-designers-and-maintainers | active | 1.3.0-rc.5 | -| [MailStack User Manual](USER_MANUAL.md) | User Manual | mailbox-users | active | 1.3.0-rc.5 | +| [MailStack Administrator Guide](ADMIN_GUIDE.md) | Admin Guide | mailstack-administrators | active | 1.3.1 | +| [MailStack Baseline](BASELINE.md) | Baseline | maintainers-and-operators | active | 1.3.1 | +| [MailStack Accessibility Specification](design/ACCESSIBILITY_SPECIFICATION.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.1 | +| [MailStack Future UI Roadmap](design/FUTURE_UI_ROADMAP.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.1 | +| [MailStack Responsive Specification](design/RESPONSIVE_SPECIFICATION.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.1 | +| [MailStack UI Component Matrix](design/COMPONENT_MATRIX.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.1 | +| [MailStack UI Foundation](design/UI_FOUNDATION.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.1 | +| [MailStack UI Implementation Status](design/IMPLEMENTATION_STATUS.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.1 | +| [MailStack UI Screen Catalog](design/SCREEN_CATALOG.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.1 | +| [Documentation Policy](DOCUMENTATION_POLICY.md) | Documentation Policy | contributors-and-maintainers | active | 1.3.1 | +| [How to Use MailStack](HOW_TO_USE.md) | How To | mailbox-users-and-administrators | active | 1.3.1 | +| [Documentation and Feature Baseline](phases/PHASE-000-BASELINE.md) | Phase | users-operators-and-maintainers | active | 1.3.1 | +| [Installation, Recovery and Inbound Delivery Reliability](phases/PHASE-003-INSTALLATION-RECOVERY-AND-INBOUND-DELIVERY-RELIABILITY.md) | Phase | users-operators-and-maintainers | active | 1.3.1 | +| [Release, Upgrade and Operational Reliability](phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md) | Phase | users-operators-and-maintainers | active | 1.3.1 | +| [Shared UI Foundation and Application Shell](phases/PHASE-002-SHARED-UI-FOUNDATION-AND-APPLICATION-SHELL.md) | Phase | users-operators-designers-and-maintainers | active | 1.3.1 | +| [UI Design Intake Baseline](phases/PHASE-001-UI-DESIGN-INTAKE-BASELINE.md) | Phase | users-operators-designers-and-maintainers | active | 1.3.1 | +| [MailStack User Manual](USER_MANUAL.md) | User Manual | mailbox-users | active | 1.3.1 | ## Scope boundary diff --git a/documents/USER_MANUAL.md b/documents/USER_MANUAL.md index f6180f5..6e904fc 100644 --- a/documents/USER_MANUAL.md +++ b/documents/USER_MANUAL.md @@ -4,7 +4,7 @@ title: MailStack User Manual document_type: user-manual audience: mailbox-users status: active -version: 1.3.0-rc.5 +version: 1.3.1 last_reviewed: 2026-08-17 --- diff --git a/documents/design/ACCESSIBILITY_SPECIFICATION.md b/documents/design/ACCESSIBILITY_SPECIFICATION.md index 0426bd1..7c70998 100644 --- a/documents/design/ACCESSIBILITY_SPECIFICATION.md +++ b/documents/design/ACCESSIBILITY_SPECIFICATION.md @@ -4,7 +4,7 @@ title: MailStack Accessibility Specification document_type: design-reference audience: designers-developers-and-maintainers status: active -version: 1.3.0-rc.5 +version: 1.3.1 last_reviewed: 2026-08-17 --- diff --git a/documents/design/COMPONENT_MATRIX.md b/documents/design/COMPONENT_MATRIX.md index 1704ad3..6fdfe6e 100644 --- a/documents/design/COMPONENT_MATRIX.md +++ b/documents/design/COMPONENT_MATRIX.md @@ -4,7 +4,7 @@ title: MailStack UI Component Matrix document_type: design-reference audience: designers-developers-and-maintainers status: active -version: 1.3.0-rc.5 +version: 1.3.1 last_reviewed: 2026-08-17 --- diff --git a/documents/design/FUTURE_UI_ROADMAP.md b/documents/design/FUTURE_UI_ROADMAP.md index 3946d17..98eb567 100644 --- a/documents/design/FUTURE_UI_ROADMAP.md +++ b/documents/design/FUTURE_UI_ROADMAP.md @@ -4,7 +4,7 @@ title: MailStack Future UI Roadmap document_type: design-reference audience: designers-developers-and-maintainers status: active -version: 1.3.0-rc.5 +version: 1.3.1 last_reviewed: 2026-08-17 --- diff --git a/documents/design/IMPLEMENTATION_STATUS.md b/documents/design/IMPLEMENTATION_STATUS.md index 1e54a22..e685c78 100644 --- a/documents/design/IMPLEMENTATION_STATUS.md +++ b/documents/design/IMPLEMENTATION_STATUS.md @@ -4,7 +4,7 @@ title: MailStack UI Implementation Status document_type: design-reference audience: designers-developers-and-maintainers status: active -version: 1.3.0-rc.5 +version: 1.3.1 last_reviewed: 2026-08-17 --- diff --git a/documents/design/RESPONSIVE_SPECIFICATION.md b/documents/design/RESPONSIVE_SPECIFICATION.md index 437a204..b1f9b87 100644 --- a/documents/design/RESPONSIVE_SPECIFICATION.md +++ b/documents/design/RESPONSIVE_SPECIFICATION.md @@ -4,7 +4,7 @@ title: MailStack Responsive Specification document_type: design-reference audience: designers-developers-and-maintainers status: active -version: 1.3.0-rc.5 +version: 1.3.1 last_reviewed: 2026-08-17 --- diff --git a/documents/design/SCREEN_CATALOG.md b/documents/design/SCREEN_CATALOG.md index 65cbbb9..90af447 100644 --- a/documents/design/SCREEN_CATALOG.md +++ b/documents/design/SCREEN_CATALOG.md @@ -4,7 +4,7 @@ title: MailStack UI Screen Catalog document_type: design-reference audience: designers-developers-and-maintainers status: active -version: 1.3.0-rc.5 +version: 1.3.1 last_reviewed: 2026-08-17 --- diff --git a/documents/design/UI_FOUNDATION.md b/documents/design/UI_FOUNDATION.md index c9711e0..2e3df92 100644 --- a/documents/design/UI_FOUNDATION.md +++ b/documents/design/UI_FOUNDATION.md @@ -4,7 +4,7 @@ title: MailStack UI Foundation document_type: design-reference audience: designers-developers-and-maintainers status: active -version: 1.3.0-rc.5 +version: 1.3.1 last_reviewed: 2026-08-17 --- diff --git a/documents/phases/PHASE-000-BASELINE.md b/documents/phases/PHASE-000-BASELINE.md index b9f00ff..5795960 100644 --- a/documents/phases/PHASE-000-BASELINE.md +++ b/documents/phases/PHASE-000-BASELINE.md @@ -4,7 +4,7 @@ title: Documentation and Feature Baseline document_type: phase audience: users-operators-and-maintainers status: active -version: 1.3.0-rc.5 +version: 1.3.1 last_reviewed: 2026-08-17 phase_id: PHASE-000 --- diff --git a/documents/phases/PHASE-001-UI-DESIGN-INTAKE-BASELINE.md b/documents/phases/PHASE-001-UI-DESIGN-INTAKE-BASELINE.md index 51de382..67ab415 100644 --- a/documents/phases/PHASE-001-UI-DESIGN-INTAKE-BASELINE.md +++ b/documents/phases/PHASE-001-UI-DESIGN-INTAKE-BASELINE.md @@ -4,7 +4,7 @@ title: UI Design Intake Baseline document_type: phase audience: users-operators-designers-and-maintainers status: active -version: 1.3.0-rc.5 +version: 1.3.1 last_reviewed: 2026-08-17 phase_id: PHASE-001 --- diff --git a/documents/phases/PHASE-002-SHARED-UI-FOUNDATION-AND-APPLICATION-SHELL.md b/documents/phases/PHASE-002-SHARED-UI-FOUNDATION-AND-APPLICATION-SHELL.md index 1898ff1..6d967ee 100644 --- a/documents/phases/PHASE-002-SHARED-UI-FOUNDATION-AND-APPLICATION-SHELL.md +++ b/documents/phases/PHASE-002-SHARED-UI-FOUNDATION-AND-APPLICATION-SHELL.md @@ -4,7 +4,7 @@ title: Shared UI Foundation and Application Shell document_type: phase audience: users-operators-designers-and-maintainers status: active -version: 1.3.0-rc.5 +version: 1.3.1 last_reviewed: 2026-08-17 phase_id: PHASE-002 --- diff --git a/documents/phases/PHASE-003-INSTALLATION-RECOVERY-AND-INBOUND-DELIVERY-RELIABILITY.md b/documents/phases/PHASE-003-INSTALLATION-RECOVERY-AND-INBOUND-DELIVERY-RELIABILITY.md index ca2a24a..b10ba27 100644 --- a/documents/phases/PHASE-003-INSTALLATION-RECOVERY-AND-INBOUND-DELIVERY-RELIABILITY.md +++ b/documents/phases/PHASE-003-INSTALLATION-RECOVERY-AND-INBOUND-DELIVERY-RELIABILITY.md @@ -4,7 +4,7 @@ title: Installation, Recovery and Inbound Delivery Reliability document_type: phase audience: users-operators-and-maintainers status: active -version: 1.3.0-rc.5 +version: 1.3.1 last_reviewed: 2026-08-17 phase_id: PHASE-003 --- diff --git a/documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md b/documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md index 2c2a6f3..a04212c 100644 --- a/documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md +++ b/documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md @@ -4,7 +4,7 @@ title: Release, Upgrade and Operational Reliability document_type: phase audience: users-operators-and-maintainers status: active -version: 1.3.0-rc.5 +version: 1.3.1 last_reviewed: 2026-08-17 phase_id: PHASE-004 --- @@ -68,7 +68,8 @@ rollback tool that fails closed around archive integrity, migration risk, and re Treat `mailstack-1.3.0-rc.4-source.zip`, SHA-256 `58f06adea7c813e9861799d20e392441367bf64f6513d6e0634455d2011d4eac`, tag `v1.3.0-rc.4`, and commit `896dbcc2ed1f38d9c618bf0b712efe5923f92e56` as the immutable official source baseline. The working -repository version remains `1.3.0-rc.5`; do not retag or rewrite RC4. For future releases, merge the +source-baseline version is `1.3.1`; do not retag or rewrite RC4, and do not publish `v1.3.1` until the +new main/CI and remaining release-acceptance gates pass. For future releases, merge the release commit to `main`, require successful `main` CI, create the matching `v` tag at the current `main` head, and push the tag. The release workflow then builds/verifies the deterministic source archive and publishes the GitHub Release automatically. `workflow_dispatch` validates and @@ -113,6 +114,21 @@ contracts, documentation/design/inventory gates, dependency/security/application audit, and deterministic release build/verification. Its automated tests are non-destructive and do not claim a real host upgrade; PHASE-004D is the live acceptance boundary. +## PHASE-004C CI correction and 1.3.1 baseline mark + +GitHub Actions run `32097491341` tested exact PHASE-004C commit +`47e62bb6c0acd0216fb261f47f85959655b489e0`. All gates through the dependency vulnerability audit +passed, including the focused upgrade/archive/rollback contracts. Ruff then stopped the workflow on +four E501 line-length findings and one SIM102 nested-`if` finding in +`mailbox-app/scripts/verify_upgrade_archive.py`; downstream runtime and release gates were skipped. +The correction changes only formatting/control-expression layout needed for Ruff compliance and does +not change upgrade/archive/migration/rollback behavior. + +At the owner's explicit request, the corrected source is version-marked `1.3.1` and the deterministic +build/release documentation is synchronized to that identity. This is a source-baseline freeze, not +a claim that the stable-looking version has passed the still-outstanding live PHASE-004D/operational +acceptance or has been published as a GitHub Release. + ## Documentation impact PHASE-004A updated the root/application changelogs, build/release examples, README status wording, diff --git a/mailbox-app/CHANGELOG.md b/mailbox-app/CHANGELOG.md index 027fa9d..c8c73f4 100644 --- a/mailbox-app/CHANGELOG.md +++ b/mailbox-app/CHANGELOG.md @@ -1,9 +1,10 @@ # Changelog -## 1.3.0-rc.5 — Unreleased +## 1.3.1 — Unpublished source baseline - PHASE-004C controlled existing-server upgrade tooling now verifies deterministic release ZIP/SHA assets, creates a coordinated data backup and source rollback snapshot before mutation, stages application/public-site source, converges Python dependencies, gates migrations explicitly, preserves mail-facing services during the source mutation window, and performs post-upgrade verification. - Added a reviewed source/runtime rollback command that verifies snapshot checksums and refuses implicit database/Maildir restoration or unacknowledged forward-schema rollback. +- Corrected Ruff-only formatting/style findings in the PHASE-004C archive verifier; no upgrade/runtime semantics changed. ## 1.3.0-rc.5 — Forensic/docs baseline finalization (unreleased) diff --git a/mailbox-app/pyproject.toml b/mailbox-app/pyproject.toml index b3b3266..d5660df 100644 --- a/mailbox-app/pyproject.toml +++ b/mailbox-app/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "mailstack" -version = "1.3.0rc5" +version = "1.3.1" description = "MailStack self-hosted receive-only mail server and shared team inbox" authors = [{ name = "Vib Tools" }] maintainers = [{ name = "Vib Tools" }] diff --git a/mailbox-app/scripts/verify_upgrade_archive.py b/mailbox-app/scripts/verify_upgrade_archive.py index bbb27c7..a9ba55a 100644 --- a/mailbox-app/scripts/verify_upgrade_archive.py +++ b/mailbox-app/scripts/verify_upgrade_archive.py @@ -101,7 +101,10 @@ def read_checksum(checksum_path: Path, archive: Path) -> str: return expected.lower() -def verify_archive(archive_path: Path, checksum_path: Path) -> tuple[zipfile.ZipFile, str, str, dict[str, str]]: +def verify_archive( + archive_path: Path, + checksum_path: Path, +) -> tuple[zipfile.ZipFile, str, str, dict[str, str]]: expected = read_checksum(checksum_path, archive_path) actual = sha256(archive_path) if actual != expected: @@ -221,13 +224,20 @@ def migration_map_from_archive(archive: zipfile.ZipFile, prefix: str) -> dict[st continue relative = name.removeprefix(marker) pure = PurePosixPath(relative) - if len(pure.parts) == 4 and pure.parts[0] == "apps" and pure.parts[2] == "migrations": - if re.fullmatch(r"[0-9].*\.py", pure.name): - result[relative] = sha256_bytes(archive.read(name)) + if ( + len(pure.parts) == 4 + and pure.parts[0] == "apps" + and pure.parts[2] == "migrations" + and re.fullmatch(r"[0-9].*\.py", pure.name) + ): + result[relative] = sha256_bytes(archive.read(name)) return result -def compare_migrations(current: dict[str, str], target: dict[str, str]) -> tuple[list[str], list[str], list[str]]: +def compare_migrations( + current: dict[str, str], + target: dict[str, str], +) -> tuple[list[str], list[str], list[str]]: added = sorted(set(target) - set(current)) removed = sorted(set(current) - set(target)) modified = sorted(path for path in set(current) & set(target) if current[path] != target[path]) @@ -275,11 +285,13 @@ def main() -> int: package_version = str(pyproject["project"]["version"]) if package_version != package_from_release(target_version): raise UpgradeArchiveError( - f"target package version mismatch: VERSION={target_version}, project.version={package_version}" + "target package version mismatch: " + f"VERSION={target_version}, project.version={package_version}" ) if target_order <= current_order: raise UpgradeArchiveError( - f"target version must be newer than current version: current={current_version}, target={target_version}" + "target version must be newer than current version: " + f"current={current_version}, target={target_version}" ) current_migrations = migration_map_from_current(app_root) diff --git a/scripts/forensic_audit.py b/scripts/forensic_audit.py index ab73447..97dec9f 100644 --- a/scripts/forensic_audit.py +++ b/scripts/forensic_audit.py @@ -53,6 +53,7 @@ "docs/SECURITY_REVIEW.md", "docs/PERFORMANCE_REVIEW.md", "docs/RELEASE_NOTES_1.3.0.md", + "docs/RELEASE_NOTES_1.3.1.md", "documents/README.md", "documents/USER_MANUAL.md", "documents/HOW_TO_USE.md", diff --git a/scripts/test_release_workflow.py b/scripts/test_release_workflow.py index e1b73de..91f72e3 100644 --- a/scripts/test_release_workflow.py +++ b/scripts/test_release_workflow.py @@ -188,6 +188,7 @@ def test_workflow_contract() -> None: "--latest", 'dist/*.zip', 'dist/*.sha256', + 'docs/RELEASE_NOTES_1.3.1.md', ) for marker in required: assert marker in text, marker