name: Build on: push: paths: - ".github/workflows/build.yml" - "app/**" pull_request: paths: - ".github/workflows/build.yml" - "app/**" schedule: - cron: '22 4 * * *' jobs: build: if: ${{ always() }} runs-on: ubuntu-latest container: image: docker.io/zmkfirmware/zmk-build-arm:2.5 needs: compile-matrix strategy: matrix: include: ${{ fromJSON(needs.compile-matrix.outputs.include-list) }} steps: - name: Checkout uses: actions/checkout@v2 - name: Cache west modules uses: actions/cache@v2 env: cache-name: cache-zephyr-modules with: path: | modules/ tools/ zephyr/ bootloader/ key: 4-${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('app/west.yml') }} restore-keys: | 4-${{ runner.os }}-build-${{ env.cache-name }}- 4-${{ runner.os }}-build- 4-${{ runner.os }}- timeout-minutes: 2 continue-on-error: true - name: Initialize workspace (west init) run: west init -l app - name: Update modules (west update) run: west update - name: Export Zephyr CMake package (west zephyr-export) run: west zephyr-export - name: Use Node.js uses: actions/setup-node@v2 with: node-version: '14.x' - name: Install @actions/artifact run: npm install @actions/artifact - name: Build and upload artifacts uses: actions/github-script@v4 id: boards-list with: script: | const fs = require('fs'); const artifact = require('@actions/artifact'); const artifactClient = artifact.create(); const execSync = require('child_process').execSync; const buildShieldArgs = JSON.parse(`${{ matrix.shieldArgs }}`); let error = false; for (const shieldArgs of buildShieldArgs) { try { const output = execSync(`west build -s app -p -b ${{ matrix.board }} -- ${shieldArgs.shield ? '-DSHIELD=' + shieldArgs.shield : ''} ${shieldArgs['cmake-args'] || ''}`); console.log(output.toString()); const fileExtensions = ['hex', 'uf2']; let files = []; for (const extension of fileExtensions) { const path = 'build/zephyr/zmk.' + extension; if (fs.existsSync(path)) { files.push(path); } } const rootDirectory = 'build/zephyr'; const options = { continueOnError: true } const artifactName = `${{ matrix.board }}${shieldArgs.shield ? '-' + shieldArgs.shield : ''}-zmk`; await artifactClient.uploadArtifact(artifactName, files, rootDirectory, options); } catch (e) { console.error(`Failed to build or upload ${{ matrix.board }} ${shieldArgs.shield} ${shieldArgs['cmake-args']}`); console.error(e); error = true; } } if (error) { throw new Error('Failed to build one or more configurations'); } compile-matrix: if: ${{ always() }} runs-on: ubuntu-latest needs: [core-coverage, board-changes, nightly] outputs: include-list: ${{ steps.compile-list.outputs.result }} steps: - name: Join build lists uses: actions/github-script@v4 id: compile-list with: script: | const coreCoverage = `${{ needs.core-coverage.outputs.core-include }}`; const coreCoverageArray = coreCoverage ? JSON.parse(coreCoverage) : []; const boardChanges = `${{ needs.board-changes.outputs.boards-include }}`; const boardChangesArray = boardChanges ? JSON.parse(boardChanges) : []; const nightly = `${{ needs.nightly.outputs.nightly-include }}`; const nightlyArray = nightly ? JSON.parse(nightly) : []; const combined = [...coreCoverageArray, ...boardChangesArray, ...nightlyArray]; const combinedUnique = [...new Map(combined.map(el => [JSON.stringify(el), el])).values()]; const perBoard = {}; for (const configuration of combinedUnique) { if (!perBoard[configuration.board]) perBoard[configuration.board] = []; perBoard[configuration.board].push({ shield: configuration.shield, 'cmake-args': configuration['cmake-args'] }) } const includeList = []; for (const [board, shieldArgs] of Object.entries(perBoard)) { includeList.push({ board, shieldArgs: JSON.stringify(shieldArgs) }); } return includeList; core-coverage: if: ${{ needs.get-changed-files.outputs.core-changes == 'true' }} runs-on: ubuntu-latest needs: get-changed-files outputs: core-include: ${{ steps.core-list.outputs.result }} steps: - uses: actions/github-script@v4 id: core-list with: script: | // break out to file const coreCoverage = { boards: [ 'nice_nano_v2', 'nrfmicro_13', 'proton_c' ], shields: [ 'corne_left', 'corne_right', 'romac', 'settings_reset', 'tidbit' ], include: [ { "board": "bdn9_rev2" }, { "board": "nice60" }, { "board": "nrf52840_m2", "shield": "m60" }, { "board": "planck_rev6" }, { "board": "proton_c", "shield": "clueboard_california" }, { "board": "nice_nano_v2", "shield": "kyria_left", "cmake-args": "-DCONFIG_ZMK_DISPLAY=y", }, { "board": "nice_nano_v2", "shield": "kyria_right", "cmake-args": "-DCONFIG_ZMK_DISPLAY=y", }, { "board": "nice_nano", "shield": "romac_plus", "cmake-args": "-DCONFIG_ZMK_RGB_UNDERGLOW=y -DCONFIG_WS2812_STRIP=y" } ] }; let include = []; coreCoverage.boards.forEach(b => { coreCoverage.shields.forEach(s => { include.push({ board: b, shield: s }); }); }); return [...include, ...coreCoverage.include]; board-changes: if: ${{ needs.get-changed-files.outputs.board-changes == 'true' }} runs-on: ubuntu-latest needs: [get-grouped-hardware, get-changed-files] outputs: boards-include: ${{ steps.boards-list.outputs.result }} steps: - name: Checkout uses: actions/checkout@v2 - name: Use Node.js uses: actions/setup-node@v2 with: node-version: '14.x' - name: Install js-yaml run: npm install js-yaml - uses: actions/github-script@v4 id: boards-list with: script: | const fs = require('fs'); const yaml = require('js-yaml'); const changedFiles = JSON.parse(`${{ needs.get-changed-files.outputs.changed-files }}`); const metadata = JSON.parse(`${{ needs.get-grouped-hardware.outputs.organized-metadata }}`); const boardChanges = new Set(changedFiles.filter(f => f.startsWith('app/boards')).map(f => f.split('/').slice(0, 4).join('/'))); include = []; for (const bc of boardChanges) { const globber = await glob.create(bc + "/*.zmk.yml"); const files = await globber.glob(); const aggregated = files.flatMap((f) => yaml.loadAll(fs.readFileSync(f, "utf8")) ); aggregated.forEach(hm => { switch (hm.type) { case "board": if (hm.features && hm.features.includes("keys")) { if (hm.siblings) { hm.siblings.forEach(sib => { include.push({ board: sib }); }); } else { include.push({ board: hm.id }); } } else if (hm.exposes) { hm.exposes.forEach(i => { metadata.interconnects[i].shields.forEach(s => { if (s.siblings) { s.siblings.forEach(sib => { include.push({ board: hm.id, shield: sib }); }); } else { include.push({ board: hm.id, shield: s.id }); } }); }); } else { console.error("Board without keys or interconnect"); } break; case "shield": if (hm.features && hm.features.includes("keys")) { hm.requires.forEach(i => { metadata.interconnects[i].boards.forEach(b => { if (hm.siblings) { hm.siblings.forEach(sib => { include.push({ board: b.id, shield: sib }); }); } else { include.push({ board: b.id, shield: hm.id }); } }); }); } break; case "interconnect": break; } }); } return include; nightly: if: ${{ github.event_name == 'schedule' }} runs-on: ubuntu-latest needs: get-grouped-hardware outputs: nightly-include: ${{ steps.nightly-list.outputs.result }} steps: - name: Create nightly list uses: actions/github-script@v4 id: nightly-list with: script: | const metadata = JSON.parse(`${{ needs.get-grouped-hardware.outputs.organized-metadata }}`); let include = []; metadata.onboard.forEach(b => { if (b.siblings) { b.siblings.forEach(sib => { include.push({ board: sib }); }); } else { include.push({ board: b.id }); } }); Object.values(metadata.interconnects).forEach(i => { i.boards.forEach(b => { i.shields.forEach(s => { if (s.siblings) { s.siblings.forEach(sib => { include.push({ board: b.id, shield: sib }); }); } else { include.push({ board: b.id, shield: s.id }); } }); }); }); return include; get-grouped-hardware: runs-on: ubuntu-latest outputs: organized-metadata: ${{ steps.organize-metadata.outputs.result }} steps: - name: Checkout uses: actions/checkout@v2 - name: Use Node.js uses: actions/setup-node@v2 with: node-version: '14.x' - name: Install js-yaml run: npm install js-yaml - name: Aggregate Metadata uses: actions/github-script@v4 id: aggregate-metadata with: script: | const fs = require('fs'); const yaml = require('js-yaml'); const globber = await glob.create("app/boards/**/*.zmk.yml"); const files = await globber.glob(); const aggregated = files.flatMap((f) => yaml.loadAll(fs.readFileSync(f, "utf8")) ); return JSON.stringify(aggregated).replace(/\\/g,"\\\\"); result-encoding: string - name: Organize Metadata uses: actions/github-script@v4 id: organize-metadata with: script: | const hardware = JSON.parse(`${{ steps.aggregate-metadata.outputs.result }}`); const grouped = hardware.reduce((agg, hm) => { switch (hm.type) { case "board": if (hm.features && hm.features.includes("keys")) { agg.onboard.push(hm); } else if (hm.exposes) { hm.exposes.forEach((element) => { let ic = agg.interconnects[element] || { boards: [], shields: [], }; ic.boards.push(hm); agg.interconnects[element] = ic; }); } else { console.error("Board without keys or interconnect"); } break; case "shield": if (hm.features && hm.features.includes("keys")) { hm.requires.forEach((id) => { let ic = agg.interconnects[id] || { boards: [], shields: [] }; ic.shields.push(hm); agg.interconnects[id] = ic; }); } break; case "interconnect": let ic = agg.interconnects[hm.id] || { boards: [], shields: [] }; ic.interconnect = hm; agg.interconnects[hm.id] = ic; break; } return agg; }, { onboard: [], interconnects: {} }); return JSON.stringify(grouped).replace(/\\/g,"\\\\"); result-encoding: string get-changed-files: if: ${{ github.event_name != 'schedule' }} runs-on: ubuntu-latest outputs: changed-files: ${{ steps.changed-files.outputs.all }} board-changes: ${{ steps.board-changes.outputs.result }} core-changes: ${{ steps.core-changes.outputs.result }} steps: - uses: Ana06/get-changed-files@v2.0.0 id: changed-files with: format: 'json' - uses: actions/github-script@v4 id: board-changes with: script: | const changedFiles = JSON.parse(`${{ steps.changed-files.outputs.all }}`); const boardChanges = changedFiles.filter(f => f.startsWith('app/boards')); return boardChanges.length ? 'true' : 'false'; result-encoding: string - uses: actions/github-script@v4 id: core-changes with: script: | const changedFiles = JSON.parse(`${{ steps.changed-files.outputs.all }}`); const boardChanges = changedFiles.filter(f => f.startsWith('app/boards')); const appChanges = changedFiles.filter(f => f.startsWith('app')); const ymlChanges = changedFiles.includes('.github/workflows/build.yml'); return boardChanges.length < appChanges.length || ymlChanges ? 'true' : 'false'; result-encoding: string