name: Security Scan # Build and Test audits dependencies on every push, but a quiet week with no # pushes means no run — and newly disclosed vulnerabilities or delivery drift # land against unchanged code. The six-hour schedule cheaply verifies that the # advertised stable release remains locked and bound to its activation marker. # The weekly schedule re-scans every dependency and performs the full public # delivery verification. A failed scheduled run emails the repository owner. on: schedule: - cron: '17 */6 * * *' # six-hour stable release lock and activation watch - cron: '30 5 * * 1' # weekly, Monday 05:30 UTC, before the 06:00 triage run workflow_dispatch: workflow_run: workflows: [Release Convergence] types: [completed] branches: [main] permissions: contents: read jobs: release-continuity: name: Latest stable release continuity # Stable convergence gets an immediate independent read-back. Prerelease # convergence is excluded because this job intentionally follows the # advertised stable channel. The six-hour schedule stops after release and # activation identity resolution; weekly, manual, and convergence runs # continue through every artifact and customer-facing delivery surface. if: ${{ github.event_name != 'workflow_run' || !contains(github.event.workflow_run.display_title, '-') }} runs-on: ubuntu-24.04 timeout-minutes: 60 steps: - name: Checkout release verification controls uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Go if: ${{ github.event.schedule != '17 */6 * * *' }} uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod - name: Set up Helm if: ${{ github.event.schedule != '17 */6 * * *' }} uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1 with: version: v3.15.2 - name: Set up Docker Buildx if: ${{ github.event.schedule != '17 */6 * * *' }} uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: Resolve the advertised stable release id: release env: GH_TOKEN: ${{ github.token }} REPOSITORY: ${{ github.repository }} run: | set -euo pipefail mkdir -p release-continuity-evidence release_json=release-continuity-evidence/release.json if ! gh api \ -H 'Accept: application/vnd.github+json' \ -H 'X-GitHub-Api-Version: 2026-03-10' \ "repos/${REPOSITORY}/releases/latest" > "${release_json}"; then python3 scripts/release_control/release_continuity.py release \ --release-json "${release_json}" \ --diagnostic release-continuity-evidence/release-diagnostic.json \ --github-output "${GITHUB_OUTPUT}" || true exit 1 fi python3 scripts/release_control/release_continuity.py release \ --release-json "${release_json}" \ --diagnostic release-continuity-evidence/release-diagnostic.json \ --github-output "${GITHUB_OUTPUT}" - name: Reject orphaned stable publication tags id: frontier if: ${{ !cancelled() && steps.release.outputs.referenceable == 'true' }} env: GH_TOKEN: ${{ github.token }} REPOSITORY: ${{ github.repository }} run: | set -euo pipefail evidence=release-continuity-evidence gh api --paginate --slurp \ -H 'Accept: application/vnd.github+json' \ -H 'X-GitHub-Api-Version: 2026-03-10' \ "repos/${REPOSITORY}/git/matching-refs/tags/v?per_page=100" \ > "${evidence}/stable-refs.json" gh api --paginate --slurp \ -H 'Accept: application/vnd.github+json' \ -H 'X-GitHub-Api-Version: 2026-03-10' \ "repos/${REPOSITORY}/releases?per_page=100" \ > "${evidence}/releases.json" fetch_registry_tags() { local registry_name="$1" auth_url="$2" registry_url="$3" image="$4" output="$5" local token token="$(curl -fsSL --retry 4 --retry-delay 2 --retry-all-errors "${auth_url}")" token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"${token}")" curl -fsSL --retry 4 --retry-delay 2 --retry-all-errors \ -H "Authorization: Bearer ${token}" \ "${registry_url}/v2/${image}/tags/list?n=10000" \ | jq --arg name "${registry_name}/${image}" '.name = $name' > "${output}" } owner="${GITHUB_REPOSITORY_OWNER,,}" for image in pulse pulse-control-plane; do file_image="${image//-/_}" fetch_registry_tags \ ghcr.io \ "https://ghcr.io/token?service=ghcr.io&scope=repository:${owner}/${image}:pull" \ https://ghcr.io "${owner}/${image}" \ "${evidence}/ghcr-${file_image}-tags.json" done # The server image is public on both registries. The control-plane # image is publicly enumerable only on GHCR, so an unauthenticated # continuity audit must not depend on Docker Hub credentials. fetch_registry_tags \ docker.io \ "https://auth.docker.io/token?service=registry.docker.io&scope=repository:rcourtman/pulse:pull" \ https://registry-1.docker.io rcourtman/pulse \ "${evidence}/docker-pulse-tags.json" python3 scripts/release_control/release_continuity.py frontier \ --release-json "${evidence}/release.json" \ --stable-refs-json "${evidence}/stable-refs.json" \ --releases-json "${evidence}/releases.json" \ --registry-tags-json "${evidence}/ghcr-pulse-tags.json" \ --registry-tags-json "${evidence}/docker-pulse-tags.json" \ --registry-tags-json "${evidence}/ghcr-pulse_control_plane-tags.json" \ --diagnostic "${evidence}/frontier-diagnostic.json" - name: Bind the release activation marker id: activation # A structurally valid tag/id/SHA remains safe lookup data even when # release trust fails. Inspect its marker so one defect cannot hide a # second; all public-surface verification below still requires both # release and activation admission to succeed. if: ${{ !cancelled() && steps.release.outputs.referenceable == 'true' }} env: GH_TOKEN: ${{ github.token }} REPOSITORY: ${{ github.repository }} TAG: ${{ steps.release.outputs.tag }} run: | set -euo pipefail release_json=release-continuity-evidence/release.json marker=release-continuity-evidence/release-activation.json if ! gh release download "${TAG}" \ --repo "${REPOSITORY}" \ --pattern release-activation.json \ --dir release-continuity-evidence; then python3 scripts/release_control/release_continuity.py activation \ --release-json "${release_json}" \ --activation-json "${marker}" \ --diagnostic release-continuity-evidence/activation-diagnostic.json \ --github-output "${GITHUB_OUTPUT}" || true exit 1 fi python3 scripts/release_control/release_continuity.py activation \ --release-json "${release_json}" \ --activation-json "${marker}" \ --diagnostic release-continuity-evidence/activation-diagnostic.json \ --github-output "${GITHUB_OUTPUT}" - name: Verify immutable release and build provenance id: packet if: ${{ always() && steps.release.outcome == 'success' && steps.frontier.outcome == 'success' && steps.activation.outcome == 'success' && github.event.schedule != '17 */6 * * *' }} env: GH_TOKEN: ${{ github.token }} REPOSITORY: ${{ github.repository }} TAG: ${{ steps.release.outputs.tag }} RELEASE_ID: ${{ steps.release.outputs.release_id }} SOURCE_SHA: ${{ steps.release.outputs.source_sha }} run: >- ./scripts/verify-github-release-integrity.sh "${TAG}" "${REPOSITORY}" "${RELEASE_ID}" "${SOURCE_SHA}" - name: Authenticate every published release asset id: assets if: ${{ always() && steps.release.outcome == 'success' && steps.frontier.outcome == 'success' && steps.activation.outcome == 'success' && github.event.schedule != '17 */6 * * *' }} env: PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }} REPOSITORY: ${{ github.repository }} TAG: ${{ steps.release.outputs.tag }} run: ./scripts/validate-published-release.sh "${TAG}" "${REPOSITORY}" - name: Verify exact-version container identities id: containers if: ${{ always() && steps.release.outcome == 'success' && steps.frontier.outcome == 'success' && steps.activation.outcome == 'success' && github.event.schedule != '17 */6 * * *' }} env: GH_TOKEN: ${{ github.token }} REPOSITORY: ${{ github.repository }} TAG: ${{ steps.release.outputs.tag }} SOURCE_SHA: ${{ steps.release.outputs.source_sha }} EXPECTED_SERVER_DIGEST: ${{ steps.activation.outputs.server_image_digest }} EXPECTED_CONTROL_PLANE_DIGEST: ${{ steps.activation.outputs.control_plane_image_digest }} run: | set -euo pipefail proof="$(./scripts/verify-release-container-images.sh "${TAG}" "${SOURCE_SHA}" "${REPOSITORY}")" printf '%s\n' "${proof}" server_digest="$(awk -F= '$1 == "server_digest" {print $2}' <<<"${proof}")" control_plane_digest="$(awk -F= '$1 == "control_plane_digest" {print $2}' <<<"${proof}")" if [ "${server_digest}" != "${EXPECTED_SERVER_DIGEST}" ] || \ [ "${control_plane_digest}" != "${EXPECTED_CONTROL_PLANE_DIGEST}" ]; then echo "::error::Exact-version container identities moved from the committed activation marker." exit 1 fi - name: Verify stable container discovery aliases id: aliases if: ${{ always() && steps.release.outcome == 'success' && steps.frontier.outcome == 'success' && steps.activation.outcome == 'success' && github.event.schedule != '17 */6 * * *' }} env: TAG: ${{ steps.release.outputs.tag }} EXPECTED_SERVER_DIGEST: ${{ steps.activation.outputs.server_image_digest }} EXPECTED_CONTROL_PLANE_DIGEST: ${{ steps.activation.outputs.control_plane_image_digest }} REGISTRY_OWNER: ${{ github.repository_owner }} run: >- ./scripts/verify-stable-container-aliases.sh "${TAG}" "${EXPECTED_SERVER_DIGEST}" "${EXPECTED_CONTROL_PLANE_DIGEST}" "${REGISTRY_OWNER}" - name: Verify exact-version Helm identity id: helm if: ${{ always() && steps.release.outcome == 'success' && steps.frontier.outcome == 'success' && steps.activation.outcome == 'success' && github.event.schedule != '17 */6 * * *' }} env: GH_TOKEN: ${{ github.token }} REPOSITORY: ${{ github.repository }} TAG: ${{ steps.release.outputs.tag }} SOURCE_SHA: ${{ steps.release.outputs.source_sha }} EXPECTED_HELM_DIGEST: ${{ steps.activation.outputs.helm_chart_digest }} run: >- ./scripts/verify-release-helm-chart.sh "${TAG}" "${SOURCE_SHA}" "${REPOSITORY}" "${EXPECTED_HELM_DIGEST}" - name: Record continuity evidence if: ${{ always() }} env: REPOSITORY: ${{ github.repository }} RUN_ID: ${{ github.run_id }} RUN_ATTEMPT: ${{ github.run_attempt }} TRIGGER_EVENT: ${{ github.event_name }} TRIGGER_SCHEDULE: ${{ github.event.schedule }} CONVERGENCE_RUN_ID: ${{ github.event.workflow_run.id }} CONVERGENCE_RUN_ATTEMPT: ${{ github.event.workflow_run.run_attempt }} CONVERGENCE_CONCLUSION: ${{ github.event.workflow_run.conclusion }} CONVERGENCE_DISPLAY_TITLE: ${{ github.event.workflow_run.display_title }} TAG: ${{ steps.release.outputs.tag }} RELEASE_ID: ${{ steps.release.outputs.release_id }} SOURCE_SHA: ${{ steps.release.outputs.source_sha }} ACTIVATION_SHA256: ${{ steps.activation.outputs.activation_sha256 }} SERVER_IMAGE_DIGEST: ${{ steps.activation.outputs.server_image_digest }} CONTROL_PLANE_IMAGE_DIGEST: ${{ steps.activation.outputs.control_plane_image_digest }} HELM_CHART_DIGEST: ${{ steps.activation.outputs.helm_chart_digest }} RELEASE_RESULT: ${{ steps.release.outcome }} FRONTIER_RESULT: ${{ steps.frontier.outcome }} ACTIVATION_RESULT: ${{ steps.activation.outcome }} PACKET_RESULT: ${{ steps.packet.outcome }} ASSET_RESULT: ${{ steps.assets.outcome }} CONTAINER_RESULT: ${{ steps.containers.outcome }} ALIAS_RESULT: ${{ steps.aliases.outcome }} HELM_RESULT: ${{ steps.helm.outcome }} run: | set -euo pipefail mkdir -p release-continuity-evidence checked_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" mode=full if [ "${TRIGGER_SCHEDULE}" = '17 */6 * * *' ]; then mode=release_lock fi for diagnostic in release frontier activation; do path="release-continuity-evidence/${diagnostic}-diagnostic.json" if [ ! -s "${path}" ]; then jq -n \ --arg check "${diagnostic}" \ '{schema_version: 1, check: $check, status: "not_run", identity: {}, violations: []}' \ > "${path}" fi done jq -n \ --slurpfile release_diagnostic release-continuity-evidence/release-diagnostic.json \ --slurpfile frontier_diagnostic release-continuity-evidence/frontier-diagnostic.json \ --slurpfile activation_diagnostic release-continuity-evidence/activation-diagnostic.json \ --arg schema_version "1" \ --arg checked_at "${checked_at}" \ --arg repository "${REPOSITORY}" \ --arg run_id "${RUN_ID}" \ --arg run_attempt "${RUN_ATTEMPT}" \ --arg trigger_event "${TRIGGER_EVENT}" \ --arg trigger_schedule "${TRIGGER_SCHEDULE}" \ --arg mode "${mode}" \ --arg convergence_run_id "${CONVERGENCE_RUN_ID}" \ --arg convergence_run_attempt "${CONVERGENCE_RUN_ATTEMPT}" \ --arg convergence_conclusion "${CONVERGENCE_CONCLUSION}" \ --arg convergence_display_title "${CONVERGENCE_DISPLAY_TITLE}" \ --arg tag "${TAG}" \ --arg release_id "${RELEASE_ID}" \ --arg source_sha "${SOURCE_SHA}" \ --arg activation_sha256 "${ACTIVATION_SHA256}" \ --arg server_image_digest "${SERVER_IMAGE_DIGEST}" \ --arg control_plane_image_digest "${CONTROL_PLANE_IMAGE_DIGEST}" \ --arg helm_chart_digest "${HELM_CHART_DIGEST}" \ --arg release_result "${RELEASE_RESULT}" \ --arg frontier_result "${FRONTIER_RESULT}" \ --arg activation_result "${ACTIVATION_RESULT}" \ --arg packet_result "${PACKET_RESULT}" \ --arg asset_result "${ASSET_RESULT}" \ --arg container_result "${CONTAINER_RESULT}" \ --arg alias_result "${ALIAS_RESULT}" \ --arg helm_result "${HELM_RESULT}" \ '{ schema_version: ($schema_version | tonumber), checked_at: $checked_at, repository: $repository, workflow_run: {id: $run_id, attempt: $run_attempt}, trigger: { event: $trigger_event, schedule: $trigger_schedule, mode: $mode, release_convergence_run: { id: $convergence_run_id, attempt: $convergence_run_attempt, conclusion: $convergence_conclusion, display_title: $convergence_display_title } }, release: { tag: $tag, id: $release_id, source_sha: $source_sha, activation_sha256: $activation_sha256, server_image_digest: $server_image_digest, control_plane_image_digest: $control_plane_image_digest, helm_chart_digest: $helm_chart_digest }, diagnostics: { release_identity: $release_diagnostic[0], stable_publication_frontier: $frontier_diagnostic[0], activation_binding: $activation_diagnostic[0] }, checks: { release_resolution: $release_result, stable_publication_frontier: $frontier_result, activation_binding: $activation_result, immutable_packet_and_provenance: $packet_result, authenticated_assets: $asset_result, container_identities: $container_result, stable_container_aliases: $alias_result, helm_identity: $helm_result } }' > release-continuity-evidence/continuity-evidence.json cat release-continuity-evidence/continuity-evidence.json >> "${GITHUB_STEP_SUMMARY}" - name: Retain continuity evidence if: ${{ always() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: release-continuity-${{ steps.release.outputs.tag || 'unresolved' }}-${{ github.run_id }}-${{ github.run_attempt }} path: release-continuity-evidence if-no-files-found: error retention-days: 90 container-lifecycle: name: Container trust and support window if: ${{ github.event_name != 'workflow_run' && github.event.schedule != '17 */6 * * *' }} runs-on: ubuntu-24.04 timeout-minutes: 30 steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod - name: Check governed image pins and lifecycle runway run: >- go test ./scripts/installtests -run '^(TestGovernedContainerBaseSupportWindow|TestEveryDockerfilePinsExternalBases|TestNodeToolchainParity|TestIntegrationContainersUseGovernedImmutableBases|TestDockerBuildUsesCanonicalReleaseLdflags|TestProviderMSPControlPlaneDockerfileBuildsReleaseLicenseBinary)$' govulncheck: name: Go Vulnerability Scan (${{ matrix.directory }}) if: ${{ github.event_name != 'workflow_run' && github.event.schedule != '17 */6 * * *' }} runs-on: ubuntu-24.04 timeout-minutes: 45 strategy: fail-fast: false matrix: directory: - '.' - 'tests/integration/mock-github-server' steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod - name: Install govulncheck run: go install golang.org/x/vuln/cmd/govulncheck@v1.7.0 - name: Scan Go module for reachable vulnerabilities working-directory: ${{ matrix.directory }} run: govulncheck ./... npm-audit: name: npm Dependency Audit (${{ matrix.name }}) if: ${{ github.event_name != 'workflow_run' && github.event.schedule != '17 */6 * * *' }} runs-on: ubuntu-24.04 timeout-minutes: 20 strategy: fail-fast: false matrix: include: - name: root tooling directory: '.' - name: product frontend directory: 'frontend-modern' - name: account frontend directory: 'internal/cloudcp/portal/frontend' - name: integration tests directory: 'tests/integration' steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '24' - name: Audit complete dependency graph id: audit-complete continue-on-error: true working-directory: ${{ matrix.directory }} run: bash "$GITHUB_WORKSPACE/scripts/npm-audit-retry.sh" all --package-lock-only - name: Audit production dependencies id: audit-production continue-on-error: true working-directory: ${{ matrix.directory }} run: bash "$GITHUB_WORKSPACE/scripts/npm-audit-retry.sh" production --package-lock-only - name: Require dependency audits if: ${{ !cancelled() }} env: COMPLETE_AUDIT_RESULT: ${{ steps.audit-complete.outcome }} PRODUCTION_AUDIT_RESULT: ${{ steps.audit-production.outcome }} run: | if [ "${COMPLETE_AUDIT_RESULT}" != success ] || \ [ "${PRODUCTION_AUDIT_RESULT}" != success ]; then echo "::error::One or more dependency audits failed." exit 1 fi