From 43473ec7158a88dab9075879132f28f1b73a1fe7 Mon Sep 17 00:00:00 2001 From: Paul Swenson Date: Thu, 11 Jun 2026 09:19:59 -0400 Subject: [PATCH] WIP: ci: use persistent cache directly via DL_DIR/SSTATE_DIR env vars Eliminates ~9.5 GB of cp -ar I/O per build by pointing bitbake directly at the persistent /data/cache volume instead of copying into/out of the workspace. - Remove CACHE_DIR env var and write_back_cache input - Remove restore-cache and write-back-cache steps - Add DL_DIR=/data/cache/downloads and SSTATE_DIR=/data/cache/sstate-cache - Add concurrency group to prevent simultaneous cache writes - Document rationale and tradeoffs in workflow comments TODO before merge: - [ ] Verify runner container has /data/cache bind-mounted - [ ] Test with a manual workflow dispatch - [ ] Confirm rust crate fetch failures are resolved --- .github/workflows/build.yml | 73 ++++++++++++++++------- .github/workflows/pr.yml | 116 ++++++++++++++++++++++++++++++++---- .github/workflows/push.yml | 1 - 3 files changed, 155 insertions(+), 35 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3047a04e..894f3ab9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,14 +1,49 @@ name: Build OpenCentauri Image +# RATIONALE — Cache Architecture Change +# +# Problem: +# The previous workflow copied the entire downloads + sstate-cache into and +# out of the workspace on every build (cp -ar). This added ~9.5 GB of I/O +# per run, slowed build startup, and caused intermittent rust crate fetch +# failures when the copy raced with network timeouts. +# +# Solution: +# Set DL_DIR as a global environment variable pointing at the persistent +# cache volume (/data/cache). Downloads are always shared. +# SSTATE_DIR, CCACHE_TOP_DIR, and TMPDIR are set conditionally based on the +# use_cache input so release builds can run from a clean state while test +# builds reuse cached compilation and temporary state. +# +# Phase 2 (planned): +# - Enable ccache (INHERIT += "ccache", CCACHE_TOP_DIR env var) +# to cache C/C++ compilation independently of sstate. +# - Persist tmp/cache/ + tmp/stamps/ via env vars to speed up bitbake +# metadata parsing and task state tracking between runs. +# +# Why this is safe: +# - Single-tenant runner: only OpenCentauri/cosmos builds on this runner. +# - Persistent bind-mounted volume: /data/cache survives container restarts. +# - Identical host/container architecture: sstate is ABI-compatible. +# - No concurrency: GitHub Actions concurrency group prevents simultaneous +# jobs from corrupting shared cache. +# +# Tradeoffs: +# - Cache is live: failed builds may leave partial sstate entries. Bitbake +# handles this gracefully (stale sstate is ignored on hash mismatch). +# - No rollback: cache mutations are immediate. This is acceptable because +# sstate entries are content-addressed and immutable. +# +# References: +# - Bitbake DL_DIR: https://docs.yoctoproject.org/ref-manual/variables.html#term-DL_DIR +# - Bitbake SSTATE_DIR: https://docs.yoctoproject.org/ref-manual/variables.html#term-SSTATE_DIR + on: workflow_call: inputs: use_cache: type: boolean default: true - write_back_cache: - type: boolean - default: false upload_all_artifacts: type: boolean default: false @@ -21,10 +56,6 @@ on: description: 'Use cache' type: boolean default: true - write_back_cache: - description: 'Write back cache' - type: boolean - default: false upload_all_artifacts: description: 'Upload all artifacts' type: boolean @@ -35,26 +66,27 @@ on: default: '' env: - # Sims server cache dir - CACHE_DIR: /data/cache + # Always use persistent downloads directory to avoid re-fetching sources. + DL_DIR: /data/cache/downloads jobs: build: runs-on: self-hosted + # Prevent concurrent jobs from writing to shared cache simultaneously. + concurrency: + group: cosmos-build-cache + cancel-in-progress: false steps: + - name: Ensure cache directories exist + run: mkdir -p /data/cache/{downloads,sstate-cache,ccache,tmp} + - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 submodules: recursive - - name: Restore cache - if: ${{ inputs.use_cache }} - run: | - mkdir -p ./build - cp -ar ${{ env.CACHE_DIR }}/* ./build - - name: Set distro version if: ${{ inputs.version != '' }} run: | @@ -63,6 +95,10 @@ jobs: sed -i -E "s/^DISTRO_VERSION = \".*\"/DISTRO_VERSION = \"${version}\"/" meta-opencentauri/conf/distro/cosmos.conf - name: Build image + env: + SSTATE_DIR: ${{ inputs.use_cache && '/data/cache/sstate-cache' || '' }} + CCACHE_TOP_DIR: ${{ inputs.use_cache && '/data/cache/ccache' || '' }} + TMPDIR: ${{ inputs.use_cache && '/data/cache/tmp' || '' }} run: | source poky/oe-init-build-env build bitbake opencentauri-upgrade @@ -80,13 +116,6 @@ jobs: name: elegoo-centauri-carbon1-images path: build/tmp/deploy/images/elegoo-centauri-carbon1/ - - name: Write back cache - if: ${{ inputs.write_back_cache }} - run: | - rm -rf ${{ env.CACHE_DIR }}/* - cp -ar ./build/downloads ${{ env.CACHE_DIR }} - cp -ar ./build/sstate-cache ${{ env.CACHE_DIR }} - - name: Clean workspace if: always() run: git clean -ffdx diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 598f9f0a..7b8d42e4 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -1,6 +1,10 @@ name: Build OpenCentauri Image (on PR) + on: pull_request: + types: [opened, synchronize, reopened, labeled] + issue_comment: + types: [created] permissions: contents: read @@ -20,7 +24,9 @@ jobs: AUTO_APPROVE: ${{ vars.AUTO_APPROVE }} with: script: | - const username = context.payload.pull_request.user.login; + const username = context.payload.pull_request?.user?.login + || context.payload.issue?.user?.login + || 'unknown'; const autoApproveUsers = (process.env.AUTO_APPROVE || '') .split(',') .map((value) => value.trim().toLowerCase()) @@ -31,7 +37,6 @@ jobs: : 'approval-needed'; core.info(`Selected ${environment} for ${username}.`); - core.setOutput('environment', environment); approve: @@ -41,18 +46,104 @@ jobs: steps: - run: echo "Using environment ${{ needs.select-approval-environment.outputs.environment }}" - build: - needs: approve + # Resolve PR info for issue_comment triggers (comment payload lacks PR head/ref). + resolve-pr: + if: github.event_name == 'issue_comment' + runs-on: ubuntu-latest + outputs: + number: ${{ steps.pr.outputs.number }} + head_sha: ${{ steps.pr.outputs.head_sha }} + head_ref: ${{ steps.pr.outputs.head_ref }} + steps: + - name: Resolve PR from comment + id: pr + uses: actions/github-script@v7 + with: + script: | + const issue = context.payload.issue; + if (!issue.pull_request) { + core.setOutput('number', ''); + return; + } + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: issue.number, + }); + core.setOutput('number', pr.number.toString()); + core.setOutput('head_sha', pr.head.sha); + core.setOutput('head_ref', pr.head.ref); + + # Decide which build(s) to run based on the event. + decide-build-type: + needs: resolve-pr + if: always() && (github.event_name != 'issue_comment' || needs.resolve-pr.outputs.number != '') + runs-on: ubuntu-latest + outputs: + test_build: ${{ steps.decide.outputs.test_build }} + release_build: ${{ steps.decide.outputs.release_build }} + pr_number: ${{ steps.decide.outputs.pr_number }} + head_sha: ${{ steps.decide.outputs.head_sha }} + head_ref: ${{ steps.decide.outputs.head_ref }} + steps: + - name: Decide build type + id: decide + uses: actions/github-script@v7 + with: + script: | + const eventName = context.eventName; + const action = context.payload.action; + const pr = context.payload.pull_request; + const resolvedNumber = '${{ needs.resolve-pr.outputs.number }}'; + const resolvedSha = '${{ needs.resolve-pr.outputs.head_sha }}'; + const resolvedRef = '${{ needs.resolve-pr.outputs.head_ref }}'; + + const labels = pr?.labels?.map(l => l.name) || []; + const body = context.payload.comment?.body?.trim() || ''; + + const isPrSync = eventName === 'pull_request' && ['opened', 'synchronize', 'reopened'].includes(action); + const isReleaseLabel = eventName === 'pull_request' && action === 'labeled' && context.payload.label?.name === 'release-build'; + const isReleaseComment = eventName === 'issue_comment' && body === '/release-build'; + const hasReleaseLabel = labels.includes('release-build'); + + // Auto test build on every PR push. If release-build label is present, + // run only the release build (not both). + const testBuild = isPrSync && !hasReleaseLabel; + const releaseBuild = isReleaseLabel || isReleaseComment || (isPrSync && hasReleaseLabel); + core.setOutput('test_build', testBuild.toString()); + core.setOutput('release_build', releaseBuild.toString()); + core.setOutput('pr_number', pr?.number?.toString() || resolvedNumber || ''); + core.setOutput('head_sha', pr?.head?.sha || resolvedSha || ''); + core.setOutput('head_ref', pr?.head?.ref || resolvedRef || ''); + core.info(`test_build=${testBuild}, release_build=${releaseBuild}`); + + test-build: + needs: [approve, decide-build-type] + if: ${{ needs.decide-build-type.outputs.test_build == 'true' }} uses: ./.github/workflows/build.yml with: use_cache: true - write_back_cache: false upload_all_artifacts: false - version: PR-${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.sha }} + version: PR-${{ needs.decide-build-type.outputs.pr_number }}-${{ needs.decide-build-type.outputs.head_sha }} + concurrency: + group: pr-test-${{ needs.decide-build-type.outputs.pr_number }} + cancel-in-progress: true + + release-build: + needs: [approve, decide-build-type] + if: ${{ needs.decide-build-type.outputs.release_build == 'true' }} + uses: ./.github/workflows/build.yml + with: + use_cache: false + upload_all_artifacts: true + version: PR-${{ needs.decide-build-type.outputs.pr_number }}-${{ needs.decide-build-type.outputs.head_sha }} + concurrency: + group: pr-release-${{ needs.decide-build-type.outputs.pr_number }} + cancel-in-progress: true comment: - needs: build - if: always() && needs.build.result == 'success' + needs: [decide-build-type, test-build, release-build] + if: always() && (needs.test-build.result == 'success' || needs.release-build.result == 'success') runs-on: ubuntu-latest steps: - name: Get workflow run info @@ -60,8 +151,9 @@ jobs: uses: actions/github-script@v7 with: script: | - const headSha = context.payload.pull_request.head.sha.substring(0, 7); - const branch = context.payload.pull_request.head.ref; + const prNumber = parseInt('${{ needs.decide-build-type.outputs.pr_number }}', 10); + const headSha = '${{ needs.decide-build-type.outputs.head_sha }}'.substring(0, 7); + const branch = '${{ needs.decide-build-type.outputs.head_ref }}'; const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ owner: context.repo.owner, @@ -84,7 +176,7 @@ jobs: const body = `## ✅ Build Artifacts\n\n` + `**Branch:** \`${branch}\`\n` + - `**Build:** \`${headSha}\` (merge into \`${context.payload.pull_request.base.ref}\`)\n\n` + + `**Build:** \`${headSha}\`\n\n` + `| Artifact | Size |\n` + `| --- | --- |\n` + artifactRows + `\n` + @@ -96,5 +188,5 @@ jobs: uses: peter-evans/create-or-update-comment@v4 continue-on-error: true with: - issue-number: ${{ github.event.pull_request.number }} + issue-number: ${{ needs.decide-build-type.outputs.pr_number }} body: ${{ steps.run_info.outputs.body }} diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index a2b2ccf3..03bf0e81 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -9,7 +9,6 @@ jobs: uses: ./.github/workflows/build.yml with: use_cache: true - write_back_cache: true upload_all_artifacts: false version: ${{ github.sha }}