From e0de0a5423cedf3e9e134687f890fbee815fa438 Mon Sep 17 00:00:00 2001 From: "Zachary J. Fields (OpenClaw)" <215570245+zakoverflow@users.noreply.github.com> Date: Thu, 8 Oct 2026 12:31:16 -0500 Subject: [PATCH 1/7] Fix md5srv HIL job failing with HTTP 403 while installing Bats. - The md5srv-test job (the first job of the nightly card.binary HIL workflow) failed in "Setup Bats and bats libs" with "curl: (22) The requested URL returned error: 403". The card.binary hardware job needs md5srv-test, so it was skipped. - The workflow does not pass bats-version to bats-core/bats-action, so the action looks up the latest Bats release through api.github.com/repos/bats-core/bats-core/releases/latest. The BATS_VERSION env var in this workflow is not read by the action. - bats-action 3.0.0 makes that lookup without a token. Unauthenticated API calls share a 60 requests/hour limit per source IP, and GitHub answers with 403 once the limit is used up. curl -f hides the response body, so the log can't confirm the reason, but this is the likely one. - bats-action 3.0.1 sends the job's GITHUB_TOKEN with the lookup and the tarball download. That is the upstream fix for this lookup. Bump to 3.0.1. No other change: same inputs, same Bats version resolution, same libraries. - Checked locally: actionlint reports no issues in md5srv-tests.yml, and the 3.0.1 install logic resolves and installs Bats v1.14.0, after which the md5srv bats suite passes (14 tests, 0 failures). --- .github/workflows/md5srv-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/md5srv-tests.yml b/.github/workflows/md5srv-tests.yml index 5da2365b..653ad9cd 100644 --- a/.github/workflows/md5srv-tests.yml +++ b/.github/workflows/md5srv-tests.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Setup Bats and bats libs id: setup-bats - uses: bats-core/bats-action@3.0.0 + uses: bats-core/bats-action@3.0.1 with: bats-install: true file-install: false From b75e22880cdd30ba5bdb4acf1a79b24d31285311 Mon Sep 17 00:00:00 2001 From: "Zachary J. Fields (OpenClaw)" <215570245+zakoverflow@users.noreply.github.com> Date: Thu, 8 Oct 2026 12:34:57 -0500 Subject: [PATCH 2/7] Pin Bats to 1.14.0 in the md5srv HIL job. - Pass bats-version: 1.14.0 to bats-core/bats-action. With an explicit version the action skips its api.github.com releases/latest lookup, so the install no longer depends on GitHub API rate limits at all. - 1.14.0 is the version the nightly has been installing as "latest", so this does not change which Bats the tests run on. - Remove the BATS_VERSION: 1.10.0 env var. The action never read it, and its value did not match the Bats version actually in use. - Keep bats-action 3.0.1. Its token on the tarball download stays as defense in depth. --- .github/workflows/md5srv-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/md5srv-tests.yml b/.github/workflows/md5srv-tests.yml index 653ad9cd..4a73e845 100644 --- a/.github/workflows/md5srv-tests.yml +++ b/.github/workflows/md5srv-tests.yml @@ -14,13 +14,13 @@ 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.1 with: bats-install: true + bats-version: 1.14.0 file-install: false detik-install: false - name: Checkout From 85326ec3104b003879ea748ece5aa85309168118 Mon Sep 17 00:00:00 2001 From: Rob Lauer Date: Mon, 19 Oct 2026 15:15:52 -0500 Subject: [PATCH 3/7] ci: fix workflow bugs, bump actions, and tighten permissions Bug fixes: - check_architecture_docs: `! grep` skips errexit, so only the last pattern was ever enforced. Check grep's status explicitly and fail on a match or on a missing path. - publish_ci_image: a force_build dispatch from any branch pushed ghcr.io/blues/note_c_ci:latest. Gate on master and require every image-consuming job to pass, not just run_unit_tests. - Image-consuming jobs used `if: always()`, which runs even after cancellation and tests against the stale :latest image when the image build fails. Run only when the image build succeeded or was skipped. - md5srv "Re-run Tests" invoked $HOME/.local/bin/bats, which bats-action never installs, so the retry could not run. Use bats from PATH and only retry when the test step itself failed. - HIL route checks: `[ -n $route_uid ]` was always true and neither check caught jq's "null". Quote and reject "null". - Coveralls was pinned to the moving `master` branch; use v2 with `file`. Hygiene: - ci.yml concurrency: cancel superseded PR runs; master runs get a per-SHA group so a pending run is never cancelled. - timeout-minutes on every job; least-privilege permissions. - Drop the unused buildx setup from the load-ci-image composite action, and the unused ghcr login and redundant `load: true` from the image build. - Add dependabot for GitHub Actions (workflows and composite actions). Action bumps: checkout v7, setup-python v7, upload-artifact v7, download-artifact v8, paths-filter v4, docker login/setup-buildx v4, build-push v7, bats-action 4.0.0 (with github-token), tailscale v4, configure-aws-credentials v6, action-junit-report v6, actions-gh-pages v4. Co-Authored-By: Claude Opus 5.5 --- .github/actions/load-ci-image/action.yml | 6 +- .github/dependabot.yml | 18 ++++ .github/workflows/ci.yml | 102 ++++++++++++-------- .github/workflows/md5srv-tests.yml | 14 ++- .github/workflows/notecard-binary-tests.yml | 18 ++-- .github/workflows/publish_docs_site.yml | 13 ++- 6 files changed, 111 insertions(+), 60 deletions(-) create mode 100644 .github/dependabot.yml 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..923d655a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,25 +8,37 @@ 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 + 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' @@ -36,64 +48,62 @@ jobs: build_ci_docker_image: runs-on: ubuntu-latest + 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 + 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() }} + 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 @@ -107,12 +117,13 @@ jobs: check_libc_dependencies: runs-on: ubuntu-latest - if: ${{ always() }} + 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 @@ -126,10 +137,11 @@ jobs: run_integration_tests: runs-on: ubuntu-latest + 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 @@ -142,12 +154,13 @@ jobs: run_unit_tests: runs-on: ubuntu-latest - if: ${{ always() }} + 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 +180,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() }} + 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 @@ -193,12 +208,13 @@ jobs: run_astyle: runs-on: ubuntu-latest - if: ${{ always() }} + 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 @@ -212,12 +228,13 @@ jobs: run_cppcheck: runs-on: ubuntu-latest - if: ${{ always() }} + 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 +251,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 @@ -258,20 +276,24 @@ jobs: 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' }} + 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 4a73e845..9a77f188 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 + timeout-minutes: 10 defaults: run: shell: bash @@ -17,24 +21,26 @@ jobs: steps: - name: Setup Bats and bats libs id: setup-bats - uses: bats-core/bats-action@3.0.1 + uses: bats-core/bats-action@4.0.0 with: bats-install: true bats-version: 1.14.0 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..f9716df3 100644 --- a/.github/workflows/notecard-binary-tests.yml +++ b/.github/workflows/notecard-binary-tests.yml @@ -65,7 +65,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 +75,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 +83,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 +174,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 +191,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 +308,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 +318,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 +326,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..c5d39ad4 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 + 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 From f6ab6beac27f3ba5a1d1383e5e865f982fa91acd Mon Sep 17 00:00:00 2001 From: Rob Lauer Date: Mon, 19 Oct 2026 15:41:43 -0500 Subject: [PATCH 4/7] ci: skip the hardware HIL job on Dependabot PRs Dependabot PR runs get no Actions secrets (only Dependabot secrets, and none are configured), so the Tailscale, Notehub and ngrok steps would fail. The secret-free md5srv tests still run. Re-runs keep github.actor, so they stay skipped; a maintainer can validate HIL changes on a Dependabot branch with workflow_dispatch. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/notecard-binary-tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/notecard-binary-tests.yml b/.github/workflows/notecard-binary-tests.yml index f9716df3..da8dd916 100644 --- a/.github/workflows/notecard-binary-tests.yml +++ b/.github/workflows/notecard-binary-tests.yml @@ -33,9 +33,11 @@ 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 # A healthy run reserves, flashes, and runs the suite in well under this. From dd732dda55afcd63661336f8880ca379df650211 Mon Sep 17 00:00:00 2001 From: Rob Lauer Date: Mon, 19 Oct 2026 15:41:43 -0500 Subject: [PATCH 5/7] docs: record the CI strategy in ARCHITECTURE.md Add a Continuous Integration subsection under Testing Strategy covering what runs where, the CI image publication policy, when the hardware tests run or are skipped, and why CI stays on GitHub-hosted runners. Co-Authored-By: Claude Opus 5.5 --- ARCHITECTURE.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 9be6e6f4..2caf48d8 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/`: + +- `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: From b03e64c454ce2000d7df5425f7e94f27f2b64420 Mon Sep 17 00:00:00 2001 From: Rob Lauer Date: Mon, 19 Oct 2026 15:57:02 -0500 Subject: [PATCH 6/7] ci: pin Linux jobs to ubuntu-24.04 and drop the ignored paths-filter base ubuntu-latest starts moving to Ubuntu 26 on 2026-10-19. The host-run jobs (HIL apt packages including ngrok's buster repo, Python 3.11, the docs build's doxygen, the integration test's compiler) are exposed to that change, so pin every Linux job to ubuntu-24.04 (the image ubuntu-latest resolves to today) and move to 26.04 deliberately later. macOS stays on macos-latest. paths-filter ignores `base` on pull_request events and v4 warns about it. On push and dispatch an unset base falls back to the default branch (master), so removing it changes nothing. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 23 ++++++++++----------- .github/workflows/md5srv-tests.yml | 2 +- .github/workflows/notecard-binary-tests.yml | 2 +- .github/workflows/publish_docs_site.yml | 2 +- ARCHITECTURE.md | 2 +- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 923d655a..8452eba3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ concurrency: jobs: check_dockerfile_changed: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 5 permissions: contents: read @@ -41,13 +41,12 @@ jobs: 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 }} @@ -75,7 +74,7 @@ jobs: retention-days: 7 check_architecture_docs: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 5 steps: @@ -96,7 +95,7 @@ jobs: done build_docs: - runs-on: ubuntu-latest + 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] @@ -116,7 +115,7 @@ 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 + 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] @@ -136,7 +135,7 @@ 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: @@ -153,7 +152,7 @@ jobs: run: ctest --test-dir build-integration --output-on-failure run_unit_tests: - runs-on: ubuntu-latest + 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] @@ -187,7 +186,7 @@ jobs: format: lcov run_low_mem_unit_tests: - runs-on: ubuntu-latest + 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] @@ -207,7 +206,7 @@ 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 + 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] @@ -227,7 +226,7 @@ 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 + 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] @@ -275,7 +274,7 @@ jobs: # or using a different mocking approach on macOS. publish_ci_image: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 permissions: contents: read diff --git a/.github/workflows/md5srv-tests.yml b/.github/workflows/md5srv-tests.yml index 9a77f188..0336966d 100644 --- a/.github/workflows/md5srv-tests.yml +++ b/.github/workflows/md5srv-tests.yml @@ -9,7 +9,7 @@ permissions: jobs: test-md5srv: # job id - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 10 defaults: run: diff --git a/.github/workflows/notecard-binary-tests.yml b/.github/workflows/notecard-binary-tests.yml index da8dd916..d093d4bc 100644 --- a/.github/workflows/notecard-binary-tests.yml +++ b/.github/workflows/notecard-binary-tests.yml @@ -39,7 +39,7 @@ jobs: 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. diff --git a/.github/workflows/publish_docs_site.yml b/.github/workflows/publish_docs_site.yml index c5d39ad4..38ab0446 100644 --- a/.github/workflows/publish_docs_site.yml +++ b/.github/workflows/publish_docs_site.yml @@ -14,7 +14,7 @@ concurrency: jobs: publish_docs_site: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 10 steps: - uses: actions/checkout@v7 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 2caf48d8..4e9aceaa 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -104,7 +104,7 @@ Before heavy Docker-based test runs in the OpenClaw workspace, follow the resour ### Continuous Integration -CI runs on GitHub-hosted runners from `.github/workflows/`: +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. From c4eff52d2153d160b2d23e1b819f74f12f8d7352 Mon Sep 17 00:00:00 2001 From: Rob Lauer Date: Mon, 19 Oct 2026 16:10:07 -0500 Subject: [PATCH 7/7] ci: install bats libraries under runner.temp so their cache restores bats-action defaults bats-support/bats-assert to /usr/lib, and the cache restore runs tar as the unprivileged runner user, so every run logged "Failed to restore ... tar exit code 2" and re-downloaded the libraries. bats-action's README documents this and recommends a path under $HOME. runner.temp qualifies and sits outside the workspace, so the later checkout isn't affected by the root-owned install. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/md5srv-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/md5srv-tests.yml b/.github/workflows/md5srv-tests.yml index 0336966d..c94343a5 100644 --- a/.github/workflows/md5srv-tests.yml +++ b/.github/workflows/md5srv-tests.yml @@ -25,6 +25,9 @@ jobs: 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 }}