Files
pulse-triage[bot] ad1cfd33c3 fix(ci): qualify grouped release action pin consumers
The grouped action upgrade leaves signing, network and publication consumer assertions on superseded pins. Align those contracts and check every consumer against reviewed immutable upstream manifests, retaining exact dispatch and release trust boundaries without claiming hosted execution.

Change-source: pulse-maintainer
2026-09-10 00:35:47 +01:00

469 lines
22 KiB
YAML

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