From 9aa371105fd71f38fbdff3cabe1b745b8bfc81fa Mon Sep 17 00:00:00 2001 From: SimpleTest Date: Thu, 13 Aug 2026 08:18:10 +0300 Subject: [PATCH] Prepare production artifacts before release --- docs/operations.md | 17 ++++++++ scripts/production-release-clean.sh | 4 +- scripts/production-release.sh | 39 +++++++++++-------- test/scripts/production_release_clean_test.py | 16 ++++++++ 4 files changed, 58 insertions(+), 18 deletions(-) diff --git a/docs/operations.md b/docs/operations.md index 6258b50..7836be3 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -1272,6 +1272,21 @@ allows only the absent Play App Signing certificate; the stricter publishing Android through Google Play. The plan neither uploads a bundle nor creates a backup. +Prepare and verify the immutable Git bundle and `linux/amd64` image archive on +the development workstation before authorising any production mutation: + +```bash +./scripts/production-release-clean.sh prepare whoneedhelp +``` + +`prepare` repeats the read-only production and environment checks, reads the +production image-build inputs into a mode-`0600` temporary file, and then +creates or verifies the commit-bound artifacts under `output/releases/`. It +does not upload an artifact, create a production backup, change the remote +checkout, load an image, run a migration, or restart a service. A later +`apply` for the same commit verifies and reuses those exact artifacts instead +of compiling them again. + After reviewing the exact commit printed by the plan, execution additionally requires an explicit per-commit confirmation: @@ -1361,6 +1376,8 @@ including those edits: ```bash ./scripts/production-release-clean.sh plan whoneedhelp +./scripts/production-release-clean.sh prepare whoneedhelp + WNH_PRODUCTION_RELEASE_CONFIRM='whoneedhelp.com:EXACT_COMMIT' \ ./scripts/production-release-clean.sh apply whoneedhelp ``` diff --git a/scripts/production-release-clean.sh b/scripts/production-release-clean.sh index 9cfacb2..c577432 100755 --- a/scripts/production-release-clean.sh +++ b/scripts/production-release-clean.sh @@ -7,9 +7,9 @@ action=${1:-plan} ssh_target=${2:-whoneedhelp} case "$action" in - plan | apply) ;; + plan | prepare | apply) ;; *) - echo "Usage: $0 [plan|apply] [SSH_TARGET]" >&2 + echo "Usage: $0 [plan|prepare|apply] [SSH_TARGET]" >&2 exit 2 ;; esac diff --git a/scripts/production-release.sh b/scripts/production-release.sh index 31b451f..c57777e 100755 --- a/scripts/production-release.sh +++ b/scripts/production-release.sh @@ -8,9 +8,9 @@ remote_root=${WNH_PRODUCTION_REMOTE_ROOT:-/srv/who_need_help-production} expected_domain=${WNH_PRODUCTION_DOMAIN:-whoneedhelp.com} case "$action" in - plan | apply) ;; + plan | prepare | apply) ;; *) - echo "Usage: $0 [plan|apply] [SSH_TARGET]" >&2 + echo "Usage: $0 [plan|prepare|apply] [SSH_TARGET]" >&2 exit 2 ;; esac @@ -106,22 +106,24 @@ if [[ -n "$(git -C "$ROOT" status --porcelain --untracked-files=no)" ]]; then fi confirmation="$expected_domain:$local_commit" -if [[ "${WNH_PRODUCTION_RELEASE_CONFIRM:-}" != "$confirmation" ]]; then - echo "Release execution requires explicit approval in this exact process:" >&2 - echo "WNH_PRODUCTION_RELEASE_CONFIRM=$confirmation $0 apply $ssh_target" >&2 - exit 2 -fi - -if [[ "$migration_policy" == "forward_only" ]]; then - forward_confirmation="$expected_domain:$local_commit:forward-only" - if [[ "${WNH_PRODUCTION_FORWARD_ONLY_CONFIRM:-}" != "$forward_confirmation" ]]; then - echo "This release contains migrations that are not safe for an automatic old-image rollback." >&2 - echo "A failed deployment after migration starts will keep the old application stopped." >&2 - echo "Review the migration and recovery plan, then approve this exact boundary:" >&2 - echo "WNH_PRODUCTION_FORWARD_ONLY_CONFIRM=$forward_confirmation \\" >&2 - echo " WNH_PRODUCTION_RELEASE_CONFIRM=$confirmation $0 apply $ssh_target" >&2 +if [[ "$action" == "apply" ]]; then + if [[ "${WNH_PRODUCTION_RELEASE_CONFIRM:-}" != "$confirmation" ]]; then + echo "Release execution requires explicit approval in this exact process:" >&2 + echo "WNH_PRODUCTION_RELEASE_CONFIRM=$confirmation $0 apply $ssh_target" >&2 exit 2 fi + + if [[ "$migration_policy" == "forward_only" ]]; then + forward_confirmation="$expected_domain:$local_commit:forward-only" + if [[ "${WNH_PRODUCTION_FORWARD_ONLY_CONFIRM:-}" != "$forward_confirmation" ]]; then + echo "This release contains migrations that are not safe for an automatic old-image rollback." >&2 + echo "A failed deployment after migration starts will keep the old application stopped." >&2 + echo "Review the migration and recovery plan, then approve this exact boundary:" >&2 + echo "WNH_PRODUCTION_FORWARD_ONLY_CONFIRM=$forward_confirmation \\" >&2 + echo " WNH_PRODUCTION_RELEASE_CONFIRM=$confirmation $0 apply $ssh_target" >&2 + exit 2 + fi + fi fi "$ROOT/scripts/prepare-production-release.sh" @@ -151,6 +153,11 @@ chmod 600 "$production_env" "$ROOT/scripts/prepare-production-images.sh" "$production_env" cleanup_production_env +if [[ "$action" == "prepare" ]]; then + echo "Production release artifacts are prepared and verified locally; no remote state was changed." + exit 0 +fi + remote_release_dir="$remote_root/output/releases/incoming" remote_bundle="$remote_release_dir/$(basename -- "$bundle")" remote_image_archive="$remote_release_dir/$(basename -- "$image_archive")" diff --git a/test/scripts/production_release_clean_test.py b/test/scripts/production_release_clean_test.py index 2f6b34e..4bdcfd3 100644 --- a/test/scripts/production_release_clean_test.py +++ b/test/scripts/production_release_clean_test.py @@ -119,6 +119,22 @@ class ProductionReleaseCleanTest(unittest.TestCase): self.assertEqual(result.returncode, 23) self.assert_release_worktree_removed() + def test_prepare_is_forwarded_through_the_same_clean_checkout(self): + dirty = self.project / "README.md" + dirty.write_text("user-owned change\n", encoding="utf-8") + + self.run_command( + [str(self.scripts / WRAPPER.name), "prepare", "production-alias"], + env=self.environment(), + ) + + captured = self.capture.read_text(encoding="utf-8") + self.assertIn("status=\n", captured) + self.assertIn("args=prepare production-alias", captured) + self.assertIn(f"commit={self.commit}", captured) + self.assertEqual(dirty.read_text(encoding="utf-8"), "user-owned change\n") + self.assert_release_worktree_removed() + if __name__ == "__main__": unittest.main()