mirror of
https://github.com/soxoj/maigret.git
synced 2026-09-10 03:47:40 +02:00
`pyinstaller.yml` only runs on pushes to `main` and `dev`, so `maigret_standalone.exe` exists only on the moving `nightly-*` tags. Every stable release from v0.6.1 to v0.6.5 has zero assets, and winget and Scoop cannot pin a moving tag by hash. - add the `release: types: [published]` trigger, the same one `python-publish.yml` uses. Checkout resolves `refs/tags/<tag>`, so the exe is built from the released code. - guard the two nightly-only steps on `github.event_name == 'push'`, otherwise `NIGHTLY_TAG` becomes `nightly-v0.6.5` and creates a junk tag beside the real one. - add a step attaching the binary to the published release with every `omit*DuringUpdate` set, so the title, body, prerelease and draft state are left alone. - `tests/test_workflows.py`: the `release_step` fixture asserted there was exactly one `release-action` step, which a second one breaks. It now selects a step by the event it is guarded on, and the new step gets its own guards: every release step must be event specific, the published one must set all four omits, and it must target `github.event.release.tag_name` rather than `NIGHTLY_TAG`. Existing releases are not backfilled. A release now costs two PyInstaller runs, one on the tag and one on the push to `main` that follows.
160 lines
6.0 KiB
Python
160 lines
6.0 KiB
Python
"""Guards for the release-publishing GitHub Actions workflow.
|
|
|
|
Regression tests for #2959. The PyInstaller development build used to publish
|
|
itself with ``makeLatest: true`` under a tag named after the branch it was built
|
|
from, which caused three separate problems:
|
|
|
|
1. the Windows dev build held the repository's "Latest release" marker ahead of
|
|
every stable release, and stole it back on every push to ``main``;
|
|
2. the tag never moved, so it kept naming the first build while the attached
|
|
``.exe`` was replaced on every push;
|
|
3. the tags ``main`` / ``dev`` shadowed the branches of the same name, making a
|
|
bare ``main`` refname ambiguous in every clone.
|
|
|
|
These tests read the workflow as data, so they fail if any of the three
|
|
conditions is reintroduced.
|
|
|
|
The workflow also attaches the binary to releases a human publishes, which is a
|
|
second ``release-action`` step with the opposite requirements: it must not touch
|
|
the title, the notes or the prerelease flag that the publisher chose. Both steps
|
|
are covered below, and each must stay guarded on a specific ``github.event_name``
|
|
so neither ever runs on the other's event.
|
|
"""
|
|
|
|
import os
|
|
|
|
import pytest
|
|
|
|
yaml = pytest.importorskip("yaml")
|
|
|
|
WORKFLOW_PATH = os.path.join(
|
|
os.path.dirname(os.path.dirname(os.path.realpath(__file__))),
|
|
".github",
|
|
"workflows",
|
|
"pyinstaller.yml",
|
|
)
|
|
|
|
RELEASE_ACTION = "ncipollo/release-action"
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def workflow():
|
|
with open(WORKFLOW_PATH, encoding="utf-8") as f:
|
|
return yaml.safe_load(f)
|
|
|
|
|
|
def _release_action_steps(workflow):
|
|
steps = workflow["jobs"]["build"]["steps"]
|
|
return [s for s in steps if RELEASE_ACTION in s.get("uses", "")]
|
|
|
|
|
|
def _only_step_for_event(workflow, event):
|
|
matching = [
|
|
s
|
|
for s in _release_action_steps(workflow)
|
|
if f"github.event_name == '{event}'" in s.get("if", "")
|
|
]
|
|
assert len(matching) == 1, (
|
|
f"expected exactly one {RELEASE_ACTION} step guarded on the {event!r} "
|
|
f"event, found {len(matching)}"
|
|
)
|
|
return matching[0]
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def release_step(workflow):
|
|
"""The nightly development release, published on every push to a branch."""
|
|
return _only_step_for_event(workflow, "push")
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def published_release_step(workflow):
|
|
"""The step attaching the binary to a release a human published."""
|
|
return _only_step_for_event(workflow, "release")
|
|
|
|
|
|
def _push_branches(workflow):
|
|
# PyAML resolves the bare `on` key to the boolean True (YAML 1.1 treats it as
|
|
# a truthy literal), so accept either spelling.
|
|
triggers = workflow.get("on", workflow.get(True))
|
|
return triggers["push"]["branches"]
|
|
|
|
|
|
def _resolve(expression, workflow, branch):
|
|
"""Expand the workflow-level env and `github.ref_name` in an expression."""
|
|
for name, value in (workflow.get("env") or {}).items():
|
|
expression = expression.replace("${{ env.%s }}" % name, str(value))
|
|
return expression.replace("${{ github.ref_name }}", branch)
|
|
|
|
|
|
def test_dev_build_is_a_prerelease(release_step):
|
|
# Keeps the build out of /releases/latest and out of the `release: released`
|
|
# event that publishes to PyPI.
|
|
assert str(release_step["with"]["prerelease"]).lower() == "true"
|
|
|
|
|
|
def test_dev_build_never_claims_the_latest_marker(release_step):
|
|
assert str(release_step["with"]["makeLatest"]).lower() == "false"
|
|
|
|
|
|
def test_release_tag_does_not_shadow_a_branch(workflow, release_step):
|
|
tag = release_step["with"]["tag"]
|
|
for branch in _push_branches(workflow):
|
|
resolved = _resolve(tag, workflow, branch)
|
|
assert resolved != branch, (
|
|
f"tag {resolved!r} shadows the {branch!r} branch: a bare "
|
|
f"{branch!r} refname would resolve to the tag in every clone"
|
|
)
|
|
assert resolved, f"tag resolved to an empty string for branch {branch!r}"
|
|
|
|
|
|
def test_release_tag_is_moved_to_the_built_commit(workflow, release_step):
|
|
# release-action only creates a tag when it is missing, and GitHub ignores
|
|
# target_commitish for an existing tag, so an explicit push is what keeps the
|
|
# tag in step with the attached binary.
|
|
tag = release_step["with"]["tag"]
|
|
scripts = [s["run"] for s in workflow["jobs"]["build"]["steps"] if "run" in s]
|
|
moves_tag = any(
|
|
"refs/tags/" in script and "--force" in script for script in scripts
|
|
)
|
|
assert moves_tag, (
|
|
f"no step force-pushes {tag!r}; without it the tag stays pinned to the "
|
|
"first build while the release keeps getting new binaries"
|
|
)
|
|
assert workflow.get("permissions", {}).get("contents") == "write"
|
|
|
|
|
|
def test_every_release_step_is_guarded_by_event(workflow):
|
|
# An unguarded step would run on both events: on a published release it
|
|
# would mint a junk nightly-vX.Y.Z tag, and on a push it would try to
|
|
# update a release that does not exist.
|
|
for step in _release_action_steps(workflow):
|
|
assert "github.event_name" in step.get("if", ""), (
|
|
f"{step.get('name')!r} is not guarded by event and would run on both"
|
|
)
|
|
|
|
|
|
def test_published_release_keeps_the_metadata_it_was_given(published_release_step):
|
|
# Whoever published the release chose its title, notes, prerelease and draft
|
|
# state. release-action replaces all four with its own defaults while
|
|
# updating unless every omit is set, so this step must only add the asset.
|
|
options = published_release_step["with"]
|
|
for key in (
|
|
"omitNameDuringUpdate",
|
|
"omitBodyDuringUpdate",
|
|
"omitPrereleaseDuringUpdate",
|
|
"omitDraftDuringUpdate",
|
|
):
|
|
assert str(options.get(key)).lower() == "true", (
|
|
f"{key} is not set; publishing would overwrite the release notes"
|
|
)
|
|
|
|
|
|
def test_published_release_targets_the_published_tag(published_release_step):
|
|
# NIGHTLY_TAG expands to `nightly-v0.6.6` on a release event, so the step
|
|
# has to name the tag from the event payload instead.
|
|
tag = published_release_step["with"]["tag"]
|
|
assert "github.event.release.tag_name" in tag, (
|
|
f"expected the published tag, got {tag!r}"
|
|
)
|