Merge pull request #135459 from NousResearch/fix/rc5-darwin-baseline
fix(release): stable releases pass signed-package acceptance when no upgrade baseline exists
This commit is contained in:
9
.github/workflows/stable-release.yml
vendored
9
.github/workflows/stable-release.yml
vendored
@@ -279,6 +279,7 @@ jobs:
|
||||
outputs:
|
||||
windows: ${{ steps.plan.outputs.windows }}
|
||||
macos: ${{ steps.plan.outputs.macos }}
|
||||
baseline: ${{ steps.plan.outputs.baseline }}
|
||||
steps:
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7.0.1
|
||||
with:
|
||||
@@ -313,6 +314,7 @@ jobs:
|
||||
outputs:
|
||||
windows: ${{ steps.plan.outputs.windows }}
|
||||
macos: ${{ steps.plan.outputs.macos }}
|
||||
baseline: ${{ steps.plan.outputs.baseline }}
|
||||
steps:
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7.0.1
|
||||
with:
|
||||
@@ -347,6 +349,7 @@ jobs:
|
||||
outputs:
|
||||
windows: ${{ steps.plan.outputs.windows }}
|
||||
macos: ${{ steps.plan.outputs.macos }}
|
||||
baseline: ${{ steps.plan.outputs.baseline }}
|
||||
steps:
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7.0.1
|
||||
with:
|
||||
@@ -412,6 +415,8 @@ jobs:
|
||||
windows-packaged:
|
||||
name: Windows signed-package acceptance
|
||||
needs: transitions-win32
|
||||
# Without a published stable baseline there is no OLD package to upgrade from.
|
||||
if: needs.transitions-win32.outputs.baseline != 'none'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(needs.transitions-win32.outputs.windows) }}
|
||||
@@ -428,6 +433,8 @@ jobs:
|
||||
macos-packaged-arm64:
|
||||
name: macOS arm64 signed-package acceptance
|
||||
needs: transitions-darwin-arm64
|
||||
# Without a published stable baseline there is no OLD package to upgrade from.
|
||||
if: needs.transitions-darwin-arm64.outputs.baseline != 'none'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(needs.transitions-darwin-arm64.outputs.macos) }}
|
||||
@@ -444,6 +451,8 @@ jobs:
|
||||
macos-packaged-x64:
|
||||
name: macOS x64 signed-package acceptance
|
||||
needs: transitions-darwin-x64
|
||||
# Without a published stable baseline there is no OLD package to upgrade from.
|
||||
if: needs.transitions-darwin-x64.outputs.baseline != 'none'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(needs.transitions-darwin-x64.outputs.macos) }}
|
||||
|
||||
@@ -83,6 +83,12 @@ SKIPPED_BY = {
|
||||
}
|
||||
|
||||
|
||||
# Each signed-package upgrade arm and the transitions job that plans it.
|
||||
PACKAGED_BY = {"macos-packaged-arm64": "transitions-darwin-arm64",
|
||||
"macos-packaged-x64": "transitions-darwin-x64",
|
||||
"windows-packaged": "transitions-win32"}
|
||||
|
||||
|
||||
def gate_expectations(required: list[str], *, skip_bundles: bool, skip_tests: bool) -> dict:
|
||||
"""Each gated job's required result under the claim's flags."""
|
||||
active = {flag for flag, on in (("skipBundles", skip_bundles), ("skipTests", skip_tests)) if on}
|
||||
@@ -95,6 +101,13 @@ def require_gate(needs: dict, required: list[str], *, skip_bundles: bool, skip_t
|
||||
if not required or len(set(required)) != len(required):
|
||||
raise ValueError("Invalid required-job list")
|
||||
expected = gate_expectations(required, skip_bundles=skip_bundles, skip_tests=skip_tests)
|
||||
for packaged, planner in PACKAGED_BY.items():
|
||||
# Without a published baseline there is no OLD package, so the planner
|
||||
# skips the upgrade arm. Only that planner's own successful output can excuse it.
|
||||
planned = needs.get(planner, {})
|
||||
if (expected.get(packaged) == "success" and planned.get("result") == "success"
|
||||
and (planned.get("outputs") or {}).get("baseline") == "none"):
|
||||
expected[packaged] = "skipped"
|
||||
failures = [f"{name}={needs.get(name, {}).get('result', 'missing')} (expected {want})"
|
||||
for name, want in expected.items() if needs.get(name, {}).get("result") != want]
|
||||
if failures:
|
||||
@@ -640,14 +653,21 @@ def _stage_transition(env: dict, archive: str, base: str, row: dict) -> dict:
|
||||
"manifest_sha256": hashlib.sha256(file.read_bytes()).hexdigest()}
|
||||
|
||||
|
||||
def _published_baseline(env: dict, base: str) -> dict:
|
||||
def _published_baseline(env: dict, base: str) -> dict | None:
|
||||
"""The previous stable package manifest, or None when no stable release shipped one.
|
||||
|
||||
A release that skipped bundles never writes the pointer, so its absence
|
||||
only means there is no OLD package to upgrade from.
|
||||
"""
|
||||
try:
|
||||
previous = read_manifest(env.get("BASELINE_MANIFEST_URL") or f"{base}/releases/stable/release-candidates.json",
|
||||
expected_origin=base)
|
||||
except urllib.error.HTTPError as error:
|
||||
if error.code == 404:
|
||||
raise ValueError("No published stable package baseline. Supply baseline-manifest for an actual previous stable release; acceptance cannot be skipped.") from error
|
||||
raise
|
||||
if error.code != 404:
|
||||
raise
|
||||
if env.get("BASELINE_MANIFEST_URL"):
|
||||
raise ValueError("The supplied baseline-manifest does not exist") from error
|
||||
return None
|
||||
published = json.loads(output(["gh", "release", "view", previous["tag"], "--repo", env["GITHUB_REPOSITORY"], "--json", "tagName,isDraft,isPrerelease"]))
|
||||
if published["tagName"] != previous["tag"] or published["isDraft"] or published["isPrerelease"]:
|
||||
raise ValueError("Upgrade baseline must be a published stable release")
|
||||
@@ -677,10 +697,17 @@ def transitions(env: dict) -> None:
|
||||
previous = _published_baseline(env, base)
|
||||
receipt_manifest = _receipt_from_env(env, receipt, "RECEIPT", base)
|
||||
matrices = {"windows": {"include": []}, "macos": {"include": []}}
|
||||
if previous is None:
|
||||
# Native smokes still gate this release. Its packages become the
|
||||
# baseline that the next bundle release must upgrade from.
|
||||
validate_receipt(receipt_manifest, receipt, receipt_manifest.get("tag"),
|
||||
receipt_manifest.get("commit"), base, archive=receipt_manifest.get("archive"))
|
||||
emit({**matrices, "baseline": "none"}, env)
|
||||
return
|
||||
for row in plan_receipt_transitions(previous, receipt_manifest, receipt, base):
|
||||
matrices[row["transition"]["platform"]]["include"].append(
|
||||
_stage_transition(env, archive, base, row))
|
||||
emit(matrices, env)
|
||||
emit({**matrices, "baseline": "published"}, env)
|
||||
|
||||
|
||||
# The candidate manifest is written after the smokes (decision 23): the smoke
|
||||
|
||||
@@ -937,6 +937,38 @@ def test_transitions_from_the_bundle_receipt_emit_two_windows_rows(tmp_path, r2_
|
||||
assert macos["include"] == []
|
||||
|
||||
|
||||
def test_missing_baseline_plans_no_upgrade_arm_but_a_missing_supplied_one_blocks(
|
||||
tmp_path, r2_server, https_origin, monkeypatch):
|
||||
from scripts.releases import stable
|
||||
|
||||
url, digest = _staged_receipt(tmp_path, r2_server, https_origin, monkeypatch, "darwin-arm64")
|
||||
env = {**_transitions_env(tmp_path, https_origin.base, "darwin-arm64", url, digest),
|
||||
"BASELINE_MANIFEST_URL": ""}
|
||||
stable.main(["transitions"], env)
|
||||
emitted = dict(line.split("=", 1)
|
||||
for line in (tmp_path / "output").read_text(encoding="utf-8").splitlines())
|
||||
assert emitted["baseline"] == "none"
|
||||
assert json.loads(emitted["macos"]) == json.loads(emitted["windows"]) == {"include": []}
|
||||
env["BASELINE_MANIFEST_URL"] = f"{https_origin.base}/baseline.json"
|
||||
with pytest.raises(ValueError, match="supplied baseline-manifest does not exist"):
|
||||
stable.main(["transitions"], env)
|
||||
|
||||
|
||||
def test_gate_excuses_an_upgrade_arm_only_for_its_planners_no_baseline_output():
|
||||
from scripts.releases.stable import PACKAGED_BY, require_gate
|
||||
|
||||
required = [*PACKAGED_BY, *PACKAGED_BY.values()]
|
||||
needs = {name: {"result": "skipped"} for name in PACKAGED_BY}
|
||||
needs.update({name: {"result": "success", "outputs": {"baseline": "none"}}
|
||||
for name in PACKAGED_BY.values()})
|
||||
require_gate(needs, required, skip_bundles=False, skip_tests=False)
|
||||
for packaged, planner in PACKAGED_BY.items():
|
||||
for planned in ({"result": "success", "outputs": {"baseline": "published"}},
|
||||
{"result": "success"}, {"result": "failure", "outputs": {"baseline": "none"}}):
|
||||
with pytest.raises(ValueError, match=packaged):
|
||||
require_gate({**needs, planner: planned}, required, skip_bundles=False, skip_tests=False)
|
||||
|
||||
|
||||
def test_candidate_manifest_needs_every_call_and_stages_the_archive_manifest(
|
||||
tmp_path, r2_server, https_origin, monkeypatch):
|
||||
from scripts.releases import stable
|
||||
|
||||
@@ -415,6 +415,9 @@ local helper suite. Never store the bootstrap output as a repo channel list.
|
||||
The last successful stable release that shipped bundles records
|
||||
`releases/stable/release-candidates.json` on the configured R2 public origin.
|
||||
A release that skipped bundles does not replace it.
|
||||
When no baseline exists, the release still runs every native smoke but skips
|
||||
the signed-package upgrade arms. A `baseline-manifest` input that does not
|
||||
exist is a blocker.
|
||||
It identifies actual Windows universal MSIX bundles, macOS ZIPs and package
|
||||
provenance. The next run combines those records with its candidate manifest
|
||||
and uses the existing native bundled-update drivers.
|
||||
|
||||
Reference in New Issue
Block a user