Build releases from clean detached checkout

This commit is contained in:
SimpleTest 2026-08-13 08:05:34 +03:00
parent 3c8c0b3cf7
commit d79e4e436b
4 changed files with 232 additions and 0 deletions

View File

@ -1352,6 +1352,25 @@ backup destination remains an operational requirement.
## Rollback boundary ## Rollback boundary
The release image is immutable and migrations run as a separate one-shot role. The release image is immutable and migrations run as a separate one-shot role.
If the main workspace contains intentional tracked edits that must not enter
the release (for example local agent instructions), run the same workflow from
an exact clean detached worktree instead of stashing, discarding, or silently
including those edits:
```bash
./scripts/production-release-clean.sh plan whoneedhelp
WNH_PRODUCTION_RELEASE_CONFIRM='whoneedhelp.com:EXACT_COMMIT' \
./scripts/production-release-clean.sh apply whoneedhelp
```
The wrapper selects the current `HEAD`, creates a task-owned detached worktree,
runs `production-release.sh` there with the original immutable artifact root,
and removes the worktree on success, failure, or interrupt. The underlying
release still requires the exact production confirmation and any applicable
forward-only migration confirmation. It does not stash, reset, stage, commit,
or copy changes from the main workspace.
Before a schema rollout, create and restore-test a current backup. Application Before a schema rollout, create and restore-test a current backup. Application
rollback and database migration rollback are separate decisions: do not run an rollback and database migration rollback are separate decisions: do not run an
Ecto down migration merely because an image is rolled back. Inspect the exact Ecto down migration merely because an image is rolled back. Inspect the exact

View File

