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..20b4ec74 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,55 @@ name: CI branches: [master] pull_request: +permissions: + contents: read + pull-requests: write + jobs: + label: + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v5 + + clang-format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: recursive + + - uses: actions/setup-python@v6 + with: + python-version: '3.14' + + - name: Install clang-format-21 + run: | + wget https://apt.llvm.org/llvm.sh + chmod +x llvm.sh + sudo ./llvm.sh 21 + 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@v6 + with: + submodules: recursive + + - uses: actions/setup-python@v6 + 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 + build: runs-on: ubuntu-latest steps: @@ -19,19 +67,85 @@ jobs: - name: Install PlatformIO Core run: pip install --upgrade platformio - - name: Install clang-format-21 - run: | - wget https://apt.llvm.org/llvm.sh - chmod +x llvm.sh - sudo ./llvm.sh 21 - sudo apt-get update - sudo apt-get install -y clang-format-21 - - - 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) - - 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" + + - name: Upload firmware.bin artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ github.event_name == 'pull_request' && format('firmware-bin-pr-{0}-{1}', github.event.pull_request.number, steps.short_sha.outputs.short) || format('firmware-bin-{0}', steps.short_sha.outputs.short) }} + path: .pio/build/default/firmware.bin + if-no-files-found: error + + - name: Comment PR with firmware stats and firmware.bin + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const marker = ''; + const ram = `${{ steps.fw_stats.outputs.ram_line }}`.trim(); + const flash = `${{ steps.fw_stats.outputs.flash_line }}`.trim(); + const prNumber = context.payload.pull_request?.number; + const shortSha = `${{ steps.short_sha.outputs.short }}`.trim() || context.sha.substring(0, 7); + const artifactName = prNumber + ? `firmware-bin-pr-${prNumber}-${shortSha}` + : `firmware-bin-${shortSha}`; + let firmwareLine = 'Firmware: artifact not found'; + try { + const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.runId, + }); + const firmwareArtifact = artifacts.artifacts.find((artifact) => artifact.name === artifactName); + if (firmwareArtifact) { + const artifactUiUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/artifacts/${firmwareArtifact.id}`; + firmwareLine = `Firmware: [${artifactName}](${artifactUiUrl})`; + } else { + firmwareLine = `Firmware: artifact not found (${artifactName})`; + } + } catch (error) { + firmwareLine = `Firmware: artifact lookup failed (${error.message})`; + } + const body = `${marker}\n**Firmware build stats**\n\n\`\`\`\n${ram || 'RAM: not found'}\n${flash || 'Flash: not found'}\n\`\`\`\n\n**Firmware binary**\n${firmwareLine}`; + 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-formatting-check.yml b/.github/workflows/pr-formatting-check.yml index 044b7b64..fd9c4d02 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, + }); + }