Wire install.sh smoke gate into create-release.yml release pipeline

The smoke gate workflow exists from commit 065ebdb27 but until it is
called from create-release.yml it does not actually protect any release.
That is exactly the regression class that let rc.1 → rc.5 ship with a
broken install.sh: nothing in the release pipeline exercised the
documented secure-install flow against the published GitHub Release URL.

Wire install-sh-smoke.yml as a downstream workflow_call after
validate_release_assets succeeds. Gated on
historical_asset_backfill_only != 'true' since asset-backfill flows
re-upload to an already-published release and the smoke would just
re-confirm what hasn't changed.

Pre-install structural checks were verified locally against rc.5 — the
gate correctly fires the banner / agent-banner / --version handler
assertions against the broken release. The end-to-end container portion
(privileged systemd boot, install.sh execution, /api/health, /api/version
match) will run for the first time on the next release that publishes
through this workflow; existing retry loops on systemd readiness,
service activation, and health endpoint absorb transient runner flakes.

Add install-sh-smoke.yml to the deployment-installability canonical files
and to the release-promotion proof policy's match_files, and add
scripts/installtests/build_release_assets_test.go to that policy's
exact_files (matching the existing pin set for related policies in the
deployment-installability subsystem). Update subsystem_lookup_test.py
fixtures that pinned the exact_files list literally.

Pinned the create-release.yml wiring in build_release_assets_test.go
alongside the validate-release-assets wiring so the smoke step cannot
silently be unwired.

Document the gate's contract responsibilities in
deployment-installability Extension Point 2.
This commit is contained in:
rcourtman 2026-05-12 11:44:04 +01:00
parent 065ebdb276
commit 7c0f654253
5 changed files with 66 additions and 1 deletions

View file

@ -1062,3 +1062,30 @@ jobs:
release_id: ${{ needs.create_release.outputs.release_id }}
draft: ${{ github.event.inputs.draft_only == 'true' }}
target_commitish: ${{ needs.create_release.outputs.target_commitish }}
# End-to-end install.sh smoke against the just-published release. Catches
# runtime regressions in the documented Proxmox-LXC / systemd install flow
# that the build-time validate-release.sh checks cannot see: the script
# parses fine, signs cleanly, but fails to actually install or boot Pulse.
# This class of regression broke silently across v6 rc.1 → rc.5 because no
# existing gate exercised the documented secure-install commands against
# the published GitHub Release URL.
#
# Gated on validate_release_assets success — the smoke depends on the
# published asset bundle being well-formed, so we only run it after the
# cheaper content checks pass. Skipped for the historical-backfill path
# since that flow re-uploads to an already-published release and the
# smoke would just re-confirm what hasn't changed.
install_sh_smoke:
needs:
- prepare
- validate_release_assets
if: ${{ always() && needs.prepare.result == 'success' && needs.validate_release_assets.result == 'success' && needs.prepare.outputs.historical_asset_backfill_only != 'true' }}
permissions:
contents: read
uses: ./.github/workflows/install-sh-smoke.yml
secrets: inherit
with:
tag: ${{ needs.prepare.outputs.tag }}
version: ${{ needs.prepare.outputs.version }}
repository: ${{ github.repository }}

View file

