nix-build-images #14
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
| # Required GitHub secrets: | |
| # GCP_WORKLOAD_IDENTITY_PROVIDER — WIF provider resource name | |
| # e.g. projects/123456/locations/global/workloadIdentityPools/github-pool/providers/github-provider | |
| # GCP_SERVICE_ACCOUNT — sourceos-ci@socioprophet-platform.iam.gserviceaccount.com | |
| # KATELLO_URL — https://<sourceos-katello-external-ip> | |
| # KATELLO_ADMIN_PASSWORD — Foreman admin password | |
| # | |
| # GCS bucket: sourceos-artifacts-socioprophet (us-central1) | |
| # Run infra/gcp/setup-gcs-bucket.sh and infra/gcp/setup-wif.sh once before using this workflow. | |
| name: nix-build-images | |
| on: | |
| schedule: | |
| - cron: '0 3 * * *' # 03:00 UTC daily | |
| workflow_dispatch: | |
| inputs: | |
| target: | |
| description: 'Which config to build' | |
| required: false | |
| default: 'all' | |
| type: choice | |
| options: [all, canary-x86_64, stable-x86_64, exit-x86_64, builder-aarch64] | |
| promote_to: | |
| description: 'Promote after build' | |
| required: false | |
| default: 'dev' | |
| type: choice | |
| options: [dev, candidate, stable] | |
| push: | |
| branches: [main] | |
| paths: | |
| - 'flake.nix' | |
| - 'flake.lock' | |
| - 'hosts/**' | |
| - 'images/**' | |
| - 'profiles/**' | |
| - 'modules/**' | |
| - 'packages/**' | |
| - '.github/workflows/nix-build-images.yml' | |
| env: | |
| GCS_BUCKET: sourceos-artifacts-socioprophet | |
| GCP_PROJECT: socioprophet-platform | |
| jobs: | |
| build-x86_64: | |
| name: build ${{ matrix.host }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 120 | |
| permissions: | |
| contents: read | |
| id-token: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - host: canary-x86_64 | |
| image_pkg: sourceos-image-qcow2-canary | |
| build_image: true | |
| - host: stable-x86_64 | |
| image_pkg: sourceos-image-qcow2-stable | |
| build_image: true | |
| - host: exit-x86_64 | |
| image_pkg: "" | |
| build_image: false | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: DeterminateSystems/nix-installer-action@v14 | |
| - uses: DeterminateSystems/magic-nix-cache-action@v8 | |
| - name: Free disk space | |
| run: | | |
| sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android | |
| df -h | |
| - name: Build ${{ matrix.host }} system toplevel | |
| run: | | |
| nix build \ | |
| .#nixosConfigurations.${{ matrix.host }}.config.system.build.toplevel \ | |
| --print-build-logs --show-trace \ | |
| 2>&1 | tee /tmp/build-toplevel-${{ matrix.host }}.log | |
| echo "toplevel_path=$(readlink -f result)" >> "$GITHUB_OUTPUT" | |
| id: toplevel | |
| - name: Build ${{ matrix.host }} qcow2 image | |
| if: matrix.build_image == true | |
| run: | | |
| nix build \ | |
| .#packages.x86_64-linux.${{ matrix.image_pkg }} \ | |
| --print-build-logs --show-trace \ | |
| 2>&1 | tee /tmp/build-image-${{ matrix.host }}.log | |
| IMAGE_PATH="$(readlink -f result)" | |
| # nixos-generators qcow output is a directory containing the .qcow2 | |
| QCOW=$(find "$IMAGE_PATH" -name "*.qcow2" | head -1) | |
| echo "image_path=$IMAGE_PATH" >> "$GITHUB_OUTPUT" | |
| echo "qcow_path=$QCOW" >> "$GITHUB_OUTPUT" | |
| id: image | |
| - name: Authenticate to GCP | |
| if: matrix.build_image == true | |
| uses: google-github-actions/auth@v2 | |
| with: | |
| workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} | |
| service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} | |
| - name: Upload qcow2 to GCS | |
| if: matrix.build_image == true && steps.image.outputs.qcow_path != '' | |
| uses: google-github-actions/upload-cloud-storage@v2 | |
| with: | |
| path: ${{ steps.image.outputs.qcow_path }} | |
| destination: ${{ env.GCS_BUCKET }}/images/x86_64/${{ matrix.host }}/${{ github.sha }}.qcow2 | |
| process_gcloudignore: false | |
| - name: Upload build logs | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: build-logs-${{ matrix.host }} | |
| path: /tmp/build-*.log | |
| retention-days: 7 | |
| - name: Summarize | |
| run: | | |
| echo "## ${{ matrix.host }}" >> "$GITHUB_STEP_SUMMARY" | |
| echo "toplevel: \`${{ steps.toplevel.outputs.toplevel_path }}\`" >> "$GITHUB_STEP_SUMMARY" | |
| if [[ "${{ matrix.build_image }}" == "true" ]]; then | |
| echo "qcow2: \`gs://${{ env.GCS_BUCKET }}/images/x86_64/${{ matrix.host }}/${{ github.sha }}.qcow2\`" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| build-aarch64: | |
| name: build builder-aarch64 | |
| runs-on: [self-hosted, aarch64-linux, Linux] | |
| timeout-minutes: 120 | |
| permissions: | |
| contents: read | |
| id-token: write | |
| if: > | |
| github.event_name == 'push' || | |
| (github.event_name == 'workflow_dispatch' && | |
| (github.event.inputs.target == 'all' || github.event.inputs.target == 'builder-aarch64')) | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install Nix if not present | |
| run: | | |
| NIX_BIN=/nix/var/nix/profiles/default/bin/nix | |
| if ! "$NIX_BIN" --version &>/dev/null 2>&1; then | |
| export HOME=/root | |
| # Remove partial nixbld group/users from any previous failed install | |
| for n in $(seq 1 32); do userdel "nixbld${n}" 2>/dev/null || true; done | |
| groupdel nixbld 2>/dev/null || true | |
| curl -fsSL https://nixos.org/nix/install | sudo sh -s -- --daemon --yes | |
| sudo systemctl start nix-daemon.service || true | |
| sleep 5 | |
| fi | |
| echo "extra-experimental-features = nix-command flakes" | sudo tee -a /etc/nix/nix.conf | |
| sudo systemctl restart nix-daemon.service || true | |
| . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh || true | |
| echo "/nix/var/nix/profiles/default/bin" >> "$GITHUB_PATH" | |
| "$NIX_BIN" --version | |
| - name: Build builder-aarch64 system toplevel | |
| run: | | |
| nix build \ | |
| .#nixosConfigurations.builder-aarch64.config.system.build.toplevel \ | |
| --impure --print-build-logs --show-trace \ | |
| 2>&1 | tee /tmp/build-builder-aarch64.log | |
| STORE_PATH="$(readlink -f result)" | |
| echo "store_path=$STORE_PATH" >> "$GITHUB_OUTPUT" | |
| id: build | |
| - name: Export NixOS closure as NAR | |
| run: | | |
| STORE_PATH="${{ steps.build.outputs.store_path }}" | |
| NAR_FILE="/tmp/builder-aarch64-${{ github.sha }}.nar.zst" | |
| nix-store --export $(nix-store -qR "$STORE_PATH") | zstd -T0 -o "$NAR_FILE" | |
| echo "nar_file=$NAR_FILE" >> "$GITHUB_OUTPUT" | |
| id: nar | |
| - name: Authenticate to GCP | |
| uses: google-github-actions/auth@v2 | |
| with: | |
| workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} | |
| service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} | |
| - name: Upload NAR to GCS | |
| uses: google-github-actions/upload-cloud-storage@v2 | |
| with: | |
| path: ${{ steps.nar.outputs.nar_file }} | |
| destination: ${{ env.GCS_BUCKET }}/closures/aarch64/builder-aarch64/${{ github.sha }}.nar.zst | |
| process_gcloudignore: false | |
| - name: Upload build log | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: build-log-builder-aarch64 | |
| path: /tmp/build-builder-aarch64.log | |
| retention-days: 7 | |
| publish-katello: | |
| name: publish to Katello | |
| runs-on: ubuntu-latest | |
| needs: [build-x86_64, build-aarch64] | |
| if: always() && (needs.build-x86_64.result == 'success' || needs.build-aarch64.result == 'success') | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Determine promote target | |
| id: promote | |
| run: | | |
| TARGET="${{ github.event.inputs.promote_to || 'dev' }}" | |
| echo "target=$TARGET" >> "$GITHUB_OUTPUT" | |
| - name: Publish and promote content views via Katello API | |
| env: | |
| KATELLO_URL: ${{ secrets.KATELLO_URL }} | |
| KATELLO_ADMIN_PASSWORD: ${{ secrets.KATELLO_ADMIN_PASSWORD }} | |
| GIT_SHA: ${{ github.sha }} | |
| PROMOTE_TO: ${{ steps.promote.outputs.target }} | |
| run: | | |
| SHORT="${GIT_SHA:0:8}" | |
| BUILT_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)" | |
| CURL="curl -sf -u admin:${KATELLO_ADMIN_PASSWORD} --insecure -H Content-Type:application/json" | |
| # Resolve org ID once (organisation name "SocioProphet" → numeric id) | |
| ORG_ID=$($CURL "${KATELLO_URL}/katello/api/organizations" \ | |
| | python3 -c "import json,sys; rs=json.load(sys.stdin)['results']; print(next((r['id'] for r in rs if r['name']=='SocioProphet'), 1))") | |
| echo "Org ID: $ORG_ID" | |
| # Resolve target lifecycle environment ID | |
| ENV_ID=$($CURL "${KATELLO_URL}/katello/api/organizations/${ORG_ID}/environments" \ | |
| | python3 -c "import json,sys; rs=json.load(sys.stdin)['results']; print(next((r['id'] for r in rs if r['name']=='${PROMOTE_TO}'), ''))") | |
| echo "Promote-to env '${PROMOTE_TO}' id: $ENV_ID" | |
| wait_task() { | |
| local TASK_ID="$1" | |
| for i in $(seq 1 60); do | |
| STATE=$($CURL "${KATELLO_URL}/foreman_tasks/api/tasks/${TASK_ID}" \ | |
| | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('state','?'),d.get('result','?'))") | |
| echo " task $TASK_ID: $STATE" | |
| case "$STATE" in | |
| "stopped success") return 0 ;; | |
| "stopped "*) echo " TASK FAILED"; return 1 ;; | |
| esac | |
| sleep 10 | |
| done | |
| echo " TASK TIMED OUT"; return 1 | |
| } | |
| publish_and_promote() { | |
| local CV_NAME="$1" | |
| # Look up CV ID by name | |
| CV_ID=$($CURL "${KATELLO_URL}/katello/api/organizations/${ORG_ID}/content_views?search=name%3D${CV_NAME}" \ | |
| | python3 -c "import json,sys; vs=json.load(sys.stdin)['results']; print(vs[0]['id'] if vs else '')") | |
| [[ -z "$CV_ID" ]] && { echo "CV $CV_NAME not found — skipping"; return 0; } | |
| # Publish new version | |
| echo "Publishing $CV_NAME (id=$CV_ID)..." | |
| PUB_TASK=$($CURL -X POST "${KATELLO_URL}/katello/api/content_views/${CV_ID}/publish" \ | |
| -d "{\"description\":\"Built from ${SHORT} at ${BUILT_AT}\"}" \ | |
| | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))") | |
| [[ -z "$PUB_TASK" ]] && { echo "Publish task missing id"; return 1; } | |
| wait_task "$PUB_TASK" || return 1 | |
| # Get the latest version id (highest id = just published) | |
| VER_ID=$($CURL "${KATELLO_URL}/katello/api/content_view_versions?content_view_id=${CV_ID}" \ | |
| | python3 -c "import json,sys; vs=json.load(sys.stdin)['results']; print(sorted(vs,key=lambda v:v['id'],reverse=True)[0]['id'] if vs else '')") | |
| [[ -z "$VER_ID" ]] && { echo "No version found after publish"; return 1; } | |
| echo " Latest version id=$VER_ID" | |
| # Promote to target environment | |
| if [[ -n "$ENV_ID" ]]; then | |
| echo " Promoting version $VER_ID to ${PROMOTE_TO} (env $ENV_ID)..." | |
| PROMO_TASK=$($CURL -X POST "${KATELLO_URL}/katello/api/content_view_versions/${VER_ID}/promote" \ | |
| -d "{\"environment_ids\":[${ENV_ID}]}" \ | |
| | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))") | |
| [[ -n "$PROMO_TASK" ]] && wait_task "$PROMO_TASK" || echo " Promote task missing id — skipping" | |
| fi | |
| echo "Done: $CV_NAME" | |
| } | |
| publish_and_promote "sourceos-builder-aarch64" | |
| publish_and_promote "sourceos-x86_64" | |
| emit-gitrefbuild: | |
| name: emit GitRefBuild record | |
| runs-on: ubuntu-latest | |
| needs: [publish-katello] | |
| if: always() && needs.publish-katello.result == 'success' | |
| permissions: | |
| contents: read | |
| id-token: write | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Authenticate to GCP | |
| uses: google-github-actions/auth@v2 | |
| with: | |
| workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} | |
| service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} | |
| - name: Emit GitRefBuild JSON | |
| run: | | |
| RECORD_ID="urn:srcos:git-ref-build:$(date -u +%Y%m%d)-${{ github.sha }}" | |
| BUILT_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)" | |
| python3 - <<EOF | |
| import json | |
| record = { | |
| "id": "${RECORD_ID}", | |
| "type": "GitRefBuild", | |
| "specVersion": "2.0.0", | |
| "repoUri": "https://github.com/SourceOS-Linux/source-os", | |
| "gitRef": "${{ github.ref }}", | |
| "gitCommit": "${{ github.sha }}", | |
| "status": "succeeded", | |
| "triggeredBy": "github-actions", | |
| "triggeredAt": "${BUILT_AT}", | |
| "completedAt": "${BUILT_AT}", | |
| "outputs": [ | |
| { | |
| "outputKind": "qcow2", | |
| "artifactRef": "gs://sourceos-artifacts-socioprophet/images/x86_64/canary-x86_64/${{ github.sha }}.qcow2" | |
| }, | |
| { | |
| "outputKind": "qcow2", | |
| "artifactRef": "gs://sourceos-artifacts-socioprophet/images/x86_64/stable-x86_64/${{ github.sha }}.qcow2" | |
| }, | |
| { | |
| "outputKind": "bundle", | |
| "artifactRef": "gs://sourceos-artifacts-socioprophet/closures/aarch64/builder-aarch64/${{ github.sha }}.nar.zst" | |
| } | |
| ] | |
| } | |
| with open("/tmp/gitrefbuild.json", "w") as f: | |
| json.dump(record, f, indent=2) | |
| print(json.dumps(record, indent=2)) | |
| EOF | |
| - name: Upload GitRefBuild record to GCS | |
| uses: google-github-actions/upload-cloud-storage@v2 | |
| with: | |
| path: /tmp/gitrefbuild.json | |
| destination: ${{ env.GCS_BUCKET }}/records/git-ref-builds/${{ github.sha }}.json | |
| process_gcloudignore: false |