diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 00000000..eae3f875 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,38 @@ +firmware: + - changed-files: + - any-glob-to-any-file: + - src/** + - lib/** + - open-x4-sdk/** + +ui: + - changed-files: + - any-glob-to-any-file: + - src/activities/** + - src/network/html/** + - docs/images/** + +epub: + - changed-files: + - any-glob-to-any-file: + - lib/Epub/** + +network: + - changed-files: + - any-glob-to-any-file: + - src/network/** + - src/util/UrlUtils.* + - lib/OpdsParser/** + +docs: + - changed-files: + - any-glob-to-any-file: + - docs/** + - README* + - CHANGELOG* + +tests: + - changed-files: + - any-glob-to-any-file: + - test/** + - scripts/** diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 286f14aa..5a8cb085 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,24 +1,25 @@ -name: CI -'on': +name: CI (build) + +on: push: branches: [master] pull_request: +permissions: + contents: read + jobs: - build: + clang-format: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 with: submodules: recursive - - uses: actions/setup-python@v6 + - uses: actions/setup-python@v5 with: python-version: '3.14' - - name: Install PlatformIO Core - run: pip install --upgrade platformio - - name: Install clang-format-21 run: | wget https://apt.llvm.org/llvm.sh @@ -27,11 +28,77 @@ jobs: sudo apt-get update sudo apt-get install -y clang-format-21 + - name: Run clang-format + run: | + PATH="/usr/lib/llvm-21/bin:$PATH" ./bin/clang-format-fix + git diff --exit-code || (echo "Please run 'bin/clang-format-fix' to fix formatting issues" && exit 1) + + cppcheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - uses: actions/setup-python@v5 + with: + python-version: '3.14' + + - name: Install PlatformIO Core + run: pip install --upgrade platformio + - name: Run cppcheck run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high - - name: Run clang-format - run: PATH="/usr/lib/llvm-21/bin:$PATH" ./bin/clang-format-fix && git diff --exit-code || (echo "Please run 'bin/clang-format-fix' to fix formatting issues" && exit 1) + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - uses: actions/setup-python@v5 + with: + python-version: '3.14' + + - name: Install PlatformIO Core + run: pip install --upgrade platformio - name: Build CrossPoint - run: pio run + run: | + set -euo pipefail + pio run | tee pio.log + + - name: Capture short SHA + id: short_sha + run: echo "short=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT" + + - name: Extract firmware stats + id: fw_stats + run: | + set -euo pipefail + ram_line="$(grep -E "RAM:\\s" -m1 pio.log || true)" + flash_line="$(grep -E "Flash:\\s" -m1 pio.log || true)" + echo "ram_line=${ram_line}" >> "$GITHUB_OUTPUT" + echo "flash_line=${flash_line}" >> "$GITHUB_OUTPUT" + { + echo "## Firmware build stats" + if [ -n "$ram_line" ]; then echo "- ${ram_line}"; else echo "- RAM: not found"; fi + if [ -n "$flash_line" ]; then echo "- ${flash_line}"; else echo "- Flash: not found"; fi + } >> "$GITHUB_STEP_SUMMARY" + + # Upload both the binary and the stats/log so Stage 2 can read them without checkout. + - name: Upload firmware.bin artifact + uses: actions/upload-artifact@v4 + with: + name: firmware-bin + path: .pio/build/default/firmware.bin + if-no-files-found: error + + - name: Upload build metadata artifact + uses: actions/upload-artifact@v4 + with: + name: build-meta + path: | + pio.log + if-no-files-found: error \ No newline at end of file diff --git a/.github/workflows/pr-formatting-check.yml b/.github/workflows/pr-formatting-check.yml index 044b7b64..7808fd71 100644 --- a/.github/workflows/pr-formatting-check.yml +++ b/.github/workflows/pr-formatting-check.yml @@ -9,6 +9,7 @@ on: permissions: statuses: write + pull-requests: write jobs: title-check: @@ -21,6 +22,44 @@ jobs: egress-policy: audit - name: Check PR Title + id: title_check uses: amannn/action-semantic-pull-request@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Comment with changelog hint on failure + if: failure() + uses: actions/github-script@v7 + with: + script: | + const marker = ''; + const error = + `${{ steps.title_check.outputs.error_message || steps.title_check.outputs.error || '' }}`.trim(); + const details = error ? `\n\n**Error:** ${error}` : '\n\n**Error:** See workflow logs.'; + const body = `${marker} +**PR title check failed** + +Please use a Conventional Commit-style prefix (e.g., \`feat:\`, \`fix:\`, \`docs:\`, \`chore:\`). +If this change should appear in release notes, ensure the title reflects the correct category.${details}`; + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const existing = comments.find((comment) => comment.body && comment.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } diff --git a/.github/workflows/pr-writer.yml b/.github/workflows/pr-writer.yml new file mode 100644 index 00000000..fec4f42b --- /dev/null +++ b/.github/workflows/pr-writer.yml @@ -0,0 +1,145 @@ +comment_firmware: + runs-on: ubuntu-latest + steps: + - name: Find the matching CI run for this PR SHA + id: find_run + uses: actions/github-script@v8 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const pr = context.payload.pull_request; + const headSha = pr.head.sha; + + const { data } = await github.rest.actions.listWorkflowRunsForRepo({ + owner, + repo, + event: "pull_request", + per_page: 50, + }); + + const run = data.workflow_runs.find(r => r.head_sha === headSha && r.name === "CI (build)"); + if (!run) core.setFailed(`No matching CI (build) run found for head_sha=${headSha}.`); + + core.setOutput("run_id", String(run.id)); + core.setOutput("run_html_url", run.html_url); + + - name: Locate artifact IDs in the CI run + id: artifacts + uses: actions/github-script@v8 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const runId = Number("${{ steps.find_run.outputs.run_id }}"); + + const { data } = await github.rest.actions.listWorkflowRunArtifacts({ + owner, + repo, + run_id: runId, + per_page: 100, + }); + + const artifacts = data.artifacts || []; + const fw = artifacts.find(a => a.name === "firmware-bin"); + const meta = artifacts.find(a => a.name === "build-meta"); + + if (!meta) core.setFailed("build-meta artifact not found in CI run artifacts."); + // firmware-bin is nice-to-have for linking; fail if you want. + core.setOutput("fw_id", fw ? String(fw.id) : ""); + core.setOutput("meta_id", String(meta.id)); + + - name: Download build-meta artifact zip and extract pio.log + id: parse_log + shell: bash + env: + OWNER: ${{ github.repository_owner }} + REPO: ${{ github.event.repository.name }} + META_ID: ${{ steps.artifacts.outputs.meta_id }} + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + + # Download artifact zip via GitHub API (will redirect to blob storage; -L follows) + api="https://api.github.com/repos/${OWNER}/${REPO}/actions/artifacts/${META_ID}/zip" + curl -sSL \ + -H "Authorization: Bearer ${GH_TOKEN}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + -o build-meta.zip \ + "${api}" + + mkdir -p build-meta + unzip -q build-meta.zip -d build-meta + + if [[ ! -f build-meta/pio.log ]]; then + echo "pio.log not found inside build-meta artifact" + echo "ram_line=" >> "$GITHUB_OUTPUT" + echo "flash_line=" >> "$GITHUB_OUTPUT" + exit 0 + fi + + ram_line="$(grep -E "RAM:\s" -m1 build-meta/pio.log || true)" + flash_line="$(grep -E "Flash:\s" -m1 build-meta/pio.log || true)" + + echo "ram_line=${ram_line}" >> "$GITHUB_OUTPUT" + echo "flash_line=${flash_line}" >> "$GITHUB_OUTPUT" + + - name: Post/update PR comment with RAM/Flash + artifact links + uses: actions/github-script@v8 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const prNumber = context.payload.pull_request.number; + + const runId = Number("${{ steps.find_run.outputs.run_id }}"); + const runUrl = "${{ steps.find_run.outputs.run_html_url }}"; + + const marker = ''; + const ram = `${{ steps.parse_log.outputs.ram_line }}`.trim(); + const flash = `${{ steps.parse_log.outputs.flash_line }}`.trim(); + + const fwId = `${{ steps.artifacts.outputs.fw_id }}`.trim(); + const metaId = `${{ steps.artifacts.outputs.meta_id }}`.trim(); + + const fwUrl = fwId ? `https://github.com/${owner}/${repo}/actions/runs/${runId}/artifacts/${fwId}` : null; + const metaUrl = metaId ? `https://github.com/${owner}/${repo}/actions/runs/${runId}/artifacts/${metaId}` : null; + + const body = +`${marker} +**Firmware build stats** + +\`\`\` +${ram || "RAM: not found"} +${flash || "Flash: not found"} +\`\`\` + +**Artifacts** +- CI run: ${runUrl} +- Firmware binary: ${fwUrl ? `[firmware-bin](${fwUrl})` : "artifact not found"} +- Build logs: ${metaUrl ? `[build-meta](${metaUrl})` : "artifact not found"} +`; + + const { data: comments } = await github.rest.issues.listComments({ + owner, + repo, + issue_number: prNumber, + per_page: 100, + }); + + const existing = comments.find(c => (c.body || "").includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner, + repo, + issue_number: prNumber, + body, + }); + } \ No newline at end of file