@ -37,6 +37,7 @@ server-side update execution surfaces.
14. `.github/workflows/release-dry-run.yml`
15. `.github/workflows/update-demo-server.yml`
16. `.github/workflows/validate-release-assets.yml`
17. `.github/workflows/install-sh-smoke.yml`
16. `.github/ISSUE_TEMPLATE/v6_rc_feedback.yml`
17. `docs/RELEASE_NOTES.md`
18. `docs/releases/`
@ -118,7 +119,25 @@ server-side update execution surfaces.
## Extension Points
1. Add or change deployment-type detection, update planning, or apply behavior through `internal/updates/`
2. Add or change release-build metadata injection, Docker build-context allowlists, release artifact assembly, governed promotion metadata resolution, the canonical version file, operator-facing release packet content, prerelease feedback intake wording, historical published-release integrity backfill, release asset validation status publication, download endpoint checksum/signature header proof, or the canonical in-repo v6 upgrade guide through `scripts/build-release.sh`, `scripts/release_asset_common.sh`, `scripts/backfill-release-assets.sh`, `scripts/release_ldflags.sh`, `scripts/check-workflow-dispatch-inputs.py`, `scripts/release_control/render_release_body.py`, `scripts/release_control/resolve_release_promotion.py`, `scripts/release_control/record_rc_to_ga_rehearsal.py`, `scripts/release_control/internal/record_rc_to_ga_rehearsal.py`, `scripts/release_control/release_promotion_policy_support.py`, `.dockerignore`, `Dockerfile`, `.github/ISSUE_TEMPLATE/v6_rc_feedback.yml`, `docs/RELEASE_NOTES.md`, `docs/releases/`, `docs/UPGRADE_v6.md`, `docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md`, `docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md`, `docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md`, `scripts/validate-release.sh`, `scripts/validate-published-release.sh`, the operator dispatch helpers `scripts/trigger-release.sh` and `scripts/trigger-release-dry-run.sh`, and the governed release workflows `.github/workflows/backfill-release-assets.yml`, `.github/workflows/create-release.yml`, `.github/workflows/deploy-demo-server.yml`, `.github/workflows/helm-pages.yml`, `.github/workflows/publish-docker.yml`, `.github/workflows/publish-helm-chart.yml`, `.github/workflows/promote-floating-tags.yml`, `.github/workflows/release-dry-run.yml`, `.github/workflows/update-demo-server.yml`, and `.github/workflows/validate-release-assets.yml`
2. Add or change release-build metadata injection, Docker build-context allowlists, release artifact assembly, governed promotion metadata resolution, the canonical version file, operator-facing release packet content, prerelease feedback intake wording, historical published-release integrity backfill, release asset validation status publication, download endpoint checksum/signature header proof, end-to-end install.sh smoke against the published release, or the canonical in-repo v6 upgrade guide through `scripts/build-release.sh`, `scripts/release_asset_common.sh`, `scripts/backfill-release-assets.sh`, `scripts/release_ldflags.sh`, `scripts/check-workflow-dispatch-inputs.py`, `scripts/release_control/render_release_body.py`, `scripts/release_control/resolve_release_promotion.py`, `scripts/release_control/record_rc_to_ga_rehearsal.py`, `scripts/release_control/internal/record_rc_to_ga_rehearsal.py`, `scripts/release_control/release_promotion_policy_support.py`, `.dockerignore`, `Dockerfile`, `.github/ISSUE_TEMPLATE/v6_rc_feedback.yml`, `docs/RELEASE_NOTES.md`, `docs/releases/`, `docs/UPGRADE_v6.md`, `docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md`, `docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md`, `docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md`, `scripts/validate-release.sh`, `scripts/validate-published-release.sh`, the operator dispatch helpers `scripts/trigger-release.sh` and `scripts/trigger-release-dry-run.sh`, and the governed release workflows `.github/workflows/backfill-release-assets.yml`, `.github/workflows/create-release.yml`, `.github/workflows/deploy-demo-server.yml`, `.github/workflows/helm-pages.yml`, `.github/workflows/install-sh-smoke.yml`, `.github/workflows/publish-docker.yml`, `.github/workflows/publish-helm-chart.yml`, `.github/workflows/promote-floating-tags.yml`, `.github/workflows/release-dry-run.yml`, `.github/workflows/update-demo-server.yml`, and `.github/workflows/validate-release-assets.yml`
The `install-sh-smoke.yml` workflow runs end-to-end against the
published release in a privileged systemd container: it downloads
`install.sh` and `install.sh.sshsig` from the GitHub Release URL,
runs the README-documented `ssh-keygen -Y verify` step against the
real signed asset using the README's pinned key, re-checks the
server-installer banner / `--version)` arg handler / agent-banner
absence against the published bytes (not just the local build), then
actually runs `bash install.sh --archive <tarball> --disable-auto-updates`
inside the container and asserts `systemctl is-active pulse`, a 200
from `/api/health`, and a version match from `/api/version`.
`create-release.yml` must call this workflow as a downstream
`workflow_call` after `validate-release-assets.yml` succeeds for every
release that is not a `historical_asset_backfill_only` run; without
that wiring the smoke gate exists but never protects a release.
The README's pinned `pulse-installer` ed25519 key must verify
`install.sh.sshsig` for the published release; this is enforced by
`scripts/validate-release.sh` at build time and re-verified by
`install-sh-smoke.yml` against the served asset.
3. Add or change root server installer, shell installer, Docker bootstrap installer, Windows installer, container-agent installer, repo-root compose defaults, or auto-update script behavior through `install.sh`, `scripts/install.sh`, `scripts/install-docker.sh`, `scripts/install.ps1`, `scripts/install-container-agent.sh`, `docker-compose.yml`, and `scripts/pulse-auto-update.sh`
The top-level `install.sh` asset published on GitHub Releases must be the
root Pulse SERVER installer (the LXC / systemd / Proxmox VE installer that

View file

@ -2810,6 +2810,7 @@
".github/workflows/backfill-release-assets.yml",
".github/workflows/create-release.yml",
".github/workflows/helm-pages.yml",
".github/workflows/install-sh-smoke.yml",
".github/workflows/promote-floating-tags.yml",
".github/workflows/publish-docker.yml",
".github/workflows/publish-helm-chart.yml",
@ -2834,6 +2835,7 @@
"allow_same_subsystem_tests": false,
"test_prefixes": [],
"exact_files": [
"scripts/installtests/build_release_assets_test.go",
"scripts/release_control/internal/record_rc_to_ga_rehearsal_test.py",
"scripts/release_control/release_promotion_policy_support_test.py",
"scripts/release_control/release_promotion_policy_test.py",

View file

@ -162,6 +162,17 @@ func TestCreateReleaseUploadsPowerShellInstaller(t *testing.T) {
`Draft release ${RELEASE_ID} target_commitish is ${ACTUAL_TARGET_COMMITISH}, expected ${HEAD_SHA}.`,
`./scripts/backfill-release-assets.sh --tag "${{ needs.prepare.outputs.tag }}" --repo "${{ github.repository }}"`,
`./scripts/validate-published-release.sh "${{ needs.prepare.outputs.tag }}" "${{ github.repository }}"`,
// End-to-end install.sh smoke must run downstream of
// validate_release_assets on every release that is not a
// historical asset backfill. Without this wiring the smoke
// workflow exists but never actually protects a release —
// exactly the regression class that let rc.1 → rc.5 ship with
// broken install.sh.
`uses: ./.github/workflows/install-sh-smoke.yml`,
`install_sh_smoke:`,
`needs.validate_release_assets.result == 'success'`,
`needs.prepare.outputs.historical_asset_backfill_only != 'true'`,
`repository: ${{ github.repository }}`,
}
for _, needle := range required {
if !strings.Contains(workflow, needle) {

View file

@ -4201,6 +4201,7 @@ class SubsystemLookupTest(unittest.TestCase):
self.assertEqual(
match["verification_requirement"]["exact_files"],
[
"scripts/installtests/build_release_assets_test.go",
"scripts/release_control/internal/record_rc_to_ga_rehearsal_test.py",
"scripts/release_control/release_promotion_policy_support_test.py",
"scripts/release_control/release_promotion_policy_test.py",
@ -4235,6 +4236,7 @@ class SubsystemLookupTest(unittest.TestCase):
self.assertEqual(
match["verification_requirement"]["exact_files"],
[
"scripts/installtests/build_release_assets_test.go",
"scripts/release_control/internal/record_rc_to_ga_rehearsal_test.py",
"scripts/release_control/release_promotion_policy_support_test.py",
"scripts/release_control/release_promotion_policy_test.py",
@ -4276,6 +4278,7 @@ class SubsystemLookupTest(unittest.TestCase):
self.assertEqual(
match["verification_requirement"]["exact_files"],
[
"scripts/installtests/build_release_assets_test.go",
"scripts/release_control/internal/record_rc_to_ga_rehearsal_test.py",
"scripts/release_control/release_promotion_policy_support_test.py",
"scripts/release_control/release_promotion_policy_test.py",
@ -4310,6 +4313,7 @@ class SubsystemLookupTest(unittest.TestCase):
self.assertEqual(
match["verification_requirement"]["exact_files"],
[
"scripts/installtests/build_release_assets_test.go",
"scripts/release_control/internal/record_rc_to_ga_rehearsal_test.py",
"scripts/release_control/release_promotion_policy_support_test.py",
"scripts/release_control/release_promotion_policy_test.py",
@ -4344,6 +4348,7 @@ class SubsystemLookupTest(unittest.TestCase):
self.assertEqual(
match["verification_requirement"]["exact_files"],
[
"scripts/installtests/build_release_assets_test.go",
"scripts/release_control/internal/record_rc_to_ga_rehearsal_test.py",
"scripts/release_control/release_promotion_policy_support_test.py",
"scripts/release_control/release_promotion_policy_test.py",
@ -4378,6 +4383,7 @@ class SubsystemLookupTest(unittest.TestCase):
self.assertEqual(
match["verification_requirement"]["exact_files"],
[
"scripts/installtests/build_release_assets_test.go",
"scripts/release_control/internal/record_rc_to_ga_rehearsal_test.py",
"scripts/release_control/release_promotion_policy_support_test.py",
"scripts/release_control/release_promotion_policy_test.py",