Skip to content

Clean-host image: pass the host architecture to every builder #36

Clean-host image: pass the host architecture to every builder

Clean-host image: pass the host architecture to every builder #36

Workflow file for this run

name: Native Linux release
on:
push:
tags: ['v*']
concurrency:
group: native-release-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: read
jobs:
# Owner provisioning is intentionally required. No environment, key, or
# reviewed tooling commit is fabricated by this workflow.
configuration:
runs-on: ubuntu-24.04
timeout-minutes: 5
outputs:
tooling: ${{ steps.validate.outputs.tooling }}
environment: ${{ steps.validate.outputs.environment }}
steps:
- id: validate
name: Require approved signing configuration and repository identity
env:
SIGNING_ENVIRONMENT: ${{ vars.NATIVE_RELEASE_SIGNING_ENVIRONMENT }}
TOOLING_COMMIT: ${{ vars.NATIVE_RELEASE_TOOLING_COMMIT }}
REPOSITORY_ID: ${{ github.repository_id }}
OWNER_ID: ${{ github.repository_owner_id }}
run: |
node --input-type=module <<'NODE'
import assert from 'node:assert/strict';
import { appendFileSync } from 'node:fs';
const env = process.env;
assert.equal(env.GITHUB_REPOSITORY, 'ShadowfetchStudio/ShadowCode');
assert.equal(env.REPOSITORY_ID, '1377099349');
assert.equal(env.OWNER_ID, '209457103');
assert.equal(env.GITHUB_EVENT_NAME, 'push');
assert.equal(env.GITHUB_REF_TYPE, 'tag');
assert.match(env.GITHUB_REF_NAME, /^v(0|[1-9][0-9]{0,8})\.(0|[1-9][0-9]{0,8})\.(0|[1-9][0-9]{0,8})$/);
assert.match(env.TOOLING_COMMIT || '', /^[a-f0-9]{40}$/, 'Provision a reviewed immutable tooling commit');
assert.match(env.SIGNING_ENVIRONMENT || '', /^[A-Za-z0-9][A-Za-z0-9 _.-]{0,254}$/, 'Provision an existing protected signing environment');
appendFileSync(env.GITHUB_OUTPUT, `tooling=${env.TOOLING_COMMIT}\nenvironment=${env.SIGNING_ENVIRONMENT}\n`);
NODE
build:
needs: configuration
runs-on: ubuntu-24.04
timeout-minutes: 150
env:
RUSTUP_TOOLCHAIN: '1.95.0'
SHADOWCODE_BUILD_JOBS: '4'
# The Debian package must pass lintian (errors fail; warnings are reported).
SHADOWCODE_REQUIRE_LINTIAN: '1'
outputs:
artifact-id: ${{ steps.unsigned.outputs.artifact-id }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '22'
cache: npm
cache-dependency-path: ui/package-lock.json
- name: Native package dependencies
run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev librsvg2-dev libayatana-appindicator3-dev libasound2-dev patchelf webkit2gtk-driver xvfb appstream dbus-daemon dbus-bin lintian desktop-file-utils
- name: Test release staging and immutable retry behavior
run: node scripts/native-release-verification.mjs run release-tests
- name: Verify publisher authentication and private release snapshots
run: node scripts/native-release-verification.mjs run release-auth
- name: Validate native release tag
run: node scripts/native-release-verification.mjs run release-tag
- name: Managed llama.cpp build dependencies
run: sudo apt-get install -y cmake ninja-build libssl-dev libvulkan-dev glslc spirv-headers
- name: Install the pinned Rust toolchain
run: rustup toolchain install 1.95.0 --profile minimal --component rustfmt --component clippy
- name: Build and test the embedded interface
run: node scripts/native-release-verification.mjs run interface
- name: Verify the exact GLib backport and optimized iterator behavior
run: node scripts/native-release-verification.mjs run glib-backport
- name: Verify native source
run: node scripts/native-release-verification.mjs run native-source
- name: Qualify cleanup over actual built UI dependencies and output
run: node scripts/native-release-verification.mjs run built-project-cleanup
- name: Preserve built-project cleanup diagnostics
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: built-project-cleanup-diagnostics
path: artifacts/built-project-cleanup
- name: Exercise native application behavior
run: node scripts/native-release-verification.mjs run native-behavior
- name: Exercise the native desktop window
run: node scripts/native-release-verification.mjs run native-window
- name: Qualify 100 sequential tasks through the native window
run: node scripts/native-release-verification.mjs run native-endurance
- name: Preserve native window and Markdown worker diagnostics
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: native-window-diagnostics
path: |
artifacts/native
artifacts/native-markdown
artifacts/native-endurance
artifacts/native-x11
- name: Build the managed llama.cpp runtime (Vulkan + all CPU variants)
run: node scripts/native-release-verification.mjs run managed-runtime
- name: Build and inspect release packages
run: node scripts/native-release-verification.mjs run packages
- name: Install and first-launch both release packages in an offline clean host
run: node scripts/native-release-verification.mjs run clean-host-packages
- name: Exercise the packaged application
run: node scripts/native-release-verification.mjs run packaged-behavior
- name: Preserve final X11 session diagnostics
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: native-x11-session-diagnostics
path: artifacts/native-x11
- name: Create release checksums
run: cp artifacts/native-package/SHA256SUMS target/release/bundle/SHA256SUMS
- name: Verify installer replacement and checksum refusal
run: node scripts/native-release-verification.mjs run installer
- name: Preserve exact release verification receipts
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: native-release-verification
path: artifacts/release-verification
- name: Prepare immutable unsigned handoff with all required receipts
run: node scripts/prepare-native-release.mjs "$RUNNER_TEMP/unsigned-release"
- id: unsigned
name: Retain this run's exact unsigned handoff
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: unsigned-release-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/unsigned-release
if-no-files-found: error
compression-level: 0
# aarch64 packages are built and qualified natively on GitHub's Arm runner
# with the same required gates and handed off unsigned exactly like x86_64.
# The signer authenticates each target's bundle; its release metadata carries
# an -aarch64 suffix so both targets publish into one release.
build-aarch64:
needs: configuration
runs-on: ubuntu-24.04-arm
timeout-minutes: 180
env:
RUSTUP_TOOLCHAIN: '1.95.0'
SHADOWCODE_BUILD_JOBS: '4'
SHADOWCODE_REQUIRE_LINTIAN: '1'
outputs:
artifact-id: ${{ steps.unsigned.outputs.artifact-id }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '22'
cache: npm
cache-dependency-path: ui/package-lock.json
- name: Native package dependencies
run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev librsvg2-dev libayatana-appindicator3-dev libasound2-dev patchelf webkit2gtk-driver xvfb appstream dbus-daemon dbus-bin lintian desktop-file-utils xdg-utils cmake ninja-build libssl-dev libvulkan-dev glslc spirv-headers gcc-14 g++-14
- name: Install the pinned Rust toolchain
run: rustup toolchain install 1.95.0 --profile minimal --component rustfmt --component clippy
- name: Run every required release gate in order on aarch64
run: |
set -euo pipefail
gates=$(node --input-type=module -e "const { REQUIRED_GATES } = await import('./scripts/native-release-verification.mjs'); console.log(REQUIRED_GATES.join(' '))")
for gate in $gates; do
# The installer gate verifies the checksums the packages gate wrote.
if [ "$gate" = installer ]; then cp artifacts/native-package/SHA256SUMS target/release/bundle/SHA256SUMS; fi
node scripts/native-release-verification.mjs run "$gate"
done
- name: Preserve aarch64 qualification diagnostics
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: native-release-aarch64-diagnostics
path: |
artifacts/release-verification
artifacts/built-project-cleanup
artifacts/native
artifacts/native-markdown
artifacts/native-endurance
artifacts/native-x11
artifacts/native-package
- name: Prepare immutable unsigned aarch64 handoff with all required receipts
run: node scripts/prepare-native-release.mjs "$RUNNER_TEMP/unsigned-release"
- id: unsigned
name: Retain this run's exact unsigned aarch64 handoff
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: unsigned-release-aarch64-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/unsigned-release
if-no-files-found: error
compression-level: 0
sign:
needs: [configuration, build, build-aarch64]
runs-on: ubuntu-24.04
timeout-minutes: 30
permissions:
contents: read
actions: read
environment: ${{ needs.configuration.outputs.environment }}
outputs:
artifact-id: ${{ steps.signed.outputs.artifact-id }}
steps:
- name: Checkout independently approved signing tooling and public trust
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: ${{ needs.configuration.outputs.tooling }}
path: trusted
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '22'
- name: Require one build artifact identity per target
env:
RELEASE_ARTIFACT_ID: ${{ needs.build.outputs.artifact-id }}
AARCH64_ARTIFACT_ID: ${{ needs.build-aarch64.outputs.artifact-id }}
run: node -e "const assert = require('node:assert/strict'); for (const id of [process.env.RELEASE_ARTIFACT_ID, process.env.AARCH64_ARTIFACT_ID]) assert.match(id || '', /^[1-9][0-9]*$/); assert.notEqual(process.env.RELEASE_ARTIFACT_ID, process.env.AARCH64_ARTIFACT_ID)"
- name: Download only the immutable artifact ID produced by this build
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
artifact-ids: ${{ needs.build.outputs.artifact-id }}
path: ${{ runner.temp }}/unsigned-download
merge-multiple: true
- name: Download only the immutable artifact ID produced by the aarch64 build
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
artifact-ids: ${{ needs.build-aarch64.outputs.artifact-id }}
path: ${{ runner.temp }}/unsigned-download-aarch64
merge-multiple: true
- name: Sign and reverify each target's qualified data with the protected key
working-directory: trusted
env:
NATIVE_RELEASE_SIGNING_KEY_PEM: ${{ secrets.NATIVE_RELEASE_SIGNING_KEY_PEM }}
run: |
mkdir -m 700 "$RUNNER_TEMP/signed-release"
node scripts/sign-native-release.mjs "$RUNNER_TEMP/unsigned-download" release/trust "$RUNNER_TEMP/signed-release/x86_64"
node scripts/sign-native-release.mjs "$RUNNER_TEMP/unsigned-download-aarch64" release/trust "$RUNNER_TEMP/signed-release/aarch64" aarch64-unknown-linux-gnu
- id: signed
name: Retain only authenticated public release data
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: signed-release-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/signed-release
if-no-files-found: error
compression-level: 0
publish:
needs: [configuration, build, sign]
runs-on: ubuntu-24.04
timeout-minutes: 30
permissions:
contents: write
actions: read
steps:
- name: Checkout independently approved publication tooling and public trust
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: ${{ needs.configuration.outputs.tooling }}
path: trusted
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '22'
- name: Require one signed artifact identity
env:
RELEASE_ARTIFACT_ID: ${{ needs.sign.outputs.artifact-id }}
run: node -e "require('node:assert/strict').match(process.env.RELEASE_ARTIFACT_ID || '', /^[1-9][0-9]*$/)"
- name: Download only this signing job's immutable public bundle
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
artifact-ids: ${{ needs.sign.outputs.artifact-id }}
path: ${{ runner.temp }}/signed-download
merge-multiple: true
- name: Authenticate private snapshots before any release operation
working-directory: trusted
env:
GH_TOKEN: ${{ github.token }}
run: node scripts/publish-native-release.mjs "$RUNNER_TEMP/signed-download/x86_64" release/trust "$RUNNER_TEMP/signed-download/aarch64"
# Signed APT repository on GitHub Pages (gh-pages branch, path apt/). Only
# the .deb that passed publisher authentication is indexed; the repository
# signing key is a separate OpenPGP key held in the protected environment.
apt-repository:
needs: [configuration, sign, publish]
runs-on: ubuntu-24.04
timeout-minutes: 20
permissions:
contents: write
environment: ${{ needs.configuration.outputs.environment }}
steps:
- name: Checkout independently approved tooling and public trust
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: ${{ needs.configuration.outputs.tooling }}
path: trusted
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '22'
- name: Require a pinned APT signing fingerprint
env:
APT_FPR: ${{ vars.NATIVE_APT_SIGNING_FINGERPRINT }}
run: node -e "require('node:assert/strict').match(process.env.APT_FPR || '', /^[0-9A-F]{40}$/, 'Provision NATIVE_APT_SIGNING_FINGERPRINT')"
- name: Download only this signing job's immutable public bundle
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
artifact-ids: ${{ needs.sign.outputs.artifact-id }}
path: ${{ runner.temp }}/signed-download
merge-multiple: true
- name: Authenticate each target's .deb against the tracked trust bundle
working-directory: trusted
run: |
version="${GITHUB_REF_NAME#v}"
mkdir -m 700 "$RUNNER_TEMP/verified-deb"
for pair in x86_64:amd64 aarch64:arm64; do
bash scripts/verify-native-release.sh \
--bundle-dir "$RUNNER_TEMP/signed-download/${pair%%:*}" \
--trust-dir release/trust \
--artifact "ShadowCode_${version}_${pair#*:}.deb" \
--stage-dir "$RUNNER_TEMP/verified-deb/${pair#*:}" \
--expect-version "$version"
done
- name: Checkout the published Pages branch
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
continue-on-error: true
id: pages
with:
ref: gh-pages
path: pages
- name: Build and sign the repository, keeping earlier pool packages
working-directory: trusted
env:
APT_REPO_SIGNING_KEY: ${{ secrets.NATIVE_APT_SIGNING_KEY }}
APT_FPR: ${{ vars.NATIVE_APT_SIGNING_FINGERPRINT }}
run: |
previous=()
[ -d "$GITHUB_WORKSPACE/pages/apt" ] && previous=(--previous "$GITHUB_WORKSPACE/pages/apt")
node scripts/build-apt-repo.mjs --out "$RUNNER_TEMP/apt" --fingerprint "$APT_FPR" "${previous[@]}" "$RUNNER_TEMP"/verified-deb/*/*.deb
gpgv --keyring "$RUNNER_TEMP/apt/shadowcode-archive-keyring.gpg" "$RUNNER_TEMP/apt/dists/stable/InRelease"
- name: Publish to gh-pages
env:
GH_TOKEN: ${{ github.token }}
run: |
if [ "${{ steps.pages.outcome }}" != success ]; then
rm -rf pages && mkdir pages && cd pages
git init -q -b gh-pages
git remote add origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
touch .nojekyll
else
cd pages
fi
rm -rf apt && cp -a "$RUNNER_TEMP/apt" apt
git add -A
git -c user.name=github-actions -c user.email=github-actions@users.noreply.github.com \
commit -q -m "APT repository for ${GITHUB_REF_NAME}"
git push origin HEAD:gh-pages