Prepare production artifacts before release

This commit is contained in:
SimpleTest 2026-08-13 08:18:10 +03:00
parent d79e4e436b
commit 9aa371105f
4 changed files with 58 additions and 18 deletions

View File

@ -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
```

View File

@ -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

View File

@ -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")"

View File

@ -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()