diff --git a/.github/actions/load-ci-image/action.yml b/.github/actions/load-ci-image/action.yml index 596335a8..263a1c11 100644 --- a/.github/actions/load-ci-image/action.yml +++ b/.github/actions/load-ci-image/action.yml @@ -1,12 +1,10 @@ name: 'Load note-c CI Docker image' +description: 'Download the note-c CI Docker image artifact and load it into the local Docker engine' runs: using: 'composite' steps: - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - name: Download image artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: note_c_ci_image path: /tmp diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..068c50b3 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +version: 2 + +updates: + - package-ecosystem: "github-actions" + directories: + - "/" + - "/.github/actions/*" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 5 + commit-message: + prefix: "chore(actions)" + groups: + actions-minor-patch: + update-types: + - "minor" + - "patch" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa31d998..8452eba3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,92 +8,101 @@ on: workflow_dispatch: inputs: force_build: - description: 'Force rebuild and publish Docker image (even if Dockerfile unchanged)' + description: 'Force rebuild of the Docker image (even if Dockerfile unchanged); only published when run on master' required: false type: boolean default: false +permissions: + contents: read + +# Master runs get a per-SHA group so a newer push can't cancel a pending one. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}${{ github.ref == 'refs/heads/master' && format('-{0}', github.sha) || '' }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} + jobs: check_dockerfile_changed: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: read + pull-requests: read outputs: changed: ${{ steps.filter.outputs.changed }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 # TODO: This is a 3rd party GitHub action from some dude. Ideally, we'd # use something more "official". - name: Check if Dockerfile changed - uses: dorny/paths-filter@v2 + uses: dorny/paths-filter@v4 id: filter with: - base: 'master' filters: | changed: - '.devcontainer/ci/Dockerfile' build_ci_docker_image: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 + timeout-minutes: 30 needs: [check_dockerfile_changed] if: ${{ needs.check_dockerfile_changed.outputs.changed == 'true' || inputs.force_build == true }} steps: - name: Checkout code - uses: actions/checkout@v4 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + uses: actions/checkout@v7 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v4 - name: Rebuild image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v7 with: context: . file: .devcontainer/ci/Dockerfile - load: true tags: ghcr.io/blues/note_c_ci:latest outputs: type=docker,dest=/tmp/note_c_ci_image.tar - name: Upload image artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: note_c_ci_image path: /tmp/note_c_ci_image.tar + retention-days: 7 check_architecture_docs: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 + timeout-minutes: 5 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Check architecture docs do not reference removed JSON map run: | architecture_json_pattern='architecture[.]''json' embed_script_pattern='embed-architecture''-json' for pattern in "$architecture_json_pattern" "$embed_script_pattern"; do - ! grep -R "$pattern" \ + rc=0 + grep -R "$pattern" \ AGENTS.md ARCHITECTURE.md CLAUDE.md GEMINI.md \ .github/copilot-instructions.md .cursor/rules/architecture-docs.mdc \ - docs + docs || rc=$? + [ "$rc" -eq 1 ] || exit 1 done build_docs: - runs-on: ubuntu-latest - if: ${{ always() }} + runs-on: ubuntu-24.04 + timeout-minutes: 10 + if: ${{ !cancelled() && (needs.build_ci_docker_image.result == 'success' || needs.build_ci_docker_image.result == 'skipped') }} needs: [build_ci_docker_image] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Load CI Docker image # Only load the Docker image artifact if build_ci_docker_image actually @@ -106,13 +115,14 @@ jobs: docker run --rm --volume $(pwd):/note-c/ --workdir /note-c/ --entrypoint ./scripts/build_docs.sh ghcr.io/blues/note_c_ci:latest check_libc_dependencies: - runs-on: ubuntu-latest - if: ${{ always() }} + runs-on: ubuntu-24.04 + timeout-minutes: 10 + if: ${{ !cancelled() && (needs.build_ci_docker_image.result == 'success' || needs.build_ci_docker_image.result == 'skipped') }} needs: [build_ci_docker_image] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Load CI Docker image # Only load the Docker image artifact if build_ci_docker_image actually @@ -125,11 +135,12 @@ jobs: docker run --rm --volume $(pwd):/note-c/ --workdir /note-c/ --entrypoint ./scripts/check_libc_dependencies.sh ghcr.io/blues/note_c_ci:latest run_integration_tests: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 + timeout-minutes: 10 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Configure add_subdirectory integration test run: cmake -S test/integration -B build-integration @@ -141,13 +152,14 @@ jobs: run: ctest --test-dir build-integration --output-on-failure run_unit_tests: - runs-on: ubuntu-latest - if: ${{ always() }} + runs-on: ubuntu-24.04 + timeout-minutes: 20 + if: ${{ !cancelled() && (needs.build_ci_docker_image.result == 'success' || needs.build_ci_docker_image.result == 'skipped') }} needs: [build_ci_docker_image] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Load CI Docker image # Only load the Docker image artifact if build_ci_docker_image actually @@ -167,19 +179,21 @@ jobs: run: sudo sed -i 's/\/note-c\///g' ./build/test/coverage/lcov.info - name: Publish test coverage - uses: coverallsapp/github-action@master + uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} - path-to-lcov: ./build/test/coverage/lcov.info + file: ./build/test/coverage/lcov.info + format: lcov run_low_mem_unit_tests: - runs-on: ubuntu-latest - if: ${{ always() }} + runs-on: ubuntu-24.04 + timeout-minutes: 20 + if: ${{ !cancelled() && (needs.build_ci_docker_image.result == 'success' || needs.build_ci_docker_image.result == 'skipped') }} needs: [build_ci_docker_image] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Load CI Docker image # Only load the Docker image artifact if build_ci_docker_image actually @@ -192,13 +206,14 @@ jobs: docker run --rm --volume $(pwd):/note-c/ --workdir /note-c/ --entrypoint ./scripts/run_unit_tests.sh ghcr.io/blues/note_c_ci:latest --mem-check --low-mem --single-precision run_astyle: - runs-on: ubuntu-latest - if: ${{ always() }} + runs-on: ubuntu-24.04 + timeout-minutes: 10 + if: ${{ !cancelled() && (needs.build_ci_docker_image.result == 'success' || needs.build_ci_docker_image.result == 'skipped') }} needs: [build_ci_docker_image] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Load CI Docker image # Only load the Docker image artifact if build_ci_docker_image actually @@ -211,13 +226,14 @@ jobs: docker run --rm --volume $(pwd):/note-c/ --workdir /note-c/ --entrypoint ./scripts/run_astyle.sh ghcr.io/blues/note_c_ci:latest run_cppcheck: - runs-on: ubuntu-latest - if: ${{ always() }} + runs-on: ubuntu-24.04 + timeout-minutes: 10 + if: ${{ !cancelled() && (needs.build_ci_docker_image.result == 'success' || needs.build_ci_docker_image.result == 'skipped') }} needs: [build_ci_docker_image] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Load CI Docker image # Only load the Docker image artifact if build_ci_docker_image actually @@ -234,10 +250,11 @@ jobs: # comparisons in n_cjson.c / n_ftoa.c use explicit (JNUMBER) casts so # AppleClang -Wimplicit-const-int-float-conversion does not fail -Werror. runs-on: macos-latest + timeout-minutes: 15 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Configure run: cmake -B build -DNOTE_C_BUILD_TESTS=ON @@ -257,21 +274,25 @@ jobs: # or using a different mocking approach on macOS. publish_ci_image: - runs-on: ubuntu-latest - # Make sure unit tests unit tests passed before publishing. - needs: [build_ci_docker_image, run_unit_tests] - # Only publish the image if this is a push event (or manual trigger with force_build) and the Docker image was rebuilt - if: ${{ (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.force_build == true)) && needs.build_ci_docker_image.result == 'success' }} + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: read + packages: write + # Make sure every job that uses the image passed before publishing. + needs: [build_ci_docker_image, build_docs, check_libc_dependencies, run_unit_tests, run_low_mem_unit_tests, run_astyle, run_cppcheck] + # Only publish from master, on a push (or manual trigger with force_build), and only if the Docker image was rebuilt. + if: ${{ github.ref == 'refs/heads/master' && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.force_build == true)) && needs.build_ci_docker_image.result == 'success' }} steps: - name: Download image artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: note_c_ci_image path: /tmp - name: Login to GitHub Container Registry - uses: docker/login-action@v2 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/md5srv-tests.yml b/.github/workflows/md5srv-tests.yml index 5da2365b..c94343a5 100644 --- a/.github/workflows/md5srv-tests.yml +++ b/.github/workflows/md5srv-tests.yml @@ -4,9 +4,13 @@ on: workflow_dispatch: workflow_call: # reusable workflow +permissions: + contents: read + jobs: test-md5srv: # job id - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 + timeout-minutes: 10 defaults: run: shell: bash @@ -14,27 +18,32 @@ jobs: TERM: xterm-256color MD5SRV_TIMEOUT: 5 MD5SRV_DIR: ./test/hitl/scripts - BATS_VERSION: 1.10.0 steps: - name: Setup Bats and bats libs id: setup-bats - uses: bats-core/bats-action@3.0.0 + uses: bats-core/bats-action@4.0.0 with: bats-install: true + bats-version: 1.14.0 + # Outside $HOME the libraries' cache can't be restored without root. + support-path: ${{ runner.temp }}/bats-support + assert-path: ${{ runner.temp }}/bats-assert file-install: false detik-install: false + github-token: ${{ github.token }} - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Run Tests + id: run-tests env: BATS_LIB_PATH: ${{ steps.setup-bats.outputs.lib-path }} run: | cd ${{env.MD5SRV_DIR}} bats -p --print-output-on-failure . - name: Re-run Tests - if: failure() + if: failure() && steps.run-tests.outcome == 'failure' env: BATS_LIB_PATH: ${{ steps.setup-bats.outputs.lib-path }} run: | cd ${{env.MD5SRV_DIR}} - $HOME/.local/bin/bats -p --print-output-on-failure -x . + bats -p --print-output-on-failure -x . diff --git a/.github/workflows/notecard-binary-tests.yml b/.github/workflows/notecard-binary-tests.yml index cc434e28..d093d4bc 100644 --- a/.github/workflows/notecard-binary-tests.yml +++ b/.github/workflows/notecard-binary-tests.yml @@ -33,11 +33,13 @@ jobs: notecard-binary-test: needs: md5srv-test - # Skip on fork PRs, nightly builds, etc. -> Secrets (Tailscale, Notehub, ngrok) aren't available. + # Skip where Actions secrets (Tailscale, Notehub, ngrok) aren't available: + # runs in forks of this repo, fork PRs, and Dependabot PRs. if: >- github.event.repository.fork != true && + github.actor != 'dependabot[bot]' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 # A healthy run reserves, flashes, and runs the suite in well under this. # The cap ensures a stuck serial monitor fails fast instead of burning the # default 6h job timeout. @@ -65,7 +67,7 @@ jobs: CREATE_NOTEHUB_ROUTES: true steps: - name: Connect to Tailscale - uses: tailscale/github-action@v3 + uses: tailscale/github-action@v4 with: oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} oauth-secret: ${{ secrets.TS_OAUTH_CLIENT_SECRET }} @@ -75,7 +77,7 @@ jobs: # Needed so we can download Notecard firmware from S3. - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: arn:aws:iam::163050375163:role/S3ReadAccessForHil_LabGitHubActions aws-region: us-east-1 @@ -83,12 +85,12 @@ jobs: # Needed to run PlatformIO (installed and invoked below). - name: Set up Python 3.11 - uses: actions/setup-python@v4 + uses: actions/setup-python@v7 with: python-version: '3.11' - name: Checkout note-c repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Generate MD5 Server Token run: | @@ -174,7 +176,7 @@ jobs: -H "Authorization: Bearer $NOTEHUB_ACCESS_TOKEN" -d @-` echo "Notehub HTTP route: $route" route_uid=`echo $route | jq -r .uid` - if [ -n "$route_uid" ]; then + if [ -n "$route_uid" ] && [ "$route_uid" != "null" ]; then echo "NOTEHUB_HTTP_ROUTE_UID=$route_uid" >> $GITHUB_ENV else echo "Failed to create or parse Notehub HTTP route." @@ -191,7 +193,7 @@ jobs: -H "Authorization: Bearer $NOTEHUB_ACCESS_TOKEN" -d @-` echo "Notehub proxy route: $route" route_uid=`echo "$route" | jq -r .uid` - if [ -n $route_uid ]; then + if [ -n "$route_uid" ] && [ "$route_uid" != "null" ]; then echo "NOTEHUB_PROXY_ROUTE_UID=$route_uid" >> $GITHUB_ENV echo "NOTEHUB_PROXY_ROUTE_ALIAS=$ALIAS" >> $GITHUB_ENV else @@ -308,7 +310,7 @@ jobs: kill $PID 2>/dev/null - name: Publish test report - uses: mikepenz/action-junit-report@v3 + uses: mikepenz/action-junit-report@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} if: env.GITHUB_TOKEN && (success() || failure()) # always run even if the previous step fails @@ -318,7 +320,7 @@ jobs: require_tests: true - name: Upload trace log - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: notecard-trace @@ -326,7 +328,7 @@ jobs: if-no-files-found: ignore - name: Upload MD5 server log - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: md5srv-log diff --git a/.github/workflows/publish_docs_site.yml b/.github/workflows/publish_docs_site.yml index b367659b..38ab0446 100644 --- a/.github/workflows/publish_docs_site.yml +++ b/.github/workflows/publish_docs_site.yml @@ -8,12 +8,19 @@ on: permissions: contents: write +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }} + cancel-in-progress: false + jobs: publish_docs_site: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 + timeout-minutes: 10 steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v3 + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: '3.x' - name: Install Python dependencies run: | @@ -31,7 +38,7 @@ jobs: TZ=UTC ./scripts/build_docs.sh - name: Deploy docs site to GitHub Pages - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 if: ${{ github.event_name == 'push' }} with: publish_branch: gh-pages diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 9be6e6f4..4e9aceaa 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -102,6 +102,17 @@ Use the existing unit test suite for core request, JSON, hook, and transport beh Before heavy Docker-based test runs in the OpenClaw workspace, follow the resource policy in the workspace `AGENTS.md` and run the resource check first. +### Continuous Integration + +CI runs on GitHub-hosted runners from `.github/workflows/`. Linux jobs are pinned to `ubuntu-24.04` so runner-image upgrades happen deliberately; the macOS job stays on `macos-latest`. + +- `ci.yml`: unit tests (with coverage and valgrind memcheck), low-memory/single-precision tests, astyle, cppcheck, the docs build, and the libc dependency check run inside the `ghcr.io/blues/note_c_ci` image built from `.devcontainer/ci/Dockerfile`. The `add_subdirectory` integration test and a build-only macOS job run directly on the runner. +- CI image: when the Dockerfile changes (or `force_build` is dispatched), the image is rebuilt and every image-consuming job tests that build. `:latest` is published only from `master`, and only after all of those jobs pass. +- `notecard-binary-tests.yml`: hardware-in-the-loop `card.binary` tests on a Notestation, run for same-repo PRs, nightly, and on demand. They need Actions secrets, so they are skipped for fork and Dependabot PRs; the secret-free `md5srv-tests.yml` still runs. +- `publish_docs_site.yml`: pushes to `master` publish the docs site to `gh-pages`. + +GitHub-hosted runners are free for this public repository. Namespace runners were evaluated in October 2026 and not adopted: the unit-test critical path is valgrind-bound and the hardware tests are device-bound, so faster runners would not materially shorten either. + ## Update Triggers Update this file or `docs/architecture/` when a change affects: