Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 76 additions & 75 deletions .github/workflows/nixos-pr-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,17 @@ name: Build PiFinder NixOS (testable PRs)
# the real ATTIC_TOKEN and a read-write GITHUB_TOKEN even for fork PRs. The
# contributor's code is checked out explicitly (head SHA) and built. This is
# only reached after a maintainer applies the `testable` (or `preview`) label —
# that label is the security boundary: it runs contributor code on the
# self-hosted aarch64 runner with the cache push token, so review the diff
# before labeling, and re-review on each new push to a labeled PR.
# that label is the security boundary: it runs contributor code with the cache
# push token, so review the diff before labeling, and re-review on each new push
# to a labeled PR.
#
# Build strategy: the free GitHub-hosted ubuntu-24.04-arm runner builds first.
# Its cores are faster than the Pi5, and the Attic cache (cache.pifinder.eu) is
# a substituter, so anything already built — including the patched kernel once
# it has been built once — is downloaded, not recompiled. Each successful build
# is pushed back to Attic, so the first build after a kernel/source change is
# the only slow one; every build after it is fast. The self-hosted Pi5 is a
# last-resort fallback that only runs if the hosted build fails.
on:
pull_request_target:
types: [labeled, synchronize, opened]
Expand All @@ -23,15 +31,16 @@ permissions:
actions: read

jobs:
# Try the Pi5 native aarch64 runner first (fast).
build-native:
# Primary: free GitHub-hosted arm64 runner (native aarch64, no QEMU).
build-hosted:
if: |
contains(github.event.pull_request.labels.*.name, 'preview') ||
contains(github.event.pull_request.labels.*.name, 'testable')
runs-on: [self-hosted, aarch64]
timeout-minutes: 30
runs-on: ubuntu-24.04-arm
# Generous: only a kernel/source change compiles from scratch (~1 h on this
# 4-core runner); everything else substitutes from Attic in minutes.
timeout-minutes: 150
outputs:
success: ${{ steps.build.outcome == 'success' }}
store_path: ${{ steps.push.outputs.store_path }}
steps:
# Build the contributor's code. persist-credentials:false so the
Expand All @@ -41,71 +50,67 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
persist-credentials: false
# Fork PRs: pull_request_target checkout of the head requires opt-in
# since actions/checkout began refusing it. The head SHA is pinned
# explicitly and only reached after the `testable`/`preview` label,
# which is the review gate for running contributor code.
allow-unsafe-pr-checkout: true

- name: Ensure nix is on PATH (self-hosted runner)
run: |
echo "/nix/var/nix/profiles/default/bin" >> "$GITHUB_PATH"
echo "$HOME/.nix-profile/bin" >> "$GITHUB_PATH"
- uses: DeterminateSystems/nix-installer-action@main
with:
determinate: false
extra-conf: |
extra-system-features = big-parallel
extra-substituters = https://cache.pifinder.eu/pifinder
extra-trusted-public-keys = pifinder:8UU/O3oLkaJHHUyqEcPGl+9F1m4MqDca39Ewl49jBmE=

- name: Setup Attic substituter (cache.pifinder.eu)
- name: Attic login for push
env:
ATTIC_TOKEN: ${{ secrets.ATTIC_TOKEN }}
run: |
if [ -z "$ATTIC_TOKEN" ]; then
echo "No ATTIC_TOKEN — pull-only via the public substituter"
exit 0
fi
nix profile install nixpkgs#attic-client
attic login pifinder https://cache.pifinder.eu "$ATTIC_TOKEN"
attic use pifinder:pifinder

- name: Build NixOS system closure
id: build
- name: Build NixOS system and on-device dev shell closures
run: |
nix build .#nixosConfigurations.pifinder.config.system.build.toplevel \
-L --no-link
nix build .#devShells.aarch64-linux.default -L --no-link

- name: Push to Attic
id: push
env:
ATTIC_TOKEN: ${{ secrets.ATTIC_TOKEN }}
run: |
STORE_PATH=$(nix build .#nixosConfigurations.pifinder.config.system.build.toplevel \
--json | jq -r '.[].outputs.out')
attic push pifinder:pifinder "$STORE_PATH"
DEV_SHELL_PATH=$(nix build .#devShells.aarch64-linux.default \
--json | jq -r '.[].outputs.out')
echo "store_path=$STORE_PATH" >> "$GITHUB_OUTPUT"
if [ -n "$ATTIC_TOKEN" ]; then
attic push pifinder:pifinder "$STORE_PATH"
attic push pifinder:pifinder "$DEV_SHELL_PATH"
else
echo "No ATTIC_TOKEN — skipping push; build is verify-only"
fi

