mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Accelerate release qualification with exact-SHA worker
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Executable
+249
@@ -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
@@ -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",
|
||||
|
||||
Executable
+131
@@ -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
|
||||
@@ -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" \
|
||||
|
||||
@@ -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" \
|
||||
|
||||
Reference in New Issue
Block a user