From 830f2ed55333bbee2da7636b2e968db30995cb72 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Fri, 9 Oct 2026 11:24:41 +0100 Subject: [PATCH 01/10] chore(ci): add stage pipeline, pin pip/uv/deps, issue forms and make pr --- .github/ISSUE_TEMPLATE/bug_report.md | 38 -- .github/ISSUE_TEMPLATE/bug_report.yml | 54 +++ .github/ISSUE_TEMPLATE/config.yml | 5 + .github/ISSUE_TEMPLATE/feature_request.md | 20 - .github/ISSUE_TEMPLATE/feature_request.yml | 26 + .../pull_request_template.md | 68 --- .github/dependabot.yml | 18 + .github/pull_request_template.md | 29 ++ .github/release.yml | 19 + .github/workflows/codeql.yml | 117 ++--- .github/workflows/dev-ci.yml | 152 +++--- .github/workflows/pr-title-lint.yml | 13 +- .github/workflows/release-please.yml | 20 - .github/workflows/stage-ci.yml | 23 - .github/workflows/stage-signoff.yml | 22 + .gitignore | 2 - .release-please-manifest.json | 3 - Makefile | 17 +- SECURITY.md | 104 ++++ pyproject.toml | 28 +- release-please-config.json | 8 - scripts/create-pr.sh | 450 ++++++++++++++++++ scripts/install-updater.sh | 23 +- scripts/install.sh | 12 +- scripts/post-merge | 2 +- scripts/requirements-dev.txt | 46 +- scripts/requirements-pip.txt | 2 + scripts/requirements.txt | 7 +- tests/scripts/test_requirements_pins.py | 36 +- tests/updater/test_service_unit.py | 4 +- updater/executor.py | 2 + updater/hooks/BlocksScreen.sh | 1 + updater/service.py | 3 +- 33 files changed, 954 insertions(+), 420 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml delete mode 100644 .github/PULL_REQUEST_TEMPLATE/pull_request_template.md create mode 100644 .github/dependabot.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/release.yml delete mode 100644 .github/workflows/release-please.yml delete mode 100644 .github/workflows/stage-ci.yml create mode 100644 .github/workflows/stage-signoff.yml delete mode 100644 .release-please-manifest.json create mode 100644 SECURITY.md delete mode 100644 release-please-config.json create mode 100755 scripts/create-pr.sh create mode 100644 scripts/requirements-pip.txt diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index dd84ea78..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] - -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..ba8160f1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,54 @@ +name: Bug report +description: Something on the printer screen is broken or behaves wrongly +labels: ["bug"] +body: + - type: textarea + id: what-happened + attributes: + label: What happened + description: What went wrong, and what you expected instead. + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to reproduce + placeholder: | + 1. Open ... + 2. Tap ... + 3. See ... + validations: + required: true + - type: dropdown + id: branch + attributes: + label: Branch + description: The branch the printer tracks. + options: + - Release-2.0 + - main + - stage + - dev + - Other + validations: + required: true + - type: input + id: commit + attributes: + label: Commit + description: "`git -C ~/BlocksScreen rev-parse --short HEAD` on the printer, if known." + - type: input + id: hardware + attributes: + label: Printer and hardware + placeholder: e.g. RF50, Raspberry Pi 5, AMU attached + - type: textarea + id: logs + attributes: + label: Logs + description: Relevant lines from logs/BlocksScreen.log and klippy.log. + render: shell + - type: textarea + id: context + attributes: + label: Screenshots and context diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..b1e8ee9d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Report a security vulnerability + url: https://github.com/BlocksTechnology/BlocksScreen/security/advisories/new + about: Report privately instead of opening a public issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index bbcbbe7d..00000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: '' -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..97c0b661 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,26 @@ +name: Feature request +description: Suggest a new feature or an improvement +labels: ["enhancement"] +body: + - type: textarea + id: problem + attributes: + label: Problem + description: What is hard or impossible today, and who it affects. + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed solution + description: What you would like to happen. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + - type: textarea + id: context + attributes: + label: Screenshots and context diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md deleted file mode 100644 index e65e9dbb..00000000 --- a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -name: Pull Request -about: Propose code changes -title: '' -labels: '' -assignees: '' - ---- - -# PR Checklist (Delete the section after checking everything) - -- [ ] PR title follows Conventional Commits: `type: description` or `type(scope): description`, type is one of `feat|fix|docs|refactor|test|chore|perf` (e.g. `fix(updater): retry apt lock on timeout`). Enforced by CI - this is what drives automatic version bumps, so a rejected title means no version credit for this change. -- [ ] Make sure you are requesting to PR **feature/bugfix/refactor branch**. -- [ ] Make sure you are making the pull request against the **dev** branch. -- [ ] Make sure you include labels to the PR -- [ ] Request a reviewer for the current PR -- [ ] After the PR is published, make sure you view the tests conducted by github actions, download artifacts and inspect them. Failure to pass the tests will result in a request for changes. - - - -# Description - -Select the type: -- [ ] Feature -- [ ] Bug fix -- [ ] Code refactor -- [ ] Documentation - -Include a summary of the changes made in this PR and which issue is fixed. - -If the current PR is related to a bug introduced in another PR please insert the reference of the previous PR with **#**. - - -Add a concise checklist of the implemented changes, as exemplified below. - -- Change 1. -- Change 2. -- Change 3. -- ... - - -**Depending on the type of change the current PR relates to, delete sections that are not applicable.** - - -# Motivation - -Include a detailed explanation for the current PR. Delete section if not applicable. - -# Tests -Please describe the conducted tests, and include logs, reports on the tests. - -Also include test configuration. - - -Delete section if not applicable. -# Screenshots - -Include screenshots of the changes. Mostly used for UI changes. - -Delete section if not applicable. - - -# Future work - -Emphasize future tasks and improvements tied directly to this pull request. -Include only non-breaking changes to this pull request - -Delete section if not applicable. \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..03fa5135 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +version: 2 +updates: + # Actions only: pip bumps would break the printers' aarch64 PyQt6 6.7.1 ceiling. + - package-ecosystem: github-actions + directory: / + target-branch: dev + schedule: + interval: monthly + # Skip releases younger than a week, the window most hijacked tags are caught in. + cooldown: + default-days: 7 + labels: ["dependencies"] + groups: + actions: + patterns: ["*"] + commit-message: + prefix: chore + include: scope diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..53a5fbad --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,29 @@ + + +## Description +- [ ] Feature +- [ ] Bug fix +- [ ] Code refactor +- [ ] Documentation + +## Summary + + +## Related issues + + +## Changes +- + +## Testing + +- [ ] `make check` and `make docstrcov` pass +- [ ] Tested on simulator / printer + +## Screenshots + + +## Reviewer notes + diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 00000000..f630e312 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,19 @@ +# Categories for GitHub's "Generate release notes"; first matching label wins, so "*" stays last. +changelog: + categories: + - title: Breaking changes + labels: ["breaking"] + - title: Features + labels: ["enhancement"] + - title: Bug fixes + labels: ["bug"] + - title: Performance + labels: ["performance"] + - title: Refactoring + labels: ["refactor"] + - title: Documentation + labels: ["documentation"] + - title: Dependencies + labels: ["dependencies"] + - title: Other changes + labels: ["*"] diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 32b2789d..ed14df38 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,105 +1,52 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# name: "CodeQL Advanced" +# No paths-ignore: code scanning merge protection waits forever for a skipped analysis. on: push: - paths-ignore: - - 'BlocksScreen/lib/ui/**' - - 'extras/**' - - 'scripts/**' + branches: [main, dev, stage] pull_request: - paths-ignore: - - 'BlocksScreen/lib/ui/**' - - 'extras/**' - - 'scripts/**' schedule: - cron: '27 5 * * 0' +permissions: + contents: read + +# Stage's code_scanning rule needs results for the exact SHA, so push runs are never cancelled. +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.sha }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: analyze: name: Analyze (${{ matrix.language }}) - # Runner size impacts CodeQL analysis time. To learn more, please see: - # - https://gh.io/recommended-hardware-resources-for-running-codeql - # - https://gh.io/supported-runners-and-hardware-resources - # - https://gh.io/using-larger-runners (GitHub.com only) - # Consider using larger runners or machines with greater resources for possible analysis time improvements. - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + runs-on: ubuntu-24.04 + timeout-minutes: 30 permissions: - # required for all workflows security-events: write - - # required to fetch internal or private CodeQL packs - packages: read - - # only required for workflows in private repositories - actions: read contents: read strategy: fail-fast: false matrix: include: - - language: python - build-mode: none - # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' - # Use `c-cpp` to analyze code written in C, C++ or both - # Use 'java-kotlin' to analyze code written in Java, Kotlin or both - # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both - # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, - # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. - # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how - # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages - steps: - - name: Checkout repository - uses: actions/checkout@v4 + - language: python + build-mode: none + - language: actions + build-mode: none - # Add any setup steps before running the `github/codeql-action/init` action. - # This includes steps like installing compilers or runtimes (`actions/setup-node` - # or others). This is typically only required for manual builds. - # - name: Setup runtime (example) - # uses: actions/setup-example@v1 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v4 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - # If the analyze step fails for one of the languages you are analyzing with - # "We were unable to automatically build your code", modify the matrix above - # to set the build mode to "manual" for that language. Then modify this step - # to build your code. - # â„šī¸ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - name: Run manual build steps - if: matrix.build-mode == 'manual' - shell: bash - run: | - echo 'If you are using a "manual" build mode for one or more of the' \ - 'languages you are analyzing, replace this with the commands to build' \ - 'your code, for example:' - echo ' make bootstrap' - echo ' make release' - exit 1 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 - with: - category: "/language:${{matrix.language}}" + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Initialize CodeQL + uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/dev-ci.yml b/.github/workflows/dev-ci.yml index a7d17075..48d82fe1 100644 --- a/.github/workflows/dev-ci.yml +++ b/.github/workflows/dev-ci.yml @@ -1,32 +1,53 @@ name: CI-dev-pipeline +# No paths filter: a required check that never starts leaves the PR pending forever. on: + # A squash merge creates a SHA no PR run tested; stage can only require checks this run reports. + push: + branches: [dev] pull_request: - branches: - - dev - paths-ignore: - - "scripts/**" - - "BlocksScreen/lib/ui/**" - - "extras/**" + branches-ignore: + - main + +permissions: + contents: read + +# PR runs supersede each other; push runs are grouped per SHA so a fast second merge never cancels the first. +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.sha }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +defaults: + run: + # Explicit bash adds -o pipefail, so `| tee` keeps the tool's exit code. + shell: bash + jobs: ci-checks: - runs-on: ubuntu-latest + # Pinned: ubuntu-26.04 has no Python 3.11.2 build. + runs-on: ubuntu-24.04 + timeout-minutes: 15 strategy: fail-fast: false matrix: + # Debian 12 bookworm python3, what the printers run. python-version: ["3.11.2"] test-type: [ruff, pylint, pytest, docstrcov, security] steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ matrix.python-version }} cache: pip - cache-dependency-path: scripts/requirements-dev.txt + cache-dependency-path: | + scripts/requirements.txt + scripts/requirements-dev.txt - name: Install system dependencies if: matrix.test-type == 'pytest' @@ -36,80 +57,53 @@ jobs: - name: Install dependencies run: | - echo "Installing dependencies" - python -m pip install --upgrade pip - pip install -r scripts/requirements-dev.txt + python -m pip install -r scripts/requirements-pip.txt + pip install -r scripts/requirements.txt -r scripts/requirements-dev.txt - - name: Run Test ${{ matrix.test-type }} + # The fleet installs wheels only (sdbus aside), so a pin without an aarch64 wheel passes CI yet breaks every box. + - name: Check pins have bookworm aarch64 wheels + if: matrix.test-type == 'security' run: | - echo "Starting test runs" - if [ "${{ matrix.test-type }}" == "ruff" ]; then - echo "Running Formatting Test" - ruff check --output-format=github --target-version=py311 --config=pyproject.toml > ruff-output.txt 2>&1 - ruff format --diff --target-version=py311 --config=pyproject.toml >> ruff-output.txt 2>&1 - echo "Ruff finished" - fi - if [ "${{ matrix.test-type }}" == "pylint" ]; then - echo "Running Pylint Code Test" - pylint -j$(nproc) --recursive=y BlocksScreen/ > pylint-output.txt 2>&1 - echo "Pylint finished" - fi - - if [ "${{ matrix.test-type }}" == "pytest" ]; then - if [ -d "tests/" ] && [ "$(ls -A tests/)" ]; then - echo "Running Python unit tests" - pytest tests/ --doctest-modules --junitxml=junit/test-results.xml --cov=BlocksScreen/ --cov-report=xml --cov-report=html > pytest-output.txt 2>&1 - else - echo "No tests directory no need to proceed with tests" - fi - fi - - if [ "${{ matrix.test-type }}" == "docstrcov" ]; then - echo "Running docstring coverage test" - docstr-coverage BlocksScreen/ --exclude '.*/BlocksScreen/lib/ui/.*?$' --fail-under=80 --skip-magic --skip-init --skip-private --skip-property > docstr-cov-output.txt 2>&1 - fi - - if [ "${{matrix.test-type }}" == "security" ]; then - echo "Running bandit security test" - bandit -c pyproject.toml -r . -f json -o bandit-output.json 2>&1 - fi + # Bookworm glibc is 2.36; pip does not expand manylinux tags downwards, so list each one. + plats=(--platform manylinux2014_aarch64) + for m in $(seq 17 36); do plats+=(--platform "manylinux_2_${m}_aarch64"); done + pip install --dry-run --quiet --ignore-installed --only-binary :all: "${plats[@]}" \ + --python-version 3.11 --implementation cp --abi cp311 \ + -r <(grep -v '^sdbus' scripts/requirements.txt) - - name: Upload ruff artifact - if: always() && matrix.test-type == 'ruff' - uses: actions/upload-artifact@v4 - with: - name: ruff-results - path: ruff-output.txt - - - name: Upload Pylint Artifacts - if: always() && matrix.test-type == 'pylint' - uses: actions/upload-artifact@v4 - with: - name: pylint-results - path: pylint-output.txt + - name: Run ${{ matrix.test-type }} + env: + TEST_TYPE: ${{ matrix.test-type }} + run: | + case "$TEST_TYPE" in + ruff) + rc=0 + ruff check --output-format=github --target-version=py311 --config=pyproject.toml 2>&1 | tee ruff-output.txt || rc=1 + ruff format --diff --target-version=py311 --config=pyproject.toml 2>&1 | tee -a ruff-output.txt || rc=1 + exit $rc + ;; + pylint) + pylint -j"$(nproc)" --recursive=y BlocksScreen/ 2>&1 | tee pylint-output.txt + ;; + pytest) + pytest tests/ --doctest-modules --junitxml=junit/test-results.xml --cov=BlocksScreen/ --cov-report=xml --cov-report=html 2>&1 | tee pytest-output.txt + ;; + docstrcov) + docstr-coverage BlocksScreen/ --exclude '.*/BlocksScreen/lib/ui/.*?$' --fail-under=80 --skip-magic --skip-init --skip-private --skip-property 2>&1 | tee docstrcov-output.txt + ;; + security) + bandit -c pyproject.toml -r . 2>&1 | tee security-output.txt + ;; + esac - - name: Upload Pytest Artifacts - if: always() && matrix.test-type == 'pytest' - uses: actions/upload-artifact@v4 + - name: Upload ${{ matrix.test-type }} report + if: always() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: - name: pytest-results + name: ${{ matrix.test-type }}-results path: | - pytest-output.txt - junit/test-results.xml + *-output.txt + junit/ coverage.xml htmlcov/ - continue-on-error: true - - - name: Upload docstr coverage report - if: always() && matrix.test-type == 'docstrcov' - uses: actions/upload-artifact@v4 - with: - name: docstr-coverage - path: docstr-cov-output.txt - - - name: Upload bandit security report - if: always() && matrix.test-type == 'security' - uses: actions/upload-artifact@v4 - with: - name: bandit-output - path: bandit-output.json + if-no-files-found: ignore diff --git a/.github/workflows/pr-title-lint.yml b/.github/workflows/pr-title-lint.yml index efbb0b6b..4880e3ee 100644 --- a/.github/workflows/pr-title-lint.yml +++ b/.github/workflows/pr-title-lint.yml @@ -1,17 +1,22 @@ name: Lint PR Title +# pull_request, not pull_request_target: _target runs the default branch's copy and is +# blocked by default on public repos from 2026-11-02. +# Only PRs into dev: their titles become the squash commits; promotion PRs are named freely. on: - pull_request_target: - types: [opened, edited, synchronize] + pull_request: + branches: [dev] + types: [opened, edited, synchronize, reopened] permissions: pull-requests: read jobs: lint-pr-title: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 + timeout-minutes: 5 steps: - - uses: amannn/action-semantic-pull-request@v5 + - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml deleted file mode 100644 index 58fc3da4..00000000 --- a/.github/workflows/release-please.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: release-please - -on: - push: - branches: - - main - -permissions: - contents: write - pull-requests: write - -jobs: - release-please: - runs-on: ubuntu-latest - steps: - - uses: googleapis/release-please-action@v4 - with: - token: ${{ secrets.RELEASE_PLEASE_PAT }} - config-file: release-please-config.json - manifest-file: .release-please-manifest.json diff --git a/.github/workflows/stage-ci.yml b/.github/workflows/stage-ci.yml deleted file mode 100644 index 6d8de6be..00000000 --- a/.github/workflows/stage-ci.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: stage-ci - -on: - branches: - - stage - paths-ignore: - - "scripts/**" - - "BlocksScreen/lib/ui/**" - - "extras/**" - workflow_run: - workflows: ["dev-test-code"] - types: - - completed - jobs: - ci-stage: - if: ${{ github.event.workflow_run.conclusion == 'success' }} - runs-on: ubuntu-latest - steps: - - name: Checkout repo - uses: actions/checkout@v4 - - - name: Run staging pipeline - run: echo "Running staging integration tests..." \ No newline at end of file diff --git a/.github/workflows/stage-signoff.yml b/.github/workflows/stage-signoff.yml new file mode 100644 index 00000000..2ea84811 --- /dev/null +++ b/.github/workflows/stage-signoff.yml @@ -0,0 +1,22 @@ +name: Stage sign-off + +# Approving this run records a `staging` deployment for the stage tip; the Prod and +# Release Branches rulesets require it before a PR can merge. +on: + push: + branches: [stage] + +permissions: {} + +concurrency: + group: stage-signoff + cancel-in-progress: true + +jobs: + signoff: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + environment: staging + steps: + - name: Record staging sign-off + run: echo "Staging printers validated $GITHUB_SHA" diff --git a/.gitignore b/.gitignore index 174738c8..8dddf5a8 100644 --- a/.gitignore +++ b/.gitignore @@ -164,8 +164,6 @@ cython_debug/ *.log # TimedRotatingFileHandler appends a date suffix, so *.log alone leaves logs/ dirty on every box. logs/ -# Bench-only acceptance/diagnostic suite: stays local, never shipped to the fleet. -scripts/healthcheck/ */*Zone.Identifier *:Zone.Identifier diff --git a/.release-please-manifest.json b/.release-please-manifest.json deleted file mode 100644 index 1772e6fa..00000000 --- a/.release-please-manifest.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - ".": "1.0.0" -} diff --git a/Makefile b/Makefile index 300a8f58..63e00566 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: all init init-dev venv run lint format-check security check \ test test-all test-unit test-network test-ui test-integration test-fast \ coverage coverage-all coverage-network clean clean-venv \ - docstrcov rcc rcc-all diagnose help + docstrcov rcc rcc-all pr help .DEFAULT_GOAL := help SHELL := /bin/bash @@ -39,8 +39,8 @@ venv: ## Print venv activation command (source manually — subshells cannot exp init: ## Install production dependencies $(PIP) install -r scripts/requirements.txt -init-dev: ## Install dev + test dependencies - $(PIP) install -r scripts/requirements-dev.txt +init-dev: ## Install dev + test dependencies (PyQt6 pin comes from requirements.txt) + $(PIP) install -r scripts/requirements.txt -r scripts/requirements-dev.txt # ───────────────────────────────────────────────────────────────────────────── ##@ Run @@ -142,17 +142,12 @@ docstrcov: ## Check docstring coverage (fail-under=80%, matches CI) --fail-under 80 \ --skip-magic --skip-init --skip-private --skip-property - - # ───────────────────────────────────────────────────────────────────────────── -##@ Diagnostics +##@ Contributing # ───────────────────────────────────────────────────────────────────────────── -# scripts/healthcheck/ is git-ignored (bench-only), so degrade with a hint instead of a shell error. -diagnose: ## Deep-check a running machine: health, USB forensics, power (scripts/healthcheck/, local-only, needs sudo) - @test -x scripts/healthcheck/bs-diag.sh \ - || { echo "scripts/healthcheck/ not present on this checkout (bench-only, git-ignored)"; exit 1; } - sudo scripts/healthcheck/bs-diag.sh all +pr: ## Open a PR step by step: title, labels, local gates, review before push + @scripts/create-pr.sh # ───────────────────────────────────────────────────────────────────────────── ##@ Cleanup diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..e1820970 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,104 @@ +# Security Policy + +This policy covers BlocksScreen, the touchscreen interface and field updater that run on Blocks 3D printers. +It explains how to report a vulnerability, what is in scope, and what you can expect from us. + +**Do not report security vulnerabilities through public GitHub issues, discussions or pull requests.** + +## Reporting a vulnerability + +Report privately through [GitHub private vulnerability reporting](https://github.com/BlocksTechnology/BlocksScreen/security/advisories/new) (Security tab, "Report a vulnerability"). +This is the preferred channel: the report, the discussion and the fix stay private until we publish an advisory. + +If you cannot use GitHub, email [info@blockstec.com](mailto:info@blockstec.com) with the subject `SECURITY: BlocksScreen`. +We will move the report into a private advisory and invite you to it. + +Please include: + +- The affected branch and commit (`git -C ~/BlocksScreen rev-parse --short HEAD` on the printer) +- The printer model and how BlocksScreen was installed +- Steps to reproduce, ideally a minimal proof of concept +- The impact: what an attacker gains and what access they need first +- Optionally, a [CVSS 4.0](https://www.first.org/cvss/calculator/4.0) vector and a suggested fix + +Send plain text or Markdown. +Do not attach binaries; if the proof of concept needs a crafted file, include a script that generates it. + +If you used AI tools to find or write up the issue, say which ones and verify every claim yourself before submitting. +Reports with unverified or fabricated details will be closed. + +## What to expect + +| Step | Target | +|---|---| +| Acknowledge your report | 3 business days | +| Initial assessment: confirmed, needs more information, or declined with reasons | 10 business days | +| Status updates until resolved | At least every 14 days | +| Fix released and advisory published | Within 90 days of the report | + +We are a small team, so these are targets rather than guarantees. +If a fix needs longer, we will explain why and agree a new date with you. + +Once a report is confirmed, we: + +1. Open a draft GitHub Security Advisory and may invite you to a temporary private fork to work on the fix. +2. Fix it on `dev`, validate it on our staging printers through `stage`, then release it to the production branch. +3. Publish the advisory, request a CVE through GitHub when the issue qualifies, and credit you unless you prefer to stay anonymous. + +## Coordinated disclosure + +Please keep the details private until we publish the advisory or 90 days have passed since your report, whichever comes first. +If the vulnerability is being actively exploited, we will work with you on a shorter timeline. + +We do not run a paid bug bounty. +Reporters are credited in the advisory and the release notes. + +## Supported versions + +BlocksScreen ships from git branches rather than numbered releases, and printers receive fixes through the built-in updater. +Only the latest commit of each production branch is supported; printers on older commits must update to get fixes. + +| Branch | Role | Supported | +|---|---|---| +| `main`, `Release-2.0` | Production, tracked by the printer fleet | Yes | +| `dev`, `stage` | Development and pre-release testing | Yes, fixed in place | +| Forks and modified installs | Not maintained by Blocks Technology | No | + +## Scope + +In scope is the code in this repository: + +- The BlocksScreen application, including how it handles data from Moonraker and files from USB drives or printer storage (G-code metadata, thumbnails) +- The updater, its D-Bus service and the privileged helpers it calls +- The install, bootstrap and deploy scripts, systemd units and D-Bus policy shipped here + +Report these to their own maintainers instead: Klipper, Moonraker, KlipperScreen, Happy Hare, Spoolman, Debian packages and Python dependencies. +If BlocksScreen uses one of them in an insecure way, that is in scope here. +Blocks Technology websites and online services are not covered by this policy. + +BlocksScreen assumes the printer sits on a trusted local network. +Do not expose the printer's services (Moonraker, SSH, web interfaces) directly to the internet; use a VPN for remote access. + +The following are not treated as vulnerabilities: + +- Actions available to anyone at the printer through the touchscreen, which has no login by design +- Attacks that need an already compromised printer, root or SSH access, or a man-in-the-middle position on the local network +- Attacks that need the printer's services exposed directly to the internet +- Automated scanner output or theoretical issues without a working proof of concept +- Denial of service by flooding the network or the printer's services +- Social engineering of Blocks Technology staff or customers + +## Safe harbor + +We consider security research carried out in good faith under this policy to be authorized, and we will not pursue or support legal action against you for it. +Good faith means you: + +- Test only on printers you own or are explicitly authorized to test +- Avoid privacy violations, data destruction and service disruption, and stop as soon as you reach data that is not yours +- Give us reasonable time to fix the issue before disclosing it + +If you are unsure whether something is allowed, ask through the reporting channel before going further. + +--- + +Last updated: 2026-10-07 diff --git a/pyproject.toml b/pyproject.toml index 2de2d62b..86b48757 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "BlocksScreen" -dynamic = ['version'] +dynamic = ['version', 'dependencies', 'optional-dependencies'] description = "GUI for BLOCKS Printers running Klipper" authors = [ { name = "Hugo do Carmo Costa", email = "hugo.santos.costa@gmail.com" }, @@ -9,33 +9,15 @@ maintainers = [ { name = "Guilherme Costa", email = "guilherme.costa@blockstec.com" }, { name = "Roberto Martins ", email = "roberto.martins@blockstec.com" }, ] -dependencies = [ - 'altgraph==0.17.4', - 'certifi==2025.10.5', - 'charset-normalizer==3.4.4', - 'idna==3.15', - 'numpy==2.3.4', - 'pefile==2024.8.26', - 'PyQt6==6.10.0', - 'PyQt6-Qt6==6.10.0', - 'PyQt6_sip==13.10.2', - 'requests>=2.32.5', - 'sdbus==0.14.1', - 'sdbus-networkmanager==2.0.0', - 'typing==3.7.4.3', - 'websocket-client==1.9.0', - 'qrcode==8.2', -] requires-python = "==3.11.2" readme = "README.md" license = { text = "GNU Affero General Public License v3.0 or later" } keywords = ["GUI", "klipper", "BlocksScreen", "BLOCKS"] -[project.optional-dependencies] -dev = ["ruff", "pylint", "pytest", "pytest-cov", "docstr_coverage"] -stage = ["bandit"] -full-dev = ["BlockScreen[dev,stage]"] - +# The requirements files are the only place versions live; a second list here drifted to pins the Pi cannot install. +[tool.setuptools.dynamic] +dependencies = { file = ["scripts/requirements.txt"] } +optional-dependencies.dev = { file = ["scripts/requirements-dev.txt"] } [project.urls] Homepage = "https://blockstec.com" diff --git a/release-please-config.json b/release-please-config.json deleted file mode 100644 index d3a9fa7f..00000000 --- a/release-please-config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", - "packages": { - ".": { - "release-type": "simple" - } - } -} diff --git a/scripts/create-pr.sh b/scripts/create-pr.sh new file mode 100755 index 00000000..0f79aa2e --- /dev/null +++ b/scripts/create-pr.sh @@ -0,0 +1,450 @@ +#!/usr/bin/env bash +# make pr: guided pull request creation matching the repo's title lint, labels and PR template. +set -euo pipefail + +if ((BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 4))); then + echo "error: bash 4.4 or newer is required (found $BASH_VERSION)" >&2 + exit 1 +fi +# Without this a failed prompt inside $(...) is ignored and menus loop forever on EOF. +shopt -s inherit_errexit + +usage() { + cat <<'EOF' +Usage: make pr + +Opens a pull request for the current branch, step by step: + 1. Checks gh login, push access, branch, uncommitted changes, base branch, existing PR + 2. Title one line, type(scope)!: summary as enforced by the PR title lint; also the + commit message when you chose to commit your uncommitted changes + 3. Labels area label (type and breaking come from the title), reviewers, draft + 4. Local gates optional make check + make docstrcov before anything leaves your machine + 5. Description the PR template in your editor, Changes prefilled from your commits + 6. Review every commit and setting is shown; nothing is pushed until you type y + +Requires git, the GitHub CLI (gh) logged in, and push access to origin. +EOF +} + +case ${1:-} in + -h | --help) usage; exit 0 ;; + "") ;; + *) usage >&2; exit 2 ;; +esac + +if [[ -t 2 && -z ${NO_COLOR:-} ]]; then + BOLD=$'\e[1m' DIM=$'\e[2m' RED=$'\e[31m' GREEN=$'\e[32m' YELLOW=$'\e[33m' CYAN=$'\e[36m' RESET=$'\e[0m' +else + BOLD="" DIM="" RED="" GREEN="" YELLOW="" CYAN="" RESET="" +fi + +# All output goes to stderr so that functions can return values on stdout. +say() { printf '%s\n' "$*" >&2; } +ok() { say " ${GREEN}✔${RESET} $*"; } +warn() { say " ${YELLOW}!${RESET} $*"; } +die() { say " ${RED}✖ $*${RESET}"; exit 1; } +step() { say ""; say "${BOLD}[$1/6] $2${RESET}"; } +row() { printf ' %s%-12s%s %s\n' "$DIM" "$1" "$RESET" "$2" >&2; } + +trim() { + local s=$1 + s=${s#"${s%%[![:space:]]*}"} + printf '%s' "${s%"${s##*[![:space:]]}"}" +} + +# ask PROMPT [DEFAULT]: one trimmed line from the user; EOF aborts. +ask() { + local reply hint="" + if [[ -n ${2:-} ]]; then hint=" ${DIM}[$2]${RESET}"; fi + read -rp " ${CYAN}?${RESET} $1$hint: " reply || die "no input, aborting" + reply=$(trim "$reply") + printf '%s' "${reply:-${2:-}}" +} + +# confirm PROMPT DEFAULT(y|n) +confirm() { + local reply hint="y/N" + if [[ $2 == y ]]; then hint="Y/n"; fi + while true; do + read -rp " ${CYAN}?${RESET} $1 ${DIM}[$hint]${RESET} " reply || die "no input, aborting" + case ${reply:-$2} in + [yY] | [yY][eE][sS]) return 0 ;; + [nN] | [nN][oO]) return 1 ;; + esac + say " Answer y or n." + done +} + +# menu PROMPT DEFAULT_INDEX OPTION...: prints the chosen 0-based index; arrow keys via fzf, else a numbered list. +menu() { + local prompt=$1 default=$2 reply i + local -a options=("${@:3}") + if command -v fzf >/dev/null && [[ -t 0 && -t 2 ]]; then + # The index travels hidden in field 1, so the visible label never has to be parsed back. + reply=$(for i in "${!options[@]}"; do printf '%d\t%s\n' "$i" "${options[i]}"; done | + fzf --height=~20 --layout=reverse --no-sort --delimiter='\t' --with-nth=2.. \ + --prompt=" ? $prompt > " --header="↑/↓ move, type to filter, Enter select, Esc abort" \ + --bind "load:pos($((default + 1)))") || die "nothing selected, aborting" + say " ${CYAN}?${RESET} $prompt: ${reply#*$'\t'}" + printf '%s' "${reply%%$'\t'*}" + return + fi + for i in "${!options[@]}"; do + printf ' %s%d)%s %s\n' "$CYAN" $((i + 1)) "$RESET" "${options[i]}" >&2 + done + while true; do + reply=$(ask "$prompt" $((default + 1))) + # 10# keeps "08" from being parsed as an invalid octal number. + if [[ $reply =~ ^[0-9]+$ ]] && ((10#$reply >= 1 && 10#$reply <= ${#options[@]})); then + printf '%s' $((10#$reply - 1)) + return + fi + say " Enter a number from 1 to ${#options[@]}." + done +} + +# pick PROMPT OPTION...: prints the chosen options comma-separated, or nothing; Tab marks several in fzf. +pick() { + local prompt=$1 reply i joined + local -a options=("${@:2}") chosen=() numbers=() + if command -v fzf >/dev/null && [[ -t 0 && -t 2 ]]; then + # Enter with nothing marked returns the line under the cursor, hence the "(none)" line on top. + reply=$(printf '%s\n' "(none)" "${options[@]}" | + fzf --multi --height=~20 --layout=reverse --no-sort \ + --prompt=" ? $prompt > " --header="↑/↓ move, Tab mark, Enter confirm, Esc abort") || + die "nothing selected, aborting" + while read -r i; do + if [[ $i != "(none)" ]]; then chosen+=("$i"); fi + done <<<"$reply" + else + for i in "${!options[@]}"; do + printf ' %s%d)%s %s\n' "$CYAN" $((i + 1)) "$RESET" "${options[i]}" >&2 + done + while true; do + reply=$(ask "$prompt, numbers separated by spaces (Enter for none)") + read -ra numbers <<<"${reply//,/ }" + chosen=() + for i in "${numbers[@]}"; do + if [[ $i =~ ^[0-9]+$ ]] && ((10#$i >= 1 && 10#$i <= ${#options[@]})); then + chosen+=("${options[10#$i - 1]}") + else + say " Enter numbers from 1 to ${#options[@]}." + continue 2 + fi + done + break + done + fi + joined=$(IFS=,; printf '%s' "${chosen[*]}") + say " ${CYAN}?${RESET} $prompt: ${joined:-none}" + printf '%s' "$joined" +} + +# True when the Summary section has text besides HTML comments, which may span lines. +has_summary() { + awk ' + /^## / { in_summary = ($0 == "## Summary"); next } + in_summary { + line = $0; text = "" + while (line != "") { + if (in_comment) { + end = index(line, "-->") + if (!end) break + line = substr(line, end + 3); in_comment = 0 + } else { + start = index(line, " + -## Description +# PR Checklist + +- [ ] Title follows Conventional Commits, e.g. `fix(updater): retry apt lock on timeout`; CI rejects anything else +- [ ] Opened from a feature, bugfix or refactor branch against **dev** +- [ ] Labels added and a reviewer requested +- [ ] `make check` and `make docstrcov` pass +- [ ] CI results checked once the PR is open; failing checks mean changes requested + +# Description - [ ] Feature - [ ] Bug fix - [ ] Code refactor - [ ] Documentation -## Summary - - -## Related issues - + ## Changes - -## Testing - -- [ ] `make check` and `make docstrcov` pass +# Motivation + + +# Tests + - [ ] Tested on simulator / printer -## Screenshots - +# Screenshots + -## Reviewer notes - +# Future work + diff --git a/Makefile b/Makefile index 63e00566..1b7d6f71 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,10 @@ PYTEST_IGNORE := --ignore=$(TESTS)/network/test_sdbus_integration.py PYTEST_FLAGS ?= -vvv NM_INTEGRATION := NM_INTEGRATION_TESTS=1 +# Tracked and staged .py files only: untracked local scripts never reach CI, so they must not fail a gate. +# wildcard drops files deleted from the working tree but still in the index. +GIT_PY = $(wildcard $(shell git ls-files -- '*.py')) + PYRCC5 := /usr/bin/pyrcc5 QRC_DIR := BlocksScreen/lib/ui/resources @@ -77,15 +81,19 @@ rcc-all: ## Force recompile all .qrc files ##@ Linting & Security # ───────────────────────────────────────────────────────────────────────────── +# Stays on the folder like CI: a file list also lints lib/ and devices/, which CI's recursive run skips (no __init__.py). lint: ## Run pylint $(PYTHON) -m pylint -j$(shell nproc) --recursive=y $(SRC)/ -format-check: ## Verify formatting without modifying files (matches CI exactly) - $(PYTHON) -m ruff check --target-version=py311 --config=pyproject.toml - $(PYTHON) -m ruff format --diff --target-version=py311 --config=pyproject.toml +# --force-exclude: without it ruff ignores its exclude list for paths passed explicitly. +format-check: ## Verify formatting without modifying files (tracked files only) + @echo "ruff check + ruff format --diff on $(words $(GIT_PY)) tracked files" + @$(PYTHON) -m ruff check --force-exclude --target-version=py311 --config=pyproject.toml $(GIT_PY) + @$(PYTHON) -m ruff format --diff --force-exclude --target-version=py311 --config=pyproject.toml $(GIT_PY) -security: ## Run bandit security scan (whole repo, matches CI scope) - $(PYTHON) -m bandit -c pyproject.toml -r . +security: ## Run bandit security scan (tracked files only) + @echo "bandit on $(words $(GIT_PY)) tracked files" + @$(PYTHON) -m bandit -c pyproject.toml $(GIT_PY) check: format-check lint security test-fast ## Full pre-push gate (mirrors CI) diff --git a/scripts/create-pr.sh b/scripts/create-pr.sh index 0f79aa2e..3596141d 100755 --- a/scripts/create-pr.sh +++ b/scripts/create-pr.sh @@ -17,7 +17,7 @@ Opens a pull request for the current branch, step by step: 1. Checks gh login, push access, branch, uncommitted changes, base branch, existing PR 2. Title one line, type(scope)!: summary as enforced by the PR title lint; also the commit message when you chose to commit your uncommitted changes - 3. Labels area label (type and breaking come from the title), reviewers, draft + 3. Labels area label (type and breaking come from the title), reviewers, draft; you are the assignee 4. Local gates optional make check + make docstrcov before anything leaves your machine 5. Description the PR template in your editor, Changes prefilled from your commits 6. Review every commit and setting is shown; nothing is pushed until you type y @@ -140,10 +140,11 @@ pick() { printf '%s' "$joined" } -# True when the Summary section has text besides HTML comments, which may span lines. +# True when Description has text besides its type boxes and HTML comments, which may span lines. has_summary() { awk ' - /^## / { in_summary = ($0 == "## Summary"); next } + /^#+ / { in_summary = ($0 == "# Description"); next } + in_summary && /^- \[[ xX]\] / { next } in_summary { line = $0; text = "" while (line != "") { @@ -413,7 +414,7 @@ while true; do say " Opening $editor; save and close it to continue." sh -c "$editor \"\$1\"" editor "$body" || die "the editor failed; your draft is kept at $body" if has_summary "$body"; then break; fi - warn "The Summary section is empty; reviewers need the what and the why." + warn "The Description has no summary; reviewers need the what and the why." confirm "Open the editor again?" y || exit 1 done ok "Description ready" @@ -426,6 +427,7 @@ row Repository "$repo" row Title "$title" row Base "$base <- $branch" row Labels "${labels_text:-none}" +row Assignee "$me" row Reviewers "${reviewers:-none}" row Draft "$draft" row "Local gates" "$gates" @@ -440,7 +442,7 @@ git push -u origin "$branch" || die "push rejected; if origin/$branch has commits you lack, run 'git pull --rebase' and retry (never force-push a shared branch)" pushed=1 -args=(-R "$repo" --base "$base" --head "$branch" --title "$title" --body-file "$body") +args=(-R "$repo" --base "$base" --head "$branch" --title "$title" --body-file "$body" --assignee "$me") for label in "${apply[@]}"; do args+=(--label "$label"); done if [[ -n $reviewers ]]; then args+=(--reviewer "$reviewers"); fi if [[ $draft == yes ]]; then args+=(--draft); fi From 7155261eb168641550f85192d97c5c0054d47758 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Fri, 9 Oct 2026 15:08:49 +0100 Subject: [PATCH 03/10] chore(ci): send the stage change list to the Hardware team via Teams and Outlook --- .github/workflows/stage-signoff.yml | 45 +++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/.github/workflows/stage-signoff.yml b/.github/workflows/stage-signoff.yml index 2ea84811..8daf59ad 100644 --- a/.github/workflows/stage-signoff.yml +++ b/.github/workflows/stage-signoff.yml @@ -13,6 +13,51 @@ concurrency: cancel-in-progress: true jobs: + # No environment: it would wait for the approval it announces, so the webhook is a repository secret. + notify: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: read + steps: + - name: Send the change list to the Hardware team + env: + GH_TOKEN: ${{ github.token }} + WEBHOOK: ${{ secrets.HW_TEAMS_WEBHOOK }} + run: | + before=$(jq -r .before "$GITHUB_EVENT_PATH") + if [[ $before =~ ^0+$ ]]; then + echo "::notice::stage was created by this push, no previous tip to compare" + exit 0 + fi + gh api --paginate "repos/$GITHUB_REPOSITORY/compare/$before...$GITHUB_SHA?per_page=100" \ + --jq '.commits[].commit.message | split("\n")[0]' > subjects.txt + # Squash subjects are the PR titles; @html because they are free text from PR authors. + jq -Rn \ + --arg url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" \ + --arg run "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ + --arg before "$before" --arg sha "$GITHUB_SHA" --arg actor "$GITHUB_ACTOR" ' + [inputs] | reverse as $all | ($all | length) as $n | $sha[:8] as $short + | (if $n == 1 then "1 change" else "\($n) changes" end) as $count + | ($all[:40] | map("
  • " + (@html | sub("\\(#(?[0-9]+)\\)$"; + "(#\(.n))")) + "
  • ") | join("")) as $items + | { + subject: "BlocksScreen stage \($short): \($count) to test", + html: ("

    Stage is now at \($short), pushed by \($actor | @html). " + + "\($count) since the last stage update, newest first:

      \($items)
    " + + (if $n > 40 then "

    And \($n - 40) older, see the full diff.

    " else "" end) + + "

    Full diff | " + + "Sign-off run

    ") + }' subjects.txt > payload.json + jq -r .html payload.json >> "$GITHUB_STEP_SUMMARY" + if [[ -z $WEBHOOK ]]; then + echo "::warning::HW_TEAMS_WEBHOOK is not set, the Hardware team was not notified" + exit 0 + fi + # Payload {subject, html} feeds a Power Automate flow that posts it to Teams and emails it via Outlook. + curl --fail-with-body -sS --retry 3 -H 'Content-Type: application/json' \ + --data-binary @payload.json "$WEBHOOK" + signoff: runs-on: ubuntu-24.04 timeout-minutes: 5 From d8ec039b2a41723b7a2a3bee1e107d9c095be515 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Fri, 9 Oct 2026 17:11:00 +0100 Subject: [PATCH 04/10] chore(ci): keep every stage notify, check the pin closure, CI on all PRs --- .github/pull_request_template.md | 2 +- .github/workflows/dev-ci.yml | 35 ++++++++++++++++++----------- .github/workflows/stage-signoff.yml | 12 +++++----- Makefile | 2 +- scripts/create-pr.sh | 17 +++++++++----- 5 files changed, 42 insertions(+), 26 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index a40f4b66..15aec893 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,7 +6,7 @@ - [ ] Title follows Conventional Commits, e.g. `fix(updater): retry apt lock on timeout`; CI rejects anything else - [ ] Opened from a feature, bugfix or refactor branch against **dev** - [ ] Labels added and a reviewer requested -- [ ] `make check` and `make docstrcov` pass +- [ ] `make check` passes - [ ] CI results checked once the PR is open; failing checks mean changes requested # Description diff --git a/.github/workflows/dev-ci.yml b/.github/workflows/dev-ci.yml index 48d82fe1..4a796961 100644 --- a/.github/workflows/dev-ci.yml +++ b/.github/workflows/dev-ci.yml @@ -5,9 +5,8 @@ on: # A squash merge creates a SHA no PR run tested; stage can only require checks this run reports. push: branches: [dev] + # Every base, main included, so a hotfix PR is checked like any other. pull_request: - branches-ignore: - - main permissions: contents: read @@ -60,17 +59,6 @@ jobs: python -m pip install -r scripts/requirements-pip.txt pip install -r scripts/requirements.txt -r scripts/requirements-dev.txt - # The fleet installs wheels only (sdbus aside), so a pin without an aarch64 wheel passes CI yet breaks every box. - - name: Check pins have bookworm aarch64 wheels - if: matrix.test-type == 'security' - run: | - # Bookworm glibc is 2.36; pip does not expand manylinux tags downwards, so list each one. - plats=(--platform manylinux2014_aarch64) - for m in $(seq 17 36); do plats+=(--platform "manylinux_2_${m}_aarch64"); done - pip install --dry-run --quiet --ignore-installed --only-binary :all: "${plats[@]}" \ - --python-version 3.11 --implementation cp --abi cp311 \ - -r <(grep -v '^sdbus' scripts/requirements.txt) - - name: Run ${{ matrix.test-type }} env: TEST_TYPE: ${{ matrix.test-type }} @@ -96,6 +84,27 @@ jobs: ;; esac + # The fleet installs wheels only (sdbus packages aside), so a pin without an aarch64 wheel passes CI yet breaks every box. + # After the scan, not before, so a pin failure never hides bandit's result. + - name: Check pins are complete and have bookworm aarch64 wheels + if: ${{ !cancelled() && matrix.test-type == 'security' }} + run: | + # Bookworm glibc is 2.36; pip does not expand manylinux tags downwards, so list each one. + plats=(--platform manylinux2014_aarch64) + for m in $(seq 17 36); do plats+=(--platform "manylinux_2_${m}_aarch64"); done + pip install --dry-run --quiet --ignore-installed --only-binary :all: "${plats[@]}" \ + --python-version 3.11 --implementation cp --abi cp311 --report "$RUNNER_TEMP/closure.json" \ + -r <(grep -v '^sdbus' scripts/requirements.txt) + # Boxes resolve this file with plain pip, so a dependency missing from it floats to whatever is newest. + unpinned=$(jq -r --rawfile reqs scripts/requirements.txt ' + def norm: ascii_downcase | gsub("[-_.]+"; "-"); + [$reqs | splits("\n") | select(test("^[A-Za-z0-9]")) | split("==")[0] | norm] as $pinned + | .install[].metadata.name | norm | select(IN($pinned[]) | not)' "$RUNNER_TEMP/closure.json") + if [[ -n $unpinned ]]; then + echo "::error file=scripts/requirements.txt::Installed but not pinned with ==: ${unpinned//$'\n'/, }" + exit 1 + fi + - name: Upload ${{ matrix.test-type }} report if: always() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 diff --git a/.github/workflows/stage-signoff.yml b/.github/workflows/stage-signoff.yml index 8daf59ad..2c124f1b 100644 --- a/.github/workflows/stage-signoff.yml +++ b/.github/workflows/stage-signoff.yml @@ -8,10 +8,6 @@ on: permissions: {} -concurrency: - group: stage-signoff - cancel-in-progress: true - jobs: # No environment: it would wait for the approval it announces, so the webhook is a repository secret. notify: @@ -54,14 +50,18 @@ jobs: echo "::warning::HW_TEAMS_WEBHOOK is not set, the Hardware team was not notified" exit 0 fi - # Payload {subject, html} feeds a Power Automate flow that posts it to Teams and emails it via Outlook. - curl --fail-with-body -sS --retry 3 -H 'Content-Type: application/json' \ + # Payload {subject, html} feeds a Power Automate flow (Teams webhook trigger) that emails it via Outlook. + curl --fail-with-body -sS --retry 3 --max-time 30 -H 'Content-Type: application/json' \ --data-binary @payload.json "$WEBHOOK" signoff: runs-on: ubuntu-24.04 timeout-minutes: 5 environment: staging + # Only the newest stage tip needs sign-off; notify stays out so every push reports its own range. + concurrency: + group: stage-signoff + cancel-in-progress: true steps: - name: Record staging sign-off run: echo "Staging printers validated $GITHUB_SHA" diff --git a/Makefile b/Makefile index 1b7d6f71..268269ca 100644 --- a/Makefile +++ b/Makefile @@ -95,7 +95,7 @@ security: ## Run bandit security scan (tracked files only) @echo "bandit on $(words $(GIT_PY)) tracked files" @$(PYTHON) -m bandit -c pyproject.toml $(GIT_PY) -check: format-check lint security test-fast ## Full pre-push gate (mirrors CI) +check: format-check lint security test-fast docstrcov ## Full pre-push gate (mirrors CI) # ───────────────────────────────────────────────────────────────────────────── ##@ Tests diff --git a/scripts/create-pr.sh b/scripts/create-pr.sh index 3596141d..5df3ffdc 100755 --- a/scripts/create-pr.sh +++ b/scripts/create-pr.sh @@ -18,7 +18,7 @@ Opens a pull request for the current branch, step by step: 2. Title one line, type(scope)!: summary as enforced by the PR title lint; also the commit message when you chose to commit your uncommitted changes 3. Labels area label (type and breaking come from the title), reviewers, draft; you are the assignee - 4. Local gates optional make check + make docstrcov before anything leaves your machine + 4. Local gates optional make check before anything leaves your machine 5. Description the PR template in your editor, Changes prefilled from your commits 6. Review every commit and setting is shown; nothing is pushed until you type y @@ -38,6 +38,13 @@ else BOLD="" DIM="" RED="" GREEN="" YELLOW="" CYAN="" RESET="" fi +# The menus bind fzf's load event and pos() action, both 0.36+; older fzf gets the numbered lists. +USE_FZF=0 +if [[ -t 0 && -t 2 ]] && command -v fzf >/dev/null && + printf '%s\n' 0.36 "$(fzf --version | cut -d' ' -f1)" | sort -VC; then + USE_FZF=1 +fi + # All output goes to stderr so that functions can return values on stdout. say() { printf '%s\n' "$*" >&2; } ok() { say " ${GREEN}✔${RESET} $*"; } @@ -79,7 +86,7 @@ confirm() { menu() { local prompt=$1 default=$2 reply i local -a options=("${@:3}") - if command -v fzf >/dev/null && [[ -t 0 && -t 2 ]]; then + if ((USE_FZF)); then # The index travels hidden in field 1, so the visible label never has to be parsed back. reply=$(for i in "${!options[@]}"; do printf '%d\t%s\n' "$i" "${options[i]}"; done | fzf --height=~20 --layout=reverse --no-sort --delimiter='\t' --with-nth=2.. \ @@ -107,7 +114,7 @@ menu() { pick() { local prompt=$1 reply i joined local -a options=("${@:2}") chosen=() numbers=() - if command -v fzf >/dev/null && [[ -t 0 && -t 2 ]]; then + if ((USE_FZF)); then # Enter with nothing marked returns the line under the cursor, hence the "(none)" line on top. reply=$(printf '%s\n' "(none)" "${options[@]}" | fzf --multi --height=~20 --layout=reverse --no-sort \ @@ -377,8 +384,8 @@ if confirm "Open as a draft (not ready for review yet)?" n; then draft=yes; fi # ── 4. Local gates ─────────────────────────────────────────────────────────── step 4 "Local gates" gates="not run" -if confirm "Run make check and make docstrcov now (the CI gates, a few minutes)?" y; then - if make --no-print-directory check docstrcov; then +if confirm "Run make check now (the CI gates, a few minutes)?" y; then + if make --no-print-directory check; then # The gates ran on the working tree, so a dirty tree did not test exactly what is pushed. if ((dirty)); then gates="passed with uncommitted changes" From 566619da8af57e6e30987fefe1ece4f87136af7f Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Fri, 9 Oct 2026 17:15:13 +0100 Subject: [PATCH 05/10] chore(ci): keep every stage notify, check the pin closure, CI on all PRs --- .github/pull_request_template.md | 4 ++-- scripts/create-pr.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 15aec893..906dc4e5 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -11,8 +11,8 @@ # Description - [ ] Feature -- [ ] Bug fix -- [ ] Code refactor +- [ ] Bug Fix +- [ ] Code Refactor - [ ] Documentation