# Wait up to ~15 min for the native builder, then decide on the hosted fallback.
native-wait:
# Last-resort fallback: self-hosted Pi5. Only runs if the hosted build failed
# (e.g. hosted-runner outage or capacity). The Pi5 is slow for a from-scratch
# kernel, hence the longer timeout.
build-pi5:
needs: build-hosted
if: |
contains(github.event.pull_request.labels.*.name, 'preview') ||
contains(github.event.pull_request.labels.*.name, 'testable')
runs-on: ubuntu-latest
timeout-minutes: 20
outputs:
need_emulated: ${{ steps.wait.outputs.need_emulated }}
steps:
- name: Wait for native build
id: wait
env:
GH_TOKEN: ${{ github.token }}
run: |
for i in $(seq 1 30); do
sleep 30
RESULT=$(gh api "repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/jobs" \
--jq '.jobs[] | select(.name == "build-native") | .conclusion // "pending"' 2>/dev/null || echo "pending")
echo "Check $i/30: build-native=$RESULT"
if [ "$RESULT" = "success" ]; then
echo "need_emulated=false" >> "$GITHUB_OUTPUT"
exit 0
elif [ "$RESULT" = "failure" ] || [ "$RESULT" = "cancelled" ]; then
echo "need_emulated=true" >> "$GITHUB_OUTPUT"
exit 0
fi
done
echo "Native build not done after 15 min, falling back to emulated"
echo "need_emulated=true" >> "$GITHUB_OUTPUT"

# Fallback on a free hosted arm64 runner (native aarch64, no QEMU).
build-emulated:
needs: native-wait
if: needs.native-wait.outputs.need_emulated == 'true'
runs-on: ubuntu-24.04-arm
timeout-minutes: 60
always() &&
(contains(github.event.pull_request.labels.*.name, 'preview') ||
contains(github.event.pull_request.labels.*.name, 'testable')) &&
needs.build-hosted.result == 'failure'
runs-on: [self-hosted, aarch64]
timeout-minutes: 240
outputs:
store_path: ${{ steps.push.outputs.store_path }}
steps:
Expand All @@ -114,54 +119,50 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
persist-credentials: false
# Fork PRs: pull_request_target checkout of the head requires opt-in
# since actions/checkout began refusing it. The head SHA is pinned
# explicitly and only reached after the `testable`/`preview` label,
# which is the review gate for running contributor code.
allow-unsafe-pr-checkout: true

- uses: DeterminateSystems/nix-installer-action@main
with:
determinate: false
extra-conf: |
extra-system-features = big-parallel
extra-substituters = https://cache.pifinder.eu/pifinder
extra-trusted-public-keys = pifinder:VkemNaMqXDcsYlpONItSvOOcBIa1vEfnpyqdetr3gck=
- name: Ensure nix is on PATH (self-hosted runner)
run: |
echo "/nix/var/nix/profiles/default/bin" >> "$GITHUB_PATH"
echo "$HOME/.nix-profile/bin" >> "$GITHUB_PATH"

- name: Attic login for push
- name: Setup Attic substituter (cache.pifinder.eu)
env:
ATTIC_TOKEN: ${{ secrets.ATTIC_TOKEN }}
run: |
if [ -z "$ATTIC_TOKEN" ]; then
echo "No ATTIC_TOKEN — pull-only via the public substituter"
exit 0
fi
nix profile install nixpkgs#attic-client
attic login pifinder https://cache.pifinder.eu "$ATTIC_TOKEN"
attic use pifinder:pifinder

- name: Build NixOS system closure
- name: Build NixOS system and on-device dev shell closures
run: |
nix build .#nixosConfigurations.pifinder.config.system.build.toplevel \
-L --no-link
nix build .#devShells.aarch64-linux.default -L --no-link

- name: Push to Attic
id: push
env:
ATTIC_TOKEN: ${{ secrets.ATTIC_TOKEN }}
run: |
STORE_PATH=$(nix build .#nixosConfigurations.pifinder.config.system.build.toplevel \
--json | jq -r '.[].outputs.out')
DEV_SHELL_PATH=$(nix build .#devShells.aarch64-linux.default \
--json | jq -r '.[].outputs.out')
attic push pifinder:pifinder "$STORE_PATH"
attic push pifinder:pifinder "$DEV_SHELL_PATH"
echo "store_path=$STORE_PATH" >> "$GITHUB_OUTPUT"
if [ -n "$ATTIC_TOKEN" ]; then
attic push pifinder:pifinder "$STORE_PATH"
else
echo "No ATTIC_TOKEN — skipping push; build is verify-only"
fi

# Stamp the PR's build into the metadata-only nixos-manifest branch. Runs the
# TRUSTED scripts from the base branch (default checkout), never the fork's,
# since this step holds the write token.
update-manifest:
needs: [build-native, build-emulated]
needs: [build-hosted, build-pi5]
if: |
always() &&
(needs.build-native.result == 'success' || needs.build-emulated.result == 'success')
(needs.build-hosted.result == 'success' || needs.build-pi5.result == 'success')
runs-on: ubuntu-latest
permissions:
contents: write
Expand All @@ -173,7 +174,7 @@ jobs:

- name: Update generated manifest branch
env:
STORE_PATH: ${{ needs.build-native.outputs.store_path || needs.build-emulated.outputs.store_path }}
STORE_PATH: ${{ needs.build-hosted.outputs.store_path || needs.build-pi5.outputs.store_path }}
GH_REPOSITORY: ${{ github.repository }}
run: |
set -euo pipefail
Expand Down
Loading