Files
pulse/scripts/installtests/build_release_assets_test.go
T
courtmanr@gmail.com f45c08902e Prepare v6.2.0-rc.1 release
First candidate on the v6.2.0 line, following stable v6.1.2. Carries
the External Probes Pro feature, the three-part multi-site Proxmox
identity isolation work, cloned machine-id collapse detection,
unattended-update service recovery, the TrueNAS same-host redirect
follow, and verified telemetry outcomes. Version pins move to
6.2.0-rc.1 across the repo root, Docker bootstrap defaults, and Helm
metadata per the deployment-installability contract; stable install
pointers remain on v6.1.2 until governed promotion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 14:08:29 +01:00

1910 lines
83 KiB
Go

package installtests
import (
"crypto/ed25519"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"golang.org/x/crypto/ssh"
)
func TestBuildReleaseUsesV6InstallScripts(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "build-release.sh"))
if err != nil {
t.Fatalf("read build-release.sh: %v", err)
}
script := string(content)
required := []string{
`SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"`,
`PULSE_REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"`,
`cd "${PULSE_REPO_ROOT}"`,
`source "${SCRIPT_DIR}/release_asset_common.sh"`,
`RENDERED_INSTALLERS_DIR="${BUILD_DIR}/rendered-installers"`,
`go run ./scripts/render_installers.go \`,
// The published install.sh asset is the server installer (root install.sh).
// The rendered AGENT installer is shipped inside tarballs and Docker images
// at ./scripts/install.sh and served at the running server's /install.sh
// endpoint, but is intentionally not a top-level GitHub Releases asset:
// pulse-auto-update.sh, the root install.sh's own --rc/--version flows, and
// the README quickstart all expect releases/<tag>/install.sh
// to be the server installer that accepts --version vX.Y.Z.
`cp install.sh "$RELEASE_DIR/install.sh"`,
`[ -f "${RENDERED_INSTALLERS_DIR}/install.ps1" ] && cp "${RENDERED_INSTALLERS_DIR}/install.ps1" "$RELEASE_DIR/install.ps1"`,
`cp "$BUILD_DIR/pulse-agent-linux-amd64" "$RELEASE_DIR/"`,
`cp "$BUILD_DIR/pulse-agent-linux-arm64" "$RELEASE_DIR/"`,
`cp "$BUILD_DIR/pulse-agent-linux-armv7" "$RELEASE_DIR/"`,
`cp "$BUILD_DIR/pulse-agent-linux-armv6" "$RELEASE_DIR/"`,
`cp "$BUILD_DIR/pulse-agent-linux-386" "$RELEASE_DIR/"`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("build-release.sh missing required release asset copy: %s", needle)
}
}
// Sanity-check the opposite drift: the rendered AGENT installer must NOT be
// the published install.sh asset. Publishing it there shipped a broken LXC
// install + auto-update path across every v6 RC (rc.1 → rc.5).
if strings.Contains(script, `cp "${RENDERED_INSTALLERS_DIR}/install.sh" "$RELEASE_DIR/install.sh"`) {
t.Fatal("build-release.sh must not publish the rendered agent install.sh as the top-level release asset")
}
requiredScriptWiring := []string{
`agent_ldflags="$(./scripts/release_ldflags.sh agent --version "v${VERSION}" "${update_ldflags_args[@]}")"`,
`server_ldflags="$(./scripts/release_ldflags.sh server --version "v${VERSION}" --build-time "${build_time}" --git-commit "${git_commit}" "${license_ldflags_args[@]}" "${update_ldflags_args[@]}")"`,
`release_go_build_args=(-buildvcs=false -trimpath)`,
`"${release_go_build_args[@]}"`,
`RELEASE_PACKET_SBOM="pulse-v${VERSION}-release.sbom.spdx.json"`,
`pulse_release_prepare_signing_state "pulse-installer" "pulse-install"`,
`trap 'pulse_release_cleanup_signing_state' EXIT`,
`--installer-ssh-public-key "${PULSE_RELEASE_UPDATE_SSH_PUBLIC_KEY}"`,
`pulse_release_generate_packet_sbom "${RELEASE_DIR}" "${RELEASE_PACKET_SBOM}"`,
`mapfile -t checksum_files < <(pulse_release_collect_checksum_files "${RELEASE_DIR}")`,
`pulse_release_write_checksums_and_signatures "${RELEASE_DIR}" "${checksum_files[@]}"`,
}
for _, needle := range requiredScriptWiring {
if !strings.Contains(script, needle) {
t.Fatalf("build-release.sh missing canonical ldflags wiring: %s", needle)
}
}
if builds, cleanBuilds := strings.Count(script, `env $build_env go build \`), strings.Count(script, `"${release_go_build_args[@]}"`); builds != cleanBuilds {
t.Fatalf("build-release.sh must disable automatic VCS stamping on every release go build: builds=%d clean_builds=%d", builds, cleanBuilds)
}
helperBytes, err := os.ReadFile(repoFile("scripts", "release_asset_common.sh"))
if err != nil {
t.Fatalf("read release_asset_common.sh: %v", err)
}
helper := string(helperBytes)
helperRequired := []string{
`: "${PULSE_SCRIPTS_DIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"`,
`: "${PULSE_REPO_ROOT:=$(cd "${PULSE_SCRIPTS_DIR}/.." && pwd)}"`,
`go -C "${PULSE_REPO_ROOT}" run ./scripts/release_update_key.go "$@"`,
`pulse_release_go_run_update_key public-key --private-key "${PULSE_UPDATE_SIGNING_KEY}"`,
`pulse_release_go_run_update_key fingerprint --public-key "${PULSE_RELEASE_UPDATE_PUBLIC_KEY}"`,
`pulse_release_go_run_update_key public-key-ssh --private-key "${PULSE_UPDATE_SIGNING_KEY}"`,
`pulse_release_go_run_update_key openssh-private-key --private-key "${PULSE_UPDATE_SIGNING_KEY}"`,
`pulse_release_go_run_update_key sign --private-key "${PULSE_UPDATE_SIGNING_KEY}" --file "${absolute_file}"`,
`PULSE_UPDATE_SIGNING_PUBLIC_KEY`,
`PULSE_UPDATE_SIGNING_PUBLIC_KEY_FINGERPRINT`,
`Verified update signing public key fingerprint: ${PULSE_RELEASE_UPDATE_PUBLIC_KEY_FINGERPRINT}`,
`ssh-keygen -q -Y sign`,
`"${resolved_tool}" "dir:${release_dir}" -o "spdx-json=${tmp_sbom}"`,
`if compgen -G "pulse-*.sbom.spdx.json" > /dev/null; then`,
`find . -maxdepth 1 -type f \( -name '*.sig' -o -name '*.sshsig' \) -delete`,
}
for _, needle := range helperRequired {
if !strings.Contains(helper, needle) {
t.Fatalf("release_asset_common.sh missing canonical release asset wiring: %s", needle)
}
}
}
func shieldsBadgeMessage(value string) string {
return strings.ReplaceAll(value, "-", "--")
}
func TestCreateReleaseUploadsPowerShellInstaller(t *testing.T) {
content, err := os.ReadFile(repoFile(".github", "workflows", "create-release.yml"))
if err != nil {
t.Fatalf("read create-release.yml: %v", err)
}
validationContent, err := os.ReadFile(repoFile(".github", "workflows", "validate-release-assets.yml"))
if err != nil {
t.Fatalf("read validate-release-assets.yml: %v", err)
}
workflow := string(content)
validationWorkflow := string(validationContent)
required := []string{
`historical_asset_backfill_only:`,
`description: 'Repair an already-published release packet in place without rebuilding binaries'`,
`SYFT_VERSION="1.42.4"`,
`SYFT_ARCHIVE="syft_${SYFT_VERSION}_linux_amd64.tar.gz"`,
`SYFT_SHA256="590650c2743b83f327d1bf9bec64f6f83b7fec504187bb84f500c862bf8f2a0f"`,
`install -m 0755 "${TMP_DIR}/syft" /usr/local/bin/syft`,
`release_upload_with_retry "${TAG}" release/*.sbom.spdx.json --clobber`,
`release/pulse-agent-linux-amd64`,
`release/pulse-agent-linux-arm64`,
`release/pulse-agent-linux-armv7`,
`release/pulse-agent-linux-armv6`,
`release/pulse-agent-linux-386`,
`release/pulse-agent-freebsd-amd64`,
`release/pulse-agent-freebsd-arm64`,
`release/pulse-agent-windows-amd64.exe`,
`release/pulse-agent-windows-arm64.exe`,
`release/pulse-agent-windows-386.exe`,
`release_upload_with_retry "${TAG}" release/install.sh --clobber`,
`if [ -f release/install.ps1 ]; then`,
`release_upload_with_retry "${TAG}" release/install.ps1 --clobber`,
`release_upload_with_retry "${TAG}" release/*.sig --clobber`,
`release_upload_with_retry "${TAG}" release/*.sshsig --clobber`,
`gh release upload "$@"`,
`gh release upload failed on attempt ${attempt}/${max_attempts}; retrying in ${wait_seconds}s`,
`gh release upload failed after ${max_attempts} attempts`,
`uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4`,
`subject-path: release/*`,
`gh api "repos/${{ github.repository }}/releases?per_page=100" --paginate`,
`git push origin "refs/tags/${TAG}" --force`,
`--rawfile body "$NOTES_FILE"`,
`--input "$RELEASE_PAYLOAD"`,
`--expected-body-file "$NOTES_FILE"`,
`historical_asset_backfill_only=${HISTORICAL_ASSET_BACKFILL_ONLY}`,
`if: ${{ always() && needs.prepare.result == 'success' && needs.build_release_candidate.result == 'success' && needs.create_release.result == 'success' && needs.prepare.outputs.historical_asset_backfill_only != 'true' }}`,
`candidate_manifest_artifact: ${{ needs.build_release_candidate.outputs.manifest_artifact_name }}`,
`if: ${{ needs.prepare.outputs.historical_asset_backfill_only == 'true' }}`,
`permissions:`,
`issues: write`,
`statuses: write`,
`ACTUAL_RELEASE_TAG=$(jq -r '.tag_name // empty' "$RELEASE_JSON_FILE")`,
`ACTUAL_TARGET_COMMITISH=$(jq -r '.target_commitish // empty' "$RELEASE_JSON_FILE")`,
`Draft release ${RELEASE_ID} is bound to tag ${ACTUAL_RELEASE_TAG}, expected ${TAG}.`,
`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 }}`,
// Helm chart publish must be called explicitly from create-release
// because the draft→PATCH(draft=false) publish path does NOT fire
// the `release: published` webhook (GitHub-documented quirk). v6
// rc.1 → rc.5 published successfully but never produced a Helm
// chart on the GitHub Pages index, breaking
// `helm install pulse pulse/pulse --version 6.0.0-rc.X`.
`uses: ./.github/workflows/publish-helm-chart.yml`,
`publish_helm_chart:`,
`chart_version: ${{ needs.prepare.outputs.version }}`,
`app_version: ${{ needs.prepare.outputs.version }}`,
// promote-floating-tags chains off publish-docker via workflow_run,
// but when publish-docker fails (rc.3 → rc.5 all did) the chain
// silently doesn't fire and latest/major/minor docker tags stay
// stale. Defensive workflow_call backup, gated on
// validate_release_assets succeeding (which waits for the image to
// be pullable, so the tag points at a real manifest).
`uses: ./.github/workflows/promote-floating-tags.yml`,
`promote_floating_tags:`,
`tag: ${{ needs.prepare.outputs.tag }}`,
`prerelease: ${{ needs.prepare.outputs.is_prerelease == 'true' }}`,
// Draft-only mode (draft_only=true input) keeps the release as a
// draft and skips the publish step. The three workflow_call'd
// downstreams must skip in that mode too — install-sh-smoke can't
// reach the /releases/download/<tag>/ URL of a draft (404), and
// helm publish + tag promotion would advance externally-visible
// state to a release that hasn't been promoted out of draft yet.
`needs.prepare.outputs.historical_asset_backfill_only != 'true' && github.event.inputs.draft_only != 'true'`,
}
for _, needle := range required {
if !strings.Contains(workflow, needle) {
t.Fatalf("create-release.yml missing required installer upload step: %s", needle)
}
}
publishedReleaseGuard := `needs.prepare.outputs.historical_asset_backfill_only != 'true' && github.event.inputs.draft_only != 'true'`
for _, job := range []string{"install_sh_smoke", "publish_helm_chart", "promote_floating_tags"} {
block := workflowJobBlock(t, workflow, job)
if !strings.Contains(block, publishedReleaseGuard) {
t.Fatalf("create-release.yml job %s must skip historical backfill and draft-only runs before invoking downstream workflow_call", job)
}
}
if !strings.Contains(workflow, `draft: ${{ github.event.inputs.draft_only == 'true' }}`) {
t.Fatal("create-release.yml must pass the actual draft_only state into validate-release-assets")
}
if strings.Contains(workflow, `provenance: false`) {
t.Fatal("create-release.yml must not disable release-image provenance")
}
validationRequired := []string{
`statuses: write`,
`curl --fail-with-body --silent --show-error -X POST`,
`"context": "Release Asset Validation"`,
`--arg tag "${{ steps.context.outputs.tag }}"`,
`--arg target_commitish "${{ steps.context.outputs.target_commitish }}"`,
`{body: $body, tag_name: $tag, target_commitish: $target_commitish}`,
`{draft: true, tag_name: $tag, target_commitish: $target_commitish}`,
`Validation release body update detached release tag`,
`Validation release body update changed target_commitish`,
`Validate release body integrity`,
`--validate-body-file "$RELEASE_BODY_FILE"`,
`--expected-body-file "$CLEAN_BODY_FILE"`,
`Quarantine malformed release body`,
`The release was quarantined as a draft without deleting its assets.`,
}
for _, needle := range validationRequired {
if !strings.Contains(validationWorkflow, needle) {
t.Fatalf("validate-release-assets.yml missing required status publication contract: %s", needle)
}
}
}
func TestCurrentStablePatchReleasePacketTracksInstallMetadata(t *testing.T) {
version := currentReleaseVersion(t)
if isPrereleaseVersion(version) {
t.Skip("current release is a prerelease")
}
previous, ok := previousStablePatchVersion(version)
if !ok {
t.Skip("current release is not a stable patch release")
}
releaseBranch := requiredReleaseBranchForVersion(t, version)
releaseNotesPath := repoFile("docs", "releases", "RELEASE_NOTES_v"+version+".md")
changelogPath := repoFile("docs", "releases", "V6_CHANGELOG_v"+version+".md")
assertFileContainsAllNormalized(t, releaseNotesPath,
"`v"+version+"` is a stable patch release",
"`v"+previous+"`",
"Proxmox monitoring is more authoritative",
"TrueNAS monitoring uses the supported JSON-RPC transport",
"QNAP and Unraid installs now fail early",
"not Authenticode-signed",
"Unknown Publisher",
"`no-mobile-impact`",
"rollback target for this patch release is `v"+previous+"`",
)
assertFileContainsAll(t, changelogPath,
"Version: `v"+version+"`",
"Rollback target: `v"+previous+"`",
"Promotion path: stable patch hotfix from `"+releaseBranch+"`",
"Proxmox and PBS monitoring",
"TrueNAS uses its supported JSON-RPC transport",
"Unified Agent installs on constrained QNAP and Unraid roots",
"`v6.1.2`-only",
"Unknown Publisher",
"Mobile decision: `no-mobile-impact`",
)
assertFileContainsAll(t, repoFile("docs", "RELEASE_NOTES.md"),
"docs/releases/RELEASE_NOTES_v"+version+".md",
"docs/releases/V6_CHANGELOG_v"+version+".md",
)
assertFileContainsAll(t, repoFile("docs", "UPGRADE_v6.md"),
"docs/releases/RELEASE_NOTES_v"+version+".md",
"docs/releases/V6_CHANGELOG_v"+version+".md",
)
assertFileContainsAll(t, repoFile("deploy", "helm", "pulse", "Chart.yaml"),
"version: "+version,
`appVersion: "`+version+`"`,
"raw.githubusercontent.com/rcourtman/Pulse/v"+version+"/docs/images/pulse-logo.svg",
"blob/v"+version+"/docs/KUBERNETES.md",
)
assertFileContainsAll(t, repoFile("deploy", "helm", "pulse", "README.md"),
"Version-"+version+"-informational",
"AppVersion-"+version+"-informational",
"Autogenerated from chart metadata using [helm-docs v1.14.2]",
)
assertFileContainsAll(t, repoFile("docker-compose.yml"),
"image: ${PULSE_IMAGE:-rcourtman/pulse:"+version+"}",
)
assertFileContainsAll(t, repoFile("scripts", "install-docker.sh"),
`CANONICAL_DEFAULT_PULSE_VERSION="`+version+`"`,
)
assertFileContainsAllNormalized(t, repoFile("docs", "release-control", "v6", "internal", "subsystems", "deployment-installability.md"),
"The active stable `v"+version+"` cut sets the repo-root `VERSION`, repo-root `docker-compose.yml` image default, `scripts/install-docker.sh` fallback, and Helm chart release metadata to the same `"+version+"` release version.",
"This patch release uses the stable hotfix path with `rollback_version=v"+previous+"`, `hotfix_exception=true`, a release-owner reason, and no fabricated same-version RC tag.",
"For the active stable `v"+version+"` cut, the repo-root compose default and `scripts/install-docker.sh` fallback must both pin `"+version+"`",
)
}
func TestCurrentStableMinorReleasePacketTracksInstallMetadata(t *testing.T) {
version := currentReleaseVersion(t)
if isPrereleaseVersion(version) {
t.Skip("current release is a prerelease")
}
parts, valid := parseStableVersion(version)
if !valid || parts[1] == 0 || parts[2] != 0 {
t.Skip("current release is not a stable minor release")
}
previous, ok := previousStableForPrereleaseVersion(version + "-rc.1")
if !ok {
t.Fatal("stable minor release has no earlier stable rollback packet")
}
releaseNotesPath := repoFile("docs", "releases", "RELEASE_NOTES_v"+version+".md")
changelogPath := repoFile("docs", "releases", "V6_CHANGELOG_v"+version+".md")
assertFileContainsAllNormalized(t, releaseNotesPath,
"`v"+version+"` is a stable minor release",
"stable `v"+previous+"`",
"## Highlights",
"Operational Trust",
"Actions provides a dedicated inbox",
"existing Pulse Mobile candidate",
"not Authenticode-signed",
"The rollback target is `v"+previous+"`",
)
assertFileContainsAllNormalized(t, changelogPath,
"Version: `v"+version+"`",
"Previous stable: `v"+previous+"`",
"Rollback target: `v"+previous+"`",
"Promotion path: owner-approved exact-SHA stable cutoff from `main`",
"Mobile decision: `existing-mobile-build-compatible`",
)
assertFileContainsAll(t, repoFile("docs", "RELEASE_NOTES.md"),
"docs/releases/RELEASE_NOTES_v"+version+".md",
"docs/releases/V6_CHANGELOG_v"+version+".md",
)
assertFileContainsAll(t, repoFile("docs", "UPGRADE_v6.md"),
"docs/releases/RELEASE_NOTES_v"+version+".md",
"docs/releases/V6_CHANGELOG_v"+version+".md",
)
assertFileContainsAll(t, repoFile("deploy", "helm", "pulse", "Chart.yaml"),
"version: "+version,
`appVersion: "`+version+`"`,
"raw.githubusercontent.com/rcourtman/Pulse/v"+version+"/docs/images/pulse-logo.svg",
"blob/v"+version+"/docs/KUBERNETES.md",
)
assertFileContainsAll(t, repoFile("deploy", "helm", "pulse", "README.md"),
"Version-"+version+"-informational",
"AppVersion-"+version+"-informational",
"Autogenerated from chart metadata using [helm-docs v1.14.2]",
)
assertFileContainsAll(t, repoFile("docker-compose.yml"),
"image: ${PULSE_IMAGE:-rcourtman/pulse:"+version+"}",
)
assertFileContainsAll(t, repoFile("scripts", "install-docker.sh"),
`CANONICAL_DEFAULT_PULSE_VERSION="`+version+`"`,
)
assertFileContainsAllNormalized(t, repoFile("docs", "release-control", "v6", "internal", "subsystems", "deployment-installability.md"),
"The active stable `v"+version+"` cut sets the repo-root `VERSION`, repo-root `docker-compose.yml` image default, `scripts/install-docker.sh` fallback, and Helm chart release metadata to the same `"+version+"` release version.",
"`rollback_version=v"+previous+"`",
"The exact stable `main` SHA must pass the no-publication dry run before the same SHA is dispatched through the single-build publish workflow.",
"The stable server cut is classified `existing-mobile-build-compatible`.",
"explicit version-bound decision",
"For the active stable `v"+version+"` cut, the repo-root compose default and `scripts/install-docker.sh` fallback must both pin `"+version+"`",
)
}
func TestCurrentSupportPrereleasePacketTracksInstallMetadata(t *testing.T) {
version := currentReleaseVersion(t)
if !isPrereleaseVersion(version) {
t.Skip("current release is stable")
}
previous, ok := previousStableForPrereleaseVersion(version)
if !ok {
t.Skip("current prerelease does not have a previous stable patch")
}
releaseNotesPath := repoFile("docs", "releases", "RELEASE_NOTES_v"+version+".md")
changelogPath := repoFile("docs", "releases", "V6_CHANGELOG_v"+version+".md")
assertFileContainsAllNormalized(t, releaseNotesPath,
"`v"+version+"` is a release candidate",
"## Highlights",
"stable `v"+previous+"`",
"External Probes (Pulse Pro)",
"run from a connected Pulse agent outside the monitored site",
"assigned to a connected Pulse agent through the new **Run from** control",
"travel over the signed agent configuration channel",
"per-target source attribution",
"Only the currently assigned agent's results are accepted",
"an assigned check never also runs locally",
"surface as indeterminate",
"resumes local execution automatically if the entitlement lapses",
"Local availability checks remain free and unchanged in every edition",
"cloned `/etc/machine-id`",
"`availability_probe_targets`",
"`availability_probe_agents`",
"disclosed in `docs/PRIVACY.md`",
"Contradicting captured TLS certificate fingerprints now veto the merge",
"same TLS identity doctrine as connection consolidation",
"positive same-machine evidence before it can fold into a named cluster",
"no longer leaves the service down",
"same-host HTTP-to-HTTPS redirect",
"`CAP_NET_RAW`",
"no mobile compatibility change",
"No public mobile-store rollout",
"not yet Authenticode-signed",
"No unsigned-Windows exception applies to any `v6.2.0` release",
"rollback target is `v"+previous+"`",
)
assertFileContainsAllNormalized(t, changelogPath,
"Version: `v"+version+"`",
"Previous stable: `v"+previous+"`",
"Rollback target: `v"+previous+"`",
"Promotion path: exact-SHA single-build release candidate from `main`",
"External Probes (Pulse Pro)",
"gated by the `external_probe` entitlement at assignment time",
"delivered to their agent over the signed agent-config channel",
"skipped by the local poller",
"accepted only from the currently assigned agent",
"derive to indeterminate at read time when reports go stale",
"Host-agent identity-collapse detection for cloned `/etc/machine-id`",
"Telemetry counts `availability_probe_targets` and `availability_probe_agents`",
"Availability probe execution moved into a shared package",
"`postcss` build tooling is updated to `8.5.23`",
"remain isolated when TLS identity evidence contradicts",
"closing the first-poll window where an unclassified node could be folded into another site's cluster slot",
"Failed unattended updates restart a previously active Pulse service",
"follow safe same-host HTTPS redirects",
"Windows signing decision: Authenticode through SignPath is the mandatory signing backend",
"Mobile decision: `no-mobile-impact`",
"no companion build upload or public store rollout",
"the license-server entitlement catalog carrying `external_probe` was deployed on 2026-07-27, before this cut",
)
assertFileContainsAll(t, repoFile("docs", "RELEASE_NOTES.md"),
"docs/releases/RELEASE_NOTES_v"+version+".md",
"docs/releases/V6_CHANGELOG_v"+version+".md",
"current v6 support release candidate packet",
)
assertFileContainsAll(t, repoFile("docs", "UPGRADE_v6.md"),
"docs/releases/RELEASE_NOTES_v"+version+".md",
"docs/releases/V6_CHANGELOG_v"+version+".md",
"current v6 support release candidate packet",
)
assertFileContainsAll(t, repoFile("deploy", "helm", "pulse", "Chart.yaml"),
"version: "+version,
`appVersion: "`+version+`"`,
"raw.githubusercontent.com/rcourtman/Pulse/v"+version+"/docs/images/pulse-logo.svg",
"blob/v"+version+"/docs/KUBERNETES.md",
)
assertFileContainsAll(t, repoFile("deploy", "helm", "pulse", "README.md"),
"Version-"+shieldsBadgeMessage(version)+"-informational",
"AppVersion-"+shieldsBadgeMessage(version)+"-informational",
"Autogenerated from chart metadata using [helm-docs v1.14.2]",
)
assertFileContainsAll(t, repoFile("docker-compose.yml"),
"image: ${PULSE_IMAGE:-rcourtman/pulse:"+version+"}",
)
assertFileContainsAll(t, repoFile("scripts", "install-docker.sh"),
`CANONICAL_DEFAULT_PULSE_VERSION="`+version+`"`,
)
assertFileContainsAllNormalized(t, repoFile("docs", "release-control", "v6", "internal", "subsystems", "deployment-installability.md"),
"The active support prerelease `v"+version+"` cut sets the repo-root `VERSION`, repo-root `docker-compose.yml` image default, `scripts/install-docker.sh` fallback, and Helm chart release metadata to the same `"+version+"` release version.",
"This support prerelease keeps `rollback_version=v"+previous+"`, publishes a versioned public GitHub prerelease plus versioned Docker and Helm artifacts, and does not move stable/latest install pointers or stable semver aliases.",
"For the active support prerelease `v"+version+"` cut, the repo-root compose default and `scripts/install-docker.sh` fallback must both pin `"+version+"` until the next governed stable cut moves them forward.",
"The `v"+version+"` server cut is classified `no-mobile-impact`; no companion build upload is part of this cut. The existing mobile candidate programme remains separate, and the release packet must not describe a public store rollout.",
)
}
func TestBackfillReleaseWorkflowRepairsPublishedAssetsWithoutRebuilds(t *testing.T) {
scriptBytes, err := os.ReadFile(repoFile("scripts", "backfill-release-assets.sh"))
if err != nil {
t.Fatalf("read backfill-release-assets.sh: %v", err)
}
script := string(scriptBytes)
scriptRequired := []string{
`SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"`,
`PULSE_REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"`,
`cd "${PULSE_REPO_ROOT}"`,
`source "${SCRIPT_DIR}/release_asset_common.sh"`,
`gh release view "${TAG}" -R "${REPO}" --json isDraft,tagName`,
`Error: ${TAG} is still a draft release; use the normal release pipeline instead of historical backfill.`,
`gh release download "${TAG}" -R "${REPO}" --dir "${RELEASE_DIR}" --clobber`,
`pulse_release_prepare_signing_state "pulse-installer" "pulse-install"`,
`pulse_release_generate_packet_sbom "${PAYLOAD_DIR}" "${RELEASE_PACKET_SBOM}"`,
`pulse_release_write_checksums_and_signatures "${RELEASE_DIR}" "${checksum_files[@]}"`,
`gh release upload "${TAG}" "${RELEASE_DIR}/checksums.txt" --clobber`,
`gh release upload "${TAG}" "${RELEASE_DIR}"/*.sha256 --clobber`,
`gh release upload "${TAG}" "${RELEASE_DIR}"/*.sig --clobber`,
`gh release upload "${TAG}" "${RELEASE_DIR}"/*.sshsig --clobber`,
`gh release upload "${TAG}" "${RELEASE_DIR}/${RELEASE_PACKET_SBOM}" --clobber`,
}
for _, needle := range scriptRequired {
if !strings.Contains(script, needle) {
t.Fatalf("backfill-release-assets.sh missing required historical backfill step: %s", needle)
}
}
workflowBytes, err := os.ReadFile(repoFile(".github", "workflows", "backfill-release-assets.yml"))
if err != nil {
t.Fatalf("read backfill-release-assets.yml: %v", err)
}
workflow := string(workflowBytes)
workflowRequired := []string{
`name: Backfill Release Assets`,
`workflow_dispatch:`,
`contents: write`,
`runs-on: ubuntu-24.04`,
`uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3`,
`uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0`,
`SYFT_VERSION="1.42.4"`,
`SYFT_ARCHIVE="syft_${SYFT_VERSION}_linux_amd64.tar.gz"`,
`SYFT_SHA256="590650c2743b83f327d1bf9bec64f6f83b7fec504187bb84f500c862bf8f2a0f"`,
`./scripts/backfill-release-assets.sh --tag "${{ inputs.tag }}" --repo "${{ github.repository }}"`,
`PULSE_UPDATE_SIGNING_KEY: ${{ secrets.PULSE_UPDATE_SIGNING_KEY }}`,
`PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}`,
`./scripts/validate-published-release.sh "${{ inputs.tag }}" "${{ github.repository }}"`,
}
for _, needle := range workflowRequired {
if !strings.Contains(workflow, needle) {
t.Fatalf("backfill-release-assets.yml missing required release-repair step: %s", needle)
}
}
}
func TestReleaseValidationRequiresSignedSidecars(t *testing.T) {
localValidatorBytes, err := os.ReadFile(repoFile("scripts", "validate-release.sh"))
if err != nil {
t.Fatalf("read validate-release.sh: %v", err)
}
localValidator := string(localValidatorBytes)
localRequired := []string{
`"pulse-v${PULSE_VERSION}-release.sbom.spdx.json"`,
`release_sbom="pulse-${PULSE_TAG}-release.sbom.spdx.json"`,
`error "checksums.txt is missing ${release_sbom}"`,
`success "Release SBOM is listed in checksums.txt"`,
`info "Validating SSH signature sidecars..."`,
`if [ ! -s "checksums.txt.sshsig" ]; then`,
`error "Missing or empty checksums.txt.sshsig"`,
`if [ ! -s "${filename}.sshsig" ]; then`,
`error "Missing or empty ${filename}.sshsig"`,
`success "SSH signature sidecars validated"`,
`validate_download_binary_headers() {`,
`http_header_value "X-Checksum-Sha256"`,
`http_header_value "X-Signature-Ed25519"`,
`http_header_value "X-Signature-SSHSIG"`,
`url="http://127.0.0.1:${HOST_PORT}/${script_name}"`,
`^# Pulse Unified Agent Installer`,
`--token-file`,
`TokenFile`,
`Install script endpoints returned required signature headers`,
`Download endpoints returned binaries with checksum and signature headers for all platforms/architectures`,
`Offline self-heal: download endpoint works with checksum and signature headers without outbound network`,
// Server installer identity guard — see the rc.1 → rc.5 regression where
// the rendered agent installer shipped as the top-level install.sh asset
// for 30 days before anyone noticed. Removing any of these unpins the asset.
`Validating install.sh is the Pulse server installer`,
`grep -qE '^# Pulse Installer Script'`,
`grep -qE '^[[:space:]]*--version\)'`,
`Pulse Unified Agent Installer`,
`bash "$install_sh_path" --help`,
`Install specific version (e.g.`,
// README key drift guard — across v6 rc.2 → rc.5 the README pinned a
// stale ed25519 key that did not verify install.sh.sshsig, so anyone
// following the secure-install path saw "Could not verify signature".
// validate-release.sh must extract the README's pinned key and actually
// run ssh-keygen -Y verify against the signed installer.
`Validating README pinned signature key matches install.sh.sshsig`,
`grep -oE "ssh-ed25519 [A-Za-z0-9+/=]+ pulse-installer" "$readme_path"`,
`ssh-keygen -Y verify \`,
`README's pinned signature key does not verify install.sh.sshsig`,
}
readmeBytes, err := os.ReadFile(repoFile("README.md"))
if err != nil {
t.Fatalf("read README.md: %v", err)
}
readme := string(readmeBytes)
// Lock in the actual signing key documented to customers. This is the public
// counterpart of PULSE_UPDATE_SIGNING_KEY and matches what install.sh and
// scripts/pulse-auto-update.sh have embedded. A future edit cannot silently
// regress to the stale Ds21c5 key without tripping this assertion.
const correctReadmeKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZd/DaH+BldzOkq1A8KVTcFk73nAyrE8aJOyf7i00jm pulse-installer"
if !strings.Contains(readme, correctReadmeKey) {
t.Fatalf("README.md must pin the correct pulse-installer ed25519 key for install.sh signature verification")
}
const staleReadmeKey = "Ds21c5oPk2khrdHlsw1aZ9EJKoTsyalGzhb0hdwJrkV"
if strings.Contains(readme, staleReadmeKey) {
t.Fatalf("README.md still references the stale pulse-installer key Ds21c5...; rc.2 → rc.5 shipped this drift")
}
// Format drift guard — ssh-keygen -Y verify -f expects an allowed_signers
// file whose FIRST field is the principal. The docs shipped the key in
// authorized_keys order (principal last, parsed as a comment), so the
// documented verification failed against a perfectly good signature.
// Reported by a customer against v6.0.5 on 2026-07-13.
const allowedSignersLine = `pulse-installer namespaces="pulse-install" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZd/DaH+BldzOkq1A8KVTcFk73nAyrE8aJOyf7i00jm pulse-installer`
if !strings.Contains(readme, allowedSignersLine) {
t.Fatalf("README.md verification snippet must publish the key as an allowed_signers line (principal first), not authorized_keys order")
}
installDocsBytes, err := os.ReadFile(repoFile("docs", "INSTALL.md"))
if err != nil {
t.Fatalf("read docs/INSTALL.md: %v", err)
}
installDocs := string(installDocsBytes)
if !strings.Contains(installDocs, correctReadmeKey) {
t.Fatalf("docs/INSTALL.md must pin the correct pulse-installer ed25519 key")
}
if strings.Contains(installDocs, staleReadmeKey) {
t.Fatalf("docs/INSTALL.md still references the stale pulse-installer key Ds21c5...")
}
if !strings.Contains(installDocs, allowedSignersLine) {
t.Fatalf("docs/INSTALL.md verification snippet must publish the key as an allowed_signers line (principal first), not authorized_keys order")
}
for _, needle := range localRequired {
if !strings.Contains(localValidator, needle) {
t.Fatalf("validate-release.sh missing signed sidecar validation: %s", needle)
}
}
if strings.Contains(localValidator, `url="http://127.0.0.1:${HOST_PORT}/download/${script_name}"`) {
t.Fatal("validate-release.sh must smoke-test /install.sh and /install.ps1, not non-existent /download/install.* routes")
}
publishedValidatorBytes, err := os.ReadFile(repoFile("scripts", "validate-published-release.sh"))
if err != nil {
t.Fatalf("read validate-published-release.sh: %v", err)
}
publishedValidator := string(publishedValidatorBytes)
publishedRequired := []string{
`RELEASE_SBOM="pulse-${TAG}-release.sbom.spdx.json"`,
`echo "Failed to download ${RELEASE_SBOM} for ${TAG}" >&2`,
`echo "${RELEASE_SBOM} is empty for ${TAG}" >&2`,
`CHECKSUMS_SIG_PATH="${TMP_DIR}/checksums.txt.sshsig"`,
`"${BASE_URL}/checksums.txt.sshsig"`,
`echo "Failed to download checksums.txt.sshsig for ${TAG}" >&2`,
`sshsig_path="${TMP_DIR}/${filename}.sshsig"`,
`"${artifact_url}.sshsig"`,
`echo "Failed to download ${filename}.sshsig" >&2`,
`Published release assets for ${TAG} match checksums.txt, *.sha256 files, and required *.sshsig sidecars.`,
}
for _, needle := range publishedRequired {
if !strings.Contains(publishedValidator, needle) {
t.Fatalf("validate-published-release.sh missing signed sidecar validation: %s", needle)
}
}
contractBytes, err := os.ReadFile(repoFile("docs", "release-control", "v6", "internal", "subsystems", "deployment-installability.md"))
if err != nil {
t.Fatalf("read deployment-installability contract: %v", err)
}
contract := string(contractBytes)
contractRequired := []string{
"`scripts/validate-release.sh`",
"`scripts/validate-published-release.sh`",
"`scripts/backfill-release-assets.sh`",
"`.github/workflows/backfill-release-assets.yml`",
"`scripts/validate-release.sh`, and",
"`scripts/release_asset_common.sh`",
"must derive the embedded update trust root",
"standalone SPDX JSON SBOM",
"already-published packet",
"derived integrity assets",
"and fail validation if",
"published artifact or",
"`checksums.txt` is missing its `.sshsig` sidecar",
"release-packet SBOM is absent",
"download endpoints must return checksum and signature headers",
"must disable Go's automatic VCS stamping",
"`-buildvcs=false`",
}
for _, needle := range contractRequired {
if !strings.Contains(contract, needle) {
t.Fatalf("deployment-installability contract missing signed sidecar validation requirement: %s", needle)
}
}
}
func TestDockerAndDemoBuildsUseCanonicalReleaseLdflags(t *testing.T) {
dockerfileBytes, err := os.ReadFile(repoFile("Dockerfile"))
if err != nil {
t.Fatalf("read Dockerfile: %v", err)
}
dockerfile := string(dockerfileBytes)
dockerRequired := []string{
`FROM --platform=linux/amd64 node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 AS frontend-builder`,
`FROM --platform=linux/amd64 golang:1.26.5-alpine@sha256:0178a641fbb4858c5f1b48e34bdaabe0350a330a1b1149aabd498d0699ff5fb2 AS backend-builder`,
`FROM backend-builder AS release-assets-builder`,
`FROM alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc AS agent_runtime`,
`FROM alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc AS pulse-runtime-base`,
`FROM pulse-runtime-base AS hosted_runtime`,
`FROM pulse-runtime-base AS runtime`,
`COPY scripts/release_ldflags.sh ./scripts/release_ldflags.sh`,
`COPY scripts/release_update_key.go ./scripts/release_update_key.go`,
`COPY scripts/render_installers.go ./scripts/render_installers.go`,
`ARG PULSE_LICENSE_PUBLIC_KEY_SHA256`,
`--mount=type=secret,id=pulse_license_public_key,required=false`,
`--mount=type=secret,id=pulse_update_signing_key,required=false`,
`ARG PULSE_UPDATE_SIGNING_PUBLIC_KEY`,
`LICENSE_PUBLIC_KEY="$(tr -d '\r\n' < /run/secrets/pulse_license_public_key)"`,
`EXPECTED_LICENSE_PUBLIC_KEY_SHA256="${PULSE_LICENSE_PUBLIC_KEY_SHA256#SHA256:}"`,
`mounted license public key does not match PULSE_LICENSE_PUBLIC_KEY_SHA256.`,
`UPDATE_PUBLIC_KEYS="$(go run ./scripts/release_update_key.go public-key --private-key "${UPDATE_SIGNING_KEY}")"`,
`mounted update signing key does not match PULSE_UPDATE_SIGNING_PUBLIC_KEY.`,
`./scripts/release_ldflags.sh server --version "${VERSION}" --build-time "${BUILD_TIME}" --git-commit "${GIT_COMMIT}"`,
`./scripts/release_ldflags.sh agent --version "${VERSION}"`,
`-buildvcs=false`,
`go run ./scripts/render_installers.go --source-dir ./scripts --output-dir /app/rendered-installers`,
`--allow-empty-installer-ssh-public-key`,
`ssh-keygen -q -Y sign -f "${OPENSSH_SIGNING_KEY}" -n pulse-install`,
`COPY --from=release-assets-builder /app/rendered-installers/install.sh /opt/pulse/scripts/install.sh`,
`COPY --from=release-assets-builder /app/pulse-agent-* /opt/pulse/bin/`,
}
for _, needle := range dockerRequired {
if !strings.Contains(dockerfile, needle) {
t.Fatalf("Dockerfile missing canonical release ldflags usage: %s", needle)
}
}
hostedStart := strings.Index(dockerfile, `FROM pulse-runtime-base AS hosted_runtime`)
runtimeStart := strings.Index(dockerfile, `FROM pulse-runtime-base AS runtime`)
if hostedStart == -1 || runtimeStart == -1 || hostedStart > runtimeStart {
t.Fatal("Dockerfile must define hosted_runtime from pulse-runtime-base before the full runtime stage")
}
hostedStage := dockerfile[hostedStart:runtimeStart]
if strings.Contains(hostedStage, "rendered-installers") || strings.Contains(hostedStage, "/opt/pulse/bin") {
t.Fatalf("hosted_runtime target must not depend on installer rendering or embedded agent artifacts:\n%s", hostedStage)
}
if strings.Contains(dockerfile, `FROM --platform=linux/amd64 node:20-alpine AS frontend-builder`) ||
strings.Contains(dockerfile, `FROM --platform=linux/amd64 golang:1.26.5-alpine AS backend-builder`) ||
strings.Contains(dockerfile, `FROM alpine:3.20 AS agent_runtime`) ||
strings.Contains(dockerfile, `FROM alpine:3.20 AS pulse-runtime-base`) {
t.Fatal("Dockerfile base images must be pinned by immutable @sha256 digests")
}
if builds, cleanBuilds := strings.Count(dockerfile, " go build \\"), strings.Count(dockerfile, "-buildvcs=false"); builds != cleanBuilds {
t.Fatalf("Dockerfile release go builds must all disable automatic VCS stamping: builds=%d clean_builds=%d", builds, cleanBuilds)
}
workflowBytes, err := os.ReadFile(repoFile(".github", "workflows", "deploy-demo-server.yml"))
if err != nil {
t.Fatalf("read deploy-demo-server workflow: %v", err)
}
workflow := string(workflowBytes)
workflowRequired := []string{
`./scripts/release_ldflags.sh server --version "${VERSION}" --build-time "${BUILD_TIME}" --git-commit "${GIT_COMMIT}"`,
`-buildvcs=false`,
`demo-stable`,
`workflow_dispatch:`,
`target:`,
}
for _, needle := range workflowRequired {
if !strings.Contains(workflow, needle) {
t.Fatalf("deploy-demo-server workflow missing canonical release ldflags usage: %s", needle)
}
}
if strings.Contains(workflow, `preview-v6`) || strings.Contains(workflow, `demo-preview-v6`) {
t.Fatal("deploy-demo-server workflow must not keep a separate v6 preview demo target after GA")
}
}
func TestAgentRuntimeImageDefaultsToUnifiedHostAndDockerMonitoring(t *testing.T) {
dockerfileBytes, err := os.ReadFile(repoFile("Dockerfile"))
if err != nil {
t.Fatalf("read Dockerfile: %v", err)
}
dockerfile := string(dockerfileBytes)
required := []string{
`mkdir -p /var/lib/pulse-agent`,
`PULSE_DISABLE_AUTO_UPDATE=true`,
`PULSE_ENABLE_HOST=true`,
`PULSE_ENABLE_DOCKER=true`,
`PULSE_AGENT_ID_FILE=/var/lib/pulse-agent/agent-id`,
`PULSE_STATE_DIR=/var/lib/pulse-agent`,
`VOLUME ["/var/lib/pulse-agent"]`,
`ENTRYPOINT ["/usr/local/bin/pulse-agent"]`,
}
for _, needle := range required {
if !strings.Contains(dockerfile, needle) {
t.Fatalf("Dockerfile agent_runtime missing unified host and Docker contract: %s", needle)
}
}
if strings.Contains(dockerfile, `PULSE_ENABLE_HOST=false`) {
t.Fatal("agent_runtime must not silently force every deployment into workload-only mode")
}
if strings.Contains(dockerfile, `ENTRYPOINT ["/usr/local/bin/pulse-agent", "--enable-docker", "--enable-host=false"]`) {
t.Fatal("agent_runtime must not hard-code module flags in ENTRYPOINT; env defaults keep user args overridable")
}
}
func TestReleaseCandidateRequiresPlatformNativeAgentSigning(t *testing.T) {
candidateWorkflowPath := repoFile(".github", "workflows", "build-release-candidate.yml")
assertFileContainsAll(t, candidateWorkflowPath,
`require_macos_signing:`,
`require_windows_signing:`,
`sign-macos-agent:`,
`codesign --force --timestamp --options runtime`,
`xcrun notarytool submit`,
`--output-format json > notarization-result.json`,
`result.get('status') != 'Accepted'`,
`codesign --verify --deep --strict --verbose=2`,
`sign-windows-agent:`,
`windows_signing_backend:`,
`signpath/github-action-submit-signing-request@b9d91eadd323de506c0c81cf0c7fe7438f3360fd # v2`,
`github-artifact-id: ${{ steps.upload-unsigned-windows.outputs.artifact-id }}`,
`SIGNPATH_API_TOKEN`,
`SIGNPATH_ORGANIZATION_ID`,
`SIGNPATH_PROJECT_SLUG`,
`SIGNPATH_SIGNING_POLICY_SLUG`,
`SIGNPATH_ARTIFACT_CONFIGURATION_SLUG`,
`SIGNPATH_EXPECTED_CERTIFICATE_SUBJECT`,
`signtool sign`,
`signtool verify /pa /v`,
`windows-signing-evidence.json`,
`signerThumbprint`,
`PULSE_AGENT_NATIVE_BINARIES_DIR:`,
)
candidateWorkflow, err := os.ReadFile(candidateWorkflowPath)
if err != nil {
t.Fatalf("read build-release-candidate.yml: %v", err)
}
if strings.Contains(string(candidateWorkflow), `spctl --assess --type execute`) {
t.Fatal("bare command-line Mach-O binaries must not use Gatekeeper app assessment after notarization")
}
assertFileContainsAll(t, repoFile(".github", "workflows", "create-release.yml"),
`require_macos_signing: true`,
`require_windows_signing: ${{ needs.prepare.outputs.require_windows_signing == 'true' }}`,
`unsigned_windows_exception:`,
`unsigned_windows_reason:`,
`windows_signing_backend: signpath`,
)
assertFileContainsAll(t, repoFile(".github", "workflows", "release-dry-run.yml"),
`Definitive Dry-Run Verdict`,
`require_windows_signing: ${{ !contains(inputs.version, '-') && !((inputs.version == '6.1.0' || inputs.version == '6.1.1' || inputs.version == '6.1.2') && inputs.unsigned_windows_exception) }}`,
`require_result "exact-SHA release candidate" "$CANDIDATE_RESULT" success`,
`require_result "stable demo no-mutation verification" "$DEMO_RESULT" success`,
)
assertFileContainsAll(t, repoFile("scripts", "release_control", "resolve_release_promotion.py"),
`version not in {"6.1.0", "6.1.1", "6.1.2"}`,
`unsigned_windows_reason is required`,
`not Authenticode-signed`,
`require_windows_signing = not is_prerelease and not unsigned_windows_exception`,
)
assertFileContainsAll(t, repoFile("scripts", "build-release.sh"),
`PULSE_AGENT_NATIVE_BINARIES_DIR`,
`native_targets=()`,
`PULSE_REQUIRE_MACOS_SIGNING:-false`,
`native_targets+=(darwin-amd64 darwin-arm64)`,
`PULSE_REQUIRE_WINDOWS_SIGNING:-false`,
`native_targets+=(windows-amd64 windows-arm64 windows-386)`,
`Applied required platform-native signed Unified Agent binaries.`,
`required native signing is enabled but PULSE_AGENT_NATIVE_BINARIES_DIR is empty.`,
)
}
func TestReleaseWorkflowsUseSecretSafeAttestedImageBuilds(t *testing.T) {
createReleaseBytes, err := os.ReadFile(repoFile(".github", "workflows", "create-release.yml"))
if err != nil {
t.Fatalf("read create-release.yml: %v", err)
}
candidateWorkflowBytes, err := os.ReadFile(repoFile(".github", "workflows", "build-release-candidate.yml"))
if err != nil {
t.Fatalf("read build-release-candidate.yml: %v", err)
}
createRelease := string(createReleaseBytes) + "\n" + string(candidateWorkflowBytes)
createReleaseRequired := []string{
`provenance: mode=max`,
`sbom: true`,
`secrets: |`,
`id: license_key_cache`,
`PULSE_LICENSE_PUBLIC_KEY_SHA256=${{ steps.license_key_cache.outputs.sha256 }}`,
`PULSE_UPDATE_SIGNING_PUBLIC_KEY=${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}`,
`pulse_license_public_key=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}`,
`pulse_update_signing_key=${{ secrets.PULSE_UPDATE_SIGNING_KEY }}`,
`PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}`,
`Validate installer signing key pins`,
`go run ./scripts/release_update_key.go public-key-ssh`,
`install.sh scripts/pulse-auto-update.sh release/pulse-auto-update.sh`,
`does not trust the configured release signing key.`,
`DOCKER_BUILDKIT: 1`,
`--secret id=pulse_license_public_key,env=PULSE_LICENSE_PUBLIC_KEY`,
`--secret id=pulse_update_signing_key,env=PULSE_UPDATE_SIGNING_KEY`,
`--build-arg PULSE_LICENSE_PUBLIC_KEY_SHA256="${PULSE_LICENSE_PUBLIC_KEY_SHA256}"`,
`--build-arg PULSE_UPDATE_SIGNING_PUBLIC_KEY="${PULSE_UPDATE_SIGNING_PUBLIC_KEY}"`,
`id-token: write`,
`attestations: write`,
`uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4`,
}
for _, needle := range createReleaseRequired {
if !strings.Contains(createRelease, needle) {
t.Fatalf("create-release.yml missing attested secret-safe release build contract: %s", needle)
}
}
if strings.Contains(createRelease, `PULSE_LICENSE_PUBLIC_KEY=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}`) {
t.Fatal("create-release.yml must not pass the license public key through docker build args")
}
publishBytes, err := os.ReadFile(repoFile(".github", "workflows", "publish-docker.yml"))
if err != nil {
t.Fatalf("read publish-docker.yml: %v", err)
}
publish := string(publishBytes)
publishRequired := []string{
`provenance: mode=max`,
`sbom: true`,
`secrets: |`,
`id: license_key_cache`,
`id: build_control_plane_image`,
`file: deploy/provider-msp/Dockerfile.control-plane`,
`PULSE_LICENSE_PUBLIC_KEY_SHA256=${{ steps.license_key_cache.outputs.sha256 }}`,
`PULSE_UPDATE_SIGNING_PUBLIC_KEY=${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}`,
`pulse_license_public_key=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}`,
`pulse_update_signing_key=${{ secrets.PULSE_UPDATE_SIGNING_KEY }}`,
`subject-name: docker.io/rcourtman/pulse`,
`subject-name: ghcr.io/${{ github.repository_owner }}/pulse`,
`subject-name: docker.io/rcourtman/pulse-control-plane`,
`subject-name: ghcr.io/${{ github.repository_owner }}/pulse-control-plane`,
`rcourtman/pulse-control-plane:${{ steps.version.outputs.tag }}`,
`ghcr.io/${{ github.repository_owner }}/pulse-control-plane:${{ steps.version.outputs.tag }}`,
// pulse-agent ships as release-asset binaries, not as a Docker
// image (see commit dropping the agent image publish steps).
// The agent attestation subject-names intentionally do not
// appear in publish-docker.yml.
`push-to-registry: true`,
`create-storage-record: false`,
`id-token: write`,
`attestations: write`,
}
for _, needle := range publishRequired {
if !strings.Contains(publish, needle) {
t.Fatalf("publish-docker.yml missing attested secret-safe publish contract: %s", needle)
}
}
if strings.Contains(publish, `PULSE_LICENSE_PUBLIC_KEY=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}`) {
t.Fatal("publish-docker.yml must not pass the license public key through docker build args")
}
}
func TestDeploymentDefaultsPinVersionedImagesAndHelmDocsChecksum(t *testing.T) {
versionBytes, err := os.ReadFile(repoFile("VERSION"))
if err != nil {
t.Fatalf("read VERSION: %v", err)
}
version := strings.TrimSpace(string(versionBytes))
if version == "" {
t.Fatal("VERSION is empty")
}
composeBytes, err := os.ReadFile(repoFile("docker-compose.yml"))
if err != nil {
t.Fatalf("read docker-compose.yml: %v", err)
}
compose := string(composeBytes)
if !strings.Contains(compose, "image: ${PULSE_IMAGE:-rcourtman/pulse:"+version+"}") {
t.Fatalf("docker-compose.yml must pin the governed release version:\n%s", compose)
}
if strings.Contains(compose, ":latest") {
t.Fatalf("docker-compose.yml must not default to a floating latest tag:\n%s", compose)
}
installDockerBytes, err := os.ReadFile(repoFile("scripts", "install-docker.sh"))
if err != nil {
t.Fatalf("read install-docker.sh: %v", err)
}
installDocker := string(installDockerBytes)
if !strings.Contains(installDocker, `CANONICAL_DEFAULT_PULSE_VERSION="`+version+`"`) {
t.Fatalf("install-docker.sh must pin the governed release version:\n%s", installDocker)
}
if strings.Contains(installDocker, ":latest") {
t.Fatalf("install-docker.sh must not default to a floating latest tag:\n%s", installDocker)
}
chartBytes, err := os.ReadFile(repoFile("deploy", "helm", "pulse", "Chart.yaml"))
if err != nil {
t.Fatalf("read Helm Chart.yaml: %v", err)
}
chart := string(chartBytes)
chartRequired := []string{
"version: " + version,
`appVersion: "` + version + `"`,
"https://raw.githubusercontent.com/rcourtman/Pulse/v" + version + "/docs/images/pulse-logo.svg",
"https://github.com/rcourtman/Pulse/blob/v" + version + "/docs/KUBERNETES.md",
}
for _, needle := range chartRequired {
if !strings.Contains(chart, needle) {
t.Fatalf("Helm Chart.yaml must pin the governed release version, missing %s:\n%s", needle, chart)
}
}
if previous, ok := previousStablePatchVersion(version); ok && strings.Contains(chart, "v"+previous) {
t.Fatalf("Helm Chart.yaml must not retain the previous stable patch tag v%s:\n%s", previous, chart)
}
if previous, ok := previousPrereleaseVersion(version); ok && strings.Contains(chart, "v"+previous) {
t.Fatalf("Helm Chart.yaml must not retain the previous prerelease tag v%s:\n%s", previous, chart)
}
chartReadmeBytes, err := os.ReadFile(repoFile("deploy", "helm", "pulse", "README.md"))
if err != nil {
t.Fatalf("read Helm README.md: %v", err)
}
chartReadme := string(chartReadmeBytes)
badgeVersion := shieldsBadgeMessage(version)
chartReadmeRequired := []string{
"![Version: " + version + "](https://img.shields.io/badge/Version-" + badgeVersion + "-informational?style=flat-square)",
"![AppVersion: " + version + "](https://img.shields.io/badge/AppVersion-" + badgeVersion + "-informational?style=flat-square)",
}
for _, needle := range chartReadmeRequired {
if !strings.Contains(chartReadme, needle) {
t.Fatalf("Helm README.md must reflect the governed release version, missing %s:\n%s", needle, chartReadme)
}
}
if previous, ok := previousStablePatchVersion(version); ok && strings.Contains(chartReadme, previous) {
t.Fatalf("Helm README.md must not retain the previous stable patch version %s:\n%s", previous, chartReadme)
}
if previous, ok := previousPrereleaseVersion(version); ok && strings.Contains(chartReadme, previous) {
t.Fatalf("Helm README.md must not retain the previous prerelease version %s:\n%s", previous, chartReadme)
}
helmPagesBytes, err := os.ReadFile(repoFile(".github", "workflows", "helm-pages.yml"))
if err != nil {
t.Fatalf("read helm-pages.yml: %v", err)
}
helmPages := string(helmPagesBytes)
required := []string{
`HELM_DOCS_VERSION="1.14.2"`,
`HELM_DOCS_ARCHIVE="helm-docs_${HELM_DOCS_VERSION}_Linux_x86_64.tar.gz"`,
`HELM_DOCS_SHA256="a8cf72ada34fad93285ba2a452b38bdc5bd52cc9a571236244ec31022928d6cc"`,
`sha256sum --check --`,
`name: Ensure chart release and pages index`,
`gh release create "${CHART_RELEASE}" "${CHART_PATH}"`,
`helm repo index "${index_work}"`,
`git -C "${workdir}/gh-pages" push origin HEAD:gh-pages`,
`grep -q "version: ${VERSION}"`,
}
for _, needle := range required {
if !strings.Contains(helmPages, needle) {
t.Fatalf("helm-pages.yml missing checksum-verified helm-docs install step: %s", needle)
}
}
}
func TestHelmChartDoesNotPublishRetiredExplorePrepassMonitoring(t *testing.T) {
chartDir := repoFile("deploy", "helm", "pulse")
err := filepath.WalkDir(chartDir, func(path string, d os.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if d.IsDir() {
return nil
}
switch filepath.Ext(path) {
case ".yaml", ".json", ".md":
default:
return nil
}
content, readErr := os.ReadFile(path)
if readErr != nil {
return readErr
}
text := string(content)
for _, forbidden := range []string{
"prometheusRule",
"pulse_ai_explore",
"Explore pre-pass",
"explore_runs_total",
} {
if strings.Contains(text, forbidden) {
t.Fatalf("helm chart file %s must not publish retired Assistant explore-prepass monitoring %q", path, forbidden)
}
}
return nil
})
if err != nil {
t.Fatalf("walk helm chart: %v", err)
}
}
func TestDeployDemoWorkflowFailsClosedForStableAndVerifiesFrontendParity(t *testing.T) {
workflowBytes, err := os.ReadFile(repoFile(".github", "workflows", "deploy-demo-server.yml"))
if err != nil {
t.Fatalf("read deploy-demo-server workflow: %v", err)
}
workflow := string(workflowBytes)
required := []string{
`DEMO_EXPECTED_HOSTNAME: ${{ vars.DEMO_EXPECTED_HOSTNAME }}`,
`DEMO_LOCAL_BASE_URL: ${{ vars.DEMO_LOCAL_BASE_URL }}`,
`[ -n "$DEMO_EXPECTED_HOSTNAME" ] || { echo "::error::DEMO_EXPECTED_HOSTNAME is required in the selected demo environment."; exit 1; }`,
`[ -n "$DEMO_LOCAL_BASE_URL" ] || { echo "::error::DEMO_LOCAL_BASE_URL is required in the selected demo environment."; exit 1; }`,
`ENVIRONMENT_NAME="demo-stable"`,
`options:`,
` - stable`,
`Capture expected frontend entry asset`,
`Verify target host identity`,
`uses: tailscale/github-action@306e68a486fd2350f2bfc3b19fcd143891a4a2d8 # v4`,
`ping: ${{ secrets.DEMO_SERVER_HOST }}`,
`bash .github/scripts/check-demo-reachability.sh`,
`bash .github/scripts/setup-demo-ssh.sh`,
`SERVICE_NAME="pulse"`,
`Unsupported demo target: ${TARGET}`,
`Demo environment points at host $REMOTE_HOSTNAME but expected $DEMO_EXPECTED_HOSTNAME.`,
`Verify frontend parity`,
`Verify public browser smoke`,
`./scripts/run_demo_public_browser_smoke.sh`,
`extract_entry_asset()`,
`<script\b[^>]*\bsrc=\"(/assets/index-[^\"]*\.js)\"`,
`Remote service is serving $REMOTE_ASSET but the build expected $EXPECTED_ASSET.`,
`Public demo is serving $PUBLIC_ASSET but the build expected $EXPECTED_ASSET.`,
}
for _, needle := range required {
if !strings.Contains(workflow, needle) {
t.Fatalf("deploy-demo-server workflow missing stable isolation or frontend parity proof: %s", needle)
}
}
for _, forbidden := range []string{`pulse-v6-preview`, `preview-v6`, `demo-preview-v6`} {
if strings.Contains(workflow, forbidden) {
t.Fatalf("deploy-demo-server workflow must not retain retired v6 preview target %s", forbidden)
}
}
}
func TestUpdateDemoWorkflowUsesGovernedNetworkPath(t *testing.T) {
workflowBytes, err := os.ReadFile(repoFile(".github", "workflows", "update-demo-server.yml"))
if err != nil {
t.Fatalf("read update-demo-server workflow: %v", err)
}
workflow := string(workflowBytes)
required := []string{
`- name: Tailscale`,
`uses: tailscale/github-action@306e68a486fd2350f2bfc3b19fcd143891a4a2d8 # v4`,
`oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}`,
`oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}`,
`tags: tag:infra`,
`version: '1.94.2'`,
`ping: ${{ secrets.DEMO_SERVER_HOST }}`,
`bash .github/scripts/check-demo-reachability.sh`,
`workflow_call:`,
`verify_only:`,
`Refuse mutation during verification-only checks`,
`uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0`,
`go run ./scripts/release_update_key.go public-key-ssh`,
`sed -i "s|^PINNED_RELEASE_SSH_PUBLIC_KEY=.*|PINNED_RELEASE_SSH_PUBLIC_KEY=\"${TRUSTED_SSH_PUBLIC_KEY}\"|" /tmp/pulse-install.sh`,
`Verify target host identity`,
`bash .github/scripts/setup-demo-ssh.sh`,
`Demo environment points at host $REMOTE_HOSTNAME but expected $DEMO_EXPECTED_HOSTNAME.`,
`Prepare demo host storage`,
`KEEP_BACKUPS=2`,
`Removing demo backup to restore install headroom: %s`,
`Pruning demo volatile runtime stores to restore install headroom.`,
`sudo find "$CONFIG_DIR" -xdev -type f`,
`-name "metrics.db"`,
`Removing demo volatile store: %s`,
`Demo host does not have enough free space to back up $CONFIG_DIR before install.`,
`Restore demo runtime configuration`,
`resolve_config_dir`,
`set_env_value DEMO_MODE true`,
`set_env_value PULSE_MOCK_MODE true`,
`ensure_demo_fixture_entitlement`,
`"demo_fixtures"`,
`del(.integrity)`,
`Demo fixture entitlement ensured in governed demo billing state.`,
`/api/license/runtime-capabilities`,
`Mock mode enabled`,
`Demo server mock mode did not enable after entitlement sync`,
`Verify public browser smoke`,
`./scripts/run_demo_public_browser_smoke.sh`,
}
for _, needle := range required {
if !strings.Contains(workflow, needle) {
t.Fatalf("update-demo-server workflow missing governed network path: %s", needle)
}
}
}
func TestDemoSshSetupHelperHandlesIpLiteralTargets(t *testing.T) {
helperBytes, err := os.ReadFile(repoFile(".github", "scripts", "setup-demo-ssh.sh"))
if err != nil {
t.Fatalf("read demo ssh setup helper: %v", err)
}
helper := string(helperBytes)
required := []string{
`is_ip_literal()`,
`ipaddress.ip_address(sys.argv[1])`,
`host_needs_dns=false`,
`Demo SSH host is an IP literal; skipping DNS resolution wait.`,
`[ "$host_needs_dns" = "true" ] && ! getent hosts "$DEMO_SERVER_HOST"`,
`ssh-keyscan -T 10 -H "$DEMO_SERVER_HOST"`,
`MAX_SSH_SETUP_ATTEMPTS="${DEMO_SSH_SETUP_ATTEMPTS:-3}"`,
`Demo network preflight passed, but ssh-keyscan did not return host keys.`,
}
for _, needle := range required {
if !strings.Contains(helper, needle) {
t.Fatalf("demo ssh setup helper missing guarded IP/hostname behavior: %s", needle)
}
}
tmpDir := t.TempDir()
fakeBin := filepath.Join(tmpDir, "bin")
if err := os.MkdirAll(fakeBin, 0o755); err != nil {
t.Fatalf("create fake bin: %v", err)
}
getentMarker := filepath.Join(tmpDir, "getent-called")
if err := os.WriteFile(filepath.Join(fakeBin, "getent"), []byte("#!/bin/sh\n: > \"$GETENT_MARKER\"\nexit 1\n"), 0o755); err != nil {
t.Fatalf("write fake getent: %v", err)
}
if err := os.WriteFile(filepath.Join(fakeBin, "ssh-keyscan"), []byte("#!/bin/sh\nprintf '100.109.163.95 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDemo\\n'\n"), 0o755); err != nil {
t.Fatalf("write fake ssh-keyscan: %v", err)
}
homeDir := filepath.Join(tmpDir, "home")
cmd := exec.Command("bash", repoFile(".github", "scripts", "setup-demo-ssh.sh"))
cmd.Env = append(os.Environ(),
"DEMO_SERVER_HOST=100.109.163.95",
"DEMO_SERVER_SSH_KEY=fake-private-key",
"GETENT_MARKER="+getentMarker,
"HOME="+homeDir,
"PATH="+fakeBin+string(os.PathListSeparator)+os.Getenv("PATH"),
)
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("demo ssh setup helper failed for IP literal: %v\n%s", err, output)
}
if _, err := os.Stat(getentMarker); !os.IsNotExist(err) {
t.Fatalf("demo ssh setup helper must not require getent hosts for IP literals; stat err=%v", err)
}
knownHosts, err := os.ReadFile(filepath.Join(homeDir, ".ssh", "known_hosts"))
if err != nil {
t.Fatalf("read generated known_hosts: %v", err)
}
if !strings.Contains(string(knownHosts), "ssh-ed25519") {
t.Fatalf("known_hosts missing captured key: %s", knownHosts)
}
if !strings.Contains(string(output), "Demo SSH host is an IP literal; skipping DNS resolution wait.") {
t.Fatalf("helper output did not report IP literal path: %s", output)
}
}
func TestDemoReachabilityHelperSeparatesTailnetAndSshTransportProof(t *testing.T) {
helperBytes, err := os.ReadFile(repoFile(".github", "scripts", "check-demo-reachability.sh"))
if err != nil {
t.Fatalf("read demo reachability helper: %v", err)
}
helper := string(helperBytes)
for _, needle := range []string{
`tailscale status --json`,
`tailscale ping --c 3 --timeout 10s "$DEMO_SERVER_HOST"`,
`nc -z -w 5 "$DEMO_SERVER_HOST" "$TCP_PORT"`,
`Runner Tailscale DNS:`,
`Runner Tailscale tags:`,
`Demo peer is not present in the runner peer map yet.`,
`Verify sshd and the host firewall on tailscale0.`,
} {
if !strings.Contains(helper, needle) {
t.Fatalf("demo reachability helper missing diagnostic contract: %s", needle)
}
}
tmpDir := t.TempDir()
fakeBin := filepath.Join(tmpDir, "bin")
if err := os.MkdirAll(fakeBin, 0o755); err != nil {
t.Fatalf("create fake bin: %v", err)
}
tailscaleScript := `#!/bin/sh
if [ "$1" = "status" ]; then
printf '%s\n' '{"BackendState":"Running","Self":{"TailscaleIPs":["100.100.100.1"]},"Peer":{"demo":{"TailscaleIPs":["100.109.163.95"],"Online":true,"Active":true,"Relay":"lhr"}}}'
exit 0
fi
if [ "$1" = "ping" ]; then
echo 'pong from demo'
exit 0
fi
exit 1
`
if err := os.WriteFile(filepath.Join(fakeBin, "tailscale"), []byte(tailscaleScript), 0o755); err != nil {
t.Fatalf("write fake tailscale: %v", err)
}
if err := os.WriteFile(filepath.Join(fakeBin, "nc"), []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
t.Fatalf("write fake nc: %v", err)
}
cmd := exec.Command("bash", repoFile(".github", "scripts", "check-demo-reachability.sh"))
cmd.Env = append(os.Environ(),
"DEMO_SERVER_HOST=100.109.163.95",
"PATH="+fakeBin+string(os.PathListSeparator)+os.Getenv("PATH"),
)
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("demo reachability helper failed: %v\n%s", err, output)
}
for _, needle := range []string{"Tailscale backend: Running", "Demo peer state: online=True active=True relay=lhr", "Demo SSH transport is reachable over Tailscale."} {
if !strings.Contains(string(output), needle) {
t.Fatalf("demo reachability output missing %q: %s", needle, output)
}
}
}
func TestDemoPublicBrowserSmokeWaitsForVisibleLoginUI(t *testing.T) {
scriptBytes, err := os.ReadFile(repoFile("scripts", "demo_public_browser_smoke.cjs"))
if err != nil {
t.Fatalf("read demo public browser smoke script: %v", err)
}
script := string(scriptBytes)
required := []string{
`waitUntil: 'domcontentloaded'`,
`getByLabel('Username').waitFor({ state: 'visible', timeout: 120000 })`,
`getByLabel('Password').waitFor({ state: 'visible', timeout: 120000 })`,
`getByRole('button', { name: 'Sign in to Pulse' }).waitFor({ state: 'visible', timeout: 120000 })`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("demo public browser smoke missing visible-login readiness proof: %s", needle)
}
}
if strings.Contains(script, `waitUntil: 'networkidle'`) {
t.Fatal("demo public browser smoke still depends on networkidle instead of visible login readiness")
}
}
func TestDockerfileStagesShippedDocsForEmbeddedFrontendBuild(t *testing.T) {
dockerfileBytes, err := os.ReadFile(repoFile("Dockerfile"))
if err != nil {
t.Fatalf("read Dockerfile: %v", err)
}
dockerfile := string(dockerfileBytes)
required := []string{
`COPY docs/ /app/docs/`,
`COPY SECURITY.md TERMS.md /app/`,
}
for _, needle := range required {
if !strings.Contains(dockerfile, needle) {
t.Fatalf("Dockerfile missing shipped-doc build input: %s", needle)
}
}
dockerignoreBytes, err := os.ReadFile(repoFile(".dockerignore"))
if err != nil {
t.Fatalf("read .dockerignore: %v", err)
}
dockerignore := string(dockerignoreBytes)
requiredAllowlist := []string{
`!docs/`,
`!docs/**`,
`!SECURITY.md`,
`!TERMS.md`,
}
for _, needle := range requiredAllowlist {
if !strings.Contains(dockerignore, needle) {
t.Fatalf(".dockerignore missing shipped-doc allowlist entry: %s", needle)
}
}
}
func TestDockerfileStampsTelemetryDeploymentMethod(t *testing.T) {
dockerfileBytes, err := os.ReadFile(repoFile("Dockerfile"))
if err != nil {
t.Fatalf("read Dockerfile: %v", err)
}
if !strings.Contains(string(dockerfileBytes), `ENV PULSE_DEPLOYMENT_METHOD=container_other`) {
t.Fatal("Dockerfile must stamp the closed fallback deployment method for container images")
}
}
func TestReleaseUpdateKeyFingerprintUsesCanonicalRawPublicKeyHash(t *testing.T) {
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate signing key: %v", err)
}
cmd := exec.Command("go", "run", "./scripts/release_update_key.go", "fingerprint", "--private-key", base64.StdEncoding.EncodeToString(privateKey))
cmd.Dir = repoFile()
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("release_update_key.go fingerprint failed: %v\n%s", err, output)
}
sum := sha256.Sum256(publicKey)
expected := "SHA256:" + base64.StdEncoding.EncodeToString(sum[:])
if got := strings.TrimSpace(string(output)); got != expected {
t.Fatalf("fingerprint mismatch: got %q want %q", got, expected)
}
}
func TestReleaseUpdateKeyPublicKeySSHAcceptsPublicKey(t *testing.T) {
publicKey, _, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate signing key: %v", err)
}
cmd := exec.Command("go", "run", "./scripts/release_update_key.go", "public-key-ssh", "--public-key", base64.StdEncoding.EncodeToString(publicKey), "--comment", "pulse-installer")
cmd.Dir = repoFile()
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("release_update_key.go public-key-ssh failed: %v\n%s", err, output)
}
sshPublicKey, err := ssh.NewPublicKey(publicKey)
if err != nil {
t.Fatalf("derive SSH public key: %v", err)
}
expected := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(sshPublicKey))) + " pulse-installer"
if got := strings.TrimSpace(string(output)); got != expected {
t.Fatalf("SSH public key mismatch: got %q want %q", got, expected)
}
}
func TestReleaseAssetCommonRunsUpdateKeyThroughModulePath(t *testing.T) {
if _, err := exec.LookPath("bash"); err != nil {
t.Skip("bash not installed")
}
if _, err := exec.LookPath("go"); err != nil {
t.Skip("go not installed")
}
cmd := exec.Command("bash", "-lc", "source ./scripts/release_asset_common.sh; pulse_release_go_run_update_key")
cmd.Dir = repoFile()
output, err := cmd.CombinedOutput()
if err == nil {
t.Fatalf("expected release_update_key.go usage failure, got success:\n%s", output)
}
text := string(output)
if !strings.Contains(text, "release_update_key.go public-key") {
t.Fatalf("expected release_update_key.go usage output, got:\n%s", output)
}
if strings.Contains(text, "use of internal package") {
t.Fatalf("release helper invoked update key outside module import boundary:\n%s", output)
}
}
func TestReleaseAssetCommonRejectsUnexpectedUpdateSigningPublicKey(t *testing.T) {
if _, err := exec.LookPath("bash"); err != nil {
t.Skip("bash not installed")
}
if _, err := exec.LookPath("go"); err != nil {
t.Skip("go not installed")
}
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate signing key: %v", err)
}
unexpectedPublicKey, _, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate unexpected public key: %v", err)
}
cmd := exec.Command("bash", "-lc", "source ./scripts/release_asset_common.sh; pulse_release_prepare_signing_state pulse-installer pulse-install")
cmd.Dir = repoFile()
cmd.Env = append(os.Environ(),
"PULSE_UPDATE_SIGNING_KEY="+base64.StdEncoding.EncodeToString(privateKey),
"PULSE_UPDATE_SIGNING_PUBLIC_KEY="+base64.StdEncoding.EncodeToString(unexpectedPublicKey),
)
output, err := cmd.CombinedOutput()
if err == nil {
t.Fatalf("expected release_asset_common.sh to reject a mismatched signing public key:\n%s", output)
}
if !strings.Contains(string(output), "does not match PULSE_UPDATE_SIGNING_PUBLIC_KEY") {
t.Fatalf("expected mismatched signing public key error, got:\n%s", output)
}
}
// TestBuildReleasePackagesPulseMcpForAllPlatforms pins the
// distribution path for pulse-mcp: each Pulse release must build
// the MCP adapter for the same multi-OS matrix as the unified
// agent and emit per-platform tarballs/zips, bare binaries (for
// /releases/latest/download/ redirect compatibility), and the
// install-mcp.sh script into RELEASE_DIR. Drift in any of those
// strings means an integrator following the published install
// path hits a 404 on the release endpoint instead of a working
// binary.
func TestBuildReleasePackagesPulseMcpForAllPlatforms(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "build-release.sh"))
if err != nil {
t.Fatalf("read build-release.sh: %v", err)
}
script := string(content)
required := []string{
// Build loop wires through ./cmd/pulse-mcp.
`-o "$output_path" \
./cmd/pulse-mcp`,
// Per-platform packaging follows the pulse-agent shape
// exactly so the upload step's glob does not need
// special cases.
`tar -czf "$RELEASE_DIR/pulse-mcp-v${VERSION}-linux-amd64.tar.gz" -C "$BUILD_DIR" pulse-mcp-linux-amd64`,
`tar -czf "$RELEASE_DIR/pulse-mcp-v${VERSION}-darwin-arm64.tar.gz" -C "$BUILD_DIR" pulse-mcp-darwin-arm64`,
`zip -j "$RELEASE_DIR/pulse-mcp-v${VERSION}-windows-amd64.zip" "$BUILD_DIR/pulse-mcp-windows-amd64.exe"`,
// Bare-binary copies for the /releases/latest/download/
// redirect that install-mcp.sh fetches by default.
`cp "$BUILD_DIR/pulse-mcp-linux-amd64" "$RELEASE_DIR/"`,
`cp "$BUILD_DIR/pulse-mcp-darwin-amd64" "$RELEASE_DIR/"`,
`cp "$BUILD_DIR/pulse-mcp-darwin-arm64" "$RELEASE_DIR/"`,
`cp "$BUILD_DIR/pulse-mcp-windows-amd64.exe" "$RELEASE_DIR/"`,
// The installer scripts themselves must reach
// RELEASE_DIR so the GitHub Releases asset upload can
// publish them as the canonical curl-pipe-bash entry
// point.
`cp scripts/install-mcp.sh "$RELEASE_DIR/install-mcp.sh"`,
`[ -f scripts/install-mcp.ps1 ] && cp scripts/install-mcp.ps1 "$RELEASE_DIR/install-mcp.ps1"`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("build-release.sh missing pulse-mcp distribution wiring: %s", needle)
}
}
// install-mcp.sh and install-mcp.ps1 must both exist as
// shipped scripts; the build pipeline references them, so
// missing-file drift breaks release builds rather than
// quietly ships an installer that 404s.
if _, err := os.Stat(repoFile("scripts", "install-mcp.sh")); err != nil {
t.Fatalf("scripts/install-mcp.sh missing: %v", err)
}
if _, err := os.Stat(repoFile("scripts", "install-mcp.ps1")); err != nil {
t.Fatalf("scripts/install-mcp.ps1 missing: %v", err)
}
// install-mcp.sh's install-dir resolution and SHA256
// verification are load-bearing: dropping either silently
// turns the installer into "curl | bash with no integrity
// check," which is the failure mode the hook is here to
// prevent. Pin the touchstones.
mcpScript, err := os.ReadFile(repoFile("scripts", "install-mcp.sh"))
if err != nil {
t.Fatalf("read install-mcp.sh: %v", err)
}
for _, needle := range []string{
`detect_platform()`,
`choose_install_dir()`,
`PULSE_MCP_NO_VERIFY`,
`checksums.txt`,
`sha256 mismatch`,
} {
if !strings.Contains(string(mcpScript), needle) {
t.Fatalf("install-mcp.sh missing required helper or guard: %s", needle)
}
}
mcpPowerShell, err := os.ReadFile(repoFile("scripts", "install-mcp.ps1"))
if err != nil {
t.Fatalf("read install-mcp.ps1: %v", err)
}
for _, needle := range []string{
`function Resolve-Architecture`,
`PULSE_MCP_NO_VERIFY`,
`checksums.txt`,
`Get-FileHash -Path $tmp -Algorithm SHA256`,
`sha256 mismatch`,
} {
if !strings.Contains(string(mcpPowerShell), needle) {
t.Fatalf("install-mcp.ps1 missing required helper or guard: %s", needle)
}
}
}
// The release-pipeline downstream workflows and private Pro publication path
// share the same root cause: v6 rc.1 -> rc.6 silently broke because GitHub's
// `release: published` webhook doesn't fire when create-release.yml's draft ->
// PATCH(draft=false) promotion path is used, `workflow_run` chains don't fire
// when their upstream fails, and the private Pro path was left as a manual
// checklist step. The fix is explicit post-release orchestration after
// validate_release_assets succeeds. The tests below pin the trigger
// declarations, resolver logic, and private Pro dispatch contract so the
// regression class can't return.
func TestInstallShSmokeWorkflowPresent(t *testing.T) {
assertFileContainsAll(t, repoFile(".github", "workflows", "install-sh-smoke.yml"),
// Inputs and triggers.
`name: install.sh Smoke (Published Release)`,
`workflow_call:`,
`workflow_dispatch:`,
// Pull straight from the published release URL (not local release/).
`releases/download/${TAG}`,
`install.sh.sshsig`,
`pulse-${TAG}-linux-amd64.tar.gz`,
// README key extraction + ssh-keygen verify against the asset.
`grep -oE 'ssh-ed25519 [A-Za-z0-9+/=]+ pulse-installer' README.md`,
`ssh-keygen -Y verify \`,
`-I pulse-installer \`,
`-n pulse-install \`,
`-s install.sh.sshsig < install.sh`,
// Server-installer identity assertions, mirroring validate-release.sh.
`grep -qE '^# Pulse Installer Script' install.sh`,
`grep -q 'Pulse Unified Agent Installer' install.sh`,
`grep -qE '^[[:space:]]*--version\)' install.sh`,
// End-to-end install in a privileged systemd container.
`jrei/systemd-debian:12`,
`bash install.sh --archive /smoke/${tarball} --disable-auto-updates`,
`systemctl is-active pulse`,
// curl --retry handles its own poll loop instead of a bash for-loop.
`--retry 30 --retry-delay 2 --retry-connrefused --retry-all-errors http://127.0.0.1:7655/api/health`,
// Authoritative version check via /api/version (not /api/health).
`curl -fsS http://127.0.0.1:7655/api/version`,
`Installed version mismatch. Expected`,
)
}
func TestPromoteFloatingTagsReachableViaWorkflowCall(t *testing.T) {
assertFileContainsAll(t, repoFile(".github", "workflows", "promote-floating-tags.yml"),
`workflow_call:`,
`tag:`,
`description: "Release tag (e.g., v6.0.0). Required for workflow_call."`,
`prerelease:`,
`type: boolean`,
// Job condition must accept workflow_call alongside workflow_dispatch.
`github.event_name == 'workflow_call'`,
// Tag resolver must prefer inputs over the workflow_run derivation.
`if [ -n "${INPUT_TAG}" ]; then`,
`TAG="${INPUT_TAG}"`,
)
}
func TestPublishHelmChartReachableViaWorkflowCall(t *testing.T) {
workflowPath := repoFile(".github", "workflows", "publish-helm-chart.yml")
assertFileContainsAll(t, workflowPath,
`workflow_call:`,
`chart_version:`,
`description: "Chart version (e.g., 6.0.0-rc.5). Required for workflow_call."`,
`required: true`,
`type: string`,
`app_version:`,
// Chart-version resolver prefers inputs over release-event tag.
`if [ -n "${INPUT_CHART_VERSION}" ]; then`,
`RELEASE_TAG="${RELEASE_TAG_NAME}"`,
`name: Verify public GHCR chart read`,
`helm registry logout ghcr.io || true`,
`helm show chart`,
`oci://ghcr.io/${{ github.repository_owner }}/pulse-chart/pulse`,
`--version "${{ steps.versions.outputs.chart_version }}"`,
)
content, err := os.ReadFile(workflowPath)
if err != nil {
t.Fatalf("read publish-helm-chart.yml: %v", err)
}
workflow := string(content)
for _, forbidden := range []string{
`versions/latest/restore`,
`-f visibility=public`,
`Package visibility configuration attempted`,
} {
if strings.Contains(workflow, forbidden) {
t.Fatalf("publish-helm-chart.yml must verify chart readability instead of masking GHCR visibility API failures; found %q", forbidden)
}
}
}
func TestReleasePipelinePromotesOneImmutableCandidate(t *testing.T) {
createBytes, err := os.ReadFile(repoFile(".github", "workflows", "create-release.yml"))
if err != nil {
t.Fatalf("read create-release.yml: %v", err)
}
candidateBytes, err := os.ReadFile(repoFile(".github", "workflows", "build-release-candidate.yml"))
if err != nil {
t.Fatalf("read build-release-candidate.yml: %v", err)
}
validationBytes, err := os.ReadFile(repoFile(".github", "workflows", "validate-release-assets.yml"))
if err != nil {
t.Fatalf("read validate-release-assets.yml: %v", err)
}
createWorkflow := string(createBytes)
candidateWorkflow := string(candidateBytes)
validationWorkflow := string(validationBytes)
createJob := workflowJobBlock(t, createWorkflow, "create_release")
backendJob := workflowJobBlock(t, createWorkflow, "backend_tests")
integrationJob := workflowJobBlock(t, createWorkflow, "integration_tests")
validationJob := workflowJobBlock(t, createWorkflow, "validate_release_assets")
privateJob := workflowJobBlock(t, createWorkflow, "publish_private_pro_runtime")
for _, needle := range []string{
`./scripts/build-release.sh "${{ inputs.version }}"`,
`scripts/validate-release.sh "${{ inputs.version }}" --skip-docker`,
`scripts/release_candidate_manifest.py create`,
`compression-level: 0`,
`retention-days: 1`,
} {
if !strings.Contains(candidateWorkflow, needle) {
t.Fatalf("build-release-candidate.yml missing single-build contract: %s", needle)
}
}
for _, needle := range []string{
`Download immutable release candidate`,
`scripts/release_candidate_manifest.py verify-local`,
`needs.build_release_candidate.outputs.artifact_name`,
} {
if !strings.Contains(createJob, needle) {
t.Fatalf("create_release missing candidate promotion contract: %s", needle)
}
}
if strings.Contains(createJob, "scripts/build-release.sh") {
t.Fatal("create_release must promote the verified candidate instead of rebuilding release assets")
}
if !strings.Contains(backendJob, "- frontend_checks") || !strings.Contains(integrationJob, "- frontend_checks") {
t.Fatal("backend and integration jobs must consume the shared verified frontend bundle")
}
if strings.Contains(integrationJob, "- backend_tests") {
t.Fatal("integration tests must run in parallel with backend tests")
}
if !strings.Contains(integrationJob, `tests/66-organization-sharing-approval-ui.spec.ts`) {
t.Fatal("integration release gate missing current organization-sharing coverage")
}
if strings.Contains(integrationJob, `tests/03-multi-tenant.spec.ts`) {
t.Fatal("integration release gate must not target the quarantined multi-tenant spec")
}
if strings.Contains(validationJob, "- publish_docker") {
t.Fatal("release asset digest validation must run in parallel with Docker publication")
}
if !strings.Contains(privateJob, "- create_release") || strings.Contains(privateJob, "- validate_release_assets") {
t.Fatal("private Pro publication must start after release creation without waiting for asset validation")
}
for _, needle := range []string{
`inputs.candidate_manifest_artifact != ''`,
`scripts/release_candidate_manifest.py verify-release`,
`inputs.candidate_manifest_artifact == ''`,
} {
if !strings.Contains(validationWorkflow, needle) {
t.Fatalf("validate-release-assets.yml missing fast digest contract: %s", needle)
}
}
}
func TestCreateReleasePublishesPrivateProRuntime(t *testing.T) {
content, err := os.ReadFile(repoFile(".github", "workflows", "create-release.yml"))
if err != nil {
t.Fatalf("read create-release.yml: %v", err)
}
workflow := string(content)
job := workflowJobBlock(t, workflow, "publish_private_pro_runtime")
for _, needle := range []string{
`needs.create_release.result == 'success'`,
`github.event.inputs.draft_only != 'true'`,
`startsWith(needs.prepare.outputs.version, '6.')`,
`GH_TOKEN: ${{ secrets.WORKFLOW_PAT }}`,
`r2_prefix="${TAG}-pro-$(date -u '+%Y%m%d')-${GITHUB_RUN_ID}"`,
`gh workflow run build-pro-release.yml`,
`--repo rcourtman/pulse-enterprise`,
`-f pulse_ref="${TAG}"`,
`-f version="${VERSION}"`,
`-f upload_actions_artifact=false`,
`-f upload_to_r2=true`,
`-f publish_docker_image=true`,
`-f docker_image=license.pulserelay.pro/pulse-pro`,
`-f r2_prefix="${r2_prefix}"`,
`-f allow_stable_ga_publish="${allow_ga_publish}"`,
`wait_for_workflow rcourtman/pulse-enterprise "Build Pro Release" main "${build_started_at}" "private Pro build"`,
`gh workflow run promote-paid-runtime-release.yml`,
`--repo rcourtman/pulse-pro`,
`-f r2_prefix="${r2_prefix}"`,
`-f allow_ga_prefix="${allow_ga_publish}"`,
`wait_for_workflow rcourtman/pulse-pro "Promote Paid Runtime Release" main "${promote_started_at}" "private Pro live promotion"`,
`echo "::error::${label} failed with conclusion=${conclusion}: ${url}"`,
} {
if !strings.Contains(job, needle) {
t.Fatalf("publish_private_pro_runtime missing required contract: %s", needle)
}
}
if strings.Contains(job, "continue-on-error: true") {
t.Fatal("publish_private_pro_runtime must fail the release pipeline when private Pro publication or promotion fails")
}
}
func TestHelmAgentRuntimePointsAtRealImage(t *testing.T) {
// The helm chart's agent.enabled=true workload used to default to
// ghcr.io/rcourtman/pulse-agent — an image that was never published.
// The chart now points at the main rcourtman/pulse image and uses an
// arch-resolved /usr/local/bin/pulse-agent symlink baked into the
// runtime stage. This test pins:
// 1. values.yaml uses the main image
// 2. values.yaml has the command override
// 3. the agent template renders the command
// 4. the Dockerfile creates the symlink for every supported arch
// 5. validate-release.sh asserts the symlink exists in the published image
// Reverting any one of these unwires the chart back to ImagePullBackOff.
valuesBytes, err := os.ReadFile(repoFile("deploy", "helm", "pulse", "values.yaml"))
if err != nil {
t.Fatalf("read values.yaml: %v", err)
}
values := string(valuesBytes)
if !strings.Contains(values, "repository: rcourtman/pulse\n") {
t.Fatal("agent.image.repository must default to rcourtman/pulse (single-image agent + server)")
}
// Match the actual config value, not casual mentions in surrounding
// comments that explain why the default changed.
if strings.Contains(values, "repository: ghcr.io/rcourtman/pulse-agent") {
t.Fatal("agent.image.repository must not reference the never-published ghcr.io/rcourtman/pulse-agent image")
}
if !strings.Contains(values, "- /usr/local/bin/pulse-agent") {
t.Fatal("agent.command must default to /usr/local/bin/pulse-agent so the main image's server ENTRYPOINT is overridden")
}
agentTemplate, err := os.ReadFile(repoFile("deploy", "helm", "pulse", "templates", "agent.yaml"))
if err != nil {
t.Fatalf("read agent.yaml: %v", err)
}
tmpl := string(agentTemplate)
if !strings.Contains(tmpl, "{{- if .Values.agent.command }}") {
t.Fatal("agent.yaml template must conditionally render command from .Values.agent.command")
}
if !strings.Contains(tmpl, "command:\n {{- toYaml .Values.agent.command | nindent 12 }}") {
t.Fatal("agent.yaml template must render command via toYaml so list values pass through correctly")
}
assertFileContainsAll(t, repoFile("Dockerfile"),
`ln -s /opt/pulse/bin/pulse-agent-linux-arm64 /usr/local/bin/pulse-agent`,
`ln -s /opt/pulse/bin/pulse-agent-linux-armv7 /usr/local/bin/pulse-agent`,
`ln -s /opt/pulse/bin/pulse-agent-linux-amd64 /usr/local/bin/pulse-agent`,
)
assertFileContainsAll(t, repoFile("scripts", "validate-release.sh"),
`Validating /usr/local/bin/pulse-agent arch-resolved symlink`,
`[ -L /usr/local/bin/pulse-agent ]`,
`/usr/local/bin/pulse-agent target is not executable`,
)
}
func repoFile(parts ...string) string {
root := filepath.Join("..", "..")
segments := append([]string{root}, parts...)
return filepath.Join(segments...)
}
// assertFileContainsAll reads the file at path and fails the test if any of
// the required substrings is missing. The standard pinning-test shape in
// this package.
func assertFileContainsAll(t *testing.T, path string, required ...string) {
t.Helper()
content, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
s := string(content)
for _, needle := range required {
if !strings.Contains(s, needle) {
t.Fatalf("%s missing required substring: %s", path, needle)
}
}
}
func assertFileContainsAllNormalized(t *testing.T, path string, required ...string) {
t.Helper()
content, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
s := normalizedInstallTestWhitespace(string(content))
for _, needle := range required {
if !strings.Contains(s, normalizedInstallTestWhitespace(needle)) {
t.Fatalf("%s missing required normalized substring: %s", path, needle)
}
}
}
func normalizedInstallTestWhitespace(text string) string {
return strings.Join(strings.Fields(text), " ")
}
func workflowJobBlock(t *testing.T, workflow, job string) string {
t.Helper()
startMarker := "\n " + job + ":\n"
start := strings.Index(workflow, startMarker)
if start == -1 {
t.Fatalf("workflow missing job %s", job)
}
start += 1
rest := workflow[start+len(" "+job+":\n"):]
end := len(rest)
for _, line := range strings.Split(rest, "\n") {
if strings.HasPrefix(line, " ") && !strings.HasPrefix(line, " ") {
candidate := strings.Index(rest, "\n"+line)
if candidate >= 0 {
end = candidate
break
}
}
}
return workflow[start : start+len(" "+job+":\n")+end]
}