Skip to content

Commit 81df7ff

Browse files
committed
Harden release runtime gates
1 parent b111500 commit 81df7ff

11 files changed

Lines changed: 546 additions & 36 deletions

.github/workflows/ci.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ jobs:
121121
122122
while IFS= read -r path; do
123123
case "$path" in
124-
.github/workflows/ci.yml)
124+
.github/workflows/*.yml)
125125
test=true
126126
tooling=true
127127
pwg=true
@@ -131,7 +131,7 @@ jobs:
131131
README.md|pyproject.toml|MANIFEST.in|src/*|tests/*|data/*|extensions/*)
132132
test=true
133133
;;
134-
tools/check_gnome_shell_extension.py|tools/pack_gnome_shell_extension.sh|tools/prepare_release.py|tools/release_status.py)
134+
tools/check_gnome_shell_extension.py|tools/pack_gnome_shell_extension.sh|tools/prepare_release.py|tools/release_gates.py|tools/release_preflight.py|tools/release_runtime_gate.py|tools/release_status.py)
135135
test=true
136136
;;
137137
esac
@@ -149,13 +149,13 @@ jobs:
149149
esac
150150
151151
case "$path" in
152-
.github/workflows/ci.yml|tools/check_pipewire_gobject.py|src/mini_eq/pipewire_backend.py|src/mini_eq/analyzer.py)
152+
.github/workflows/*.yml|tools/check_pipewire_gobject.py|tools/release_gates.py|tools/release_runtime_gate.py|src/mini_eq/pipewire_backend.py|src/mini_eq/analyzer.py)
153153
pwg=true
154154
;;
155155
esac
156156
157157
case "$path" in
158-
.github/workflows/ci.yml|io.github.bhack.mini-eq.yaml|python3-dependencies.yaml|flatpak/*|src/*|data/*|pyproject.toml|MANIFEST.in|tools/check_pipewire_gobject.py)
158+
.github/workflows/*.yml|io.github.bhack.mini-eq.yaml|python3-dependencies.yaml|flatpak/*|src/*|data/*|pyproject.toml|MANIFEST.in|tools/check_pipewire_gobject.py|tools/release_gates.py|tools/release_runtime_gate.py)
159159
flatpak=true
160160
;;
161161
esac

.github/workflows/release.yml

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ on:
4242
required: true
4343
type: boolean
4444
default: false
45+
force_flatpak_runtime_smoke:
46+
description: Run the Flatpak routing runtime smoke even when runtime-sensitive files did not change.
47+
required: true
48+
type: boolean
49+
default: false
4550

4651
permissions:
4752
contents: read
@@ -99,9 +104,155 @@ jobs:
99104
echo "::warning::publishing PyPI without creating the GitHub release uses a separate build from release assets"
100105
fi
101106
107+
runtime-risk:
108+
name: Detect runtime smoke gate
109+
needs: validate-inputs
110+
runs-on: ubuntu-24.04
111+
112+
outputs:
113+
required: ${{ steps.detect.outputs.required }}
114+
base_tag: ${{ steps.detect.outputs.base_tag }}
115+
116+
steps:
117+
- name: Check out repository
118+
uses: actions/checkout@v6
119+
with:
120+
ref: ${{ inputs.target }}
121+
fetch-depth: 0
122+
123+
- name: Detect runtime-sensitive changes
124+
id: detect
125+
env:
126+
CREATE_GITHUB_RELEASE: ${{ inputs.create_github_release }}
127+
DRY_RUN: ${{ inputs.dry_run }}
128+
FORCE_FLATPAK_RUNTIME_SMOKE: ${{ inputs.force_flatpak_runtime_smoke }}
129+
PUBLISH_PYPI: ${{ inputs.publish_pypi }}
130+
PUBLISH_TESTPYPI: ${{ inputs.publish_testpypi }}
131+
TAG_NAME: ${{ inputs.tag_name }}
132+
run: |
133+
set -euo pipefail
134+
135+
release_requested=false
136+
if { [ "$CREATE_GITHUB_RELEASE" = "true" ] || [ "$PUBLISH_TESTPYPI" = "true" ] || [ "$PUBLISH_PYPI" = "true" ]; } \
137+
&& [ "$DRY_RUN" = "false" ]; then
138+
release_requested=true
139+
fi
140+
141+
python3 tools/release_runtime_gate.py \
142+
--tag "$TAG_NAME" \
143+
--release-requested "$release_requested" \
144+
--force "$FORCE_FLATPAK_RUNTIME_SMOKE" \
145+
--github-output "$GITHUB_OUTPUT" \
146+
--github-summary "$GITHUB_STEP_SUMMARY"
147+
148+
flatpak-runtime-smoke:
149+
name: Flatpak routing runtime smoke
150+
needs: runtime-risk
151+
if: ${{ needs['runtime-risk'].outputs.required == 'true' }}
152+
runs-on: ubuntu-24.04
153+
timeout-minutes: 90
154+
155+
steps:
156+
- name: Check out repository
157+
uses: actions/checkout@v6
158+
with:
159+
ref: ${{ inputs.target }}
160+
161+
- name: Install Flatpak and PipeWire test dependencies
162+
run: |
163+
sudo apt-get update
164+
sudo apt-get install -y \
165+
dbus-user-session \
166+
flatpak \
167+
jq \
168+
pipewire \
169+
pipewire-bin \
170+
wireplumber
171+
172+
- name: Build and run Flatpak routing runtime smoke
173+
run: tools/run_flatpak_runtime_smoke_ci.sh
174+
175+
runtime-gate:
176+
name: Require runtime smoke gate
177+
needs:
178+
- runtime-risk
179+
- flatpak-runtime-smoke
180+
if: ${{ always() }}
181+
runs-on: ubuntu-24.04
182+
183+
steps:
184+
- name: Require runtime-risk job
185+
if: ${{ needs['runtime-risk'].result != 'success' }}
186+
run: exit 1
187+
188+
- name: Require Flatpak routing smoke
189+
if: ${{ needs['runtime-risk'].outputs.required == 'true' && needs['flatpak-runtime-smoke'].result != 'success' }}
190+
run: |
191+
echo "::error::Flatpak routing runtime smoke is required for this release and did not pass."
192+
exit 1
193+
194+
- name: Confirm runtime gate
195+
run: |
196+
if [ "${{ needs['runtime-risk'].outputs.required }}" = "true" ]; then
197+
echo "Required Flatpak routing runtime smoke passed."
198+
else
199+
echo "Flatpak routing runtime smoke was not required for this dispatch."
200+
fi
201+
202+
preflight:
203+
name: Release preflight
204+
needs: validate-inputs
205+
runs-on: ubuntu-24.04
206+
timeout-minutes: 60
207+
208+
steps:
209+
- name: Check out repository
210+
uses: actions/checkout@v6
211+
with:
212+
ref: ${{ inputs.target }}
213+
fetch-depth: 0
214+
215+
- name: Install release preflight dependencies
216+
run: |
217+
sudo apt-get update
218+
sudo apt-get install -y \
219+
appstream \
220+
build-essential \
221+
dbus-user-session \
222+
desktop-file-utils \
223+
gir1.2-adw-1 \
224+
gir1.2-gtk-4.0 \
225+
git \
226+
gnome-shell \
227+
gobject-introspection \
228+
libebur128-1 \
229+
libgirepository1.0-dev \
230+
libglib2.0-dev \
231+
libpipewire-0.3-dev \
232+
libspa-0.2-modules \
233+
meson \
234+
ninja-build \
235+
pipewire \
236+
pkg-config \
237+
python3 \
238+
python3-cairo \
239+
python3-dev \
240+
python3-gi \
241+
python3-numpy \
242+
python3-pip \
243+
python3-setuptools \
244+
python3-venv \
245+
wireplumber
246+
247+
- name: Run release preflight
248+
run: docker/run-release-preflight.sh
249+
102250
build:
103251
name: Build release artifacts
104-
needs: validate-inputs
252+
needs:
253+
- validate-inputs
254+
- runtime-gate
255+
- preflight
105256
runs-on: ubuntu-24.04
106257

107258
steps:

docs/release.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ Run the narrowest gate that covers the release risk:
1616
verification, then Flathub stable PR.
1717
- **When PipeWire, routing, analyzer, Flatpak permissions, runtime dependencies,
1818
or shutdown changed:** local Flatpak install, Flatpak runtime smoke, and
19-
interactive real-music testing before merge or release.
19+
interactive real-music testing before merge or release. Public release
20+
workflow dispatches also run the isolated Flatpak routing smoke as a blocking
21+
job when runtime-sensitive files changed since the previous release tag.
2022
- **When background mode, Start at Login, hidden-window lifecycle, or Shell
2123
control changed:** one clean-permission Flatpak portal smoke in a real GNOME
2224
session.
@@ -201,7 +203,10 @@ MINI_EQ_RUN_LIVE_UI=1 python3 -m pytest tests/test_mini_eq_live_ui_runtime.py -q
201203

202204
There is an optional hosted Flatpak runtime smoke path in the `CI` workflow.
203205
Use it as extra signal or for smoke-harness work; keep the local runtime smoke
204-
as the release check when app/runtime routing behavior changed.
206+
as the release check when app/runtime routing behavior changed. The `Release`
207+
workflow also runs release preflight as a blocking job and has its own blocking
208+
copy of the Flatpak routing smoke gate for public TestPyPI, PyPI, and GitHub
209+
release dispatches.
205210

206211
## Package Channels
207212

tests/test_check_flatpak_runtime.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,19 @@ def test_flatpak_runtime_smoke_accepts_flathub_test_refs(app_ref: str) -> None:
2121
app_ref,
2222
"--check-deps",
2323
]
24+
25+
26+
def test_flatpak_runtime_smoke_includes_extra_flatpak_run_args(monkeypatch: pytest.MonkeyPatch) -> None:
27+
monkeypatch.setenv(
28+
"MINI_EQ_FLATPAK_RUN_ARGS",
29+
"--filesystem=/tmp/mini-eq-runtime/pipewire-0:ro --env=PIPEWIRE_RUNTIME_DIR=/tmp/mini-eq-runtime",
30+
)
31+
32+
assert check_flatpak_runtime.flatpak_run_command("io.github.bhack.mini-eq//master", "--check-deps") == [
33+
"flatpak",
34+
"run",
35+
"--filesystem=/tmp/mini-eq-runtime/pipewire-0:ro",
36+
"--env=PIPEWIRE_RUNTIME_DIR=/tmp/mini-eq-runtime",
37+
"io.github.bhack.mini-eq//master",
38+
"--check-deps",
39+
]

tests/test_github_workflows.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from __future__ import annotations
2+
3+
import re
4+
from pathlib import Path
5+
6+
ROOT = Path(__file__).resolve().parents[1]
7+
8+
9+
def workflow_text(path: str) -> str:
10+
return (ROOT / ".github/workflows" / path).read_text(encoding="utf-8")
11+
12+
13+
def test_workflow_expressions_use_bracket_access_for_hyphenated_needs() -> None:
14+
offenders: list[str] = []
15+
pattern = re.compile(r"needs\.[A-Za-z0-9_]*-[A-Za-z0-9_-]*")
16+
17+
for workflow in (ROOT / ".github/workflows").glob("*.yml"):
18+
for line_number, line in enumerate(workflow.read_text(encoding="utf-8").splitlines(), start=1):
19+
if pattern.search(line):
20+
offenders.append(f"{workflow.relative_to(ROOT)}:{line_number}:{line.strip()}")
21+
22+
assert offenders == []
23+
24+
25+
def test_release_workflow_blocks_publish_on_runtime_gate() -> None:
26+
release_yml = workflow_text("release.yml")
27+
28+
assert "tools/release_runtime_gate.py" in release_yml
29+
assert "flatpak-runtime-smoke:" in release_yml
30+
assert "runtime-gate:" in release_yml
31+
assert "needs['flatpak-runtime-smoke'].result != 'success'" in release_yml
32+
assert "runtime-gate" in release_yml.partition("build:")[2].partition("runs-on:")[0]
33+
34+
35+
def test_release_workflow_blocks_publish_on_release_preflight() -> None:
36+
release_yml = workflow_text("release.yml")
37+
38+
assert "preflight:" in release_yml
39+
assert "docker/run-release-preflight.sh" in release_yml
40+
assert "preflight" in release_yml.partition("build:")[2].partition("runs-on:")[0]
41+
42+
43+
def test_ci_scope_treats_release_workflow_and_release_gate_tools_as_tested_changes() -> None:
44+
ci_yml = workflow_text("ci.yml")
45+
46+
assert ".github/workflows/*.yml)" in ci_yml
47+
assert "tools/release_gates.py" in ci_yml
48+
assert "tools/release_runtime_gate.py" in ci_yml

0 commit comments

Comments
 (0)