Release VM Assets #47
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |