From 071fa9045541e959cdb7ae304181836a34d4c419 Mon Sep 17 00:00:00 2001 From: abrichr Date: Wed, 28 Oct 2026 23:03:56 -0400 Subject: [PATCH] fix(ci): set the commit release-package's plan step decides for The plan step ran release_plan.py with --tip "$RELEASE_COMMIT" but never set RELEASE_COMMIT, so under set -u every run past the Packages check failed with "RELEASE_COMMIT: unbound variable" (run 40809031545) and no package could publish. It is now github.sha, the commit the step's checkout holds. A test checks that every shell variable a step in the workflow reads is set by the job, the step, the runner or the script itself. --- .github/workflows/release-package.yml | 2 ++ tests/test_release_plan.py | 39 +++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/.github/workflows/release-package.yml b/.github/workflows/release-package.yml index fc056ffbd..6507cd1bf 100644 --- a/.github/workflows/release-package.yml +++ b/.github/workflows/release-package.yml @@ -114,6 +114,8 @@ jobs: EVENT_NAME: ${{ github.event_name }} PACKAGE: ${{ inputs.package || 'all' }} RESUME: ${{ inputs.resume && 'true' || 'false' }} + # The commit the checkout above holds: main's tip for this run. + RELEASE_COMMIT: ${{ github.sha }} run: | set -euo pipefail args=(--package "$PACKAGE" --tip "$RELEASE_COMMIT" --github-output "$GITHUB_OUTPUT") diff --git a/tests/test_release_plan.py b/tests/test_release_plan.py index 5383560c3..2c927796f 100644 --- a/tests/test_release_plan.py +++ b/tests/test_release_plan.py @@ -10,6 +10,7 @@ from __future__ import annotations import importlib.util +import re from pathlib import Path import pytest @@ -150,3 +151,41 @@ def downloads(job: str) -> set[str]: # It tags only what PyPI serves, at the built (or already tagged) commit. assert "pypi_dist.py compare" in runs and "pypi_dist.py fetch" in runs assert "$GITHUB_SHA" not in runs and "$RELEASE_COMMIT" in runs + + +def test_every_shell_variable_a_step_reads_is_set() -> None: + # The plan step once read $RELEASE_COMMIT without setting it, so every run + # that got past the Packages check failed with "unbound variable". + jobs = yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))["jobs"] + builtin = { + "GITHUB_OUTPUT", + "GITHUB_REPOSITORY", + "GITHUB_ENV", + "GITHUB_SHA", + "GITHUB_STEP_SUMMARY", + "GITHUB_WORKSPACE", + "GITHUB_REF", + "GITHUB_EVENT_NAME", + "RUNNER_TEMP", + } + for name, job in jobs.items(): + for step in job["steps"]: + run = step.get("run", "") + assigned = set( + re.findall( + r"^\s*(?:export\s+|local\s+)?([A-Za-z_][A-Za-z0-9_]*)=", run, re.M + ) + ) + assigned |= set(re.findall(r"\bfor\s+([A-Za-z_][A-Za-z0-9_]*)\s+in\b", run)) + assigned |= set( + re.findall(r"\bread\s+(?:-r\s+)?([A-Za-z_][A-Za-z0-9_]*)", run) + ) + defined = ( + set(job.get("env") or {}) + | set(step.get("env") or {}) + | builtin + | assigned + ) + used = set(re.findall(r"\$\{?([A-Z][A-Z0-9_]*)\b", run)) + missing = used - defined + assert not missing, f"{name} / {step.get('name')}: {sorted(missing)}"