Skip to content

Prepare Nebula 3 Linux release #41

Prepare Nebula 3 Linux release

Prepare Nebula 3 Linux release #41

name: Prepare Nebula 3 Linux release
on:
push:
tags:
- "nebula-v3.*"
workflow_dispatch:
inputs:
release_tag:
description: Existing nebula-v3.* tag to prepare
required: true
type: string
scope:
description: Playwright coverage to run
required: true
default: impacted
type: choice
options:
- impacted
- full
selection:
description: Optional catalog selectors (area:, project:, or entry:); overrides automatic impact selection
required: false
type: string
review_reason:
description: Coverage rationale and deliberate exclusions
required: true
type: string
full_approval:
description: Exceptional full run only; type RUN_FULL_SUITE after explicit user approval
required: false
type: string
permissions:
actions: read
contents: read
concurrency:
group: nebula3-release-${{ inputs.release_tag || github.ref_name }}
cancel-in-progress: false
jobs:
validate:
runs-on: ubuntu-22.04
outputs:
tag: ${{ steps.release.outputs.tag }}
version: ${{ steps.release.outputs.version }}
channel: ${{ steps.release.outputs.channel }}
prerelease: ${{ steps.release.outputs.prerelease }}
commit: ${{ steps.release.outputs.commit }}
build_timestamp: ${{ steps.release.outputs.build_timestamp }}
steps:
- name: Check out the immutable release tag
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
ref: ${{ inputs.release_tag || github.ref_name }}
- name: Validate tag, commit, and synchronized versions
id: release
env:
RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }}
run: |
case "$RELEASE_TAG" in
nebula-v3.*) ;;
*) printf 'invalid Nebula 3 release tag: %s\n' "$RELEASE_TAG" >&2; exit 1 ;;
esac
version="${RELEASE_TAG#nebula-v}"
python scripts/nebula3_version.py check --expected "$version"
test -s "docs/releases/$version.md"
test "$(git tag --points-at HEAD | grep -Fx "$RELEASE_TAG")" = "$RELEASE_TAG"
case "$version" in
*-*) channel=prerelease; prerelease=true ;;
*) channel=stable; prerelease=false ;;
esac
commit="$(git rev-parse HEAD)"
build_timestamp="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
{
printf 'tag=%s\n' "$RELEASE_TAG"
printf 'version=%s\n' "$version"
printf 'channel=%s\n' "$channel"
printf 'prerelease=%s\n' "$prerelease"
printf 'commit=%s\n' "$commit"
printf 'build_timestamp=%s\n' "$build_timestamp"
} >> "$GITHUB_OUTPUT"
impact-baseline:
needs: validate
runs-on: ubuntu-22.04
outputs:
sha: ${{ steps.baseline.outputs.sha }}
tag: ${{ steps.baseline.outputs.tag }}
run_id: ${{ steps.baseline.outputs.run_id }}
steps:
- name: Check out the validated tag
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
ref: ${{ needs.validate.outputs.tag }}
- name: Resolve the latest successful preparation baseline
id: baseline
env:
GH_TOKEN: ${{ github.token }}
CANDIDATE_SHA: ${{ needs.validate.outputs.commit }}
run: |
gh api \
"repos/$GITHUB_REPOSITORY/actions/workflows/nebula3-release.yml/runs?status=completed&per_page=100" \
> "$RUNNER_TEMP/release-runs.json"
baseline="$(python scripts/playwright_impact.py release-baseline \
--runs-json "$RUNNER_TEMP/release-runs.json" \
--candidate "$CANDIDATE_SHA")"
sha="$(jq -r '.sha // empty' <<<"$baseline")"
tag="$(jq -r '.tag // empty' <<<"$baseline")"
run_id="$(jq -r '.run_id // empty' <<<"$baseline")"
if test -n "$sha" && test -n "$tag" \
&& git rev-parse --verify "$tag^{commit}" >/dev/null \
&& test "$(git rev-parse "$tag^{commit}")" = "$sha" \
&& git merge-base --is-ancestor "$sha" "$CANDIDATE_SHA"
then
printf 'sha=%s\ntag=%s\nrun_id=%s\n' "$sha" "$tag" "$run_id" >> "$GITHUB_OUTPUT"
else
printf 'No trustworthy baseline; explicit coverage review is required, not full coverage.\n'
printf 'sha=\ntag=\nrun_id=\n' >> "$GITHUB_OUTPUT"
fi
sandbox-integration:
needs: [validate, playwright]
runs-on: ubuntu-22.04
env:
POETRY_VIRTUALENVS_IN_PROJECT: "true"
steps:
- name: Check out the validated tag
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ needs.validate.outputs.tag }}
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.12"
cache: pip
- name: Install sandbox integration boundary
run: |
python -m pip install "poetry==2.1.3"
poetry install --with dev --no-interaction
# Hosted runners carry large SDKs this job never uses. Reclaim their
# space before pulling and extending the Kali runtime image.
sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc
df -h /
sudo apt-get update
sudo apt-get install -y podman uidmap slirp4netns fuse-overlayfs
podman info --format '{{.Host.Security.Rootless}}' | grep -Fx true
- name: Prepare real-Core and disposable-runtime fixtures
run: |
podman pull docker.io/kalilinux/kali-rolling:latest
runner_uid="$(id -u)"
user_service="/sys/fs/cgroup/user.slice/user-${runner_uid}.slice/user@${runner_uid}.service"
for control_group in \
/sys/fs/cgroup/user.slice \
"/sys/fs/cgroup/user.slice/user-${runner_uid}.slice" \
"$user_service" \
"$user_service/user.slice"
do
test -f "$control_group/cgroup.controllers"
grep -qw cpu "$control_group/cgroup.controllers"
echo +cpu | sudo tee "$control_group/cgroup.subtree_control" >/dev/null
done
grep -qw cpu "$user_service/user.slice/cgroup.subtree_control"
repository_digest="$(podman image inspect docker.io/kalilinux/kali-rolling:latest --format '{{index .RepoDigests 0}}')"
image_digest="${repository_digest##*@}"
source_reference="docker.io/kalilinux/kali-rolling@${image_digest}"
printf 'NEBULA_KALI_SOURCE_IMAGE=%s\n' "$source_reference" >> "$GITHUB_ENV"
# Exercise the admitted local rootless CLI path. Podman's v3 remote
# socket transport can leave attached run/TTY clients open after the
# worker exits, which turns successful workloads into false timeouts.
printf 'NEBULA_TEST_CONTAINER_RUNTIME=/usr/bin/podman\n' >> "$GITHUB_ENV"
podman build \
--file tests/v3/integration/Containerfile.operator-runtime \
--tag localhost/nebula-operator-runtime:release \
.
- name: Run sandbox integration suite
env:
NEBULA_TEST_RUNTIME_IMAGE: localhost/nebula-operator-runtime:release
run: poetry run pytest -q tests/v3/test_sandbox_integration.py
playwright:
needs: [validate, impact-baseline]
uses: ./.github/workflows/playwright-impact.yml
with:
baseline_sha: ${{ needs.impact-baseline.outputs.sha }}
candidate_sha: ${{ needs.validate.outputs.commit }}
candidate_ref: ${{ needs.validate.outputs.tag }}
scope: ${{ inputs.scope || 'impacted' }}
receipt_name: release-playwright-impact-${{ needs.validate.outputs.tag }}
selection: ${{ inputs.selection || '' }}
review_reason: ${{ inputs.review_reason || '' }}
full_approval: ${{ inputs.full_approval || '' }}
build:
needs: [validate, impact-baseline, sandbox-integration, playwright]
environment:
name: desktop-release
deployment: false
permissions:
contents: read
id-token: write
attestations: write
strategy:
fail-fast: false
matrix:
include:
- id: linux-x64
runner: ubuntu-22.04
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.runner }}
env:
NEBULA_VERSION: ${{ needs.validate.outputs.version }}
NEBULA_RELEASE_TAG: ${{ needs.validate.outputs.tag }}
NEBULA_BUILD_TARGET: ${{ matrix.target }}
NEBULA_BUILD_COMMIT: ${{ needs.validate.outputs.commit }}
NEBULA_BUILD_TIMESTAMP: ${{ needs.validate.outputs.build_timestamp }}
NEBULA_UPDATE_CHANNEL: ${{ needs.validate.outputs.channel }}
NEBULA_UPDATE_ENDPOINT: https://berylliumsec.github.io/nebula/updates/${{ needs.validate.outputs.channel }}/latest.json
NEBULA_UPDATER_PUBLIC_KEY: ${{ secrets.NEBULA_UPDATER_PUBLIC_KEY }}
steps:
- name: Check out the validated tag
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
ref: ${{ needs.validate.outputs.tag }}
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.12"
cache: pip
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "20"
cache: npm
cache-dependency-path: ui/package-lock.json
- uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30
with:
toolchain: stable
targets: ${{ matrix.target }}
- name: Install Linux bundler libraries
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev libfuse2 patchelf xvfb
- name: Validate protected updater credentials
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: |
test -n "$NEBULA_UPDATER_PUBLIC_KEY"
test -n "$TAURI_SIGNING_PRIVATE_KEY"
test -n "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD"
- name: Install the locked Nebula 3 build boundary
run: |
python -m pip install "poetry==2.1.3"
poetry install --with dev --no-interaction
npm --prefix ui ci
python scripts/nebula3_version.py check --expected "$NEBULA_VERSION"
poetry run python -m scripts.stage_playwright_runtime \
ui/src-tauri/resources/playwright-browsers \
--target "${{ matrix.target }}"
- name: Validate Linux packaging without repeating product suites
env:
NEBULA_UPDATER_PUBLIC_KEY: ${{ secrets.NEBULA_UPDATER_PUBLIC_KEY }}
run: |
poetry run python -m scripts.generate_third_party_notices \
--root "$GITHUB_WORKSPACE" \
--target "${{ matrix.target }}" \
--output build/nebula-core-metadata/THIRD_PARTY_NOTICES.txt
# Product journeys are covered by the reviewed selection/PR. A release
# validates packaging contracts, not every backend or frontend test.
poetry run pytest --collect-only -q tests/v3/test_packaging.py packaging/updater/test_generate_manifest.py
poetry run pytest -q tests/v3/test_packaging.py packaging/updater/test_generate_manifest.py
npm --prefix ui run build
install -d -m 0755 ui/src-tauri/binaries
touch "ui/src-tauri/binaries/nebula-core-${{ matrix.target }}"
chmod +x "ui/src-tauri/binaries/nebula-core-${{ matrix.target }}"
cargo check --locked --manifest-path ui/src-tauri/Cargo.toml
cargo check --locked --features direct-updater --manifest-path ui/src-tauri/Cargo.toml
- name: Generate the direct-build updater configuration
run: python scripts/tauri_release_config.py "$RUNNER_TEMP/tauri-updater.conf.json"
- name: Build signed direct AppImage and managed DEB
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: |
stage="$RUNNER_TEMP/release-stage"
bundle="ui/src-tauri/target/${{ matrix.target }}/release/bundle"
install -d -m 0755 "$stage"
NEBULA_DISTRIBUTION_CHANNEL=direct poetry run python -m scripts.build_nebula_core
sidecar="$(find ui/src-tauri/binaries -maxdepth 1 -type f -name 'nebula-core-*' -print -quit)"
test -n "$sidecar"
"$sidecar" doctor --data-dir "$RUNNER_TEMP/nebula-core-direct" --json
poetry run python scripts/package_audit.py "$sidecar"
NO_STRIP=1 NEBULA_DISTRIBUTION=direct npm --prefix ui run tauri -- build --ci --target "${{ matrix.target }}" --features direct-updater --bundles appimage --config src-tauri/tauri.direct.conf.json --config "$RUNNER_TEMP/tauri-updater.conf.json"
appimage="$(find "$bundle/appimage" -maxdepth 1 -type f -name '*.AppImage' -print -quit)"
test -n "$appimage"
test -s "$appimage.sig"
install -m 0755 "$appimage" "$stage/Nebula-$NEBULA_VERSION-linux-x86_64.AppImage"
install -m 0644 "$appimage.sig" "$stage/Nebula-$NEBULA_VERSION-linux-x86_64.AppImage.sig"
rm -rf "$bundle"
NEBULA_DISTRIBUTION_CHANNEL=managed poetry run python -m scripts.build_nebula_core
sidecar="$(find ui/src-tauri/binaries -maxdepth 1 -type f -name 'nebula-core-*' -print -quit)"
test -n "$sidecar"
"$sidecar" doctor --data-dir "$RUNNER_TEMP/nebula-core-managed" --json
poetry run python scripts/package_audit.py "$sidecar"
NEBULA_DISTRIBUTION=managed npm --prefix ui run tauri -- build --ci --target "${{ matrix.target }}" --bundles deb --config src-tauri/tauri.managed.conf.json
deb="$(find "$bundle/deb" -maxdepth 1 -type f -name '*.deb' -print -quit)"
test -n "$deb"
poetry run python -m scripts.repack_deb "$deb" --version "$NEBULA_VERSION"
install -m 0644 "$deb" "$stage/Nebula-$NEBULA_VERSION-linux-x86_64.deb"
- name: Inspect and self-test Linux installers
run: |
deb="$RUNNER_TEMP/release-stage/Nebula-$NEBULA_VERSION-linux-x86_64.deb"
appimage="$RUNNER_TEMP/release-stage/Nebula-$NEBULA_VERSION-linux-x86_64.AppImage"
dpkg-deb --info "$deb"
expected_deb_version="$NEBULA_VERSION"
case "$expected_deb_version" in *-*) expected_deb_version="${expected_deb_version/-/\~}" ;; esac
test "$(dpkg-deb --field "$deb" Version)" = "$expected_deb_version"
dpkg --compare-versions "$expected_deb_version" le "$NEBULA_VERSION"
dpkg-deb --extract "$deb" "$RUNNER_TEMP/deb-root"
test -x "$RUNNER_TEMP/deb-root/usr/bin/nebula"
test -x "$RUNNER_TEMP/deb-root/usr/bin/nebula-core"
test -x "$RUNNER_TEMP/deb-root/usr/bin/nebula-ui"
cmp packaging/linux/nebula "$RUNNER_TEMP/deb-root/usr/bin/nebula"
poetry run python scripts/package_audit.py --tree "$RUNNER_TEMP/deb-root"
xvfb-run -a "$RUNNER_TEMP/deb-root/usr/bin/nebula-ui" --self-test
xvfb-run -a poetry run python scripts/test_desktop_crash_cleanup.py "$RUNNER_TEMP/deb-root/usr/bin/nebula-ui"
appimage_extract="$RUNNER_TEMP/appimage-extract"
install -d -m 0755 "$appimage_extract"
(cd "$appimage_extract" && "$appimage" --appimage-extract >/dev/null)
poetry run python scripts/package_audit.py --tree "$appimage_extract/squashfs-root"
xvfb-run -a "$appimage" --self-test
cargo run --locked --quiet --manifest-path ui/src-tauri/Cargo.toml \
--example verify_update_signature -- \
"$appimage" "$appimage.sig" "$NEBULA_UPDATER_PUBLIC_KEY"
tampered="$RUNNER_TEMP/tampered.AppImage"
cp "$appimage" "$tampered"
printf 'tampered' >> "$tampered"
if cargo run --locked --quiet --manifest-path ui/src-tauri/Cargo.toml \
--example verify_update_signature -- \
"$tampered" "$appimage.sig" "$NEBULA_UPDATER_PUBLIC_KEY"; then
printf 'tampered updater unexpectedly passed signature verification\n' >&2
exit 1
fi
- name: Generate per-artifact SBOMs and checksums
run: |
stage="$RUNNER_TEMP/release-stage"
for artifact in "$stage"/Nebula-*; do
case "$artifact" in
*.sig|*.json) continue ;;
esac
direct=
case "$artifact" in
*-direct.dmg|*.AppImage|*.updater.tar.gz) direct=--direct ;;
esac
poetry run python -m scripts.generate_release_sbom \
--artifact "$artifact" \
--root "$GITHUB_WORKSPACE" \
--target "${{ matrix.target }}" \
$direct
done
(
cd "$stage"
shasum -a 256 Nebula-* > "SHA256SUMS-${{ matrix.id }}.txt"
)
- name: Attest release artifacts
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
with:
subject-path: ${{ runner.temp }}/release-stage/*
- name: Upload native release outputs
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: nebula3-${{ matrix.id }}
path: ${{ runner.temp }}/release-stage/*
if-no-files-found: error
linux-clean-smoke:
needs: [validate, build]
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
include:
- name: ubuntu-24.04
image: ubuntu:24.04
- name: debian-12
image: debian:12-slim
- name: kali-current
image: kalilinux/kali-rolling:latest
container: ${{ matrix.image }}
steps:
- name: Download the native Linux installer
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: nebula3-linux-x64
path: release-assets
- name: Install, self-test, diagnose, and uninstall without a development stack
env:
VERSION: ${{ needs.validate.outputs.version }}
run: |
for command in node npm cargo rustc poetry; do
if command -v "$command" >/dev/null 2>&1; then
printf 'clean image unexpectedly contains %s\n' "$command" >&2
exit 1
fi
done
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y xauth xvfb "./release-assets/Nebula-$VERSION-linux-x86_64.deb"
xvfb-run -a /usr/bin/nebula --self-test
browser="$(find /usr -path '*/playwright-browsers/*' -type f \( -name chrome -o -name headless_shell -o -name chrome-headless-shell \) -perm /111 -print -quit)"
test -n "$browser"
"$browser" --headless --no-sandbox --disable-gpu --dump-dom about:blank | grep -F '<html'
/usr/bin/nebula-core doctor --data-dir /tmp/nebula-clean-doctor --json > /tmp/doctor.json
grep -F '"mode": "analysis-only"' /tmp/doctor.json
grep -F '"host_fallback": false' /tmp/doctor.json
DEBIAN_FRONTEND=noninteractive apt-get remove -y nebula
test ! -e /usr/bin/nebula
test ! -e /usr/bin/nebula-ui
test ! -e /usr/bin/nebula-core