@ -0,0 +1,88 @@
#!/usr/bin/env bash
set -euo pipefail
umask 077
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
action=${1:-plan}
ssh_target=${2:-whoneedhelp}
case "$action" in
plan | apply) ;;
*)
echo "Usage: $0 [plan|apply] [SSH_TARGET]" >&2
exit 2
;;
esac
for command in git mktemp realpath; do
command -v "$command" >/dev/null 2>&1 || {
echo "Required command is unavailable: $command" >&2
exit 2
}
done
candidate=$(git -C "$ROOT" rev-parse --verify HEAD)
artifact_root=${WNH_PRODUCTION_RELEASE_ARTIFACT_ROOT:-"$ROOT/output/releases"}
case "$artifact_root" in
/*) ;;
*)
echo "WNH_PRODUCTION_RELEASE_ARTIFACT_ROOT must be an absolute path." >&2
exit 2
;;
esac
mkdir -p "$artifact_root"
artifact_root=$(realpath --canonicalize-existing "$artifact_root")
worktree_root=${WNH_PRODUCTION_RELEASE_WORKTREE_ROOT:-"$ROOT/output/release-worktrees"}
case "$worktree_root" in
/*) ;;
*)
echo "WNH_PRODUCTION_RELEASE_WORKTREE_ROOT must be an absolute path." >&2
exit 2
;;
esac
mkdir -p "$worktree_root"
worktree_root=$(realpath --canonicalize-existing "$worktree_root")
chmod 700 "$worktree_root"
checkout=$(mktemp -d "$worktree_root/${candidate}.XXXXXX")
rmdir "$checkout"
worktree_added=false
cleanup() {
local status=$?
trap - EXIT HUP INT TERM
if [[ "$worktree_added" == true ]]; then
git -C "$ROOT" worktree remove --force "$checkout" >/dev/null 2>&1 || true
fi
if [[ -d "$checkout" ]]; then
rmdir "$checkout" >/dev/null 2>&1 || true
fi
exit "$status"
}
trap cleanup EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
worktree_added=true
git -C "$ROOT" worktree add --quiet --detach "$checkout" "$candidate"
[[ -z "$(git -C "$checkout" status --porcelain --untracked-files=no)" ]] || {
echo "The detached release checkout is unexpectedly dirty." >&2
exit 2
}
[[ "$(git -C "$checkout" rev-parse --verify HEAD)" == "$candidate" ]] || {
echo "The detached release checkout does not match the selected commit." >&2
exit 2
}
printf 'Release candidate: %s\n' "$candidate"
printf 'Clean detached checkout: %s\n' "$checkout"
printf 'Artifact root: %s\n' "$artifact_root"
(
cd "$checkout"
WNH_PRODUCTION_RELEASE_ARTIFACT_ROOT="$artifact_root" \
./scripts/production-release.sh "$action" "$ssh_target"
)

View File

@ -1557,6 +1557,7 @@ docker run --rm \
--workdir /src \ --workdir /src \
"$PYTHON_IMAGE" python test/scripts/install_production_external_monitor_test.py "$PYTHON_IMAGE" python test/scripts/install_production_external_monitor_test.py
python3 test/scripts/production_release_artifact_root_test.py python3 test/scripts/production_release_artifact_root_test.py
python3 test/scripts/production_release_clean_test.py
docker run --rm \ docker run --rm \
--volume "$ROOT/ops/external-boundaries/mock_server.py:/src/mock_server.py:ro" \ --volume "$ROOT/ops/external-boundaries/mock_server.py:/src/mock_server.py:ro" \
"$PYTHON_IMAGE" python -c \ "$PYTHON_IMAGE" python -c \

View File

@ -0,0 +1,124 @@
import os
import shutil
import subprocess
import tempfile
import textwrap
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
WRAPPER = ROOT / "scripts" / "production-release-clean.sh"
class ProductionReleaseCleanTest(unittest.TestCase):
def setUp(self):
self.tempdir = tempfile.TemporaryDirectory()
self.base = Path(self.tempdir.name)
self.project = self.base / "project"
self.scripts = self.project / "scripts"
self.scripts.mkdir(parents=True)
shutil.copy2(WRAPPER, self.scripts / WRAPPER.name)
(self.scripts / WRAPPER.name).chmod(0o755)
self.capture = self.base / "capture.txt"
self.artifacts = self.base / "artifacts"
self.worktrees = self.base / "worktrees"
self.write_inner_script(exit_code=0)
(self.project / "README.md").write_text("committed\n", encoding="utf-8")
self.run_command(["git", "init", "--quiet"])
self.run_command(
["git", "config", "user.email", "release-test@example.invalid"]
)
self.run_command(["git", "config", "user.name", "Release test"])
self.run_command(["git", "add", "."])
self.run_command(["git", "commit", "--quiet", "-m", "fixture"])
self.commit = self.run_command(["git", "rev-parse", "HEAD"]).stdout.strip()
def tearDown(self):
self.tempdir.cleanup()
def run_command(self, command, *, check=True, env=None):
return subprocess.run(
command,
cwd=self.project,
env=env,
capture_output=True,
text=True,
check=check,
)
def write_inner_script(self, *, exit_code):
inner = self.scripts / "production-release.sh"
inner.write_text(
textwrap.dedent(
f"""\
#!/usr/bin/env bash
set -euo pipefail
printf '%s\\n' \\
"pwd=$PWD" \\
"status=$(git status --porcelain --untracked-files=no)" \\
"args=$*" \\
"commit=$(git rev-parse HEAD)" \\
"artifact_root=${{WNH_PRODUCTION_RELEASE_ARTIFACT_ROOT}}" \\
>"${{WNH_TEST_CAPTURE}}"
exit {exit_code}
"""
),
encoding="utf-8",
)
inner.chmod(0o755)
def environment(self):
env = os.environ.copy()
env.update(
{
"WNH_TEST_CAPTURE": str(self.capture),
"WNH_PRODUCTION_RELEASE_ARTIFACT_ROOT": str(self.artifacts),
"WNH_PRODUCTION_RELEASE_WORKTREE_ROOT": str(self.worktrees),
}
)
return env
def assert_release_worktree_removed(self):
self.assertEqual(list(self.worktrees.iterdir()), [])
worktree_list = self.run_command(
["git", "worktree", "list", "--porcelain"]
).stdout
self.assertNotIn(str(self.worktrees), worktree_list)
def test_runs_exact_commit_from_clean_checkout_without_touching_dirty_file(self):
dirty = self.project / "README.md"
dirty.write_text("user-owned change\n", encoding="utf-8")
result = self.run_command(
[str(self.scripts / WRAPPER.name), "plan", "production-alias"],
env=self.environment(),
)
captured = self.capture.read_text(encoding="utf-8")
self.assertIn("status=\n", captured)
self.assertIn("args=plan production-alias", captured)
self.assertIn(f"commit={self.commit}", captured)
self.assertIn(f"artifact_root={self.artifacts}", captured)
self.assertIn(f"Release candidate: {self.commit}", result.stdout)
self.assertEqual(dirty.read_text(encoding="utf-8"), "user-owned change\n")
self.assert_release_worktree_removed()
def test_failed_inner_release_preserves_exit_status_and_removes_worktree(self):
self.write_inner_script(exit_code=23)
self.run_command(["git", "add", "scripts/production-release.sh"])
self.run_command(["git", "commit", "--quiet", "-m", "failing fixture"])
result = self.run_command(
[str(self.scripts / WRAPPER.name), "apply", "production-alias"],
env=self.environment(),
check=False,
)
self.assertEqual(result.returncode, 23)
self.assert_release_worktree_removed()
if __name__ == "__main__":
unittest.main()