Accelerate release qualification with exact-SHA worker

This commit is contained in:
rcourtman
2026-08-12 17:04:50 +01:00
parent a44dc8cfc8
commit 1bb9ba4208
10 changed files with 542 additions and 5 deletions
+1
View File
@@ -308,6 +308,7 @@ jobs:
MOCK_NETWORK_ERROR: "false"
MOCK_RATE_LIMIT: "false"
MOCK_STALE_RELEASE: "false"
PULSE_E2E_DIAGNOSTIC: "1"
run: |
docker compose -f docker-compose.test.yml up -d --wait
+15 -1
View File
@@ -9609,7 +9609,21 @@
],
"coverage_gaps": [],
"candidate_lanes": [],
"work_claims": [],
"work_claims": [
{
"id": "release-v6-2-2-rc-1-lane-l1",
"agent_id": "release-v6.2.2-rc.1",
"summary": "Accelerate exact-SHA release qualification with the remote amd64 builder before v6.2.2-rc.1",
"target_id": "v6-product-lane-expansion",
"claimed_at": "2026-08-12T14:53:53Z",
"heartbeat_at": "2026-08-12T14:55:19Z",
"expires_at": "2026-08-12T18:55:19Z",
"work_item": {
"kind": "lane",
"id": "L1"
}
}
],
"open_decisions": [],
"source_of_truth_file": "docs/release-control/v6/internal/SOURCE_OF_TRUTH.md",
"resolved_decisions": [
@@ -470,7 +470,22 @@ upgrade, update, release, or artifact-selection behavior.
## Extension Points
1. Add or change deployment-type detection, update planning, or apply behavior through `internal/updates/`
2. Add or change release-build metadata injection, Docker build-context allowlists, release artifact assembly, governed promotion metadata resolution, artifact release-line validation, post-install live-runtime claim proof, the canonical version file, operator-facing release packet content, prerelease feedback intake wording, historical published-release integrity backfill, release asset validation status publication, download endpoint checksum/signature header proof, end-to-end install.sh smoke against staged or published release assets, or the canonical in-repo v6 upgrade guide through `scripts/build-release.sh`, `scripts/release_asset_common.sh`, `scripts/backfill-release-assets.sh`, `scripts/release_ldflags.sh`, `scripts/check-workflow-dispatch-inputs.py`, `scripts/release_control/live_runtime_proof.py`, `scripts/release_control/live_runtime_proof_test.py`, `scripts/release_control/mobile_release_gate.py`, `scripts/release_control/render_release_body.py`, `scripts/release_control/resolve_release_promotion.py`, `scripts/release_control/validate_artifact_release_line.py`, `scripts/release_control/record_rc_to_ga_rehearsal.py`, `scripts/release_control/internal/record_rc_to_ga_rehearsal.py`, `scripts/release_control/release_promotion_policy_support.py`, `.dockerignore`, `Dockerfile`, `.github/ISSUE_TEMPLATE/v6_rc_feedback.yml`, `docs/RELEASE_NOTES.md`, `docs/releases/`, `docs/UPGRADE_v6.md`, `docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md`, `docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md`, `docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md`, `scripts/validate-release.sh`, `scripts/validate-published-release.sh`, the operator dispatch helpers `scripts/trigger-release.sh` and `scripts/trigger-release-dry-run.sh`, and the governed release workflows `.github/workflows/backfill-release-assets.yml`, `.github/workflows/create-release.yml`, `.github/workflows/deploy-demo-server.yml`, `.github/workflows/helm-pages.yml`, `.github/workflows/install-sh-smoke.yml`, `.github/workflows/promote-floating-tags.yml`, `.github/workflows/promote-private-pro-runtime.yml`, `.github/workflows/publish-docker.yml`, `.github/workflows/publish-helm-chart.yml`, `.github/workflows/release-convergence.yml`, `.github/workflows/release-dry-run.yml`, `.github/workflows/retry-release-convergence.yml`, `.github/workflows/update-demo-server.yml`, and `.github/workflows/validate-release-assets.yml`
2. Add or change release-build metadata injection, Docker build-context allowlists, release artifact assembly, governed promotion metadata resolution, artifact release-line validation, post-install live-runtime claim proof, the canonical version file, operator-facing release packet content, prerelease feedback intake wording, historical published-release integrity backfill, release asset validation status publication, download endpoint checksum/signature header proof, end-to-end install.sh smoke against staged or published release assets, or the canonical in-repo v6 upgrade guide through `scripts/build-release.sh`, `scripts/release_asset_common.sh`, `scripts/backfill-release-assets.sh`, `scripts/release_ldflags.sh`, `scripts/check-workflow-dispatch-inputs.py`, `scripts/release-preflight-worker.sh`, `scripts/run-release-preflight.sh`, `scripts/release_control/live_runtime_proof.py`, `scripts/release_control/live_runtime_proof_test.py`, `scripts/release_control/mobile_release_gate.py`, `scripts/release_control/render_release_body.py`, `scripts/release_control/resolve_release_promotion.py`, `scripts/release_control/validate_artifact_release_line.py`, `scripts/release_control/record_rc_to_ga_rehearsal.py`, `scripts/release_control/internal/record_rc_to_ga_rehearsal.py`, `scripts/release_control/release_promotion_policy_support.py`, `.dockerignore`, `Dockerfile`, `.github/ISSUE_TEMPLATE/v6_rc_feedback.yml`, `docs/RELEASE_NOTES.md`, `docs/releases/`, `docs/UPGRADE_v6.md`, `docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md`, `docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md`, `docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md`, `scripts/validate-release.sh`, `scripts/validate-published-release.sh`, the operator dispatch helpers `scripts/trigger-release.sh` and `scripts/trigger-release-dry-run.sh`, and the governed release workflows `.github/workflows/backfill-release-assets.yml`, `.github/workflows/create-release.yml`, `.github/workflows/deploy-demo-server.yml`, `.github/workflows/helm-pages.yml`, `.github/workflows/install-sh-smoke.yml`, `.github/workflows/promote-floating-tags.yml`, `.github/workflows/promote-private-pro-runtime.yml`, `.github/workflows/publish-docker.yml`, `.github/workflows/publish-helm-chart.yml`, `.github/workflows/release-convergence.yml`, `.github/workflows/release-dry-run.yml`, `.github/workflows/retry-release-convergence.yml`, `.github/workflows/update-demo-server.yml`, and `.github/workflows/validate-release-assets.yml`
Operators may configure an external Linux amd64 worker for the two trigger
helpers. `scripts/run-release-preflight.sh` must resolve an immutable pushed
commit, stream the worker implementation stored in that commit over SSH,
and select either the rehearsal or release profile.
`scripts/release-preflight-worker.sh` must use a dedicated checkout and
persistent dependency/build caches while running the portable frontend,
backend, image-build, and browser-smoke checks that historically caused
costly hosted-workflow restarts. It must not receive signing keys, package
publication credentials, or any other release authority. An unconfigured
worker remains an operator acceleration choice, not a new canonical release
gate; the typed release gates and the self-contained GitHub release workflow
remain authoritative.
The rehearsal diagnostic spec is opt-in by design, so both the hosted
rehearsal and its worker profile must set `PULSE_E2E_DIAGNOSTIC=1`; invoking
that spec while leaving it skipped is not browser proof.
Normal releases are single-build promotions. The exact pushed SHA must
produce one release candidate with the policy-required native signing lanes
through `.github/workflows/build-release-candidate.yml` while independent
@@ -792,7 +807,7 @@ upgrade, update, release, or artifact-selection behavior.
the helper must wait for the Organization selector to hold the requested org
before a scenario navigates onward, so an interrupted org-list bootstrap
cannot fall back to `default` and mask the scoped UI under test.
6. Add or change governed release-promotion workflow inputs, operator-facing promotion metadata, the canonical version file, prerelease feedback intake prompts, artifact publication lineage enforcement, release note or changelog packet composition, or stable-promotion rehearsal summaries through `.github/workflows/create-release.yml`, `.github/workflows/helm-pages.yml`, `.github/workflows/promote-floating-tags.yml`, `.github/workflows/promote-private-pro-runtime.yml`, `.github/workflows/publish-docker.yml`, `.github/workflows/publish-helm-chart.yml`, `.github/workflows/release-convergence.yml`, `.github/workflows/release-dry-run.yml`, `.github/workflows/retry-release-convergence.yml`, `.github/workflows/update-demo-server.yml`, `.github/ISSUE_TEMPLATE/v6_rc_feedback.yml`, `docs/RELEASE_NOTES.md`, `docs/releases/`, `docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md`, `docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md`, `docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md`, `scripts/check-workflow-dispatch-inputs.py`, `scripts/release_control/mobile_release_gate.py`, `scripts/release_control/mobile_release_gate_test.py`, `scripts/release_control/render_release_body.py`, `scripts/release_control/validate_artifact_release_line.py`, `scripts/release_control/record_rc_to_ga_rehearsal.py`, `scripts/release_control/internal/record_rc_to_ga_rehearsal.py`, `scripts/release_control/release_promotion_policy_support.py`, `scripts/trigger-release.sh`, and `scripts/trigger-release-dry-run.sh`
6. Add or change governed release-promotion workflow inputs, operator-facing promotion metadata, the canonical version file, prerelease feedback intake prompts, artifact publication lineage enforcement, release note or changelog packet composition, stable-promotion rehearsal summaries, or the optional exact-SHA external-worker acceleration through `.github/workflows/create-release.yml`, `.github/workflows/helm-pages.yml`, `.github/workflows/promote-floating-tags.yml`, `.github/workflows/promote-private-pro-runtime.yml`, `.github/workflows/publish-docker.yml`, `.github/workflows/publish-helm-chart.yml`, `.github/workflows/release-convergence.yml`, `.github/workflows/release-dry-run.yml`, `.github/workflows/retry-release-convergence.yml`, `.github/workflows/update-demo-server.yml`, `.github/ISSUE_TEMPLATE/v6_rc_feedback.yml`, `docs/RELEASE_NOTES.md`, `docs/releases/`, `docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md`, `docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md`, `docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md`, `scripts/check-workflow-dispatch-inputs.py`, `scripts/release-preflight-worker.sh`, `scripts/run-release-preflight.sh`, `scripts/release_control/mobile_release_gate.py`, `scripts/release_control/mobile_release_gate_test.py`, `scripts/release_control/render_release_body.py`, `scripts/release_control/validate_artifact_release_line.py`, `scripts/release_control/record_rc_to_ga_rehearsal.py`, `scripts/release_control/internal/record_rc_to_ga_rehearsal.py`, `scripts/release_control/release_promotion_policy_support.py`, `scripts/trigger-release.sh`, and `scripts/trigger-release-dry-run.sh`
That release-promotion boundary also owns prerelease note packet lineage:
shipped RC notes must remain historically accurate, the top-level
`docs/RELEASE_NOTES.md` index must continue to point at the current shipped
@@ -4209,6 +4209,7 @@
"scripts/lib/hot-dev-auth.sh",
"scripts/lib/hot-dev-runtime.sh",
"scripts/pulse-auto-update.sh",
"scripts/release-preflight-worker.sh",
"scripts/release_asset_common.sh",
"scripts/release_candidate_manifest.py",
"scripts/release_control/customer_promotion_lease.sh",
@@ -4219,6 +4220,7 @@
"scripts/release_control/resolve_release_promotion.py",
"scripts/release_control/validate_artifact_release_line.py",
"scripts/release_ldflags.sh",
"scripts/run-release-preflight.sh",
"scripts/run_cloud_public_signup_smoke.sh",
"scripts/run_demo_public_browser_smoke.sh",
"scripts/run_hosted_staging_smoke.sh",
@@ -4352,6 +4354,7 @@
"pulse-pro:scripts/validate_paid_runtime_distribution.py",
"scripts/check-workflow-dispatch-inputs.py",
"scripts/generate-release-notes.sh",
"scripts/release-preflight-worker.sh",
"scripts/release_control/customer_promotion_lease.sh",
"scripts/release_control/internal/record_rc_to_ga_rehearsal.py",
"scripts/release_control/mobile_release_gate.py",
@@ -4360,6 +4363,7 @@
"scripts/release_control/render_release_body.py",
"scripts/release_control/resolve_release_promotion.py",
"scripts/release_control/validate_artifact_release_line.py",
"scripts/run-release-preflight.sh",
"scripts/trigger-release-dry-run.sh",
"scripts/trigger-release.sh",
"scripts/trigger-stable-patch.sh",
@@ -4372,6 +4376,7 @@
"pulse-pro:scripts/tests/test_validate_paid_runtime_distribution.py",
"scripts/installtests/build_release_assets_test.go",
"scripts/release_control/internal/record_rc_to_ga_rehearsal_test.py",
"scripts/release_control/internal/release_preflight_test.py",
"scripts/release_control/mobile_release_gate_test.py",
"scripts/release_control/release_promotion_policy_support_test.py",
"scripts/release_control/release_promotion_policy_test.py",
+249
View File
@@ -0,0 +1,249 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage: release-preflight-worker.sh <40-character-source-sha> <rehearsal|release>
Runs the portable, high-cost release checks for an exact pushed commit in a
dedicated Linux amd64 checkout. The checkout and dependency caches persist
between runs; release credentials and signing keys are neither required nor
accepted.
EOF
}
SOURCE_SHA="${1:-}"
PROFILE="${2:-}"
if [ "$SOURCE_SHA" = "--help" ] || [ "$SOURCE_SHA" = "-h" ]; then
usage
exit 0
fi
if [[ ! "$SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]]; then
echo "Error: source SHA must be a lowercase 40-character Git commit id." >&2
exit 2
fi
if [ "$PROFILE" != "rehearsal" ] && [ "$PROFILE" != "release" ]; then
echo "Error: profile must be rehearsal or release." >&2
exit 2
fi
WORKER_ROOT="${PULSE_RELEASE_PREFLIGHT_ROOT:-/opt/pulse-release-worker}"
REPOSITORY_URL="${PULSE_RELEASE_PREFLIGHT_REPOSITORY_URL:-https://github.com/rcourtman/Pulse.git}"
REPOSITORY_DIR="${WORKER_ROOT}/repo"
CACHE_DIR="${WORKER_ROOT}/cache"
RECEIPT_DIR="${WORKER_ROOT}/receipts"
RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)-${SOURCE_SHA:0:12}-${PROFILE}"
RUN_DIR="${WORKER_ROOT}/tmp/${RUN_ID}"
TIMINGS_FILE="${RUN_DIR}/timings.tsv"
TEST_DATA_DIR="${WORKER_ROOT}/test-data/${PROFILE}"
for command_name in git go node npm docker curl flock timeout; do
if ! command -v "$command_name" >/dev/null 2>&1; then
echo "Error: required worker command is missing: ${command_name}" >&2
exit 3
fi
done
if ! docker compose version >/dev/null 2>&1; then
echo "Error: Docker Compose v2 is required on the worker." >&2
exit 3
fi
if [ "$(node -p "process.versions.node.split('.')[0]")" != "20" ]; then
echo "Error: the worker must use Node.js 20 to match the release workflows." >&2
exit 3
fi
mkdir -p \
"$CACHE_DIR/go-build" \
"$CACHE_DIR/go-mod" \
"$CACHE_DIR/npm" \
"$RECEIPT_DIR" \
"$RUN_DIR" \
"$(dirname "$TEST_DATA_DIR")"
exec 9>"${WORKER_ROOT}/worker.lock"
if ! flock -n 9; then
echo "Error: another release preflight is already using this worker." >&2
exit 5
fi
# A preflight compiles and tests only. Keep publication and signing authority
# out of the worker even if its login shell happens to define these names.
unset GH_TOKEN GITHUB_TOKEN PULSE_LICENSE_PRIVATE_KEY PULSE_UPDATE_SIGNING_KEY
export GOCACHE="$CACHE_DIR/go-build"
export GOMODCACHE="$CACHE_DIR/go-mod"
export npm_config_cache="$CACHE_DIR/npm"
# Match the canonical workflow's isolated single-repository checkout. Tests
# that explicitly require private sibling repositories use this signal to
# apply their documented hosted-CI skip instead of inventing local evidence.
export GITHUB_ACTIONS=true
export CI=true
phase() {
local name="$1"
shift
local started finished
started="$(date +%s)"
echo
echo "==> ${name}"
"$@"
finished="$(date +%s)"
printf '%s\t%s\n' "$name" "$((finished - started))" >> "$TIMINGS_FILE"
echo "<== ${name}: $((finished - started))s"
}
cleanup() {
if [ -d "$REPOSITORY_DIR/tests/integration" ]; then
(
cd "$REPOSITORY_DIR/tests/integration"
docker compose -f docker-compose.test.yml down -v >/dev/null 2>&1 || true
)
fi
}
trap cleanup EXIT
if [ ! -d "$REPOSITORY_DIR/.git" ]; then
phase clone git clone "$REPOSITORY_URL" "$REPOSITORY_DIR"
fi
phase fetch git -C "$REPOSITORY_DIR" fetch --force --no-tags origin "$SOURCE_SHA"
FETCHED_SHA="$(git -C "$REPOSITORY_DIR" rev-parse 'FETCH_HEAD^{commit}')"
if [ "$FETCHED_SHA" != "$SOURCE_SHA" ]; then
echo "Error: origin returned ${FETCHED_SHA}, expected ${SOURCE_SHA}." >&2
exit 4
fi
git -C "$REPOSITORY_DIR" checkout --detach --force "$SOURCE_SHA"
git -C "$REPOSITORY_DIR" clean -ffdx
cd "$REPOSITORY_DIR"
EXPECTED_GO="$(awk '/^toolchain go/ { sub(/^toolchain go/, ""); print; exit }' go.mod)"
ACTUAL_GO="$(go env GOVERSION)"
if [ -n "$EXPECTED_GO" ] && [ "$ACTUAL_GO" != "$EXPECTED_GO" ]; then
echo "Error: worker Go toolchain is ${ACTUAL_GO}; exact-SHA source requires ${EXPECTED_GO}." >&2
exit 3
fi
phase frontend-dependencies npm --prefix frontend-modern ci
phase frontend-lint npm --prefix frontend-modern run lint
phase frontend-headers npm --prefix frontend-modern run lint:headers
phase frontend-duplication npm --prefix frontend-modern run lint:cpd
phase frontend-types npm --prefix frontend-modern run type-check
phase frontend-tests npm --prefix frontend-modern test
phase frontend-build npm --prefix frontend-modern run build
rm -rf internal/api/frontend-modern
mkdir -p internal/api/frontend-modern
cp -R frontend-modern/dist internal/api/frontend-modern/
rm -rf "$TEST_DATA_DIR"
mkdir -p "$TEST_DATA_DIR"
if [ "$PROFILE" = "rehearsal" ]; then
phase backend-serial env PULSE_DATA_DIR="$TEST_DATA_DIR" go test -p 1 ./...
else
phase backend-race env PULSE_DATA_DIR="$TEST_DATA_DIR" make test
fi
phase integration-dependencies npm --prefix tests/integration ci
PLAYWRIGHT_VERSION="$(node -p "require('./tests/integration/node_modules/@playwright/test/package.json').version")"
PLAYWRIGHT_IMAGE="mcr.microsoft.com/playwright:v${PLAYWRIGHT_VERSION}-noble"
phase playwright-image docker pull "$PLAYWRIGHT_IMAGE"
VERSION="$(tr -d '\r\n' < VERSION)"
if [ "$PROFILE" = "rehearsal" ]; then
phase pulse-image docker build \
--build-arg "VERSION=${VERSION}" \
--platform linux/amd64 \
--target runtime \
--tag pulse:test \
.
else
phase pulse-image docker build \
--build-arg GO_BUILD_TAGS= \
--build-arg "VERSION=${VERSION}" \
--platform linux/amd64 \
--target e2e_runtime \
--tag pulse:test \
.
fi
phase mock-github-image docker build --tag pulse-mock-github:test tests/integration/mock-github-server
run_playwright() {
docker run --rm \
--network host \
--ipc host \
--user "$(id -u):$(id -g)" \
--env CI=true \
--env HOME=/tmp \
--env "PULSE_E2E_DIAGNOSTIC=${PULSE_E2E_DIAGNOSTIC:-}" \
--volume "$REPOSITORY_DIR/tests/integration:/work" \
--workdir /work \
"$PLAYWRIGHT_IMAGE" \
npx playwright test "$@"
}
run_rehearsal_smoke() {
cd tests/integration
export MOCK_CHECKSUM_ERROR=false
export MOCK_NETWORK_ERROR=false
export MOCK_RATE_LIMIT=false
export MOCK_STALE_RELEASE=false
export PULSE_E2E_DIAGNOSTIC=1
docker compose -f docker-compose.test.yml up -d --wait
timeout 60 sh -c 'until curl -fsS http://localhost:7655/api/health >/dev/null; do sleep 2; done'
run_playwright tests/00-diagnostic.spec.ts --project=chromium --reporter=list
local status
status="$(curl -s -o "$RUN_DIR/update-status.json" -w '%{http_code}' http://localhost:7655/api/updates/status || true)"
case "$status" in
200|401|403) ;;
*)
echo "Unexpected /api/updates/status response: ${status}" >&2
cat "$RUN_DIR/update-status.json" >&2 || true
return 1
;;
esac
docker compose -f docker-compose.test.yml down -v
}
run_release_smoke() {
cd tests/integration
export MOCK_CHECKSUM_ERROR=false
export MOCK_NETWORK_ERROR=false
export MOCK_RATE_LIMIT=false
export MOCK_STALE_RELEASE=false
export PULSE_E2E_BOOTSTRAP_TOKEN=0123456789abcdef0123456789abcdef0123456789abcdef
docker compose -f docker-compose.test.yml up -d
timeout 60 sh -c 'until docker inspect --format="{{json .State.Health.Status}}" pulse-mock-github | grep -q healthy; do sleep 2; done'
timeout 60 sh -c 'until docker inspect --format="{{json .State.Health.Status}}" pulse-test-server | grep -q healthy; do sleep 2; done'
timeout 60 sh -c 'until curl -fsS http://localhost:7655/api/health >/dev/null; do sleep 2; done'
run_playwright tests/95-release-smoke.spec.ts --project=chromium --reporter=list
docker compose -f docker-compose.test.yml down -v
}
if [ "$PROFILE" = "rehearsal" ]; then
phase rehearsal-smoke run_rehearsal_smoke
else
phase release-smoke run_release_smoke
fi
FINISHED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
TOTAL_SECONDS="$(awk -F '\t' '{ total += $2 } END { print total + 0 }' "$TIMINGS_FILE")"
RECEIPT_PATH="${RECEIPT_DIR}/${RUN_ID}.json"
{
printf '{\n'
printf ' "schema_version": 1,\n'
printf ' "source_sha": "%s",\n' "$SOURCE_SHA"
printf ' "profile": "%s",\n' "$PROFILE"
printf ' "architecture": "%s",\n' "$(uname -m)"
printf ' "finished_at": "%s",\n' "$FINISHED_AT"
printf ' "total_phase_seconds": %s,\n' "$TOTAL_SECONDS"
printf ' "result": "success"\n'
printf '}\n'
} > "$RECEIPT_PATH"
echo
echo "Exact-SHA release preflight passed."
echo "Source SHA: ${SOURCE_SHA}"
echo "Profile: ${PROFILE}"
echo "Phase time: ${TOTAL_SECONDS}s"
echo "Receipt: ${RECEIPT_PATH}"
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
from __future__ import annotations
import os
import pathlib
import subprocess
import unittest
ROOT = pathlib.Path(__file__).resolve().parents[3]
class ReleasePreflightTest(unittest.TestCase):
def run_script(
self, *args: str, env: dict[str, str] | None = None
) -> subprocess.CompletedProcess[str]:
command_env = os.environ.copy()
if env:
command_env.update(env)
return subprocess.run(
[str(ROOT / "scripts/run-release-preflight.sh"), *args],
cwd=ROOT,
env=command_env,
check=False,
capture_output=True,
text=True,
)
def test_plan_resolves_exact_sha_and_wsl_transport(self) -> None:
sha = subprocess.check_output(
["git", "rev-parse", "HEAD"], cwd=ROOT, text=True
).strip()
result = self.run_script(
"--profile",
"rehearsal",
"--host",
"test-worker",
"--wsl-distro",
"Ubuntu",
"--plan",
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn(f"SHA: {sha}", result.stdout)
self.assertIn("Runtime: WSL Ubuntu", result.stdout)
def test_missing_optional_worker_is_a_non_gate(self) -> None:
result = self.run_script(
"--profile",
"release",
"--if-configured",
env={
"PULSE_RELEASE_PREFLIGHT_HOST": "",
"PULSE_RELEASE_PREFLIGHT_WSL_DISTRO": "",
"GIT_CONFIG_NOSYSTEM": "1",
},
)
configured_host = subprocess.run(
["git", "config", "--get", "pulse.releasePreflightHost"],
cwd=ROOT,
check=False,
capture_output=True,
text=True,
)
if configured_host.returncode == 0:
self.skipTest("repository has a local release-preflight host configured")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("canonical hosted checks", result.stdout)
def test_worker_rejects_non_exact_sha_before_touching_worker_state(self) -> None:
result = subprocess.run(
[str(ROOT / "scripts/release-preflight-worker.sh"), "HEAD", "rehearsal"],
cwd=ROOT,
check=False,
capture_output=True,
text=True,
)
self.assertEqual(result.returncode, 2)
self.assertIn("40-character Git commit id", result.stderr)
def test_dispatch_helpers_select_the_matching_profiles(self) -> None:
dry_run = (ROOT / "scripts/trigger-release-dry-run.sh").read_text()
release = (ROOT / "scripts/trigger-release.sh").read_text()
workflow = (ROOT / ".github/workflows/release-dry-run.yml").read_text()
self.assertIn("--profile rehearsal", dry_run)
self.assertIn("--profile release", release)
self.assertIn("--if-configured", dry_run)
self.assertIn("--if-configured", release)
self.assertIn('PULSE_E2E_DIAGNOSTIC: "1"', workflow)
def test_worker_has_no_publication_or_signing_authority(self) -> None:
worker = (ROOT / "scripts/release-preflight-worker.sh").read_text()
runner = (ROOT / "scripts/run-release-preflight.sh").read_text()
self.assertIn("unset GH_TOKEN GITHUB_TOKEN", worker)
self.assertIn("export GITHUB_ACTIONS=true", worker)
self.assertIn("export CI=true", worker)
self.assertNotIn("docker push", worker)
self.assertNotIn("gh release", worker)
self.assertIn("mcr.microsoft.com/playwright:v${PLAYWRIGHT_VERSION}-noble", worker)
self.assertIn(
'git show "${SOURCE_SHA}:scripts/release-preflight-worker.sh"', runner
)
self.assertIn("is not reachable from a fetched origin branch", runner)
if __name__ == "__main__":
unittest.main()
@@ -3717,6 +3717,7 @@ class SubsystemLookupTest(unittest.TestCase):
"pulse-pro:scripts/tests/test_validate_paid_runtime_distribution.py",
"scripts/installtests/build_release_assets_test.go",
"scripts/release_control/internal/record_rc_to_ga_rehearsal_test.py",
"scripts/release_control/internal/release_preflight_test.py",
"scripts/release_control/mobile_release_gate_test.py",
"scripts/release_control/release_promotion_policy_support_test.py",
"scripts/release_control/release_promotion_policy_test.py",
@@ -3756,6 +3757,7 @@ class SubsystemLookupTest(unittest.TestCase):
"pulse-pro:scripts/tests/test_validate_paid_runtime_distribution.py",
"scripts/installtests/build_release_assets_test.go",
"scripts/release_control/internal/record_rc_to_ga_rehearsal_test.py",
"scripts/release_control/internal/release_preflight_test.py",
"scripts/release_control/mobile_release_gate_test.py",
"scripts/release_control/release_promotion_policy_support_test.py",
"scripts/release_control/release_promotion_policy_test.py",
@@ -3802,6 +3804,7 @@ class SubsystemLookupTest(unittest.TestCase):
"pulse-pro:scripts/tests/test_validate_paid_runtime_distribution.py",
"scripts/installtests/build_release_assets_test.go",
"scripts/release_control/internal/record_rc_to_ga_rehearsal_test.py",
"scripts/release_control/internal/release_preflight_test.py",
"scripts/release_control/mobile_release_gate_test.py",
"scripts/release_control/release_promotion_policy_support_test.py",
"scripts/release_control/release_promotion_policy_test.py",
@@ -3841,6 +3844,7 @@ class SubsystemLookupTest(unittest.TestCase):
"pulse-pro:scripts/tests/test_validate_paid_runtime_distribution.py",
"scripts/installtests/build_release_assets_test.go",
"scripts/release_control/internal/record_rc_to_ga_rehearsal_test.py",
"scripts/release_control/internal/release_preflight_test.py",
"scripts/release_control/mobile_release_gate_test.py",
"scripts/release_control/release_promotion_policy_support_test.py",
"scripts/release_control/release_promotion_policy_test.py",
@@ -3880,6 +3884,7 @@ class SubsystemLookupTest(unittest.TestCase):
"pulse-pro:scripts/tests/test_validate_paid_runtime_distribution.py",
"scripts/installtests/build_release_assets_test.go",
"scripts/release_control/internal/record_rc_to_ga_rehearsal_test.py",
"scripts/release_control/internal/release_preflight_test.py",
"scripts/release_control/mobile_release_gate_test.py",
"scripts/release_control/release_promotion_policy_support_test.py",
"scripts/release_control/release_promotion_policy_test.py",
@@ -3919,6 +3924,7 @@ class SubsystemLookupTest(unittest.TestCase):
"pulse-pro:scripts/tests/test_validate_paid_runtime_distribution.py",
"scripts/installtests/build_release_assets_test.go",
"scripts/release_control/internal/record_rc_to_ga_rehearsal_test.py",
"scripts/release_control/internal/release_preflight_test.py",
"scripts/release_control/mobile_release_gate_test.py",
"scripts/release_control/release_promotion_policy_support_test.py",
"scripts/release_control/release_promotion_policy_test.py",
+131
View File
@@ -0,0 +1,131 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage: run-release-preflight.sh [options]
Options:
--sha SHA Exact pushed commit (default: HEAD)
--profile PROFILE rehearsal or release (required)
--host HOST SSH host (or PULSE_RELEASE_PREFLIGHT_HOST / git config)
--wsl-distro NAME Execute in this WSL distribution on the SSH host
--if-configured Succeed without running when no host is configured
--plan Print the resolved execution plan without connecting
-h, --help Show this help
Persistent repository configuration:
git config pulse.releasePreflightHost <ssh-alias>
git config pulse.releasePreflightWslDistro <distribution>
EOF
}
SOURCE_REF="HEAD"
PROFILE=""
HOST="${PULSE_RELEASE_PREFLIGHT_HOST:-}"
WSL_DISTRO="${PULSE_RELEASE_PREFLIGHT_WSL_DISTRO:-}"
IF_CONFIGURED=false
PLAN_ONLY=false
while [ "$#" -gt 0 ]; do
case "$1" in
--sha)
SOURCE_REF="${2:-}"
shift 2
;;
--profile)
PROFILE="${2:-}"
shift 2
;;
--host)
HOST="${2:-}"
shift 2
;;
--wsl-distro)
WSL_DISTRO="${2:-}"
shift 2
;;
--if-configured)
IF_CONFIGURED=true
shift
;;
--plan)
PLAN_ONLY=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Error: unknown option: $1" >&2
usage >&2
exit 2
;;
esac
done
if [ "$PROFILE" != "rehearsal" ] && [ "$PROFILE" != "release" ]; then
echo "Error: --profile must be rehearsal or release." >&2
exit 2
fi
REPOSITORY_ROOT="$(git rev-parse --show-toplevel)"
cd "$REPOSITORY_ROOT"
SOURCE_SHA="$(git rev-parse --verify "${SOURCE_REF}^{commit}")"
if [[ ! "$SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]]; then
echo "Error: could not resolve an exact source SHA from ${SOURCE_REF}." >&2
exit 2
fi
if [ -z "$HOST" ]; then
HOST="$(git config --get pulse.releasePreflightHost || true)"
fi
if [ -z "$WSL_DISTRO" ]; then
WSL_DISTRO="$(git config --get pulse.releasePreflightWslDistro || true)"
fi
if [ -n "$WSL_DISTRO" ] && [[ ! "$WSL_DISTRO" =~ ^[A-Za-z0-9._-]+$ ]]; then
echo "Error: WSL distribution names may contain only letters, digits, dot, underscore, and hyphen." >&2
exit 2
fi
if [ -z "$HOST" ]; then
if [ "$IF_CONFIGURED" = true ]; then
echo "No accelerated release-preflight worker is configured; continuing with canonical hosted checks."
exit 0
fi
echo "Error: configure --host, PULSE_RELEASE_PREFLIGHT_HOST, or pulse.releasePreflightHost." >&2
exit 2
fi
echo "Accelerated exact-SHA release preflight"
echo " Host: ${HOST}"
echo " SHA: ${SOURCE_SHA}"
echo " Profile: ${PROFILE}"
if [ -n "$WSL_DISTRO" ]; then
echo " Runtime: WSL ${WSL_DISTRO}"
else
echo " Runtime: native remote shell"
fi
if [ "$PLAN_ONLY" = true ]; then
exit 0
fi
git fetch origin --quiet
if ! git for-each-ref --format='%(refname)' --contains "$SOURCE_SHA" refs/remotes/origin/ | grep -q .; then
echo "Error: ${SOURCE_SHA} is not reachable from a fetched origin branch." >&2
exit 2
fi
if ! git cat-file -e "${SOURCE_SHA}:scripts/release-preflight-worker.sh" 2>/dev/null; then
echo "Error: ${SOURCE_SHA} does not contain scripts/release-preflight-worker.sh." >&2
exit 2
fi
if [ -n "$WSL_DISTRO" ]; then
git show "${SOURCE_SHA}:scripts/release-preflight-worker.sh" | \
ssh "$HOST" wsl.exe -d "$WSL_DISTRO" -- bash -s -- "$SOURCE_SHA" "$PROFILE"
else
git show "${SOURCE_SHA}:scripts/release-preflight-worker.sh" | \
ssh "$HOST" bash -s -- "$SOURCE_SHA" "$PROFILE"
fi
+6 -1
View File
@@ -57,7 +57,7 @@ echo "✓ On required branch (${REQUIRED_BRANCH})"
git fetch origin --quiet
LOCAL=$(git rev-parse @)
REMOTE=$(git rev-parse @{u})
REMOTE=$(git rev-parse '@{u}')
if [ "$LOCAL" != "$REMOTE" ]; then
echo "❌ Local branch is not fully pushed to origin"
echo ""
@@ -72,6 +72,11 @@ else
echo "✓ Up to date with remote"
fi
./scripts/run-release-preflight.sh \
--sha "$LOCAL" \
--profile rehearsal \
--if-configured
python3 scripts/check-workflow-dispatch-inputs.py \
--workflow-path .github/workflows/release-dry-run.yml \
--branch "$CURRENT_BRANCH" \
+6 -1
View File
@@ -68,7 +68,7 @@ echo "✓ On required branch (${REQUIRED_BRANCH})"
# Check 4: Up to date with remote
git fetch origin --quiet
LOCAL=$(git rev-parse @)
REMOTE=$(git rev-parse @{u})
REMOTE=$(git rev-parse '@{u}')
if [ "$LOCAL" != "$REMOTE" ]; then
echo "❌ Local branch is not fully pushed to origin"
@@ -84,6 +84,11 @@ else
echo "✓ Up to date with remote"
fi
./scripts/run-release-preflight.sh \
--sha "$LOCAL" \
--profile release \
--if-configured
python3 scripts/check-workflow-dispatch-inputs.py \
--workflow-path .github/workflows/create-release.yml \
--branch "$CURRENT_BRANCH" \