"""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. """ 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) @pytest.fixture(scope="module") def release_step(workflow): steps = workflow["jobs"]["build"]["steps"] matching = [s for s in steps if RELEASE_ACTION in s.get("uses", "")] assert len(matching) == 1, f"expected exactly one {RELEASE_ACTION} step" return matching[0] 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"