`release.py release` gains two flags. They can be used together. --skip-bundles ships only the claim, the GitHub release, the final tag and the Docker image. No desktop, Termux or PM bundle job runs. The final tag records candidateManifestSha256: null. Publication moves only the Docker stable/latest aliases. The R2 stable head, feeds, APT, the downloads page, the signed-package baseline and the Store stay on the previous bundle release. --skip-tests builds, signs and publishes every artifact and runs no test job: source CI, Nix, PM bundle check, Termux, Windows live, install/update E2E, bootstrap identity, native smokes, upgrade acceptance, tests/docker and the in-build vitest step. The candidate manifest records each smoke as skipped, never as passed. The flags live in the claim message (skipBundles, skipTests), next to autopublish. They are not workflow inputs, so a rerun cannot change them. admit emits them, and every job condition and gate reads them. stable.validate_claim and stable.validate_final are now the one shape check for stable.py and the sequencer. The gates stay strict. SKIPPED_BY in stable.py maps each job to the flags that remove it. `gate` requires those jobs to report skipped and every other gated job to report success. A job that ran although a flag removes it blocks the release. A release that skipped bundles never moves the R2 stable head. Two readers depended on that head: - The next version was derived from it, so the next cut would reuse the version. It now takes the newer of the R2 head and the newest published non-prerelease GitHub release with a vX.Y.Z tag. Bare v* tags do not count, because those refs are not protected yet. - The sequencer used it to decide which published releases still need their publication pass, so a bundle-less release would re-advance every 15 minutes. The head is now the newer of the R2 head and the published release whose final tag binds the Docker stable alias digest. `release` also refuses a cut when its next version already has a final tag. That closes the window between the final tag and the public release, where the published identity still names the old version. Tests: 42 release test files, 546 passed. Three tests fail on this Windows host, and they fail the same way on a clean HEAD worktree: - test_stable_release_graph::test_docker_recovery_refuses_to_replace_a_divergent_version_tag - test_release_artifacts::test_windows_metadata_is_read_from_package_and_stale_stamp_is_rejected - test_tag_builds_summary::test_admitted_failure_publishes_tag_info_without_promoting_channel[True] Not verified: no real Stable Release dispatch ran with either flag, and actionlint is not installed on this host. The workflow changes are checked by the graph tests and by running the phase-result step script.
96 lines
3.5 KiB
Python
96 lines
3.5 KiB
Python
"""The desktop-bundled-release `jobs` input: one parser for the job groups.
|
|
|
|
The workflow's admission step is the only place that reads the raw input; it
|
|
emits one `<group>=true|false` output per group and every job gates on its own
|
|
group output. Channel callers use `selects_all` where they refused
|
|
`TERMUX_ONLY=true` before, and the stable phase result uses `phase_jobs` to
|
|
judge only the selected groups.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
JOB_GROUPS = ("darwin-arm64", "darwin-x64", "win32-arm64", "win32-x64",
|
|
"win32-bundle", "linux-x64", "linux-arm64", "termux")
|
|
ALL_JOBS = ",".join(JOB_GROUPS)
|
|
|
|
# The workflow jobs each group runs, in the names desktop-bundled-release.yml
|
|
# declares. `phase_jobs` turns a selection into the list the stable phase
|
|
# result requires.
|
|
GROUP_JOBS = {
|
|
"darwin-arm64": ("build-darwin-arm64",),
|
|
"darwin-x64": ("build-darwin-x64",),
|
|
"win32-arm64": ("build-win32-arm64",),
|
|
"win32-x64": ("build-win32-x64",),
|
|
"win32-bundle": ("assemble-win32-bundle",),
|
|
"termux": ("termux-deb",),
|
|
}
|
|
# The native smoke each group runs after its build. A claim that skipped
|
|
# tests runs none of them.
|
|
GROUP_SMOKES = {
|
|
"darwin-arm64": ("smoke-darwin-arm64",),
|
|
"darwin-x64": ("smoke-darwin-x64",),
|
|
"win32-arm64": ("smoke-win32-arm64",),
|
|
"win32-x64": ("smoke-win32-x64",),
|
|
}
|
|
|
|
|
|
def parse_jobs(raw: str | None) -> dict[str, bool]:
|
|
"""Select job groups from a comma-separated list. ``None`` is the default.
|
|
|
|
An unknown name, a duplicate, an empty entry and an explicitly empty list
|
|
are refused; admission then fails before any build starts.
|
|
"""
|
|
if raw is None:
|
|
return {group: True for group in JOB_GROUPS}
|
|
names = [name.strip() for name in raw.split(",")]
|
|
if not any(names):
|
|
raise ValueError(f"jobs must name at least one group: {ALL_JOBS}")
|
|
selected = {group: False for group in JOB_GROUPS}
|
|
for name in names:
|
|
if not name:
|
|
raise ValueError("jobs contains an empty group name")
|
|
if name not in selected:
|
|
raise ValueError(f"unknown job group: {name}")
|
|
if selected[name]:
|
|
raise ValueError(f"duplicate job group: {name}")
|
|
selected[name] = True
|
|
return selected
|
|
|
|
|
|
def selects_all(raw: str | None) -> bool:
|
|
"""True only when the input selects every group."""
|
|
return all(parse_jobs(raw).values())
|
|
|
|
|
|
def phase_jobs(selected: dict[str, bool], phase: str, *, skip_tests: bool = False) -> list[str]:
|
|
"""Jobs the stable phase result judges: an unselected group never fails it.
|
|
|
|
B4 moved candidate-manifest into stable-release.yml, so the desktop
|
|
workflow no longer owns it and the phase result never names it. A claim
|
|
that skipped tests runs no smoke, so the smokes are not judged.
|
|
"""
|
|
required = ["validate"]
|
|
if phase == "publish":
|
|
return required + ["stable-publish", "stable-store"]
|
|
if phase != "candidate":
|
|
raise ValueError(f"Unknown release phase: {phase}")
|
|
for group, jobs in GROUP_JOBS.items():
|
|
if selected.get(group):
|
|
required.extend(jobs)
|
|
if not skip_tests:
|
|
required.extend(GROUP_SMOKES.get(group, ()))
|
|
return required
|
|
|
|
|
|
def main() -> None:
|
|
"""Emit one `<group>=true|false` line per group, and `all-jobs`, for `$GITHUB_OUTPUT`."""
|
|
selected = parse_jobs(os.environ.get("JOBS"))
|
|
for group in JOB_GROUPS:
|
|
print(f"{group}={'true' if selected[group] else 'false'}")
|
|
print(f"all-jobs={'true' if all(selected.values()) else 'false'}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|