Repository navigation
Prepare Nebula 3 Linux release #41
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: 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 |