Skip to content

Release VM Assets

Release VM Assets #47

name: Release VM Assets
on:
workflow_dispatch:
inputs:
channel:
description: Asset channel to publish.
required: true
type: choice
options:
- stable
- nightly
profile:
description: Profile id that owns the VM image build.
required: true
dry_run:
description: Build and validate without deploying release.capsem.org.
required: true
type: boolean
default: true
concurrency:
group: capsem-release-${{ inputs.channel }}
cancel-in-progress: false
permissions:
actions: read
attestations: write
contents: write
deployments: write
id-token: write
env:
ASSET_MANIFEST_URL: https://release.capsem.org/assets/${{ inputs.channel }}/manifest.json
jobs:
runtime-preflight:
uses: ./.github/workflows/release-runtime-preflight.yaml
with:
channel: ${{ inputs.channel }}
bootstrap_missing_first_party: true
fast-gate:
uses: ./.github/workflows/fast-gate.yaml
resolve-current-binary:
needs: runtime-preflight
runs-on: ubuntu-latest
name: Resolve current channel binary
outputs:
functional_ready: ${{ steps.functional-cohort.outputs.functional-ready }}
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
- uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c
with:
toolchain: 1.97.1
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32
with:
key: profile-release-selection
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86
- run: uv sync
- name: Select exact public-before manifest
id: public-before
run: |
python3 scripts/select-runtime-preflight-manifest.py \
--channel "${{ inputs.channel }}" \
--bootstrap-missing-first-party \
--github-output "$GITHUB_OUTPUT"
- name: Validate selected channel profile through capsem-admin
run: |
mkdir -p target
cargo run -p capsem-admin -- validate \
--channel "${{ inputs.channel }}" \
--profile "${{ inputs.profile }}" \
--json > target/profile-release-selection.json
- name: Fetch latest selected channel source manifest
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
python3 scripts/fetch-channel-source-manifest.py \
--channel "${{ inputs.channel }}" \
--profile "${{ inputs.profile }}" \
--bootstrap-missing-first-party \
--output target/channel-source/manifest.json
- name: Project inactive first-channel public-before state
if: ${{ steps.public-before.outputs.bootstrap == 'true' }}
run: |
python3 scripts/project-first-channel-before.py \
--source-manifest target/channel-source/manifest.json \
--channel "${{ inputs.channel }}" \
--bootstrap "${{ steps.public-before.outputs.bootstrap }}" \
--output target/channel-source/public-before.json
- name: Select public-before authority for exact pairing
id: public-before-authority
run: |
if [[ "${{ steps.public-before.outputs.bootstrap }}" == "true" ]]; then
echo "manifest-url=file://$PWD/target/channel-source/public-before.json" \
>> "$GITHUB_OUTPUT"
else
echo "manifest-url=${{ steps.public-before.outputs.manifest-url }}" \
>> "$GITHUB_OUTPUT"
fi
- name: Fetch exact deployed public-before package
uses: ./.github/actions/fetch-release-inputs
with:
manifest-url: ${{ steps.public-before-authority.outputs.manifest-url }}
kind: packages
output: target/profile-public-before/packages
- name: Check pulled package functional binary cohort
id: functional-cohort
run: |
uv run python scripts/stage-release-test-inputs.py \
--input-dir target/profile-public-before/packages \
--check-functional-cohort \
--github-output "$GITHUB_OUTPUT"
- name: Fetch exact deployed public-before profiles
uses: ./.github/actions/fetch-release-inputs
with:
manifest-url: ${{ steps.public-before-authority.outputs.manifest-url }}
allow-empty-profiles: ${{ steps.public-before.outputs.bootstrap }}
kind: profiles
architecture: x86_64
output: target/profile-public-before/profiles
- name: Prove public-before cohorts use one manifest
run: |
uv run python scripts/verify-release-inputs.py \
--input-dir target/profile-public-before/profiles
cmp \
target/profile-public-before/packages/manifest.json \
target/profile-public-before/profiles/manifest.json
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: profile-public-before-packages
path: target/profile-public-before/packages/
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: profile-public-before-profiles
path: target/profile-public-before/profiles/
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: profile-channel-source
path: target/channel-source/manifest.json
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: profile-release-selection
path: target/profile-release-selection.json
cloudflare-release-site-preflight:
runs-on: ubuntu-latest
name: Cloudflare release site preflight
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
DRY_RUN: ${{ inputs.dry_run }}
RELEASE_CHANNEL_PROJECT: release
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
- name: Verify Cloudflare Pages project for live publish
run: |
set -euo pipefail
if [[ "$DRY_RUN" == "true" ]]; then
echo "Dry run: skipping Cloudflare Pages project preflight."
exit 0
fi
python scripts/check-cloudflare-pages-project.py \
--project "$RELEASE_CHANNEL_PROJECT"
resolve-profile-assets:
needs: resolve-current-binary
runs-on: ubuntu-latest
name: Resolve reusable profile assets
outputs:
reuse_run_id: ${{ steps.resolve.outputs.run_id }}
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: profile-release-selection
path: target/
- name: Resolve exact prior profile asset cohort
id: resolve
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
python3 scripts/resolve-reusable-profile-assets.py \
--repository "${{ github.repository }}" \
--workflow release-assets.yaml \
--current-run-id "${{ github.run_id }}" \
--source-commit "${{ github.sha }}" \
--selection target/profile-release-selection.json \
--github-output "$GITHUB_OUTPUT"
build-assets:
needs: [cloudflare-release-site-preflight, resolve-current-binary, fast-gate, resolve-profile-assets]
if: ${{ needs.resolve-profile-assets.outputs.reuse_run_id == '' }}
strategy:
matrix:
include:
- arch: arm64
runner: ubuntu-24.04-arm
rust-target: aarch64-unknown-linux-musl
- arch: x86_64
runner: ubuntu-24.04
rust-target: x86_64-unknown-linux-musl
runs-on: ${{ matrix.runner }}
name: Build VM assets (${{ matrix.arch }})
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86
- run: uv sync
- uses: extractions/setup-just@f8a3cce218d9f83db3a2ecd90e41ac3de6cdfd9b
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320
with:
version: 10
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444
with:
node-version: 24
- name: Install musl C toolchain
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends musl-tools
- name: Install OBOM generator
run: |
npm install -g @cyclonedx/cdxgen@12.7.0
cdxgen --version
- uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c
with:
toolchain: 1.97.1
targets: ${{ matrix.rust-target }}
- name: Build VM assets (kernel + rootfs)
env:
CAPSEM_CDXGEN_CMD: cdxgen
CC_aarch64_unknown_linux_musl: musl-gcc
CC_x86_64_unknown_linux_musl: musl-gcc
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER: musl-gcc
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER: musl-gcc
run: |
just _build-kernel ${{ matrix.arch }} "${{ inputs.profile }}"
just _build-rootfs ${{ matrix.arch }} "${{ inputs.profile }}"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: vm-assets-${{ matrix.arch }}
path: assets/${{ matrix.arch }}/
reuse-assets:
needs: [cloudflare-release-site-preflight, fast-gate, resolve-profile-assets]
if: ${{ needs.resolve-profile-assets.outputs.reuse_run_id != '' }}
strategy:
matrix:
arch: [arm64, x86_64]
runs-on: ubuntu-latest
name: Reuse VM assets (${{ matrix.arch }})
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: vm-assets-${{ matrix.arch }}
path: assets/${{ matrix.arch }}/
repository: ${{ github.repository }}
run-id: ${{ needs.resolve-profile-assets.outputs.reuse_run_id }}
github-token: ${{ github.token }}
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: vm-assets-${{ matrix.arch }}
path: assets/${{ matrix.arch }}/
test-profile-pairing:
needs: [author-profile-release, resolve-current-binary]
if: ${{ always() && needs.author-profile-release.result == 'success' && needs.resolve-current-binary.result == 'success' && needs.author-profile-release.outputs.release_needed == 'true' }}
runs-on: ubuntu-24.04
name: Complete profile pairing gate
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86
- run: uv sync
- uses: extractions/setup-just@f8a3cce218d9f83db3a2ecd90e41ac3de6cdfd9b
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320
with:
version: 10
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444
with:
node-version: 24
cache: pnpm
cache-dependency-path: |
frontend/pnpm-lock.yaml
release-site/pnpm-lock.yaml
- name: Install shared functional web dependencies
run: |
(cd frontend && pnpm install --frozen-lockfile)
(cd release-site && pnpm install --frozen-lockfile)
- uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c
with:
toolchain: 1.97.1
components: llvm-tools
targets: aarch64-unknown-linux-musl,x86_64-unknown-linux-musl
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: profile-public-before-packages
path: target/profile-public-before/packages/
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: profile-public-before-profiles
path: target/profile-public-before/profiles/
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: authored-profile-channel-source
path: target/source-channel/
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: authored-profile-publication
path: target/asset-release/
- name: Enable KVM for complete functional proof
run: |
set -euo pipefail
sudo modprobe kvm
sudo chmod 0666 /dev/kvm
sudo modprobe vhost_vsock
sudo chmod 0666 /dev/vhost-vsock
test -r /dev/kvm -a -w /dev/kvm
test -r /dev/vhost-vsock -a -w /dev/vhost-vsock
- name: Prepare exact profile and pulled binary pairing
run: |
set -euo pipefail
uv run python scripts/verify-release-inputs.py \
--input-dir target/profile-public-before/packages
uv run python scripts/verify-release-inputs.py \
--input-dir target/profile-public-before/profiles
cmp \
target/profile-public-before/packages/manifest.json \
target/profile-public-before/profiles/manifest.json
PUBLICATION_IDENTITY="${{ needs.author-profile-release.outputs.publication_identity }}"
PUBLICATION_BASE="https://github.com/${{ github.repository }}/releases/download/$PUBLICATION_IDENTITY"
PUBLICATION_DIR="target/asset-release/$PUBLICATION_IDENTITY"
uv run python scripts/fetch-release-artifacts.py \
--manifest-url "file://$PWD/target/source-channel/manifest.json" \
--kind profiles \
--output target/candidate-profile-inputs \
--local-publication-base "$PUBLICATION_BASE" \
--local-publication-dir "$PUBLICATION_DIR"
uv run python scripts/verify-release-inputs.py \
--input-dir target/candidate-profile-inputs
uv run python scripts/stage-release-test-inputs.py \
--input-dir target/profile-public-before/packages \
--binary-dir target/debug
uv run python scripts/stage-release-test-inputs.py \
--input-dir target/candidate-profile-inputs \
--assets-dir assets \
--config-root target/release-config \
--shared-config-root config
CAPSEM_ASSET_MANIFEST="file://$PWD/assets/manifest.json" \
CAPSEM_CONFIG_ROOT="$PWD/target/release-config" \
CAPSEM_CONFIG_OUTPUT_ROOT="$PWD/target/config" \
bash scripts/materialize-config.sh
package=$(uv run python scripts/stage-release-test-inputs.py \
--input-dir target/profile-public-before/packages \
--print-package-path)
test -n "$package"
uv run python scripts/install-deb-runtime-dependencies.py "$package"
{
echo "CAPSEM_RELEASE_PACKAGE=$PWD/$package"
echo "CAPSEM_RELEASE_BIN_DIR=$PWD/target/debug"
echo "CAPSEM_RELEASE_INPUT_DIR=$PWD/target/candidate-profile-inputs"
echo "CAPSEM_RELEASE_CHANNEL=${{ inputs.channel }}"
echo "CAPSEM_RELEASE_TRANSITION=profile_only"
echo "CAPSEM_RELEASE_BEFORE_MANIFEST=$PWD/target/profile-public-before/profiles/manifest.json"
echo "CAPSEM_RELEASE_AFTER_MANIFEST=$PWD/target/source-channel/manifest.json"
echo "CAPSEM_RELEASE_BEFORE_PACKAGE=$PWD/$package"
echo "CAPSEM_RELEASE_BEFORE_PROFILE_INPUTS=$PWD/target/profile-public-before/profiles"
echo "CAPSEM_RELEASE_AFTER_PROFILE_INPUTS=$PWD/target/candidate-profile-inputs"
echo "CAPSEM_RELEASE_PROFILE=${{ inputs.profile }}"
echo "CAPSEM_RELEASE_CANDIDATE_PROFILE_PUBLICATION=$PWD/$PUBLICATION_DIR"
echo "CAPSEM_RELEASE_PUBLICATION_BASE=$PUBLICATION_BASE"
echo "CAPSEM_TEST_BINARY=$PWD/target/debug/capsem"
echo "CAPSEM_TEST_ASSETS_DIR=$PWD/assets"
echo "CAPSEM_TEST_CONFIG_ROOT=$PWD/target/config"
} >> "$GITHUB_ENV"
- name: Run shared artifact module
run: just _test-artifacts
- name: Record deferred profile staging boundary
if: ${{ needs.author-profile-release.outputs.activation_ready != 'true' }}
run: |
set -euo pipefail
PRODUCT_COMPATIBLE="${{ needs.author-profile-release.outputs.product_compatible }}"
FUNCTIONAL_READY="${{ needs.author-profile-release.outputs.functional_ready }}"
[[ "$PRODUCT_COMPATIBLE" == "true" || "$PRODUCT_COMPATIBLE" == "false" ]]
[[ "$FUNCTIONAL_READY" == "true" || "$FUNCTIONAL_READY" == "false" ]]
[[ "$PRODUCT_COMPATIBLE" != "true" || "$FUNCTIONAL_READY" != "true" ]]
echo "::notice::Profile assets passed static, integrity, isolation, and x86_64 KVM boot gates."
if [[ "$PRODUCT_COMPATIBLE" == "false" ]]; then
echo "::notice::The manifest-selected binary is outside this profile's declared compatibility range."
fi
if [[ "$FUNCTIONAL_READY" == "false" ]]; then
echo "::notice::The pulled package does not contain the complete functional release binary cohort."
fi
echo "::notice::An activation-ready profile cannot defer complete pairing gates."
echo "::notice::Functional and glow-up gates are deferred to release-binaries; public activation remains blocked."
- name: Run shared complete functional module
if: ${{ needs.author-profile-release.outputs.activation_ready == 'true' }}
run: just _test-functional
- name: Run shared native and update glow-up module
if: ${{ needs.author-profile-release.outputs.activation_ready == 'true' }}
run: just _test-glowup
author-profile-release:
needs: [build-assets, reuse-assets, resolve-current-binary]
if: ${{ always() && needs.resolve-current-binary.result == 'success' && ((needs.build-assets.result == 'success' && needs.reuse-assets.result == 'skipped') || (needs.build-assets.result == 'skipped' && needs.reuse-assets.result == 'success')) }}
runs-on: ubuntu-latest
name: Author selected profile release
outputs:
source_changed: ${{ steps.profile-delta.outputs.source_changed }}
activation_needed: ${{ steps.profile-delta.outputs.activation_needed }}
release_needed: ${{ steps.profile-delta.outputs.release_needed }}
product_compatible: ${{ steps.author-release.outputs.product_compatible }}
functional_ready: ${{ steps.author-release.outputs.functional_ready }}
activation_ready: ${{ steps.author-release.outputs.activation_ready }}
publication_identity: ${{ steps.author-release.outputs.publication_identity }}
env:
CHANNEL: ${{ inputs.channel }}
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320
with:
version: 10
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444
with:
node-version: 24
cache: pnpm
cache-dependency-path: release-site/pnpm-lock.yaml
- name: Install release site dependencies
run: cd release-site && pnpm install --frozen-lockfile
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86
- run: uv sync
- uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c
with:
toolchain: 1.97.1
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: vm-assets-arm64
path: assets/arm64/
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: vm-assets-x86_64
path: assets/x86_64/
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: profile-channel-source
path: target/source-channel/
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: profile-public-before-profiles
path: target/profile-public-before/profiles/
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: profile-release-selection
path: target/
- name: Generate asset manifest
run: |
cargo run -p capsem-admin -- manifest generate assets
- name: Verify profile release lane policy
run: |
uv run pytest \
tests/capsem-release/test_profile_lane_gate.py \
tests/capsem-release/test_release_lane_diff_policy.py -q
- name: Build complete asset channel preview
run: |
PROFILE_IDENTITY=$(python3 -c 'import json; print(json.load(open("target/profile-release-selection.json"))["publication_identity"])')
ASSET_BASE="https://github.com/${{ github.repository }}/releases/download/$PROFILE_IDENTITY"
MANIFEST_VERSION=$(python3 -c 'import json; print(json.load(open("target/source-channel/manifest.json"))["version"])')
uv run python scripts/build-complete-release-channel.py \
--channel-source "$CHANNEL=file://$PWD/assets/manifest.json" \
--primary-channel "$CHANNEL" \
--assets-dir assets \
--asset-source-base "$ASSET_BASE" \
--manifest-version "$MANIFEST_VERSION" \
--out-dir target/profile-candidate \
--allow-mirror-missing
- name: Check selected channel profile delta
id: profile-delta
run: |
uv run python scripts/check-profile-release-delta.py \
--source-manifest target/source-channel/manifest.json \
--candidate-manifest "target/profile-candidate/assets/$CHANNEL/manifest.json" \
--public-manifest target/profile-public-before/profiles/manifest.json \
--channel "$CHANNEL" \
--profile "${{ inputs.profile }}" \
--json-output target/profile-release-delta/delta.json
- name: Author selected channel profile in the source manifest
id: author-release
if: ${{ steps.profile-delta.outputs.release_needed == 'true' }}
run: |
set -euo pipefail
MANIFEST_VERSION=$(python3 -c 'import json; print(json.load(open("target/source-channel/manifest.json"))["version"])')
PUBLICATION_IDENTITY=$(python3 -c 'import json; print(json.load(open("target/profile-release-selection.json"))["publication_identity"])')
PUBLICATION_BASE="https://github.com/${{ github.repository }}/releases/download/$PUBLICATION_IDENTITY"
PROFILE_VERSION=$(python3 - <<'PY'
import json
channel = "${{ inputs.channel }}"
profile = "${{ inputs.profile }}"
manifest = json.load(open(f"target/profile-candidate/assets/{channel}/manifest.json"))
print(manifest["profiles"][profile]["revision"])
PY
)
cargo run -p capsem-admin -- release \
--channel "$CHANNEL" \
--profile "${{ inputs.profile }}" \
--manifest-path target/source-channel/manifest.json \
--candidate-manifest "target/profile-candidate/assets/$CHANNEL/manifest.json" \
--publication-base "$PUBLICATION_BASE" \
--manifest-version "$MANIFEST_VERSION" \
--profile-version "$PROFILE_VERSION" \
--json > target/profile-release.json
test "$(python3 -c 'import json; print(json.load(open("target/profile-release.json"))["publication_identity"])')" = "$PUBLICATION_IDENTITY"
PRODUCT_COMPATIBLE=$(python3 -c 'import json; print(str(json.load(open("target/profile-release.json"))["compatible_with_current_binary"]).lower())')
FUNCTIONAL_READY="${{ needs.resolve-current-binary.outputs.functional_ready }}"
ACTIVATION_READY=false
if [[ "$PRODUCT_COMPATIBLE" == "true" && "$FUNCTIONAL_READY" == "true" ]]; then
ACTIVATION_READY=true
fi
echo "product_compatible=$PRODUCT_COMPATIBLE" >> "$GITHUB_OUTPUT"
echo "functional_ready=$FUNCTIONAL_READY" >> "$GITHUB_OUTPUT"
echo "activation_ready=$ACTIVATION_READY" >> "$GITHUB_OUTPUT"
echo "publication_identity=$PUBLICATION_IDENTITY" >> "$GITHUB_OUTPUT"
- name: Stage and verify immutable profile publication once
if: ${{ steps.profile-delta.outputs.release_needed == 'true' }}
env:
PUBLICATION_IDENTITY: ${{ steps.author-release.outputs.publication_identity }}
run: |
set -euo pipefail
PUBLICATION_BASE="https://github.com/${{ github.repository }}/releases/download/$PUBLICATION_IDENTITY"
RELEASE_DIR="target/asset-release/$PUBLICATION_IDENTITY"
uv run python scripts/stage-profile-publication.py \
--manifest target/source-channel/manifest.json \
--profile "${{ inputs.profile }}" \
--assets-dir assets \
--config-root config \
--release-dir "$RELEASE_DIR"
uv run python scripts/verify-profile-publication.py \
--manifest "$RELEASE_DIR/channel-source-$CHANNEL.json" \
--profile "${{ inputs.profile }}" \
--publication-base "$PUBLICATION_BASE" \
--release-dir "$RELEASE_DIR"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: authored-profile-channel-source
path: target/source-channel/manifest.json
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: authored-profile-candidate
path: target/profile-candidate/
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
if: ${{ steps.profile-delta.outputs.release_needed == 'true' }}
with:
name: authored-profile-publication
path: target/asset-release/
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: profile-release-delta
path: target/profile-release-delta/
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
if: ${{ steps.profile-delta.outputs.release_needed == 'true' }}
with:
name: authored-profile-release-report
path: target/profile-release.json
publish-profile-release:
needs: [author-profile-release, test-profile-pairing]
if: ${{ always() && needs.author-profile-release.outputs.release_needed == 'true' && needs.test-profile-pairing.result == 'success' }}
runs-on: ubuntu-latest
name: Publish verified profile assets
outputs:
release_needed: ${{ needs.author-profile-release.outputs.release_needed }}
activation_ready: ${{ needs.author-profile-release.outputs.activation_ready }}
env:
CHANNEL: ${{ inputs.channel }}
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320
with:
version: 10
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444
with:
node-version: 24
cache: pnpm
cache-dependency-path: release-site/pnpm-lock.yaml
- name: Install release site dependencies
run: cd release-site && pnpm install --frozen-lockfile
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86
- run: uv sync
- uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c
with:
toolchain: 1.97.1
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: vm-assets-arm64
path: assets/arm64/
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: vm-assets-x86_64
path: assets/x86_64/
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: authored-profile-channel-source
path: target/source-channel/
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: authored-profile-candidate
path: target/profile-candidate/
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: authored-profile-publication
path: target/asset-release/
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: profile-release-selection
path: target/
- name: Build deployable channel from authored source manifest
if: ${{ needs.author-profile-release.outputs.activation_ready == 'true' }}
run: |
uv run python scripts/build-complete-release-channel.py \
--channel-source "$CHANNEL=file://$PWD/target/source-channel/manifest.json" \
--primary-channel "$CHANNEL" \
--assets-dir assets \
--manifest-version "$(python3 -c 'import json; print(json.load(open("target/source-channel/manifest.json"))["version"])')" \
--profile-source-root target/profile-candidate \
--out-dir target/release-channel
- name: Attest VM asset provenance
if: ${{ inputs.dry_run == false }}
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373
with:
subject-path: target/asset-release/profile-*/*
- name: Publish immutable GitHub profile release
env:
DRY_RUN: ${{ inputs.dry_run }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
TAG="${{ needs.author-profile-release.outputs.publication_identity }}"
PROFILE_REVISION=$(python3 -c 'import json; print(json.load(open("target/profile-release-selection.json"))["profile_revision"])')
RELEASE_DIR="target/asset-release/$TAG"
NOTES_FILE="target/asset-release/profile-release-notes.md"
PUBLICATION_BASE="https://github.com/${{ github.repository }}/releases/download/$TAG"
uv run python scripts/verify-profile-publication.py \
--manifest "$RELEASE_DIR/channel-source-$CHANNEL.json" \
--profile "${{ inputs.profile }}" \
--publication-base "$PUBLICATION_BASE" \
--release-dir "$RELEASE_DIR"
files=("$RELEASE_DIR"/*)
test "${#files[@]}" -gt 0
printf '%s\n' \
"Immutable Capsem profile release for $CHANNEL/${{ inputs.profile }}." \
> "$NOTES_FILE"
{
echo "## Immutable VM profile release"
echo
echo "- Tag: \`$TAG\`"
echo "- Dry run: \`$DRY_RUN\`"
echo "- Files:"
# shellcheck disable=SC2016
printf ' - `%s`\n' "${files[@]}"
} >> "$GITHUB_STEP_SUMMARY"
if [[ "$DRY_RUN" == "true" ]]; then
echo "Dry run: would publish immutable VM profile release $TAG"
else
CAPSEM_RELEASE_CREATE_TITLE="Capsem $CHANNEL/${{ inputs.profile }} $PROFILE_REVISION" \
CAPSEM_RELEASE_CREATE_NOTES_FILE="$NOTES_FILE" \
CAPSEM_RELEASE_CREATE_TARGET="$GITHUB_SHA" \
scripts/publish-immutable-release-assets.sh \
"$TAG" "$RELEASE_DIR"
fi
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: asset-channel-source
path: target/source-channel/manifest.json
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
if: ${{ inputs.dry_run == true }}
with:
name: asset-release-plan
path: target/asset-release/
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
if: ${{ needs.author-profile-release.outputs.activation_ready == 'true' }}
with:
name: asset-channel-preview
path: target/release-channel/
deploy-channel:
needs: publish-profile-release
if: ${{ inputs.dry_run == false && needs.publish-profile-release.outputs.release_needed == 'true' && needs.publish-profile-release.outputs.activation_ready == 'true' }}
uses: ./.github/workflows/release-channel.yaml
with:
channel: ${{ inputs.channel }}
dist_artifact: asset-channel-preview
secrets: inherit