mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Publish images from exact candidate payloads
This commit is contained in:
@@ -53,6 +53,9 @@ on:
|
||||
manifest_artifact_name:
|
||||
description: 'Release candidate manifest artifact name'
|
||||
value: ${{ jobs.build.outputs.manifest_artifact_name }}
|
||||
container_artifact_name:
|
||||
description: 'Exact-candidate container payload artifact name'
|
||||
value: ${{ jobs.build.outputs.container_artifact_name }}
|
||||
windows_signing_backend:
|
||||
description: 'Windows signing backend used for the candidate'
|
||||
value: ${{ jobs.collect-windows-signing.outputs.signing_backend }}
|
||||
@@ -560,6 +563,7 @@ jobs:
|
||||
outputs:
|
||||
artifact_name: ${{ steps.identity.outputs.artifact_name }}
|
||||
manifest_artifact_name: ${{ steps.identity.outputs.manifest_artifact_name }}
|
||||
container_artifact_name: ${{ steps.identity.outputs.container_artifact_name }}
|
||||
steps:
|
||||
- name: Resolve candidate identity
|
||||
id: identity
|
||||
@@ -567,6 +571,7 @@ jobs:
|
||||
set -euo pipefail
|
||||
echo "artifact_name=release-candidate-${GITHUB_SHA}-${{ inputs.version }}" >> "$GITHUB_OUTPUT"
|
||||
echo "manifest_artifact_name=release-candidate-manifest-${GITHUB_SHA}-${{ inputs.version }}" >> "$GITHUB_OUTPUT"
|
||||
echo "container_artifact_name=release-container-payload-${GITHUB_SHA}-${{ inputs.version }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
@@ -690,6 +695,26 @@ jobs:
|
||||
--source-sha "${GITHUB_SHA}" \
|
||||
--output release-candidate-manifest/release-candidate.json
|
||||
|
||||
- name: Create exact-candidate container payload
|
||||
run: |
|
||||
set -euo pipefail
|
||||
payload_root="$RUNNER_TEMP/release-container-payload"
|
||||
./scripts/prepare-release-container-context.sh \
|
||||
release \
|
||||
"${{ inputs.version }}" \
|
||||
"${payload_root}/payload/release"
|
||||
mkdir -p "${payload_root}/payload/compiled/binaries"
|
||||
for arch in amd64 arm64; do
|
||||
install -m 0755 \
|
||||
"release-compiled/payload/binaries/pulse-control-plane-linux-${arch}" \
|
||||
"${payload_root}/payload/compiled/binaries/pulse-control-plane-linux-${arch}"
|
||||
done
|
||||
python3 scripts/release_candidate_manifest.py create \
|
||||
--release-dir "${payload_root}/payload" \
|
||||
--version "${{ inputs.version }}" \
|
||||
--source-sha "${GITHUB_SHA}" \
|
||||
--output "${payload_root}/release-container-payload.json"
|
||||
|
||||
- name: Upload immutable release candidate
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
@@ -709,65 +734,78 @@ jobs:
|
||||
retention-days: 1
|
||||
overwrite: true
|
||||
|
||||
- name: Upload exact-candidate container payload
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ${{ steps.identity.outputs.container_artifact_name }}
|
||||
path: ${{ runner.temp }}/release-container-payload/
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
compression-level: 0
|
||||
overwrite: true
|
||||
|
||||
qualify-release-containers:
|
||||
name: Exact-Candidate Container and Helm Smoke
|
||||
needs: build
|
||||
if: ${{ always() && needs.build.result == 'success' }}
|
||||
runs-on: [self-hosted, Linux, X64, pulse-pve-build]
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Download immutable release candidate
|
||||
- name: Download exact-candidate container payload
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: ${{ needs.build.outputs.artifact_name }}
|
||||
path: release
|
||||
name: ${{ needs.build.outputs.container_artifact_name }}
|
||||
path: ${{ runner.temp }}/release-container-payload
|
||||
|
||||
- name: Download release candidate manifest
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: ${{ needs.build.outputs.manifest_artifact_name }}
|
||||
path: release-candidate-manifest
|
||||
|
||||
- name: Verify immutable release candidate
|
||||
- name: Verify exact-candidate container payload
|
||||
run: |
|
||||
python3 scripts/release_candidate_manifest.py verify-local \
|
||||
--release-dir release \
|
||||
--manifest release-candidate-manifest/release-candidate.json \
|
||||
--release-dir "$RUNNER_TEMP/release-container-payload/payload" \
|
||||
--manifest "$RUNNER_TEMP/release-container-payload/release-container-payload.json" \
|
||||
--version "${{ inputs.version }}" \
|
||||
--source-sha "${GITHUB_SHA}"
|
||||
|
||||
- name: Prepare exact-candidate container context
|
||||
run: ./scripts/prepare-release-container-context.sh release "${{ inputs.version }}" "$RUNNER_TEMP/release-container-context"
|
||||
|
||||
- name: Assemble exact-candidate runtime and agent images
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker buildx build \
|
||||
--target runtime_prebuilt \
|
||||
--platform linux/amd64 \
|
||||
--build-context "release_payload=${RUNNER_TEMP}/release-container-context" \
|
||||
--build-context "release_payload=${RUNNER_TEMP}/release-container-payload/payload/release" \
|
||||
--load \
|
||||
--tag "pulse-helm-smoke:${{ inputs.version }}" \
|
||||
.
|
||||
docker buildx build \
|
||||
--target agent_runtime_prebuilt \
|
||||
--platform linux/amd64 \
|
||||
--build-context "release_payload=${RUNNER_TEMP}/release-container-context" \
|
||||
--build-context "release_payload=${RUNNER_TEMP}/release-container-payload/payload/release" \
|
||||
--load \
|
||||
--tag "pulse-agent-candidate:${{ inputs.version }}" \
|
||||
.
|
||||
docker buildx build \
|
||||
--file deploy/provider-msp/Dockerfile.control-plane \
|
||||
--target control_plane_prebuilt \
|
||||
--platform linux/amd64 \
|
||||
--build-context "compiled_payload=${RUNNER_TEMP}/release-container-payload/payload/compiled" \
|
||||
--load \
|
||||
--tag "pulse-control-plane-candidate:${{ inputs.version }}" \
|
||||
.
|
||||
|
||||
- name: Verify container binaries match immutable candidate
|
||||
run: |
|
||||
set -euo pipefail
|
||||
expected_server="$(sha256sum "$RUNNER_TEMP/release-container-context/amd64/bin/pulse" | awk '{print $1}')"
|
||||
expected_server="$(sha256sum "$RUNNER_TEMP/release-container-payload/payload/release/amd64/bin/pulse" | awk '{print $1}')"
|
||||
actual_server="$(docker run --rm --entrypoint /bin/sh "pulse-helm-smoke:${{ inputs.version }}" -c 'sha256sum /app/pulse' | awk '{print $1}')"
|
||||
expected_agent="$(sha256sum "$RUNNER_TEMP/release-container-context/amd64/bin/pulse-agent-linux-amd64" | awk '{print $1}')"
|
||||
expected_agent="$(sha256sum "$RUNNER_TEMP/release-container-payload/payload/release/amd64/bin/pulse-agent-linux-amd64" | awk '{print $1}')"
|
||||
actual_agent="$(docker run --rm --entrypoint /bin/sh "pulse-agent-candidate:${{ inputs.version }}" -c 'sha256sum /usr/local/bin/pulse-agent' | awk '{print $1}')"
|
||||
expected_control_plane="$(sha256sum "$RUNNER_TEMP/release-container-payload/payload/compiled/binaries/pulse-control-plane-linux-amd64" | awk '{print $1}')"
|
||||
actual_control_plane="$(docker run --rm --entrypoint /bin/sh "pulse-control-plane-candidate:${{ inputs.version }}" -c 'sha256sum /usr/local/bin/pulse-control-plane' | awk '{print $1}')"
|
||||
test "${actual_server}" = "${expected_server}"
|
||||
test "${actual_agent}" = "${expected_agent}"
|
||||
test "${actual_control_plane}" = "${expected_control_plane}"
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0
|
||||
|
||||
@@ -1077,8 +1077,9 @@ jobs:
|
||||
publish_docker:
|
||||
needs:
|
||||
- prepare
|
||||
- build_release_candidate
|
||||
- create_release
|
||||
if: ${{ always() && needs.prepare.result == 'success' && needs.create_release.result == 'success' && needs.prepare.outputs.historical_asset_backfill_only != 'true' && github.event.inputs.draft_only != 'true' }}
|
||||
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' && github.event.inputs.draft_only != 'true' }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -1088,6 +1089,8 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
tag: ${{ needs.prepare.outputs.tag }}
|
||||
container_artifact: ${{ needs.build_release_candidate.outputs.container_artifact_name }}
|
||||
source_sha: ${{ needs.create_release.outputs.target_commitish }}
|
||||
|
||||
validate_release_assets:
|
||||
needs:
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
name: Publish Docker Images
|
||||
run-name: Publish Docker Images ${{ inputs.tag }}
|
||||
|
||||
# Triggered by create-release.yml after staging images pass tests.
|
||||
# Builds multi-arch images (amd64+arm64) from source and publishes to Docker Hub and GHCR.
|
||||
# Called by create-release.yml after exact-candidate image qualification passes.
|
||||
# Assembles multi-arch images (amd64+arm64) from the verified candidate payload
|
||||
# and publishes them to Docker Hub and GHCR.
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
@@ -10,10 +11,12 @@ on:
|
||||
description: 'Release tag (e.g., v4.34.0)'
|
||||
required: true
|
||||
type: string
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag (e.g., v4.34.0)'
|
||||
container_artifact:
|
||||
description: 'Exact-candidate container payload artifact from this release run'
|
||||
required: true
|
||||
type: string
|
||||
source_sha:
|
||||
description: 'Exact source commit bound to the candidate payload'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
@@ -71,9 +74,25 @@ jobs:
|
||||
- name: Check out validated release tag
|
||||
env:
|
||||
TAG: ${{ steps.version.outputs.tag }}
|
||||
EXPECTED_SOURCE_SHA: ${{ inputs.source_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git checkout --detach "refs/tags/${TAG}"
|
||||
test "$(git rev-parse HEAD)" = "${EXPECTED_SOURCE_SHA}"
|
||||
|
||||
- name: Download exact-candidate container payload
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: ${{ inputs.container_artifact }}
|
||||
path: ${{ runner.temp }}/release-container-payload
|
||||
|
||||
- name: Verify exact-candidate container payload
|
||||
run: |
|
||||
python3 scripts/release_candidate_manifest.py verify-local \
|
||||
--release-dir "$RUNNER_TEMP/release-container-payload/payload" \
|
||||
--manifest "$RUNNER_TEMP/release-container-payload/release-container-payload.json" \
|
||||
--version "${{ steps.version.outputs.version }}" \
|
||||
--source-sha "${{ inputs.source_sha }}"
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
|
||||
@@ -94,38 +113,19 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Derive license public key Docker cache key
|
||||
id: license_key_cache
|
||||
env:
|
||||
PULSE_LICENSE_PUBLIC_KEY: ${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
decoded_len="$(printf '%s' "${PULSE_LICENSE_PUBLIC_KEY}" | base64 -d | wc -c | tr -d ' ')"
|
||||
if [ "${decoded_len}" != "32" ]; then
|
||||
echo "PULSE_LICENSE_PUBLIC_KEY must decode to 32 bytes." >&2
|
||||
exit 1
|
||||
fi
|
||||
key_sha256="$(printf '%s' "${PULSE_LICENSE_PUBLIC_KEY}" | base64 -d | sha256sum | awk '{print $1}')"
|
||||
echo "sha256=${key_sha256}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Build and push Pulse server image (multi-arch)
|
||||
- name: Assemble and push Pulse server image (multi-arch)
|
||||
id: build_server_image
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||
with:
|
||||
context: .
|
||||
target: runtime
|
||||
target: runtime_prebuilt
|
||||
build-contexts: |
|
||||
release_payload=${{ runner.temp }}/release-container-payload/payload/release
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
provenance: mode=max
|
||||
sbom: true
|
||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse:buildcache
|
||||
build-args: |
|
||||
VERSION=${{ steps.version.outputs.tag }}
|
||||
PULSE_LICENSE_PUBLIC_KEY_SHA256=${{ steps.license_key_cache.outputs.sha256 }}
|
||||
PULSE_UPDATE_SIGNING_PUBLIC_KEY=${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
|
||||
secrets: |
|
||||
pulse_license_public_key=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
|
||||
pulse_update_signing_key=${{ secrets.PULSE_UPDATE_SIGNING_KEY }}
|
||||
tags: |
|
||||
rcourtman/pulse:${{ steps.version.outputs.tag }}
|
||||
rcourtman/pulse:${{ steps.version.outputs.version }}
|
||||
@@ -148,22 +148,20 @@ jobs:
|
||||
push-to-registry: true
|
||||
create-storage-record: false
|
||||
|
||||
- name: Build and push Pulse control-plane image (multi-arch)
|
||||
- name: Assemble and push Pulse control-plane image (multi-arch)
|
||||
id: build_control_plane_image
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||
with:
|
||||
context: .
|
||||
file: deploy/provider-msp/Dockerfile.control-plane
|
||||
target: control_plane_prebuilt
|
||||
build-contexts: |
|
||||
compiled_payload=${{ runner.temp }}/release-container-payload/payload/compiled
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
provenance: mode=max
|
||||
sbom: true
|
||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse-control-plane:buildcache
|
||||
build-args: |
|
||||
VERSION=${{ steps.version.outputs.tag }}
|
||||
PULSE_LICENSE_PUBLIC_KEY_SHA256=${{ steps.license_key_cache.outputs.sha256 }}
|
||||
secrets: |
|
||||
pulse_license_public_key=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
|
||||
tags: |
|
||||
rcourtman/pulse-control-plane:${{ steps.version.outputs.tag }}
|
||||
rcourtman/pulse-control-plane:${{ steps.version.outputs.version }}
|
||||
|
||||
@@ -51,7 +51,14 @@ RUN --mount=type=cache,id=pulse-control-plane-go-mod,target=/go/pkg/mod \
|
||||
-trimpath \
|
||||
-o /pulse-control-plane ./cmd/pulse-control-plane
|
||||
|
||||
FROM alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc
|
||||
FROM alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc AS control-plane-runtime-foundation
|
||||
RUN apk add --no-cache ca-certificates docker-cli
|
||||
COPY --from=builder /pulse-control-plane /usr/local/bin/pulse-control-plane
|
||||
ENTRYPOINT ["pulse-control-plane"]
|
||||
|
||||
FROM control-plane-runtime-foundation AS control_plane_prebuilt
|
||||
ARG TARGETARCH
|
||||
COPY --from=compiled_payload /binaries/pulse-control-plane-linux-${TARGETARCH:-amd64} /usr/local/bin/pulse-control-plane
|
||||
RUN chmod 755 /usr/local/bin/pulse-control-plane
|
||||
|
||||
FROM control-plane-runtime-foundation AS runtime
|
||||
COPY --from=builder /pulse-control-plane /usr/local/bin/pulse-control-plane
|
||||
|
||||
@@ -551,8 +551,9 @@ upgrade, update, release, or artifact-selection behavior.
|
||||
not build, sign, and then discard a complete public release packet first.
|
||||
Container qualification must consume the verified immutable candidate
|
||||
archives through `scripts/prepare-release-container-context.sh`, assemble
|
||||
the prebuilt runtime and agent targets without recompiling source, and
|
||||
compare the embedded server and agent digests with the candidate bytes
|
||||
the prebuilt runtime, agent, and provider control-plane targets without
|
||||
recompiling source, and compare every embedded executable digest with the
|
||||
candidate bytes
|
||||
before exercising the same local runtime through the Helm install/upgrade
|
||||
smoke. The reusable build-release-candidate workflow owns this proof so a
|
||||
standalone candidate dispatch, a release dry run, and a publishing release
|
||||
@@ -1904,26 +1905,29 @@ updated last.
|
||||
That same governed release lineage now also owns artifact attestation and
|
||||
secret-safe container builds. Release workflows must publish max-level image
|
||||
provenance plus SBOM attestations, push keyless GitHub/Sigstore attestations
|
||||
for the published server and agent images, attest the generated release packet
|
||||
assets from the `release/` directory, and pass the embedded license public key
|
||||
through BuildKit secret mounts instead of Docker build arguments so release
|
||||
metadata and image history cannot re-expose it.
|
||||
Because BuildKit secret contents are intentionally excluded from layer cache
|
||||
keys, those Docker builds must also pass a non-secret SHA-256 fingerprint of
|
||||
the mounted license public key through `PULSE_LICENSE_PUBLIC_KEY_SHA256` and
|
||||
the `Dockerfile` must verify that fingerprint before embedding the key. A
|
||||
release image build must fail closed if the fingerprint is present but the
|
||||
secret is missing, malformed, or mismatched, so cached no-key binaries cannot
|
||||
be reused for release-grade hosted or self-hosted runtime images. The matching
|
||||
installability proof lives in `scripts/installtests/build_release_assets_test.go`
|
||||
and `scripts/release_control/release_promotion_policy_test.py`, and both must
|
||||
assert the secret mount and non-secret fingerprint argument together.
|
||||
for the published server and provider control-plane images, and attest the
|
||||
generated release packet assets from the `release/` directory. The exact-SHA
|
||||
candidate compiler must validate and embed the governed license and update
|
||||
public keys once, record the resulting runtime and control-plane binaries in
|
||||
the immutable candidate manifest, and send only those verified bytes to image
|
||||
qualification and publication. `publish-docker.yml` must not receive release
|
||||
signing material or license-key build inputs and must not recompile either
|
||||
binary.
|
||||
Source-built release-grade Docker targets remain a fail-closed diagnostic and
|
||||
development boundary. When those targets are used, they must pass the license
|
||||
public key through a BuildKit secret mount rather than a Docker build argument,
|
||||
pair it with the non-secret `PULSE_LICENSE_PUBLIC_KEY_SHA256` cache key, and
|
||||
verify the fingerprint before embedding the key. The matching installability
|
||||
proof lives in `scripts/installtests/build_release_assets_test.go` and
|
||||
`scripts/release_control/release_promotion_policy_test.py`.
|
||||
The standalone hosted control-plane image is part of the same release-license
|
||||
boundary. `deploy/provider-msp/Dockerfile.control-plane` must build
|
||||
`cmd/pulse-control-plane` with `-tags release`, canonical
|
||||
`scripts/release_ldflags.sh server` metadata, an embedded license public key
|
||||
from the BuildKit `pulse_license_public_key` secret, and the same
|
||||
`PULSE_LICENSE_PUBLIC_KEY_SHA256` fingerprint gate. Provider-hosted MSP uses
|
||||
`scripts/release_ldflags.sh server` metadata, and the embedded governed license
|
||||
public key. Its source-built target must retain the BuildKit secret and
|
||||
`PULSE_LICENSE_PUBLIC_KEY_SHA256` fingerprint gate, while its published
|
||||
prebuilt target must consume the manifest-bound control-plane binaries from
|
||||
the exact candidate. Provider-hosted MSP uses
|
||||
that control-plane image for signed MSP-license enforcement, so it must not be
|
||||
possible to publish a provider MSP control-plane image that accepts
|
||||
`PULSE_LICENSE_DEV_MODE` or `PULSE_LICENSE_PUBLIC_KEY` runtime overrides.
|
||||
@@ -1931,6 +1935,10 @@ possible to publish a provider MSP control-plane image that accepts
|
||||
`rcourtman/pulse-control-plane` and
|
||||
`ghcr.io/<owner>/pulse-control-plane` from that Dockerfile, with the same
|
||||
version tags and prerelease/latest tag policy as the main Pulse runtime image.
|
||||
The reusable Docker publisher must accept the exact container artifact name
|
||||
and source SHA from its owning release run, verify both against the checked-out
|
||||
tag and candidate manifest, and expose no standalone dispatch that could
|
||||
silently rebuild different bytes for an existing release tag.
|
||||
That same supply-chain boundary also owns the checked-in build roots
|
||||
themselves. `Dockerfile` must pin its Node, Go, and Alpine bases by immutable
|
||||
manifest-list digest so multi-arch release builds do not silently drift onto a
|
||||
|
||||
@@ -108,6 +108,10 @@ for target in "${PULSE_RELEASE_SERVER_TARGETS[@]}"; do
|
||||
task_components+=(server)
|
||||
task_targets+=("${target}")
|
||||
done
|
||||
for target in "${PULSE_RELEASE_CONTROL_PLANE_TARGETS[@]}"; do
|
||||
task_components+=(control-plane)
|
||||
task_targets+=("${target}")
|
||||
done
|
||||
|
||||
build_one() {
|
||||
local component="$1"
|
||||
@@ -129,10 +133,14 @@ build_one() {
|
||||
package=./cmd/pulse
|
||||
ldflags="${server_ldflags}"
|
||||
;;
|
||||
control-plane)
|
||||
package=./cmd/pulse-control-plane
|
||||
ldflags="${server_ldflags}"
|
||||
;;
|
||||
esac
|
||||
read -r -a target_env_parts <<<"${target_env}"
|
||||
command=(go build "${release_go_build_args[@]}")
|
||||
if [[ "${component}" == server ]]; then command+=(-tags release); fi
|
||||
if [[ "${component}" == server || "${component}" == control-plane ]]; then command+=(-tags release); fi
|
||||
if [[ -n "${ldflags}" ]]; then command+=("-ldflags=${ldflags}"); fi
|
||||
command+=(-o "${output}" "${package}")
|
||||
env "${target_env_parts[@]}" GOMAXPROCS="${go_procs}" "${command[@]}"
|
||||
|
||||
@@ -91,6 +91,8 @@ func TestBuildReleaseUsesV6InstallScripts(t *testing.T) {
|
||||
for _, needle := range []string{
|
||||
`release_go_build_args=(-buildvcs=false -trimpath)`,
|
||||
`command=(go build "${release_go_build_args[@]}")`,
|
||||
`package=./cmd/pulse-control-plane`,
|
||||
`task_components+=(control-plane)`,
|
||||
} {
|
||||
if !strings.Contains(compileScript, needle) {
|
||||
t.Fatalf("build-release-binaries.sh missing clean compilation contract: %s", needle)
|
||||
@@ -168,6 +170,7 @@ func TestReleaseContainerTargetsConsumeImmutableCandidate(t *testing.T) {
|
||||
`validate_archive_entries "${archive}"`,
|
||||
`tar --no-same-owner --no-same-permissions -xzf`,
|
||||
`diff -qr --exclude=pulse`,
|
||||
`find "${output_dir}/arm64" -depth -mindepth 1`,
|
||||
} {
|
||||
if !strings.Contains(prepareScript, needle) {
|
||||
t.Fatalf("prepare-release-container-context.sh missing candidate guard: %s", needle)
|
||||
@@ -1195,8 +1198,13 @@ func TestReleaseWorkflowsUseSecretSafeAttestedImageBuilds(t *testing.T) {
|
||||
createReleaseRequired := []string{
|
||||
`Exact-Candidate Container and Helm Smoke`,
|
||||
`./scripts/prepare-release-container-context.sh`,
|
||||
`container_artifact_name`,
|
||||
`container_artifact: ${{ needs.build_release_candidate.outputs.container_artifact_name }}`,
|
||||
`source_sha: ${{ needs.create_release.outputs.target_commitish }}`,
|
||||
`release-container-payload.json`,
|
||||
`--target runtime_prebuilt`,
|
||||
`--target agent_runtime_prebuilt`,
|
||||
`--target control_plane_prebuilt`,
|
||||
`Verify container binaries match immutable candidate`,
|
||||
`PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}`,
|
||||
`Validate installer signing key pins`,
|
||||
@@ -1208,6 +1216,9 @@ func TestReleaseWorkflowsUseSecretSafeAttestedImageBuilds(t *testing.T) {
|
||||
`uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4`,
|
||||
}
|
||||
containerJob := workflowJobBlock(t, string(candidateWorkflowBytes), "qualify-release-containers")
|
||||
if !strings.Contains(containerJob, "always() && needs.build.result == 'success'") {
|
||||
t.Fatal("exact-candidate container qualification must not inherit skipped native-signing dependencies")
|
||||
}
|
||||
for _, forbidden := range []string{
|
||||
"PULSE_UPDATE_SIGNING_KEY",
|
||||
"PULSE_LICENSE_PUBLIC_KEY",
|
||||
@@ -1218,6 +1229,20 @@ func TestReleaseWorkflowsUseSecretSafeAttestedImageBuilds(t *testing.T) {
|
||||
t.Fatalf("PVE exact-candidate container qualification must not receive release authority: %s", forbidden)
|
||||
}
|
||||
}
|
||||
controlPlaneDockerfileBytes, err := os.ReadFile(repoFile("deploy", "provider-msp", "Dockerfile.control-plane"))
|
||||
if err != nil {
|
||||
t.Fatalf("read Dockerfile.control-plane: %v", err)
|
||||
}
|
||||
controlPlaneDockerfile := string(controlPlaneDockerfileBytes)
|
||||
for _, needle := range []string{
|
||||
"FROM control-plane-runtime-foundation AS control_plane_prebuilt",
|
||||
"COPY --from=compiled_payload /binaries/pulse-control-plane-linux-${TARGETARCH:-amd64}",
|
||||
"FROM control-plane-runtime-foundation AS runtime",
|
||||
} {
|
||||
if !strings.Contains(controlPlaneDockerfile, needle) {
|
||||
t.Fatalf("Dockerfile.control-plane missing exact-candidate target: %s", needle)
|
||||
}
|
||||
}
|
||||
for _, needle := range createReleaseRequired {
|
||||
if !strings.Contains(createRelease, needle) {
|
||||
t.Fatalf("create-release.yml missing attested secret-safe release build contract: %s", needle)
|
||||
@@ -1235,14 +1260,16 @@ func TestReleaseWorkflowsUseSecretSafeAttestedImageBuilds(t *testing.T) {
|
||||
publishRequired := []string{
|
||||
`provenance: mode=max`,
|
||||
`sbom: true`,
|
||||
`secrets: |`,
|
||||
`id: license_key_cache`,
|
||||
`container_artifact:`,
|
||||
`source_sha:`,
|
||||
`Download exact-candidate container payload`,
|
||||
`Verify exact-candidate container payload`,
|
||||
`target: runtime_prebuilt`,
|
||||
`release_payload=${{ runner.temp }}/release-container-payload/payload/release`,
|
||||
`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 }}`,
|
||||
`target: control_plane_prebuilt`,
|
||||
`compiled_payload=${{ runner.temp }}/release-container-payload/payload/compiled`,
|
||||
`subject-name: docker.io/rcourtman/pulse`,
|
||||
`subject-name: ghcr.io/${{ github.repository_owner }}/pulse`,
|
||||
`subject-name: docker.io/rcourtman/pulse-control-plane`,
|
||||
@@ -1263,8 +1290,15 @@ func TestReleaseWorkflowsUseSecretSafeAttestedImageBuilds(t *testing.T) {
|
||||
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")
|
||||
for _, forbidden := range []string{
|
||||
"PULSE_LICENSE_PUBLIC_KEY",
|
||||
"PULSE_UPDATE_SIGNING_KEY",
|
||||
"pulse_license_public_key",
|
||||
"pulse_update_signing_key",
|
||||
} {
|
||||
if strings.Contains(publish, forbidden) {
|
||||
t.Fatalf("publish-docker.yml must assemble the verified candidate without release build secrets: %s", forbidden)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,35 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestProviderMSPControlPlaneImageConsumesExactCandidate(t *testing.T) {
|
||||
dockerfileBytes, err := os.ReadFile(repoFile("deploy", "provider-msp", "Dockerfile.control-plane"))
|
||||
if err != nil {
|
||||
t.Fatalf("read provider MSP control-plane Dockerfile: %v", err)
|
||||
}
|
||||
publishBytes, err := os.ReadFile(repoFile(".github", "workflows", "publish-docker.yml"))
|
||||
if err != nil {
|
||||
t.Fatalf("read Docker publisher: %v", err)
|
||||
}
|
||||
dockerfile := string(dockerfileBytes)
|
||||
publish := string(publishBytes)
|
||||
assertContainsAll(t, dockerfile,
|
||||
"FROM control-plane-runtime-foundation AS control_plane_prebuilt",
|
||||
"COPY --from=compiled_payload /binaries/pulse-control-plane-linux-${TARGETARCH:-amd64}",
|
||||
"FROM control-plane-runtime-foundation AS runtime",
|
||||
)
|
||||
assertContainsAll(t, publish,
|
||||
"Verify exact-candidate container payload",
|
||||
"target: control_plane_prebuilt",
|
||||
"compiled_payload=${{ runner.temp }}/release-container-payload/payload/compiled",
|
||||
)
|
||||
assertNotContainsAny(t, publish,
|
||||
"PULSE_LICENSE_PUBLIC_KEY",
|
||||
"PULSE_UPDATE_SIGNING_KEY",
|
||||
"pulse_license_public_key",
|
||||
"pulse_update_signing_key",
|
||||
)
|
||||
}
|
||||
|
||||
func TestProviderMSPDeployComposeIsProviderModeAndStripeFree(t *testing.T) {
|
||||
composePath := repoFile("deploy", "provider-msp", "docker-compose.yml")
|
||||
composeBytes, err := os.ReadFile(composePath)
|
||||
|
||||
@@ -108,4 +108,13 @@ if ! diff -qr --exclude=pulse "${output_dir}/amd64" "${output_dir}/arm64"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The universal agent/script payload is identical in both server archives.
|
||||
# Keep one copy plus the arm64 server and VERSION needed by the multi-arch
|
||||
# runtime target so Actions transfers do not carry the same payload twice.
|
||||
find "${output_dir}/arm64" -depth -mindepth 1 \
|
||||
! -path "${output_dir}/arm64/bin" \
|
||||
! -path "${output_dir}/arm64/bin/pulse" \
|
||||
! -path "${output_dir}/arm64/VERSION" \
|
||||
-delete
|
||||
|
||||
echo "Prepared exact-candidate container context at ${output_dir}."
|
||||
|
||||
@@ -26,6 +26,11 @@ PULSE_RELEASE_SERVER_TARGETS=(
|
||||
linux-386
|
||||
)
|
||||
|
||||
PULSE_RELEASE_CONTROL_PLANE_TARGETS=(
|
||||
linux-amd64
|
||||
linux-arm64
|
||||
)
|
||||
|
||||
pulse_release_target_env() {
|
||||
case "$1" in
|
||||
linux-amd64) printf '%s\n' 'GOOS=linux GOARCH=amd64' ;;
|
||||
@@ -56,6 +61,7 @@ pulse_release_binary_filename() {
|
||||
agent) filename="pulse-agent-${target}" ;;
|
||||
mcp) filename="pulse-mcp-${target}" ;;
|
||||
server) filename="pulse-${target}" ;;
|
||||
control-plane) filename="pulse-control-plane-${target}" ;;
|
||||
*)
|
||||
echo "Error: unsupported release component: ${component}" >&2
|
||||
return 1
|
||||
|
||||
@@ -1561,7 +1561,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
||||
self.assertIn('./scripts/backfill-release-assets.sh --tag "${{ inputs.tag }}" --repo "${{ github.repository }}"', backfill_workflow)
|
||||
self.assertIn('./scripts/validate-published-release.sh "${{ inputs.tag }}" "${{ github.repository }}"', backfill_workflow)
|
||||
self.assertIn("PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}", backfill_workflow)
|
||||
self.assertIn("./scripts/prepare-release-container-context.sh", docker_build)
|
||||
self.assertIn("./scripts/prepare-release-container-context.sh", candidate_workflow)
|
||||
self.assertIn('test "${actual_server}" = "${expected_server}"', docker_build)
|
||||
self.assertIn('test "${actual_agent}" = "${expected_agent}"', docker_build)
|
||||
self.assertNotIn("secrets.", docker_build)
|
||||
@@ -1713,10 +1713,11 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
||||
self.assertNotIn("subject-name: docker.io/rcourtman/pulse-agent", publish)
|
||||
self.assertNotIn("subject-name: ghcr.io/${{ github.repository_owner }}/pulse-agent", publish)
|
||||
self.assertIn("create-storage-record: false", publish)
|
||||
self.assertIn("id: license_key_cache", publish)
|
||||
self.assertIn("PULSE_LICENSE_PUBLIC_KEY_SHA256=${{ steps.license_key_cache.outputs.sha256 }}", publish)
|
||||
self.assertIn("pulse_license_public_key=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}", publish)
|
||||
self.assertIn("PULSE_UPDATE_SIGNING_PUBLIC_KEY=${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}", publish)
|
||||
self.assertIn("target: runtime_prebuilt", publish)
|
||||
self.assertIn("target: control_plane_prebuilt", publish)
|
||||
self.assertIn("Verify exact-candidate container payload", publish)
|
||||
self.assertNotIn("PULSE_LICENSE_PUBLIC_KEY", publish)
|
||||
self.assertNotIn("PULSE_UPDATE_SIGNING_KEY", publish)
|
||||
self.assertNotIn("provenance: false", publish)
|
||||
self.assertIn("validate_artifact_release_line.py", promote)
|
||||
self.assertIn("control_plane.py --branch-for-version", demo)
|
||||
|
||||
Reference in New Issue
Block a user