The agent_runtime verify-only build pointed its registry buildcache at
ghcr.io/<owner>/pulse-agent, a package no workflow publishes. A cache ref
creates the package it points at, so every release recreated it and the
repository's Packages sidebar advertised an empty pulse-agent entry that
reads like a pullable agent image sitting beside the real pulse,
pulse-control-plane and pulse-chart/pulse packages. The only release tag it
ever carried is a stray v6.0.0-rc.3.
Move the cache to ghcr.io/<owner>/pulse:agent-buildcache, alongside the
runtime stage's existing pulse:buildcache tag, so it lives in the package
that actually ships and shares backend-builder layers with the runtime
build.
TestAgentBuildCacheDoesNotResurrectPulseAgentPackage locks this in. It scans
every workflow for registry-qualified refs to the pulse-agent package,
collapsing ${{ }} expressions first so an interpolated owner cannot hide one
behind its spaces, and asserts both the new cache target and the chart's
published agent image default. Verified red on a restored buildcache ref and
red again on a ref hidden behind ${{ github.repository_owner }}.
Two stale claims corrected while here. The chart values comment said the old
agent.image default was never published, when it did carry v6.0.0-rc.3. The
deployment-installability contract called rcourtman/pulse the only image
publish-docker.yml pushes, when the same workflow also pushes the MSP
pulse-control-plane image. The contract now also forbids buildcache
references to the pulse-agent package, which is what let this regress.
Deleting the orphaned package itself remains a manual GHCR step.
GitHub Actions Workflows
Issue Triage Automation
Files:
issue-version-label-sync.ymlissue-version-retest-comment.yml
Issue intake is split deliberately:
issue-version-label-sync.ymlis the silent metadata path. It runs onopened,edited, andreopenedissue events so version labels,needs-version-info, andneeds-retest-on-lateststay correct when maintainers tidy issue metadata.issue-version-retest-comment.ymlis the public guidance path. It only runs onopenedandreopened, and only posts reporter-facing retest guidance when the issue is an older-version bug report from a non-maintainer.- Both workflows load the shared helper at
.github/scripts/issue-version-triage.cjsso parsing and classification logic lives in one place instead of drifting across duplicated inline scripts.
Update Demo Server
File: update-demo-server.yml
Automatically updates the governed demo target after a release is published. Stable releases update the public demo. Prerelease tags no longer update a separate v6 preview demo after GA.
Configuration Required
Create one GitHub Environment:
demo-stable
The environment must define the secret names used by the governed demo target.
Required environment secrets:
-
DEMO_SERVER_SSH_KEY
- The private SSH key for accessing the demo server
- Generate with:
cat ~/.ssh/id_ed25519(or your key file) - Should be the full private key including
-----BEGINand-----ENDlines
-
DEMO_SERVER_HOST
- The hostname or IP of the demo server
-
DEMO_SERVER_USER
- The SSH username for the demo server (e.g.
rootor a deploy user with sudo access)
- The SSH username for the demo server (e.g.
Required shared secret:
- TS_OAUTH_CLIENT_ID and TS_OAUTH_SECRET
- Tailscale OAuth client (business tailnet
tawny-powan.ts.net, scope Auth Keys write, tagtag:infra) used by the governed demo deploy/update workflows before SSH - The action mints an ephemeral, pre-authorized, tagged node key per run, so runners join and garbage-collect themselves; unlike the retired static
TS_AUTHKEY, the OAuth secret does not expire every 90 days - Allows GitHub-hosted runners to reach private demo targets such as the stable
pulse-relayTailscale host - May be stored as repository secrets or repeated in the selected environment if desired
- Tailscale OAuth client (business tailnet
Required environment variables:
-
DEMO_EXPECTED_HOSTNAME
- The remote
hostnamevalue the stable demo environment is expected to report - Stable example:
pulse-relay - This is a host-identity guard: the workflow fails closed if the SSH secret points at the wrong machine
- The remote
-
DEMO_LOCAL_BASE_URL
- Local URL used on the target host for version and mock-mode verification
- Example stable value:
http://localhost:7655
-
DEMO_PUBLIC_HEALTH_URL
- Public health endpoint for the stable demo target
- Example stable value:
https://demo.pulserelay.pro/api/health
Optional environment variables:
-
DEMO_SERVICE_NAME
- Stable default:
pulse - When set, the server installer derives the instance-specific install dir, config dir, update helper, and update timer from this service identity.
- Stable default:
-
DEMO_AUTH_USER / DEMO_AUTH_PASS
- Demo credentials used for post-update mock verification
- Defaults to
demo/demowhen omitted
How It Works
- Trigger: Runs automatically when a GitHub release is published
- Target selection: Stable tags deploy to
demo-stable; prerelease tags are skipped because the public v6 preview target is retired after GA - Service identity: Stable runs default to the
pulseservice identity - Governance check: Validates the selected tag is reachable from the governed release branch for that version
- Latest check: Refuses to update the public demo unless the published tag is the latest stable release
- Network attach: Joins Tailscale before any SSH step so governed demo targets can stay on private hostnames or Tailscale IPs
- Update: SSHs to the selected demo host and runs the tag-matched root installer from that exact git tag
- Host identity check: Verifies the SSH target reports the governed expected hostname before running installer or deploy steps
- Verify: Checks that the new version is running, mock mode is active, and the public demo HTML serves the same frontend entry asset as the target service
- Browser smoke: Uses the governed Playwright helper to prove the public demo still renders the login shell in a real browser
- Cleanup: Removes SSH key from runner
Testing
To test without publishing a release:
- Go to
Actionstab in GitHub - Select
Update Demo Serverworkflow - Provide a stable tag and choose
stableorauto
Benefits
- ✅ The public demo follows the stable v6 release line after GA
- ✅ Prereleases no longer require a second public v6 preview surface
- ✅ Validates the real server installer path on the selected target
- ✅ Removes release-operator guesswork about which demo should move
Deploy Demo Server
File: deploy-demo-server.yml
Manually deploys the current branch build to the stable demo environment without changing the governed release workflow.
- Uses the same
demo-stableenvironment contract as the release-driven updater - Joins Tailscale before SSH so governed demo targets can stay on private addresses instead of requiring public runner reachability
- Requires
DEMO_EXPECTED_HOSTNAME,DEMO_LOCAL_BASE_URL, andDEMO_PUBLIC_HEALTH_URL - Supports optional
DEMO_SERVICE_NAME,DEMO_INSTALL_DIR,DEMO_TEST_PORT,DEMO_AUTH_USER, andDEMO_AUTH_PASS - Assumes the target service and install directory already exist on the host
- Verifies the SSH target reports the governed expected hostname before deploy
- Verifies that the public demo shell serves the same frontend entry asset that was built and deployed
- Uses
scripts/run_demo_public_browser_smoke.shto prove the public demo still renders the login shell in Chromium after deploy/update verification
Helm CI
File: helm-ci.yml
Runs helm lint --strict and renders the chart with common configuration combinations on every pull request that touches Helm content (and on pushes to main). This prevents regressions before they land.
- Triggered by PRs/pushes touching
deploy/helm/**, docs, or the workflow itself - Uses Helm v3.15.2
- Renders both the default deployment and an agent-enabled configuration to catch template issues
Publish Helm Chart
File: publish-helm-chart.yml
Packages the Helm chart and pushes it to the GitHub Container Registry (OCI) whenever a GitHub Release is published. Also makes the packaged .tgz available as both an Actions artifact and a release asset. The same behaviour can be triggered locally via ./scripts/package-helm-chart.sh <version> [--push].
- Triggered automatically on
release: published, or manually via workflow dispatch (requireschart_versioninput) - Chart and app versions mirror the Pulse release tag (e.g.,
v4.24.0→4.24.0) - Publishes to
oci://ghcr.io/<owner>/pulse-chart - Verifies the pushed OCI chart can be read from GHCR without registry credentials
- Requires no additional secrets—uses the built-in
GITHUB_TOKENwithpackages: writepermission