mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 04:33:17 +00:00
Integrate reviewed exact-source release watchdog
Preserve the fixed no-publication envelope, component verdicts and reviewed Delivery commit identities. Change-source: pulse-maintainer
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
name: Release Dry Run
|
||||
run-name: Release Dry Run v${{ inputs.version || 'scheduled' }}
|
||||
run-name: ${{ inputs.watchdog && format('Release Watchdog at {0}', inputs.expected_workflow_sha) || format('Release Dry Run v{0}', inputs.version || 'scheduled') }}
|
||||
|
||||
on:
|
||||
# Weekly drift watchdog: starts on main every Tuesday 07:00 UTC and selects the governed
|
||||
@@ -7,12 +7,21 @@ on:
|
||||
# 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.
|
||||
# latest stable tag preceding VERSION. The fixed on-demand watchdog uses the
|
||||
# same envelope; ordinary manual rehearsals still supply rollback explicitly.
|
||||
schedule:
|
||||
- cron: '0 7 * * 2'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
watchdog:
|
||||
description: 'Run the scheduled no-publication watchdog now, from exact reviewed main'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
expected_workflow_sha:
|
||||
description: 'Required exact main workflow SHA for watchdog dispatch; forbidden for candidate rehearsals'
|
||||
required: false
|
||||
type: string
|
||||
version:
|
||||
description: 'Optional version under rehearsal (e.g. 6.0.0-rc.2 or 6.0.0)'
|
||||
required: false
|
||||
@@ -23,7 +32,9 @@ on:
|
||||
type: string
|
||||
rollback_version:
|
||||
description: 'Required rollback stable version to rehearse (for example 6.0.4 or v6.0.4)'
|
||||
required: true
|
||||
# Candidate rehearsals still enforce this before checkout. Only the
|
||||
# fixed watchdog envelope derives the preceding stable rollback.
|
||||
required: false
|
||||
type: string
|
||||
ga_date:
|
||||
description: 'Stable v6.0.0 rehearsal only: planned GA publish date (YYYY-MM-DD)'
|
||||
@@ -70,7 +81,7 @@ permissions:
|
||||
jobs:
|
||||
build_release_candidate:
|
||||
name: Build Immutable Release Candidate
|
||||
if: ${{ inputs.version != '' }}
|
||||
if: ${{ inputs.watchdog != true && inputs.version != '' }}
|
||||
permissions:
|
||||
actions: write
|
||||
attestations: write
|
||||
@@ -115,11 +126,67 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Validate release ref
|
||||
id: mode
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
WATCHDOG_INPUT: ${{ inputs.watchdog }}
|
||||
EXPECTED_WORKFLOW_SHA_INPUT: ${{ inputs.expected_workflow_sha }}
|
||||
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 }}
|
||||
NOTE_INPUT: ${{ inputs.note }}
|
||||
MOBILE_RELEASE_DECISION: ${{ inputs.mobile_release_decision }}
|
||||
MOBILE_RELEASE_EVIDENCE: ${{ inputs.mobile_release_evidence }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
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
|
||||
WATCHDOG=false
|
||||
case "${EVENT_NAME}:${WATCHDOG_INPUT:-false}" in
|
||||
schedule:false) WATCHDOG=true ;;
|
||||
workflow_dispatch:true)
|
||||
if [ "${GITHUB_REF}" != "refs/heads/main" ] || \
|
||||
[[ ! "${EXPECTED_WORKFLOW_SHA_INPUT:-}" =~ ^[0-9a-f]{40}$ ]] || \
|
||||
[ "${EXPECTED_WORKFLOW_SHA_INPUT}" != "${GITHUB_SHA}" ]; then
|
||||
echo "::error::Watchdog dispatch requires the exact reviewed main workflow SHA."
|
||||
exit 1
|
||||
fi
|
||||
WATCHDOG=true
|
||||
;;
|
||||
workflow_dispatch:false)
|
||||
if [ -n "${EXPECTED_WORKFLOW_SHA_INPUT:-}" ] || [ -z "${ROLLBACK_VERSION_INPUT:-}" ]; then
|
||||
echo "::error::Candidate rehearsal requires explicit rollback and no watchdog SHA."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*) echo "::error::Unsupported rehearsal event or watchdog mode."; exit 1 ;;
|
||||
esac
|
||||
if [ "${WATCHDOG}" = "true" ]; then
|
||||
for name in VERSION_INPUT PROMOTED_FROM_TAG_INPUT ROLLBACK_VERSION_INPUT \
|
||||
GA_DATE_INPUT V5_EOS_DATE_INPUT HOTFIX_REASON_INPUT \
|
||||
UNSIGNED_WINDOWS_REASON_INPUT NOTE_INPUT \
|
||||
MOBILE_RELEASE_DECISION MOBILE_RELEASE_EVIDENCE; do
|
||||
if [ -n "${!name:-}" ]; then
|
||||
echo "::error::Watchdog refuses candidate input ${name}."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
for name in HOTFIX_EXCEPTION_INPUT UNSIGNED_WINDOWS_EXCEPTION_INPUT; do
|
||||
if [ "${!name:-false}" != "false" ]; then
|
||||
echo "::error::Watchdog refuses exception input ${name}."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
echo "watchdog=${WATCHDOG}" >> "$GITHUB_OUTPUT"
|
||||
echo "[OK] Dry run executing on branch ${GITHUB_REF_NAME}"
|
||||
|
||||
- name: Checkout repository
|
||||
@@ -145,11 +212,16 @@ jobs:
|
||||
id: source
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
WATCHDOG_MODE: ${{ steps.mode.outputs.watchdog }}
|
||||
REQUIRED_BRANCH: ${{ steps.branch_policy.outputs.required_branch }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TESTED_BRANCH="${GITHUB_REF_NAME}"
|
||||
if [ "${EVENT_NAME}" = "schedule" ]; then
|
||||
if [ "${WATCHDOG_MODE}" = "true" ]; then
|
||||
if [ "${EVENT_NAME}" != "schedule" ] && [ "${EVENT_NAME}" != "workflow_dispatch" ]; then
|
||||
echo "::error::Unsupported watchdog event."
|
||||
exit 1
|
||||
fi
|
||||
# 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}"
|
||||
@@ -183,6 +255,7 @@ jobs:
|
||||
id: rehearsal
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
WATCHDOG_MODE: ${{ steps.mode.outputs.watchdog }}
|
||||
VERSION_INPUT: ${{ inputs.version }}
|
||||
PROMOTED_FROM_TAG_INPUT: ${{ inputs.promoted_from_tag }}
|
||||
ROLLBACK_VERSION_INPUT: ${{ inputs.rollback_version }}
|
||||
@@ -242,10 +315,9 @@ jobs:
|
||||
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.
|
||||
if [ "${WATCHDOG_MODE}" = "true" ] && [ -z "${ROLLBACK_VERSION_INPUT:-}" ]; then
|
||||
# Both admitted watchdog paths have no candidate inputs. Ordinary
|
||||
# candidate rehearsals retain their explicit-rollback requirement.
|
||||
HELPER_ARGS+=(--derive-rollback-latest-stable)
|
||||
echo "[OK] Scheduled rehearsal: deriving rollback target from the latest preceding stable tag"
|
||||
fi
|
||||
@@ -266,6 +338,7 @@ jobs:
|
||||
id: mobile_release
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
WATCHDOG_MODE: ${{ steps.mode.outputs.watchdog }}
|
||||
MOBILE_RELEASE_DECISION: ${{ inputs.mobile_release_decision }}
|
||||
MOBILE_RELEASE_EVIDENCE: ${{ inputs.mobile_release_evidence }}
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.rehearsal.outputs.version }}
|
||||
@@ -273,7 +346,7 @@ jobs:
|
||||
set -euo pipefail
|
||||
DECISION="${MOBILE_RELEASE_DECISION:-}"
|
||||
EVIDENCE="${MOBILE_RELEASE_EVIDENCE:-}"
|
||||
if [ -z "${DECISION}" ] && [ "${EVENT_NAME}" = "schedule" ]; then
|
||||
if [ -z "${DECISION}" ] && [ "${WATCHDOG_MODE}" = "true" ]; then
|
||||
DECISION="no-mobile-impact"
|
||||
EVIDENCE="Scheduled release dry-run watchdog; no mobile release packet is being dispatched."
|
||||
fi
|
||||
|
||||
@@ -131,11 +131,22 @@ product checks use the selected checkout; a missing branch or policy mismatch
|
||||
fails closed rather than falling back to main.
|
||||
|
||||
Manual dispatch retains its governed event branch and exact event SHA, VERSION
|
||||
equality and explicit rollback requirement. Only scheduled runs may derive the
|
||||
preceding stable rollback target. Summaries distinguish workflow event revision
|
||||
equality and explicit rollback requirement. The one additional dispatch mode
|
||||
is a fixed no-publication watchdog: `watchdog=true` plus the exact reviewed
|
||||
main `expected_workflow_sha`, with no candidate, exception, mobile or note
|
||||
inputs. It checks the main ref and event SHA before checkout, then uses the
|
||||
same one-fetch governed-source selection and preceding stable rollback
|
||||
derivation as the schedule. A moving control head fails closed. Ordinary
|
||||
candidate dispatches cannot use the watchdog SHA or omit explicit rollback.
|
||||
Summaries distinguish workflow event revision
|
||||
from tested branch and revision; unresolved selection is not test evidence.
|
||||
This watchdog is not an admitted release candidate and cannot qualify or
|
||||
replace the fixed release packet.
|
||||
replace the fixed release packet. Its distinct `Release Watchdog at <sha>`
|
||||
title cannot satisfy an exact-version candidate rehearsal. The candidate-build
|
||||
job is always skipped in watchdog mode; actual preflight, demo resolver,
|
||||
no-mutation verification and definitive verdict must all succeed. Delivery's
|
||||
reviewed dispatcher fixes repository, workflow, main and both inputs, retains
|
||||
intent before POST and never replays a refused or uncertain watchdog attempt.
|
||||
|
||||
Verification: `rehearsal_source_test.py` executes the source-selection shell
|
||||
against local Git fixtures. `release_promotion_policy_test.py` pins workflow
|
||||
@@ -3869,13 +3880,14 @@ for stable-versus-prerelease metadata validation shared by `.github/workflows/re
|
||||
and `.github/workflows/create-release.yml`. Promotion rollback targets, promoted
|
||||
prerelease lineage, soak checks, and GA/v5 notice metadata may not drift between those
|
||||
two workflows through duplicated inline shell validation.
|
||||
One scoped exception keeps the weekly drift watchdog viable: scheduled
|
||||
One scoped exception keeps the drift watchdog viable: scheduled
|
||||
`release-dry-run.yml` runs carry no `workflow_dispatch` inputs (GitHub does
|
||||
not apply input defaults to `schedule` events), so the rehearsal step passes
|
||||
`--derive-rollback-latest-stable` and the resolver fills the empty
|
||||
`rollback_version` with the latest stable repository tag preceding the
|
||||
rehearsal version. The derivation flag is gated on the `schedule` event in
|
||||
the workflow; manual rehearsal dispatches and real promotions must still
|
||||
rehearsal version. The derivation flag is gated on admitted watchdog mode
|
||||
(schedule or the exact-main, candidate-input-free dispatch envelope described
|
||||
above); ordinary candidate rehearsal dispatches and real promotions must still
|
||||
supply an explicit stable `rollback_version`, and the resolver still fails
|
||||
closed when the input is empty and the flag is absent.
|
||||
`scripts/release_control/validate_artifact_release_line.py` is the canonical
|
||||
|
||||
@@ -2415,6 +2415,35 @@ func TestUpdateDemoResolverChecksOutOutputHelperBeforeUse(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseWatchdogIsControlBoundAndCannotBuildCandidate(t *testing.T) {
|
||||
workflow, err := os.ReadFile(repoFile(".github", "workflows", "release-dry-run.yml"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
text := string(workflow)
|
||||
for _, required := range []string{
|
||||
"Release Watchdog at {0}",
|
||||
"if: ${{ inputs.watchdog != true && inputs.version != '' }}",
|
||||
`[ "${EXPECTED_WORKFLOW_SHA_INPUT}" != "${GITHUB_SHA}" ]`,
|
||||
`[ "${GITHUB_REF}" != "refs/heads/main" ]`,
|
||||
`Candidate rehearsal requires explicit rollback and no watchdog SHA.`,
|
||||
`Watchdog refuses candidate input ${name}.`,
|
||||
`Watchdog refuses exception input ${name}.`,
|
||||
"WATCHDOG_MODE: ${{ steps.mode.outputs.watchdog }}",
|
||||
"verify_only: true",
|
||||
`require_result "stable demo no-mutation verification" "$DEMO_RESULT" success`,
|
||||
} {
|
||||
if !strings.Contains(text, required) {
|
||||
t.Fatalf("fixed release watchdog missing safety contract: %s", required)
|
||||
}
|
||||
}
|
||||
guard := strings.Index(text, "- name: Validate release ref")
|
||||
checkout := strings.Index(text, "- name: Checkout repository")
|
||||
if guard < 0 || checkout <= guard {
|
||||
t.Fatal("watchdog control identity and envelope must be checked before checkout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDemoMutationAndRecoverySharePhysicalTargetLock(t *testing.T) {
|
||||
updateBytes, err := os.ReadFile(repoFile(".github", "workflows", "update-demo-server.yml"))
|
||||
if err != nil {
|
||||
|
||||
@@ -64,9 +64,10 @@ class RehearsalSourceTest(unittest.TestCase):
|
||||
self.git("add", ".")
|
||||
self.git("commit", "-m", "fixture")
|
||||
|
||||
def select(self, event="schedule", branch="main", sha=None, required="release/v6.4"):
|
||||
def select(self, event="schedule", branch="main", sha=None, required="release/v6.4", watchdog=None):
|
||||
env = dict(self.env, EVENT_NAME=event, GITHUB_REF_NAME=branch,
|
||||
GITHUB_SHA=sha or self.main, REQUIRED_BRANCH=required,
|
||||
WATCHDOG_MODE=watchdog or ("true" if event == "schedule" else "false"),
|
||||
GITHUB_OUTPUT=str(self.output))
|
||||
return subprocess.run(
|
||||
["bash", "-euo", "pipefail", "-c", step("Select exact rehearsal source")],
|
||||
@@ -83,6 +84,69 @@ class RehearsalSourceTest(unittest.TestCase):
|
||||
self.assertIn(self.main, result.stdout)
|
||||
self.assertNotEqual(self.main, self.release)
|
||||
|
||||
def mode(self, **overrides):
|
||||
env = dict(self.env, EVENT_NAME="workflow_dispatch", WATCHDOG_INPUT="true",
|
||||
EXPECTED_WORKFLOW_SHA_INPUT=self.main, GITHUB_SHA=self.main,
|
||||
GITHUB_REF="refs/heads/main", GITHUB_REF_NAME="main",
|
||||
GITHUB_OUTPUT=str(self.output))
|
||||
env.update(overrides)
|
||||
return subprocess.run(["bash", "-euo", "pipefail", "-c", step("Validate release ref")],
|
||||
cwd=self.repo, env=env, text=True, capture_output=True)
|
||||
|
||||
def test_fixed_watchdog_dispatch_selects_same_source_as_schedule(self):
|
||||
admitted = self.mode()
|
||||
self.assertEqual(admitted.returncode, 0, admitted.stderr)
|
||||
self.assertEqual(self.output.read_text(), "watchdog=true\n")
|
||||
result = self.select("workflow_dispatch", watchdog="true")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(self.git("rev-parse", "HEAD"), self.release)
|
||||
self.assertIn(self.main, result.stdout)
|
||||
self.assertIn("tested_sha=" + self.release, self.output.read_text())
|
||||
|
||||
def test_watchdog_rejects_moved_control_wrong_ref_and_unknown_event_before_selection(self):
|
||||
for overrides in (
|
||||
{"EXPECTED_WORKFLOW_SHA_INPUT": "a" * 40},
|
||||
{"EXPECTED_WORKFLOW_SHA_INPUT": "not-a-sha"},
|
||||
{"EXPECTED_WORKFLOW_SHA_INPUT": ""},
|
||||
{"GITHUB_REF": "refs/heads/release/v6.4"},
|
||||
{"GITHUB_REF": "refs/tags/v6.4.5"},
|
||||
{"EVENT_NAME": "push"}, {"WATCHDOG_INPUT": "yes"},
|
||||
):
|
||||
with self.subTest(overrides=overrides):
|
||||
result = self.mode(**overrides)
|
||||
self.assertNotEqual(result.returncode, 0, result.stdout)
|
||||
self.assertFalse(self.output.exists())
|
||||
self.assertEqual(self.git("rev-parse", "HEAD"), self.main)
|
||||
|
||||
def test_watchdog_rejects_every_candidate_and_exception_input(self):
|
||||
for name in (
|
||||
"VERSION_INPUT", "PROMOTED_FROM_TAG_INPUT", "ROLLBACK_VERSION_INPUT",
|
||||
"GA_DATE_INPUT", "V5_EOS_DATE_INPUT", "HOTFIX_REASON_INPUT",
|
||||
"UNSIGNED_WINDOWS_REASON_INPUT", "NOTE_INPUT", "MOBILE_RELEASE_DECISION",
|
||||
"MOBILE_RELEASE_EVIDENCE", "HOTFIX_EXCEPTION_INPUT", "UNSIGNED_WINDOWS_EXCEPTION_INPUT",
|
||||
):
|
||||
with self.subTest(name=name):
|
||||
self.assertNotEqual(self.mode(**{name: "true"}).returncode, 0)
|
||||
self.assertFalse(self.output.exists())
|
||||
self.assertEqual(self.mode(HOTFIX_EXCEPTION_INPUT="false",
|
||||
UNSIGNED_WINDOWS_EXCEPTION_INPUT="false").returncode, 0)
|
||||
|
||||
def test_schedule_and_manual_candidate_envelopes_remain_separate(self):
|
||||
result = self.mode(EVENT_NAME="schedule", WATCHDOG_INPUT="", EXPECTED_WORKFLOW_SHA_INPUT="")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(self.output.read_text(), "watchdog=true\n")
|
||||
self.output.unlink()
|
||||
for overrides in ({}, {"ROLLBACK_VERSION_INPUT": "v6.4.1", "EXPECTED_WORKFLOW_SHA_INPUT": self.main}):
|
||||
with self.subTest(overrides=overrides):
|
||||
env = {"WATCHDOG_INPUT": "false", "EXPECTED_WORKFLOW_SHA_INPUT": ""}
|
||||
env.update(overrides)
|
||||
self.assertNotEqual(self.mode(**env).returncode, 0)
|
||||
self.assertFalse(self.output.exists())
|
||||
result = self.mode(WATCHDOG_INPUT="false", EXPECTED_WORKFLOW_SHA_INPUT="",
|
||||
ROLLBACK_VERSION_INPUT="v6.4.1")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(self.output.read_text(), "watchdog=false\n")
|
||||
|
||||
def test_manual_wrong_branch_rejected_without_switching(self):
|
||||
result = self.select("workflow_dispatch")
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
@@ -104,6 +168,17 @@ class RehearsalSourceTest(unittest.TestCase):
|
||||
self.assertEqual(self.git("rev-parse", "HEAD"), self.main)
|
||||
self.assertFalse(self.output.exists())
|
||||
|
||||
def test_explicit_watchdog_missing_branch_and_policy_drift_fail_closed(self):
|
||||
self.assertNotEqual(self.select("workflow_dispatch", watchdog="true",
|
||||
required="release/missing").returncode, 0)
|
||||
self.assertFalse(self.output.exists())
|
||||
self.git("checkout", "release/v6.4")
|
||||
self.version("6.6.0-beta.1")
|
||||
self.git("checkout", "main")
|
||||
self.assertNotEqual(self.select("workflow_dispatch", watchdog="true").returncode, 0)
|
||||
self.assertEqual(self.git("rev-parse", "HEAD"), self.main)
|
||||
self.assertFalse(self.output.exists())
|
||||
|
||||
def test_selected_version_must_still_belong_to_governed_branch(self):
|
||||
self.git("checkout", "release/v6.4")
|
||||
self.version("6.6.0-beta.1")
|
||||
@@ -119,7 +194,7 @@ class RehearsalSourceTest(unittest.TestCase):
|
||||
metadata = step("Resolve rehearsal metadata")
|
||||
self.assertIn('if [ "${TESTED_BRANCH}" != "$REQUIRED_BRANCH" ]; then', metadata)
|
||||
self.assertIn('if [ "$FILE_VERSION" != "$VERSION" ]; then', metadata)
|
||||
self.assertIn('if [ "${EVENT_NAME}" = "schedule" ] && [ -z "${ROLLBACK_VERSION_INPUT:-}" ]; then', metadata)
|
||||
self.assertIn('if [ "${WATCHDOG_MODE}" = "true" ] && [ -z "${ROLLBACK_VERSION_INPUT:-}" ]; then', metadata)
|
||||
self.assertIn('--derive-rollback-latest-stable', metadata)
|
||||
workflow = WORKFLOW.read_text()
|
||||
self.assertIn('TESTED_SHA: ${{ needs.dry-run.outputs.tested_sha }}', workflow)
|
||||
|
||||
@@ -408,6 +408,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
||||
dry_run = jobs["dry-run"]
|
||||
steps = dry_run["steps"]
|
||||
names = [step["name"] for step in steps]
|
||||
self.assertLess(names.index("Validate release ref"), names.index("Checkout repository"))
|
||||
self.assertLess(names.index("Resolve required release branch"),
|
||||
names.index("Select exact rehearsal source"))
|
||||
self.assertLess(names.index("Select exact rehearsal source"),
|
||||
@@ -415,6 +416,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
||||
source = next(step for step in steps if step.get("id") == "source")
|
||||
self.assertEqual(source["env"]["REQUIRED_BRANCH"],
|
||||
"${{ steps.branch_policy.outputs.required_branch }}")
|
||||
self.assertEqual(source["env"]["WATCHDOG_MODE"], "${{ steps.mode.outputs.watchdog }}")
|
||||
self.assertIn('git checkout --detach "${TESTED_SHA}"', source["run"])
|
||||
self.assertIn('"${TESTED_SHA}" != "${GITHUB_SHA}"', source["run"])
|
||||
metadata = next(step for step in steps if step.get("id") == "rehearsal")
|
||||
@@ -424,9 +426,13 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
||||
metadata["run"])
|
||||
self.assertIn('if [ "$FILE_VERSION" != "$VERSION" ]; then', metadata["run"])
|
||||
self.assertIn(
|
||||
'if [ "${EVENT_NAME}" = "schedule" ] && [ -z "${ROLLBACK_VERSION_INPUT:-}" ]; then',
|
||||
'if [ "${WATCHDOG_MODE}" = "true" ] && [ -z "${ROLLBACK_VERSION_INPUT:-}" ]; then',
|
||||
metadata["run"],
|
||||
)
|
||||
self.assertEqual(metadata["env"]["WATCHDOG_MODE"], "${{ steps.mode.outputs.watchdog }}")
|
||||
self.assertEqual(jobs["build_release_candidate"]["if"],
|
||||
"${{ inputs.watchdog != true && inputs.version != '' }}")
|
||||
self.assertIn("Release Watchdog at {0}", yaml.safe_load(workflow)["run-name"])
|
||||
for key in ("tested_sha", "tested_branch"):
|
||||
self.assertEqual(dry_run["outputs"][key],
|
||||
"${{ steps.source.outputs." + key + " }}")
|
||||
@@ -1707,7 +1713,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
||||
self.assertIn(promotion_metadata_envelope(), normalize_ws(template))
|
||||
self.assertIn("rc-to-ga-rehearsal-summary", workflow)
|
||||
self.assertIn("build_release_candidate:", workflow)
|
||||
self.assertIn("if: ${{ inputs.version != '' }}", workflow)
|
||||
self.assertIn("if: ${{ inputs.watchdog != true && inputs.version != '' }}", workflow)
|
||||
self.assertIn("require_macos_signing: true", workflow)
|
||||
self.assertIn(
|
||||
"require_windows_signing: false",
|
||||
@@ -2194,12 +2200,19 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
||||
dry_run_workflow = read(".github/workflows/release-dry-run.yml")
|
||||
self.assertIn("Required rollback stable version to rehearse", dry_run_workflow)
|
||||
self.assertIn("rollback_version:\n description: 'Required rollback stable version to rehearse", dry_run_workflow)
|
||||
self.assertIn("required: true", dry_run_workflow)
|
||||
# Scheduled watchdog runs carry no dispatch inputs, so the rehearsal
|
||||
# step must derive the rollback target; the derive flag stays gated on
|
||||
# the schedule event so manual dispatches keep explicit rollback.
|
||||
# The fixed watchdog has no rollback override, so its dispatch schema
|
||||
# must permit omission. Ordinary manual rehearsals still require an
|
||||
# explicit rollback in the pre-checkout admission shell.
|
||||
dry_run = yaml.safe_load(dry_run_workflow)
|
||||
inputs = dry_run.get("on", dry_run.get(True))["workflow_dispatch"]["inputs"]
|
||||
self.assertIs(inputs["rollback_version"]["required"], False)
|
||||
self.assertIn('workflow_dispatch:false)', dry_run_workflow)
|
||||
self.assertIn('[ -z "${ROLLBACK_VERSION_INPUT:-}" ]', dry_run_workflow)
|
||||
self.assertIn('Candidate rehearsal requires explicit rollback and no watchdog SHA.', dry_run_workflow)
|
||||
# Both admitted watchdog modes derive rollback; a candidate rehearsal
|
||||
# can never reach that derivation, including when the input is absent.
|
||||
self.assertIn(
|
||||
'if [ "${EVENT_NAME}" = "schedule" ] && [ -z "${ROLLBACK_VERSION_INPUT:-}" ]; then',
|
||||
'if [ "${WATCHDOG_MODE}" = "true" ] && [ -z "${ROLLBACK_VERSION_INPUT:-}" ]; then',
|
||||
dry_run_workflow,
|
||||
)
|
||||
self.assertIn("--derive-rollback-latest-stable", dry_run_workflow)
|
||||
|
||||
Reference in New Issue
Block a user