From cd77cea647cca0e8fd7a77f8fcdc4d283f6cb14d Mon Sep 17 00:00:00 2001 From: Ryan Cook Date: Thu, 8 Oct 2026 16:12:44 -0400 Subject: [PATCH] ci: skip runtime and image jobs for documentation-only changes Signed-off-by: Ryan Cook --- .github/workflows/ci-scope.yml | 28 ++++++ .github/workflows/docker-image.yml | 125 +++++++++++++++++---------- .github/workflows/host-artifacts.yml | 5 ++ .github/workflows/quadlet.yml | 5 ++ .github/workflows/sample-apps.yml | 8 +- .github/workflows/sops.yml | 5 ++ .github/workflows/unit-tests.yml | 7 ++ docs/release_notes.rst | 12 +++ scripts/ci-change-scope.py | 42 +++++++++ tests/ci/test_change_scope.py | 83 ++++++++++++++++++ 10 files changed, 273 insertions(+), 47 deletions(-) create mode 100644 .github/workflows/ci-scope.yml create mode 100644 scripts/ci-change-scope.py create mode 100644 tests/ci/test_change_scope.py diff --git a/.github/workflows/ci-scope.yml b/.github/workflows/ci-scope.yml new file mode 100644 index 00000000..3cdea8be --- /dev/null +++ b/.github/workflows/ci-scope.yml @@ -0,0 +1,28 @@ +name: CI change scope +on: + workflow_call: + outputs: + runtime: + description: Whether runtime and image checks are needed + value: ${{ jobs.scope.outputs.runtime }} +permissions: + contents: read +jobs: + scope: + runs-on: ubuntu-26.04 + timeout-minutes: 5 + outputs: + runtime: ${{ steps.scope.outputs.runtime }} + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + fetch-depth: 0 + - name: Test change classification + run: python3 -m unittest discover -s tests/ci -v + - name: Classify changed files + id: scope + env: + CI_EVENT: ${{ github.event_name }} + CI_BASE: ${{ github.event.pull_request.base.sha || github.event.before }} + CI_HEAD: ${{ github.event.pull_request.head.sha || github.sha }} + run: python3 scripts/ci-change-scope.py diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index ac5e6493..be686900 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -10,8 +10,13 @@ env: CGO_ENABLED: 0 jobs: + changes: + uses: ./.github/workflows/ci-scope.yml + build: + needs: [changes] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 @@ -38,6 +43,8 @@ jobs: path: /tmp/fetchit.tar build-ansible-amd: + needs: [changes] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -64,10 +71,10 @@ jobs: path: /tmp/ansible-amd.tar build-ansible-arm: + needs: [changes, build, raw-validate, fetchit-config-target-no-config-validate, fetchit-config-reload-validate, clean-validate, kube-validate, systemd-validate, systemd-enable-validate, systemd-user-enable-validate, systemd-autoupdate-validate, systemd-restart-validate, systemd-validate-exact-file, multi-engine-validate, make-change-to-repo, filetransfer-validate, filetransfer-validate-exact-file, ssh-validate, ansible-validate, loader-validate, disconnected-validate] + if: ${{ needs.changes.outputs.runtime == 'true' && (!cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled')) }} runs-on: ubuntu-26.04-arm - needs: [ build, raw-validate, fetchit-config-target-no-config-validate, fetchit-config-reload-validate, clean-validate, kube-validate, systemd-validate, systemd-enable-validate, systemd-user-enable-validate, systemd-autoupdate-validate, systemd-restart-validate, systemd-validate-exact-file, multi-engine-validate, make-change-to-repo, filetransfer-validate, filetransfer-validate-exact-file, ssh-validate, ansible-validate, loader-validate, disconnected-validate ] # PRs skip make-change-to-repo; failed or cancelled validation still blocks builds. - if: ${{ !cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -96,6 +103,8 @@ jobs: path: /tmp/ansible-arm.tar pull-and-archive: + needs: [changes] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -118,6 +127,8 @@ jobs: path: /tmp/colors.tar build-systemd-amd: + needs: [changes] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -144,10 +155,10 @@ jobs: path: /tmp/systemd-amd.tar build-systemd-arm: + needs: [changes, build, raw-validate, fetchit-config-target-no-config-validate, fetchit-config-reload-validate, clean-validate, kube-validate, systemd-validate, systemd-enable-validate, systemd-user-enable-validate, systemd-autoupdate-validate, systemd-restart-validate, systemd-validate-exact-file, multi-engine-validate, make-change-to-repo, filetransfer-validate, filetransfer-validate-exact-file, ssh-validate, ansible-validate, loader-validate, disconnected-validate] + if: ${{ needs.changes.outputs.runtime == 'true' && (!cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled')) }} runs-on: ubuntu-26.04-arm - needs: [ build, raw-validate, fetchit-config-target-no-config-validate, fetchit-config-reload-validate, clean-validate, kube-validate, systemd-validate, systemd-enable-validate, systemd-user-enable-validate, systemd-autoupdate-validate, systemd-restart-validate, systemd-validate-exact-file, multi-engine-validate, make-change-to-repo, filetransfer-validate, filetransfer-validate-exact-file, ssh-validate, ansible-validate, loader-validate, disconnected-validate ] # PRs skip make-change-to-repo; failed or cancelled validation still blocks builds. - if: ${{ !cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -176,8 +187,9 @@ jobs: path: /tmp/systemd-arm.tar fetchit-config-target-no-config-validate: + needs: [changes, build, pull-and-archive] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 - needs: [ build, pull-and-archive ] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -227,8 +239,9 @@ jobs: run: sudo podman ps fetchit-config-reload-validate: + needs: [changes, build, pull-and-archive] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 - needs: [ build, pull-and-archive ] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -278,8 +291,9 @@ jobs: run: sudo podman ps fetchit-config-PAT-validate: + needs: [changes, build, pull-and-archive] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 - needs: [ build, pull-and-archive ] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -332,8 +346,9 @@ jobs: run: sudo podman ps fetchit-PAT-kube-validate: + needs: [changes, build, pull-and-archive] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 - needs: [ build, pull-and-archive ] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -383,8 +398,9 @@ jobs: run: sudo podman ps raw-validate: + needs: [changes, build, pull-and-archive] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 - needs: [ build, pull-and-archive ] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -455,8 +471,9 @@ jobs: fi podman-secret-validate: + needs: [changes, build, pull-and-archive] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 - needs: [ build, pull-and-archive ] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -532,8 +549,9 @@ jobs: fi podman-config-secret-validate: + needs: [changes, build, pull-and-archive] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 - needs: [ build, pull-and-archive ] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -626,8 +644,9 @@ jobs: fi config-env-raw-validate: + needs: [changes, build, pull-and-archive] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 - needs: [ build, pull-and-archive ] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -680,8 +699,9 @@ jobs: - name: Verify cap2 runs as the non-root sample user run: test "$(sudo podman exec cap2 id -u)" = 1001 ssh-validate: + needs: [changes, build] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 - needs: [build] timeout-minutes: 12 steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -702,8 +722,9 @@ jobs: run: bash tests/integration/ssh/validate.sh filetransfer-validate: + needs: [changes, build, pull-and-archive] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 - needs: [ build, pull-and-archive ] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -744,8 +765,9 @@ jobs: run: sudo podman ps -a filetransfer-validate-exact-file: + needs: [changes, build, pull-and-archive] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 - needs: [ build, pull-and-archive ] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -783,8 +805,9 @@ jobs: run: sudo podman ps -a systemd-validate: + needs: [changes, build, build-systemd-amd] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 - needs: [ build, build-systemd-amd ] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -819,8 +842,9 @@ jobs: run: sudo podman ps -a systemd-autoupdate-validate: + needs: [changes, build, build-systemd-amd] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 - needs: [ build, build-systemd-amd ] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -904,8 +928,9 @@ jobs: run: sudo podman ps -a systemd-enable-validate: + needs: [changes, build, build-systemd-amd] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 - needs: [ build, build-systemd-amd ] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -955,8 +980,9 @@ jobs: run: timeout 150 bash -c -- 'sysd=inactive ; until [ $sysd = "active" ]; do sysd=$(sudo systemctl is-active httpd.service); done' systemd-user-enable-validate: + needs: [changes, build, build-systemd-amd] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 - needs: [ build, build-systemd-amd ] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -1013,8 +1039,9 @@ jobs: run: timeout 150 bash -c -- 'c=0 ; until [ $c -eq 1 ]; do c=$(podman ps | grep httpd | wc -l); done' systemd-restart-validate: + needs: [changes, build, build-systemd-amd] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 - needs: [ build, build-systemd-amd ] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -1067,8 +1094,9 @@ jobs: run: timeout 150 bash -c -- 'sysd=inactive ; until [ $sysd = "active" ]; do sysd=$(sudo systemctl is-active httpd.service); done' ansible-validate: + needs: [changes, build, build-ansible-amd] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 - needs: [ build, build-ansible-amd ] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -1124,8 +1152,9 @@ jobs: run: sudo podman logs fetchit systemd-validate-exact-file: + needs: [changes, build, pull-and-archive] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 - needs: [ build, pull-and-archive ] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -1163,8 +1192,9 @@ jobs: run: sudo podman ps -a kube-validate: + needs: [changes, build, pull-and-archive] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 - needs: [ build, pull-and-archive ] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -1202,8 +1232,9 @@ jobs: run: sudo podman ps clean-validate: + needs: [changes, build, pull-and-archive] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 - needs: [ build, pull-and-archive ] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -1256,8 +1287,9 @@ jobs: run: if [[ $(sudo podman image ls | grep alpine) ]] ; then exit 1; else echo "Image correctly removed" 1; fi glob-validate: + needs: [changes, build, pull-and-archive] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 - needs: [ build, pull-and-archive ] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -1307,8 +1339,9 @@ jobs: run: test "$(sudo podman exec cap1 id -u)" = 1001 multi-engine-validate: + needs: [changes, build, pull-and-archive] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 - needs: [ build, pull-and-archive ] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -1376,8 +1409,9 @@ jobs: run: if [[ $(sudo podman ps | grep -v CON= | grep colors | grep -v cap | wc -l) = "2" ]] ; then echo "Container successfully launched"; else exit 1; fi multi-engine-skew-validate: + needs: [changes, build, pull-and-archive] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 - needs: [ build, pull-and-archive ] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -1448,11 +1482,9 @@ jobs: run: if [[ $(sudo podman ps | grep -v CON= | grep -v cap | grep colors | wc -l) = "2" ]] ; then echo "Container successfully launched"; else exit 1; fi make-change-to-repo: + needs: [changes, build, pull-and-archive] + if: ${{ needs.changes.outputs.runtime == 'true' && ((github.event_name == 'push' || github.event_name == 'schedule') && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))) }} runs-on: ubuntu-26.04 - if: > - (github.event_name == 'push' || github.event_name == 'schedule') && - (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) - needs: [ build, pull-and-archive ] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: @@ -1543,8 +1575,9 @@ jobs: run: sudo podman logs fetchit loader-validate: + needs: [changes, build, pull-and-archive] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 - needs: [ build, pull-and-archive ] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -1600,8 +1633,9 @@ jobs: run: sudo podman logs fetchit disconnected-validate: + needs: [changes, build, build-ansible-amd, pull-and-archive] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 - needs: [ build, build-ansible-amd, pull-and-archive ] steps: - name: checkout with token uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -1689,11 +1723,9 @@ jobs: run: sudo podman logs fetchit push-amd-image-to-registry: + needs: [changes, build, raw-validate, podman-secret-validate, podman-config-secret-validate, fetchit-config-target-no-config-validate, fetchit-config-reload-validate, clean-validate, kube-validate, systemd-validate, systemd-enable-validate, systemd-user-enable-validate, systemd-autoupdate-validate, systemd-restart-validate, systemd-validate-exact-file, multi-engine-validate, make-change-to-repo, filetransfer-validate, filetransfer-validate-exact-file, ssh-validate, ansible-validate, loader-validate, disconnected-validate] + if: ${{ needs.changes.outputs.runtime == 'true' && ((github.event_name == 'push' || github.event_name == 'schedule') && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))) }} runs-on: ubuntu-26.04 - needs: [ build, raw-validate, podman-secret-validate, podman-config-secret-validate, fetchit-config-target-no-config-validate, fetchit-config-reload-validate, clean-validate, kube-validate, systemd-validate, systemd-enable-validate, systemd-user-enable-validate, systemd-autoupdate-validate, systemd-restart-validate, systemd-validate-exact-file, multi-engine-validate, make-change-to-repo, filetransfer-validate, filetransfer-validate-exact-file, ssh-validate, ansible-validate, loader-validate, disconnected-validate ] - if: > - (github.event_name == 'push' || github.event_name == 'schedule') && - (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - uses: ./.github/actions/setup-podman @@ -1719,10 +1751,10 @@ jobs: build-arm-and-manifest-list: + needs: [changes, build, raw-validate, podman-secret-validate, podman-config-secret-validate, fetchit-config-target-no-config-validate, fetchit-config-reload-validate, clean-validate, kube-validate, systemd-validate, systemd-enable-validate, systemd-user-enable-validate, systemd-autoupdate-validate, systemd-restart-validate, systemd-validate-exact-file, multi-engine-validate, make-change-to-repo, filetransfer-validate, filetransfer-validate-exact-file, ssh-validate, ansible-validate, loader-validate, disconnected-validate] + if: ${{ needs.changes.outputs.runtime == 'true' && (!cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled')) }} runs-on: ubuntu-26.04-arm - needs: [ build, raw-validate, podman-secret-validate, podman-config-secret-validate, fetchit-config-target-no-config-validate, fetchit-config-reload-validate, clean-validate, kube-validate, systemd-validate, systemd-enable-validate, systemd-user-enable-validate, systemd-autoupdate-validate, systemd-restart-validate, systemd-validate-exact-file, multi-engine-validate, make-change-to-repo, filetransfer-validate, filetransfer-validate-exact-file, ssh-validate, ansible-validate, loader-validate, disconnected-validate ] # PRs skip make-change-to-repo; failed or cancelled validation still blocks builds. - if: ${{ !cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - uses: ./.github/actions/setup-podman @@ -1750,8 +1782,9 @@ jobs: path: /tmp/fetchit-arm.tar push-manifest-list-to-registry: + needs: [changes, build-arm-and-manifest-list] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 - needs: [ build-arm-and-manifest-list ] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -1808,13 +1841,11 @@ jobs: podman manifest push --all --authfile ~/.docker/config.json --format=docker quay.io/fetchit/fetchit:latest docker://quay.io/fetchit/fetchit:latest publish-binary-to-github: + needs: [changes, build-arm-and-manifest-list] + if: ${{ needs.changes.outputs.runtime == 'true' && ((github.event_name == 'push' || github.event_name == 'schedule') && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))) }} permissions: contents: write runs-on: ubuntu-26.04 - needs: [ build-arm-and-manifest-list ] - if: > - (github.event_name == 'push' || github.event_name == 'schedule') && - (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - uses: ./.github/actions/setup-podman @@ -1858,8 +1889,9 @@ jobs: gh release upload latest ./fetchit-arm ./fetchit-amd --clobber create-ansible-manifest: + needs: [changes, ansible-validate, build-ansible-arm] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 - needs: [ ansible-validate, build-ansible-arm ] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -1916,8 +1948,9 @@ jobs: podman manifest push --all --authfile ~/.docker/config.json --format=docker quay.io/fetchit/fetchit-ansible:latest docker://quay.io/fetchit/fetchit-ansible:latest create-systemd-manifest: + needs: [changes, systemd-validate, systemd-enable-validate, systemd-user-enable-validate, systemd-autoupdate-validate, build-systemd-arm] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 - needs: [ systemd-validate, systemd-enable-validate, systemd-user-enable-validate, systemd-autoupdate-validate, build-systemd-arm ] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 diff --git a/.github/workflows/host-artifacts.yml b/.github/workflows/host-artifacts.yml index d3167761..1c2ac426 100644 --- a/.github/workflows/host-artifacts.yml +++ b/.github/workflows/host-artifacts.yml @@ -7,7 +7,12 @@ on: permissions: contents: read jobs: + changes: + uses: ./.github/workflows/ci-scope.yml + lifecycle: + needs: [changes] + if: ${{ needs.changes.outputs.runtime == 'true' }} strategy: fail-fast: false matrix: diff --git a/.github/workflows/quadlet.yml b/.github/workflows/quadlet.yml index 0091e7b5..a50e9a24 100644 --- a/.github/workflows/quadlet.yml +++ b/.github/workflows/quadlet.yml @@ -10,7 +10,12 @@ permissions: contents: read jobs: + changes: + uses: ./.github/workflows/ci-scope.yml + lifecycle: + needs: [changes] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 timeout-minutes: 25 strategy: diff --git a/.github/workflows/sample-apps.yml b/.github/workflows/sample-apps.yml index a593ab4d..1a6a8b55 100644 --- a/.github/workflows/sample-apps.yml +++ b/.github/workflows/sample-apps.yml @@ -13,7 +13,12 @@ permissions: contents: read jobs: + changes: + uses: ./.github/workflows/ci-scope.yml + samples: + needs: [changes] + if: ${{ needs.changes.outputs.runtime == 'true' }} strategy: fail-fast: false matrix: @@ -63,7 +68,8 @@ jobs: sudo journalctl -u podman.service --no-pager -n 100 assemble: - needs: samples + needs: [changes, samples] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 timeout-minutes: 15 steps: diff --git a/.github/workflows/sops.yml b/.github/workflows/sops.yml index a7d3b6c4..bf052d04 100644 --- a/.github/workflows/sops.yml +++ b/.github/workflows/sops.yml @@ -7,7 +7,12 @@ on: permissions: contents: read jobs: + changes: + uses: ./.github/workflows/ci-scope.yml + lifecycle: + needs: [changes] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 timeout-minutes: 25 strategy: diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index e45dbfb1..f1fcceba 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -10,7 +10,12 @@ permissions: contents: read jobs: + changes: + uses: ./.github/workflows/ci-scope.yml + test: + needs: [changes] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 env: CGO_ENABLED: '1' @@ -39,6 +44,8 @@ jobs: run: go test -race -mod=readonly -tags "$BUILDTAGS" ./pkg/engine -run 'TestStatus|TestMirror|TestCommonGit|TestHelperContainer|TestRetirement|TestConfigReloadCan|TestRollback|TestDisconnected|TestFileTransfer' fedora: + needs: [changes] + if: ${{ needs.changes.outputs.runtime == 'true' }} runs-on: ubuntu-26.04 container: quay.io/fedora/fedora:44 env: diff --git a/docs/release_notes.rst b/docs/release_notes.rst index 3cbcaaac..4c241a87 100644 --- a/docs/release_notes.rst +++ b/docs/release_notes.rst @@ -4,6 +4,18 @@ Release notes Unreleased ---------- +Documentation-only CI +~~~~~~~~~~~~~~~~~~~~~~ + +Docs-only pull requests and main pushes now skip Go, image-build, and Podman +runtime jobs. A shared lightweight change check classifies ``docs/**``, root +Markdown files, and ``.readthedocs.yml``/``.readthedocs.yaml`` as documentation. +The strict Sphinx job continues to run. Code, examples, dependency files, scripts, +workflow/action changes, and unknown paths keep the full runtime checks, including +mixed code/docs changes. Scheduled/manual sample checks still run. Missing Git +history conservatively runs tests. Job-level skips preserve check results without +workflow-level path filtering. + Trailing slashes in Git target paths ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/scripts/ci-change-scope.py b/scripts/ci-change-scope.py new file mode 100644 index 00000000..caa5099a --- /dev/null +++ b/scripts/ci-change-scope.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +"""Conservatively decide whether a GitHub event needs runtime/image jobs.""" +import os +import subprocess + + +def documentation_only(paths): + return bool(paths) and all( + path.startswith("docs/") + or path in {".readthedocs.yml", ".readthedocs.yaml"} + or ("/" not in path and path.endswith(".md")) + for path in paths + ) + + +def runtime_required(event, base, head): + if event not in {"pull_request", "push"} or not base or not head: + return True + try: + if event == "pull_request": + base = subprocess.check_output( + ["git", "merge-base", base, head], text=True + ).strip() + paths = subprocess.check_output( + ["git", "diff", "--name-only", "--no-renames", "-z", base, head] + ).decode("utf-8", errors="surrogateescape").split("\0") + except subprocess.CalledProcessError: + # Missing history or an unresolvable push baseline must not skip tests. + return True + return not documentation_only([path for path in paths if path]) + + +if __name__ == "__main__": + runtime = runtime_required( + os.environ.get("CI_EVENT", ""), + os.environ.get("CI_BASE", ""), + os.environ.get("CI_HEAD", ""), + ) + result = "true" if runtime else "false" + print(f"Runtime/image checks required: {result}") + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output: + output.write(f"runtime={result}\n") diff --git a/tests/ci/test_change_scope.py b/tests/ci/test_change_scope.py new file mode 100644 index 00000000..f0a4f849 --- /dev/null +++ b/tests/ci/test_change_scope.py @@ -0,0 +1,83 @@ +import importlib.util +from pathlib import Path +import subprocess +import os +import tempfile +import unittest +from unittest.mock import patch + +spec = importlib.util.spec_from_file_location( + "scope", Path(__file__).resolve().parents[2] / "scripts/ci-change-scope.py" +) +scope = importlib.util.module_from_spec(spec) +spec.loader.exec_module(scope) + + +class ChangeScopeTests(unittest.TestCase): + def test_documentation(self): + self.assertTrue(scope.documentation_only([ + "docs/methods.rst", "docs/conf.py", "README.md", ".readthedocs.yml" + ])) + + def test_runtime_inputs_and_mixed_changes(self): + for path in ["pkg/engine/kube.go", "go.mod", "go.sum", "Dockerfile", + "examples/readme.md", "examples/kube/pod.yaml", "scripts/build.sh", + ".github/workflows/docs.yml", ".github/actions/setup-podman/action.yml", + "tests/ci/test_change_scope.py", "unknown-file"]: + with self.subTest(path=path): + self.assertFalse(scope.documentation_only(["docs/index.rst", path])) + self.assertFalse(scope.documentation_only([])) + + def test_manual_scheduled_and_missing_history_run(self): + for event in ["schedule", "workflow_dispatch", "unknown"]: + self.assertTrue(scope.runtime_required(event, "base", "head")) + self.assertTrue(scope.runtime_required("push", "", "head")) + with patch.object(scope.subprocess, "check_output", side_effect=subprocess.CalledProcessError(1, "git")): + self.assertTrue(scope.runtime_required("push", "missing", "head")) + + def test_null_delimiters_and_renames(self): + with patch.object(scope.subprocess, "check_output", return_value=b"docs/a\nb.rst\0README.md\0") as command: + self.assertFalse(scope.runtime_required("push", "base", "head")) + self.assertIn("--no-renames", command.call_args.args[0]) + # Moving a runtime file into docs must still exercise runtime checks. + with patch.object(scope.subprocess, "check_output", return_value=b"pkg/old.go\0docs/old.go\0"): + self.assertTrue(scope.runtime_required("push", "base", "head")) + + def test_real_git_diff_docs_mixed_and_code_to_docs_rename(self): + with tempfile.TemporaryDirectory() as directory: + previous = os.getcwd() + try: + os.chdir(directory) + def git(*args): + return subprocess.check_output(["git", *args], stderr=subprocess.DEVNULL, text=True).strip() + git("init") + git("config", "user.name", "CI test") + git("config", "user.email", "ci@example.invalid") + Path("docs").mkdir() + Path("docs/index.rst").write_text("before") + Path("runtime.go").write_text("package engine") + git("add", ".") + git("commit", "-m", "base") + base = git("rev-parse", "HEAD") + Path("docs/index.rst").write_text("after") + git("add", ".") + git("commit", "-m", "docs") + docs = git("rev-parse", "HEAD") + self.assertFalse(scope.runtime_required("push", base, docs)) + self.assertFalse(scope.runtime_required("pull_request", base, docs)) + git("mv", "runtime.go", "docs/runtime.go") + git("commit", "-m", "move runtime into docs") + self.assertTrue(scope.runtime_required("push", docs, git("rev-parse", "HEAD"))) + self.assertTrue(scope.runtime_required("pull_request", base, git("rev-parse", "HEAD"))) + finally: + os.chdir(previous) + + def test_pull_request_uses_merge_base(self): + with patch.object(scope.subprocess, "check_output", side_effect=["ancestor\n", b"docs/index.rst\0"]) as command: + self.assertFalse(scope.runtime_required("pull_request", "base", "head")) + self.assertEqual(command.call_args_list[0].args[0], ["git", "merge-base", "base", "head"]) + self.assertEqual(command.call_args_list[1].args[0][-2:], ["ancestor", "head"]) + + +if __name__ == "__main__": + unittest.main()