From 603045e42d18a53f5a6f88b34dfd2c4497a34390 Mon Sep 17 00:00:00 2001 From: FredApps <153173854+FredApps@users.noreply.github.com> Date: Wed, 11 Nov 2026 22:46:06 +0100 Subject: [PATCH] release.yml: a dry-run input builds and checks without releasing (#208) * release.yml: a dry-run input builds and checks without releasing For a nightly run on main that finds release-only breakage before release day. On 11 Nov 2026 gridcraft's riscv64 build, materialcraft's private-repo FreeBSD jobs, wordcraft's Print job and the riscv64 apt mirrors each surfaced only during the release, one fix PR and release run at a time. With dry-run: no job enters the release environment (no signing secrets; the environment admits only the release branch), the macOS job and the draft are skipped, and the Flatpak OCI layouts are not pushed to GHCR or Docker Hub. * tests: the Flatpak OCI push and the release environment also stay out of a dry run --- .github/workflows/release.yml | 25 +++++++++++++------- tests/test_release_dry_run.py | 39 +++++++++++++++++++++++++++++++ tests/test_release_flatpak_oci.py | 4 ++-- tests/test_sign_linux.py | 3 ++- 4 files changed, 60 insertions(+), 11 deletions(-) create mode 100644 tests/test_release_dry_run.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c2e7dba..6fb468a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -84,6 +84,14 @@ on: type: boolean required: false default: false + dry-run: + description: >- + Build and check without releasing: no signing, no `release` environment, no macOS (its + larger runner is paid for), no Flatpak OCI push and no draft. For a nightly run on main + that finds release-only breakage (riscv64, Flatpak, FreeBSD, packaging) before release day. + type: boolean + required: false + default: false secrets: APPLE_CERTIFICATE: @@ -214,9 +222,10 @@ jobs: macos: timeout-minutes: 90 name: macOS universal + if: ${{ !inputs.dry-run }} needs: [version] runs-on: macos-15-xlarge - environment: release + environment: ${{ !inputs.dry-run && 'release' || '' }} env: CARGO_TERM_COLOR: always CARGO_INCREMENTAL: 0 @@ -336,7 +345,7 @@ jobs: name: Windows ${{ matrix.arch }} needs: [version] runs-on: windows-latest - environment: release + environment: ${{ !inputs.dry-run && 'release' || '' }} strategy: fail-fast: false matrix: @@ -446,7 +455,7 @@ jobs: needs: [version] # The signing key lives in the release environment, as it does for the macOS, Windows and # flatpak jobs; without this the Linux packages are built unsigned. - environment: release + environment: ${{ !inputs.dry-run && 'release' || '' }} runs-on: ${{ matrix.runner }} strategy: fail-fast: false @@ -543,7 +552,7 @@ jobs: needs: [version, linux] runs-on: ${{ matrix.runner }} # LINUX_SIGNING_KEY comes from the repo's release environment, as the macOS secrets do. - environment: release + environment: ${{ !inputs.dry-run && 'release' || '' }} permissions: contents: read packages: write @@ -619,7 +628,7 @@ jobs: packaging/linux/flatpak-bundle.sh "$tarball" printf 'tarball=%s\n' "$tarball" >> "$GITHUB_OUTPUT" - name: Publish Flatpak OCI layout to GHCR and Docker Hub - if: github.event_name != 'pull_request' + if: github.event_name != 'pull_request' && !inputs.dry-run env: RELEASE_VERSION: ${{ needs.version.outputs.version }} ARCH: ${{ matrix.arch }} @@ -1051,7 +1060,7 @@ jobs: name: Linux riscv64 if: inputs.has-riscv64 needs: [version] - environment: release + environment: ${{ !inputs.dry-run && 'release' || '' }} # Not 22.04 like the other Linux jobs: Ubuntu never built jammy's ncurses security update for # riscv64, so the amd64 and riscv64 libtinfo6 can't match and the cross deps don't install (#44). runs-on: ubuntu-24.04 @@ -1217,10 +1226,10 @@ jobs: release: timeout-minutes: 10 name: Draft GitHub Release - if: "!cancelled() && !contains(needs.*.result, 'failure')" + if: "!inputs.dry-run && !cancelled() && !contains(needs.*.result, 'failure')" needs: [version, macos, windows, linux, linux-riscv64, flatpak, arch, freebsd, web, server] runs-on: ubuntu-24.04 - environment: release + environment: ${{ !inputs.dry-run && 'release' || '' }} permissions: contents: write steps: diff --git a/tests/test_release_dry_run.py b/tests/test_release_dry_run.py new file mode 100644 index 0000000..8804fdf --- /dev/null +++ b/tests/test_release_dry_run.py @@ -0,0 +1,39 @@ +"""release.yml's dry-run input: a nightly run on main builds and checks but releases nothing.""" +from pathlib import Path +import unittest + +import yaml + +ROOT = Path(__file__).resolve().parents[1] +DATA = yaml.load((ROOT / '.github/workflows/release.yml').read_text(), Loader=yaml.BaseLoader) +JOBS = DATA['jobs'] +DRY = '!inputs.dry-run' + + +class DryRunTests(unittest.TestCase): + def test_the_input_defaults_to_a_real_release(self): + spec = DATA['on']['workflow_call']['inputs']['dry-run'] + self.assertEqual((spec['type'], spec['default']), ('boolean', 'false')) + + def test_no_job_enters_the_release_environment_on_a_dry_run(self): + # The environment holds the signing secrets and only admits the release branch. + for name, job in JOBS.items(): + env = job.get('environment') + if env is not None: + self.assertIn(DRY, env, name) + self.assertNotEqual(env.strip(), 'release', name) + + def test_the_draft_and_macos_are_skipped(self): + # The draft is the release; macOS runs on a paid larger runner. + for name in ('release', 'macos'): + self.assertIn(DRY, JOBS[name].get('if', ''), name) + + def test_nothing_is_pushed_to_a_registry(self): + for name, job in JOBS.items(): + for step in job.get('steps', []): + if 'skopeo copy' in step.get('run', '') or 'docker push' in step.get('run', ''): + self.assertIn(DRY, step.get('if', ''), f"{name}: {step.get('name')}") + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_release_flatpak_oci.py b/tests/test_release_flatpak_oci.py index 453ce62..2fef642 100644 --- a/tests/test_release_flatpak_oci.py +++ b/tests/test_release_flatpak_oci.py @@ -98,8 +98,8 @@ class FlatpakOciPublishTests(unittest.TestCase): self.assertEqual(result.returncode, 0, result.stdout + result.stderr) self.assertEqual(self.log.read_text(), "") - def test_the_step_never_runs_for_a_pull_request(self): - self.assertEqual(STEP.get("if"), "github.event_name != 'pull_request'") + def test_the_step_never_runs_for_a_pull_request_or_a_dry_run(self): + self.assertEqual(STEP.get("if"), "github.event_name != 'pull_request' && !inputs.dry-run") def test_dockerhub_credentials_trigger_dual_push_for_stable_release(self): result = self.publish("1.2.0", "refs/heads/release", DOCKERHUB_USERNAME="user", DOCKERHUB_TOKEN="secret") diff --git a/tests/test_sign_linux.py b/tests/test_sign_linux.py index b30e706..007cc47 100644 --- a/tests/test_sign_linux.py +++ b/tests/test_sign_linux.py @@ -156,7 +156,8 @@ class SigningWiringTests(unittest.TestCase): def test_the_flatpak_job_imports_the_key_before_building_the_bundle(self): job = WORKFLOW["jobs"]["flatpak"] - self.assertEqual(job["environment"], "release") + # The release environment holds the signing key; a dry run (release.yml's dry-run) stays out. + self.assertEqual(job["environment"], "${{ !inputs.dry-run && 'release' || '' }}") uses = [s.get("uses", "") for s in job["steps"]] names = [s.get("name") for s in job["steps"]] imported = next(i for i, u in enumerate(uses) if u.startswith("storytold/craft-common-actions/actions/import-signing-key@"))