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
This commit is contained in:
FredApps authored and GitHub committed 2026-11-11 22:46:06 +01:00
1 parent 15058dc71e
commit 603045e42d
4 files changed
+60 -11

No files matched your search

+17 -8
View File
@@ -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:
+39
View File
@@ -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()
+2 -2
View File
@@ -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")
+2 -1
View File
@@ -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@"))