From c1275199195c2d629e467ec4ff541ff2da2a98b5 Mon Sep 17 00:00:00 2001 From: Arnout Engelen Date: Thu, 12 Feb 2026 11:42:39 +0100 Subject: [PATCH 01/10] setup-bazel: remove wildcard allowlisting incorrectly introduced in https://github.com/apache/infrastructure-actions/pull/419/changes Also adding 0.18.0 as used in https://github.com/apache/skywalking-data-collect-protocol/blob/master/.github/workflows/ci.yaml --- actions.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/actions.yml b/actions.yml index 5084fa02..a0592776 100644 --- a/actions.yml +++ b/actions.yml @@ -168,11 +168,11 @@ azure/setup-helm: 1a275c3b69536ee54be43f2070a358922e12c8d4: tag: v4.3.1 bazel-contrib/setup-bazel: - '*': - expires_at: 2026-12-21 - keep: true 8d2cb86a3680a820c3e219597279ce3f80d17a47: - tag: v0.15.0 + tag: 0.15.0 + expires_at: 2026-03-31 + 083175551ceeceebc757ebee2127fde78840ca77: + tag: 0.18.0 betahuhn/repo-file-sync-action: 8b92be3375cf1d1b0cd579af488a9255572e4619: tag: v1.21.1 From d1b0e7b07c1081b903b56e1778888b4fb253d8da Mon Sep 17 00:00:00 2001 From: Arnout Engelen Date: Thu, 12 Feb 2026 12:10:45 +0100 Subject: [PATCH 02/10] clean up remaining 'no expiration' follow-up on #363, apparently missed some --- actions.yml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/actions.yml b/actions.yml index a0592776..bf0406de 100644 --- a/actions.yml +++ b/actions.yml @@ -363,7 +363,6 @@ google-github-actions/setup-gcloud: tag: v3.0.1 gradle/actions/setup-gradle: 4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2: - expires_at: 2050-01-01 tag: v5.0.0 gr2m/twitter-together: '*': @@ -429,7 +428,6 @@ jidicula/clang-format-action: keep: true Jimver/cuda-toolkit: 6008063726ffe3309d1b22e413d9e88fed91a2f2: - expires_at: 2050-01-01 tag: v0.2.29 jlumbroso/free-disk-space: '*': @@ -757,14 +755,11 @@ slackapi/slack-github-action: 91efab103c0de0a537f72a35f6b8cda0ee76bf0a: tag: v2.1.1 golangci/golangci-lint-action: - 1481404843c368bc19ca9406f87d6e0fc97bdcfd: - expires_at: 2050-08-01 - tag: v7.0.0 - keep: true 55c2c1448f86e01eaae002a5a3a9624417608d84: - expires_at: 2050-08-01 tag: v6.5.2 - keep: true + expires_at: 2026-04-24 + 1481404843c368bc19ca9406f87d6e0fc97bdcfd: + tag: v7.0.0 mlugg/setup-zig: 8d6198c65fb0feaa111df26e6b467fea8345e46f: tag: v2.0.5 From 5b42b9792155b6694a7d954ec41ba3beee8204f3 Mon Sep 17 00:00:00 2001 From: Arnout Engelen Date: Thu, 12 Feb 2026 12:30:37 +0100 Subject: [PATCH 03/10] avoid having both 'keep' and 'expires_at' Remove 'expires_at' if there is also 'keep', since 'keep' currently takes precendence. We'll want to remove the 'keep' entries for most of these cases eventually, but let's do that gradually to reduce impact. See also https://github.com/apache/infrastructure-actions/issues/252 --- actions.yml | 113 ---------------------------------------------------- 1 file changed, 113 deletions(-) diff --git a/actions.yml b/actions.yml index bf0406de..86fc1d3b 100644 --- a/actions.yml +++ b/actions.yml @@ -24,17 +24,11 @@ astral-sh/setup-uv: expires_at: 2026-03-15 681c641aba71e4a1c380be3ab5e12ad51f415867: tag: v7.1.6 -bytedeco/javacpp-presets/.github/actions/*: - '*': - expires_at: 2025-08-01 - keep: true dtolnay/rust-toolchain: stable: - expires_at: 2025-08-01 keep: true golangci/*: '*': - expires_at: 2025-08-01 keep: true pypa/gh-action-pip-audit: 1220774d901786e6f652ae159f7b6bc8fea6d266: @@ -42,39 +36,30 @@ pypa/gh-action-pip-audit: expires_at: 2026-03-31 pypa/gh-action-pypi-publish: release/v1*: - expires_at: 2025-08-01 keep: true pytooling/actions/with-post-step: '*': - expires_at: 2025-08-01 keep: true quarto-dev/quarto-actions/*: '*': - expires_at: 2025-08-01 keep: true r-lib/actions/*: '*': - expires_at: 2025-08-01 keep: true readthedocs/actions/preview: '*': - expires_at: 2025-08-01 keep: true rustsec/*: '*': - expires_at: 2025-08-01 keep: true vhelm/chart-releaser-action: '*': - expires_at: 2025-08-01 keep: true AdoptOpenJDK/install-jdk: '*': - expires_at: 2025-08-01 keep: true BobAnkh/auto-generate-changelog: '*': - expires_at: 2025-08-01 keep: true dorny/test-reporter: dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3: @@ -93,7 +78,6 @@ DavidAnson/markdownlint-cli2-action: tag: v22.0.0 EnricoMi/publish-unit-test-result-action: '*': - expires_at: 2025-08-01 keep: true JamesIves/github-pages-deploy-action: 6c2d9db40f9296374acc17b90404b6e8864128c8: @@ -115,11 +99,9 @@ Kesin11/actions-timeline: tag: v2.2.5 PyO3/maturin-action: '*': - expires_at: 2025-08-01 keep: true TobKed/label-when-approved-action: '*': - expires_at: 2025-08-01 keep: true VirtusLab/scala-cli-setup: 68bd9c30954d20e6cb6ddaf01b3b38336f25df4b: @@ -127,15 +109,12 @@ VirtusLab/scala-cli-setup: tag: v1.10.1 actions-cool/check-user-permission: '*': - expires_at: 2025-08-01 keep: true actions-cool/issues-helper: '*': - expires_at: 2025-08-01 keep: true actions-cool/maintain-one-comment: '*': - expires_at: 2025-08-01 keep: true addnab/docker-run-action: 4f65fabd2431ebc8d299f8e5a018d79a769ae185: @@ -148,15 +127,12 @@ al-cheb/configure-pagefile-action: tag: v1.5 amannn/action-semantic-pull-request: '*': - expires_at: 2025-08-01 keep: true arduino/setup-protoc: '*': - expires_at: 2025-08-01 keep: true awalsh128/cache-apt-pkgs-action: '*': - expires_at: 2025-08-01 keep: true aws-actions/configure-aws-credentials: 00943011d9042930efac3dcd3a170e4273319bc8: @@ -189,15 +165,12 @@ browser-actions/setup-geckodriver: 5ef1526ed36211ab6cb531ec1cfb11f924ca2dee: bufbuild/buf-breaking-action: '*': - expires_at: 2025-08-01 keep: true bufbuild/buf-lint-action: '*': - expires_at: 2025-08-01 keep: true bufbuild/buf-setup-action: '*': - expires_at: 2025-08-01 keep: true burnett01/rsync-deployments: 0dc935cdecc5f5e571865e60d2a6cdc673704823: @@ -207,46 +180,36 @@ burnett01/rsync-deployments: tag: 8.0.3 burrunan/gradle-cache-action: '*': - expires_at: 2025-08-01 keep: true bytedeco/javacpp-presets: '*': - expires_at: 2025-08-01 keep: true carloscastrojumo/github-cherry-pick-action: 503773289f4a459069c832dc628826685b75b4b3: tag: v1.0.10 carlosperate/arm-none-eabi-gcc-action: '*': - expires_at: 2025-08-01 keep: true check-spelling/check-spelling: '*': - expires_at: 2025-08-01 keep: true chromaui/action: '*': - expires_at: 2025-08-01 keep: true cicirello/javadoc-cleanup: '*': - expires_at: 2025-08-01 keep: true clechasseur/rs-cargo: '*': - expires_at: 2025-08-01 keep: true codecov/codecov-action: '*': - expires_at: 2025-08-01 keep: true codelytv/pr-size-labeler: '*': - expires_at: 2025-08-01 keep: true codspeedhq/action: '*': - expires_at: 2025-08-01 keep: true commit-check/commit-check-action: a0193b1ca486178b85d7a1db145af34cd227f81f: @@ -256,19 +219,15 @@ commit-check/commit-check-action: tag: v2.2.1 conda-incubator/setup-miniconda: '*': - expires_at: 2025-08-01 keep: true container-tools/kind-action: '*': - expires_at: 2025-08-01 keep: true container-tools/microshift-action: '*': - expires_at: 2025-08-01 keep: true coursier/cache-action: '*': - expires_at: 2025-08-01 keep: true bebeeb0e6f48ebad66d3783946588ecf43114433: tag: 6.4.7 @@ -280,7 +239,6 @@ coursier/setup-action: tag: v1.3.9 expires_at: 2026-02-24 '*': - expires_at: 2025-08-01 keep: true 0ed4d7e7c42eae80e14370990582092c749253c4: tag: v2.0.0 @@ -295,7 +253,6 @@ cpp-linter/cpp-linter-action: tag: v2.15.1 crate-ci/typos: '*': - expires_at: 2025-08-01 keep: true crazy-max/ghaction-import-gpg: e89d40939c28e39f97cf32126055eeae86ba74ec: @@ -304,18 +261,15 @@ damccorm/tag-ur-it: 6fa72bbf1a2ea157b533d7e7abeafdb5855dbea5: dawidd6/action-download-artifact: '*': - expires_at: 2025-08-01 keep: true dawidd6/action-send-mail: 6d98ae34d733f9a723a9e04e94f2f24ba05e1402: tag: v6 delaguardo/setup-graalvm: '*': - expires_at: 2025-08-01 keep: true dlang-community/setup-dlang: '*': - expires_at: 2025-08-01 keep: true docker://jekyll/jekyll: sha256:400b8d1569f118bca8a3a09a25f32803b00a55d1ea241feaf5f904d66ca9c625: @@ -325,17 +279,14 @@ docker://pandoc/core: docker/setup-qemu-action: 29109295f81e9208d7d86ff1c6c12d2833863392: tag: v3.6.0 - keep: true dorny/paths-filter: de90cc6fb38fc0963ad72b210f1f284cd68cea36: tag: v3.0.2 easimon/maximize-build-space: '*': - expires_at: 2025-08-01 keep: true eps1lon/actions-label-merge-conflict: '*': - expires_at: 2025-08-01 keep: true erisu/apache-rat-action: 46fb01ce7d8f76bdcd7ab10e7af46e1ea95ca01c: @@ -345,15 +296,12 @@ erisu/license-checker-action: tag: v2.0.1 gaurav-nelson/github-action-markdown-link-check: '*': - expires_at: 2025-08-01 keep: true geekyeggo/delete-artifact: '*': - expires_at: 2025-08-01 keep: true golang/govulncheck-action: '*': - expires_at: 2025-08-01 keep: true google-github-actions/auth: 7c6bc770dae815cd3e89ee6cdf493a5fab2cc093: @@ -366,7 +314,6 @@ gradle/actions/setup-gradle: tag: v5.0.0 gr2m/twitter-together: '*': - expires_at: 2025-08-01 keep: true graalvm/setup-graalvm: eec48106e0bf45f2976c2ff0c3e22395cced8243: @@ -384,7 +331,6 @@ gradle/wrapper-validation-action: tag: v3.5.0 gsactions/commit-message-checker: '*': - expires_at: 2025-08-01 keep: true hadolint/hadolint-action: 2332a7b74a6de0dda2e2221d575162eba76ba5e5: @@ -406,85 +352,68 @@ helm/kind-action: tag: v1.13.0 houseabsolute/actions-rust-cross: '*': - expires_at: 2025-08-01 keep: true ilammy/msvc-dev-cmd: '*': - expires_at: 2025-08-01 keep: true ilammy/setup-nasm: 72793074d3c8cdda771dba85f6deafe00623038b: tag: v1.5.2 jarvusinnovations/background-action: '*': - expires_at: 2025-08-01 keep: true jasonetco/create-an-issue: 1b14a70e4d8dc185e5cc76d3bec9eab20257b2c5: tag: v2 jidicula/clang-format-action: '*': - expires_at: 2025-08-01 keep: true Jimver/cuda-toolkit: 6008063726ffe3309d1b22e413d9e88fed91a2f2: tag: v0.2.29 jlumbroso/free-disk-space: '*': - expires_at: 2025-08-01 keep: true jrouly/scalafmt-native-action: 14620cde093e5ff6bfbbecd4f638370024287b9d: tag: v4 '*': - expires_at: 2025-08-01 keep: true julia-actions/julia-buildpkg: '*': - expires_at: 2025-08-01 keep: true julia-actions/julia-docdeploy: '*': - expires_at: 2025-08-01 keep: true julia-actions/julia-processcoverage: '*': - expires_at: 2025-08-01 keep: true julia-actions/julia-runtest: '*': - expires_at: 2025-08-01 keep: true julia-actions/setup-julia: '*': - expires_at: 2025-08-01 keep: true juliaregistries/tagbot: '*': - expires_at: 2025-08-01 keep: true jwgmeligmeyling/checkstyle-github-action: '*': - expires_at: 2025-08-01 keep: true jwgmeligmeyling/pmd-github-action: 322e346bd76a0757c4d54ff9209e245965aa066d: tag: v1.2 jwgmeligmeyling/spotbugs-github-action: '*': - expires_at: 2025-08-01 keep: true kiegroup/github-action-build-chain: '*': - expires_at: 2025-08-01 keep: true korandoru/hawkeye: '*': - expires_at: 2025-08-01 keep: true leafo/gh-actions-lua: '*': - expires_at: 2025-08-01 keep: true leafo/gh-actions-luarocks: 97053c556d6ce2c8e26eb7ac93743437c7af7248: @@ -493,52 +422,42 @@ manusa/actions-setup-minikube: b589f2d61bf96695c546929c72b38563e856059d: tag: v2.14.0 '*': - expires_at: 2025-08-01 keep: true maxim-lobanov/setup-xcode: '*': - expires_at: 2025-08-01 keep: true medyagh/setup-minikube: '*': - expires_at: 2025-08-01 keep: true mikepenz/action-junit-report: '*': - expires_at: 2025-08-01 keep: true mozilla-actions/sccache-action: 7d986dd989559c6ecdb630a3fd2557667be217ad: tag: v0.0.9 msys2/setup-msys2: '*': - expires_at: 2025-08-01 keep: true mvasigh/dispatch-action: '*': - expires_at: 2025-08-01 keep: true nanasess/setup-chromedriver: '*': - expires_at: 2025-08-01 keep: true ncipollo/release-action: b7eabc95ff50cbeeedec83973935c8f306dfcd0b: tag: v1.20.0 nick-fields/retry: '*': - expires_at: 2025-08-01 keep: true nwtgck/actions-netlify: 4cbaf4c08f1a7bfa537d6113472ef4424e4eb654: tag: v3.0.0 ocaml/setup-ocaml: '*': - expires_at: 2025-08-01 keep: true olafurpg/setup-scala: '*': - expires_at: 2025-08-01 keep: true opentofu/setup-opentofu: 000eeb8522f0572907c393e8151076c205fdba1b: @@ -548,7 +467,6 @@ opentofu/setup-opentofu: tag: v1.0.8 oracle-actions/setup-java: '*': - expires_at: 2025-08-01 keep: true orhun/git-cliff-action: d77b37db2e3f7398432d34b72a12aa3e2ba87e51: @@ -558,44 +476,36 @@ orhun/git-cliff-action: tag: v4.7.0 ossf/scorecard-action: '*': - expires_at: 2025-08-01 keep: true peaceiris/actions-gh-pages: '*': - expires_at: 2025-08-01 keep: true peaceiris/actions-hugo: '*': - expires_at: 2025-08-01 keep: true peaceiris/actions-mdbook: '*': - expires_at: 2025-08-01 keep: true peter-evans/create-or-update-comment: e8674b075228eee787fea43ef493e45ece1004c9: tag: v5.0.0 peter-evans/create-pull-request: '*': - expires_at: 2025-08-01 keep: true phoenix-actions/test-reporting: f957cd93fc2d848d556fa0d03c57bc79127b6b5e: tag: v15 pnpm/action-setup: '*': - expires_at: 2025-08-01 keep: true potiuk/cancel-workflow-runs: '*': - expires_at: 2025-08-01 keep: true pre-commit/action: 2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd: tag: v3.0.1 pyTooling/Actions: '*': - expires_at: 2025-08-01 keep: true pypa/cibuildwheel: 9c00cb4f6b517705a3794b22395aedc36257242c: @@ -608,27 +518,21 @@ pypa/cibuildwheel: tag: v3.3.1 pytooling/actions: '*': - expires_at: 2025-08-01 keep: true quarto-dev/quarto-actions: '*': - expires_at: 2025-08-01 keep: true reactivecircus/android-emulator-runner: '*': - expires_at: 2025-08-01 keep: true readthedocs/actions: '*': - expires_at: 2025-08-01 keep: true release-drafter/release-drafter: '*': - expires_at: 2025-08-01 keep: true ruby/setup-ruby: '*': - expires_at: 2025-08-01 keep: true sbt/setup-sbt: '*': @@ -643,7 +547,6 @@ sbt/setup-sbt: tag: v1.1.16 scacap/action-surefire-report: '*': - expires_at: 2025-08-01 keep: true 5609ce4db72c09db044803b344a8968fd1f315da: tag: v1.9.1 @@ -661,23 +564,18 @@ scalacenter/sbt-dependency-submission: # tag: v3.2.1 scottbrenner/puppet-lint-action: '*': - expires_at: 2025-08-01 keep: true securego/gosec: '*': - expires_at: 2025-08-01 keep: true shivammathur/setup-php: '*': - expires_at: 2025-08-01 keep: true shogo82148/actions-setup-perl: '*': - expires_at: 2025-08-01 keep: true shufo/auto-assign-reviewer-by-files: '*': - expires_at: 2025-08-01 keep: true sigstore/cosign-installer: faadad0cce49287aee09b3a48701e75088a2c6ad: @@ -688,27 +586,21 @@ snok/install-poetry: tag: v1 softprops/action-gh-release: '*': - expires_at: 2025-08-01 keep: true stCarolas/setup-maven: '*': - expires_at: 2025-08-01 keep: true subosito/flutter-action: '*': - expires_at: 2025-08-01 keep: true swatinem/rust-cache: '*': - expires_at: 2025-08-01 keep: true swift-actions/setup-swift: '*': - expires_at: 2025-08-01 keep: true taiki-e/install-action: '*': - expires_at: 2025-08-01 keep: true tcort/github-action-markdown-link-check: f3d33029dca1c4a24b87e2df648f9f4604ef6533: @@ -718,11 +610,9 @@ tcort/github-action-markdown-link-check: tag: v1.1.2 test-summary/action: '*': - expires_at: 2025-08-01 keep: true timonvs/pr-labeler-action: '*': - expires_at: 2025-08-01 keep: true untitaker/hyperlink: e66bb17cc9ae341677431edec3b893a0aa6ac747: @@ -741,15 +631,12 @@ vimtor/action-zip: tag: v1 vishalsinha21/dynamic-checklist: '*': - expires_at: 2025-08-01 keep: true wei/curl: '*': - expires_at: 2025-08-01 keep: true xpol/setup-lua: '*': - expires_at: 2025-08-01 keep: true slackapi/slack-github-action: 91efab103c0de0a537f72a35f6b8cda0ee76bf0a: From a7c14147b80856c41b3722699a4afd65d10a08fb Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Thu, 12 Feb 2026 11:27:52 +0100 Subject: [PATCH 04/10] Fix coursier/cache-action reference Add the "current" Git commit ID for the moving tag. --- actions.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/actions.yml b/actions.yml index 86fc1d3b..fe35fc72 100644 --- a/actions.yml +++ b/actions.yml @@ -227,13 +227,14 @@ container-tools/microshift-action: '*': keep: true coursier/cache-action: - '*': - keep: true + c5ca79321d170b8a18c288d9cadc2a6037166d0f: + tag: v8.0.0 bebeeb0e6f48ebad66d3783946588ecf43114433: + tag: v7.0.0 + expires_at: 2026-08-01 + 4e2615869d13561d626ed48655e1a39e5b192b3c: tag: 6.4.7 expires_at: 2026-04-28 - c5ca79321d170b8a18c288d9cadc2a6037166d0f: - tag: 6.4.7 coursier/setup-action: 039f736548afa5411c1382f40a5bd9c2d30e0383: tag: v1.3.9 From b9bc41410c82707e92adae72c0cf2b8e30769a70 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Thu, 12 Feb 2026 11:27:59 +0100 Subject: [PATCH 05/10] Fix leafo/gh-actions-luarocks reference Update the moving tag `v5` with the current SHA. --- actions.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/actions.yml b/actions.yml index fe35fc72..74b408bc 100644 --- a/actions.yml +++ b/actions.yml @@ -418,6 +418,9 @@ leafo/gh-actions-lua: keep: true leafo/gh-actions-luarocks: 97053c556d6ce2c8e26eb7ac93743437c7af7248: + expires_at: 2026-08-01 + tag: v5 + 4c082a5fad45388feaeb0798dbd82dbd7dc65bca: tag: v5 manusa/actions-setup-minikube: b589f2d61bf96695c546929c72b38563e856059d: From 5132cdc547599f0895f94f0af621f8e037d65b73 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Fri, 7 Nov 2025 10:31:23 +0100 Subject: [PATCH 06/10] Verify GH action tag/SHA combinations This change introduces a new function `verify_actions` to validate the contents against GitHub. TL;DR The function verifies that the SHAs specified in `actions.yml` exist in the GH repo. Also ensures that the SHA exists on the Git tag, if the `tag` attribute is specified. The rest of the (currently spaghetti code) function is a lot of output and error(failure) and warning collection. Although it issues quite a few GH API requests, the rate limiter should not kick in (with an authenticated GH token). I opted to rely on the HTTP/1.1 `urllib.request` stuff, which has no connection-reuse. The alternative would have been to add a dependency. The algorithm roughly works like this, for each action specified in `actions.yml`: * Issue a warning and stop, if the name is like `OWNER/*` ("wildcard" repository). Can't verify Git SHAs in this case. * Issue a warning and stop, if the name is like `docker:*` (not implemented) * Issue an error and stop, if the name doesn't start with an `OWNER/REPO` pattern. * Each expired entry is just skipped * If there is a wildcard reference and a SHA reference, issue an error. Then, for each reference for an action: * If no `tag` is specified, let GH resolve the commit SHA. Emit a warning to add the value of the `tag` attribute, if the SHA can be resolved. Otherwise, emit an error. * If `tag` is specified: * Add the SHA to the set of requested-shas-by-tag * Call GH's "matching-refs" endpoint for the 'tag' value * Emit en error, if the object type is not a tag or commit. * Also resolve 'tag' object types to 'commit' object types. * Add each returned SHA to the set of valid-shas-by-tag. * For each "requested tag" verify that the sets of valid and requested shas intersect. If not, emit an error. --- .github/workflows/update_actions.yml | 9 +- actions.yml | 3 + gateway/action_tags.py | 304 +++++++++++++++++++++++++++ gateway/gateway.py | 1 + gateway/test_action_tags.py | 170 +++++++++++++++ 5 files changed, 486 insertions(+), 1 deletion(-) create mode 100644 gateway/action_tags.py create mode 100644 gateway/test_action_tags.py diff --git a/.github/workflows/update_actions.yml b/.github/workflows/update_actions.yml index 0b0cd31d..676f4724 100644 --- a/.github/workflows/update_actions.yml +++ b/.github/workflows/update_actions.yml @@ -32,7 +32,9 @@ jobs: - run: pip install ruyaml - name: Update actions.yml - shell: python + shell: python + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | import sys sys.path.append("./gateway/") @@ -41,6 +43,11 @@ jobs: g.update_actions(".github/workflows/dummy.yml", "actions.yml") g.update_patterns("approved_patterns.yml", "actions.yml") + import action_tags as at + result = at.verify_actions("actions.yml") + if result.has_failures(): + raise Exception(f"Verify actions result summary:\n{result}") + - name: Commit and push changes if: ${{ github.event_name != 'pull_request' }} run: | diff --git a/actions.yml b/actions.yml index 74b408bc..5559cf9c 100644 --- a/actions.yml +++ b/actions.yml @@ -554,6 +554,9 @@ scacap/action-surefire-report: keep: true 5609ce4db72c09db044803b344a8968fd1f315da: tag: v1.9.1 + # GH API requests from GH hosted runners fail with 403 and the following error message: + # 'Although you appear to have the correct authorization credentials, the `ScaCap` organization has an IP allow list enabled, and your IP address is not permitted to access this resource.' + ignore_gh_api_errors: true scala-steward-org/scala-steward-action: 53d486a68877f4a6d1e110e8058fe21e593db356: tag: v2.77.0 diff --git a/gateway/action_tags.py b/gateway/action_tags.py new file mode 100644 index 00000000..82933b29 --- /dev/null +++ b/gateway/action_tags.py @@ -0,0 +1,304 @@ +# /// script +# requires-python = ">=3.13" +# dependencies = [ +# "ruyaml", +# ] +# /// + +import os +import re +from urllib.error import HTTPError + +import ruyaml + +from datetime import date +from urllib.request import Request, urlopen +from pathlib import Path +from ruyaml import CommentedMap, CommentedSeq +from gateway import ActionsYAML, load_yaml, on_gha + +re_github_actions_repo_wildcard = r"^[A-Za-z0-9-_.]+/[*]$" +re_github_actions_repo = r"^([A-Za-z0-9-_.]+/[A-Za-z0-9-_.]+)(/.+)?$" +# Something like 'pytooling/actions/with-post-step' or 'readthedocs/actions/preview'. +re_docker_image = r"^docker://.+" +re_git_sha = r"^[a-f0-9]{7,}$" + +class ActionTagsCheckResult(object): + def __init__(self, log_to_console: bool = True): + self.log_to_console = log_to_console + self.logs = [] + self.failures = [] + self.warnings = [] + + def log(self, message: str) -> None: + if self.log_to_console: + print(message) + self.logs.append(message) + + def failure(self, message: str, indent: str) -> None: + self.log(f"{indent} ❌ {message}") + self.failures.append(message) + + def warning(self, message: str, indent: str) -> None: + self.log(f"{indent} ⚡ {message}") + self.warnings.append(message) + + def has_failures(self) -> bool: + return len(self.failures) > 0 + + def has_warnings(self) -> bool: + return len(self.warnings) > 0 + + def __str__(self): + return ( + ''.join([f"FAILURE: {failure}\n" for failure in self.failures]) + + ''.join([f"WARNING: {warning}\n" for warning in self.warnings])) + + +class ApiResponse(object): + def __init__(self, req_url: str, status: int, reason: str, headers: dict[str, str], body: str): + self.req_url = req_url + self.status = status + self.reason = reason + self.headers = headers + self.body = body + + +def _gh_api_get(url_abspath: str) -> ApiResponse: + headers: dict[str, str] = { + 'Accept': 'application/vnd.github.v3+json', + } + # Use GH_TOKEN, if available. + # Unauthorized GH API requests are quite rate-limited. + # Tip: add an extra space before 'export' to prevent adding the line to the shell history. + # export GH_TOKEN=$(gh auth token) + gh_token = os.environ['GH_TOKEN'] + if gh_token: + headers['Authorization'] = f"Bearer {gh_token}" + req_url = f"https://api.github.com{url_abspath}" + request = Request(url=req_url, headers=headers) + try: + with urlopen(request) as response: + return ApiResponse(req_url, response.status, response.reason, dict(response.headers), response.read().decode('utf-8')) + except HTTPError as e: + return ApiResponse(req_url, e.code, e.reason, dict(e.headers), e.read().decode('utf-8')) + except Exception as e: + print(f"Failed to fetch '{req_url}' from GitHub API") + raise e + +def _gh_get_commit_object(owner_repo: str, sha: str) -> ApiResponse: + return _gh_api_get(f"/repos/{owner_repo}/git/commits/{sha}") + +def _gh_get_tag(owner_repo: str, tag_sha: str) -> ApiResponse: + return _gh_api_get(f"/repos/{owner_repo}/git/tags/{tag_sha}") + +def _gh_matching_tags(owner_repo: str, tag: str) -> ApiResponse: + return _gh_api_get(f"/repos/{owner_repo}/git/matching-refs/tags/{tag}") + +def verify_actions(actions: Path | ActionsYAML | str, log_to_console: bool = True, today: date = date.today()) -> ActionTagsCheckResult: + """ + Validates the contents of the actions file against GitHub. + + The function verifies that the SHAs specified in `actions.yml` exist in the GH repo. + Also ensures that the SHA exists on the Git tag if the `tag` attribute is specified. + + The algorithm roughly works like this, for each action specified in `actions.yml`: + * Issue a warning and stop if the name is like `OWNER/*` ("wildcard" repository). + Can't verify Git SHAs in this case. + * Issue a warning and stop if the name is like `docker:*` (not implemented) + * Issue an error and stop if the name doesn't start with an `OWNER/REPO` pattern. + * Each expired entry is just skipped + * If there is a wildcard reference and an SHA reference, issue an error. + + Then, for each reference for an action: + * If no `tag` is specified, let GH resolve the commit SHA. + Emit a warning to add the value of the `tag` attribute if the SHA can be resolved. + Otherwise, emit an error. + * If `tag` is specified: + * Add the SHA to the set of requested-shas-by-tag + * Call GitHub's "matching-refs" endpoint for the 'tag' value + * Emit en error if the object type is not a tag or commit. + * Also resolve 'tag' object types to 'commit' object types. + * Add each returned SHA to the set of valid-shas-by-tag. + * For each "requested tag" verify that the sets of valid and requested shas intersect. If not, emit an error. + + Args: + actions: Path to the actions list file (mandatory) + log_to_console: Whether to log messages immediately to the console (default: True) + today: The current date (default: today) + """ + if on_gha(): + print(f"::group::Verify GitHub Actions") + gh_token = os.environ['GH_TOKEN'] + if not gh_token or len(gh_token) == 0: + raise Exception("GH_TOKEN environment variable is not set or empty") + + if isinstance(actions, Path) or isinstance(actions, str): + actions = load_yaml(actions) + actions_yaml: ActionsYAML = actions + + result = ActionTagsCheckResult(log_to_console=log_to_console or on_gha()) + + for name, action in actions_yaml.items(): + gh_repo_matcher = re.match(re_github_actions_repo, name) + if gh_repo_matcher is not None: + owner_repo = gh_repo_matcher.group(1) + result.log(f"Checking GitHub action {name} in GH repo 'https://github.com/{owner_repo}'...") + valid_shas_by_tag: dict[str, set[str]] = {} + requested_shas_by_tag: dict[str, set[str]] = {} + has_wildcard = False + has_wildcard_msg_emitted = False + # Flag whether to not error out on tag/SHA mismatches due to explicitly ignored GH API errors. + has_ignored_api_errors = False + for ref, details in action.items(): + if details and 'expires_at' in details: + expires_at: date = details.get('expires_at') + if expires_at < today: + # skip expired entries + result.log(f" .. ref '{ref}' is expired, skipping") + continue + + # noinspection PyTypedDict + ignore_gh_api_errors = details and 'ignore_gh_api_errors' in details and details['ignore_gh_api_errors'] == True + if ignore_gh_api_errors: + result.warning(f"ignore_gh_api_errors is set to true: will ignore GH API errors for action {name} ref '{ref}'", " ..") + + if ref == '*': + # "wildcard" SHA - what would we... + result.log(f" .. detected wildcard ref") + if len(requested_shas_by_tag) > 0 and not has_wildcard_msg_emitted: + result.warning(f"GitHub action {name} references a wildcard SHA but also has specific SHAs", " ..") + has_wildcard_msg_emitted = True + has_wildcard = True + continue + elif re.match(re_git_sha, ref): + result.log(f" .. detected entry with Git SHA '{ref}'") + if has_wildcard and not has_wildcard_msg_emitted: + result.warning(f"GitHub action {name} references a wildcard SHA but also has specific SHAs", " ..") + has_wildcard_msg_emitted = True + + if not details or not 'tag' in details: + result.log(f" .. no Git tag") + # https://docs.github.com/en/rest/git/commits?apiVersion=2022-11-28#get-a-commit-object + response = _gh_get_commit_object(owner_repo, ref) + match response.status: + case 200: + result.warning(f"GitHub action {name} references existing commit SHA '{ref}' but does not specify the tag name for it.", " ..") + case 404: + result.failure(f"GitHub action {name} references non existing commit SHA '{ref}': HTTP/{response.status}: {response.reason}, API URL: {response.req_url}", " ..") + case _: + m = f"Failed to fetch Git SHA '{ref}' from GitHub repo 'https://github.com/{owner_repo}': HTTP/{response.status}: {response.reason}, API URL: {response.req_url}\n{response.body}" + if ignore_gh_api_errors: + has_ignored_api_errors = True + result.warning(m, " ..") + else: + result.failure(m, " ..") + else: + tag: str = details.get('tag') + result.log(f" .. collecting Git SHAs for tag {tag}") + + if not tag in requested_shas_by_tag: + requested_shas_by_tag[tag] = set() + requested_shas_by_tag[tag].add(ref) + + if not tag in valid_shas_by_tag: + valid_shas_by_tag[tag] = set() + valid_shas_for_tag = valid_shas_by_tag[tag] + + # https://docs.github.com/en/rest/git/refs?apiVersion=2022-11-28#list-matching-references + response = _gh_matching_tags(owner_repo, tag) + match response.status: + case 200: + response_json: CommentedSeq = ruyaml.YAML().load(response.body) + for msg in response_json: + tag_ref_map: CommentedMap = msg + tag_object: CommentedMap = tag_ref_map["object"] + tab_object_type: str = tag_object["type"] + tag_object_sha: str = tag_object["sha"] + result.log(f" .. GH yields {tab_object_type} SHA '{tag_object_sha}' for '{tag_ref_map['ref']}'") + match tab_object_type: + case "tag": + valid_shas_for_tag.add(tag_object_sha) + # https://docs.github.com/en/rest/git/tags?apiVersion=2022-11-28#get-a-tag + response2 = _gh_get_tag(owner_repo, tag_object_sha) + match response2.status: + case 200: + tag_object_sha = ruyaml.YAML().load(response2.body)["object"]["sha"] + valid_shas_for_tag.add(tag_object_sha) + result.log(f" .. GH returns commit SHA '{tag_object_sha}' for previous tag SHA") + case 404: + result.log(f" .. commit SHA '{tag_object_sha}' does not exist") + case _: + m = f"Failed to fetch details for Git tag '{tag}' from GitHub repo 'https://github.com/{owner_repo}': HTTP/{response2.status}: {response2.reason}, API URL: {response2.req_url}\n{response2.body}" + if ignore_gh_api_errors: + has_ignored_api_errors = True + result.warning(m, " ..") + else: + result.failure(m, " ..") + case "commit": + valid_shas_for_tag.add(tag_object_sha) + case "branch": + result.failure(f"Branch references mentioned for Git tag '{tag}' for GitHub action {name}", " ..") + case _: + result.failure(f"Invalid Git object type '{tag_object['type']}' for Git tag '{tag}' in GitHub repo 'https://github.com/{owner_repo}'", " ..") + case _: + m = f"Failed to fetch matching Git tags for '{tag}' from GitHub repo 'https://github.com/{owner_repo}': HTTP/{response.status}: {response.reason}, API URL: {response.req_url}\n{response.body}" + if ignore_gh_api_errors: + result.warning(m, " ..") + has_ignored_api_errors = True + else: + result.failure(m, " ..") + else: + result.failure(f"GitHub action {name} references an invalid Git SHA '{ref}'", " ..") + + for req_tag, req_shas in requested_shas_by_tag.items(): + result.log(f" .. checking tag '{req_tag}'") + result.log(f" .. referenced SHAs: {req_shas}") + valid_shas = valid_shas_by_tag.get(req_tag) + result.log(f" .. verified SHAs: {valid_shas if len(valid_shas)>0 else '(none)'}") + if not valid_shas: + m = f"GitHub action {name} references Git tag '{req_tag}' via SHAs '{req_shas}' but no SHAs for tag could be found - does the Git tag exist?" + if has_ignored_api_errors: + result.warning(m, "") + else: + result.failure(m, "") + elif req_shas.isdisjoint(valid_shas): + m = f"GitHub action {name} references Git tag '{req_tag}' via SHAs '{req_shas}' but none of those matches the valid SHAs '{valid_shas}'" + result.failure(m, "") + else: + result.log(f" ✅ GitHub action {name} definition for tag '{req_tag}' is good!") + + elif re.match(re_github_actions_repo_wildcard, name): + result.warning(f"Ignoring '{name}' because it uses a GitHub repository wildcard ...", "") + + elif re.match(re_docker_image, name): + result.warning(f"Ignoring '{name}' because it references a Docker image ...", "") + + else: + m = f"Cannot determine action kind for '{name}'" + result.failure(m, "") + + if on_gha(): + if result.has_failures() or result.has_warnings(): + with open(os.environ["GITHUB_STEP_SUMMARY"], "a") as f: + f.write(f"# GitHub Actions verification result\n") + if len(result.failures) > 0: + f.write(f"## Failures ({len(result.failures)})\n") + f.write('```\n') + for msg in result.failures: + f.write(f"{msg}\n\n") + f.write('```\n') + if len(result.warnings) > 0: + f.write(f"## Warnings ({len(result.warnings)})\n") + f.write('```\n') + for msg in result.warnings: + f.write(f"{msg}\n\n") + f.write('```\n') + f.write(f"## Log\n") + f.write('```\n') + for msg in result.logs: + f.write(f"{msg}\n") + f.write('```\n') + print("::endgroup::") + + return result diff --git a/gateway/gateway.py b/gateway/gateway.py index c195f527..832efa2d 100644 --- a/gateway/gateway.py +++ b/gateway/gateway.py @@ -24,6 +24,7 @@ class RefDetails(TypedDict): expires_at: date keep: NotRequired[bool] + tag: NotRequired[str] ActionRefs = Dict[str, RefDetails] diff --git a/gateway/test_action_tags.py b/gateway/test_action_tags.py new file mode 100644 index 00000000..cdcc596b --- /dev/null +++ b/gateway/test_action_tags.py @@ -0,0 +1,170 @@ +import pytest +from action_tags import * + +def test_patterns(): + assert re.match(re_github_actions_repo, "foo/bar") + assert not re.match(re_github_actions_repo, "foo/*") + assert re.match(re_github_actions_repo, "foo/bar/.github/actions/*") + assert re.match(re_github_actions_repo, "foo/bar/.github/actions/some.yml") + assert re.match(re_docker_image, "docker://foo/bar") + +@pytest.mark.skipif(os.environ.get('GH_TOKEN') is None, reason="GH_TOKEN environment variable should be set for this test as it issues GitHub API requests.") +def test_sha_without_tag(): + # noinspection PyTypeChecker + result = verify_actions({ + "sbt/setup-sbt": { + "3e125ece5c3e5248e18da9ed8d2cce3d335ec8dd": { + }, + }, + }) + assert result.failures == [] + assert result.warnings == [ + "GitHub action sbt/setup-sbt references existing commit SHA '3e125ece5c3e5248e18da9ed8d2cce3d335ec8dd' but does not specify the tag name for it." + ] + +@pytest.mark.skipif(os.environ.get('GH_TOKEN') is None, reason="GH_TOKEN environment variable should be set for this test as it issues GitHub API requests.") +def test_sha_non_existent(): + # noinspection PyTypeChecker + result = verify_actions({ + "sbt/setup-sbt": { + "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef": { + }, + }, + }) + assert result.failures == [ + "GitHub action sbt/setup-sbt references non existing commit SHA 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeef': HTTP/404: Not Found, API URL: https://api.github.com/repos/sbt/setup-sbt/git/commits/deadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + ] + assert result.warnings == [] + +@pytest.mark.skipif(os.environ.get('GH_TOKEN') is None, reason="GH_TOKEN environment variable should be set for this test as it issues GitHub API requests.") +def test_tag_sha_vs_commit_sha(): + # noinspection PyTypeChecker + result = verify_actions({ + "1Password/load-secrets-action": { + "e4feb4d8a7cd938b64370099b1893e05c58c3a84": { + "tag": "v3.0.0" + }, + }, + }) + assert " .. GH yields tag SHA 'e4feb4d8a7cd938b64370099b1893e05c58c3a84' for 'refs/tags/v3.0.0'" in result.logs + assert " .. GH returns commit SHA '13f58eec611f8e5db52ec16247f58c508398f3e6' for previous tag SHA" in result.logs + assert result.failures == [] + assert result.warnings == [] + +@pytest.mark.skipif(os.environ.get('GH_TOKEN') is None, reason="GH_TOKEN environment variable should be set for this test as it issues GitHub API requests.") +def test_tag_sha_eq_commit_sha(): + # noinspection PyTypeChecker + result = verify_actions({ + "1Password/load-secrets-action": { + "13f58eec611f8e5db52ec16247f58c508398f3e6": { + "tag": "v3.0.0" + }, + }, + }) + assert " .. GH yields tag SHA 'e4feb4d8a7cd938b64370099b1893e05c58c3a84' for 'refs/tags/v3.0.0'" in result.logs + assert " .. GH returns commit SHA '13f58eec611f8e5db52ec16247f58c508398f3e6' for previous tag SHA" in result.logs + assert result.failures == [] + assert result.warnings == [] + +@pytest.mark.skipif(os.environ.get('GH_TOKEN') is None, reason="GH_TOKEN environment variable should be set for this test as it issues GitHub API requests.") +def test_non_existing_tag(): + # noinspection PyTypeChecker + result = verify_actions({ + "1Password/load-secrets-action": { + "13f58eec611f8e5db52ec16247f58c508398f3e6": { + "tag": "v_ne_3.0.0" + }, + }, + }) + assert result.failures == [ + "GitHub action 1Password/load-secrets-action references Git tag 'v_ne_3.0.0' via SHAs '{'13f58eec611f8e5db52ec16247f58c508398f3e6'}' but no SHAs for tag could be found - does the Git tag exist?" + ] + assert result.warnings == [] + +@pytest.mark.skipif(os.environ.get('GH_TOKEN') is None, reason="GH_TOKEN environment variable should be set for this test as it issues GitHub API requests.") +def test_non_existing_tag_sha(): + # noinspection PyTypeChecker + result = verify_actions({ + "astral-sh/setup-uv": { + "b75a909f75acd358c2196fb9a5f1299a9a8868a4": { + "tag": "v7.1.2" + }, + }, + }) + assert result.failures == [ + "GitHub action astral-sh/setup-uv references Git tag 'v7.1.2' via SHAs '{'b75a909f75acd358c2196fb9a5f1299a9a8868a4'}' but none of those matches the valid SHAs '{'85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41'}'" + ] + assert result.warnings == [] + +@pytest.mark.skipif(os.environ.get('GH_TOKEN') is None, reason="GH_TOKEN environment variable should be set for this test as it issues GitHub API requests.") +def test_repo_multiple_actions_repo_works(): + # noinspection PyTypeChecker + result = verify_actions({ + "gradle/actions/setup-gradle": { + "4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2": { + "tag": "v5.0.0" + }, + }, + "gradle/actions/wrapper-validation": { + "748248ddd2a24f49513d8f472f81c3a07d4d50e1": { + "tag": "v4.4.4" + }, + }, + }) + assert result.failures == [] + assert result.warnings == [] + assert " ✅ GitHub action gradle/actions/setup-gradle definition for tag 'v5.0.0' is good!" in result.logs + assert " ✅ GitHub action gradle/actions/wrapper-validation definition for tag 'v4.4.4' is good!" in result.logs + +@pytest.mark.skipif(os.environ.get('GH_TOKEN') is None, reason="GH_TOKEN environment variable should be set for this test as it issues GitHub API requests.") +def test_wildcard_warnings_1(): + # noinspection PyTypeChecker + _test_wildcard_warnings({ + "sbt/setup-sbt": { + '*': { + "expires_at": date(2026, 2,28), + }, + "17575ea4e18dd928fe5968dbe32294b97923d65b": { + "expires_at": date(2025, 12,29), + "tag": "v1.1.13" + }, + "3e125ece5c3e5248e18da9ed8d2cce3d335ec8dd": { + "tag": "v1.1.14" + }, + }, + }) + +@pytest.mark.skipif(os.environ.get('GH_TOKEN') is None, reason="GH_TOKEN environment variable should be set for this test as it issues GitHub API requests.") +def test_wildcard_warnings_2(): + """ + Similar to test_wildcard_warnings_1, but with the wildcard SHA at the end. + """ + # noinspection PyTypeChecker + _test_wildcard_warnings({ + "sbt/setup-sbt": { + "17575ea4e18dd928fe5968dbe32294b97923d65b": { + "expires_at": date(2025, 12,29), + "tag": "v1.1.13" + }, + "3e125ece5c3e5248e18da9ed8d2cce3d335ec8dd": { + "tag": "v1.1.14" + }, + '*': { + "expires_at": date(2026, 2,28), + }, + }, + }) + +def _test_wildcard_warnings(refs: ActionsYAML): + result = verify_actions(refs, today=date(2025, 12, 21)) + assert not " .. ref '*' is expired, skipping" in result.logs + assert result.failures == [] + assert result.warnings == [ + "GitHub action sbt/setup-sbt references a wildcard SHA but also has specific SHAs", + ] + + # wildcard expired + result = verify_actions(refs, today=date(2026, 3, 1)) + assert " .. ref '*' is expired, skipping" in result.logs + assert result.failures == [] + assert result.warnings == [] From 2714ffbe473bfebc6fbc439cde4a8c6d669d2bc4 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Thu, 12 Feb 2026 11:05:02 +0100 Subject: [PATCH 07/10] move GH actions tags check to separate workflow --- .github/workflows/check_action_tags.yml | 46 +++++++++++++++++++++++++ .github/workflows/update_actions.yml | 9 +---- 2 files changed, 47 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/check_action_tags.yml diff --git a/.github/workflows/check_action_tags.yml b/.github/workflows/check_action_tags.yml new file mode 100644 index 00000000..74bb0374 --- /dev/null +++ b/.github/workflows/check_action_tags.yml @@ -0,0 +1,46 @@ +name: Check action tags +on: + workflow_dispatch: + push: + branches: + - main + paths: + - ".github/workflows/dummy.yml" + pull_request: + paths: + - ".github/workflows/update_actions.yml" + - ".github/workflows/dummy.yml" + - gateway/* + +permissions: + contents: read + +# We want workflows on main to run in order to avoid losing data through race conditions +concurrency: "${{ github.ref }}-${{ github.workflow }}" + +jobs: + check_action_tags: + runs-on: ubuntu-latest + steps: + + - uses: actions/checkout@v6 + + - run: pip install ruyaml + + - name: Update actions.yml and check action tags + # This step is similar to the one in update_actions.yml but also verifies the actions' tags + shell: python + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + import sys + sys.path.append("./gateway/") + + import gateway as g + g.update_actions(".github/workflows/dummy.yml", "actions.yml") + g.update_patterns("approved_patterns.yml", "actions.yml") + + import action_tags as at + result = at.verify_actions("actions.yml") + if result.has_failures(): + raise Exception(f"Verify actions result summary:\n{result}") diff --git a/.github/workflows/update_actions.yml b/.github/workflows/update_actions.yml index 676f4724..0b0cd31d 100644 --- a/.github/workflows/update_actions.yml +++ b/.github/workflows/update_actions.yml @@ -32,9 +32,7 @@ jobs: - run: pip install ruyaml - name: Update actions.yml - shell: python - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: python run: | import sys sys.path.append("./gateway/") @@ -43,11 +41,6 @@ jobs: g.update_actions(".github/workflows/dummy.yml", "actions.yml") g.update_patterns("approved_patterns.yml", "actions.yml") - import action_tags as at - result = at.verify_actions("actions.yml") - if result.has_failures(): - raise Exception(f"Verify actions result summary:\n{result}") - - name: Commit and push changes if: ${{ github.event_name != 'pull_request' }} run: | From 0b8c39ae183e26608f5dc31bd43eb01a42136402 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Sat, 14 Feb 2026 10:16:37 +0100 Subject: [PATCH 08/10] consider the 'keep' flag --- gateway/action_tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gateway/action_tags.py b/gateway/action_tags.py index 82933b29..573c3a4b 100644 --- a/gateway/action_tags.py +++ b/gateway/action_tags.py @@ -151,7 +151,7 @@ def verify_actions(actions: Path | ActionsYAML | str, log_to_console: bool = Tru # Flag whether to not error out on tag/SHA mismatches due to explicitly ignored GH API errors. has_ignored_api_errors = False for ref, details in action.items(): - if details and 'expires_at' in details: + if details and 'expires_at' in details and not details.get('keep'): expires_at: date = details.get('expires_at') if expires_at < today: # skip expired entries From 07cb01823df54fe4023e9d5c536a3a480ff43b2c Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Mon, 16 Feb 2026 09:40:38 +0100 Subject: [PATCH 09/10] add convenience run-script --- .github/workflows/check_action_tags.yml | 33 ++++++++------ gateway/action_tags.py | 24 +++++++--- gateway/run_action_tags.py | 58 +++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 19 deletions(-) create mode 100644 gateway/run_action_tags.py diff --git a/.github/workflows/check_action_tags.yml b/.github/workflows/check_action_tags.yml index 74bb0374..b31d8316 100644 --- a/.github/workflows/check_action_tags.yml +++ b/.github/workflows/check_action_tags.yml @@ -1,3 +1,22 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + name: Check action tags on: workflow_dispatch: @@ -29,18 +48,6 @@ jobs: - name: Update actions.yml and check action tags # This step is similar to the one in update_actions.yml but also verifies the actions' tags - shell: python env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - import sys - sys.path.append("./gateway/") - - import gateway as g - g.update_actions(".github/workflows/dummy.yml", "actions.yml") - g.update_patterns("approved_patterns.yml", "actions.yml") - - import action_tags as at - result = at.verify_actions("actions.yml") - if result.has_failures(): - raise Exception(f"Verify actions result summary:\n{result}") + run: python gateway/run_action_tags.py diff --git a/gateway/action_tags.py b/gateway/action_tags.py index 573c3a4b..d984e24c 100644 --- a/gateway/action_tags.py +++ b/gateway/action_tags.py @@ -1,9 +1,21 @@ -# /// script -# requires-python = ">=3.13" -# dependencies = [ -# "ruyaml", -# ] -# /// +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# import os import re diff --git a/gateway/run_action_tags.py b/gateway/run_action_tags.py new file mode 100644 index 00000000..e68d91cd --- /dev/null +++ b/gateway/run_action_tags.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +# Helper script to run the action tag verification. +# +# You must have a GH_TOKEN environment variable set to a GitHub PAT w/ read accessto public repos. +# +# From a local environment use: +# uvx --with ruyaml python gateway/run_action_tags.py +# Or if you're using a virtualenv: +# python gateway/run_action_tags.py + +import os +from pathlib import Path + +from action_tags import verify_actions +from gateway import update_actions, update_patterns + + +def run_main(): + if not 'GH_TOKEN' in os.environ: + raise Exception("GH_TOKEN environment variable should be must.") + + cwd = Path(os.getcwd()) + dummy_workflow = cwd / ".github/workflows/dummy.yml" + actions_yaml = cwd / "actions.yml" + approved_patterns_yaml = cwd / "approved_patterns.yml" + + if not dummy_workflow.exists() or not actions_yaml.exists() or not approved_patterns_yaml.exists(): + raise Exception(f"Missing required files: {dummy_workflow.absolute()}, {actions_yaml.absolute()}, {approved_patterns_yaml.absolute()}") + + update_actions(dummy_workflow, actions_yaml) + update_patterns(approved_patterns_yaml, actions_yaml) + + result = verify_actions(actions_yaml) + if result.has_failures(): + raise Exception(f"Verify actions result summary:\n{result}") + + +if __name__ == "__main__": + run_main() From ca6bcb96f62830aaf641cab1672cbf4f700ad05f Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Mon, 16 Feb 2026 09:59:38 +0100 Subject: [PATCH 10/10] add special handling for historic actions allowance against branches --- actions.yml | 4 ++++ gateway/action_tags.py | 7 ++++++- gateway/gateway.py | 10 ++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/actions.yml b/actions.yml index 5559cf9c..cfb444db 100644 --- a/actions.yml +++ b/actions.yml @@ -27,6 +27,8 @@ astral-sh/setup-uv: dtolnay/rust-toolchain: stable: keep: true + # 'stable' is a branch, not a Git SHA + ignore_invalid_git_sha: true golangci/*: '*': keep: true @@ -37,6 +39,8 @@ pypa/gh-action-pip-audit: pypa/gh-action-pypi-publish: release/v1*: keep: true + # 'release/v1*' is a branch wildcard, not a Git SHA + ignore_invalid_git_sha: true pytooling/actions/with-post-step: '*': keep: true diff --git a/gateway/action_tags.py b/gateway/action_tags.py index d984e24c..bd5b6c61 100644 --- a/gateway/action_tags.py +++ b/gateway/action_tags.py @@ -261,7 +261,12 @@ def verify_actions(actions: Path | ActionsYAML | str, log_to_console: bool = Tru else: result.failure(m, " ..") else: - result.failure(f"GitHub action {name} references an invalid Git SHA '{ref}'", " ..") + ignore_invalid_git_sha = details and 'ignore_invalid_git_sha' in details and details['ignore_invalid_git_sha'] == True + if ignore_invalid_git_sha: + result.warning(f"GitHub action {name} references an invalid Git SHA but 'ignore_invalid_git_sha' is set: will ignore invalid Git SHA '{ref}'", " ..") + else: + result.failure(f"GitHub action {name} references an invalid Git SHA '{ref}'", " ..") + raise Exception("foo") for req_tag, req_shas in requested_shas_by_tag.items(): result.log(f" .. checking tag '{req_tag}'") diff --git a/gateway/gateway.py b/gateway/gateway.py index 832efa2d..05340f26 100644 --- a/gateway/gateway.py +++ b/gateway/gateway.py @@ -25,6 +25,16 @@ class RefDetails(TypedDict): expires_at: date keep: NotRequired[bool] tag: NotRequired[str] + # Action tags check: Ignore invalid Git SHA and GitHub API errors. + # Some actions are allowed to use branches. + # This should really be used in only very exceptional cases + # and MUST NEVER be used for new actions. + ignore_invalid_git_sha: NotRequired[bool] + # Action tags check: Ignore GitHub API errors in special situations, + # when for example the repository has an IP allow list enabled preventing checks using the GH API + # against such repositories. Sample error message in such cases: + # 'Although you appear to have the correct authorization credentials, the `ScaCap` organization has an IP allow list enabled, and your IP address is not permitted to access this resource.' + ignore_gh_api_errors: NotRequired[bool] ActionRefs = Dict[str, RefDetails]