mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
9a6a5811a7
The standalone setup-node proposal lacked lifecycle and deployment evidence. Preserve Node 24, explicit cache controls and native Windows proof steps while upgrading the immutable action revision; add consumer regression coverage for the removed dummy auth-token assumption. Keep grouped signing and deployment action upgrades separate. Change-source: pulse-maintainer
597 lines
26 KiB
YAML
597 lines
26 KiB
YAML
name: Release Dry Run
|
|
run-name: Release Dry Run v${{ inputs.version || 'scheduled' }}
|
|
|
|
on:
|
|
# Weekly drift watchdog: starts on main every Tuesday 07:00 UTC and selects the governed
|
|
# release branch so fixture, manifest, and load-calibration drift surfaces a
|
|
# week at a time instead of piling up until an RC publish. Scheduled runs
|
|
# carry no workflow_dispatch inputs (GitHub does not apply input defaults to
|
|
# schedule events), so the rehearsal step derives the rollback target as the
|
|
# latest stable tag preceding VERSION. Manual dispatches must still supply
|
|
# rollback_version explicitly.
|
|
schedule:
|
|
- cron: '0 7 * * 2'
|
|
workflow_dispatch:
|
|
inputs:
|
|
version:
|
|
description: 'Optional version under rehearsal (e.g. 6.0.0-rc.2 or 6.0.0)'
|
|
required: false
|
|
type: string
|
|
promoted_from_tag:
|
|
description: 'Stable rehearsal only: prerelease tag being promoted (for example v6.0.0-rc.2)'
|
|
required: false
|
|
type: string
|
|
rollback_version:
|
|
description: 'Required rollback stable version to rehearse (for example 6.0.4 or v6.0.4)'
|
|
required: true
|
|
type: string
|
|
ga_date:
|
|
description: 'Stable v6.0.0 rehearsal only: planned GA publish date (YYYY-MM-DD)'
|
|
required: false
|
|
type: string
|
|
v5_eos_date:
|
|
description: 'Stable v6.0.0 rehearsal only: Pulse v5 end-of-support date (YYYY-MM-DD)'
|
|
required: false
|
|
type: string
|
|
hotfix_exception:
|
|
description: 'Stable rehearsal only: bypass 72-hour prerelease soak for urgent customer harm'
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
hotfix_reason:
|
|
description: 'Stable rehearsal only: reason for hotfix soak exception'
|
|
required: false
|
|
type: string
|
|
unsigned_windows_exception:
|
|
description: 'Optional version-bound override after SignPath availability is restored; not required while the standing unavailable policy is active'
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
unsigned_windows_reason:
|
|
description: 'Owner reason for an explicit version-bound unsigned Windows override'
|
|
required: false
|
|
type: string
|
|
note:
|
|
description: 'Optional note/reason for the dry run'
|
|
required: false
|
|
type: string
|
|
mobile_release_decision:
|
|
description: 'Mobile impact decision: no-mobile-impact, existing-mobile-build-compatible, mobile-candidate-uploaded, or mobile-candidate-required'
|
|
required: false
|
|
type: string
|
|
mobile_release_evidence:
|
|
description: 'Evidence for existing-mobile-build-compatible or mobile-candidate-uploaded decisions'
|
|
required: false
|
|
type: string
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
build_release_candidate:
|
|
name: Build Immutable Release Candidate
|
|
if: ${{ inputs.version != '' }}
|
|
permissions:
|
|
actions: write
|
|
attestations: write
|
|
contents: read
|
|
id-token: write
|
|
uses: ./.github/workflows/build-release-candidate.yml
|
|
secrets: inherit
|
|
with:
|
|
version: ${{ inputs.version }}
|
|
require_macos_signing: true
|
|
# Keep this aligned with WINDOWS_AUTHENTICODE_AVAILABLE in
|
|
# scripts/release_control/resolve_release_promotion.py. Restore both only
|
|
# after the release owner confirms production credentials and certificate
|
|
# authorization are ready.
|
|
require_windows_signing: false
|
|
windows_signing_backend: signpath
|
|
|
|
dry-run:
|
|
name: Preflight Release Checks (No Publish)
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 90
|
|
permissions:
|
|
contents: read
|
|
packages: read
|
|
outputs:
|
|
tested_sha: ${{ steps.source.outputs.tested_sha }}
|
|
tested_branch: ${{ steps.source.outputs.tested_branch }}
|
|
version: ${{ steps.rehearsal.outputs.version }}
|
|
tag: ${{ steps.rehearsal.outputs.tag }}
|
|
is_prerelease: ${{ steps.rehearsal.outputs.is_prerelease }}
|
|
promoted_from_tag: ${{ steps.rehearsal.outputs.promoted_from_tag }}
|
|
rollback_tag: ${{ steps.rehearsal.outputs.rollback_tag }}
|
|
rollback_command: ${{ steps.rehearsal.outputs.rollback_command }}
|
|
ga_date: ${{ steps.rehearsal.outputs.ga_date }}
|
|
v5_eos_date: ${{ steps.rehearsal.outputs.v5_eos_date }}
|
|
soak_hours: ${{ steps.rehearsal.outputs.soak_hours }}
|
|
hotfix_exception: ${{ steps.rehearsal.outputs.hotfix_exception }}
|
|
hotfix_reason: ${{ steps.rehearsal.outputs.hotfix_reason }}
|
|
require_windows_signing: ${{ steps.rehearsal.outputs.require_windows_signing }}
|
|
unsigned_windows_exception: ${{ steps.rehearsal.outputs.unsigned_windows_exception }}
|
|
unsigned_windows_reason: ${{ steps.rehearsal.outputs.unsigned_windows_reason }}
|
|
|
|
steps:
|
|
- name: Validate release ref
|
|
run: |
|
|
if [[ "${GITHUB_REF}" != refs/heads/* ]]; then
|
|
echo "::error::Release dry run must be executed from a branch ref. Current ref: ${GITHUB_REF}"
|
|
exit 1
|
|
fi
|
|
echo "[OK] Dry run executing on branch ${GITHUB_REF_NAME}"
|
|
|
|
- name: Checkout repository
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
fetch-depth: 0
|
|
|
|
- name: Resolve required release branch
|
|
id: branch_policy
|
|
env:
|
|
VERSION_INPUT: ${{ inputs.version }}
|
|
run: |
|
|
VERSION="${VERSION_INPUT:-}"
|
|
if [ -z "$VERSION" ]; then
|
|
VERSION="$(tr -d '\r\n' < VERSION)"
|
|
fi
|
|
REQUIRED_BRANCH="$(python3 scripts/release_control/control_plane.py --branch-for-version "${VERSION}")"
|
|
python3 scripts/write_github_output.py required_branch "${REQUIRED_BRANCH}"
|
|
echo "[OK] Governed release branch for ${VERSION} is ${REQUIRED_BRANCH}"
|
|
|
|
- name: Select exact rehearsal source
|
|
id: source
|
|
env:
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
REQUIRED_BRANCH: ${{ steps.branch_policy.outputs.required_branch }}
|
|
run: |
|
|
set -euo pipefail
|
|
TESTED_BRANCH="${GITHUB_REF_NAME}"
|
|
if [ "${EVENT_NAME}" = "schedule" ]; then
|
|
# Resolve once; never test a moving branch or rewrite GITHUB_SHA.
|
|
git check-ref-format "refs/heads/${REQUIRED_BRANCH}"
|
|
git fetch --no-tags origin "refs/heads/${REQUIRED_BRANCH}"
|
|
TESTED_SHA="$(git rev-parse --verify 'FETCH_HEAD^{commit}')"
|
|
SELECTED_VERSION="$(git show "${TESTED_SHA}:VERSION" | tr -d '\r\n')"
|
|
SELECTED_BRANCH="$(python3 scripts/release_control/control_plane.py --branch-for-version "${SELECTED_VERSION}")"
|
|
if [ "${SELECTED_BRANCH}" != "${REQUIRED_BRANCH}" ]; then
|
|
echo "::error::Selected VERSION no longer belongs to ${REQUIRED_BRANCH}."
|
|
exit 1
|
|
fi
|
|
git checkout --detach "${TESTED_SHA}"
|
|
TESTED_BRANCH="${REQUIRED_BRANCH}"
|
|
elif [ "${EVENT_NAME}" = "workflow_dispatch" ]; then
|
|
TESTED_SHA="$(git rev-parse --verify 'HEAD^{commit}')"
|
|
if [ "${TESTED_BRANCH}" != "${REQUIRED_BRANCH}" ] || [ "${TESTED_SHA}" != "${GITHUB_SHA}" ]; then
|
|
echo "::error::Manual rehearsal must retain the exact event SHA on ${REQUIRED_BRANCH}."
|
|
exit 1
|
|
fi
|
|
else
|
|
echo "::error::Unsupported rehearsal event: ${EVENT_NAME}"
|
|
exit 1
|
|
fi
|
|
# These values are ref-checked / git-produced, never free-form inputs.
|
|
{
|
|
echo "tested_sha=${TESTED_SHA}"
|
|
echo "tested_branch=${TESTED_BRANCH}"
|
|
} >> "$GITHUB_OUTPUT"
|
|
echo "[OK] Workflow event SHA ${GITHUB_SHA}; tested ${TESTED_BRANCH}@${TESTED_SHA}"
|
|
|
|
- name: Resolve rehearsal metadata
|
|
id: rehearsal
|
|
env:
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
VERSION_INPUT: ${{ inputs.version }}
|
|
PROMOTED_FROM_TAG_INPUT: ${{ inputs.promoted_from_tag }}
|
|
ROLLBACK_VERSION_INPUT: ${{ inputs.rollback_version }}
|
|
GA_DATE_INPUT: ${{ inputs.ga_date }}
|
|
V5_EOS_DATE_INPUT: ${{ inputs.v5_eos_date }}
|
|
HOTFIX_EXCEPTION_INPUT: ${{ inputs.hotfix_exception }}
|
|
HOTFIX_REASON_INPUT: ${{ inputs.hotfix_reason }}
|
|
UNSIGNED_WINDOWS_EXCEPTION_INPUT: ${{ inputs.unsigned_windows_exception }}
|
|
UNSIGNED_WINDOWS_REASON_INPUT: ${{ inputs.unsigned_windows_reason }}
|
|
TESTED_BRANCH: ${{ steps.source.outputs.tested_branch }}
|
|
WORKFLOW_OUTPUT_1: ${{ steps.branch_policy.outputs.required_branch }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
VERSION="${VERSION_INPUT:-}"
|
|
if [ -z "$VERSION" ]; then
|
|
VERSION="$(tr -d '\r\n' < VERSION)"
|
|
fi
|
|
|
|
TAG="v${VERSION}"
|
|
IS_PRERELEASE="false"
|
|
if [[ "$VERSION" =~ -rc\.[0-9]+$ ]] || [[ "$VERSION" =~ -alpha\.[0-9]+$ ]] || [[ "$VERSION" =~ -beta\.[0-9]+$ ]]; then
|
|
IS_PRERELEASE="true"
|
|
fi
|
|
|
|
REQUIRED_BRANCH="${WORKFLOW_OUTPUT_1}"
|
|
|
|
if [ "${TESTED_BRANCH}" != "$REQUIRED_BRANCH" ]; then
|
|
echo "::error::Rehearsal version ${VERSION} requires branch ${REQUIRED_BRANCH}, but tested source is on ${TESTED_BRANCH}."
|
|
exit 1
|
|
fi
|
|
|
|
FILE_VERSION="$(tr -d '\r\n' < VERSION)"
|
|
if [ "$FILE_VERSION" != "$VERSION" ]; then
|
|
echo "::error::VERSION file (${FILE_VERSION}) does not match rehearsal version (${VERSION})."
|
|
exit 1
|
|
fi
|
|
|
|
git fetch --prune origin main "${REQUIRED_BRANCH}" --tags
|
|
|
|
HELPER_ARGS=(
|
|
--version "${VERSION}"
|
|
--promoted-from-tag "${PROMOTED_FROM_TAG_INPUT:-}"
|
|
--rollback-version "${ROLLBACK_VERSION_INPUT:-}"
|
|
--ga-date "${GA_DATE_INPUT:-}"
|
|
--v5-eos-date "${V5_EOS_DATE_INPUT:-}"
|
|
--hotfix-reason "${HOTFIX_REASON_INPUT:-}"
|
|
)
|
|
if [ "${HOTFIX_EXCEPTION_INPUT:-false}" = "true" ]; then
|
|
HELPER_ARGS+=(--hotfix-exception)
|
|
fi
|
|
if [ "${UNSIGNED_WINDOWS_EXCEPTION_INPUT:-false}" = "true" ]; then
|
|
HELPER_ARGS+=(
|
|
--unsigned-windows-exception
|
|
--unsigned-windows-reason "${UNSIGNED_WINDOWS_REASON_INPUT:-}"
|
|
)
|
|
elif [ -n "${UNSIGNED_WINDOWS_REASON_INPUT:-}" ]; then
|
|
HELPER_ARGS+=(--unsigned-windows-reason "${UNSIGNED_WINDOWS_REASON_INPUT}")
|
|
fi
|
|
if [ "${EVENT_NAME}" = "schedule" ] && [ -z "${ROLLBACK_VERSION_INPUT:-}" ]; then
|
|
# Scheduled watchdog runs carry no dispatch inputs; derive the
|
|
# rollback target instead of failing. Manual dispatches never get
|
|
# this flag, so their explicit-rollback requirement stands.
|
|
HELPER_ARGS+=(--derive-rollback-latest-stable)
|
|
echo "[OK] Scheduled rehearsal: deriving rollback target from the latest preceding stable tag"
|
|
fi
|
|
|
|
python3 scripts/release_control/resolve_release_promotion.py \
|
|
"${HELPER_ARGS[@]}" > "$RUNNER_TEMP/rehearsal-metadata.out"
|
|
|
|
{
|
|
echo "version=${VERSION}"
|
|
echo "tag=${TAG}"
|
|
echo "is_prerelease=${IS_PRERELEASE}"
|
|
cat "$RUNNER_TEMP/rehearsal-metadata.out"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
echo "[OK] Rehearsal metadata validated for ${TAG}"
|
|
|
|
- name: Validate mobile release decision
|
|
id: mobile_release
|
|
env:
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
MOBILE_RELEASE_DECISION: ${{ inputs.mobile_release_decision }}
|
|
MOBILE_RELEASE_EVIDENCE: ${{ inputs.mobile_release_evidence }}
|
|
WORKFLOW_OUTPUT_1: ${{ steps.rehearsal.outputs.version }}
|
|
run: |
|
|
set -euo pipefail
|
|
DECISION="${MOBILE_RELEASE_DECISION:-}"
|
|
EVIDENCE="${MOBILE_RELEASE_EVIDENCE:-}"
|
|
if [ -z "${DECISION}" ] && [ "${EVENT_NAME}" = "schedule" ]; then
|
|
DECISION="no-mobile-impact"
|
|
EVIDENCE="Scheduled release dry-run watchdog; no mobile release packet is being dispatched."
|
|
fi
|
|
python3 scripts/release_control/mobile_release_gate.py \
|
|
--version "${WORKFLOW_OUTPUT_1}" \
|
|
--decision "${DECISION}" \
|
|
--evidence "${EVIDENCE}" \
|
|
--github-annotations
|
|
|
|
- name: Set up Node.js
|
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
with:
|
|
node-version: '24'
|
|
package-manager-cache: false
|
|
|
|
- name: Install frontend dependencies
|
|
run: npm --prefix frontend-modern ci
|
|
|
|
- name: Build frontend bundle for Go embed
|
|
run: |
|
|
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/
|
|
|
|
- name: Lint frontend
|
|
run: npm --prefix frontend-modern run lint
|
|
|
|
- name: Audit header composition
|
|
run: npm --prefix frontend-modern run lint:headers
|
|
|
|
- name: Check frontend copy-paste duplication
|
|
run: npm --prefix frontend-modern run lint:cpd
|
|
|
|
- name: Install docker-compose
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y docker-compose
|
|
|
|
- name: Set up Go
|
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
|
with:
|
|
go-version-file: go.mod
|
|
cache: false
|
|
|
|
- name: Run backend tests
|
|
# Serial package execution keeps non-race SLO tests meaningful on
|
|
# 2-core hosted runners instead of measuring cross-package contention.
|
|
run: go test -p 1 ./...
|
|
|
|
- name: Prepare integration test dependencies
|
|
working-directory: tests/integration
|
|
run: |
|
|
npm ci
|
|
npx playwright install --with-deps chromium
|
|
|
|
- name: Build Pulse binaries for integration tests
|
|
run: make build
|
|
|
|
- name: Log in to GHCR for build cache
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
if [ -z "${GH_TOKEN:-}" ]; then
|
|
echo "::error::GITHUB_TOKEN not available for GHCR login"
|
|
exit 1
|
|
fi
|
|
echo "$GH_TOKEN" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
|
|
|
|
- name: Build Docker images for integration tests
|
|
run: |
|
|
docker build -t pulse:test --target runtime .
|
|
docker build -t pulse-mock-github:test tests/integration/mock-github-server
|
|
env:
|
|
PULSE_LICENSE_PUBLIC_KEY: ${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
|
|
|
|
- name: Run integration diagnostics
|
|
working-directory: tests/integration
|
|
env:
|
|
MOCK_CHECKSUM_ERROR: "false"
|
|
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
|
|
|
|
echo "Verifying Pulse API is reachable..."
|
|
timeout 60 sh -c 'until curl -fsS http://localhost:7655/api/health > /dev/null; do sleep 2; done'
|
|
|
|
echo "Running Playwright diagnostics..."
|
|
mkdir -p diagnostic-evidence
|
|
set -o pipefail
|
|
npx playwright test tests/00-diagnostic.spec.ts --project=chromium --retries=0 --reporter=list \
|
|
2>&1 | tee diagnostic-evidence/playwright-diagnostic.log
|
|
|
|
echo "Running update API route smoke check..."
|
|
STATUS=$(curl -s -o /tmp/update-status.json -w "%{http_code}" http://localhost:7655/api/updates/status || true)
|
|
echo "Update status endpoint returned HTTP ${STATUS}"
|
|
case "${STATUS}" in
|
|
200|401|403)
|
|
;;
|
|
*)
|
|
echo "Unexpected response from /api/updates/status"
|
|
cat /tmp/update-status.json || true
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
- name: Collect integration diagnostic runtime evidence
|
|
if: always()
|
|
working-directory: tests/integration
|
|
run: |
|
|
mkdir -p diagnostic-evidence
|
|
docker compose -f docker-compose.test.yml ps --all \
|
|
> diagnostic-evidence/docker-compose-ps.txt 2>&1 || true
|
|
docker compose -f docker-compose.test.yml logs --no-color \
|
|
> diagnostic-evidence/docker-compose.log 2>&1 || true
|
|
|
|
- name: Cleanup integration environment
|
|
if: always()
|
|
working-directory: tests/integration
|
|
run: docker compose -f docker-compose.test.yml down -v || true
|
|
|
|
- name: Upload integration diagnostic evidence
|
|
if: always()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: release-dry-run-integration-diagnostics
|
|
path: |
|
|
tests/integration/diagnostic-evidence/
|
|
tests/integration/test-results/
|
|
if-no-files-found: error
|
|
|
|
- name: Write rehearsal summary
|
|
if: always()
|
|
env:
|
|
TESTED_SHA: ${{ steps.source.outputs.tested_sha }}
|
|
TESTED_BRANCH: ${{ steps.source.outputs.tested_branch }}
|
|
NOTE: ${{ inputs.note }}
|
|
REHEARSAL_CONCLUSION: ${{ steps.rehearsal.conclusion }}
|
|
JOB_CONCLUSION: ${{ job.status }}
|
|
WORKFLOW_OUTPUT_1: ${{ steps.rehearsal.outputs.version }}
|
|
WORKFLOW_OUTPUT_2: ${{ steps.rehearsal.outputs.tag }}
|
|
WORKFLOW_OUTPUT_3: ${{ steps.rehearsal.outputs.is_prerelease == 'true' && 'rc' || 'stable' }}
|
|
WORKFLOW_OUTPUT_4: ${{ steps.rehearsal.outputs.promoted_from_tag }}
|
|
WORKFLOW_OUTPUT_5: ${{ steps.rehearsal.outputs.rollback_tag }}
|
|
WORKFLOW_OUTPUT_6: ${{ steps.rehearsal.outputs.rollback_command }}
|
|
WORKFLOW_OUTPUT_7: ${{ steps.rehearsal.outputs.soak_hours }}
|
|
WORKFLOW_OUTPUT_8: ${{ steps.rehearsal.outputs.ga_date }}
|
|
WORKFLOW_OUTPUT_9: ${{ steps.rehearsal.outputs.v5_eos_date }}
|
|
WORKFLOW_OUTPUT_10: ${{ steps.rehearsal.outputs.hotfix_exception }}
|
|
WORKFLOW_OUTPUT_11: ${{ steps.rehearsal.outputs.hotfix_reason }}
|
|
WORKFLOW_OUTPUT_12: ${{ steps.rehearsal.outputs.require_windows_signing }}
|
|
WORKFLOW_OUTPUT_13: ${{ steps.rehearsal.outputs.unsigned_windows_exception }}
|
|
WORKFLOW_OUTPUT_14: ${{ steps.rehearsal.outputs.unsigned_windows_reason }}
|
|
run: |
|
|
mkdir -p release-dry-run
|
|
SUMMARY_FILE="release-dry-run/rc-to-ga-rehearsal-summary.md"
|
|
RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
|
if [ "${REHEARSAL_CONCLUSION}" != "success" ]; then
|
|
{
|
|
echo "# Prerelease-to-GA Rehearsal Summary"
|
|
echo ""
|
|
echo "- Workflow run: ${RUN_URL}"
|
|
echo "- Workflow event: ${GITHUB_REF_NAME}@${GITHUB_SHA}"
|
|
echo "- Tested source: ${TESTED_BRANCH:-unresolved}@${TESTED_SHA:-unresolved}"
|
|
echo "- Result: ${JOB_CONCLUSION}"
|
|
if [ -n "${NOTE}" ]; then
|
|
echo "- Operator note: ${NOTE}"
|
|
fi
|
|
echo ""
|
|
echo "## Result"
|
|
echo ""
|
|
echo "This run did not produce a valid promotion metadata envelope."
|
|
echo "Do not use this artifact to clear \`rc-to-ga-promotion-readiness\`."
|
|
echo "Fix the failed rehearsal metadata or branch-state preconditions and rerun the workflow."
|
|
} > "$SUMMARY_FILE"
|
|
else
|
|
{
|
|
echo "# Prerelease-to-GA Rehearsal Summary"
|
|
echo ""
|
|
echo "- Workflow run: ${RUN_URL}"
|
|
echo "- Workflow event: ${GITHUB_REF_NAME}@${GITHUB_SHA}"
|
|
echo "- Tested source: ${TESTED_BRANCH:-unresolved}@${TESTED_SHA:-unresolved}"
|
|
echo "- Version: ${WORKFLOW_OUTPUT_1}"
|
|
echo "- Candidate stable tag: ${WORKFLOW_OUTPUT_2}"
|
|
echo "- Promotion channel: ${WORKFLOW_OUTPUT_3}"
|
|
if [ -n "${WORKFLOW_OUTPUT_4}" ]; then
|
|
echo "- Promoted prerelease tag: ${WORKFLOW_OUTPUT_4}"
|
|
fi
|
|
if [ -n "${WORKFLOW_OUTPUT_5}" ]; then
|
|
echo "- Rollback target: ${WORKFLOW_OUTPUT_5}"
|
|
fi
|
|
if [ -n "${WORKFLOW_OUTPUT_6}" ]; then
|
|
echo "- Rollback command: \`${WORKFLOW_OUTPUT_6}\`"
|
|
fi
|
|
if [ -n "${WORKFLOW_OUTPUT_7}" ]; then
|
|
echo "- Prerelease soak hours at rehearsal time: ${WORKFLOW_OUTPUT_7}"
|
|
fi
|
|
if [ -n "${WORKFLOW_OUTPUT_8}" ]; then
|
|
echo "- Planned GA date: ${WORKFLOW_OUTPUT_8}"
|
|
fi
|
|
if [ -n "${WORKFLOW_OUTPUT_9}" ]; then
|
|
echo "- Planned v5 end-of-support date: ${WORKFLOW_OUTPUT_9}"
|
|
fi
|
|
echo "- Hotfix exception: ${WORKFLOW_OUTPUT_10}"
|
|
if [ -n "${WORKFLOW_OUTPUT_11}" ]; then
|
|
echo "- Hotfix reason: ${WORKFLOW_OUTPUT_11}"
|
|
fi
|
|
echo "- Windows Authenticode required: ${WORKFLOW_OUTPUT_12}"
|
|
echo "- Unsigned Windows exception: ${WORKFLOW_OUTPUT_13}"
|
|
if [ -n "${WORKFLOW_OUTPUT_14}" ]; then
|
|
echo "- Unsigned Windows reason: ${WORKFLOW_OUTPUT_14}"
|
|
fi
|
|
if [ -n "${NOTE}" ]; then
|
|
echo "- Operator note: ${NOTE}"
|
|
fi
|
|
echo ""
|
|
echo "## Result"
|
|
echo ""
|
|
echo "This run exercised the non-publish release path and validated the current promotion contract on the selected branch."
|
|
echo "Record this run URL in the release ticket when clearing \`rc-to-ga-promotion-readiness\`."
|
|
echo ""
|
|
echo "## Governed Record"
|
|
echo ""
|
|
echo "Materialize the dated rehearsal record from this exact run with:"
|
|
echo "\`python3 scripts/release_control/record_rc_to_ga_rehearsal.py --run-id ${{ github.run_id }}\`"
|
|
echo ""
|
|
echo "If you do not pass \`--output\`, the recorder writes to \`docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-rehearsal-<record-date>.md\`."
|
|
} > "$SUMMARY_FILE"
|
|
fi
|
|
|
|
cat "$SUMMARY_FILE" >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
- name: Upload rehearsal summary artifact
|
|
if: always()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: rc-to-ga-rehearsal-summary
|
|
path: release-dry-run/rc-to-ga-rehearsal-summary.md
|
|
|
|
demo_path_preflight:
|
|
name: Verify Current Stable Demo Path (No Mutation)
|
|
needs: dry-run
|
|
permissions:
|
|
contents: read
|
|
uses: ./.github/workflows/update-demo-server.yml
|
|
secrets: inherit
|
|
with:
|
|
tag: latest
|
|
target: stable
|
|
verify_only: true
|
|
|
|
release_dry_run_verdict:
|
|
name: Definitive Dry-Run Verdict
|
|
needs:
|
|
- build_release_candidate
|
|
- dry-run
|
|
- demo_path_preflight
|
|
if: ${{ always() }}
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 10
|
|
steps:
|
|
- name: Enforce exact-SHA rehearsal outcomes
|
|
env:
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
VERSION_INPUT: ${{ inputs.version }}
|
|
TESTED_SHA: ${{ needs.dry-run.outputs.tested_sha }}
|
|
TESTED_BRANCH: ${{ needs.dry-run.outputs.tested_branch }}
|
|
RESOLVED_VERSION: ${{ needs.dry-run.outputs.version }}
|
|
CANDIDATE_RESULT: ${{ needs.build_release_candidate.result }}
|
|
PREFLIGHT_RESULT: ${{ needs.dry-run.result }}
|
|
DEMO_RESULT: ${{ needs.demo_path_preflight.result }}
|
|
WINDOWS_SIGNING_BACKEND: ${{ needs.build_release_candidate.outputs.windows_signing_backend }}
|
|
REQUIRE_WINDOWS_SIGNING: ${{ needs.dry-run.outputs.require_windows_signing }}
|
|
UNSIGNED_WINDOWS_EXCEPTION: ${{ needs.dry-run.outputs.unsigned_windows_exception }}
|
|
UNSIGNED_WINDOWS_REASON: ${{ needs.dry-run.outputs.unsigned_windows_reason }}
|
|
run: |
|
|
set -euo pipefail
|
|
failed=0
|
|
require_result() {
|
|
local label="$1"
|
|
local actual="$2"
|
|
local expected="$3"
|
|
if [ "$actual" != "$expected" ]; then
|
|
echo "::error::${label} ended as ${actual}; expected ${expected}."
|
|
failed=1
|
|
fi
|
|
}
|
|
|
|
require_result "release preflight" "$PREFLIGHT_RESULT" success
|
|
require_result "stable demo no-mutation verification" "$DEMO_RESULT" success
|
|
if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ -n "${VERSION_INPUT:-}" ]; then
|
|
require_result "exact-SHA release candidate" "$CANDIDATE_RESULT" success
|
|
elif [ "$CANDIDATE_RESULT" != "skipped" ]; then
|
|
echo "::error::Scheduled watchdog candidate result was ${CANDIDATE_RESULT}; expected skipped."
|
|
failed=1
|
|
fi
|
|
|
|
{
|
|
echo "## Definitive dry-run verdict"
|
|
echo
|
|
echo "- Workflow event SHA: \`${GITHUB_SHA}\`"
|
|
echo "- Tested source: \`${TESTED_BRANCH:-unresolved}@${TESTED_SHA:-unresolved}\`"
|
|
echo "- Version: \`${RESOLVED_VERSION:-unresolved}\`"
|
|
echo "- Release preflight: \`${PREFLIGHT_RESULT}\`"
|
|
echo "- Stable demo verification: \`${DEMO_RESULT}\`"
|
|
echo "- Release candidate: \`${CANDIDATE_RESULT}\`"
|
|
echo "- Windows Authenticode required: \`${REQUIRE_WINDOWS_SIGNING}\`"
|
|
echo "- Unsigned Windows exception: \`${UNSIGNED_WINDOWS_EXCEPTION}\`"
|
|
if [ -n "${UNSIGNED_WINDOWS_REASON:-}" ]; then
|
|
echo "- Unsigned Windows reason: ${UNSIGNED_WINDOWS_REASON}"
|
|
fi
|
|
if [ -n "${WINDOWS_SIGNING_BACKEND:-}" ]; then
|
|
echo "- Windows signing backend: \`${WINDOWS_SIGNING_BACKEND}\`"
|
|
fi
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
exit "$failed"
|