Compare commits

..

131 Commits

Author SHA1 Message Date
042160c390 fix(claude): correct skill docs per PR review feedback
Fix validate skill description to say "precommit" instead of "test",
and fix check-pins SHA guidance to use origin/main instead of HEAD.
2026-03-08 03:55:04 +02:00
3d1d8e1013 fix(claude): add tool availability guards and fix skill docs
Add jq availability checks to hook scripts (block-rules-yml.sh,
post-edit-write.sh) and wrap actionlint call in command -v guard,
consistent with project rules #2 and #10. Fix validate skill to
reflect actual make all pipeline order and note that make test
runs separately.
2026-03-07 22:13:43 +02:00
df3e034d42 chore(claude): add hooks, skills, and agents for Claude Code
Add auto-formatting hooks (ruff, shfmt, prettier, actionlint),
rules.yml edit blocker, 5 skills (/release, /test-action,
/new-action, /validate, /check-pins), and 2 subagents
(action-validator, test-coverage-reviewer). Update CLAUDE.md
with hook documentation.
2026-03-07 20:54:37 +02:00
renovate[bot]
242ecca8f0 chore(deps): update pre-commit hook astral-sh/ruff-pre-commit (v0.15.4 → v0.15.5) (#495)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-07 17:07:03 +02:00
renovate[bot]
97105fc2a9 chore(deps): lock file maintenance (#494)
* chore(deps): update actions/setup-node action (v6.2.0 → v6.3.0)

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): lock file maintenance

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

---------

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-06 11:17:12 +02:00
renovate[bot]
e9deac2a01 chore(deps)!: update docker/metadata-action (v5.10.0 → v6.0.0) (#493)
* chore(deps): update actions/setup-node action (v6.2.0 → v6.3.0)

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps)!: update docker/metadata-action (v5.10.0 → v6.0.0)

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

---------

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-06 11:16:38 +02:00
renovate[bot]
480c06b83b chore(deps): update actions/setup-node action (v6.2.0 → v6.3.0) (#491)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-06 11:16:07 +02:00
renovate[bot]
07ce8df887 chore(deps)!: update docker/build-push-action (v6.19.2 → v7.0.0) (#492)
* chore(deps): update actions/setup-node action (v6.2.0 → v6.3.0)

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps)!: update docker/build-push-action (v6.19.2 → v7.0.0)

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

---------

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-06 11:15:36 +02:00
renovate[bot]
0f29424163 chore(deps): update actions/setup-dotnet action (v5.1.0 → v5.2.0) (#490)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-06 11:15:00 +02:00
renovate[bot]
ce81e51641 chore(deps): update raven-actions/actionlint action (v2.1.1 → v2.1.2) (#488)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-06 11:14:39 +02:00
renovate[bot]
261323db29 chore(deps): update actions/dependency-review-action action (v4.8.3 → v4.9.0) (#489)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-06 11:14:15 +02:00
renovate[bot]
c679f0c863 chore(deps): update pre-commit hook astral-sh/uv-pre-commit (0.10.7 → 0.10.8) (#486)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-06 02:51:06 +02:00
renovate[bot]
128b2f1713 chore(deps): update oven-sh/setup-bun action (v2.1.2 → v2.1.3) (#485)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-06 02:50:33 +02:00
renovate[bot]
08393e8063 chore(deps): update github/codeql-action action (v4.32.4 → v4.32.6) (#484)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-06 02:49:32 +02:00
renovate[bot]
ed9f205433 chore(deps): update aquasecurity/trivy-action action (0.34.1 → 0.34.2) (#483)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-06 02:49:00 +02:00
Copilot
ae4ad9ec80 fix: harden workflow permissions with deny-all top-level and least-privilege job scopes (#482) 2026-03-06 02:44:56 +02:00
renovate[bot]
455267f892 chore(deps): update pre-commit hook bridgecrewio/checkov (3.2.506 → 3.2.507) (#487)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-06 02:41:33 +02:00
renovate[bot]
d1af04260d chore(deps)!: update docker/login-action (v3.7.0 → v4.0.0) (#477) 2026-03-05 22:41:05 +02:00
renovate[bot]
0921e373ce chore(deps)!: update docker/setup-buildx-action (v3.12.0 → v4.0.0) (#478) 2026-03-05 22:26:51 +02:00
renovate[bot]
6bbe5089d2 chore(deps)!: update docker/setup-qemu-action (v3.7.0 → v4.0.0) (#479) 2026-03-05 22:15:39 +02:00
renovate[bot]
7cf51e5364 chore(deps): lock file maintenance (#481) 2026-03-05 22:14:11 +02:00
renovate[bot]
72c6155089 chore(deps)!: update github/issue-metrics (v3.25.5 → v4.1.0) (#480) 2026-03-05 22:07:00 +02:00
renovate[bot]
6e8f2aae9d chore(deps): update pre-commit hook astral-sh/uv-pre-commit (0.10.5 → 0.10.7) (#475)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 17:22:01 +02:00
renovate[bot]
f15daec6dc chore(deps): update pre-commit hook astral-sh/ruff-pre-commit (v0.15.2 → v0.15.4) (#474)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 17:21:37 +02:00
renovate[bot]
66870c6d0c chore(deps): update oxsecurity/megalinter action (v9.3.0 → v9.4.0) (#476)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 17:20:54 +02:00
renovate[bot]
03eeb4c39f chore(deps): update astral-sh/setup-uv action (v7.3.0 → v7.3.1) (#473)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 03:04:57 +02:00
renovate[bot]
992b64a580 chore(deps)!: update hashicorp/setup-terraform (v3.1.2 → v4.0.0) (#471)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 02:35:56 +02:00
renovate[bot]
f114b11df1 chore(deps): lock file maintenance (#472)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 02:35:20 +02:00
bd59245cd7 fix(deps): replace step-security/retry and update action pins (#468)
* fix(deps): replace step-security/retry with nick-fields/retry

* chore(deps): update github action sha pins via pinact

* refactor: remove common-retry references from tests and validators

* chore: simplify description fallback and update action count

* docs: remove hardcoded test counts from memory and docs

Replace exact "769 tests" references with qualitative language
so these files don't go stale as test count grows.
2026-03-02 02:31:26 +02:00
dependabot[bot]
d919327c7e chore(deps): bump minimatch (#466) 2026-02-28 16:20:54 +02:00
renovate[bot]
8faacf8a1c chore(deps): update actions/dependency-review-action action (v4.8.2 → v4.8.3) (#461)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-24 20:09:57 +02:00
renovate[bot]
bbca76975e chore(deps): update pre-commit hook astral-sh/ruff-pre-commit (v0.15.1 → v0.15.2) (#462)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-24 20:06:53 +02:00
renovate[bot]
b75d237069 chore(deps): update pre-commit hook bridgecrewio/checkov (3.2.502 → 3.2.506) (#464)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-24 20:06:32 +02:00
renovate[bot]
7973e4945b chore(deps): update markdownlint-cli2 (0.20.0 → 0.21.0) (#465)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-24 20:06:10 +02:00
renovate[bot]
2ce9325ff9 chore(deps): update pre-commit hook astral-sh/uv-pre-commit (0.10.3 → 0.10.5) (#463)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-24 20:05:35 +02:00
renovate[bot]
37e80e5224 chore(deps): lock file maintenance (#457) 2026-02-23 21:43:14 +02:00
renovate[bot]
2555420036 chore(deps): update aquasecurity/trivy-action action (0.34.0 → 0.34.1) (#458)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-23 21:30:15 +02:00
renovate[bot]
2e4525cb96 chore(deps): update github/codeql-action action (v4.32.3 → v4.32.4) (#459)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-23 21:29:42 +02:00
renovate[bot]
a75db3a84a chore(deps): update actions/stale action (v10.1.1 → v10.2.0) (#460)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-23 21:29:07 +02:00
renovate[bot]
309f4460ec chore(deps): update pre-commit hook davidanson/markdownlint-cli2 (v0.20.0 → v0.21.0) (#456)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-17 23:20:07 +02:00
renovate[bot]
55897dfdeb chore(deps): update pre-commit hook rhysd/actionlint (v1.7.10 → v1.7.11) (#455)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-17 19:15:21 +02:00
renovate[bot]
88a0b89d8d chore(deps): update pre-commit hook astral-sh/ruff-pre-commit (v0.15.0 → v0.15.1) (#452)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-17 19:13:47 +02:00
renovate[bot]
0131cbfcf6 chore(deps): update docker/build-push-action action (v6.18.0 → v6.19.2) (#451)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-17 19:13:25 +02:00
renovate[bot]
f36f50e375 chore(deps): update aquasecurity/trivy-action action (0.33.1 → 0.34.0) (#450)
* chore(deps): update pre-commit hook rhysd/actionlint (v1.7.10 → v1.7.11)

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update aquasecurity/trivy-action action (0.33.1 → 0.34.0)

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

---------

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-17 19:11:17 +02:00
renovate[bot]
f0c398f47d chore(deps): update pre-commit hook bridgecrewio/checkov (3.2.500 → 3.2.502) (#454)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-17 19:00:40 +02:00
renovate[bot]
1eb60955d1 chore(deps): update pre-commit hook astral-sh/uv-pre-commit (0.10.0 → 0.10.3) (#453)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-17 18:59:38 +02:00
renovate[bot]
291bb2fdc4 chore(deps): update github/codeql-action action (v4.32.2 → v4.32.3) (#449)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-16 09:31:18 +02:00
renovate[bot]
8fa4dc84f2 chore(deps): lock file maintenance (#448)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-16 09:30:59 +02:00
renovate[bot]
c40f80e9c5 chore(deps): update actions/setup-python action (v6.1.0 → v6.2.0) (#439)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-10 13:34:34 +02:00
renovate[bot]
20fb4bc79c chore(deps): update astral-sh/setup-uv action (v7.2.1 → v7.3.0) (#440)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-10 13:33:59 +02:00
renovate[bot]
9277758f30 chore(deps): update docker/login-action action (v3.6.0 → v3.7.0) (#441)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-10 13:33:40 +02:00
renovate[bot]
a9605c642f chore(deps): update github/codeql-action action (v4.31.9 → v4.32.2) (#442)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-10 13:32:33 +02:00
renovate[bot]
6d25c0f8b6 chore(deps): update peter-evans/create-pull-request action (v8.0.0 → v8.1.0) (#443)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-10 13:31:35 +02:00
renovate[bot]
6c04d8b197 chore(deps): update image python to v3.14.3 (#444)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-10 13:31:12 +02:00
renovate[bot]
e6c7e60e25 chore(deps): update pre-commit hook adrienverge/yamllint (v1.37.1 → v1.38.0) (#445)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-10 13:30:49 +02:00
renovate[bot]
01292232b4 chore(deps): update pre-commit hook astral-sh/ruff-pre-commit (v0.14.14 → v0.15.0) (#446)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-10 13:30:28 +02:00
renovate[bot]
052b78f9f7 chore(deps): update pre-commit hook astral-sh/uv-pre-commit (0.9.28 → 0.10.0) (#447)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-10 13:30:12 +02:00
renovate[bot]
f371da218e chore(deps): update pre-commit hook astral-sh/ruff-pre-commit (v0.14.11 → v0.14.14) (#434)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 10:11:35 +02:00
renovate[bot]
175a9f5356 chore(deps): update pre-commit hook astral-sh/uv-pre-commit (0.9.24 → 0.9.28) (#435)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 10:11:20 +02:00
renovate[bot]
b3299e0670 chore(deps): update pre-commit hook bridgecrewio/checkov (3.2.497 → 3.2.500) (#436)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 10:11:00 +02:00
renovate[bot]
fb37d38f17 chore(deps): update actions/setup-go action (v6.1.0 → v6.2.0) (#437)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 10:10:43 +02:00
renovate[bot]
80621c08b4 chore(deps): update actions/setup-node action (v6.1.0 → v6.2.0) (#438)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 10:09:53 +02:00
renovate[bot]
77429988fd chore(deps): update raven-actions/actionlint action (v2.1.0 → v2.1.1) (#432)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 07:53:24 +02:00
renovate[bot]
f5cedd5870 chore(deps): update oven-sh/setup-bun action (v2.1.0 → v2.1.2) (#431)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 07:52:59 +02:00
renovate[bot]
0b0e96a2ed chore(deps): update actions/setup-dotnet action (v5.0.1 → v5.1.0) (#433)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 07:52:16 +02:00
renovate[bot]
3b71d19480 chore(deps): update actions/cache action (v5.0.2 → v5.0.3) (#429)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 07:51:51 +02:00
renovate[bot]
51861a9b40 chore(deps): update astral-sh/setup-uv action (v7.2.0 → v7.2.1) (#430)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 07:51:30 +02:00
renovate[bot]
f98ae7cd7d chore(deps): update actions/cache action (v5.0.1 → v5.0.2) (#426)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-21 01:45:19 +02:00
cc842575b9 fix: add tag existence check to version-maintenance workflow (#425)
* fix: add tag existence check to version-maintenance workflow

Prevents workflow failure when major version tag doesn't exist by
checking for and creating the tag before running action-versioning.

* fix: add git config for tag creation in version-maintenance workflow

GitHub Actions runners don't have default git user configuration,
which causes annotated tag creation to fail. Add user.name and
user.email config before creating tags.
2026-01-20 19:38:35 +02:00
renovate[bot]
cbfddb2433 chore(deps): update pre-commit hook astral-sh/uv-pre-commit (0.9.22 → 0.9.24) (#424)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-13 07:15:16 +02:00
renovate[bot]
5664cdbfbf chore(deps): update pre-commit hook astral-sh/ruff-pre-commit (v0.14.10 → v0.14.11) (#423)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-13 07:14:48 +02:00
renovate[bot]
e740f9d893 chore(deps): update astral-sh/setup-uv action (v7.1.6 → v7.2.0) (#422)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-12 16:35:21 +02:00
a247b78178 fix: markdownlint rules and daily releases (#421)
* fix: disable markdownlint table alignment rule

* fix(ci): daily release only if changes
2026-01-09 02:10:00 +02:00
renovate[bot]
56ff9a511c chore(deps): update oven-sh/setup-bun action (v2.0.2 → v2.1.0) (#416)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-07 10:02:49 +02:00
renovate[bot]
81310f9bd7 chore(deps): update oxsecurity/megalinter action (v9.2.0 → v9.3.0) (#417)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-07 10:02:20 +02:00
renovate[bot]
95b8856c3f chore(deps): update pre-commit hook astral-sh/uv-pre-commit (0.9.18 → 0.9.22) (#418)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-07 10:01:16 +02:00
renovate[bot]
e69ddbc1e2 chore(deps): update pre-commit hook bridgecrewio/checkov (3.2.496 → 3.2.497) (#419)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-07 10:00:55 +02:00
renovate[bot]
28e81adc2b chore(deps): update pre-commit hook rhysd/actionlint (v1.7.9 → v1.7.10) (#420)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-07 10:00:39 +02:00
renovate[bot]
fb25736f7e chore(deps): update pre-commit hook bridgecrewio/checkov (3.2.495 → 3.2.496) (#414)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-30 08:39:35 +02:00
54886c3fd5 fix: validate-inputs actions pip installation step (#413) 2025-12-24 13:29:18 +02:00
renovate[bot]
fd030b418f chore(deps): update stefanzweifel/git-auto-commit-action action (v7.0.0 → v7.1.0) (#411)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-23 13:56:47 +02:00
96c305c557 refactor: centralize validation logic with validate_with helper (#412)
* chore: sonarcloud fixes

* chore: coderabbit cr fixes
2025-12-23 13:29:37 +02:00
renovate[bot]
5b4e9c8e11 chore(deps): update docker/setup-buildx-action action (v3.11.1 → v3.12.0) (#410)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-22 07:42:23 +02:00
renovate[bot]
2d0bff84ad chore(deps): update github/issue-metrics action (v3.25.4 → v3.25.5) (#407)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-22 07:40:20 +02:00
renovate[bot]
98f260793c chore(deps): update pre-commit hook astral-sh/ruff-pre-commit (v0.14.9 → v0.14.10) (#408)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-22 07:39:39 +02:00
renovate[bot]
09ae7517d6 chore(deps): update pre-commit hook astral-sh/uv-pre-commit (0.9.17 → 0.9.18) (#409)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-22 07:39:17 +02:00
renovate[bot]
61ebe619a8 chore(deps): update github/codeql-action action (v4.31.8 → v4.31.9) (#406)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-22 07:38:46 +02:00
renovate[bot]
a1d55ac125 chore(deps)!: update peter-evans/create-pull-request (v7.0.11 → v8.0.0) (#405)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-22 03:16:24 +02:00
renovate[bot]
db86bb2f0d chore(deps)!: update actions/download-artifact (v6.0.0 → v7.0.0) (#396) 2025-12-17 23:36:39 +02:00
renovate[bot]
5e7b2fbc11 chore(deps)!: update actions/upload-artifact (v5.0.0 → v6.0.0) (#397) 2025-12-17 23:35:53 +02:00
renovate[bot]
43126631c2 chore(deps)!: update actions/cache (v4.3.0 → v5.0.1) (#395) 2025-12-17 23:35:26 +02:00
renovate[bot]
f6ed49a6dd chore(deps): update astral-sh/setup-uv action (v7.1.5 → v7.1.6) (#398)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-16 13:06:42 +02:00
renovate[bot]
23ac5dbca3 chore(deps): update github/codeql-action action (v4.31.7 → v4.31.8) (#399)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-16 13:05:57 +02:00
renovate[bot]
a8031d3922 chore(deps): update raven-actions/actionlint action (v2.0.1 → v2.1.0) (#400)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-16 13:05:24 +02:00
renovate[bot]
30149dd950 chore(deps): update image python to v3.14.2 (#401)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-16 09:41:53 +02:00
renovate[bot]
3a3cdcdefe chore(deps): update pre-commit hook astral-sh/ruff-pre-commit (v0.14.8 → v0.14.9) (#402)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-16 09:36:11 +02:00
renovate[bot]
7d28006a83 chore(deps): update pre-commit hook astral-sh/uv-pre-commit (0.9.16 → 0.9.17) (#403)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-16 09:34:05 +02:00
renovate[bot]
4008db6517 chore(deps): update markdownlint-cli2 (0.19.0 → 0.20.0) (#404)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-16 09:18:09 +02:00
renovate[bot]
7aa206a02a chore(deps): update image python to v3.14.1 (#391)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-08 15:03:07 +02:00
renovate[bot]
8481bbb5cd chore(deps): update pre-commit hook astral-sh/uv-pre-commit (0.9.13 → 0.9.16) (#393)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-08 15:02:40 +02:00
renovate[bot]
4c0068e6e7 chore(deps): update pre-commit hook davidanson/markdownlint-cli2 (v0.19.1 → v0.20.0) (#394)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-08 15:02:19 +02:00
renovate[bot]
5cecfe7cbe chore(deps): update pre-commit hook astral-sh/ruff-pre-commit (v0.14.7 → v0.14.8) (#392)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-08 15:02:01 +02:00
renovate[bot]
0288a1c8b8 chore(deps): update astral-sh/setup-uv action (v7.1.4 → v7.1.5) (#390)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-08 07:39:13 +02:00
44a11e9773 chore: update actions, cleanup pr-lint and pre-commit (#389)
* chore: update actions, cleanup pr-lint

* chore: cleanup pre-commit config, formatting

* chore: revert sigstore/cosign-installer downgrade

* chore: formatting
2025-12-07 02:24:33 +02:00
renovate[bot]
a52399cf74 chore(deps): update pre-commit hook astral-sh/ruff-pre-commit (v0.14.6 → v0.14.7) (#385)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 18:45:39 +02:00
renovate[bot]
803165db8f chore(deps): update docker/metadata-action action (v5.9.0 → v5.10.0) (#387)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 18:45:19 +02:00
renovate[bot]
d69ed9e999 chore(deps): update pre-commit hook astral-sh/uv-pre-commit (0.9.11 → 0.9.13) (#386)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 18:44:55 +02:00
renovate[bot]
8eea6f781b chore(deps): update pre-commit hook gitleaks/gitleaks (v8.29.1 → v8.30.0) (#388)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 18:44:21 +02:00
renovate[bot]
4889586a94 chore(deps): update python (3.11.14 → 3.14.0) (#382)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 09:12:42 +02:00
renovate[bot]
e02ca4d843 chore(deps): update shivammathur/setup-php action (2.35.5 → 2.36.0) (#383)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 09:10:48 +02:00
renovate[bot]
13ef0db9ba chore(deps): update oxsecurity/megalinter action (v9.1.0 → v9.2.0) (#381)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 09:10:23 +02:00
renovate[bot]
c366e99ee3 chore(deps)!: update node (22.21.1 → 24.11.1) (#380)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 07:50:44 +02:00
fbbb487332 fix(pr-lint): corepack detection, tweaks and yarn fix (#379) 2025-11-30 13:46:16 +02:00
abe24f8570 feat(ci): versioning change (#378)
* chore: remove bylines from actions

* feat: new daily release action

* chore(ci): ignore false positive in codeql, fix others

* fix: cr comments
2025-11-28 10:56:52 +02:00
9aa16a8164 feat: use our own actions in our workflows (#377)
* feat: use our own actions in our workflows

* fix: add missing inputs to validate-inputs, refactor node

* chore: cr comment fixes

* fix: update-validators formatting

* chore: update validators, add tests, conventions

* feat: validate severity with severity_enum

* feat: add 10 generic validators to improve input validation coverage

Add comprehensive validation system improvements across multiple phases:

Phase 2A - Quick Wins:
- Add multi_value_enum validator for 2-10 value enumerations
- Add exit_code_list validator for Unix/Linux exit codes (0-255)
- Refactor coverage_driver to use multi_value_enum

Phase 2B - High-Value Validators:
- Add key_value_list validator with shell injection prevention
- Add path_list validator with path traversal and glob support

Quick Wins - Additional Enums:
- Add network_mode validator for Docker network modes
- Add language_enum validator for language detection
- Add framework_mode validator for PHP framework modes
- Update boolean pattern to include 'push'

Phase 2C - Specialized Validators:
- Add json_format validator for JSON syntax validation
- Add cache_config validator for Docker BuildKit cache configs

Improvements:
- All validators include comprehensive security checks
- Pattern-based validation with clear error messages
- 23 new test methods with edge case coverage
- Update special case mappings for 20+ inputs
- Fix build-args mapping test expectation

Coverage impact: 22 actions now at 100% validation (88% → 92%)
Test suite: 762 → 785 tests (+23 tests, all passing)

* chore: regenerate rules.yml with improved validator coverage

Regenerate validation rules for all actions with new validators:

- compress-images: 86% → 100% (+1 input: ignore-paths)
- docker-build: 63% → 100% (+4 inputs: cache configs, platform-build-args)
- docker-publish: 73% → 100% (+1 input: build-args)
- language-version-detect: 67% → 100% (+1 input: language)
- php-tests: 89% (fixed framework→framework_mode mapping)
- prettier-lint: 86% → 100% (+2 inputs: file-pattern, plugins)
- security-scan: 86% (maintained coverage)

Overall: 23 of 25 actions now at 100% validation coverage (92%)

* fix: address PR #377 review comments

- Add | None type annotations to 6 optional parameters (PEP 604)
- Standardize injection pattern: remove @# from comma_separated_list validator
  (@ and # are not shell injection vectors, allows npm scoped packages)
- Remove dead code: unused value expression in key_value_list validator
- Update tests to reflect injection pattern changes
2025-11-25 23:51:03 +02:00
e58465e5d3 chore(new-release): add prefix v, add security as type (#376)
* chore(new-release): add prefix v, add security as type

* fix(pr-lint): fix pr-lint workflow

* fix(lint): prettier format
2025-11-25 14:15:55 +02:00
9fe05efeec chore: update workflows (#375) 2025-11-25 13:31:36 +02:00
449669120c fix: pr-lint UID, use printf instead of echo, tweaks (#374) 2025-11-25 13:31:11 +02:00
github-actions[bot]
d9098ddead chore: update action references to v2025 (5cc7373a22) (#372)
This commit updates all internal action references to point to the latest

v2025 tag SHA.
2025-11-25 12:35:25 +02:00
f37d940c72 fix: remove --verbose flag from PHPUnit command (#373)
Root Cause:
- PHPUnit 10 and 11 (released 2024) removed the --verbose flag
- Running `vendor/bin/phpunit --verbose` results in "Unknown option" error
- Multiple projects (Debian packages, Laravel) encountered this in 2024

Evidence:
- GitHub issues #5647 in sebastianbergmann/phpunit
- Debian bug reports #1070508, #1070509, #1070510
- Laravel framework discussion #46672

Solution:
- Remove --verbose flag from line 457
- PHPUnit's default output is sufficient for parsing
- Output parsing logic (lines 465-486) works with standard format
- Tests confirm action designed for PHPUnit 10+ output format

Impact:
- Fixes compatibility with PHPUnit 10.x and 11.x
- Maintains backward compatibility (flag was informational only)
- No change to test result parsing behavior
2025-11-25 12:25:29 +02:00
renovate[bot]
eea547998d chore(deps): update docker/build-push-action action (v6.9.0 → v6.18.0) (#371)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 07:58:54 +02:00
renovate[bot]
49159fc895 chore(deps): update actions/setup-go action (v6.0.0 → v6.1.0) (#370)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 07:57:09 +02:00
renovate[bot]
89fd0f3627 chore(deps): update pre-commit hook rhysd/actionlint (v1.7.8 → v1.7.9) (#369)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 07:53:25 +02:00
renovate[bot]
83cf08ff76 chore(deps): update pre-commit hook renovatebot/pre-commit-hooks (42.19.0 → 42.19.3) (#368)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 07:53:07 +02:00
renovate[bot]
90ab7c645c chore(deps): update pre-commit hook gitleaks/gitleaks (v8.29.0 → v8.29.1) (#367)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 07:52:40 +02:00
renovate[bot]
d05e898ea9 chore(deps): update astral-sh/setup-uv action (v7.1.3 → v7.1.4) (#362)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 01:13:56 +02:00
renovate[bot]
650ebb87b8 chore(deps): update peter-evans/create-pull-request action (v7.0.8 → v7.0.9) (#363)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 01:13:38 +02:00
renovate[bot]
13316bd827 chore(deps): update pre-commit hook astral-sh/ruff-pre-commit (v0.14.5 → v0.14.6) (#364)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 01:13:04 +02:00
renovate[bot]
350fd30043 chore(deps): update pre-commit hook bridgecrewio/checkov (3.2.494 → 3.2.495) (#365)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 01:12:46 +02:00
renovate[bot]
587853a9cd chore(deps): update pre-commit hook davidanson/markdownlint-cli2 (v0.19.0 → v0.19.1) (#366)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 01:12:28 +02:00
Copilot
6cde6d088d fix: disable MegaLinter autofixing in pr-lint action (#361)
* Initial plan

* fix: disable MegaLinter autofixing in pr-lint action

Co-authored-by: ivuorinen <11024+ivuorinen@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: ivuorinen <11024+ivuorinen@users.noreply.github.com>
2025-11-24 00:38:14 +02:00
104 changed files with 4758 additions and 2630 deletions

View File

@@ -0,0 +1,30 @@
You review action.yml files against the repository's critical prevention rules.
Check each action.yml file for these violations:
1. All external action refs are SHA-pinned (not @main/@v1)
2. All internal action refs use `ivuorinen/actions/name@SHA` format
3. Shell scripts use `set -eu` (POSIX, not bash)
4. Steps with referenced outputs have `id:` fields
5. Tool availability checked before use (`command -v`)
6. Variables properly quoted (`"$var"`)
7. `$GITHUB_OUTPUT` uses `printf`, not `echo`
8. No nested `${{ }}` in quoted YAML strings
9. Token inputs use `${{ github.token }}` default
10. Fallbacks provided for tools not on all runners
Run `actionlint` on each file. Report violations with file path, line, and fix suggestion.
To find all action.yml files:
```bash
find . -name "action.yml" -not -path "./.git/*"
```
For each file, read it and check against all 10 rules. Then run:
```bash
actionlint <file>
```
Output a summary table of violations found, grouped by action.

View File

@@ -0,0 +1,33 @@
You review test coverage for GitHub Actions in this monorepo.
For each action:
1. Read the action.yml to understand inputs, outputs, and steps
2. Read the corresponding test files in `_tests/unit/<action-name>/`
3. Check if all inputs have validation tests
4. Check if error paths are tested (missing required inputs, invalid values)
5. Check if shell scripts have edge case tests (spaces in paths, empty strings, special chars)
6. Report coverage gaps with specific test suggestions
To find all actions and their tests:
```bash
ls -d */action.yml | sed 's|/action.yml||'
ls -d _tests/unit/*/
```
Compare the two lists to find actions without any tests.
For each action with tests, check coverage of:
- All required inputs validated
- All optional inputs with defaults tested
- Error conditions (missing inputs, invalid formats)
- Edge cases in shell logic (empty strings, special characters, spaces in paths)
- Output values verified
Output a coverage report with:
- Actions with no tests (critical)
- Actions with partial coverage (list missing test cases)
- Actions with good coverage (brief confirmation)

View File

@@ -0,0 +1,21 @@
#!/bin/sh
set -eu
# Read JSON input from stdin to get the file path
if ! command -v jq >/dev/null 2>&1; then
echo "Error: jq is required but not found" >&2
exit 1
fi
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.filePath // empty')
if [ -z "$FILE_PATH" ]; then
exit 0
fi
case "$FILE_PATH" in
*/rules.yml)
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"rules.yml files are auto-generated. Run make update-validators instead."}}'
;;
esac

View File

@@ -0,0 +1,46 @@
#!/bin/sh
set -eu
# Read JSON input from stdin to get the file path
if ! command -v jq >/dev/null 2>&1; then
echo "Error: jq is required but not found" >&2
exit 1
fi
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.filePath // empty')
if [ -z "$FILE_PATH" ]; then
exit 0
fi
case "$FILE_PATH" in
*/rules.yml)
# rules.yml should not be reached here (blocked by PreToolUse),
# but skip formatting just in case
exit 0
;;
*.py)
ruff format --quiet "$FILE_PATH" 2>/dev/null || true
ruff check --fix --quiet "$FILE_PATH" 2>/dev/null || true
;;
*.sh)
shfmt -w "$FILE_PATH" 2>/dev/null || true
shellcheck "$FILE_PATH" 2>&1 || true
;;
*.yml | *.yaml | *.json)
npx prettier --write "$FILE_PATH" 2>/dev/null || true
;;
*.md)
npx prettier --write "$FILE_PATH" 2>/dev/null || true
;;
esac
# Run actionlint on action.yml files
case "$FILE_PATH" in
*/action.yml)
if command -v actionlint >/dev/null 2>&1; then
actionlint "$FILE_PATH" 2>&1 || true
fi
;;
esac

26
.claude/settings.json Normal file
View File

@@ -0,0 +1,26 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rules-yml.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/post-edit-write.sh"
}
]
}
]
}
}

View File

@@ -0,0 +1,40 @@
---
name: check-pins
description: Verify all action references are properly SHA-pinned
disable-model-invocation: true
---
# Check SHA-Pinned Action References
## 1. Check version references
```bash
make check-version-refs
```
This verifies that all `ivuorinen/actions/*` references in `action.yml` files use SHA-pinned commits.
## 2. Check local references
```bash
make check-local-refs
```
This verifies that test workflows use `./action-name` format (local references are allowed in tests).
## 3. Interpret results
**Violations to fix:**
- `@main` or `@v*` references in `action.yml` files must be replaced with full SHA commits
- `./action-name` in `action.yml` (non-test) files must use `ivuorinen/actions/action-name@<SHA>`
- External actions must be pinned to SHA commits, not version tags
**How to get the SHA for pinning:**
```bash
# After pushing, get the SHA of the latest commit on the remote
git rev-parse origin/main
```
Use a SHA that exists on the remote. Local-only commits won't resolve when the action is used externally.

View File

@@ -0,0 +1,60 @@
---
name: new-action
description: Scaffold a new GitHub Action with all required files
disable-model-invocation: true
---
# Scaffold a New GitHub Action
## 1. Gather information
Ask the user for:
- **Action name** (kebab-case, e.g. `my-new-action`)
- **Description** (one line)
- **Category** (setup, linting, testing, build, publishing, repository, utility)
- **Inputs** (name, description, required, default for each)
- **What it does** (shell commands, composite steps, etc.)
## 2. Create directory and action.yml
Create `<action-name>/action.yml` following the existing action patterns:
- Use `composite` runs type
- Include `set -eu` in shell scripts (POSIX sh, not bash)
- Use `${{ github.token }}` for token defaults
- Pin all external action references to SHA commits
- Pin internal action references using `ivuorinen/actions/action-name@<SHA>`
- Add `id:` to steps whose outputs are referenced
## 3. Generate validation rules
```bash
make update-validators
```
This generates `<action-name>/rules.yml` from the action's inputs.
## 4. Generate test scaffolding
```bash
make generate-tests
```
## 5. Generate README
```bash
make docs
```
## 6. Run validation
```bash
make all
```
Fix any issues before considering the action complete.
## 7. Update repository overview
Remind the user to update the Serena memory `repository_overview` if they use Serena.

View File

@@ -0,0 +1,57 @@
---
name: release
description: Create a new CalVer release with validation checks
disable-model-invocation: true
---
# Release Workflow
Follow these steps to create a new CalVer release:
## 1. Pre-flight checks
Run the full validation pipeline:
```bash
make all
```
If any step fails, fix the issues before proceeding.
## 2. Check version references
Verify all action references are properly pinned:
```bash
make check-version-refs
make check-local-refs
```
## 3. Prepare the release
Run release preparation (updates version references):
```bash
make release-prep
```
Review the changes with `git diff`.
## 4. Confirm with user
Ask the user to confirm:
- The version number (defaults to `vYYYY.MM.DD` based on today's date)
- That all changes look correct
## 5. Create the release
```bash
make release VERSION=vYYYY.MM.DD
```
Replace `vYYYY.MM.DD` with the confirmed version.
## 6. Verify
Show the user the created tag and any output from the release process.

View File

@@ -0,0 +1,34 @@
---
name: test-action
description: Run tests for a specific GitHub Action by name
disable-model-invocation: true
---
# Test a Specific Action
## 1. Identify the action
Ask the user which action to test if not already specified.
List available actions if needed:
```bash
ls -d */action.yml | sed 's|/action.yml||'
```
## 2. Run tests
```bash
make test-action ACTION=<action-name>
```
## 3. Display results
Show the test output. If tests fail, read the relevant test files in `_tests/unit/<action-name>/` and the action's `action.yml` to help diagnose the issue.
## 4. Coverage (optional)
If the user wants coverage information:
```bash
make test-coverage
```

View File

@@ -0,0 +1,51 @@
---
name: validate
description: Run full validation pipeline (docs, format, lint, precommit)
disable-model-invocation: true
---
# Full Validation Pipeline
Run the complete validation pipeline:
```bash
make all
```
This runs in order: `install-tools` -> `update-validators` -> `docs` -> `update-catalog` -> `format` -> `lint` -> `precommit`
**Note:** `make test` must be run separately.
## If validation fails
### Formatting issues
```bash
make format
```
Then re-run `make all`.
### Linting issues
- **actionlint**: Check action.yml syntax, step IDs, expression usage
- **shellcheck**: POSIX compliance, quoting, variable usage
- **ruff**: Python style and errors
- **markdownlint**: Markdown formatting
- **prettier**: YAML/JSON/MD formatting
### Test failures
```bash
make test
```
Read the failing test output and fix the underlying action or test.
### Documentation drift
```bash
make docs
```
Regenerates READMEs from action.yml files.

View File

@@ -17,12 +17,12 @@ runs:
using: composite
steps:
- name: Install uv
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1
with:
enable-cache: true
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version-file: pyproject.toml
@@ -31,7 +31,7 @@ runs:
run: uv sync --frozen
- name: Setup Node.js
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: '24'
cache: npm

View File

@@ -15,3 +15,19 @@ paths-ignore:
# Use security and quality query suite
queries:
- uses: security-and-quality
# Suppress specific false positives
# These findings have been manually reviewed and determined to be false positives
# with appropriate security controls in place
query-filters:
# docker-publish: Code injection in validated context
# False positive: User input is validated and sanitized before use
# - Only relative paths and trusted git URLs are allowed
# - Absolute paths and arbitrary URLs are rejected
# - Path traversal attempts are blocked
# - Custom contexts require explicit opt-in via use-custom-context: true
# - Wraps docker/build-push-action (trusted Docker-maintained action)
# - Action is designed for trusted workflows only (documented in action.yml)
- exclude:
id: js/actions/code-injection
kind: problem

View File

@@ -1,6 +1,7 @@
module.exports = {
types: [
{ types: ['feat', 'feature', 'Feat'], label: '🎉 New Features' },
{ types: ['security'], label: '🔐 Security' },
{ types: ['fix', 'bugfix', 'Fix'], label: '🐛 Bugfixes' },
{ types: ['improvements', 'enhancement'], label: '🔨 Improvements' },
{ types: ['perf'], label: '🏎️ Performance Improvements' },

View File

@@ -17,10 +17,7 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
actions: read
pull-requests: read
permissions: {}
jobs:
analyze:
@@ -29,6 +26,9 @@ jobs:
timeout-minutes: 30
permissions:
contents: read
actions: read
pull-requests: read
security-events: write
statuses: write
issues: write
@@ -39,212 +39,30 @@ jobs:
with:
fetch-depth: 0
- name: Check Required Configurations
id: check-configs
shell: sh
run: |
# Initialize all flags as false
{
echo "run_gitleaks=false"
echo "run_trivy=true"
} >> "$GITHUB_OUTPUT"
# Check Gitleaks configuration and license
if [ -f ".gitleaks.toml" ] && [ -n "${{ secrets.GITLEAKS_LICENSE }}" ]; then
echo "Gitleaks config and license found"
printf '%s\n' "run_gitleaks=true" >> "$GITHUB_OUTPUT"
else
echo "::warning::Gitleaks config or license missing - skipping Gitleaks scan"
fi
- name: Run actionlint
uses: raven-actions/actionlint@3a24062651993d40fed1019b58ac6fbdfbf276cc # v2.0.1
- name: Run Security Scan
id: security-scan
uses: ./security-scan
with:
cache: true
fail-on-error: true
shellcheck: false
- name: Run Gitleaks
if: steps.check-configs.outputs.run_gitleaks == 'true'
uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2.3.9
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
with:
config-path: .gitleaks.toml
report-format: sarif
report-path: gitleaks-report.sarif
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@a11da62073708815958ea6d84f5650c78a3ef85b # master
with:
scan-type: 'fs'
scanners: 'vuln,config,secret'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
timeout: '10m'
- name: Verify SARIF files
id: verify-sarif
shell: sh
run: |
# Initialize outputs
{
echo "has_trivy=false"
echo "has_gitleaks=false"
} >> "$GITHUB_OUTPUT"
# Check Trivy results
if [ -f "trivy-results.sarif" ]; then
if jq -e . </dev/null 2>&1 <"trivy-results.sarif"; then
printf '%s\n' "has_trivy=true" >> "$GITHUB_OUTPUT"
else
echo "::warning::Trivy SARIF file exists but is not valid JSON"
fi
fi
# Check Gitleaks results if it ran
if [ "${{ steps.check-configs.outputs.run_gitleaks }}" = "true" ]; then
if [ -f "gitleaks-report.sarif" ]; then
if jq -e . </dev/null 2>&1 <"gitleaks-report.sarif"; then
printf '%s\n' "has_gitleaks=true" >> "$GITHUB_OUTPUT"
else
echo "::warning::Gitleaks SARIF file exists but is not valid JSON"
fi
fi
fi
- name: Upload Trivy results
if: steps.verify-sarif.outputs.has_trivy == 'true'
uses: github/codeql-action/upload-sarif@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
with:
sarif_file: 'trivy-results.sarif'
category: 'trivy'
- name: Upload Gitleaks results
if: steps.verify-sarif.outputs.has_gitleaks == 'true'
uses: github/codeql-action/upload-sarif@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
with:
sarif_file: 'gitleaks-report.sarif'
category: 'gitleaks'
- name: Archive security reports
if: always()
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: security-reports-${{ github.run_id }}
path: |
${{ steps.verify-sarif.outputs.has_trivy == 'true' && 'trivy-results.sarif' || '' }}
${{ steps.verify-sarif.outputs.has_gitleaks == 'true' && 'gitleaks-report.sarif' || '' }}
retention-days: 30
- name: Analyze Results
if: always()
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const fs = require('fs');
try {
let totalIssues = 0;
let criticalIssues = 0;
const analyzeSarif = (file, tool) => {
if (!fs.existsSync(file)) {
console.log(`No results file found for ${tool}`);
return null;
}
try {
const sarif = JSON.parse(fs.readFileSync(file, 'utf8'));
return sarif.runs.reduce((acc, run) => {
if (!run.results) return acc;
const critical = run.results.filter(r =>
r.level === 'error' ||
r.level === 'critical' ||
(r.ruleId || '').toLowerCase().includes('critical')
).length;
return {
total: acc.total + run.results.length,
critical: acc.critical + critical
};
}, { total: 0, critical: 0 });
} catch (error) {
console.log(`Error analyzing ${tool} results: ${error.message}`);
return null;
}
};
// Only analyze results from tools that ran successfully
const results = {
trivy: ${{ steps.verify-sarif.outputs.has_trivy }} ?
analyzeSarif('trivy-results.sarif', 'trivy') : null,
gitleaks: ${{ steps.verify-sarif.outputs.has_gitleaks }} ?
analyzeSarif('gitleaks-report.sarif', 'gitleaks') : null
};
// Aggregate results
Object.entries(results).forEach(([tool, result]) => {
if (result) {
totalIssues += result.total;
criticalIssues += result.critical;
console.log(`${tool}: ${result.total} total, ${result.critical} critical issues`);
}
});
// Create summary
const summary = `## Security Scan Summary
- Total Issues Found: ${totalIssues}
- Critical Issues: ${criticalIssues}
### Tool Breakdown
${Object.entries(results)
.filter(([_, r]) => r)
.map(([tool, r]) =>
`- ${tool}: ${r.total} total, ${r.critical} critical`
).join('\n')}
### Tools Run Status
- Trivy: ${{ steps.verify-sarif.outputs.has_trivy }}
- Gitleaks: ${{ steps.check-configs.outputs.run_gitleaks }}
`;
// Set output
core.setOutput('total_issues', totalIssues);
core.setOutput('critical_issues', criticalIssues);
// Add job summary
await core.summary
.addRaw(summary)
.write();
// Fail if critical issues found
if (criticalIssues > 0) {
core.setFailed(`Found ${criticalIssues} critical security issues`);
}
} catch (error) {
core.setFailed(`Analysis failed: ${error.message}`);
}
gitleaks-license: ${{ secrets.GITLEAKS_LICENSE }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Notify on Critical Issues
if: failure()
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
if: failure() && steps.security-scan.outputs.critical_issues != '0'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |-
const { repo, owner } = context.repo;
const critical = core.getInput('critical_issues');
const critical = '${{ steps.security-scan.outputs.critical_issues }}';
const total = '${{ steps.security-scan.outputs.total_issues }}';
const body = `🚨 Critical security issues found in GitHub Actions
${critical} critical security issues were found during the security scan.
${critical} critical security issues (out of ${total} total) were found during the security scan.
### Scan Results
- Trivy: ${{ steps.verify-sarif.outputs.has_trivy == 'true' && 'Completed' || 'Skipped/Failed' }}
- Gitleaks: ${{ steps.check-configs.outputs.run_gitleaks == 'true' && 'Completed' || 'Skipped' }}
- Actionlint: Completed
- Trivy: ${{ steps.security-scan.outputs.has_trivy_results == 'true' && 'Completed' || 'Skipped/Failed' }}
- Gitleaks: ${{ steps.security-scan.outputs.has_gitleaks_results == 'true' && 'Completed' || 'Skipped' }}
[View detailed scan results](https://github.com/${owner}/${repo}/actions/runs/${context.runId})

View File

@@ -23,25 +23,26 @@ on:
default: 'latest'
type: string
permissions:
contents: read
packages: write
permissions: {}
jobs:
build-and-push:
name: Build and Push Testing Image
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Log in to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -49,7 +50,7 @@ jobs:
- name: Extract metadata
id: meta
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
images: ghcr.io/${{ github.repository_owner }}/actions
tags: |
@@ -59,7 +60,7 @@ jobs:
type=raw,value=${{ github.event.inputs.tag }},enable=${{ github.event.inputs.tag != '' }}
- name: Build and push Docker image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
context: _tools/docker-testing-tools
file: _tools/docker-testing-tools/Dockerfile

View File

@@ -13,17 +13,16 @@ on:
- cron: '30 1 * * 0' # Run at 1:30 AM UTC every Sunday
merge_group:
permissions:
actions: read
contents: read
permissions: {}
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ubuntu-latest
permissions:
security-events: write
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
@@ -42,4 +41,5 @@ jobs:
with:
language: ${{ matrix.language }}
queries: security-and-quality
config-file: .github/codeql/codeql-config.yml
token: ${{ github.token }}

View File

@@ -1,51 +0,0 @@
---
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: 'CodeQL'
on:
push:
branches:
- 'main'
pull_request:
branches:
- 'main'
schedule:
- cron: '30 1 * * 0' # Run at 1:30 AM UTC every Sunday
merge_group:
permissions:
actions: read
contents: read
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
security-events: write
strategy:
fail-fast: false
matrix:
language:
- 'actions'
- 'javascript'
- 'python'
steps: # Add languages used in your actions
- name: Checkout repository
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta
- name: Initialize CodeQL
uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
with:
languages: ${{ matrix.language }}
queries: security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
with:
category: '/language:${{matrix.language}}'

View File

@@ -4,14 +4,15 @@ name: 'Dependency Review'
on:
- pull_request
permissions:
contents: read
permissions: {}
jobs:
dependency-review:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: 'Checkout Repository'
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta
- name: 'Dependency Review'
uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2
uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0

View File

@@ -5,8 +5,7 @@ on:
schedule:
- cron: '3 2 1 * *'
permissions:
contents: read
permissions: {}
jobs:
build:
@@ -30,7 +29,7 @@ jobs:
echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV"
- name: Run issue-metrics tool
uses: github/issue-metrics@637a24e71b78bc10881e61972b19ea9ff736e14a # v3.25.2
uses: github/issue-metrics@41a7961f701cc64490f32e143af8ef479b93e87d # v4.1.0
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SEARCH_QUERY: 'repo:ivuorinen/actions is:issue created:${{ env.last_month }} -reason:"not planned"'

View File

@@ -6,7 +6,7 @@ on:
schedule:
- cron: '0 21 * * *' # 00:00 at Europe/Helsinki
permissions: read-all
permissions: {}
jobs:
new-daily-release:
@@ -21,26 +21,45 @@ jobs:
steps:
- uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta
with:
fetch-depth: 0 # Fetch all history and tags for comparison
- name: Create tag if necessary
uses: fregante/daily-version-action@fb1a60b7c4daf1410cd755e360ebec3901e58588 # v2.1.3
- name: Create daily release
id: daily-version
run: |
set -eu
- name: Create changelog text
if: steps.daily-version.outputs.created
id: changelog
uses: loopwerk/tag-changelog@941366edb8920e2071eae0449031830984b9f26e # v1.3.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
config_file: .github/tag-changelog-config.js
VERSION="v$(date '+%Y.%m.%d')"
printf '%s\n' "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Create release
if: steps.daily-version.outputs.created
uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0
# Check if release already exists
if gh release view "$VERSION" >/dev/null 2>&1; then
printf '%s\n' "created=false" >> "$GITHUB_OUTPUT"
printf '%s\n' "Release $VERSION already exists - skipping"
exit 0
fi
# Get the most recent tag
PREVIOUS_TAG=$(git tag --sort=-version:refname | head -1)
# Check if there are any changes since the previous tag
if [ -n "$PREVIOUS_TAG" ]; then
CHANGES=$(git rev-list "$PREVIOUS_TAG"..HEAD --count)
if [ "$CHANGES" -eq 0 ]; then
printf '%s\n' "created=false" >> "$GITHUB_OUTPUT"
printf '%s\n' "No changes since $PREVIOUS_TAG - skipping release"
exit 0
fi
printf '%s\n' "Found $CHANGES commit(s) since $PREVIOUS_TAG"
fi
# Create release with auto-generated changelog (also creates tag)
gh release create "$VERSION" \
--title "Release $VERSION" \
--generate-notes \
--target main
printf '%s\n' "created=true" >> "$GITHUB_OUTPUT"
printf '%s\n' "Created release $VERSION"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag: ${{ steps.daily-version.outputs.version }}
name: Release ${{ steps.daily-version.outputs.version }}
body: ${{ steps.changelog.outputs.changes }}
allowUpdates: true
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -24,17 +24,9 @@ on:
merge_group:
env:
# Apply linter fixes configuration
APPLY_FIXES: all
APPLY_FIXES_EVENT: pull_request
APPLY_FIXES_MODE: commit
# Disable linters that do not work or conflict
# MegaLinter configuration - these override the action's defaults
DISABLE_LINTERS: REPOSITORY_DEVSKIM
# Additional settings
VALIDATE_ALL_CODEBASE: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
GITHUB_TOKEN: ${{ secrets.FIXIMUS_TOKEN || secrets.GITHUB_TOKEN }}
# Report configuration
REPORT_OUTPUT_FOLDER: megalinter-reports
@@ -45,9 +37,7 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
packages: read # Required for private dependencies
permissions: {}
jobs:
megalinter:
@@ -72,111 +62,27 @@ jobs:
token: ${{ secrets.FIXIMUS_TOKEN || secrets.GITHUB_TOKEN }}
fetch-depth: 0
- name: MegaLinter
id: ml
uses: oxsecurity/megalinter/flavors/cupcake@62c799d895af9bcbca5eacfebca29d527f125a57 # v9.1.0
- name: Check MegaLinter Results
id: check-results
if: always()
shell: sh
run: |
printf '%s\n' "status=success" >> "$GITHUB_OUTPUT"
if [ -f "${{ env.REPORT_OUTPUT_FOLDER }}/megalinter.log" ]; then
if grep -q "ERROR\|CRITICAL" "${{ env.REPORT_OUTPUT_FOLDER }}/megalinter.log"; then
echo "Linting errors found"
printf '%s\n' "status=failure" >> "$GITHUB_OUTPUT"
fi
else
echo "::warning::MegaLinter log file not found"
fi
- name: Upload Reports
if: always()
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
- name: Run MegaLinter
id: pr-lint
uses: ./pr-lint
with:
name: MegaLinter reports
path: |
megalinter-reports
mega-linter.log
retention-days: 30
token: ${{ secrets.FIXIMUS_TOKEN || secrets.GITHUB_TOKEN }}
username: fiximus
email: github-bot@ivuorinen.net
- name: Upload SARIF Report
if: always() && hashFiles('megalinter-reports/sarif/*.sarif')
uses: github/codeql-action/upload-sarif@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with:
sarif_file: megalinter-reports/sarif
category: megalinter
- name: Prepare Git for Fixes
if: steps.ml.outputs.has_updated_sources == 1
shell: sh
run: |
sudo chown -Rc $UID .git/
git config --global user.name "fiximus"
git config --global user.email "github-bot@ivuorinen.net"
- name: Create Pull Request
if: |
steps.ml.outputs.has_updated_sources == 1 &&
(env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) &&
env.APPLY_FIXES_MODE == 'pull_request' &&
(github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) &&
!contains(github.event.head_commit.message, 'skip fix')
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
id: cpr
with:
token: ${{ secrets.FIXIMUS_TOKEN || secrets.GITHUB_TOKEN }}
commit-message: '[MegaLinter] Apply linters automatic fixes'
title: '[MegaLinter] Apply linters automatic fixes'
labels: bot
branch: megalinter/fixes-${{ github.ref_name }}
branch-suffix: timestamp
delete-branch: true
body: |
## MegaLinter Fixes
MegaLinter has identified and fixed code style issues.
### 🔍 Changes Made
- Automated code style fixes
- Formatting improvements
- Lint error corrections
### 📝 Notes
- Please review the changes carefully
- Run tests before merging
- Verify formatting matches project standards
> Generated automatically by MegaLinter
- name: Commit Fixes
if: |
steps.ml.outputs.has_updated_sources == 1 &&
(env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) &&
env.APPLY_FIXES_MODE == 'commit' &&
github.ref != 'refs/heads/main' &&
(github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) &&
!contains(github.event.head_commit.message, 'skip fix')
uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0
with:
token: ${{ secrets.FIXIMUS_TOKEN || secrets.GITHUB_TOKEN }}
branch: ${{ github.event.pull_request.head.ref || github.head_ref || github.ref }}
commit_message: |
style: apply MegaLinter fixes
[skip ci]
commit_user_name: fiximus
commit_user_email: github-bot@ivuorinen.net
push_options: --force
- name: Create Status Check
- name: Check Results
if: always()
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const status = '${{ steps.check-results.outputs.status }}';
const status = '${{ steps.pr-lint.outputs.validation_status }}';
const conclusion = status === 'success' ? 'success' : 'failure';
const summary = `## MegaLinter Results

View File

@@ -7,8 +7,7 @@ on:
tags:
- 'v*'
permissions:
contents: read
permissions: {}
jobs:
release:
@@ -17,6 +16,6 @@ jobs:
contents: write
steps:
- uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta
- uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
- uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with:
generate_release_notes: true

View File

@@ -18,11 +18,7 @@ on:
- '**/*.yaml'
- '.github/workflows/**'
permissions:
contents: read
pull-requests: write
issues: write
actions: read
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
@@ -32,6 +28,11 @@ jobs:
security-analysis:
name: Security Analysis
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
actions: read
steps:
- name: Checkout PR
@@ -53,7 +54,7 @@ jobs:
# Record the base commit for diffing without checking it out
# Keep PR head checked out so scanners analyze the new changes
BASE_REF="refs/remotes/origin-base/${{ github.event.pull_request.base.ref }}"
echo "BASE_REF=${BASE_REF}" >> $GITHUB_ENV
echo "BASE_REF=${BASE_REF}" >> "$GITHUB_ENV"
echo "Base ref: ${BASE_REF}"
git log -1 --oneline "${BASE_REF}"

View File

@@ -8,10 +8,7 @@ on:
workflow_call:
workflow_dispatch:
permissions:
contents: read
packages: read
statuses: read
permissions: {}
jobs:
stale:
@@ -25,7 +22,7 @@ jobs:
steps:
- name: 🚀 Run stale
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 30

View File

@@ -22,7 +22,7 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: read-all
permissions: {}
jobs:
labels:
@@ -31,6 +31,7 @@ jobs:
timeout-minutes: 10
permissions:
contents: read
issues: write
steps:

View File

@@ -73,14 +73,14 @@ jobs:
if: always()
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
if: always() && hashFiles('_tests/reports/test-results.sarif') != ''
with:
sarif_file: _tests/reports/test-results.sarif
category: github-actions-tests
- name: Upload unit test results
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
if: always()
with:
name: unit-test-results
@@ -125,15 +125,15 @@ jobs:
shell: sh
run: |
if [ -d "_tests/reports/integration" ] && [ -n "$(find _tests/reports/integration -type f 2>/dev/null)" ]; then
printf '%s\n' "reports-found=true" >> $GITHUB_OUTPUT
printf '%s\n' "reports-found=true" >> "$GITHUB_OUTPUT"
echo "Integration test reports found"
else
printf '%s\n' "reports-found=false" >> $GITHUB_OUTPUT
printf '%s\n' "reports-found=false" >> "$GITHUB_OUTPUT"
echo "No integration test reports found"
fi
- name: Upload integration test results
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
if: always() && steps.check-integration-reports.outputs.reports-found == 'true'
with:
name: integration-test-results
@@ -167,7 +167,7 @@ jobs:
run: make test-coverage
- name: Upload coverage report
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: coverage-report
path: _tests/coverage/
@@ -263,7 +263,7 @@ jobs:
steps:
- name: Download test results
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
pattern: '*-test-results'
merge-multiple: true

View File

@@ -12,15 +12,16 @@ on:
required: false
type: string
permissions:
contents: write
pull-requests: write
issues: write
permissions: {}
jobs:
check-and-update:
name: Check Version References
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
steps:
- name: Checkout Repository
@@ -40,6 +41,29 @@ jobs:
printf '%s\n' "major=v$current_year" >> "$GITHUB_OUTPUT"
fi
- name: Ensure Major Version Tag Exists
id: ensure-tag
shell: sh
env:
MAJOR_VERSION: ${{ steps.version.outputs.major }}
run: |
set -eu
git fetch --tags --force
if git rev-list -n 1 "$MAJOR_VERSION" >/dev/null 2>&1; then
echo "Tag $MAJOR_VERSION already exists"
printf '%s\n' "created=false" >> "$GITHUB_OUTPUT"
else
echo "Tag $MAJOR_VERSION not found, creating..."
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "$MAJOR_VERSION" -m "Major version $MAJOR_VERSION"
git push origin "$MAJOR_VERSION"
echo "Created and pushed tag $MAJOR_VERSION"
printf '%s\n' "created=true" >> "$GITHUB_OUTPUT"
fi
- name: Run Action Versioning
id: action-versioning
uses: ./action-versioning
@@ -49,7 +73,7 @@ jobs:
- name: Create Pull Request
if: steps.action-versioning.outputs.updated == 'true'
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore: update action references to ${{ steps.version.outputs.major }}'
@@ -68,8 +92,6 @@ jobs:
```bash
make check-version-refs
```
🤖 Auto-generated by version-maintenance workflow
branch: automated/version-update-${{ steps.version.outputs.major }}
delete-branch: true
labels: |
@@ -78,7 +100,7 @@ jobs:
- name: Check for Annual Bump
if: steps.action-versioning.outputs.needs-annual-bump == 'true'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const currentYear = new Date().getFullYear();
@@ -120,8 +142,6 @@ jobs:
\`\`\`bash
make check-version-refs
\`\`\`
🤖 Auto-generated by version-maintenance workflow
`,
labels: ['maintenance', 'high-priority']
});

View File

@@ -9,5 +9,6 @@
"siblings_only": true
},
"MD033": false,
"MD041": false
"MD041": false,
"MD060": false
}

View File

@@ -14,7 +14,7 @@ repos:
types: [markdown, python, yaml]
files: ^(docs/.*|README\.md|CONTRIBUTING\.md|CHANGELOG\.md|.*\.py|.*\.ya?ml)$
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.9.11
rev: 0.10.8
hooks:
- id: uv-lock
- id: uv-sync
@@ -44,18 +44,18 @@ repos:
args: [--autofix, --no-sort-keys]
- repo: https://github.com/DavidAnson/markdownlint-cli2
rev: v0.19.0
rev: v0.21.0
hooks:
- id: markdownlint-cli2
args: [--fix]
- repo: https://github.com/adrienverge/yamllint
rev: v1.37.1
rev: v1.38.0
hooks:
- id: yamllint
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.5
rev: v0.15.5
hooks:
# Run the linter with auto-fix
- id: ruff-check
@@ -78,24 +78,19 @@ repos:
exclude: '^_tests/.*\.sh$'
- repo: https://github.com/rhysd/actionlint
rev: v1.7.8
rev: v1.7.11
hooks:
- id: actionlint
args: ['-shellcheck=']
- repo: https://github.com/renovatebot/pre-commit-hooks
rev: 42.19.0
hooks:
- id: renovate-config-validator
- repo: https://github.com/bridgecrewio/checkov.git
rev: '3.2.494'
rev: '3.2.507'
hooks:
- id: checkov
args:
- '--quiet'
- repo: https://github.com/gitleaks/gitleaks
rev: v8.29.0
rev: v8.30.0
hooks:
- id: gitleaks

View File

@@ -1 +1 @@
3.14.0
3.14.3

View File

@@ -6,6 +6,7 @@
- **Branch**: main
- **External Usage**: `ivuorinen/actions/<action-name>@main`
- **Total Actions**: 43 self-contained actions
- **Dogfooding**: Workflows use local actions (pr-lint, codeql-analysis, security-scan)
## Structure
@@ -18,7 +19,7 @@
├── validate-inputs/ # Centralized validation
│ ├── validators/ # 9 specialized modules
│ ├── scripts/ # Rule/test generators
│ └── tests/ # 769 pytest tests
│ └── tests/ # pytest tests
├── _tests/ # ShellSpec framework
├── _tools/ # Development utilities
├── .github/workflows/ # CI/CD workflows
@@ -31,13 +32,15 @@
**Linting (13)**: ansible-lint-fix, biome-check/fix, csharp-lint-check, eslint-check/fix, go-lint, pr-lint, pre-commit, prettier-check/fix, python-lint-fix, terraform-lint-fix
**Security (1)**: security-scan (actionlint, Gitleaks, Trivy scanning)
**Build (3)**: csharp-build, go-build, docker-build
**Publishing (5)**: npm-publish, docker-publish, docker-publish-gh, docker-publish-hub, csharp-publish
**Testing (3)**: php-tests, php-laravel-phpunit, php-composer
**Repository (9)**: github-release, release-monthly, sync-labels, stale, compress-images, common-cache, common-file-check, common-retry, codeql-analysis
**Repository (8)**: github-release, release-monthly, sync-labels, stale, compress-images, common-cache, common-file-check, codeql-analysis
**Utilities (3)**: version-file-parser, version-validator, validate-inputs
@@ -74,14 +77,39 @@ make test # All tests (pytest + ShellSpec)
## Testing Framework
- **ShellSpec**: GitHub Actions and shell scripts
- **pytest**: Python validators (769 tests, 100% pass rate)
- **pytest**: Python validators (100% pass rate)
- **Test Generator**: Automatic scaffolding for new actions
## Current Status
- ✅ All tests passing (769/769)
- ✅ All tests passing
- ✅ Zero linting issues
- ✅ Modular validator architecture
- ✅ Convention-based validation
- ✅ Test generation system
- ✅ Full backward compatibility
## Dogfooding Strategy
The repository actively dogfoods its own actions in workflows:
**Fully Dogfooded Workflows**:
- **pr-lint.yml**: Uses `./pr-lint` (was 204 lines, now 112 lines - 45% reduction)
- **action-security.yml**: Uses `./security-scan` (was 264 lines, now 82 lines - 69% reduction)
- **codeql-new.yml**: Uses `./codeql-analysis`
- **sync-labels.yml**: Uses `./sync-labels`
- **version-maintenance.yml**: Uses `./action-versioning`
**Intentionally External**:
- **build-testing-image.yml**: Uses docker/\* actions directly (needs metadata extraction)
- Core GitHub actions (checkout, upload-artifact, setup-\*) kept for standardization
**Benefits**:
- Early detection of action issues
- Real-world testing of actions
- Reduced workflow duplication
- Improved maintainability
- Better documentation through usage examples

View File

@@ -2,7 +2,7 @@
## Status: PRODUCTION READY ✅
- 769 tests passing (100%)
- All tests passing (100%)
- Zero linting issues
- Modular architecture complete
@@ -60,7 +60,7 @@ validate-inputs/
├── scripts/
│ ├── update-validators.py # Rule generator
│ └── generate-tests.py # Test generator
└── tests/ # 769 pytest tests
└── tests/ # pytest tests
<action>/CustomValidator.py # Action-specific validators
```

View File

@@ -25,12 +25,22 @@
### Folders
- `.serena/` Internal config (do not edit)
- `.claude/hooks/` Claude Code hook scripts (auto-format, lint, block rules.yml edits)
- `.claude/skills/` Claude Code skills (`/release`, `/test-action`, `/new-action`, `/validate`, `/check-pins`)
- `.claude/agents/` Claude Code subagents (action-validator, test-coverage-reviewer)
- `.github/` Workflows/templates
- `_tests/` ShellSpec tests
- `_tools/` Helper tools
- `validate-inputs/` Python validation system + tests
- `*/rules.yml` Auto-generated validation rules
### Claude Code Hooks
**Auto-formatting**: PostToolUse hooks auto-format files on Edit/Write (ruff for .py, shfmt for .sh, prettier for .yml/.yaml/.json/.md, actionlint for action.yml)
**Blocked edits**: PreToolUse hook blocks direct edits to `rules.yml` (auto-generated, use `make update-validators`)
**Hook schema**: `matcher` is a regex string matching tool names (e.g. `"Edit|Write"`), not an object. File filtering done in hook scripts via stdin JSON (`jq -r '.tool_input.file_path'`)
**Reference**: `$CLAUDE_PROJECT_DIR` for project-relative paths in hook commands
### Memory System
**Location**: `.serena/memories/` (9 consolidated memories for context)

View File

@@ -22,9 +22,9 @@ Each action is fully self-contained and can be used independently in any GitHub
## 📚 Action Catalog
This repository contains **25 reusable GitHub Actions** for CI/CD automation.
This repository contains **26 reusable GitHub Actions** for CI/CD automation.
### Quick Reference (25 Actions)
### Quick Reference (26 Actions)
| Icon | Action | Category | Description | Key Features |
|:----:|:-----------------------------------------------------|:-----------|:----------------------------------------------------------------|:---------------------------------------------|
@@ -34,7 +34,7 @@ This repository contains **25 reusable GitHub Actions** for CI/CD automation.
| 🛡️ | [`codeql-analysis`][codeql-analysis] | Repository | Run CodeQL security analysis for a single language with conf... | Auto-detection, Token auth, Outputs |
| 🖼️ | [`compress-images`][compress-images] | Repository | Compress images on demand (workflow_dispatch), and at 11pm e... | Token auth, Outputs |
| 📝 | [`csharp-build`][csharp-build] | Build | Builds and tests C# projects. | Caching, Auto-detection, Token auth, Outputs |
| 📝 | [`csharp-lint-check`][csharp-lint-check] | Linting | Runs linters like StyleCop or dotnet-format for C# code styl... | Auto-detection, Token auth, Outputs |
| 📝 | [`csharp-lint-check`][csharp-lint-check] | Linting | Runs linters like StyleCop or dotnet-format for C# code styl... | Caching, Auto-detection, Token auth, Outputs |
| 📦 | [`csharp-publish`][csharp-publish] | Publishing | Publishes a C# project to GitHub Packages. | Caching, Auto-detection, Token auth, Outputs |
| 📦 | [`docker-build`][docker-build] | Build | Builds a Docker image for multiple architectures with enhanc... | Caching, Auto-detection, Token auth, Outputs |
| ☁️ | [`docker-publish`][docker-publish] | Publishing | Simple wrapper to publish Docker images to GitHub Packages a... | Token auth, Outputs |
@@ -49,6 +49,7 @@ This repository contains **25 reusable GitHub Actions** for CI/CD automation.
| ✅ | [`prettier-lint`][prettier-lint] | Linting | Run Prettier in check or fix mode with advanced configuratio... | Caching, Auto-detection, Token auth, Outputs |
| 📝 | [`python-lint-fix`][python-lint-fix] | Linting | Lints and fixes Python files, commits changes, and uploads S... | Caching, Auto-detection, Token auth, Outputs |
| 📦 | [`release-monthly`][release-monthly] | Repository | Creates a release for the current month, incrementing patch ... | Token auth, Outputs |
| 🛡️ | [`security-scan`][security-scan] | Security | Comprehensive security scanning for GitHub Actions including... | Caching, Token auth, Outputs |
| 📦 | [`stale`][stale] | Repository | A GitHub Action to close stale issues and pull requests. | Token auth, Outputs |
| 🏷️ | [`sync-labels`][sync-labels] | Repository | Sync labels from a YAML file to a GitHub repository | Token auth, Outputs |
| 🖥️ | [`terraform-lint-fix`][terraform-lint-fix] | Linting | Lints and fixes Terraform files with advanced validation and... | Token auth, Outputs |
@@ -74,7 +75,7 @@ This repository contains **25 reusable GitHub Actions** for CI/CD automation.
|:-----------------------------------------------|:------------------------------------------------------|:---------------------------------------------|:---------------------------------------------|
| 📦 [`ansible-lint-fix`][ansible-lint-fix] | Lints and fixes Ansible playbooks, commits changes... | Ansible, YAML | Caching, Token auth, Outputs |
| ✅ [`biome-lint`][biome-lint] | Run Biome linter in check or fix mode | JavaScript, TypeScript, JSON | Caching, Auto-detection, Token auth, Outputs |
| 📝 [`csharp-lint-check`][csharp-lint-check] | Runs linters like StyleCop or dotnet-format for C#... | C#, .NET | Auto-detection, Token auth, Outputs |
| 📝 [`csharp-lint-check`][csharp-lint-check] | Runs linters like StyleCop or dotnet-format for C#... | C#, .NET | Caching, Auto-detection, Token auth, Outputs |
| ✅ [`eslint-lint`][eslint-lint] | Run ESLint in check or fix mode with advanced conf... | JavaScript, TypeScript | Caching, Auto-detection, Token auth, Outputs |
| 📝 [`go-lint`][go-lint] | Run golangci-lint with advanced configuration, cac... | Go | Caching, Token auth, Outputs |
| ✅ [`pr-lint`][pr-lint] | Runs MegaLinter against pull requests | Conventional Commits | Caching, Auto-detection, Token auth, Outputs |
@@ -115,6 +116,12 @@ This repository contains **25 reusable GitHub Actions** for CI/CD automation.
| 📦 [`stale`][stale] | A GitHub Action to close stale issues and pull req... | GitHub Actions | Token auth, Outputs |
| 🏷️ [`sync-labels`][sync-labels] | Sync labels from a YAML file to a GitHub repositor... | YAML, GitHub | Token auth, Outputs |
#### 🛡️ Security (1 action)
| Action | Description | Languages | Features |
|:-------------------------------------|:------------------------------------------------------|:----------|:-----------------------------|
| 🛡️ [`security-scan`][security-scan] | Comprehensive security scanning for GitHub Actions... | - | Caching, Token auth, Outputs |
#### ✅ Validation (1 action)
| Action | Description | Languages | Features |
@@ -131,7 +138,7 @@ This repository contains **25 reusable GitHub Actions** for CI/CD automation.
| [`codeql-analysis`][codeql-analysis] | - | ✅ | ✅ | ✅ |
| [`compress-images`][compress-images] | - | - | ✅ | ✅ |
| [`csharp-build`][csharp-build] | ✅ | ✅ | ✅ | ✅ |
| [`csharp-lint-check`][csharp-lint-check] | - | ✅ | ✅ | ✅ |
| [`csharp-lint-check`][csharp-lint-check] | | ✅ | ✅ | ✅ |
| [`csharp-publish`][csharp-publish] | ✅ | ✅ | ✅ | ✅ |
| [`docker-build`][docker-build] | ✅ | ✅ | ✅ | ✅ |
| [`docker-publish`][docker-publish] | - | - | ✅ | ✅ |
@@ -146,6 +153,7 @@ This repository contains **25 reusable GitHub Actions** for CI/CD automation.
| [`prettier-lint`][prettier-lint] | ✅ | ✅ | ✅ | ✅ |
| [`python-lint-fix`][python-lint-fix] | ✅ | ✅ | ✅ | ✅ |
| [`release-monthly`][release-monthly] | - | - | ✅ | ✅ |
| [`security-scan`][security-scan] | ✅ | - | ✅ | ✅ |
| [`stale`][stale] | - | - | ✅ | ✅ |
| [`sync-labels`][sync-labels] | - | - | ✅ | ✅ |
| [`terraform-lint-fix`][terraform-lint-fix] | - | - | ✅ | ✅ |
@@ -224,6 +232,7 @@ All actions can be used independently in your workflows:
[prettier-lint]: prettier-lint/README.md
[python-lint-fix]: python-lint-fix/README.md
[release-monthly]: release-monthly/README.md
[security-scan]: security-scan/README.md
[stale]: stale/README.md
[sync-labels]: sync-labels/README.md
[terraform-lint-fix]: terraform-lint-fix/README.md

View File

@@ -183,9 +183,6 @@ validate_input_python "docker-build" "tag" "v1.0.0" # success
validate_input_python "pre-commit" "config-file" "config.yml" # success
validate_input_python "pre-commit" "config-file" "../etc/pass" # failure
# Injection detection
validate_input_python "common-retry" "command" "echo test" # success
validate_input_python "common-retry" "command" "rm -rf /; " # failure
```
### Helper Functions from spec_helper.sh
@@ -482,11 +479,6 @@ End
✅ **Always include**:
```bash
It "rejects command injection"
When call validate_input_python "common-retry" "command" "rm -rf /; "
The status should be failure
End
It "rejects path traversal"
When call validate_input_python "pre-commit" "config-file" "../etc/passwd"
The status should be failure

View File

@@ -6,8 +6,8 @@ set -euo pipefail
# Source setup utilities
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
# shellcheck source=_tests/framework/setup.sh
# shellcheck disable=SC1091
source "${SCRIPT_DIR}/setup.sh"
# Action testing utilities
@@ -57,6 +57,13 @@ get_action_name() {
uv run "$script_dir/../shared/validation_core.py" --name "$action_file"
}
get_action_runs_using() {
local action_file="$1"
local script_dir
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
uv run "$script_dir/../shared/validation_core.py" --runs-using "$action_file"
}
# Check if an input is required in an action.yml file
is_input_required() {
local action_file="$1"
@@ -69,7 +76,7 @@ is_input_required() {
required_status=$(uv run "$script_dir/../shared/validation_core.py" --property "$action_file" "$input_name" "required")
# Return 0 (success) if input is required, 1 (failure) if optional
[[ $required_status == "required" ]]
[[ "$required_status" == "required" ]]
}
# Test input validation using Python validation module
@@ -363,5 +370,5 @@ run_action_tests() {
}
# Export all functions
export -f validate_action_yml get_action_inputs get_action_outputs get_action_name is_input_required
export -f validate_action_yml get_action_inputs get_action_outputs get_action_name get_action_runs_using is_input_required
export -f test_input_validation test_action_outputs test_external_usage measure_action_time run_action_tests

View File

@@ -21,6 +21,9 @@ import sys
import yaml # pylint: disable=import-error
# Default value for unknown action names (matches shared.validation_core.DEFAULT_UNKNOWN)
_DEFAULT_UNKNOWN = "Unknown"
class ActionValidator:
"""Handles validation of GitHub Action inputs using Python regex engine."""
@@ -86,7 +89,7 @@ class ActionValidator:
return True, ""
# Check for environment variable reference (e.g., $GITHUB_TOKEN)
if re.match(r"^\$[A-Za-z_][A-Za-z0-9_]*$", token):
if re.match(r"^\$[A-Za-z_]\w*$", token, re.ASCII):
return True, ""
# Check against all known token patterns
@@ -261,7 +264,7 @@ def get_input_property(action_file: str, input_name: str, property_check: str) -
if property_check == "description":
description = input_data.get("description", "")
return description if description else "no-description"
return description or "no-description"
if property_check == "all_optional":
# Check if all inputs are optional (none are required)
@@ -330,16 +333,16 @@ def get_action_name(action_file: str) -> str:
action_file: Path to the action.yml file
Returns:
Action name or "Unknown" if not found
Action name or _DEFAULT_UNKNOWN if not found
"""
try:
with Path(action_file).open(encoding="utf-8") as f:
data = yaml.safe_load(f)
return data.get("name", "Unknown")
return data.get("name", _DEFAULT_UNKNOWN)
except Exception:
return "Unknown"
return _DEFAULT_UNKNOWN
def _show_usage():

View File

@@ -25,6 +25,9 @@ from typing import Any
import yaml # pylint: disable=import-error
# Default value for unknown items (used by ActionFileParser)
DEFAULT_UNKNOWN = "Unknown"
class ValidationCore:
"""Core validation functionality with standardized patterns and functions."""
@@ -334,7 +337,7 @@ class ValidationCore:
"""
if not value: # Empty values are generally allowed, except for specific cases
# Some inputs should not be empty even if they're optional
if action_name == "php-composer" and input_name in ["composer-version"]:
if action_name == "php-composer" and input_name == "composer-version":
return False, f"Empty {input_name} is not allowed"
return None, ""
@@ -497,9 +500,9 @@ class ActionFileParser:
"""Get the action name from an action.yml file."""
try:
data = ActionFileParser.load_action_file(action_file)
return data.get("name", "Unknown")
return data.get("name", DEFAULT_UNKNOWN)
except (OSError, ValueError, yaml.YAMLError, AttributeError):
return "Unknown"
return DEFAULT_UNKNOWN
@staticmethod
def get_action_inputs(action_file: str) -> list[str]:
@@ -521,6 +524,16 @@ class ActionFileParser:
except (OSError, ValueError, yaml.YAMLError, AttributeError):
return []
@staticmethod
def get_action_runs_using(action_file: str) -> str:
"""Get the runs.using value from an action.yml file."""
try:
data = ActionFileParser.load_action_file(action_file)
runs = data.get("runs", {})
return runs.get("using", "unknown")
except (OSError, ValueError, yaml.YAMLError, AttributeError):
return "unknown"
@staticmethod
def _get_required_property(input_data: dict, property_name: str) -> str:
"""Get the required/optional property."""
@@ -539,7 +552,7 @@ class ActionFileParser:
def _get_description_property(input_data: dict) -> str:
"""Get the description property."""
description = input_data.get("description", "")
return description if description else "no-description"
return description or "no-description"
@staticmethod
def _get_all_optional_property(inputs: dict) -> str:
@@ -787,6 +800,11 @@ Examples:
mode_group.add_argument("--inputs", metavar="ACTION_FILE", help="List action inputs")
mode_group.add_argument("--outputs", metavar="ACTION_FILE", help="List action outputs")
mode_group.add_argument("--name", metavar="ACTION_FILE", help="Get action name")
mode_group.add_argument(
"--runs-using",
metavar="ACTION_FILE",
help="Get action runs.using value",
)
mode_group.add_argument(
"--validate-yaml",
metavar="YAML_FILE",
@@ -834,6 +852,12 @@ def _handle_name_command(args):
print(name)
def _handle_runs_using_command(args):
"""Handle the runs-using command."""
runs_using = ActionFileParser.get_action_runs_using(args.runs_using)
print(runs_using)
def _handle_validate_yaml_command(args):
"""Handle the validate-yaml command."""
try:
@@ -853,6 +877,7 @@ def _execute_command(args):
"inputs": _handle_inputs_command,
"outputs": _handle_outputs_command,
"name": _handle_name_command,
"runs_using": _handle_runs_using_command,
"validate_yaml": _handle_validate_yaml_command,
}

View File

@@ -0,0 +1,116 @@
#!/usr/bin/env shellspec
# Unit tests for security-scan action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "security-scan action"
ACTION_DIR="security-scan"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating token input"
It "accepts valid GitHub token"
When call validate_input_python "security-scan" "token" "ghp_123456789012345678901234567890123456"
The status should be success
End
It "rejects injection in token"
When call validate_input_python "security-scan" "token" "token; rm -rf /"
The status should be failure
End
It "accepts empty token (optional)"
When call validate_input_python "security-scan" "token" ""
The status should be success
End
End
Context "when validating actionlint-enabled input"
It "accepts true value"
When call validate_input_python "security-scan" "actionlint-enabled" "true"
The status should be success
End
It "accepts false value"
When call validate_input_python "security-scan" "actionlint-enabled" "false"
The status should be success
End
It "rejects non-boolean value"
When call validate_input_python "security-scan" "actionlint-enabled" "maybe"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Security Scan"
End
It "defines all expected inputs"
inputs=$(get_action_inputs "$ACTION_FILE")
When call echo "$inputs"
The output should include "gitleaks-license"
The output should include "gitleaks-config"
The output should include "trivy-severity"
The output should include "trivy-scanners"
The output should include "trivy-timeout"
The output should include "actionlint-enabled"
The output should include "token"
End
It "defines all expected outputs"
outputs=$(get_action_outputs "$ACTION_FILE")
When call echo "$outputs"
The output should include "has_trivy_results"
The output should include "has_gitleaks_results"
The output should include "total_issues"
The output should include "critical_issues"
End
It "uses composite run type"
run_type=$(get_action_runs_using "$ACTION_FILE")
When call echo "$run_type"
The output should equal "composite"
End
End
Context "when validating inputs per conventions"
It "validates token against github_token convention"
When call validate_input_python "security-scan" "token" "ghp_123456789012345678901234567890123456"
The status should be success
End
It "validates actionlint-enabled as boolean"
When call validate_input_python "security-scan" "actionlint-enabled" "true"
The status should be success
End
It "rejects invalid boolean for actionlint-enabled"
When call validate_input_python "security-scan" "actionlint-enabled" "1"
The status should be failure
End
End
Context "when testing optional inputs"
It "accepts empty gitleaks-license"
When call validate_input_python "security-scan" "gitleaks-license" ""
The status should be success
End
It "accepts empty token"
When call validate_input_python "security-scan" "token" ""
The status should be success
End
It "accepts valid gitleaks-license value"
When call validate_input_python "security-scan" "gitleaks-license" "license-key-123"
The status should be success
End
End
End

View File

@@ -92,9 +92,6 @@ setup_default_inputs() {
"go-build" | "go-lint")
[[ "$input_name" != "go-version" ]] && export INPUT_GO_VERSION="1.21"
;;
"common-retry")
[[ "$input_name" != "command" ]] && export INPUT_COMMAND="echo test"
;;
"dotnet-version-detect")
[[ "$input_name" != "default-version" ]] && export INPUT_DEFAULT_VERSION="8.0"
;;
@@ -154,9 +151,6 @@ cleanup_default_inputs() {
"go-build" | "go-lint")
[[ "$input_name" != "go-version" ]] && unset INPUT_GO_VERSION
;;
"common-retry")
[[ "$input_name" != "command" ]] && unset INPUT_COMMAND
;;
"dotnet-version-detect")
[[ "$input_name" != "default-version" ]] && unset INPUT_DEFAULT_VERSION
;;
@@ -239,12 +233,6 @@ shellspec_mock_action_run() {
"common-file-check")
echo "found=true" >>"$GITHUB_OUTPUT"
;;
"common-retry")
echo "success=true" >>"$GITHUB_OUTPUT"
echo "attempts=1" >>"$GITHUB_OUTPUT"
echo "exit-code=0" >>"$GITHUB_OUTPUT"
echo "duration=5" >>"$GITHUB_OUTPUT"
;;
"compress-images")
echo "images_compressed=true" >>"$GITHUB_OUTPUT"
printf "compression_report=## Compression Results\n- 3 images compressed\n- 25%% size reduction\n" >>"$GITHUB_OUTPUT"

View File

@@ -76,11 +76,7 @@ if ! git diff --quiet; then
git commit -m "chore: bump major version from $OLD_VERSION to $NEW_VERSION
This commit updates all internal action references from $OLD_VERSION
to $NEW_VERSION.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>"
to $NEW_VERSION."
printf '%b' "${GREEN}✅ Committed version bump${NC}\n"
else

View File

@@ -95,7 +95,7 @@ runs:
find . -maxdepth 2 -name "action.yml" -path "*/action.yml" ! -path "./_*" ! -path "./.github/*" -exec grep -h "uses: ivuorinen/actions/" {} \; > "$temp_file"
while IFS= read -r line; do
current_sha=$(echo "$line" | grep -oE '@[a-f0-9]{40}' | sed 's/@//')
current_sha=$(printf '%s' "$line" | grep -oE '@[a-f0-9]{40}' | sed 's/@//')
if [ "$current_sha" != "$TAG_SHA" ]; then
echo "Found outdated reference: $current_sha (should be $TAG_SHA)"
@@ -153,11 +153,7 @@ runs:
git commit -m "chore: update action references to $MAJOR_VERSION ($TAG_SHA)" \
-m "" \
-m "This commit updates all internal action references to point to the latest" \
-m "$MAJOR_VERSION tag SHA." \
-m "" \
-m "🤖 Generated with [Claude Code](https://claude.com/claude-code)" \
-m "" \
-m "Co-Authored-By: Claude <noreply@anthropic.com>"
-m "$MAJOR_VERSION tag SHA."
commit_sha=$(git rev-parse HEAD)
printf '%s\n' "sha=$commit_sha" >> "$GITHUB_OUTPUT"

View File

@@ -45,7 +45,7 @@ runs:
steps:
- name: Validate Inputs
id: validate
uses: ivuorinen/actions/validate-inputs@8fb52522ab00fe73cf181ef299e56066f0b2c8d8
uses: ivuorinen/actions/validate-inputs@5cc7373a22402ee8985376bc713f00e09b5b2edb
with:
action-type: 'ansible-lint-fix'
token: ${{ inputs.token }}
@@ -75,15 +75,15 @@ runs:
- name: Setup Python
if: steps.check-files.outputs.files_found == 'true'
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.11'
python-version: '3.14'
cache: 'pip'
- name: Install ansible-lint
id: install-ansible-lint
if: steps.check-files.outputs.files_found == 'true'
uses: step-security/retry@e1d59ce1f574b32f0915e3a8df055cfe9f99be5d # v3
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
with:
timeout_minutes: 5
max_attempts: ${{ inputs.max-retries }}
@@ -122,7 +122,7 @@ runs:
- name: Commit Fixes
if: steps.check-files.outputs.files_found == 'true'
uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
with:
commit_message: 'style: apply ansible lint fixes'
commit_user_name: ${{ inputs.username }}
@@ -130,6 +130,6 @@ runs:
- name: Upload SARIF Report
if: steps.check-files.outputs.files_found == 'true'
uses: github/codeql-action/upload-sarif@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with:
sarif_file: ansible-lint.sarif

View File

@@ -181,9 +181,9 @@ runs:
echo "Detected package manager: $package_manager"
- name: Setup Node.js
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: '22'
node-version: '24'
- name: Enable Corepack
shell: sh
@@ -212,13 +212,13 @@ runs:
- name: Setup Bun
if: steps.detect-pm.outputs.package-manager == 'bun'
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
uses: oven-sh/setup-bun@ecf28ddc73e819eb6fa29df6b34ef8921c743461 # v2.1.3
with:
bun-version: latest
- name: Cache Node Dependencies
id: cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: node_modules
key: ${{ runner.os }}-biome-lint-${{ inputs.mode }}-${{ steps.detect-pm.outputs.package-manager }}-${{ hashFiles('package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb') }}
@@ -331,7 +331,7 @@ runs:
- name: Upload SARIF Report
if: inputs.mode == 'check' && always()
uses: github/codeql-action/upload-sarif@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with:
sarif_file: biome-report.sarif
@@ -365,7 +365,7 @@ runs:
- name: Commit and Push Fixes
if: inputs.mode == 'fix' && success()
uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
with:
commit_message: 'style: autofix Biome violations'
commit_user_name: ${{ inputs.username }}

View File

@@ -28,7 +28,8 @@ conventions:
mode: mode_enum
token: github_token
username: username
overrides: {}
overrides:
mode: mode_enum
statistics:
total_inputs: 6
validated_inputs: 6

View File

@@ -81,21 +81,13 @@ class CustomValidator(BaseValidator):
# Validate threads
if inputs.get("threads"):
result = self.codeql_validator.validate_threads(inputs["threads"])
for error in self.codeql_validator.errors:
if error not in self.errors:
self.add_error(error)
self.codeql_validator.clear_errors()
valid &= result
valid &= self.validate_with(
self.codeql_validator, "validate_threads", inputs["threads"]
)
# Validate RAM
if inputs.get("ram"):
result = self.codeql_validator.validate_ram(inputs["ram"])
for error in self.codeql_validator.errors:
if error not in self.errors:
self.add_error(error)
self.codeql_validator.clear_errors()
valid &= result
valid &= self.validate_with(self.codeql_validator, "validate_ram", inputs["ram"])
# Validate debug mode
if inputs.get("debug"):
@@ -226,19 +218,10 @@ class CustomValidator(BaseValidator):
Returns:
True if valid, False otherwise
"""
# Check for empty queries first
if not queries or not queries.strip():
self.add_error("CodeQL queries cannot be empty")
return False
# Use the CodeQL validator
result = self.codeql_validator.validate_codeql_queries(queries)
# Copy any errors from codeql validator
for error in self.codeql_validator.errors:
if error not in self.errors:
self.add_error(error)
self.codeql_validator.clear_errors()
return result
return self.validate_with(self.codeql_validator, "validate_codeql_queries", queries)
def validate_categories(self, categories: str) -> bool:
"""Validate CodeQL categories.
@@ -249,14 +232,7 @@ class CustomValidator(BaseValidator):
Returns:
True if valid, False otherwise
"""
# Use the CodeQL validator
result = self.codeql_validator.validate_category_format(categories)
# Copy any errors from codeql validator
for error in self.codeql_validator.errors:
if error not in self.errors:
self.add_error(error)
self.codeql_validator.clear_errors()
return result
return self.validate_with(self.codeql_validator, "validate_category_format", categories)
def validate_category(self, category: str) -> bool:
"""Validate CodeQL category (singular).
@@ -267,14 +243,7 @@ class CustomValidator(BaseValidator):
Returns:
True if valid, False otherwise
"""
# Use the CodeQL validator
result = self.codeql_validator.validate_category_format(category)
# Copy any errors from codeql validator
for error in self.codeql_validator.errors:
if error not in self.errors:
self.add_error(error)
self.codeql_validator.clear_errors()
return result
return self.validate_with(self.codeql_validator, "validate_category_format", category)
def validate_config_file(self, config_file: str) -> bool:
"""Validate CodeQL configuration file path.
@@ -287,21 +256,11 @@ class CustomValidator(BaseValidator):
"""
if not config_file or not config_file.strip():
return True
# Allow GitHub Actions expressions
if self.is_github_expression(config_file):
return True
# Use FileValidator for yaml file validation
result = self.file_validator.validate_yaml_file(config_file, "config-file")
# Copy any errors from file validator
for error in self.file_validator.errors:
if error not in self.errors:
self.add_error(error)
self.file_validator.clear_errors()
return result
return self.validate_with(
self.file_validator, "validate_yaml_file", config_file, "config-file"
)
def validate_database(self, database: str) -> bool:
"""Validate CodeQL database path.
@@ -312,25 +271,13 @@ class CustomValidator(BaseValidator):
Returns:
True if valid, False otherwise
"""
# Allow GitHub Actions expressions
if self.is_github_expression(database):
return True
# Use FileValidator for path validation
result = self.file_validator.validate_file_path(database, "database")
# Copy any errors from file validator
for error in self.file_validator.errors:
if error not in self.errors:
self.add_error(error)
self.file_validator.clear_errors()
result = self.validate_with(self.file_validator, "validate_file_path", database, "database")
# Database paths often contain the language
# e.g., "codeql-database/javascript" or "/tmp/codeql_databases/python"
# Just validate it's a reasonable path after basic validation
if result and database.startswith("/tmp/"): # noqa: S108
return True
return result
def validate_debug(self, debug: str) -> bool:
@@ -342,20 +289,9 @@ class CustomValidator(BaseValidator):
Returns:
True if valid, False otherwise
"""
# Allow GitHub Actions expressions
if self.is_github_expression(debug):
return True
# Use BooleanValidator
result = self.boolean_validator.validate_boolean(debug, "debug")
# Copy any errors from boolean validator
for error in self.boolean_validator.errors:
if error not in self.errors:
self.add_error(error)
self.boolean_validator.clear_errors()
return result
return self.validate_with(self.boolean_validator, "validate_boolean", debug, "debug")
def validate_upload_database(self, upload: str) -> bool:
"""Validate upload-database setting.
@@ -366,20 +302,11 @@ class CustomValidator(BaseValidator):
Returns:
True if valid, False otherwise
"""
# Allow GitHub Actions expressions
if self.is_github_expression(upload):
return True
# Use BooleanValidator
result = self.boolean_validator.validate_boolean(upload, "upload-database")
# Copy any errors from boolean validator
for error in self.boolean_validator.errors:
if error not in self.errors:
self.add_error(error)
self.boolean_validator.clear_errors()
return result
return self.validate_with(
self.boolean_validator, "validate_boolean", upload, "upload-database"
)
def validate_upload_sarif(self, upload: str) -> bool:
"""Validate upload-sarif setting.
@@ -390,20 +317,11 @@ class CustomValidator(BaseValidator):
Returns:
True if valid, False otherwise
"""
# Allow GitHub Actions expressions
if self.is_github_expression(upload):
return True
# Use BooleanValidator
result = self.boolean_validator.validate_boolean(upload, "upload-sarif")
# Copy any errors from boolean validator
for error in self.boolean_validator.errors:
if error not in self.errors:
self.add_error(error)
self.boolean_validator.clear_errors()
return result
return self.validate_with(
self.boolean_validator, "validate_boolean", upload, "upload-sarif"
)
def validate_packs(self, packs: str) -> bool:
"""Validate CodeQL packs.
@@ -487,16 +405,9 @@ class CustomValidator(BaseValidator):
Returns:
True if valid, False otherwise
"""
# Use the TokenValidator for proper validation
result = self.token_validator.validate_github_token(token, required=False)
# Copy any errors from token validator
for error in self.token_validator.errors:
if error not in self.errors:
self.add_error(error)
self.token_validator.clear_errors()
return result
return self.validate_with(
self.token_validator, "validate_github_token", token, required=False
)
def validate_token(self, token: str) -> bool:
"""Validate GitHub token.
@@ -507,21 +418,12 @@ class CustomValidator(BaseValidator):
Returns:
True if valid, False otherwise
"""
# Check for empty token
if not token or not token.strip():
self.add_error("Input 'token' is missing or empty")
return False
# Use the TokenValidator for proper validation
result = self.token_validator.validate_github_token(token, required=True)
# Copy any errors from token validator
for error in self.token_validator.errors:
if error not in self.errors:
self.add_error(error)
self.token_validator.clear_errors()
return result
return self.validate_with(
self.token_validator, "validate_github_token", token, required=True
)
def validate_working_directory(self, directory: str) -> bool:
"""Validate working directory path.
@@ -532,20 +434,11 @@ class CustomValidator(BaseValidator):
Returns:
True if valid, False otherwise
"""
# Allow GitHub Actions expressions
if self.is_github_expression(directory):
return True
# Use FileValidator for path validation
result = self.file_validator.validate_file_path(directory, "working-directory")
# Copy any errors from file validator
for error in self.file_validator.errors:
if error not in self.errors:
self.add_error(error)
self.file_validator.clear_errors()
return result
return self.validate_with(
self.file_validator, "validate_file_path", directory, "working-directory"
)
def validate_upload_results(self, value: str) -> bool:
"""Validate upload-results boolean value.
@@ -556,27 +449,14 @@ class CustomValidator(BaseValidator):
Returns:
True if valid, False otherwise
"""
# Check for empty
if not value or not value.strip():
self.add_error("upload-results cannot be empty")
return False
# Allow GitHub Actions expressions
if self.is_github_expression(value):
return True
# Check for uppercase TRUE/FALSE first
if value in ["TRUE", "FALSE"]:
self.add_error("Must be lowercase 'true' or 'false'")
return False
# Use BooleanValidator for normal validation
result = self.boolean_validator.validate_boolean(value, "upload-results")
# Copy any errors from boolean validator
for error in self.boolean_validator.errors:
if error not in self.errors:
self.add_error(error)
self.boolean_validator.clear_errors()
return result
return self.validate_with(
self.boolean_validator, "validate_boolean", value, "upload-results"
)

View File

@@ -107,7 +107,7 @@ runs:
using: composite
steps:
- name: Validate inputs
uses: ivuorinen/actions/validate-inputs@8fb52522ab00fe73cf181ef299e56066f0b2c8d8
uses: ivuorinen/actions/validate-inputs@5cc7373a22402ee8985376bc713f00e09b5b2edb
with:
action-type: codeql-analysis
language: ${{ inputs.language }}
@@ -186,7 +186,7 @@ runs:
echo "Using build mode: $build_mode"
- name: Initialize CodeQL
uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with:
languages: ${{ inputs.language }}
queries: ${{ inputs.queries }}
@@ -199,12 +199,12 @@ runs:
threads: ${{ inputs.threads }}
- name: Autobuild
uses: github/codeql-action/autobuild@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
uses: github/codeql-action/autobuild@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
if: ${{ steps.set-build-mode.outputs.build-mode == 'autobuild' }}
- name: Perform CodeQL Analysis
id: analysis
uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with:
category: ${{ steps.set-category.outputs.category }}
upload: ${{ inputs.upload-results }}

View File

@@ -42,7 +42,7 @@ conventions:
packs: codeql_packs
queries: codeql_queries
ram: numeric_range_256_32768
skip-queries: codeql_queries
skip-queries: boolean
source-root: file_path
threads: numeric_range_1_128
token: github_token
@@ -51,6 +51,7 @@ overrides:
build-mode: codeql_build_mode
category: category_format
config: codeql_config
language: codeql_language
output: file_path
packs: codeql_packs
queries: codeql_queries

View File

@@ -36,47 +36,35 @@ class CustomValidator(BaseValidator):
# Validate optional inputs
if inputs.get("image-quality"):
result = self.numeric_validator.validate_numeric_range(
inputs["image-quality"], min_val=0, max_val=100
valid &= self.validate_with(
self.numeric_validator,
"validate_numeric_range",
inputs["image-quality"],
min_val=0,
max_val=100,
)
for error in self.numeric_validator.errors:
if error not in self.errors:
self.add_error(error)
self.numeric_validator.clear_errors()
if not result:
valid = False
if inputs.get("png-quality"):
result = self.numeric_validator.validate_numeric_range(
inputs["png-quality"], min_val=0, max_val=100
valid &= self.validate_with(
self.numeric_validator,
"validate_numeric_range",
inputs["png-quality"],
min_val=0,
max_val=100,
)
for error in self.numeric_validator.errors:
if error not in self.errors:
self.add_error(error)
self.numeric_validator.clear_errors()
if not result:
valid = False
if inputs.get("directory"):
result = self.file_validator.validate_file_path(inputs["directory"], "directory")
for error in self.file_validator.errors:
if error not in self.errors:
self.add_error(error)
self.file_validator.clear_errors()
if not result:
valid = False
valid &= self.validate_with(
self.file_validator, "validate_file_path", inputs["directory"], "directory"
)
if inputs.get("ignore-paths"):
# Validate for injection
result = self.security_validator.validate_no_injection(
inputs["ignore-paths"], "ignore-paths"
valid &= self.validate_with(
self.security_validator,
"validate_no_injection",
inputs["ignore-paths"],
"ignore-paths",
)
for error in self.security_validator.errors:
if error not in self.errors:
self.add_error(error)
self.security_validator.clear_errors()
if not result:
valid = False
return valid

View File

@@ -163,7 +163,7 @@ runs:
- name: Create New Pull Request If Needed
if: steps.calibre.outputs.markdown != ''
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ inputs.token }}
title: 'chore: compress images'

View File

@@ -2,7 +2,7 @@
# Validation rules for compress-images action
# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY
# Schema version: 1.0
# Coverage: 86% (6/7 inputs)
# Coverage: 100% (7/7 inputs)
#
# This file defines validation rules for the compress-images GitHub Action.
# Rules are automatically applied by validate-inputs action when this
@@ -24,6 +24,7 @@ optional_inputs:
- working-directory
conventions:
email: email
ignore-paths: path_list
image-quality: numeric_range_0_100
png-quality: numeric_range_0_100
token: github_token
@@ -32,10 +33,10 @@ conventions:
overrides: {}
statistics:
total_inputs: 7
validated_inputs: 6
validated_inputs: 7
skipped_inputs: 0
coverage_percentage: 86
validation_coverage: 86
coverage_percentage: 100
validation_coverage: 100
auto_detected: true
manual_review_required: false
quality_indicators:

View File

@@ -148,14 +148,14 @@ runs:
echo "Final detected .NET version: $detected_version" >&2
- name: Setup .NET SDK
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
dotnet-version: ${{ steps.detect-dotnet-version.outputs.detected-version }}
cache: true
cache-dependency-path: '**/packages.lock.json'
- name: Restore Dependencies
uses: step-security/retry@e1d59ce1f574b32f0915e3a8df055cfe9f99be5d # v3
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
with:
timeout_minutes: 10
max_attempts: ${{ inputs.max-retries }}
@@ -203,7 +203,7 @@ runs:
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: csharp-test-results
path: |

View File

@@ -164,7 +164,7 @@ runs:
echo "Final detected .NET version: $detected_version" >&2
- name: Setup .NET SDK
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
dotnet-version: ${{ steps.detect-dotnet-version.outputs.detected-version }}
cache: true
@@ -206,6 +206,6 @@ runs:
fi
- name: Upload SARIF Report
uses: github/codeql-action/upload-sarif@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with:
sarif_file: dotnet-format.sarif

View File

@@ -55,7 +55,7 @@ runs:
- name: Validate Inputs
id: validate
uses: ivuorinen/actions/validate-inputs@8fb52522ab00fe73cf181ef299e56066f0b2c8d8
uses: ivuorinen/actions/validate-inputs@5cc7373a22402ee8985376bc713f00e09b5b2edb
with:
action-type: 'csharp-publish'
token: ${{ inputs.token }}
@@ -162,14 +162,14 @@ runs:
echo "Final detected .NET version: $detected_version" >&2
- name: Setup .NET SDK
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
dotnet-version: ${{ inputs.dotnet-version || steps.detect-dotnet-version.outputs.detected-version }}
cache: true
cache-dependency-path: '**/packages.lock.json'
- name: Restore Dependencies
uses: step-security/retry@e1d59ce1f574b32f0915e3a8df055cfe9f99be5d # v3
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
with:
timeout_minutes: 10
max_attempts: ${{ inputs.max-retries }}

View File

@@ -65,35 +65,24 @@ class CustomValidator(BaseValidator):
# Validate image name
if inputs.get("image-name"):
result = self.docker_validator.validate_image_name(inputs["image-name"], "image-name")
# Propagate errors from docker validator
for error in self.docker_validator.errors:
if error not in self.errors:
self.add_error(error)
self.docker_validator.clear_errors()
valid &= result
valid &= self.validate_with(
self.docker_validator, "validate_image_name", inputs["image-name"], "image-name"
)
# Validate tag (singular - as per action.yml)
if inputs.get("tag"):
result = self.docker_validator.validate_docker_tag(inputs["tag"], "tag")
# Propagate errors
for error in self.docker_validator.errors:
if error not in self.errors:
self.add_error(error)
self.docker_validator.clear_errors()
valid &= result
valid &= self.validate_with(
self.docker_validator, "validate_docker_tag", inputs["tag"], "tag"
)
# Validate architectures/platforms
if inputs.get("architectures"):
result = self.docker_validator.validate_architectures(
inputs["architectures"], "architectures"
valid &= self.validate_with(
self.docker_validator,
"validate_architectures",
inputs["architectures"],
"architectures",
)
# Propagate errors
for error in self.docker_validator.errors:
if error not in self.errors:
self.add_error(error)
self.docker_validator.clear_errors()
valid &= result
# Validate build arguments
if inputs.get("build-args"):
@@ -101,12 +90,9 @@ class CustomValidator(BaseValidator):
# Validate push flag
if inputs.get("push"):
result = self.boolean_validator.validate_optional_boolean(inputs["push"], "push")
for error in self.boolean_validator.errors:
if error not in self.errors:
self.add_error(error)
self.boolean_validator.clear_errors()
valid &= result
valid &= self.validate_with(
self.boolean_validator, "validate_optional_boolean", inputs["push"], "push"
)
# Validate cache settings
if inputs.get("cache-from"):
@@ -117,22 +103,35 @@ class CustomValidator(BaseValidator):
# Validate cache-mode
if inputs.get("cache-mode"):
valid &= self.validate_cache_mode(inputs["cache-mode"])
valid &= self.validate_enum(
inputs["cache-mode"],
"cache-mode",
["min", "max", "inline"],
case_sensitive=True,
)
# Validate buildx-version
if inputs.get("buildx-version"):
valid &= self.validate_buildx_version(inputs["buildx-version"])
version = inputs["buildx-version"]
# Allow 'latest' as special value
if version != "latest" and not self.is_github_expression(version):
valid &= self.validate_with(
self.version_validator,
"validate_semantic_version",
version,
"buildx-version",
)
# Validate parallel-builds
if inputs.get("parallel-builds"):
result = self.numeric_validator.validate_numeric_range(
inputs["parallel-builds"], min_val=0, max_val=16, name="parallel-builds"
valid &= self.validate_with(
self.numeric_validator,
"validate_numeric_range",
inputs["parallel-builds"],
min_val=0,
max_val=16,
name="parallel-builds",
)
for error in self.numeric_validator.errors:
if error not in self.errors:
self.add_error(error)
self.numeric_validator.clear_errors()
valid &= result
# Validate boolean flags
for bool_input in [
@@ -144,29 +143,32 @@ class CustomValidator(BaseValidator):
"auto-detect-platforms",
]:
if inputs.get(bool_input):
result = self.boolean_validator.validate_optional_boolean(
inputs[bool_input], bool_input
valid &= self.validate_with(
self.boolean_validator,
"validate_optional_boolean",
inputs[bool_input],
bool_input,
)
for error in self.boolean_validator.errors:
if error not in self.errors:
self.add_error(error)
self.boolean_validator.clear_errors()
valid &= result
# Validate sbom-format
if inputs.get("sbom-format"):
valid &= self.validate_sbom_format(inputs["sbom-format"])
valid &= self.validate_enum(
inputs["sbom-format"],
"sbom-format",
["spdx-json", "cyclonedx-json", "syft-json"],
case_sensitive=True,
)
# Validate max-retries
if inputs.get("max-retries"):
result = self.numeric_validator.validate_numeric_range(
inputs["max-retries"], min_val=0, max_val=10, name="max-retries"
valid &= self.validate_with(
self.numeric_validator,
"validate_numeric_range",
inputs["max-retries"],
min_val=0,
max_val=10,
name="max-retries",
)
for error in self.numeric_validator.errors:
if error not in self.errors:
self.add_error(error)
self.numeric_validator.clear_errors()
valid &= result
return valid
@@ -209,19 +211,11 @@ class CustomValidator(BaseValidator):
Returns:
True if valid, False otherwise
"""
# Allow GitHub Actions expressions
if self.is_github_expression(dockerfile):
return True
# Use file validator for path validation
result = self.file_validator.validate_file_path(dockerfile, "dockerfile")
# Propagate errors
for error in self.file_validator.errors:
if error not in self.errors:
self.add_error(error)
self.file_validator.clear_errors()
return result
return self.validate_with(
self.file_validator, "validate_file_path", dockerfile, "dockerfile"
)
def validate_context(self, context: str) -> bool:
"""Validate build context path.
@@ -245,10 +239,9 @@ class CustomValidator(BaseValidator):
# We allow path traversal for context as Docker needs to access parent directories
# Only check for command injection patterns like ; | ` $()
dangerous_chars = [";", "|", "`", "$(", "&&", "||"]
for char in dangerous_chars:
if char in context:
self.add_error(f"Command injection detected in context: {context}")
return False
if any(char in context for char in dangerous_chars):
self.add_error(f"Command injection detected in context: {context}")
return False
return True
@@ -261,15 +254,9 @@ class CustomValidator(BaseValidator):
Returns:
True if valid, False otherwise
"""
# Use docker validator for architectures
result = self.docker_validator.validate_architectures(platforms, "platforms")
# Propagate errors
for error in self.docker_validator.errors:
if error not in self.errors:
self.add_error(error)
self.docker_validator.clear_errors()
return result
return self.validate_with(
self.docker_validator, "validate_architectures", platforms, "platforms"
)
def validate_build_args(self, build_args: str) -> bool:
"""Validate build arguments.
@@ -353,78 +340,3 @@ class CustomValidator(BaseValidator):
# Check for security issues
return self.validate_security_patterns(cache_to, "cache-to")
def validate_cache_mode(self, cache_mode: str) -> bool:
"""Validate cache mode.
Args:
cache_mode: Cache mode value
Returns:
True if valid, False otherwise
"""
# Allow GitHub Actions expressions
if self.is_github_expression(cache_mode):
return True
# Valid cache modes
valid_modes = ["min", "max", "inline"]
if cache_mode.lower() not in valid_modes:
self.add_error(f"Invalid cache-mode: {cache_mode}. Must be one of: min, max, inline")
return False
return True
def validate_buildx_version(self, version: str) -> bool:
"""Validate buildx version.
Args:
version: Buildx version
Returns:
True if valid, False otherwise
"""
# Allow GitHub Actions expressions
if self.is_github_expression(version):
return True
# Allow 'latest'
if version == "latest":
return True
# Check for security issues (semicolon injection etc)
if not self.validate_security_patterns(version, "buildx-version"):
return False
# Basic version format validation (e.g., 0.12.0, v0.12.0)
import re
if not re.match(r"^v?\d+\.\d+(\.\d+)?$", version):
self.add_error(f"Invalid buildx-version format: {version}")
return False
return True
def validate_sbom_format(self, sbom_format: str) -> bool:
"""Validate SBOM format.
Args:
sbom_format: SBOM format value
Returns:
True if valid, False otherwise
"""
# Allow GitHub Actions expressions
if self.is_github_expression(sbom_format):
return True
# Valid SBOM formats
valid_formats = ["spdx-json", "cyclonedx-json", "syft-json"]
if sbom_format.lower() not in valid_formats:
self.add_error(
f"Invalid sbom-format: {sbom_format}. "
"Must be one of: spdx-json, cyclonedx-json, syft-json"
)
return False
return True

View File

@@ -147,7 +147,7 @@ runs:
- name: Validate Inputs
id: validate
uses: ivuorinen/actions/validate-inputs@8fb52522ab00fe73cf181ef299e56066f0b2c8d8
uses: ivuorinen/actions/validate-inputs@5cc7373a22402ee8985376bc713f00e09b5b2edb
with:
action-type: 'docker-build'
image-name: ${{ inputs.image-name }}
@@ -169,13 +169,13 @@ runs:
fi
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
with:
platforms: ${{ inputs.architectures }}
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
with:
version: ${{ inputs.buildx-version }}
platforms: ${{ inputs.architectures }}
@@ -321,7 +321,7 @@ runs:
- name: Login to GitHub Container Registry
if: ${{ inputs.push == 'true' }}
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -536,7 +536,7 @@ runs:
- name: Scan Image for Vulnerabilities
id: scan
if: inputs.scan-image == 'true' && inputs.dry-run != 'true'
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1
uses: aquasecurity/trivy-action@97e0b3872f55f89b95b2f65b3dbab56962816478 # 0.34.2
with:
scan-type: 'image'
image-ref: ${{ steps.image-name.outputs.name }}:${{ inputs.tag }}

View File

@@ -2,7 +2,7 @@
# Validation rules for docker-build action
# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY
# Schema version: 1.0
# Coverage: 63% (17/27 inputs)
# Coverage: 100% (27/27 inputs)
#
# This file defines validation rules for the docker-build GitHub Action.
# Rules are automatically applied by validate-inputs action when this
@@ -45,17 +45,27 @@ optional_inputs:
conventions:
architectures: docker_architectures
auto-detect-platforms: docker_architectures
build-args: key_value_list
build-contexts: key_value_list
buildkit-version: semantic_version
buildx-version: semantic_version
cache-mode: boolean
cache-export: cache_config
cache-from: cache_config
cache-import: cache_config
cache-mode: cache_mode
context: file_path
dockerfile: file_path
dry-run: boolean
image-name: docker_image_name
max-retries: numeric_range_1_10
network: network_mode
parallel-builds: numeric_range_0_16
platform-build-args: json_format
platform-fallback: docker_architectures
sbom-format: report_format
push: boolean
sbom-format: sbom_format
scan-image: boolean
secrets: key_value_list
sign-image: boolean
tag: docker_tag
token: github_token
@@ -65,12 +75,12 @@ overrides:
sbom-format: sbom_format
statistics:
total_inputs: 27
validated_inputs: 17
validated_inputs: 27
skipped_inputs: 0
coverage_percentage: 63
validation_coverage: 63
coverage_percentage: 100
validation_coverage: 100
auto_detected: true
manual_review_required: true
manual_review_required: false
quality_indicators:
has_required_inputs: true
has_token_validation: true

View File

@@ -11,6 +11,7 @@ This validator handles Docker publish-specific validation including:
from __future__ import annotations
from pathlib import Path
import re
import sys
# Add validate-inputs directory to path to import validators
@@ -58,12 +59,9 @@ class CustomValidator(BaseValidator):
# Validate platforms
if inputs.get("platforms"):
result = self.docker_validator.validate_architectures(inputs["platforms"], "platforms")
for error in self.docker_validator.errors:
if error not in self.errors:
self.add_error(error)
self.docker_validator.clear_errors()
valid &= result
valid &= self.validate_with(
self.docker_validator, "validate_architectures", inputs["platforms"], "platforms"
)
# Validate boolean flags
for bool_input in [
@@ -74,18 +72,18 @@ class CustomValidator(BaseValidator):
"verbose",
]:
if inputs.get(bool_input):
result = self.boolean_validator.validate_optional_boolean(
inputs[bool_input], bool_input
valid &= self.validate_with(
self.boolean_validator,
"validate_optional_boolean",
inputs[bool_input],
bool_input,
)
for error in self.boolean_validator.errors:
if error not in self.errors:
self.add_error(error)
self.boolean_validator.clear_errors()
valid &= result
# Validate cache-mode
if inputs.get("cache-mode"):
valid &= self.validate_cache_mode(inputs["cache-mode"])
valid &= self.validate_enum(
inputs["cache-mode"], "cache-mode", ["min", "max", "inline"]
)
# Validate buildx-version
if inputs.get("buildx-version"):
@@ -96,24 +94,18 @@ class CustomValidator(BaseValidator):
valid &= self.validate_username(inputs["dockerhub-username"])
if inputs.get("dockerhub-password"):
# Use token validator for password/token
result = self.token_validator.validate_docker_token(
inputs["dockerhub-password"], "dockerhub-password"
valid &= self.validate_with(
self.token_validator,
"validate_docker_token",
inputs["dockerhub-password"],
"dockerhub-password",
)
for error in self.token_validator.errors:
if error not in self.errors:
self.add_error(error)
self.token_validator.clear_errors()
valid &= result
# Validate github-token
if inputs.get("github-token"):
result = self.token_validator.validate_github_token(inputs["github-token"])
for error in self.token_validator.errors:
if error not in self.errors:
self.add_error(error)
self.token_validator.clear_errors()
valid &= result
valid &= self.validate_with(
self.token_validator, "validate_github_token", inputs["github-token"]
)
return valid
@@ -156,40 +148,7 @@ class CustomValidator(BaseValidator):
Returns:
True if valid, False otherwise
"""
# Allow GitHub Actions expressions
if self.is_github_expression(registry):
return True
# Valid registry values according to action description
valid_registries = ["dockerhub", "github", "both"]
if registry.lower() not in valid_registries:
self.add_error(
f"Invalid registry: {registry}. Must be one of: dockerhub, github, or both"
)
return False
return True
def validate_cache_mode(self, cache_mode: str) -> bool:
"""Validate cache mode.
Args:
cache_mode: Cache mode value
Returns:
True if valid, False otherwise
"""
# Allow GitHub Actions expressions
if self.is_github_expression(cache_mode):
return True
# Valid cache modes
valid_modes = ["min", "max", "inline"]
if cache_mode.lower() not in valid_modes:
self.add_error(f"Invalid cache-mode: {cache_mode}. Must be one of: min, max, inline")
return False
return True
return self.validate_enum(registry, "registry", ["dockerhub", "github", "both"])
def validate_buildx_version(self, version: str) -> bool:
"""Validate buildx version.
@@ -213,8 +172,6 @@ class CustomValidator(BaseValidator):
return False
# Basic version format validation
import re
if not re.match(r"^v?\d+\.\d+(\.\d+)?$", version):
self.add_error(f"Invalid buildx-version format: {version}")
return False
@@ -244,8 +201,6 @@ class CustomValidator(BaseValidator):
return False
# Docker Hub username rules: lowercase letters, digits, periods, hyphens, underscores
import re
if not re.match(r"^[a-z0-9._-]+$", username.lower()):
self.add_error(f"Invalid Docker Hub username format: {username}")
return False

View File

@@ -112,7 +112,7 @@ runs:
dockerhub|github|both)
;;
*)
echo "::error::Invalid registry value. Must be 'dockerhub', 'github', or 'both'"
printf '%s\n' "::error::Invalid registry value. Must be 'dockerhub', 'github', or 'both'"
exit 1
;;
esac
@@ -120,7 +120,7 @@ runs:
# Validate Docker Hub credentials if needed
if [ "$INPUT_REGISTRY" = "dockerhub" ] || [ "$INPUT_REGISTRY" = "both" ]; then
if [ -z "$INPUT_DOCKERHUB_USERNAME" ] || [ -z "$INPUT_DOCKERHUB_TOKEN" ]; then
echo "::error::Docker Hub username and token are required when publishing to Docker Hub"
printf '%s\n' "::error::Docker Hub username and token are required when publishing to Docker Hub"
exit 1
fi
fi
@@ -129,49 +129,80 @@ runs:
if [ "$INPUT_REGISTRY" = "github" ] || [ "$INPUT_REGISTRY" = "both" ]; then
token="${INPUT_TOKEN:-${GITHUB_TOKEN:-}}"
if [ -z "$token" ]; then
echo "::error::GitHub token is required when publishing to GitHub Packages"
printf '%s\n' "::error::GitHub token is required when publishing to GitHub Packages"
exit 1
fi
fi
# Validate context input for security
INPUT_CONTEXT="${INPUT_CONTEXT:-.}"
case "$INPUT_CONTEXT" in
.|./*|*/*)
# Relative paths are allowed
# Check for path traversal attempts
case "$INPUT_CONTEXT" in
*/../*|../*|*/..)
printf '%s\n' "::error::Context path contains path traversal: '$INPUT_CONTEXT'"
exit 1
;;
esac
;;
/*)
echo "::error::Context cannot be an absolute path: '$INPUT_CONTEXT'"
echo "::error::Use relative paths (e.g., '.', './app') to prevent code injection"
printf '%s\n' "::error::Context cannot be an absolute path: '$INPUT_CONTEXT'"
printf '%s\n' "::error::Use relative paths (e.g., '.', './app')"
exit 1
;;
*://*)
echo "::warning::Context is a remote URL: '$INPUT_CONTEXT'"
echo "::warning::Ensure this URL is from a trusted source to prevent code injection"
git://*|git@*|https://*.git|https://github.com/*|https://gitlab.com/*)
# Allow trusted git repository URLs
printf '%s\n' "::notice::Using git repository URL for context"
;;
http://*|https://*)
printf '%s\n' "::error::Context cannot be an arbitrary HTTP URL: '$INPUT_CONTEXT'"
printf '%s\n' "::error::Only git repository URLs are allowed for remote contexts"
exit 1
;;
*)
printf '%s\n' "::error::Invalid context format: '$INPUT_CONTEXT'"
printf '%s\n' "::error::Must be a relative path or git repository URL"
exit 1
;;
esac
# Validate dockerfile input for security
INPUT_DOCKERFILE="${INPUT_DOCKERFILE:-Dockerfile}"
case "$INPUT_DOCKERFILE" in
Dockerfile|*/Dockerfile|*.dockerfile|*/*.dockerfile)
# Common dockerfile patterns are allowed
# Check for path traversal attempts
case "$INPUT_DOCKERFILE" in
*/../*|../*|*/..)
printf '%s\n' "::error::Dockerfile path contains path traversal: '$INPUT_DOCKERFILE'"
exit 1
;;
esac
;;
/*)
echo "::error::Dockerfile path cannot be absolute: '$INPUT_DOCKERFILE'"
echo "::error::Use relative paths (e.g., 'Dockerfile', './docker/Dockerfile')"
printf '%s\n' "::error::Dockerfile path cannot be absolute: '$INPUT_DOCKERFILE'"
printf '%s\n' "::error::Use relative paths (e.g., 'Dockerfile', './docker/Dockerfile')"
exit 1
;;
*://*)
echo "::error::Dockerfile path cannot be a URL: '$INPUT_DOCKERFILE'"
printf '%s\n' "::error::Dockerfile path cannot be a URL: '$INPUT_DOCKERFILE'"
exit 1
;;
*)
printf '%s\n' "::error::Invalid Dockerfile format: '$INPUT_DOCKERFILE'"
printf '%s\n' "::error::Must be 'Dockerfile', '*/Dockerfile', '*.dockerfile', or '*/*.dockerfile'"
exit 1
;;
esac
echo "Input validation completed successfully"
printf '%s\n' "Input validation completed successfully"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Determine Image Names and Tags
id: meta
@@ -223,25 +254,25 @@ runs:
# Output results
printf 'image-name=%s\n' "$base_name" >> "$GITHUB_OUTPUT"
{
echo 'tags<<EOF'
echo "$tags"
echo 'EOF'
printf '%s\n' 'tags<<EOF'
printf '%s\n' "$tags"
printf '%s\n' 'EOF'
} >> "$GITHUB_OUTPUT"
echo "Image name: $base_name"
echo "Tags:"
echo "$tags"
printf 'Image name: %s\n' "$base_name"
printf '%s\n' "Tags:"
printf '%s\n' "$tags"
- name: Login to Docker Hub
if: inputs.registry == 'dockerhub' || inputs.registry == 'both'
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
username: ${{ inputs.dockerhub-username }}
password: ${{ inputs.dockerhub-token }}
- name: Login to GitHub Container Registry
if: inputs.registry == 'github' || inputs.registry == 'both'
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -249,7 +280,7 @@ runs:
- name: Build and Push Docker Image
id: build
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
context: ${{ inputs.context }}
file: ${{ inputs.dockerfile }}

View File

@@ -2,7 +2,7 @@
# Validation rules for docker-publish action
# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY
# Schema version: 1.0
# Coverage: 73% (8/11 inputs)
# Coverage: 100% (11/11 inputs)
#
# This file defines validation rules for the docker-publish GitHub Action.
# Rules are automatically applied by validate-inputs action when this
@@ -27,25 +27,27 @@ optional_inputs:
- tags
- token
conventions:
build-args: key_value_list
context: file_path
dockerfile: file_path
dockerhub-token: github_token
dockerhub-username: username
image-name: docker_image_name
platforms: docker_architectures
registry: registry
push: boolean
registry: registry_enum
tags: docker_tag
token: github_token
overrides:
platforms: null
registry: registry_enum
statistics:
total_inputs: 11
validated_inputs: 8
skipped_inputs: 1
coverage_percentage: 73
validation_coverage: 73
validated_inputs: 11
skipped_inputs: 0
coverage_percentage: 100
validation_coverage: 100
auto_detected: true
manual_review_required: true
manual_review_required: false
quality_indicators:
has_required_inputs: false
has_token_validation: true

View File

@@ -288,9 +288,9 @@ runs:
echo "Detected package manager: $package_manager"
- name: Setup Node.js
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: '22'
node-version: '24'
- name: Enable Corepack
shell: sh
@@ -319,13 +319,13 @@ runs:
- name: Setup Bun
if: steps.detect-pm.outputs.package-manager == 'bun'
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
uses: oven-sh/setup-bun@ecf28ddc73e819eb6fa29df6b34ef8921c743461 # v2.1.3
with:
bun-version: latest
- name: Cache Node Dependencies
id: cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: node_modules
key: ${{ runner.os }}-eslint-lint-${{ inputs.mode }}-${{ steps.detect-pm.outputs.package-manager }}-${{ hashFiles('package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb') }}
@@ -457,7 +457,7 @@ runs:
- name: Upload SARIF Report
if: inputs.mode == 'check' && inputs.report-format == 'sarif' && always()
uses: github/codeql-action/upload-sarif@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with:
sarif_file: ${{ inputs.working-directory }}/eslint-results.sarif
@@ -508,7 +508,7 @@ runs:
- name: Commit and Push Fixes
if: inputs.mode == 'fix' && success()
uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
with:
commit_message: 'style: autofix ESLint violations'
commit_user_name: ${{ inputs.username }}

View File

@@ -44,7 +44,8 @@ conventions:
token: github_token
username: username
working-directory: file_path
overrides: {}
overrides:
mode: mode_enum
statistics:
total_inputs: 14
validated_inputs: 14

View File

@@ -46,6 +46,9 @@ const CATEGORIES = {
'compress-images': 'Repository',
'codeql-analysis': 'Repository',
// Security
'security-scan': 'Security',
// Validation
'validate-inputs': 'Validation',
};
@@ -120,6 +123,7 @@ const CATEGORY_ICONS = {
Build: '🏗️',
Publishing: '🚀',
Repository: '📦',
Security: '🛡️',
Validation: '✅',
};
@@ -232,7 +236,7 @@ function generateCategoryTables(actions) {
let output = '';
// Sort categories by priority
const categoryOrder = ['Setup', 'Utilities', 'Linting', 'Testing', 'Build', 'Publishing', 'Repository', 'Validation'];
const categoryOrder = ['Setup', 'Utilities', 'Linting', 'Testing', 'Build', 'Publishing', 'Repository', 'Security', 'Validation'];
for (const category of categoryOrder) {
if (!categories[category]) continue;

View File

@@ -159,13 +159,13 @@ runs:
echo "Final detected Go version: $detected_version" >&2
- name: Setup Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with:
go-version: ${{ steps.detect-go-version.outputs.detected-version }}
cache: true
- name: Download Dependencies
uses: step-security/retry@e1d59ce1f574b32f0915e3a8df055cfe9f99be5d # v3
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
with:
timeout_minutes: 10
max_attempts: ${{ inputs.max-retries }}
@@ -253,7 +253,7 @@ runs:
- name: Upload Build Artifacts
if: always()
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: go-build-artifacts
path: |

View File

@@ -37,105 +37,78 @@ class CustomValidator(BaseValidator):
# Validate working-directory if provided
if inputs.get("working-directory"):
result = self.file_validator.validate_file_path(
inputs["working-directory"], "working-directory"
valid &= self.validate_with(
self.file_validator,
"validate_file_path",
inputs["working-directory"],
"working-directory",
)
for error in self.file_validator.errors:
if error not in self.errors:
self.add_error(error)
self.file_validator.clear_errors()
if not result:
valid = False
# Validate golangci-lint-version if provided
if inputs.get("golangci-lint-version"):
value = inputs["golangci-lint-version"]
# Accept 'latest' or version format
if value != "latest" and not self.is_github_expression(value):
result = self.version_validator.validate_semantic_version(
value, "golangci-lint-version"
valid &= self.validate_with(
self.version_validator,
"validate_semantic_version",
value,
"golangci-lint-version",
)
for error in self.version_validator.errors:
if error not in self.errors:
self.add_error(error)
self.version_validator.clear_errors()
if not result:
valid = False
# Validate go-version if provided
if inputs.get("go-version"):
value = inputs["go-version"]
# Accept 'stable', 'oldstable' or version format
if value not in ["stable", "oldstable"] and not self.is_github_expression(value):
result = self.version_validator.validate_go_version(value, "go-version")
for error in self.version_validator.errors:
if error not in self.errors:
self.add_error(error)
self.version_validator.clear_errors()
if not result:
valid = False
valid &= self.validate_with(
self.version_validator, "validate_go_version", value, "go-version"
)
# Validate config-file if provided
if inputs.get("config-file"):
result = self.file_validator.validate_file_path(inputs["config-file"], "config-file")
for error in self.file_validator.errors:
if error not in self.errors:
self.add_error(error)
self.file_validator.clear_errors()
if not result:
valid = False
valid &= self.validate_with(
self.file_validator, "validate_file_path", inputs["config-file"], "config-file"
)
# Validate timeout if provided
if inputs.get("timeout"):
value = inputs["timeout"]
# Validate timeout format (e.g., 5m, 1h, 30s)
if not self.is_github_expression(value):
timeout_pattern = r"^\d+[smh]$"
if not re.match(timeout_pattern, value):
self.add_error(
f"Invalid timeout format: {value}. Expected format like '5m', '1h', '30s'"
)
valid = False
if not self.is_github_expression(value) and not re.match(r"^\d+[smh]$", value):
self.add_error(
f"Invalid timeout format: {value}. Expected format like '5m', '1h', '30s'"
)
valid = False
# Validate boolean inputs
for field in ["cache", "fail-on-error", "only-new-issues", "disable-all"]:
if inputs.get(field):
result = self.boolean_validator.validate_boolean(inputs[field], field)
for error in self.boolean_validator.errors:
if error not in self.errors:
self.add_error(error)
self.boolean_validator.clear_errors()
if not result:
valid = False
valid &= self.validate_with(
self.boolean_validator, "validate_boolean", inputs[field], field
)
# Validate report-format
if inputs.get("report-format"):
value = inputs["report-format"]
valid_formats = ["json", "sarif", "github-actions", "colored-line-number", "tab"]
if value not in valid_formats and not self.is_github_expression(value):
self.add_error(
f"Invalid report format: {value}. Must be one of: {', '.join(valid_formats)}"
)
valid = False
valid &= self.validate_enum(
inputs["report-format"],
"report-format",
["json", "sarif", "github-actions", "colored-line-number", "tab"],
case_sensitive=True,
)
# Validate max-retries
if inputs.get("max-retries"):
result = self.numeric_validator.validate_numeric_range(
inputs["max-retries"], min_val=1, max_val=10, name="max-retries"
valid &= self.validate_with(
self.numeric_validator,
"validate_numeric_range",
inputs["max-retries"],
min_val=1,
max_val=10,
name="max-retries",
)
for error in self.numeric_validator.errors:
if error not in self.errors:
self.add_error(error)
self.numeric_validator.clear_errors()
if not result:
valid = False
# Validate enable-linters and disable-linters
for field in ["enable-linters", "disable-linters"]:
if inputs.get(field):
value = inputs[field]
# First check format - must be comma-separated without spaces
if not self.is_github_expression(value):
if " " in value:
self.add_error(f"Invalid {field} format: spaces not allowed in linter list")
@@ -145,15 +118,9 @@ class CustomValidator(BaseValidator):
f"Invalid {field} format: must be comma-separated list of linters"
)
valid = False
# Then check for injection
result = self.security_validator.validate_no_injection(value, field)
for error in self.security_validator.errors:
if error not in self.errors:
self.add_error(error)
self.security_validator.clear_errors()
if not result:
valid = False
valid &= self.validate_with(
self.security_validator, "validate_no_injection", value, field
)
return valid

View File

@@ -205,7 +205,7 @@ runs:
validate_linter_list "$DISABLE_LINTERS" "disable-linters"
- name: Setup Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with:
go-version: ${{ inputs.go-version }}
cache: true
@@ -218,7 +218,7 @@ runs:
- name: Cache golangci-lint
id: cache
if: inputs.cache == 'true'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
~/.cache/golangci-lint
@@ -414,7 +414,7 @@ runs:
- name: Upload Lint Results
if: always() && inputs.report-format == 'sarif'
uses: github/codeql-action/upload-sarif@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with:
sarif_file: ${{ inputs.working-directory }}/reports/golangci-lint.sarif
category: golangci-lint

View File

@@ -36,15 +36,17 @@ conventions:
disable-linters: linter_list
enable-linters: linter_list
fail-on-error: boolean
go-version: semantic_version
go-version: go_version
golangci-lint-version: semantic_version
max-retries: numeric_range_1_10
only-new-issues: branch_name
only-new-issues: boolean
report-format: report_format
timeout: numeric_range_1_3600
timeout: timeout_with_unit
token: github_token
working-directory: file_path
overrides:
disable-linters: linter_list
enable-linters: linter_list
go-version: go_version
only-new-issues: boolean
timeout: timeout_with_unit

View File

@@ -2,7 +2,7 @@
# Validation rules for language-version-detect action
# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY
# Schema version: 1.0
# Coverage: 67% (2/3 inputs)
# Coverage: 100% (3/3 inputs)
#
# This file defines validation rules for the language-version-detect GitHub Action.
# Rules are automatically applied by validate-inputs action when this
@@ -21,16 +21,17 @@ optional_inputs:
- token
conventions:
default-version: semantic_version
language: language_enum
token: github_token
overrides: {}
statistics:
total_inputs: 3
validated_inputs: 2
validated_inputs: 3
skipped_inputs: 0
coverage_percentage: 67
validation_coverage: 67
coverage_percentage: 100
validation_coverage: 100
auto_detected: true
manual_review_required: true
manual_review_required: false
quality_indicators:
has_required_inputs: true
has_token_validation: true

View File

@@ -42,109 +42,40 @@ class CustomValidator(BaseValidator):
self.add_error("Input 'npm_token' is required")
valid = False
elif inputs["npm_token"]:
token = inputs["npm_token"]
# Check for NPM classic token format first
if token.startswith("npm_"):
# NPM classic token format: npm_ followed by 36+ alphanumeric characters
if not re.match(r"^npm_[a-zA-Z0-9]{36,}$", token):
self.add_error("Invalid NPM token format")
valid = False
# Also check for injection
result = self.security_validator.validate_no_injection(token, "npm_token")
for error in self.security_validator.errors:
if error not in self.errors:
self.add_error(error)
self.security_validator.clear_errors()
if not result:
valid = False
else:
# Otherwise validate as GitHub token
result = self.token_validator.validate_github_token(token, required=True)
for error in self.token_validator.errors:
if error not in self.errors:
self.add_error(error)
self.token_validator.clear_errors()
if not result:
valid = False
valid &= self._validate_npm_token(inputs["npm_token"])
# Validate registry-url
if inputs.get("registry-url"):
url = inputs["registry-url"]
if not self.is_github_expression(url):
# Must be http or https URL
if not url.startswith(("http://", "https://")):
self.add_error("Registry URL must use http or https protocol")
valid = False
else:
# Validate URL format
result = self.network_validator.validate_url(url, "registry-url")
for error in self.network_validator.errors:
if error not in self.errors:
self.add_error(error)
self.network_validator.clear_errors()
if not result:
valid = False
valid &= self._validate_registry_url(inputs["registry-url"])
# Validate scope
if inputs.get("scope"):
scope = inputs["scope"]
if not self.is_github_expression(scope):
# Scope must start with @ and contain only valid characters
if not scope.startswith("@"):
self.add_error("Scope must start with @ symbol")
valid = False
elif not re.match(r"^@[a-z0-9][a-z0-9\-_.]*$", scope):
self.add_error(
"Invalid scope format: must be @org-name with lowercase "
"letters, numbers, hyphens, dots, and underscores"
)
valid = False
# Check for injection
result = self.security_validator.validate_no_injection(scope, "scope")
for error in self.security_validator.errors:
if error not in self.errors:
self.add_error(error)
self.security_validator.clear_errors()
if not result:
valid = False
valid &= self._validate_scope(inputs["scope"])
# Validate access
if inputs.get("access"):
access = inputs["access"]
if not self.is_github_expression(access):
valid_access = ["public", "restricted", "private"]
if access and access not in valid_access:
self.add_error(
f"Invalid access level: {access}. Must be one of: {', '.join(valid_access)}"
)
valid = False
valid &= self.validate_enum(
inputs["access"], "access", ["public", "restricted", "private"]
)
# Validate boolean inputs (only always-auth and include-merged-tags are strict)
for field in ["always-auth", "include-merged-tags"]:
if inputs.get(field):
result = self.boolean_validator.validate_boolean(inputs[field], field)
for error in self.boolean_validator.errors:
if error not in self.errors:
self.add_error(error)
self.boolean_validator.clear_errors()
if not result:
valid = False
valid &= self.validate_with(
self.boolean_validator, "validate_boolean", inputs[field], field
)
# provenance and dry-run accept any value (npm handles them)
# No validation needed for these
# Validate package-version
if inputs.get("package-version"):
result = self.version_validator.validate_semantic_version(
inputs["package-version"], "package-version"
valid &= self.validate_with(
self.version_validator,
"validate_semantic_version",
inputs["package-version"],
"package-version",
)
for error in self.version_validator.errors:
if error not in self.errors:
self.add_error(error)
self.version_validator.clear_errors()
if not result:
valid = False
# Validate tag
if inputs.get("tag"):
@@ -161,16 +92,57 @@ class CustomValidator(BaseValidator):
# Validate working-directory and ignore-scripts as file paths
for field in ["working-directory", "ignore-scripts"]:
if inputs.get(field):
result = self.file_validator.validate_path(inputs[field], field)
for error in self.file_validator.errors:
if error not in self.errors:
self.add_error(error)
self.file_validator.clear_errors()
if not result:
valid = False
valid &= self.validate_with(
self.file_validator, "validate_path", inputs[field], field
)
return valid
def _validate_npm_token(self, token: str) -> bool:
"""Validate NPM token format."""
# Check for NPM classic token format first
if token.startswith("npm_"):
# NPM classic token format: npm_ followed by 36+ alphanumeric characters
if not re.match(r"^npm_[a-zA-Z0-9]{36,}$", token):
self.add_error("Invalid NPM token format")
return False
# Also check for injection
return self.validate_with(
self.security_validator, "validate_no_injection", token, "npm_token"
)
# Otherwise validate as GitHub token
return self.validate_with(
self.token_validator, "validate_github_token", token, required=True
)
def _validate_registry_url(self, url: str) -> bool:
"""Validate registry URL format."""
if self.is_github_expression(url):
return True
# Must be http or https URL
if not url.startswith(("http://", "https://")):
self.add_error("Registry URL must use http or https protocol")
return False
# Validate URL format
return self.validate_with(self.network_validator, "validate_url", url, "registry-url")
def _validate_scope(self, scope: str) -> bool:
"""Validate NPM scope format."""
if self.is_github_expression(scope):
return True
# Scope must start with @ and contain only valid characters
if not scope.startswith("@"):
self.add_error("Scope must start with @ symbol")
return False
if not re.match(r"^@[a-z0-9][a-z0-9\-_.]*$", scope):
self.add_error(
"Invalid scope format: must be @org-name with lowercase "
"letters, numbers, hyphens, dots, and underscores"
)
return False
# Check for injection
return self.validate_with(self.security_validator, "validate_no_injection", scope, "scope")
def get_required_inputs(self) -> list[str]:
"""Get list of required inputs."""
return ["npm_token"]

View File

@@ -121,9 +121,9 @@ runs:
echo "Detected package manager: $package_manager"
- name: Setup Node.js
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: '22'
node-version: '24'
- name: Enable Corepack
shell: sh
@@ -152,13 +152,13 @@ runs:
- name: Setup Bun
if: steps.detect-pm.outputs.package-manager == 'bun'
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
uses: oven-sh/setup-bun@ecf28ddc73e819eb6fa29df6b34ef8921c743461 # v2.1.3
with:
bun-version: latest
- name: Cache Node Dependencies
id: cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: node_modules
key: ${{ runner.os }}-npm-publish-${{ steps.detect-pm.outputs.package-manager }}-${{ hashFiles('package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb') }}

View File

@@ -22,7 +22,7 @@ optional_inputs:
- token
conventions:
npm_token: github_token
package-version: semantic_version
package-version: strict_semantic_version
registry-url: url
scope: scope
token: github_token

742
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,7 @@
"js-yaml": "^4.1.0",
"markdown-table": "^3.0.3",
"markdown-table-formatter": "^1.6.0",
"markdownlint-cli2": "^0.19.0",
"markdownlint-cli2": "^0.21.0",
"prettier": "^3.3.3",
"yaml-lint": "^1.7.0"
},

View File

@@ -33,59 +33,31 @@ class CustomValidator(BaseValidator):
# Validate token (optional)
if inputs.get("token"):
token = inputs["token"]
result = self.token_validator.validate_github_token(token)
for error in self.token_validator.errors:
if error not in self.errors:
self.add_error(error)
self.token_validator.clear_errors()
if not result:
valid = False
valid &= self.validate_with(self.token_validator, "validate_github_token", token)
# Also check for variable expansion
if not self.is_github_expression(token):
result = self.security_validator.validate_no_injection(token, "token")
for error in self.security_validator.errors:
if error not in self.errors:
self.add_error(error)
self.security_validator.clear_errors()
if not result:
valid = False
valid &= self.validate_with(
self.security_validator, "validate_no_injection", token, "token"
)
# Validate email (optional, empty means use default)
if "email" in inputs and inputs["email"] and inputs["email"] != "":
if inputs.get("email"):
email = inputs["email"]
result = self.network_validator.validate_email(email, "email")
for error in self.network_validator.errors:
if error not in self.errors:
self.add_error(error)
self.network_validator.clear_errors()
if not result:
valid = False
valid &= self.validate_with(self.network_validator, "validate_email", email, "email")
# Also check for shell metacharacters (but allow @ and .)
if not self.is_github_expression(email):
# Only check for dangerous shell metacharacters, not @ or .
dangerous_chars = [";", "&", "|", "`", "$", "(", ")", "<", ">", "\n", "\r"]
for char in dangerous_chars:
if char in email:
self.add_error(f"email: Contains dangerous character '{char}'")
valid = False
break
if any(char in email for char in dangerous_chars):
self.add_error("email: Contains dangerous shell metacharacter")
valid = False
# Validate username (optional)
if inputs.get("username"):
username = inputs["username"]
if not self.is_github_expression(username):
# Check for injection
result = self.security_validator.validate_no_injection(username, "username")
for error in self.security_validator.errors:
if error not in self.errors:
self.add_error(error)
self.security_validator.clear_errors()
if not result:
valid = False
# Check username length (GitHub usernames are max 39 characters)
valid &= self.validate_with(
self.security_validator, "validate_no_injection", username, "username"
)
if len(username) > 39:
self.add_error("Username is too long (max 39 characters)")
valid = False

View File

@@ -319,7 +319,7 @@ runs:
- name: Setup PHP
id: setup-php
uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # 2.35.5
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # 2.36.0
with:
php-version: ${{ steps.detect-php-version.outputs.detected-version }}
extensions: ${{ inputs.extensions }}
@@ -356,7 +356,7 @@ runs:
- name: Cache Composer packages
id: composer-cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
vendor
@@ -376,7 +376,7 @@ runs:
composer clear-cache
- name: Install Composer Dependencies
uses: step-security/retry@e1d59ce1f574b32f0915e3a8df055cfe9f99be5d # v3
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
with:
timeout_minutes: 10
max_attempts: ${{ inputs.max-retries }}
@@ -454,7 +454,7 @@ runs:
phpunit_output=$(composer test 2>&1) || phpunit_exit_code=$?
elif [ -f "vendor/bin/phpunit" ]; then
echo "Running PHPUnit directly..."
phpunit_output=$(vendor/bin/phpunit --verbose 2>&1) || phpunit_exit_code=$?
phpunit_output=$(vendor/bin/phpunit 2>&1) || phpunit_exit_code=$?
else
echo "::error::PHPUnit not found. Ensure Composer dependencies are installed."
exit 1

View File

@@ -2,7 +2,7 @@
# Validation rules for php-tests action
# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY
# Schema version: 1.0
# Coverage: 78% (7/9 inputs)
# Coverage: 89% (8/9 inputs)
#
# This file defines validation rules for the php-tests GitHub Action.
# Rules are automatically applied by validate-inputs action when this
@@ -27,7 +27,8 @@ optional_inputs:
conventions:
coverage: coverage_driver
email: email
framework: boolean
extensions: php_extensions
framework: framework_mode
max-retries: numeric_range_1_10
php-version: semantic_version
token: github_token
@@ -35,12 +36,12 @@ conventions:
overrides: {}
statistics:
total_inputs: 9
validated_inputs: 7
validated_inputs: 8
skipped_inputs: 0
coverage_percentage: 78
validation_coverage: 78
coverage_percentage: 89
validation_coverage: 89
auto_detected: true
manual_review_required: true
manual_review_required: false
quality_indicators:
has_required_inputs: false
has_token_validation: true

View File

@@ -40,7 +40,7 @@ runs:
steps:
- name: Validate Inputs
id: validate
uses: ivuorinen/actions/validate-inputs@8fb52522ab00fe73cf181ef299e56066f0b2c8d8
uses: ivuorinen/actions/validate-inputs@5cc7373a22402ee8985376bc713f00e09b5b2edb
with:
action-type: pr-lint
token: ${{ inputs.token }}
@@ -54,13 +54,9 @@ runs:
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta
with:
token: ${{ inputs.token || github.token }}
ref: ${{ github.event.pull_request.head.ref || github.head_ref || github.ref_name }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
# If you use VALIDATE_ALL_CODEBASE = true, you can remove this line to
# improve performance
fetch-depth: 0
# ╭──────────────────────────────────────────────────────────╮
# │ Install packages for linting │
# ╰──────────────────────────────────────────────────────────╯
@@ -74,6 +70,29 @@ runs:
if [ -f package.json ]; then
printf '%s\n' "found=true" >> "$GITHUB_OUTPUT"
# Check if packageManager field is set (for corepack)
if command -v jq >/dev/null 2>&1; then
has_package_manager=$(jq -r '.packageManager // empty' package.json 2>/dev/null || printf '')
if [ -n "$has_package_manager" ]; then
printf '%s\n' "has-package-manager=true" >> "$GITHUB_OUTPUT"
printf 'Found packageManager field: %s\n' "$has_package_manager"
else
printf '%s\n' "has-package-manager=false" >> "$GITHUB_OUTPUT"
fi
else
# Fallback: check with grep if jq not available
# Use robust pattern to verify non-empty value
if grep -q '"packageManager"[[:space:]]*:[[:space:]]*"[^"]\+"' package.json 2>/dev/null; then
printf '%s\n' "has-package-manager=true" >> "$GITHUB_OUTPUT"
printf '%s\n' "Found packageManager field in package.json"
else
printf '%s\n' "has-package-manager=false" >> "$GITHUB_OUTPUT"
fi
fi
else
# Explicitly set has-package-manager to false when package.json doesn't exist
printf '%s\n' "has-package-manager=false" >> "$GITHUB_OUTPUT"
fi
- name: Detect Package Manager
@@ -95,34 +114,39 @@ runs:
fi
printf 'package-manager=%s\n' "$package_manager" >> "$GITHUB_OUTPUT"
echo "Detected package manager: $package_manager"
printf 'Detected package manager: %s\n' "$package_manager"
- name: Setup Node.js
if: steps.detect-node.outputs.found == 'true'
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: '22'
node-version: '24'
- name: Enable Corepack
if: steps.detect-node.outputs.found == 'true'
if: steps.detect-node.outputs.found == 'true' && steps.detect-node.outputs.has-package-manager == 'true'
shell: sh
run: |
set -eu
corepack enable
printf '%s\n' "Corepack enabled - package manager will be installed automatically from package.json"
- name: Install Package Manager
if: steps.detect-node.outputs.found == 'true'
- name: Install Package Manager (Fallback)
if: steps.detect-node.outputs.found == 'true' && steps.detect-node.outputs.has-package-manager == 'false'
shell: sh
env:
PACKAGE_MANAGER: ${{ steps.detect-pm.outputs.package-manager }}
run: |
set -eu
printf 'No packageManager field found, using detected package manager: %s\n' "$PACKAGE_MANAGER"
case "$PACKAGE_MANAGER" in
pnpm)
corepack enable
corepack prepare pnpm@latest --activate
;;
yarn)
corepack enable
corepack prepare yarn@stable --activate
;;
bun|npm)
@@ -132,14 +156,14 @@ runs:
- name: Setup Bun
if: steps.detect-node.outputs.found == 'true' && steps.detect-pm.outputs.package-manager == 'bun'
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
uses: oven-sh/setup-bun@ecf28ddc73e819eb6fa29df6b34ef8921c743461 # v2.1.3
with:
bun-version: latest
- name: Cache Node Dependencies
if: steps.detect-node.outputs.found == 'true'
id: node-cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: node_modules
key: ${{ runner.os }}-pr-lint-${{ steps.detect-pm.outputs.package-manager }}-${{ hashFiles('package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb') }}
@@ -154,16 +178,21 @@ runs:
run: |
set -eu
echo "Installing dependencies using $PACKAGE_MANAGER..."
printf 'Installing dependencies using %s...\n' "$PACKAGE_MANAGER"
case "$PACKAGE_MANAGER" in
"pnpm")
pnpm install --frozen-lockfile
;;
"yarn")
if [ -f ".yarnrc.yml" ]; then
# Detect Yarn version by checking actual version output
# Yarn 2+ (Berry) uses --immutable, Yarn 1.x (Classic) uses --frozen-lockfile
yarn_version=$(yarn --version 2>/dev/null || printf '1.0.0')
if printf '%s' "$yarn_version" | grep -q '^[2-9]'; then
# Yarn 2+ (Berry) - use --immutable
yarn install --immutable
else
# Yarn 1.x (Classic) - use --frozen-lockfile
yarn install --frozen-lockfile
fi
;;
@@ -175,7 +204,7 @@ runs:
;;
esac
echo "✅ Dependencies installed successfully"
printf '✅ Dependencies installed successfully\n'
# PHP tests if composer.json exists
- name: Detect composer.json
@@ -219,12 +248,12 @@ runs:
# Parse .tool-versions file
if [ -f .tool-versions ]; then
echo "Checking .tool-versions for php..." >&2
printf 'Checking .tool-versions for php...\n' >&2
version=$(awk '/^php[[:space:]]/ {gsub(/#.*/, ""); print $2; exit}' .tool-versions 2>/dev/null || echo "")
if [ -n "$version" ]; then
version=$(clean_version "$version")
if validate_version "$version"; then
echo "Found PHP version in .tool-versions: $version" >&2
printf 'Found PHP version in .tool-versions: %s\n' "$version" >&2
detected_version="$version"
fi
fi
@@ -232,13 +261,13 @@ runs:
# Parse Dockerfile
if [ -z "$detected_version" ] && [ -f Dockerfile ]; then
echo "Checking Dockerfile for php..." >&2
printf 'Checking Dockerfile for php...\n' >&2
version=$(grep -iF "FROM" Dockerfile | grep -F "php:" | head -1 | \
sed -n -E "s/.*php:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "")
if [ -n "$version" ]; then
version=$(clean_version "$version")
if validate_version "$version"; then
echo "Found PHP version in Dockerfile: $version" >&2
printf 'Found PHP version in Dockerfile: %s\n' "$version" >&2
detected_version="$version"
fi
fi
@@ -246,29 +275,29 @@ runs:
# Parse devcontainer.json
if [ -z "$detected_version" ] && [ -f .devcontainer/devcontainer.json ]; then
echo "Checking devcontainer.json for php..." >&2
printf 'Checking devcontainer.json for php...\n' >&2
if command -v jq >/dev/null 2>&1; then
version=$(jq -r '.image // empty' .devcontainer/devcontainer.json 2>/dev/null | sed -n -E "s/.*php:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "")
if [ -n "$version" ]; then
version=$(clean_version "$version")
if validate_version "$version"; then
echo "Found PHP version in devcontainer: $version" >&2
printf 'Found PHP version in devcontainer: %s\n' "$version" >&2
detected_version="$version"
fi
fi
else
echo "jq not found; skipping devcontainer.json parsing" >&2
printf 'jq not found; skipping devcontainer.json parsing\n' >&2
fi
fi
# Parse .php-version file
if [ -z "$detected_version" ] && [ -f .php-version ]; then
echo "Checking .php-version..." >&2
printf 'Checking .php-version...\n' >&2
version=$(tr -d '\r' < .php-version | head -1)
if [ -n "$version" ]; then
version=$(clean_version "$version")
if validate_version "$version"; then
echo "Found PHP version in .php-version: $version" >&2
printf 'Found PHP version in .php-version: %s\n' "$version" >&2
detected_version="$version"
fi
fi
@@ -276,7 +305,7 @@ runs:
# Parse composer.json
if [ -z "$detected_version" ] && [ -f composer.json ]; then
echo "Checking composer.json..." >&2
printf 'Checking composer.json...\n' >&2
if command -v jq >/dev/null 2>&1; then
version=$(jq -r '.require.php // empty' composer.json 2>/dev/null | sed -n 's/[^0-9]*\([0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?\).*/\1/p')
if [ -z "$version" ]; then
@@ -285,34 +314,34 @@ runs:
if [ -n "$version" ]; then
version=$(clean_version "$version")
if validate_version "$version"; then
echo "Found PHP version in composer.json: $version" >&2
printf 'Found PHP version in composer.json: %s\n' "$version" >&2
detected_version="$version"
fi
fi
else
echo "jq not found; skipping composer.json parsing" >&2
printf 'jq not found; skipping composer.json parsing\n' >&2
fi
fi
# Use default version if nothing detected
if [ -z "$detected_version" ]; then
detected_version="$DEFAULT_VERSION"
echo "Using default PHP version: $detected_version" >&2
printf 'Using default PHP version: %s\n' "$detected_version" >&2
fi
# Set output
printf 'detected-version=%s\n' "$detected_version" >> "$GITHUB_OUTPUT"
echo "Final detected PHP version: $detected_version" >&2
printf 'Final detected PHP version: %s\n' "$detected_version" >&2
- name: Setup PHP
if: steps.detect-php.outputs.found == 'true'
uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # 2.35.5
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # 2.36.0
with:
php-version: ${{ steps.php-version.outputs.detected-version }}
tools: composer
coverage: none
env:
GITHUB_TOKEN: ${{ inputs.token }}
GITHUB_TOKEN: ${{ inputs.token || github.token }}
- name: Setup problem matchers for PHP
if: steps.detect-php.outputs.found == 'true'
@@ -322,7 +351,8 @@ runs:
run: |
set -eu
echo "::add-matcher::$RUNNER_TOOL_CACHE/php.json"
matcher_path=$(printf '%s' "$RUNNER_TOOL_CACHE/php.json" | tr -d '\n\r')
printf '%s\n' "::add-matcher::$matcher_path"
- name: Install PHP dependencies
if: steps.detect-php.outputs.found == 'true'
@@ -348,7 +378,7 @@ runs:
id: python-version
shell: sh
env:
DEFAULT_VERSION: '3.11'
DEFAULT_VERSION: '3.14'
run: |
set -eu
@@ -374,12 +404,12 @@ runs:
# Parse .tool-versions file
if [ -f .tool-versions ]; then
echo "Checking .tool-versions for python..." >&2
printf 'Checking .tool-versions for python...\n' >&2
version=$(awk '/^python[[:space:]]/ {gsub(/#.*/, ""); print $2; exit}' .tool-versions 2>/dev/null || echo "")
if [ -n "$version" ]; then
version=$(clean_version "$version")
if validate_version "$version"; then
echo "Found Python version in .tool-versions: $version" >&2
printf 'Found Python version in .tool-versions: %s\n' "$version" >&2
detected_version="$version"
fi
fi
@@ -387,13 +417,13 @@ runs:
# Parse Dockerfile
if [ -z "$detected_version" ] && [ -f Dockerfile ]; then
echo "Checking Dockerfile for python..." >&2
printf 'Checking Dockerfile for python...\n' >&2
version=$(grep -iF "FROM" Dockerfile | grep -F "python:" | head -1 | \
sed -n -E "s/.*python:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "")
if [ -n "$version" ]; then
version=$(clean_version "$version")
if validate_version "$version"; then
echo "Found Python version in Dockerfile: $version" >&2
printf 'Found Python version in Dockerfile: %s\n' "$version" >&2
detected_version="$version"
fi
fi
@@ -401,29 +431,29 @@ runs:
# Parse devcontainer.json
if [ -z "$detected_version" ] && [ -f .devcontainer/devcontainer.json ]; then
echo "Checking devcontainer.json for python..." >&2
printf 'Checking devcontainer.json for python...\n' >&2
if command -v jq >/dev/null 2>&1; then
version=$(jq -r '.image // empty' .devcontainer/devcontainer.json 2>/dev/null | sed -n -E "s/.*python:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "")
if [ -n "$version" ]; then
version=$(clean_version "$version")
if validate_version "$version"; then
echo "Found Python version in devcontainer: $version" >&2
printf 'Found Python version in devcontainer: %s\n' "$version" >&2
detected_version="$version"
fi
fi
else
echo "jq not found; skipping devcontainer.json parsing" >&2
printf 'jq not found; skipping devcontainer.json parsing\n' >&2
fi
fi
# Parse .python-version file
if [ -z "$detected_version" ] && [ -f .python-version ]; then
echo "Checking .python-version..." >&2
printf 'Checking .python-version...\n' >&2
version=$(tr -d '\r' < .python-version | head -1)
if [ -n "$version" ]; then
version=$(clean_version "$version")
if validate_version "$version"; then
echo "Found Python version in .python-version: $version" >&2
printf 'Found Python version in .python-version: %s\n' "$version" >&2
detected_version="$version"
fi
fi
@@ -431,13 +461,13 @@ runs:
# Parse pyproject.toml
if [ -z "$detected_version" ] && [ -f pyproject.toml ]; then
echo "Checking pyproject.toml..." >&2
if grep -q '^\\[project\\]' pyproject.toml; then
version=$(grep -A 20 '^\\[project\\]' pyproject.toml | grep -E '^\\s*requires-python[[:space:]]*=' | sed -n 's/[^0-9]*\([0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?\).*/\1/p' | head -1)
printf 'Checking pyproject.toml...\n' >&2
if grep -q '^\[project\]' pyproject.toml; then
version=$(grep -A 20 '^\[project\]' pyproject.toml | grep -E '^\s*requires-python[[:space:]]*=' | sed -n 's/[^0-9]*\([0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?\).*/\1/p' | head -1)
if [ -n "$version" ]; then
version=$(clean_version "$version")
if validate_version "$version"; then
echo "Found Python version in pyproject.toml: $version" >&2
printf 'Found Python version in pyproject.toml: %s\n' "$version" >&2
detected_version="$version"
fi
fi
@@ -447,16 +477,16 @@ runs:
# Use default version if nothing detected
if [ -z "$detected_version" ]; then
detected_version="$DEFAULT_VERSION"
echo "Using default Python version: $detected_version" >&2
printf 'Using default Python version: %s\n' "$detected_version" >&2
fi
# Set output
printf 'detected-version=%s\n' "$detected_version" >> "$GITHUB_OUTPUT"
echo "Final detected Python version: $detected_version" >&2
printf 'Final detected Python version: %s\n' "$detected_version" >&2
- name: Setup Python
if: steps.detect-python.outputs.found == 'true'
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ steps.python-version.outputs.detected-version }}
cache: 'pip'
@@ -485,7 +515,7 @@ runs:
id: go-version
shell: sh
env:
DEFAULT_VERSION: '1.24'
DEFAULT_VERSION: '1.25'
run: |
set -eu
@@ -511,12 +541,12 @@ runs:
# Parse .tool-versions file
if [ -f .tool-versions ]; then
echo "Checking .tool-versions for golang..." >&2
printf 'Checking .tool-versions for golang...\n' >&2
version=$(awk '/^golang[[:space:]]/ {gsub(/#.*/, ""); print $2; exit}' .tool-versions 2>/dev/null || echo "")
if [ -n "$version" ]; then
version=$(clean_version "$version")
if validate_version "$version"; then
echo "Found Go version in .tool-versions: $version" >&2
printf 'Found Go version in .tool-versions: %s\n' "$version" >&2
detected_version="$version"
fi
fi
@@ -524,13 +554,13 @@ runs:
# Parse Dockerfile
if [ -z "$detected_version" ] && [ -f Dockerfile ]; then
echo "Checking Dockerfile for golang..." >&2
printf 'Checking Dockerfile for golang...\n' >&2
version=$(grep -iF "FROM" Dockerfile | grep -F "golang:" | head -1 | \
sed -n -E "s/.*golang:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "")
if [ -n "$version" ]; then
version=$(clean_version "$version")
if validate_version "$version"; then
echo "Found Go version in Dockerfile: $version" >&2
printf 'Found Go version in Dockerfile: %s\n' "$version" >&2
detected_version="$version"
fi
fi
@@ -538,29 +568,29 @@ runs:
# Parse devcontainer.json
if [ -z "$detected_version" ] && [ -f .devcontainer/devcontainer.json ]; then
echo "Checking devcontainer.json for golang..." >&2
printf 'Checking devcontainer.json for golang...\n' >&2
if command -v jq >/dev/null 2>&1; then
version=$(jq -r '.image // empty' .devcontainer/devcontainer.json 2>/dev/null | sed -n -E "s/.*golang:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "")
if [ -n "$version" ]; then
version=$(clean_version "$version")
if validate_version "$version"; then
echo "Found Go version in devcontainer: $version" >&2
printf 'Found Go version in devcontainer: %s\n' "$version" >&2
detected_version="$version"
fi
fi
else
echo "jq not found; skipping devcontainer.json parsing" >&2
printf 'jq not found; skipping devcontainer.json parsing\n' >&2
fi
fi
# Parse .go-version file
if [ -z "$detected_version" ] && [ -f .go-version ]; then
echo "Checking .go-version..." >&2
printf 'Checking .go-version...\n' >&2
version=$(tr -d '\r' < .go-version | head -1)
if [ -n "$version" ]; then
version=$(clean_version "$version")
if validate_version "$version"; then
echo "Found Go version in .go-version: $version" >&2
printf 'Found Go version in .go-version: %s\n' "$version" >&2
detected_version="$version"
fi
fi
@@ -568,12 +598,12 @@ runs:
# Parse go.mod
if [ -z "$detected_version" ] && [ -f go.mod ]; then
echo "Checking go.mod..." >&2
printf 'Checking go.mod...\n' >&2
version=$(grep -E '^go[[:space:]]+[0-9]' go.mod | awk '{print $2}' | head -1 || echo "")
if [ -n "$version" ]; then
version=$(clean_version "$version")
if validate_version "$version"; then
echo "Found Go version in go.mod: $version" >&2
printf 'Found Go version in go.mod: %s\n' "$version" >&2
detected_version="$version"
fi
fi
@@ -582,16 +612,16 @@ runs:
# Use default version if nothing detected
if [ -z "$detected_version" ]; then
detected_version="$DEFAULT_VERSION"
echo "Using default Go version: $detected_version" >&2
printf 'Using default Go version: %s\n' "$detected_version" >&2
fi
# Set output
printf 'detected-version=%s\n' "$detected_version" >> "$GITHUB_OUTPUT"
echo "Final detected Go version: $detected_version" >&2
printf 'Final detected Go version: %s\n' "$detected_version" >&2
- name: Setup Go
if: steps.detect-go.outputs.found == 'true'
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with:
go-version: ${{ steps.go-version.outputs.detected-version }}
cache: true
@@ -602,7 +632,7 @@ runs:
- name: MegaLinter
# You can override MegaLinter flavor used to have faster performances
# More info at https://megalinter.io/latest/flavors/
uses: oxsecurity/megalinter/flavors/cupcake@62c799d895af9bcbca5eacfebca29d527f125a57 # v9.1.0
uses: oxsecurity/megalinter/flavors/cupcake@8fbdead70d1409964ab3d5afa885e18ee85388bb # v9.4.0
id: ml
# All available variables are described in documentation
@@ -620,11 +650,7 @@ runs:
# github.event_name == 'push' &&
# contains(fromJSON('["refs/heads/main", "refs/heads/master"]'), github.ref)
# }}
VALIDATE_ALL_CODEBASE: >-
${{
github.event_name == 'push' &&
contains(fromJSON('["refs/heads/main", "refs/heads/master"]'), github.ref)
}}
VALIDATE_ALL_CODEBASE: false
GITHUB_TOKEN: ${{ inputs.token || github.token }}
@@ -632,7 +658,7 @@ runs:
#
# When active, APPLY_FIXES must also be defined as environment variable
# (in .github/workflows/mega-linter.yml or other CI tool)
APPLY_FIXES: all
APPLY_FIXES: none
# Decide which event triggers application of fixes in a commit or a PR
# (pull_request, push, all)
@@ -648,124 +674,13 @@ runs:
# Uncomment to disable copy-paste and spell checks
DISABLE: COPYPASTE,SPELL
# Export env vars to make them available for subsequent expressions
- name: Export Apply Fixes Variables
shell: sh
run: |
echo "APPLY_FIXES_EVENT=pull_request" >> "$GITHUB_ENV"
echo "APPLY_FIXES_MODE=commit" >> "$GITHUB_ENV"
# Upload MegaLinter artifacts
- name: Archive production artifacts
if: success() || failure()
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: MegaLinter reports
include-hidden-files: 'true'
path: |
megalinter-reports
mega-linter.log
# Set APPLY_FIXES_IF var for use in future steps
- name: Set APPLY_FIXES_IF var
shell: sh
env:
APPLY_FIXES_CONDITION: >-
${{
steps.ml.outputs.has_updated_sources == 1 &&
(env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) &&
(github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository)
}}
run: |
set -eu
# Sanitize by removing newlines to prevent env var injection
sanitized_condition="$(echo "$APPLY_FIXES_CONDITION" | tr -d '\n\r')"
printf 'APPLY_FIXES_IF=%s\n' "$sanitized_condition" >> "${GITHUB_ENV}"
# Set APPLY_FIXES_IF_* vars for use in future steps
- name: Set APPLY_FIXES_IF_* vars
shell: sh
env:
APPLY_FIXES_IF_PR_CONDITION: ${{ env.APPLY_FIXES_IF == 'true' && env.APPLY_FIXES_MODE == 'pull_request' }}
APPLY_FIXES_IF_COMMIT_CONDITION: ${{ env.APPLY_FIXES_IF == 'true' && env.APPLY_FIXES_MODE == 'commit' && (!contains(fromJSON('["refs/heads/main", "refs/heads/master"]'), github.ref)) }}
run: |
set -eu
# Sanitize by removing newlines to prevent env var injection
sanitized_pr="$(echo "$APPLY_FIXES_IF_PR_CONDITION" | tr -d '\n\r')"
sanitized_commit="$(echo "$APPLY_FIXES_IF_COMMIT_CONDITION" | tr -d '\n\r')"
printf 'APPLY_FIXES_IF_PR=%s\n' "$sanitized_pr" >> "${GITHUB_ENV}"
printf 'APPLY_FIXES_IF_COMMIT=%s\n' "$sanitized_commit" >> "${GITHUB_ENV}"
# Create pull request if applicable
# (for now works only on PR from same repository, not from forks)
- name: Create Pull Request with applied fixes
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
id: cpr
if: env.APPLY_FIXES_IF_PR == 'true'
with:
token: ${{ inputs.token || github.token }}
commit-message: 'style: apply linter fixes'
title: 'style: apply linter fixes'
labels: bot
- name: Create PR output
if: env.APPLY_FIXES_IF_PR == 'true'
shell: sh
env:
PR_NUMBER: ${{ steps.cpr.outputs.pull-request-number }}
PR_URL: ${{ steps.cpr.outputs.pull-request-url }}
run: |
set -eu
echo "PR Number - $PR_NUMBER"
echo "PR URL - $PR_URL"
# Push new commit if applicable
# (for now works only on PR from same repository, not from forks)
- name: Prepare commit
if: env.APPLY_FIXES_IF_COMMIT == 'true'
shell: sh
env:
BRANCH_REF: >-
${{
github.event.pull_request.head.ref ||
github.head_ref ||
github.ref_name
}}
run: |
set -eu
# Fix .git directory ownership after MegaLinter container execution
sudo chown -Rc "$UID" .git/
# Ensure we're on the correct branch (not in detached HEAD state)
# This is necessary because MegaLinter may leave the repo in a detached HEAD state
current_branch=$(git rev-parse --abbrev-ref HEAD)
if [ "$current_branch" = "HEAD" ]; then
echo "Repository is in detached HEAD state, checking out $BRANCH_REF"
# Validate branch reference to prevent command injection
if ! git check-ref-format --branch "$BRANCH_REF"; then
echo "::error::Invalid branch reference format: $BRANCH_REF"
exit 1
fi
git checkout "$BRANCH_REF"
else
echo "Repository is on branch: $current_branch"
fi
- name: Commit and push applied linter fixes
uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0
if: env.APPLY_FIXES_IF_COMMIT == 'true'
with:
branch: >-
${{
github.event.pull_request.head.ref ||
github.head_ref ||
github.ref
}}
commit_message: 'style: apply linter fixes'
commit_user_name: ${{ inputs.username }}
commit_user_email: ${{ inputs.email }}

View File

@@ -34,74 +34,45 @@ class CustomValidator(BaseValidator):
# Validate pre-commit-config if provided
if "pre-commit-config" in inputs:
result = self.file_validator.validate_file_path(
inputs["pre-commit-config"], "pre-commit-config"
valid &= self.validate_with(
self.file_validator,
"validate_file_path",
inputs["pre-commit-config"],
"pre-commit-config",
)
for error in self.file_validator.errors:
if error not in self.errors:
self.add_error(error)
self.file_validator.clear_errors()
if not result:
valid = False
# Validate base-branch if provided (just check for injection)
if inputs.get("base-branch"):
# Check for dangerous characters that could cause shell injection
result = self.security_validator.validate_no_injection(
inputs["base-branch"], "base-branch"
valid &= self.validate_with(
self.security_validator,
"validate_no_injection",
inputs["base-branch"],
"base-branch",
)
for error in self.security_validator.errors:
if error not in self.errors:
self.add_error(error)
self.security_validator.clear_errors()
if not result:
valid = False
# Validate token if provided
if inputs.get("token"):
result = self.token_validator.validate_github_token(inputs["token"])
for error in self.token_validator.errors:
if error not in self.errors:
self.add_error(error)
self.token_validator.clear_errors()
if not result:
valid = False
valid &= self.validate_with(
self.token_validator, "validate_github_token", inputs["token"]
)
# Validate commit_user if provided (allow spaces for Git usernames)
# Check both underscore and hyphen versions since inputs can have either
commit_user_key = (
"commit_user"
if "commit_user" in inputs
else "commit-user"
if "commit-user" in inputs
else None
)
commit_user_key = self.get_key_variant(inputs, "commit_user", "commit-user")
if commit_user_key and inputs[commit_user_key]:
# Check for dangerous injection patterns
value = inputs[commit_user_key]
if any(char in value for char in [";", "&", "|", "`", "$", "(", ")", "\n", "\r"]):
if any(c in value for c in [";", "&", "|", "`", "$", "(", ")", "\n", "\r"]):
self.add_error(f"{commit_user_key}: Contains potentially dangerous characters")
valid = False
# Validate commit_email if provided
# Check both underscore and hyphen versions
commit_email_key = (
"commit_email"
if "commit_email" in inputs
else "commit-email"
if "commit-email" in inputs
else None
)
commit_email_key = self.get_key_variant(inputs, "commit_email", "commit-email")
if commit_email_key and inputs[commit_email_key]:
result = self.network_validator.validate_email(
inputs[commit_email_key], commit_email_key
valid &= self.validate_with(
self.network_validator,
"validate_email",
inputs[commit_email_key],
commit_email_key,
)
for error in self.network_validator.errors:
if error not in self.errors:
self.add_error(error)
self.network_validator.clear_errors()
if not result:
valid = False
return valid

View File

@@ -49,7 +49,7 @@ runs:
- name: Validate Inputs
id: validate
uses: ivuorinen/actions/validate-inputs@8fb52522ab00fe73cf181ef299e56066f0b2c8d8
uses: ivuorinen/actions/validate-inputs@5cc7373a22402ee8985376bc713f00e09b5b2edb
with:
action-type: 'pre-commit'
token: ${{ inputs.token }}
@@ -83,7 +83,7 @@ runs:
- name: Push pre-commit fixes
id: push-fixes
if: always() # Push changes even when pre-commit fails
uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
with:
commit_message: 'style(pre-commit): autofix'
commit_user_name: ${{ inputs.commit_user }}

View File

@@ -274,9 +274,9 @@ runs:
echo "Detected package manager: $package_manager"
- name: Setup Node.js
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: '22'
node-version: '24'
- name: Enable Corepack
shell: sh
@@ -305,13 +305,13 @@ runs:
- name: Setup Bun
if: steps.detect-pm.outputs.package-manager == 'bun'
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
uses: oven-sh/setup-bun@ecf28ddc73e819eb6fa29df6b34ef8921c743461 # v2.1.3
with:
bun-version: latest
- name: Cache Node Dependencies
id: cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: node_modules
key: ${{ runner.os }}-prettier-lint-${{ inputs.mode }}-${{ steps.detect-pm.outputs.package-manager }}-${{ hashFiles('package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb') }}
@@ -468,7 +468,7 @@ runs:
- name: Commit and Push Fixes
if: inputs.mode == 'fix' && success()
uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
with:
commit_message: 'style: autofix Prettier formatting'
commit_user_name: ${{ inputs.username }}

View File

@@ -2,7 +2,7 @@
# Validation rules for prettier-lint action
# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY
# Schema version: 1.0
# Coverage: 86% (12/14 inputs)
# Coverage: 100% (14/14 inputs)
#
# This file defines validation rules for the prettier-lint GitHub Action.
# Rules are automatically applied by validate-inputs action when this
@@ -34,21 +34,24 @@ conventions:
config-file: file_path
email: email
fail-on-error: boolean
file-pattern: path_list
ignore-file: file_path
max-retries: numeric_range_1_10
mode: mode_enum
plugins: linter_list
prettier-version: semantic_version
report-format: report_format
token: github_token
username: username
working-directory: file_path
overrides: {}
overrides:
mode: mode_enum
statistics:
total_inputs: 14
validated_inputs: 12
validated_inputs: 14
skipped_inputs: 0
coverage_percentage: 86
validation_coverage: 86
coverage_percentage: 100
validation_coverage: 100
auto_detected: true
manual_review_required: false
quality_indicators:

View File

@@ -31,68 +31,42 @@ class CustomValidator(BaseValidator):
valid = True
# Validate python-version if provided
if "python-version" in inputs or "python_version" in inputs:
key = "python-version" if "python-version" in inputs else "python_version"
value = inputs[key]
# Empty string should fail validation
if value == "":
version_key = self.get_key_variant(inputs, "python-version", "python_version")
if version_key:
value = inputs[version_key]
if not value:
self.add_error("Python version cannot be empty")
valid = False
elif value:
result = self.version_validator.validate_python_version(value, key)
# Propagate errors from the version validator
for error in self.version_validator.errors:
if error not in self.errors:
self.add_error(error)
self.version_validator.clear_errors()
if not result:
valid = False
else:
valid &= self.validate_with(
self.version_validator, "validate_python_version", value, version_key
)
# Validate username
if "username" in inputs:
if inputs.get("username"):
username = inputs["username"]
if username:
# Check username length (GitHub usernames are max 39 characters)
if len(username) > 39:
self.add_error("Username is too long (max 39 characters)")
valid = False
# Check for command injection patterns
if ";" in username or "`" in username or "$" in username:
self.add_error("Username contains potentially dangerous characters")
valid = False
if len(username) > 39:
self.add_error("Username is too long (max 39 characters)")
valid = False
if ";" in username or "`" in username or "$" in username:
self.add_error("Username contains potentially dangerous characters")
valid = False
# Validate email
if "email" in inputs:
email = inputs["email"]
if email:
result = self.network_validator.validate_email(email, "email")
for error in self.network_validator.errors:
if error not in self.errors:
self.add_error(error)
self.network_validator.clear_errors()
if not result:
valid = False
if inputs.get("email"):
valid &= self.validate_with(
self.network_validator, "validate_email", inputs["email"], "email"
)
# Validate token
if "token" in inputs:
if inputs.get("token"):
token = inputs["token"]
if token:
# Check for variable expansion (but allow GitHub Actions expressions)
if "${" in token and not token.startswith("${{ ") and not token.endswith(" }}"):
self.add_error("Token contains potentially dangerous variable expansion")
valid = False
else:
result = self.token_validator.validate_github_token(token)
for error in self.token_validator.errors:
if error not in self.errors:
self.add_error(error)
self.token_validator.clear_errors()
if not result:
valid = False
# Check for variable expansion (but allow GitHub Actions expressions)
if "${" in token and not token.startswith("${{ ") and not token.endswith(" }}"):
self.add_error("Token contains potentially dangerous variable expansion")
valid = False
else:
valid &= self.validate_with(self.token_validator, "validate_github_token", token)
return valid

View File

@@ -64,7 +64,7 @@ runs:
steps:
- name: Validate Inputs
id: validate
uses: ivuorinen/actions/validate-inputs@8fb52522ab00fe73cf181ef299e56066f0b2c8d8
uses: ivuorinen/actions/validate-inputs@5cc7373a22402ee8985376bc713f00e09b5b2edb
with:
action-type: 'python-lint-fix'
token: ${{ inputs.token }}
@@ -224,7 +224,7 @@ runs:
- name: Setup Python (pip)
if: steps.package-manager.outputs.package-manager == 'pip'
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ steps.python-version.outputs.detected-version }}
cache: 'pip'
@@ -237,7 +237,7 @@ runs:
- name: Setup Python (pipenv)
if: steps.package-manager.outputs.package-manager == 'pipenv'
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ steps.python-version.outputs.detected-version }}
cache: 'pipenv'
@@ -247,7 +247,7 @@ runs:
- name: Setup Python (poetry)
if: steps.package-manager.outputs.package-manager == 'poetry'
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ steps.python-version.outputs.detected-version }}
cache: 'poetry'
@@ -361,7 +361,7 @@ runs:
- name: Commit Fixes
if: ${{ fromJSON(steps.fix.outputs.fixed_count) > 0 }}
uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
with:
commit_message: 'style: apply python lint fixes'
commit_user_name: ${{ inputs.username }}
@@ -370,7 +370,7 @@ runs:
- name: Upload SARIF Report
if: steps.check-files.outputs.result == 'found'
uses: github/codeql-action/upload-sarif@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with:
sarif_file: ${{ inputs.working-directory }}/reports/flake8.sarif
category: 'python-lint'

82
security-scan/README.md Normal file
View File

@@ -0,0 +1,82 @@
# ivuorinen/actions/security-scan
## Security Scan
### Description
Comprehensive security scanning for GitHub Actions including actionlint,
Gitleaks (optional), and Trivy vulnerability scanning. Requires
'security-events: write' and 'contents: read' permissions in the workflow.
### Inputs
| name | description | required | default |
|----------------------|--------------------------------------------------------------|----------|----------------------|
| `gitleaks-license` | <p>Gitleaks license key (required for Gitleaks scanning)</p> | `false` | `""` |
| `gitleaks-config` | <p>Path to Gitleaks config file</p> | `false` | `.gitleaks.toml` |
| `trivy-severity` | <p>Severity levels to scan for (comma-separated)</p> | `false` | `CRITICAL,HIGH` |
| `trivy-scanners` | <p>Types of scanners to run (comma-separated)</p> | `false` | `vuln,config,secret` |
| `trivy-timeout` | <p>Timeout for Trivy scan</p> | `false` | `10m` |
| `actionlint-enabled` | <p>Enable actionlint scanning</p> | `false` | `true` |
| `token` | <p>GitHub token for authentication</p> | `false` | `""` |
### Outputs
| name | description |
|------------------------|-----------------------------------------------------|
| `has_trivy_results` | <p>Whether Trivy scan produced valid results</p> |
| `has_gitleaks_results` | <p>Whether Gitleaks scan produced valid results</p> |
| `total_issues` | <p>Total number of security issues found</p> |
| `critical_issues` | <p>Number of critical security issues found</p> |
### Runs
This action is a `composite` action.
### Usage
```yaml
- uses: ivuorinen/actions/security-scan@main
with:
gitleaks-license:
# Gitleaks license key (required for Gitleaks scanning)
#
# Required: false
# Default: ""
gitleaks-config:
# Path to Gitleaks config file
#
# Required: false
# Default: .gitleaks.toml
trivy-severity:
# Severity levels to scan for (comma-separated)
#
# Required: false
# Default: CRITICAL,HIGH
trivy-scanners:
# Types of scanners to run (comma-separated)
#
# Required: false
# Default: vuln,config,secret
trivy-timeout:
# Timeout for Trivy scan
#
# Required: false
# Default: 10m
actionlint-enabled:
# Enable actionlint scanning
#
# Required: false
# Default: true
token:
# GitHub token for authentication
#
# Required: false
# Default: ""
```

282
security-scan/action.yml Normal file
View File

@@ -0,0 +1,282 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-action.json
#
# REQUIRED PERMISSIONS (set these in your workflow file):
# permissions:
# security-events: write # Required for SARIF uploads
# contents: read # Required for repository access
#
---
name: Security Scan
description: |
Comprehensive security scanning for GitHub Actions including actionlint,
Gitleaks (optional), and Trivy vulnerability scanning. Requires
'security-events: write' and 'contents: read' permissions in the workflow.
author: Ismo Vuorinen
branding:
icon: shield
color: red
inputs:
gitleaks-license:
description: 'Gitleaks license key (required for Gitleaks scanning)'
required: false
default: ''
gitleaks-config:
description: 'Path to Gitleaks config file'
required: false
default: '.gitleaks.toml'
trivy-severity:
description: 'Severity levels to scan for (comma-separated)'
required: false
default: 'CRITICAL,HIGH'
trivy-scanners:
description: 'Types of scanners to run (comma-separated)'
required: false
default: 'vuln,config,secret'
trivy-timeout:
description: 'Timeout for Trivy scan'
required: false
default: '10m'
actionlint-enabled:
description: 'Enable actionlint scanning'
required: false
default: 'true'
token:
description: 'GitHub token for authentication'
required: false
default: ''
outputs:
has_trivy_results:
description: 'Whether Trivy scan produced valid results'
value: ${{ steps.verify-sarif.outputs.has_trivy }}
has_gitleaks_results:
description: 'Whether Gitleaks scan produced valid results'
value: ${{ steps.verify-sarif.outputs.has_gitleaks }}
total_issues:
description: 'Total number of security issues found'
value: ${{ steps.analyze.outputs.total_issues }}
critical_issues:
description: 'Number of critical security issues found'
value: ${{ steps.analyze.outputs.critical_issues }}
runs:
using: composite
steps:
- name: Validate Inputs
id: validate
uses: ivuorinen/actions/validate-inputs@5cc7373a22402ee8985376bc713f00e09b5b2edb
with:
action-type: security-scan
gitleaks-license: ${{ inputs.gitleaks-license }}
gitleaks-config: ${{ inputs.gitleaks-config }}
trivy-severity: ${{ inputs.trivy-severity }}
trivy-scanners: ${{ inputs.trivy-scanners }}
trivy-timeout: ${{ inputs.trivy-timeout }}
actionlint-enabled: ${{ inputs.actionlint-enabled }}
token: ${{ inputs.token }}
- name: Check Required Configurations
id: check-configs
shell: sh
run: |
set -eu
# Initialize all flags as false
{
printf '%s\n' "run_gitleaks=false"
printf '%s\n' "run_trivy=true"
printf '%s\n' "run_actionlint=${{ inputs.actionlint-enabled }}"
} >> "$GITHUB_OUTPUT"
# Check Gitleaks configuration and license
if [ -f "${{ inputs.gitleaks-config }}" ] && [ -n "${{ inputs.gitleaks-license }}" ]; then
printf 'Gitleaks config and license found\n'
printf '%s\n' "run_gitleaks=true" >> "$GITHUB_OUTPUT"
else
printf '::warning::Gitleaks config or license missing - skipping Gitleaks scan\n'
fi
- name: Run actionlint
if: steps.check-configs.outputs.run_actionlint == 'true'
uses: raven-actions/actionlint@205b530c5d9fa8f44ae9ed59f341a0db994aa6f8 # v2.1.2
with:
cache: true
fail-on-error: true
shellcheck: false
- name: Run Gitleaks
if: steps.check-configs.outputs.run_gitleaks == 'true'
uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2.3.9
env:
GITHUB_TOKEN: ${{ inputs.token || github.token }}
GITLEAKS_LICENSE: ${{ inputs.gitleaks-license }}
with:
config-path: ${{ inputs.gitleaks-config }}
report-format: sarif
report-path: gitleaks-report.sarif
- name: Run Trivy vulnerability scanner
if: steps.check-configs.outputs.run_trivy == 'true'
uses: aquasecurity/trivy-action@a11da62073708815958ea6d84f5650c78a3ef85b # master
with:
scan-type: 'fs'
scanners: ${{ inputs.trivy-scanners }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: ${{ inputs.trivy-severity }}
timeout: ${{ inputs.trivy-timeout }}
- name: Verify SARIF files
id: verify-sarif
shell: sh
run: |
set -eu
# Initialize outputs
{
printf '%s\n' "has_trivy=false"
printf '%s\n' "has_gitleaks=false"
} >> "$GITHUB_OUTPUT"
# Check Trivy results
if [ -f "trivy-results.sarif" ]; then
if jq -e . <"trivy-results.sarif" >/dev/null 2>&1; then
printf '%s\n' "has_trivy=true" >> "$GITHUB_OUTPUT"
else
printf '::warning::Trivy SARIF file exists but is not valid JSON\n'
fi
fi
# Check Gitleaks results if it ran
if [ "${{ steps.check-configs.outputs.run_gitleaks }}" = "true" ]; then
if [ -f "gitleaks-report.sarif" ]; then
if jq -e . <"gitleaks-report.sarif" >/dev/null 2>&1; then
printf '%s\n' "has_gitleaks=true" >> "$GITHUB_OUTPUT"
else
printf '::warning::Gitleaks SARIF file exists but is not valid JSON\n'
fi
fi
fi
- name: Upload Trivy results
if: steps.verify-sarif.outputs.has_trivy == 'true'
uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with:
sarif_file: 'trivy-results.sarif'
category: 'trivy'
- name: Upload Gitleaks results
if: steps.verify-sarif.outputs.has_gitleaks == 'true'
uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with:
sarif_file: 'gitleaks-report.sarif'
category: 'gitleaks'
- name: Archive security reports
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: security-reports-${{ github.run_id }}
path: |
${{ steps.verify-sarif.outputs.has_trivy == 'true' && 'trivy-results.sarif' || '' }}
${{ steps.verify-sarif.outputs.has_gitleaks == 'true' && 'gitleaks-report.sarif' || '' }}
retention-days: 30
- name: Analyze Results
id: analyze
if: always()
shell: node {0}
run: |
const fs = require('fs');
try {
let totalIssues = 0;
let criticalIssues = 0;
const analyzeSarif = (file, tool) => {
if (!fs.existsSync(file)) {
console.log(`No results file found for ${tool}`);
return null;
}
try {
const sarif = JSON.parse(fs.readFileSync(file, 'utf8'));
return sarif.runs.reduce((acc, run) => {
if (!run.results) return acc;
const critical = run.results.filter(r =>
r.level === 'error' ||
r.level === 'critical' ||
(r.ruleId || '').toLowerCase().includes('critical')
).length;
return {
total: acc.total + run.results.length,
critical: acc.critical + critical
};
}, { total: 0, critical: 0 });
} catch (error) {
console.log(`Error analyzing ${tool} results: ${error.message}`);
return null;
}
};
// Only analyze results from tools that ran successfully
const results = {
trivy: '${{ steps.verify-sarif.outputs.has_trivy }}' === 'true' ?
analyzeSarif('trivy-results.sarif', 'trivy') : null,
gitleaks: '${{ steps.verify-sarif.outputs.has_gitleaks }}' === 'true' ?
analyzeSarif('gitleaks-report.sarif', 'gitleaks') : null
};
// Aggregate results
Object.entries(results).forEach(([tool, result]) => {
if (result) {
totalIssues += result.total;
criticalIssues += result.critical;
console.log(`${tool}: ${result.total} total, ${result.critical} critical issues`);
}
});
// Create summary
const summary = `## Security Scan Summary
- Total Issues Found: ${totalIssues}
- Critical Issues: ${criticalIssues}
### Tool Breakdown
${Object.entries(results)
.filter(([_, r]) => r)
.map(([tool, r]) =>
`- ${tool}: ${r.total} total, ${r.critical} critical`
).join('\n')}
### Tools Run Status
- Actionlint: ${{ steps.check-configs.outputs.run_actionlint }}
- Trivy: ${{ steps.verify-sarif.outputs.has_trivy }}
- Gitleaks: ${{ steps.check-configs.outputs.run_gitleaks }}
`;
// Set outputs using GITHUB_OUTPUT
const outputFile = process.env.GITHUB_OUTPUT;
if (outputFile) {
fs.appendFileSync(outputFile, `total_issues=${totalIssues}\n`);
fs.appendFileSync(outputFile, `critical_issues=${criticalIssues}\n`);
}
// Add job summary using GITHUB_STEP_SUMMARY
const summaryFile = process.env.GITHUB_STEP_SUMMARY;
if (summaryFile) {
fs.appendFileSync(summaryFile, summary + '\n');
}
// Fail if critical issues found
if (criticalIssues > 0) {
console.error(`Found ${criticalIssues} critical security issues`);
process.exit(1);
}
} catch (error) {
console.error(`Analysis failed: ${error.message}`);
process.exit(1);
}

55
security-scan/rules.yml Normal file
View File

@@ -0,0 +1,55 @@
---
# Validation rules for security-scan action
# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY
# Schema version: 1.0
# Coverage: 86% (6/7 inputs)
#
# This file defines validation rules for the security-scan GitHub Action.
# Rules are automatically applied by validate-inputs action when this
# action is used.
#
schema_version: '1.0'
action: security-scan
description: |
Comprehensive security scanning for GitHub Actions including actionlint,
Gitleaks (optional), and Trivy vulnerability scanning. Requires
'security-events: write' and 'contents: read' permissions in the workflow.
generator_version: 1.0.0
required_inputs: []
optional_inputs:
- actionlint-enabled
- gitleaks-config
- gitleaks-license
- token
- trivy-scanners
- trivy-severity
- trivy-timeout
conventions:
actionlint-enabled: boolean
gitleaks-config: file_path
token: github_token
trivy-scanners: scanner_list
trivy-severity: severity_enum
trivy-timeout: timeout_with_unit
overrides:
actionlint-enabled: boolean
gitleaks-config: file_path
token: github_token
trivy-scanners: scanner_list
trivy-severity: severity_enum
trivy-timeout: timeout_with_unit
statistics:
total_inputs: 7
validated_inputs: 6
skipped_inputs: 0
coverage_percentage: 86
validation_coverage: 86
auto_detected: true
manual_review_required: false
quality_indicators:
has_required_inputs: false
has_token_validation: true
has_version_validation: false
has_file_validation: true
has_security_validation: true

View File

@@ -43,7 +43,7 @@ runs:
- name: Validate Inputs
id: validate
uses: ivuorinen/actions/validate-inputs@8fb52522ab00fe73cf181ef299e56066f0b2c8d8
uses: ivuorinen/actions/validate-inputs@5cc7373a22402ee8985376bc713f00e09b5b2edb
with:
action-type: 'stale'
token: ${{ inputs.token || github.token }}
@@ -52,7 +52,7 @@ runs:
- name: 🚀 Run stale
id: stale
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
repo-token: ${{ inputs.token || github.token }}
days-before-stale: ${{ inputs.days-before-stale }}

View File

@@ -78,16 +78,9 @@ class CustomValidator(BaseValidator):
# Validate token if provided
if "token" in inputs:
token_valid = self.token_validator.validate_github_token(
inputs["token"],
required=False, # Token is optional, defaults to ${{ github.token }}
valid &= self.validate_with(
self.token_validator, "validate_github_token", inputs["token"], required=False
)
# Copy any errors from token validator
for error in self.token_validator.errors:
if error not in self.errors:
self.add_error(error)
self.token_validator.clear_errors()
valid &= token_valid
return valid
@@ -100,27 +93,15 @@ class CustomValidator(BaseValidator):
Returns:
True if valid, False otherwise
"""
# Allow GitHub Actions expressions
if self.is_github_expression(path):
return True
# First check basic file path security
result = self.file_validator.validate_file_path(path, "labels")
# Copy any errors from file validator
for error in self.file_validator.errors:
if error not in self.errors:
self.add_error(error)
self.file_validator.clear_errors()
result = self.validate_with(self.file_validator, "validate_file_path", path, "labels")
if not result:
return False
# Check file extension
if not (path.endswith(".yml") or path.endswith(".yaml")):
self.add_error(f'Invalid labels file: "{path}". Must be a .yml or .yaml file')
return False
# Additional custom validation could go here
# For example, checking if the file exists, validating YAML structure, etc.
return True

View File

@@ -30,54 +30,32 @@ class CustomValidator(BaseValidator):
"""Validate terraform-lint-fix action inputs."""
valid = True
# Validate terraform-version if provided
if "terraform-version" in inputs:
value = inputs["terraform-version"]
# Validate terraform-version if provided (empty is OK - uses default)
if inputs.get("terraform-version"):
valid &= self.validate_with(
self.version_validator,
"validate_terraform_version",
inputs["terraform-version"],
"terraform-version",
)
# Empty string is OK - uses default
if value == "":
pass # Allow empty, will use default
elif value:
result = self.version_validator.validate_terraform_version(
value, "terraform-version"
)
# Propagate errors from the version validator
for error in self.version_validator.errors:
if error not in self.errors:
self.add_error(error)
self.version_validator.clear_errors()
if not result:
valid = False
# Validate token if provided
if "token" in inputs:
value = inputs["token"]
if value == "":
# Empty token is OK - uses default
pass
elif value:
result = self.token_validator.validate_github_token(value, required=False)
for error in self.token_validator.errors:
if error not in self.errors:
self.add_error(error)
self.token_validator.clear_errors()
if not result:
valid = False
# Validate token if provided (empty is OK - uses default)
if inputs.get("token"):
valid &= self.validate_with(
self.token_validator,
"validate_github_token",
inputs["token"],
required=False,
)
# Validate working-directory if provided
if "working-directory" in inputs:
value = inputs["working-directory"]
if value:
result = self.file_validator.validate_file_path(value, "working-directory")
for error in self.file_validator.errors:
if error not in self.errors:
self.add_error(error)
self.file_validator.clear_errors()
if not result:
valid = False
if inputs.get("working-directory"):
valid &= self.validate_with(
self.file_validator,
"validate_file_path",
inputs["working-directory"],
"working-directory",
)
return valid

View File

@@ -78,7 +78,7 @@ runs:
- name: Validate Inputs
id: validate
uses: ivuorinen/actions/validate-inputs@8fb52522ab00fe73cf181ef299e56066f0b2c8d8
uses: ivuorinen/actions/validate-inputs@5cc7373a22402ee8985376bc713f00e09b5b2edb
with:
action-type: 'terraform-lint-fix'
token: ${{ inputs.token || github.token }}
@@ -128,7 +128,7 @@ runs:
- name: Setup Terraform
if: steps.check-files.outputs.found == 'true'
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0
with:
terraform_version: ${{ inputs.terraform-version }}
terraform_wrapper: false
@@ -247,7 +247,7 @@ runs:
- name: Commit Fixes
if: steps.check-files.outputs.found == 'true' && inputs.auto-fix == 'true' && fromJSON(steps.fix.outputs.fixed_count) > 0
uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
with:
commit_message: 'style: apply terraform formatting fixes'
commit_user_name: ${{ inputs.username }}
@@ -256,7 +256,7 @@ runs:
- name: Upload SARIF Report
if: steps.check-files.outputs.found == 'true' && inputs.format == 'sarif'
uses: github/codeql-action/upload-sarif@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with:
sarif_file: ${{ env.VALIDATED_WORKING_DIR }}/reports/tflint.sarif
category: terraform-lint

440
uv.lock generated
View File

@@ -1,5 +1,5 @@
version = 1
revision = 2
revision = 3
requires-python = ">=3.10"
[[package]]
@@ -13,87 +13,115 @@ wheels = [
[[package]]
name = "coverage"
version = "7.10.4"
version = "7.13.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/4e/08b493f1f1d8a5182df0044acc970799b58a8d289608e0d891a03e9d269a/coverage-7.10.4.tar.gz", hash = "sha256:25f5130af6c8e7297fd14634955ba9e1697f47143f289e2a23284177c0061d27", size = 823798, upload-time = "2025-08-17T00:26:43.314Z" }
sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9c/f4/350759710db50362685f922259c140592dba15eb4e2325656a98413864d9/coverage-7.10.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d92d6edb0ccafd20c6fbf9891ca720b39c2a6a4b4a6f9cf323ca2c986f33e475", size = 216403, upload-time = "2025-08-17T00:24:19.083Z" },
{ url = "https://files.pythonhosted.org/packages/29/7e/e467c2bb4d5ecfd166bfd22c405cce4c50de2763ba1d78e2729c59539a42/coverage-7.10.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7202da14dc0236884fcc45665ffb2d79d4991a53fbdf152ab22f69f70923cc22", size = 216802, upload-time = "2025-08-17T00:24:21.824Z" },
{ url = "https://files.pythonhosted.org/packages/62/ab/2accdd1ccfe63b890e5eb39118f63c155202df287798364868a2884a50af/coverage-7.10.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ada418633ae24ec8d0fcad5efe6fc7aa3c62497c6ed86589e57844ad04365674", size = 243558, upload-time = "2025-08-17T00:24:23.569Z" },
{ url = "https://files.pythonhosted.org/packages/43/04/c14c33d0cfc0f4db6b3504d01a47f4c798563d932a836fd5f2dbc0521d3d/coverage-7.10.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b828e33eca6c3322adda3b5884456f98c435182a44917ded05005adfa1415500", size = 245370, upload-time = "2025-08-17T00:24:24.858Z" },
{ url = "https://files.pythonhosted.org/packages/99/71/147053061f1f51c1d3b3d040c3cb26876964a3a0dca0765d2441411ca568/coverage-7.10.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:802793ba397afcfdbe9f91f89d65ae88b958d95edc8caf948e1f47d8b6b2b606", size = 247228, upload-time = "2025-08-17T00:24:26.167Z" },
{ url = "https://files.pythonhosted.org/packages/cc/92/7ef882205d4d4eb502e6154ee7122c1a1b1ce3f29d0166921e0fb550a5d3/coverage-7.10.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d0b23512338c54101d3bf7a1ab107d9d75abda1d5f69bc0887fd079253e4c27e", size = 245270, upload-time = "2025-08-17T00:24:27.424Z" },
{ url = "https://files.pythonhosted.org/packages/ab/3d/297a20603abcc6c7d89d801286eb477b0b861f3c5a4222730f1c9837be3e/coverage-7.10.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f36b7dcf72d06a8c5e2dd3aca02be2b1b5db5f86404627dff834396efce958f2", size = 243287, upload-time = "2025-08-17T00:24:28.697Z" },
{ url = "https://files.pythonhosted.org/packages/65/f9/b04111438f41f1ddd5dc88706d5f8064ae5bb962203c49fe417fa23a362d/coverage-7.10.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fce316c367a1dc2c411821365592eeb335ff1781956d87a0410eae248188ba51", size = 244164, upload-time = "2025-08-17T00:24:30.393Z" },
{ url = "https://files.pythonhosted.org/packages/1e/e5/c7d9eb7a9ea66cf92d069077719fb2b07782dcd7050b01a9b88766b52154/coverage-7.10.4-cp310-cp310-win32.whl", hash = "sha256:8c5dab29fc8070b3766b5fc85f8d89b19634584429a2da6d42da5edfadaf32ae", size = 218917, upload-time = "2025-08-17T00:24:31.67Z" },
{ url = "https://files.pythonhosted.org/packages/66/30/4d9d3b81f5a836b31a7428b8a25e6d490d4dca5ff2952492af130153c35c/coverage-7.10.4-cp310-cp310-win_amd64.whl", hash = "sha256:4b0d114616f0fccb529a1817457d5fb52a10e106f86c5fb3b0bd0d45d0d69b93", size = 219822, upload-time = "2025-08-17T00:24:32.89Z" },
{ url = "https://files.pythonhosted.org/packages/ec/ba/2c9817e62018e7d480d14f684c160b3038df9ff69c5af7d80e97d143e4d1/coverage-7.10.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:05d5f98ec893d4a2abc8bc5f046f2f4367404e7e5d5d18b83de8fde1093ebc4f", size = 216514, upload-time = "2025-08-17T00:24:34.188Z" },
{ url = "https://files.pythonhosted.org/packages/e3/5a/093412a959a6b6261446221ba9fb23bb63f661a5de70b5d130763c87f916/coverage-7.10.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9267efd28f8994b750d171e58e481e3bbd69e44baed540e4c789f8e368b24b88", size = 216914, upload-time = "2025-08-17T00:24:35.881Z" },
{ url = "https://files.pythonhosted.org/packages/2c/1f/2fdf4a71cfe93b07eae845ebf763267539a7d8b7e16b062f959d56d7e433/coverage-7.10.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4456a039fdc1a89ea60823d0330f1ac6f97b0dbe9e2b6fb4873e889584b085fb", size = 247308, upload-time = "2025-08-17T00:24:37.61Z" },
{ url = "https://files.pythonhosted.org/packages/ba/16/33f6cded458e84f008b9f6bc379609a6a1eda7bffe349153b9960803fc11/coverage-7.10.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c2bfbd2a9f7e68a21c5bd191be94bfdb2691ac40d325bac9ef3ae45ff5c753d9", size = 249241, upload-time = "2025-08-17T00:24:38.919Z" },
{ url = "https://files.pythonhosted.org/packages/84/98/9c18e47c889be58339ff2157c63b91a219272503ee32b49d926eea2337f2/coverage-7.10.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab7765f10ae1df7e7fe37de9e64b5a269b812ee22e2da3f84f97b1c7732a0d8", size = 251346, upload-time = "2025-08-17T00:24:40.507Z" },
{ url = "https://files.pythonhosted.org/packages/6d/07/00a6c0d53e9a22d36d8e95ddd049b860eef8f4b9fd299f7ce34d8e323356/coverage-7.10.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a09b13695166236e171ec1627ff8434b9a9bae47528d0ba9d944c912d33b3d2", size = 249037, upload-time = "2025-08-17T00:24:41.904Z" },
{ url = "https://files.pythonhosted.org/packages/3e/0e/1e1b944d6a6483d07bab5ef6ce063fcf3d0cc555a16a8c05ebaab11f5607/coverage-7.10.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5c9e75dfdc0167d5675e9804f04a56b2cf47fb83a524654297000b578b8adcb7", size = 247090, upload-time = "2025-08-17T00:24:43.193Z" },
{ url = "https://files.pythonhosted.org/packages/62/43/2ce5ab8a728b8e25ced077111581290ffaef9efaf860a28e25435ab925cf/coverage-7.10.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c751261bfe6481caba15ec005a194cb60aad06f29235a74c24f18546d8377df0", size = 247732, upload-time = "2025-08-17T00:24:44.906Z" },
{ url = "https://files.pythonhosted.org/packages/a4/f3/706c4a24f42c1c5f3a2ca56637ab1270f84d9e75355160dc34d5e39bb5b7/coverage-7.10.4-cp311-cp311-win32.whl", hash = "sha256:051c7c9e765f003c2ff6e8c81ccea28a70fb5b0142671e4e3ede7cebd45c80af", size = 218961, upload-time = "2025-08-17T00:24:46.241Z" },
{ url = "https://files.pythonhosted.org/packages/e8/aa/6b9ea06e0290bf1cf2a2765bba89d561c5c563b4e9db8298bf83699c8b67/coverage-7.10.4-cp311-cp311-win_amd64.whl", hash = "sha256:1a647b152f10be08fb771ae4a1421dbff66141e3d8ab27d543b5eb9ea5af8e52", size = 219851, upload-time = "2025-08-17T00:24:48.795Z" },
{ url = "https://files.pythonhosted.org/packages/8b/be/f0dc9ad50ee183369e643cd7ed8f2ef5c491bc20b4c3387cbed97dd6e0d1/coverage-7.10.4-cp311-cp311-win_arm64.whl", hash = "sha256:b09b9e4e1de0d406ca9f19a371c2beefe3193b542f64a6dd40cfcf435b7d6aa0", size = 218530, upload-time = "2025-08-17T00:24:50.164Z" },
{ url = "https://files.pythonhosted.org/packages/9e/4a/781c9e4dd57cabda2a28e2ce5b00b6be416015265851060945a5ed4bd85e/coverage-7.10.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a1f0264abcabd4853d4cb9b3d164adbf1565da7dab1da1669e93f3ea60162d79", size = 216706, upload-time = "2025-08-17T00:24:51.528Z" },
{ url = "https://files.pythonhosted.org/packages/6a/8c/51255202ca03d2e7b664770289f80db6f47b05138e06cce112b3957d5dfd/coverage-7.10.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:536cbe6b118a4df231b11af3e0f974a72a095182ff8ec5f4868c931e8043ef3e", size = 216939, upload-time = "2025-08-17T00:24:53.171Z" },
{ url = "https://files.pythonhosted.org/packages/06/7f/df11131483698660f94d3c847dc76461369782d7a7644fcd72ac90da8fd0/coverage-7.10.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9a4c0d84134797b7bf3f080599d0cd501471f6c98b715405166860d79cfaa97e", size = 248429, upload-time = "2025-08-17T00:24:54.934Z" },
{ url = "https://files.pythonhosted.org/packages/eb/fa/13ac5eda7300e160bf98f082e75f5c5b4189bf3a883dd1ee42dbedfdc617/coverage-7.10.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7c155fc0f9cee8c9803ea0ad153ab6a3b956baa5d4cd993405dc0b45b2a0b9e0", size = 251178, upload-time = "2025-08-17T00:24:56.353Z" },
{ url = "https://files.pythonhosted.org/packages/9a/bc/f63b56a58ad0bec68a840e7be6b7ed9d6f6288d790760647bb88f5fea41e/coverage-7.10.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5f2ab6e451d4b07855d8bcf063adf11e199bff421a4ba57f5bb95b7444ca62", size = 252313, upload-time = "2025-08-17T00:24:57.692Z" },
{ url = "https://files.pythonhosted.org/packages/2b/b6/79338f1ea27b01266f845afb4485976211264ab92407d1c307babe3592a7/coverage-7.10.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:685b67d99b945b0c221be0780c336b303a7753b3e0ec0d618c795aada25d5e7a", size = 250230, upload-time = "2025-08-17T00:24:59.293Z" },
{ url = "https://files.pythonhosted.org/packages/bc/93/3b24f1da3e0286a4dc5832427e1d448d5296f8287464b1ff4a222abeeeb5/coverage-7.10.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0c079027e50c2ae44da51c2e294596cbc9dbb58f7ca45b30651c7e411060fc23", size = 248351, upload-time = "2025-08-17T00:25:00.676Z" },
{ url = "https://files.pythonhosted.org/packages/de/5f/d59412f869e49dcc5b89398ef3146c8bfaec870b179cc344d27932e0554b/coverage-7.10.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3749aa72b93ce516f77cf5034d8e3c0dfd45c6e8a163a602ede2dc5f9a0bb927", size = 249788, upload-time = "2025-08-17T00:25:02.354Z" },
{ url = "https://files.pythonhosted.org/packages/cc/52/04a3b733f40a0cc7c4a5b9b010844111dbf906df3e868b13e1ce7b39ac31/coverage-7.10.4-cp312-cp312-win32.whl", hash = "sha256:fecb97b3a52fa9bcd5a7375e72fae209088faf671d39fae67261f37772d5559a", size = 219131, upload-time = "2025-08-17T00:25:03.79Z" },
{ url = "https://files.pythonhosted.org/packages/83/dd/12909fc0b83888197b3ec43a4ac7753589591c08d00d9deda4158df2734e/coverage-7.10.4-cp312-cp312-win_amd64.whl", hash = "sha256:26de58f355626628a21fe6a70e1e1fad95702dafebfb0685280962ae1449f17b", size = 219939, upload-time = "2025-08-17T00:25:05.494Z" },
{ url = "https://files.pythonhosted.org/packages/83/c7/058bb3220fdd6821bada9685eadac2940429ab3c97025ce53549ff423cc1/coverage-7.10.4-cp312-cp312-win_arm64.whl", hash = "sha256:67e8885408f8325198862bc487038a4980c9277d753cb8812510927f2176437a", size = 218572, upload-time = "2025-08-17T00:25:06.897Z" },
{ url = "https://files.pythonhosted.org/packages/46/b0/4a3662de81f2ed792a4e425d59c4ae50d8dd1d844de252838c200beed65a/coverage-7.10.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b8e1d2015d5dfdbf964ecef12944c0c8c55b885bb5c0467ae8ef55e0e151233", size = 216735, upload-time = "2025-08-17T00:25:08.617Z" },
{ url = "https://files.pythonhosted.org/packages/c5/e8/e2dcffea01921bfffc6170fb4406cffb763a3b43a047bbd7923566708193/coverage-7.10.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:25735c299439018d66eb2dccf54f625aceb78645687a05f9f848f6e6c751e169", size = 216982, upload-time = "2025-08-17T00:25:10.384Z" },
{ url = "https://files.pythonhosted.org/packages/9d/59/cc89bb6ac869704d2781c2f5f7957d07097c77da0e8fdd4fd50dbf2ac9c0/coverage-7.10.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:715c06cb5eceac4d9b7cdf783ce04aa495f6aff657543fea75c30215b28ddb74", size = 247981, upload-time = "2025-08-17T00:25:11.854Z" },
{ url = "https://files.pythonhosted.org/packages/aa/23/3da089aa177ceaf0d3f96754ebc1318597822e6387560914cc480086e730/coverage-7.10.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e017ac69fac9aacd7df6dc464c05833e834dc5b00c914d7af9a5249fcccf07ef", size = 250584, upload-time = "2025-08-17T00:25:13.483Z" },
{ url = "https://files.pythonhosted.org/packages/ad/82/e8693c368535b4e5fad05252a366a1794d481c79ae0333ed943472fd778d/coverage-7.10.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bad180cc40b3fccb0f0e8c702d781492654ac2580d468e3ffc8065e38c6c2408", size = 251856, upload-time = "2025-08-17T00:25:15.27Z" },
{ url = "https://files.pythonhosted.org/packages/56/19/8b9cb13292e602fa4135b10a26ac4ce169a7fc7c285ff08bedd42ff6acca/coverage-7.10.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:becbdcd14f685fada010a5f792bf0895675ecf7481304fe159f0cd3f289550bd", size = 250015, upload-time = "2025-08-17T00:25:16.759Z" },
{ url = "https://files.pythonhosted.org/packages/10/e7/e5903990ce089527cf1c4f88b702985bd65c61ac245923f1ff1257dbcc02/coverage-7.10.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0b485ca21e16a76f68060911f97ebbe3e0d891da1dbbce6af7ca1ab3f98b9097", size = 247908, upload-time = "2025-08-17T00:25:18.232Z" },
{ url = "https://files.pythonhosted.org/packages/dd/c9/7d464f116df1df7fe340669af1ddbe1a371fc60f3082ff3dc837c4f1f2ab/coverage-7.10.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6c1d098ccfe8e1e0a1ed9a0249138899948afd2978cbf48eb1cc3fcd38469690", size = 249525, upload-time = "2025-08-17T00:25:20.141Z" },
{ url = "https://files.pythonhosted.org/packages/ce/42/722e0cdbf6c19e7235c2020837d4e00f3b07820fd012201a983238cc3a30/coverage-7.10.4-cp313-cp313-win32.whl", hash = "sha256:8630f8af2ca84b5c367c3df907b1706621abe06d6929f5045fd628968d421e6e", size = 219173, upload-time = "2025-08-17T00:25:21.56Z" },
{ url = "https://files.pythonhosted.org/packages/97/7e/aa70366f8275955cd51fa1ed52a521c7fcebcc0fc279f53c8c1ee6006dfe/coverage-7.10.4-cp313-cp313-win_amd64.whl", hash = "sha256:f68835d31c421736be367d32f179e14ca932978293fe1b4c7a6a49b555dff5b2", size = 219969, upload-time = "2025-08-17T00:25:23.501Z" },
{ url = "https://files.pythonhosted.org/packages/ac/96/c39d92d5aad8fec28d4606556bfc92b6fee0ab51e4a548d9b49fb15a777c/coverage-7.10.4-cp313-cp313-win_arm64.whl", hash = "sha256:6eaa61ff6724ca7ebc5326d1fae062d85e19b38dd922d50903702e6078370ae7", size = 218601, upload-time = "2025-08-17T00:25:25.295Z" },
{ url = "https://files.pythonhosted.org/packages/79/13/34d549a6177bd80fa5db758cb6fd3057b7ad9296d8707d4ab7f480b0135f/coverage-7.10.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:702978108876bfb3d997604930b05fe769462cc3000150b0e607b7b444f2fd84", size = 217445, upload-time = "2025-08-17T00:25:27.129Z" },
{ url = "https://files.pythonhosted.org/packages/6a/c0/433da866359bf39bf595f46d134ff2d6b4293aeea7f3328b6898733b0633/coverage-7.10.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e8f978e8c5521d9c8f2086ac60d931d583fab0a16f382f6eb89453fe998e2484", size = 217676, upload-time = "2025-08-17T00:25:28.641Z" },
{ url = "https://files.pythonhosted.org/packages/7e/d7/2b99aa8737f7801fd95222c79a4ebc8c5dd4460d4bed7ef26b17a60c8d74/coverage-7.10.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:df0ac2ccfd19351411c45e43ab60932b74472e4648b0a9edf6a3b58846e246a9", size = 259002, upload-time = "2025-08-17T00:25:30.065Z" },
{ url = "https://files.pythonhosted.org/packages/08/cf/86432b69d57debaef5abf19aae661ba8f4fcd2882fa762e14added4bd334/coverage-7.10.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:73a0d1aaaa3796179f336448e1576a3de6fc95ff4f07c2d7251d4caf5d18cf8d", size = 261178, upload-time = "2025-08-17T00:25:31.517Z" },
{ url = "https://files.pythonhosted.org/packages/23/78/85176593f4aa6e869cbed7a8098da3448a50e3fac5cb2ecba57729a5220d/coverage-7.10.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:873da6d0ed6b3ffc0bc01f2c7e3ad7e2023751c0d8d86c26fe7322c314b031dc", size = 263402, upload-time = "2025-08-17T00:25:33.339Z" },
{ url = "https://files.pythonhosted.org/packages/88/1d/57a27b6789b79abcac0cc5805b31320d7a97fa20f728a6a7c562db9a3733/coverage-7.10.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c6446c75b0e7dda5daa876a1c87b480b2b52affb972fedd6c22edf1aaf2e00ec", size = 260957, upload-time = "2025-08-17T00:25:34.795Z" },
{ url = "https://files.pythonhosted.org/packages/fa/e5/3e5ddfd42835c6def6cd5b2bdb3348da2e34c08d9c1211e91a49e9fd709d/coverage-7.10.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6e73933e296634e520390c44758d553d3b573b321608118363e52113790633b9", size = 258718, upload-time = "2025-08-17T00:25:36.259Z" },
{ url = "https://files.pythonhosted.org/packages/1a/0b/d364f0f7ef111615dc4e05a6ed02cac7b6f2ac169884aa57faeae9eb5fa0/coverage-7.10.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52073d4b08d2cb571234c8a71eb32af3c6923149cf644a51d5957ac128cf6aa4", size = 259848, upload-time = "2025-08-17T00:25:37.754Z" },
{ url = "https://files.pythonhosted.org/packages/10/c6/bbea60a3b309621162e53faf7fac740daaf083048ea22077418e1ecaba3f/coverage-7.10.4-cp313-cp313t-win32.whl", hash = "sha256:e24afb178f21f9ceb1aefbc73eb524769aa9b504a42b26857243f881af56880c", size = 219833, upload-time = "2025-08-17T00:25:39.252Z" },
{ url = "https://files.pythonhosted.org/packages/44/a5/f9f080d49cfb117ddffe672f21eab41bd23a46179a907820743afac7c021/coverage-7.10.4-cp313-cp313t-win_amd64.whl", hash = "sha256:be04507ff1ad206f4be3d156a674e3fb84bbb751ea1b23b142979ac9eebaa15f", size = 220897, upload-time = "2025-08-17T00:25:40.772Z" },
{ url = "https://files.pythonhosted.org/packages/46/89/49a3fc784fa73d707f603e586d84a18c2e7796707044e9d73d13260930b7/coverage-7.10.4-cp313-cp313t-win_arm64.whl", hash = "sha256:f3e3ff3f69d02b5dad67a6eac68cc9c71ae343b6328aae96e914f9f2f23a22e2", size = 219160, upload-time = "2025-08-17T00:25:42.229Z" },
{ url = "https://files.pythonhosted.org/packages/b5/22/525f84b4cbcff66024d29f6909d7ecde97223f998116d3677cfba0d115b5/coverage-7.10.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a59fe0af7dd7211ba595cf7e2867458381f7e5d7b4cffe46274e0b2f5b9f4eb4", size = 216717, upload-time = "2025-08-17T00:25:43.875Z" },
{ url = "https://files.pythonhosted.org/packages/a6/58/213577f77efe44333a416d4bcb251471e7f64b19b5886bb515561b5ce389/coverage-7.10.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3a6c35c5b70f569ee38dc3350cd14fdd0347a8b389a18bb37538cc43e6f730e6", size = 216994, upload-time = "2025-08-17T00:25:45.405Z" },
{ url = "https://files.pythonhosted.org/packages/17/85/34ac02d0985a09472f41b609a1d7babc32df87c726c7612dc93d30679b5a/coverage-7.10.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:acb7baf49f513554c4af6ef8e2bd6e8ac74e6ea0c7386df8b3eb586d82ccccc4", size = 248038, upload-time = "2025-08-17T00:25:46.981Z" },
{ url = "https://files.pythonhosted.org/packages/47/4f/2140305ec93642fdaf988f139813629cbb6d8efa661b30a04b6f7c67c31e/coverage-7.10.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a89afecec1ed12ac13ed203238b560cbfad3522bae37d91c102e690b8b1dc46c", size = 250575, upload-time = "2025-08-17T00:25:48.613Z" },
{ url = "https://files.pythonhosted.org/packages/f2/b5/41b5784180b82a083c76aeba8f2c72ea1cb789e5382157b7dc852832aea2/coverage-7.10.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:480442727f464407d8ade6e677b7f21f3b96a9838ab541b9a28ce9e44123c14e", size = 251927, upload-time = "2025-08-17T00:25:50.881Z" },
{ url = "https://files.pythonhosted.org/packages/78/ca/c1dd063e50b71f5aea2ebb27a1c404e7b5ecf5714c8b5301f20e4e8831ac/coverage-7.10.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a89bf193707f4a17f1ed461504031074d87f035153239f16ce86dfb8f8c7ac76", size = 249930, upload-time = "2025-08-17T00:25:52.422Z" },
{ url = "https://files.pythonhosted.org/packages/8d/66/d8907408612ffee100d731798e6090aedb3ba766ecf929df296c1a7ee4fb/coverage-7.10.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:3ddd912c2fc440f0fb3229e764feec85669d5d80a988ff1b336a27d73f63c818", size = 247862, upload-time = "2025-08-17T00:25:54.316Z" },
{ url = "https://files.pythonhosted.org/packages/29/db/53cd8ec8b1c9c52d8e22a25434785bfc2d1e70c0cfb4d278a1326c87f741/coverage-7.10.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a538944ee3a42265e61c7298aeba9ea43f31c01271cf028f437a7b4075592cf", size = 249360, upload-time = "2025-08-17T00:25:55.833Z" },
{ url = "https://files.pythonhosted.org/packages/4f/75/5ec0a28ae4a0804124ea5a5becd2b0fa3adf30967ac656711fb5cdf67c60/coverage-7.10.4-cp314-cp314-win32.whl", hash = "sha256:fd2e6002be1c62476eb862b8514b1ba7e7684c50165f2a8d389e77da6c9a2ebd", size = 219449, upload-time = "2025-08-17T00:25:57.984Z" },
{ url = "https://files.pythonhosted.org/packages/9d/ab/66e2ee085ec60672bf5250f11101ad8143b81f24989e8c0e575d16bb1e53/coverage-7.10.4-cp314-cp314-win_amd64.whl", hash = "sha256:ec113277f2b5cf188d95fb66a65c7431f2b9192ee7e6ec9b72b30bbfb53c244a", size = 220246, upload-time = "2025-08-17T00:25:59.868Z" },
{ url = "https://files.pythonhosted.org/packages/37/3b/00b448d385f149143190846217797d730b973c3c0ec2045a7e0f5db3a7d0/coverage-7.10.4-cp314-cp314-win_arm64.whl", hash = "sha256:9744954bfd387796c6a091b50d55ca7cac3d08767795b5eec69ad0f7dbf12d38", size = 218825, upload-time = "2025-08-17T00:26:01.44Z" },
{ url = "https://files.pythonhosted.org/packages/ee/2e/55e20d3d1ce00b513efb6fd35f13899e1c6d4f76c6cbcc9851c7227cd469/coverage-7.10.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5af4829904dda6aabb54a23879f0f4412094ba9ef153aaa464e3c1b1c9bc98e6", size = 217462, upload-time = "2025-08-17T00:26:03.014Z" },
{ url = "https://files.pythonhosted.org/packages/47/b3/aab1260df5876f5921e2c57519e73a6f6eeacc0ae451e109d44ee747563e/coverage-7.10.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7bba5ed85e034831fac761ae506c0644d24fd5594727e174b5a73aff343a7508", size = 217675, upload-time = "2025-08-17T00:26:04.606Z" },
{ url = "https://files.pythonhosted.org/packages/67/23/1cfe2aa50c7026180989f0bfc242168ac7c8399ccc66eb816b171e0ab05e/coverage-7.10.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d57d555b0719834b55ad35045de6cc80fc2b28e05adb6b03c98479f9553b387f", size = 259176, upload-time = "2025-08-17T00:26:06.159Z" },
{ url = "https://files.pythonhosted.org/packages/9d/72/5882b6aeed3f9de7fc4049874fd7d24213bf1d06882f5c754c8a682606ec/coverage-7.10.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ba62c51a72048bb1ea72db265e6bd8beaabf9809cd2125bbb5306c6ce105f214", size = 261341, upload-time = "2025-08-17T00:26:08.137Z" },
{ url = "https://files.pythonhosted.org/packages/1b/70/a0c76e3087596ae155f8e71a49c2c534c58b92aeacaf4d9d0cbbf2dde53b/coverage-7.10.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0acf0c62a6095f07e9db4ec365cc58c0ef5babb757e54745a1aa2ea2a2564af1", size = 263600, upload-time = "2025-08-17T00:26:11.045Z" },
{ url = "https://files.pythonhosted.org/packages/cb/5f/27e4cd4505b9a3c05257fb7fc509acbc778c830c450cb4ace00bf2b7bda7/coverage-7.10.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e1033bf0f763f5cf49ffe6594314b11027dcc1073ac590b415ea93463466deec", size = 261036, upload-time = "2025-08-17T00:26:12.693Z" },
{ url = "https://files.pythonhosted.org/packages/02/d6/cf2ae3a7f90ab226ea765a104c4e76c5126f73c93a92eaea41e1dc6a1892/coverage-7.10.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:92c29eff894832b6a40da1789b1f252305af921750b03ee4535919db9179453d", size = 258794, upload-time = "2025-08-17T00:26:14.261Z" },
{ url = "https://files.pythonhosted.org/packages/9e/b1/39f222eab0d78aa2001cdb7852aa1140bba632db23a5cfd832218b496d6c/coverage-7.10.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:822c4c830989c2093527e92acd97be4638a44eb042b1bdc0e7a278d84a070bd3", size = 259946, upload-time = "2025-08-17T00:26:15.899Z" },
{ url = "https://files.pythonhosted.org/packages/74/b2/49d82acefe2fe7c777436a3097f928c7242a842538b190f66aac01f29321/coverage-7.10.4-cp314-cp314t-win32.whl", hash = "sha256:e694d855dac2e7cf194ba33653e4ba7aad7267a802a7b3fc4347d0517d5d65cd", size = 220226, upload-time = "2025-08-17T00:26:17.566Z" },
{ url = "https://files.pythonhosted.org/packages/06/b0/afb942b6b2fc30bdbc7b05b087beae11c2b0daaa08e160586cf012b6ad70/coverage-7.10.4-cp314-cp314t-win_amd64.whl", hash = "sha256:efcc54b38ef7d5bfa98050f220b415bc5bb3d432bd6350a861cf6da0ede2cdcd", size = 221346, upload-time = "2025-08-17T00:26:19.311Z" },
{ url = "https://files.pythonhosted.org/packages/d8/66/e0531c9d1525cb6eac5b5733c76f27f3053ee92665f83f8899516fea6e76/coverage-7.10.4-cp314-cp314t-win_arm64.whl", hash = "sha256:6f3a3496c0fa26bfac4ebc458747b778cff201c8ae94fa05e1391bab0dbc473c", size = 219368, upload-time = "2025-08-17T00:26:21.011Z" },
{ url = "https://files.pythonhosted.org/packages/bb/78/983efd23200921d9edb6bd40512e1aa04af553d7d5a171e50f9b2b45d109/coverage-7.10.4-py3-none-any.whl", hash = "sha256:065d75447228d05121e5c938ca8f0e91eed60a1eb2d1258d42d5084fecfc3302", size = 208365, upload-time = "2025-08-17T00:26:41.479Z" },
{ url = "https://files.pythonhosted.org/packages/44/d4/7827d9ffa34d5d4d752eec907022aa417120936282fc488306f5da08c292/coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415", size = 219152, upload-time = "2026-02-09T12:56:11.974Z" },
{ url = "https://files.pythonhosted.org/packages/35/b0/d69df26607c64043292644dbb9dc54b0856fabaa2cbb1eeee3331cc9e280/coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b", size = 219667, upload-time = "2026-02-09T12:56:13.33Z" },
{ url = "https://files.pythonhosted.org/packages/82/a4/c1523f7c9e47b2271dbf8c2a097e7a1f89ef0d66f5840bb59b7e8814157b/coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a", size = 246425, upload-time = "2026-02-09T12:56:14.552Z" },
{ url = "https://files.pythonhosted.org/packages/f8/02/aa7ec01d1a5023c4b680ab7257f9bfde9defe8fdddfe40be096ac19e8177/coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f", size = 248229, upload-time = "2026-02-09T12:56:16.31Z" },
{ url = "https://files.pythonhosted.org/packages/35/98/85aba0aed5126d896162087ef3f0e789a225697245256fc6181b95f47207/coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012", size = 250106, upload-time = "2026-02-09T12:56:18.024Z" },
{ url = "https://files.pythonhosted.org/packages/96/72/1db59bd67494bc162e3e4cd5fbc7edba2c7026b22f7c8ef1496d58c2b94c/coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def", size = 252021, upload-time = "2026-02-09T12:56:19.272Z" },
{ url = "https://files.pythonhosted.org/packages/9d/97/72899c59c7066961de6e3daa142d459d47d104956db43e057e034f015c8a/coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256", size = 247114, upload-time = "2026-02-09T12:56:21.051Z" },
{ url = "https://files.pythonhosted.org/packages/39/1f/f1885573b5970235e908da4389176936c8933e86cb316b9620aab1585fa2/coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda", size = 248143, upload-time = "2026-02-09T12:56:22.585Z" },
{ url = "https://files.pythonhosted.org/packages/a8/cf/e80390c5b7480b722fa3e994f8202807799b85bc562aa4f1dde209fbb7be/coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92", size = 246152, upload-time = "2026-02-09T12:56:23.748Z" },
{ url = "https://files.pythonhosted.org/packages/44/bf/f89a8350d85572f95412debb0fb9bb4795b1d5b5232bd652923c759e787b/coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c", size = 249959, upload-time = "2026-02-09T12:56:25.209Z" },
{ url = "https://files.pythonhosted.org/packages/f7/6e/612a02aece8178c818df273e8d1642190c4875402ca2ba74514394b27aba/coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58", size = 246416, upload-time = "2026-02-09T12:56:26.475Z" },
{ url = "https://files.pythonhosted.org/packages/cb/98/b5afc39af67c2fa6786b03c3a7091fc300947387ce8914b096db8a73d67a/coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9", size = 247025, upload-time = "2026-02-09T12:56:27.727Z" },
{ url = "https://files.pythonhosted.org/packages/51/30/2bba8ef0682d5bd210c38fe497e12a06c9f8d663f7025e9f5c2c31ce847d/coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf", size = 221758, upload-time = "2026-02-09T12:56:29.051Z" },
{ url = "https://files.pythonhosted.org/packages/78/13/331f94934cf6c092b8ea59ff868eb587bc8fe0893f02c55bc6c0183a192e/coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95", size = 222693, upload-time = "2026-02-09T12:56:30.366Z" },
{ url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" },
{ url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" },
{ url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" },
{ url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" },
{ url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" },
{ url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" },
{ url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" },
{ url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" },
{ url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" },
{ url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" },
{ url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" },
{ url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" },
{ url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" },
{ url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" },
{ url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" },
{ url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" },
{ url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" },
{ url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" },
{ url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" },
{ url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" },
{ url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" },
{ url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" },
{ url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" },
{ url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" },
{ url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" },
{ url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" },
{ url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" },
{ url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" },
{ url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" },
{ url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" },
{ url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" },
{ url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" },
{ url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" },
{ url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" },
{ url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" },
{ url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" },
{ url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" },
{ url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" },
{ url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" },
{ url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" },
{ url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" },
{ url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" },
{ url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" },
{ url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" },
{ url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" },
{ url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" },
{ url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" },
{ url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" },
{ url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" },
{ url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" },
{ url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" },
{ url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" },
{ url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" },
{ url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" },
{ url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" },
{ url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" },
{ url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" },
{ url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" },
{ url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" },
{ url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" },
{ url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" },
{ url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" },
{ url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" },
{ url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" },
{ url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" },
{ url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" },
{ url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" },
{ url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" },
{ url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" },
{ url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" },
{ url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" },
{ url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" },
{ url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" },
{ url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" },
{ url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" },
{ url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" },
{ url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" },
{ url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" },
{ url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" },
{ url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" },
{ url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" },
{ url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" },
{ url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" },
{ url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" },
{ url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" },
{ url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" },
{ url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" },
{ url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" },
{ url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" },
{ url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" },
{ url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" },
]
[package.optional-dependencies]
@@ -103,23 +131,23 @@ toml = [
[[package]]
name = "exceptiongroup"
version = "1.3.0"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" },
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
]
[[package]]
name = "iniconfig"
version = "2.1.0"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
@@ -148,11 +176,11 @@ provides-extras = ["dev"]
[[package]]
name = "packaging"
version = "25.0"
version = "26.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
]
[[package]]
@@ -175,7 +203,7 @@ wheels = [
[[package]]
name = "pytest"
version = "8.4.1"
version = "9.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -186,139 +214,173 @@ dependencies = [
{ name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" },
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
]
[[package]]
name = "pytest-cov"
version = "6.2.1"
version = "7.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "coverage", extra = ["toml"] },
{ name = "pluggy" },
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" }
sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" },
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
]
[[package]]
name = "pyyaml"
version = "6.0.2"
version = "6.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" },
{ url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" },
{ url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" },
{ url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" },
{ url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" },
{ url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" },
{ url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" },
{ url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" },
{ url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" },
{ url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" },
{ url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" },
{ url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" },
{ url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" },
{ url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" },
{ url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" },
{ url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" },
{ url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" },
{ url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" },
{ url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" },
{ url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" },
{ url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" },
{ url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" },
{ url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" },
{ url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" },
{ url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" },
{ url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" },
{ url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" },
{ url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
{ url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
{ url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
{ url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
{ url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
{ url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
{ url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
{ url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
{ url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
{ url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
{ url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
{ url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
{ url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
{ url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
{ url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
{ url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
{ url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
{ url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
{ url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
{ url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
{ url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
{ url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
{ url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
{ url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
{ url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
{ url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
[[package]]
name = "ruff"
version = "0.12.9"
version = "0.15.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4a/45/2e403fa7007816b5fbb324cb4f8ed3c7402a927a0a0cb2b6279879a8bfdc/ruff-0.12.9.tar.gz", hash = "sha256:fbd94b2e3c623f659962934e52c2bea6fc6da11f667a427a368adaf3af2c866a", size = 5254702, upload-time = "2025-08-14T16:08:55.2Z" }
sdist = { url = "https://files.pythonhosted.org/packages/77/9b/840e0039e65fcf12758adf684d2289024d6140cde9268cc59887dc55189c/ruff-0.15.5.tar.gz", hash = "sha256:7c3601d3b6d76dce18c5c824fc8d06f4eef33d6df0c21ec7799510cde0f159a2", size = 4574214, upload-time = "2026-03-05T20:06:34.946Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ad/20/53bf098537adb7b6a97d98fcdebf6e916fcd11b2e21d15f8c171507909cc/ruff-0.12.9-py3-none-linux_armv6l.whl", hash = "sha256:fcebc6c79fcae3f220d05585229463621f5dbf24d79fdc4936d9302e177cfa3e", size = 11759705, upload-time = "2025-08-14T16:08:12.968Z" },
{ url = "https://files.pythonhosted.org/packages/20/4d/c764ee423002aac1ec66b9d541285dd29d2c0640a8086c87de59ebbe80d5/ruff-0.12.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aed9d15f8c5755c0e74467731a007fcad41f19bcce41cd75f768bbd687f8535f", size = 12527042, upload-time = "2025-08-14T16:08:16.54Z" },
{ url = "https://files.pythonhosted.org/packages/8b/45/cfcdf6d3eb5fc78a5b419e7e616d6ccba0013dc5b180522920af2897e1be/ruff-0.12.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5b15ea354c6ff0d7423814ba6d44be2807644d0c05e9ed60caca87e963e93f70", size = 11724457, upload-time = "2025-08-14T16:08:18.686Z" },
{ url = "https://files.pythonhosted.org/packages/72/e6/44615c754b55662200c48bebb02196dbb14111b6e266ab071b7e7297b4ec/ruff-0.12.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d596c2d0393c2502eaabfef723bd74ca35348a8dac4267d18a94910087807c53", size = 11949446, upload-time = "2025-08-14T16:08:21.059Z" },
{ url = "https://files.pythonhosted.org/packages/fd/d1/9b7d46625d617c7df520d40d5ac6cdcdf20cbccb88fad4b5ecd476a6bb8d/ruff-0.12.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b15599931a1a7a03c388b9c5df1bfa62be7ede6eb7ef753b272381f39c3d0ff", size = 11566350, upload-time = "2025-08-14T16:08:23.433Z" },
{ url = "https://files.pythonhosted.org/packages/59/20/b73132f66f2856bc29d2d263c6ca457f8476b0bbbe064dac3ac3337a270f/ruff-0.12.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d02faa2977fb6f3f32ddb7828e212b7dd499c59eb896ae6c03ea5c303575756", size = 13270430, upload-time = "2025-08-14T16:08:25.837Z" },
{ url = "https://files.pythonhosted.org/packages/a2/21/eaf3806f0a3d4c6be0a69d435646fba775b65f3f2097d54898b0fd4bb12e/ruff-0.12.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:17d5b6b0b3a25259b69ebcba87908496e6830e03acfb929ef9fd4c58675fa2ea", size = 14264717, upload-time = "2025-08-14T16:08:27.907Z" },
{ url = "https://files.pythonhosted.org/packages/d2/82/1d0c53bd37dcb582b2c521d352fbf4876b1e28bc0d8894344198f6c9950d/ruff-0.12.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72db7521860e246adbb43f6ef464dd2a532ef2ef1f5dd0d470455b8d9f1773e0", size = 13684331, upload-time = "2025-08-14T16:08:30.352Z" },
{ url = "https://files.pythonhosted.org/packages/3b/2f/1c5cf6d8f656306d42a686f1e207f71d7cebdcbe7b2aa18e4e8a0cb74da3/ruff-0.12.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a03242c1522b4e0885af63320ad754d53983c9599157ee33e77d748363c561ce", size = 12739151, upload-time = "2025-08-14T16:08:32.55Z" },
{ url = "https://files.pythonhosted.org/packages/47/09/25033198bff89b24d734e6479e39b1968e4c992e82262d61cdccaf11afb9/ruff-0.12.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fc83e4e9751e6c13b5046d7162f205d0a7bac5840183c5beebf824b08a27340", size = 12954992, upload-time = "2025-08-14T16:08:34.816Z" },
{ url = "https://files.pythonhosted.org/packages/52/8e/d0dbf2f9dca66c2d7131feefc386523404014968cd6d22f057763935ab32/ruff-0.12.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:881465ed56ba4dd26a691954650de6ad389a2d1fdb130fe51ff18a25639fe4bb", size = 12899569, upload-time = "2025-08-14T16:08:36.852Z" },
{ url = "https://files.pythonhosted.org/packages/a0/bd/b614d7c08515b1428ed4d3f1d4e3d687deffb2479703b90237682586fa66/ruff-0.12.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:43f07a3ccfc62cdb4d3a3348bf0588358a66da756aa113e071b8ca8c3b9826af", size = 11751983, upload-time = "2025-08-14T16:08:39.314Z" },
{ url = "https://files.pythonhosted.org/packages/58/d6/383e9f818a2441b1a0ed898d7875f11273f10882f997388b2b51cb2ae8b5/ruff-0.12.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:07adb221c54b6bba24387911e5734357f042e5669fa5718920ee728aba3cbadc", size = 11538635, upload-time = "2025-08-14T16:08:41.297Z" },
{ url = "https://files.pythonhosted.org/packages/20/9c/56f869d314edaa9fc1f491706d1d8a47747b9d714130368fbd69ce9024e9/ruff-0.12.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f5cd34fabfdea3933ab85d72359f118035882a01bff15bd1d2b15261d85d5f66", size = 12534346, upload-time = "2025-08-14T16:08:43.39Z" },
{ url = "https://files.pythonhosted.org/packages/bd/4b/d8b95c6795a6c93b439bc913ee7a94fda42bb30a79285d47b80074003ee7/ruff-0.12.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f6be1d2ca0686c54564da8e7ee9e25f93bdd6868263805f8c0b8fc6a449db6d7", size = 13017021, upload-time = "2025-08-14T16:08:45.889Z" },
{ url = "https://files.pythonhosted.org/packages/c7/c1/5f9a839a697ce1acd7af44836f7c2181cdae5accd17a5cb85fcbd694075e/ruff-0.12.9-py3-none-win32.whl", hash = "sha256:cc7a37bd2509974379d0115cc5608a1a4a6c4bff1b452ea69db83c8855d53f93", size = 11734785, upload-time = "2025-08-14T16:08:48.062Z" },
{ url = "https://files.pythonhosted.org/packages/fa/66/cdddc2d1d9a9f677520b7cfc490d234336f523d4b429c1298de359a3be08/ruff-0.12.9-py3-none-win_amd64.whl", hash = "sha256:6fb15b1977309741d7d098c8a3cb7a30bc112760a00fb6efb7abc85f00ba5908", size = 12840654, upload-time = "2025-08-14T16:08:50.158Z" },
{ url = "https://files.pythonhosted.org/packages/ac/fd/669816bc6b5b93b9586f3c1d87cd6bc05028470b3ecfebb5938252c47a35/ruff-0.12.9-py3-none-win_arm64.whl", hash = "sha256:63c8c819739d86b96d500cce885956a1a48ab056bbcbc61b747ad494b2485089", size = 11949623, upload-time = "2025-08-14T16:08:52.233Z" },
{ url = "https://files.pythonhosted.org/packages/47/20/5369c3ce21588c708bcbe517a8fbe1a8dfdb5dfd5137e14790b1da71612c/ruff-0.15.5-py3-none-linux_armv6l.whl", hash = "sha256:4ae44c42281f42e3b06b988e442d344a5b9b72450ff3c892e30d11b29a96a57c", size = 10478185, upload-time = "2026-03-05T20:06:29.093Z" },
{ url = "https://files.pythonhosted.org/packages/44/ed/e81dd668547da281e5dce710cf0bc60193f8d3d43833e8241d006720e42b/ruff-0.15.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6edd3792d408ebcf61adabc01822da687579a1a023f297618ac27a5b51ef0080", size = 10859201, upload-time = "2026-03-05T20:06:32.632Z" },
{ url = "https://files.pythonhosted.org/packages/c4/8f/533075f00aaf19b07c5cd6aa6e5d89424b06b3b3f4583bfa9c640a079059/ruff-0.15.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:89f463f7c8205a9f8dea9d658d59eff49db05f88f89cc3047fb1a02d9f344010", size = 10184752, upload-time = "2026-03-05T20:06:40.312Z" },
{ url = "https://files.pythonhosted.org/packages/66/0e/ba49e2c3fa0395b3152bad634c7432f7edfc509c133b8f4529053ff024fb/ruff-0.15.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba786a8295c6574c1116704cf0b9e6563de3432ac888d8f83685654fe528fd65", size = 10534857, upload-time = "2026-03-05T20:06:19.581Z" },
{ url = "https://files.pythonhosted.org/packages/59/71/39234440f27a226475a0659561adb0d784b4d247dfe7f43ffc12dd02e288/ruff-0.15.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd4b801e57955fe9f02b31d20375ab3a5c4415f2e5105b79fb94cf2642c91440", size = 10309120, upload-time = "2026-03-05T20:06:00.435Z" },
{ url = "https://files.pythonhosted.org/packages/f5/87/4140aa86a93df032156982b726f4952aaec4a883bb98cb6ef73c347da253/ruff-0.15.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391f7c73388f3d8c11b794dbbc2959a5b5afe66642c142a6effa90b45f6f5204", size = 11047428, upload-time = "2026-03-05T20:05:51.867Z" },
{ url = "https://files.pythonhosted.org/packages/5a/f7/4953e7e3287676f78fbe85e3a0ca414c5ca81237b7575bdadc00229ac240/ruff-0.15.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dc18f30302e379fe1e998548b0f5e9f4dff907f52f73ad6da419ea9c19d66c8", size = 11914251, upload-time = "2026-03-05T20:06:22.887Z" },
{ url = "https://files.pythonhosted.org/packages/77/46/0f7c865c10cf896ccf5a939c3e84e1cfaeed608ff5249584799a74d33835/ruff-0.15.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc6e7f90087e2d27f98dc34ed1b3ab7c8f0d273cc5431415454e22c0bd2a681", size = 11333801, upload-time = "2026-03-05T20:05:57.168Z" },
{ url = "https://files.pythonhosted.org/packages/d3/01/a10fe54b653061585e655f5286c2662ebddb68831ed3eaebfb0eb08c0a16/ruff-0.15.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1cb7169f53c1ddb06e71a9aebd7e98fc0fea936b39afb36d8e86d36ecc2636a", size = 11206821, upload-time = "2026-03-05T20:06:03.441Z" },
{ url = "https://files.pythonhosted.org/packages/7a/0d/2132ceaf20c5e8699aa83da2706ecb5c5dcdf78b453f77edca7fb70f8a93/ruff-0.15.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9b037924500a31ee17389b5c8c4d88874cc6ea8e42f12e9c61a3d754ff72f1ca", size = 11133326, upload-time = "2026-03-05T20:06:25.655Z" },
{ url = "https://files.pythonhosted.org/packages/72/cb/2e5259a7eb2a0f87c08c0fe5bf5825a1e4b90883a52685524596bfc93072/ruff-0.15.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65bb414e5b4eadd95a8c1e4804f6772bbe8995889f203a01f77ddf2d790929dd", size = 10510820, upload-time = "2026-03-05T20:06:37.79Z" },
{ url = "https://files.pythonhosted.org/packages/ff/20/b67ce78f9e6c59ffbdb5b4503d0090e749b5f2d31b599b554698a80d861c/ruff-0.15.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d20aa469ae3b57033519c559e9bc9cd9e782842e39be05b50e852c7c981fa01d", size = 10302395, upload-time = "2026-03-05T20:05:54.504Z" },
{ url = "https://files.pythonhosted.org/packages/5f/e5/719f1acccd31b720d477751558ed74e9c88134adcc377e5e886af89d3072/ruff-0.15.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:15388dd28c9161cdb8eda68993533acc870aa4e646a0a277aa166de9ad5a8752", size = 10754069, upload-time = "2026-03-05T20:06:06.422Z" },
{ url = "https://files.pythonhosted.org/packages/c3/9c/d1db14469e32d98f3ca27079dbd30b7b44dbb5317d06ab36718dee3baf03/ruff-0.15.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b30da330cbd03bed0c21420b6b953158f60c74c54c5f4c1dabbdf3a57bf355d2", size = 11304315, upload-time = "2026-03-05T20:06:10.867Z" },
{ url = "https://files.pythonhosted.org/packages/28/3a/950367aee7c69027f4f422059227b290ed780366b6aecee5de5039d50fa8/ruff-0.15.5-py3-none-win32.whl", hash = "sha256:732e5ee1f98ba5b3679029989a06ca39a950cced52143a0ea82a2102cb592b74", size = 10551676, upload-time = "2026-03-05T20:06:13.705Z" },
{ url = "https://files.pythonhosted.org/packages/b8/00/bf077a505b4e649bdd3c47ff8ec967735ce2544c8e4a43aba42ee9bf935d/ruff-0.15.5-py3-none-win_amd64.whl", hash = "sha256:821d41c5fa9e19117616c35eaa3f4b75046ec76c65e7ae20a333e9a8696bc7fe", size = 11678972, upload-time = "2026-03-05T20:06:45.379Z" },
{ url = "https://files.pythonhosted.org/packages/fe/4e/cd76eca6db6115604b7626668e891c9dd03330384082e33662fb0f113614/ruff-0.15.5-py3-none-win_arm64.whl", hash = "sha256:b498d1c60d2fe5c10c45ec3f698901065772730b411f164ae270bb6bfcc4740b", size = 10965572, upload-time = "2026-03-05T20:06:16.984Z" },
]
[[package]]
name = "tomli"
version = "2.2.1"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" }
sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" },
{ url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" },
{ url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" },
{ url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" },
{ url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" },
{ url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" },
{ url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" },
{ url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" },
{ url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" },
{ url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" },
{ url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" },
{ url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" },
{ url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" },
{ url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" },
{ url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" },
{ url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" },
{ url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" },
{ url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" },
{ url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" },
{ url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" },
{ url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" },
{ url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" },
{ url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" },
{ url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" },
{ url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" },
{ url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" },
{ url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" },
{ url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" },
{ url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" },
{ url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" },
{ url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" },
{ url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" },
{ url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" },
{ url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" },
{ url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" },
{ url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" },
{ url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" },
{ url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" },
{ url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" },
{ url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" },
{ url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" },
{ url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" },
{ url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" },
{ url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" },
{ url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" },
{ url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" },
{ url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" },
{ url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" },
{ url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" },
{ url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" },
{ url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" },
{ url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" },
{ url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" },
{ url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" },
{ url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" },
{ url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" },
{ url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" },
{ url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" },
{ url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" },
{ url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" },
{ url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" },
{ url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" },
{ url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" },
{ url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" },
{ url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" },
{ url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" },
{ url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" },
{ url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" },
{ url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" },
{ url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" },
{ url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" },
{ url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" },
{ url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" },
{ url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" },
{ url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" },
{ url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" },
{ url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
]
[[package]]
name = "typing-extensions"
version = "4.14.1"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]

View File

@@ -27,57 +27,45 @@ class CustomValidator(BaseValidator):
self.boolean_validator = BooleanValidator()
self.file_validator = FileValidator()
def validate_inputs(self, inputs: dict[str, str]) -> bool: # pylint: disable=too-many-branches
def validate_inputs(self, inputs: dict[str, str]) -> bool:
"""Validate validate-inputs action inputs."""
valid = True
# Validate action/action-type input
if "action" in inputs or "action-type" in inputs:
action_input = inputs.get("action") or inputs.get("action-type", "")
# Check for empty action
action_key = self.get_key_variant(inputs, "action", "action-type")
if action_key:
action_input = inputs[action_key]
if action_input == "":
self.add_error("Action name cannot be empty")
valid = False
# Allow GitHub expressions
elif action_input.startswith("${{") and action_input.endswith("}}"):
pass # GitHub expressions are valid
# Check for dangerous characters
elif any(
char in action_input
for char in [";", "`", "$", "&", "|", ">", "<", "\n", "\r", "/"]
):
self.add_error(f"Invalid characters in action name: {action_input}")
valid = False
# Validate action name format (should be lowercase with hyphens or underscores)
elif action_input and not re.match(r"^[a-z][a-z0-9_-]*[a-z0-9]$", action_input):
self.add_error(f"Invalid action name format: {action_input}")
valid = False
elif not self.is_github_expression(action_input):
# Only validate non-GitHub expressions
if any(
char in action_input
for char in [";", "`", "$", "&", "|", ">", "<", "\n", "\r", "/"]
):
self.add_error(f"Invalid characters in action name: {action_input}")
valid = False
elif action_input and not re.match(r"^[a-z][a-z0-9_-]*[a-z0-9]$", action_input):
self.add_error(f"Invalid action name format: {action_input}")
valid = False
# Validate rules-file if provided
if inputs.get("rules-file"):
result = self.file_validator.validate_file_path(inputs["rules-file"], "rules-file")
for error in self.file_validator.errors:
if error not in self.errors:
self.add_error(error)
self.file_validator.clear_errors()
if not result:
valid = False
valid &= self.validate_with(
self.file_validator, "validate_file_path", inputs["rules-file"], "rules-file"
)
# Validate fail-on-error boolean
if "fail-on-error" in inputs:
value = inputs["fail-on-error"]
# Reject empty string
if value == "":
self.add_error("fail-on-error cannot be empty")
valid = False
elif value:
result = self.boolean_validator.validate_boolean(value, "fail-on-error")
for error in self.boolean_validator.errors:
if error not in self.errors:
self.add_error(error)
self.boolean_validator.clear_errors()
if not result:
valid = False
valid &= self.validate_with(
self.boolean_validator, "validate_boolean", value, "fail-on-error"
)
return valid

View File

@@ -8,56 +8,62 @@ Centralized Python-based input validation for GitHub Actions with PCRE regex sup
### Inputs
| name | description | required | default |
|---------------------|-------------------------------------------------------------------------------------|----------|---------|
| `action` | <p>Action name to validate (alias for action-type)</p> | `false` | `""` |
| `action-type` | <p>Type of action to validate (e.g., csharp-publish, docker-build, eslint-lint)</p> | `false` | `""` |
| `rules-file` | <p>Path to validation rules file</p> | `false` | `""` |
| `fail-on-error` | <p>Whether to fail on validation errors</p> | `false` | `true` |
| `token` | <p>GitHub token for authentication</p> | `false` | `""` |
| `namespace` | <p>Namespace/username for validation</p> | `false` | `""` |
| `email` | <p>Email address for validation</p> | `false` | `""` |
| `username` | <p>Username for validation</p> | `false` | `""` |
| `dotnet-version` | <p>.NET version string</p> | `false` | `""` |
| `terraform-version` | <p>Terraform version string</p> | `false` | `""` |
| `tflint-version` | <p>TFLint version string</p> | `false` | `""` |
| `node-version` | <p>Node.js version string</p> | `false` | `""` |
| `force-version` | <p>Force version override</p> | `false` | `""` |
| `default-version` | <p>Default version fallback</p> | `false` | `""` |
| `image-name` | <p>Docker image name</p> | `false` | `""` |
| `tag` | <p>Docker image tag</p> | `false` | `""` |
| `architectures` | <p>Target architectures</p> | `false` | `""` |
| `dockerfile` | <p>Dockerfile path</p> | `false` | `""` |
| `context` | <p>Docker build context</p> | `false` | `""` |
| `build-args` | <p>Docker build arguments</p> | `false` | `""` |
| `buildx-version` | <p>Docker Buildx version</p> | `false` | `""` |
| `max-retries` | <p>Maximum retry attempts</p> | `false` | `""` |
| `image-quality` | <p>Image quality percentage</p> | `false` | `""` |
| `png-quality` | <p>PNG quality percentage</p> | `false` | `""` |
| `parallel-builds` | <p>Number of parallel builds</p> | `false` | `""` |
| `days-before-stale` | <p>Number of days before marking as stale</p> | `false` | `""` |
| `days-before-close` | <p>Number of days before closing stale items</p> | `false` | `""` |
| `pre-commit-config` | <p>Pre-commit configuration file path</p> | `false` | `""` |
| `base-branch` | <p>Base branch name</p> | `false` | `""` |
| `dry-run` | <p>Dry run mode</p> | `false` | `""` |
| `is_fiximus` | <p>Use Fiximus bot</p> | `false` | `""` |
| `prefix` | <p>Release tag prefix</p> | `false` | `""` |
| `language` | <p>Language to analyze (for CodeQL)</p> | `false` | `""` |
| `queries` | <p>CodeQL queries to run</p> | `false` | `""` |
| `packs` | <p>CodeQL query packs</p> | `false` | `""` |
| `config-file` | <p>CodeQL configuration file path</p> | `false` | `""` |
| `config` | <p>CodeQL configuration YAML string</p> | `false` | `""` |
| `build-mode` | <p>Build mode for compiled languages</p> | `false` | `""` |
| `source-root` | <p>Source code root directory</p> | `false` | `""` |
| `category` | <p>Analysis category</p> | `false` | `""` |
| `checkout-ref` | <p>Git reference to checkout</p> | `false` | `""` |
| `working-directory` | <p>Working directory for analysis</p> | `false` | `""` |
| `upload-results` | <p>Upload results to GitHub Security</p> | `false` | `""` |
| `ram` | <p>Memory in MB for CodeQL</p> | `false` | `""` |
| `threads` | <p>Number of threads for CodeQL</p> | `false` | `""` |
| `output` | <p>Output path for SARIF results</p> | `false` | `""` |
| `skip-queries` | <p>Skip running queries</p> | `false` | `""` |
| `add-snippets` | <p>Add code snippets to SARIF</p> | `false` | `""` |
| name | description | required | default |
|----------------------|-------------------------------------------------------------------------------------|----------|---------|
| `action` | <p>Action name to validate (alias for action-type)</p> | `false` | `""` |
| `action-type` | <p>Type of action to validate (e.g., csharp-publish, docker-build, eslint-lint)</p> | `false` | `""` |
| `rules-file` | <p>Path to validation rules file</p> | `false` | `""` |
| `fail-on-error` | <p>Whether to fail on validation errors</p> | `false` | `true` |
| `token` | <p>GitHub token for authentication</p> | `false` | `""` |
| `namespace` | <p>Namespace/username for validation</p> | `false` | `""` |
| `email` | <p>Email address for validation</p> | `false` | `""` |
| `username` | <p>Username for validation</p> | `false` | `""` |
| `dotnet-version` | <p>.NET version string</p> | `false` | `""` |
| `terraform-version` | <p>Terraform version string</p> | `false` | `""` |
| `tflint-version` | <p>TFLint version string</p> | `false` | `""` |
| `node-version` | <p>Node.js version string</p> | `false` | `""` |
| `force-version` | <p>Force version override</p> | `false` | `""` |
| `default-version` | <p>Default version fallback</p> | `false` | `""` |
| `image-name` | <p>Docker image name</p> | `false` | `""` |
| `tag` | <p>Docker image tag</p> | `false` | `""` |
| `architectures` | <p>Target architectures</p> | `false` | `""` |
| `dockerfile` | <p>Dockerfile path</p> | `false` | `""` |
| `context` | <p>Docker build context</p> | `false` | `""` |
| `build-args` | <p>Docker build arguments</p> | `false` | `""` |
| `buildx-version` | <p>Docker Buildx version</p> | `false` | `""` |
| `max-retries` | <p>Maximum retry attempts</p> | `false` | `""` |
| `image-quality` | <p>Image quality percentage</p> | `false` | `""` |
| `png-quality` | <p>PNG quality percentage</p> | `false` | `""` |
| `parallel-builds` | <p>Number of parallel builds</p> | `false` | `""` |
| `days-before-stale` | <p>Number of days before marking as stale</p> | `false` | `""` |
| `days-before-close` | <p>Number of days before closing stale items</p> | `false` | `""` |
| `pre-commit-config` | <p>Pre-commit configuration file path</p> | `false` | `""` |
| `base-branch` | <p>Base branch name</p> | `false` | `""` |
| `dry-run` | <p>Dry run mode</p> | `false` | `""` |
| `is_fiximus` | <p>Use Fiximus bot</p> | `false` | `""` |
| `prefix` | <p>Release tag prefix</p> | `false` | `""` |
| `language` | <p>Language to analyze (for CodeQL)</p> | `false` | `""` |
| `queries` | <p>CodeQL queries to run</p> | `false` | `""` |
| `packs` | <p>CodeQL query packs</p> | `false` | `""` |
| `config-file` | <p>CodeQL configuration file path</p> | `false` | `""` |
| `config` | <p>CodeQL configuration YAML string</p> | `false` | `""` |
| `build-mode` | <p>Build mode for compiled languages</p> | `false` | `""` |
| `source-root` | <p>Source code root directory</p> | `false` | `""` |
| `category` | <p>Analysis category</p> | `false` | `""` |
| `checkout-ref` | <p>Git reference to checkout</p> | `false` | `""` |
| `working-directory` | <p>Working directory for analysis</p> | `false` | `""` |
| `upload-results` | <p>Upload results to GitHub Security</p> | `false` | `""` |
| `ram` | <p>Memory in MB for CodeQL</p> | `false` | `""` |
| `threads` | <p>Number of threads for CodeQL</p> | `false` | `""` |
| `output` | <p>Output path for SARIF results</p> | `false` | `""` |
| `skip-queries` | <p>Skip running queries</p> | `false` | `""` |
| `add-snippets` | <p>Add code snippets to SARIF</p> | `false` | `""` |
| `gitleaks-license` | <p>Gitleaks license key</p> | `false` | `""` |
| `gitleaks-config` | <p>Gitleaks configuration file path</p> | `false` | `""` |
| `trivy-severity` | <p>Trivy severity levels to scan</p> | `false` | `""` |
| `trivy-scanners` | <p>Trivy scanner types to run</p> | `false` | `""` |
| `trivy-timeout` | <p>Trivy scan timeout</p> | `false` | `""` |
| `actionlint-enabled` | <p>Enable actionlint scanning</p> | `false` | `""` |
### Outputs
@@ -365,4 +371,40 @@ This action is a `composite` action.
#
# Required: false
# Default: ""
gitleaks-license:
# Gitleaks license key
#
# Required: false
# Default: ""
gitleaks-config:
# Gitleaks configuration file path
#
# Required: false
# Default: ""
trivy-severity:
# Trivy severity levels to scan
#
# Required: false
# Default: ""
trivy-scanners:
# Trivy scanner types to run
#
# Required: false
# Default: ""
trivy-timeout:
# Trivy scan timeout
#
# Required: false
# Default: ""
actionlint-enabled:
# Enable actionlint scanning
#
# Required: false
# Default: ""
```

View File

@@ -173,6 +173,26 @@ inputs:
description: 'Add code snippets to SARIF'
required: false
# Security-scan specific inputs
gitleaks-license:
description: 'Gitleaks license key'
required: false
gitleaks-config:
description: 'Gitleaks configuration file path'
required: false
trivy-severity:
description: 'Trivy severity levels to scan'
required: false
trivy-scanners:
description: 'Trivy scanner types to run'
required: false
trivy-timeout:
description: 'Trivy scan timeout'
required: false
actionlint-enabled:
description: 'Enable actionlint scanning'
required: false
outputs:
validation-status:
description: 'Overall validation status (success/failure)'
@@ -193,6 +213,10 @@ outputs:
runs:
using: composite
steps:
- name: Install Python dependencies
shell: bash
run: pip install pyyaml==6.0.3
- name: Validate Action Inputs with Python
id: validate
shell: bash

View File

@@ -332,7 +332,7 @@ class CustomValidator(BaseValidator):
## Quality Metrics
- **Test Coverage**: 100% (769 tests)
- **Test Coverage**: 100%
- **Validators**: 9 specialized + unlimited custom
- **Performance**: < 10ms typical validation time
- **Zero Dependencies**: Uses only Python stdlib + PyYAML

View File

@@ -114,7 +114,7 @@ class ValidationRuleGenerator:
"prefix": re.compile(r"\b(prefix|tag[_-]?prefix)\b", re.IGNORECASE),
# Boolean patterns (broad, should be lower priority)
"boolean": re.compile(
r"\b(dry-?run|verbose|enable|disable|auto|skip|force|cache|provenance|sbom|scan|sign|fail[_-]?on[_-]?error|nightly)\b",
r"\b(dry-?run|verbose|enable|disable|auto|skip|force|cache|provenance|sbom|scan|sign|push|fail[_-]?on[_-]?error|nightly)\b",
re.IGNORECASE,
),
# File extensions pattern
@@ -160,36 +160,36 @@ class ValidationRuleGenerator:
"npm_token": "github_token",
"password": "github_token",
# Complex fields that should skip validation
"build-args": None, # Can be empty
"context": None, # Default handled
"cache-from": None, # Complex cache syntax
"cache-export": None, # Complex cache syntax
"cache-import": None, # Complex cache syntax
"build-contexts": None, # Complex syntax
"secrets": None, # Complex syntax
"platform-build-args": None, # JSON format
"extensions": None, # PHP extensions list
"tools": None, # PHP tools list
"build-args": "key_value_list", # Docker build arguments (KEY=VALUE format)
"context": "file_path", # Build context path
"cache-from": "cache_config", # Docker cache configuration
"cache-export": "cache_config", # Docker cache configuration
"cache-import": "cache_config", # Docker cache configuration
"build-contexts": "key_value_list", # Docker build contexts (KEY=VALUE format)
"secrets": "key_value_list", # Docker secrets (KEY=VALUE format)
"platform-build-args": "json_format", # JSON format for platform-specific args
"extensions": "php_extensions", # PHP extensions list
"tools": "linter_list", # PHP tools list - same pattern as linters
"framework": "framework_mode", # PHP framework mode (auto, laravel, generic)
"args": None, # Composer args
"stability": None, # Composer stability
"registry-url": "url", # URL format
"scope": "scope", # NPM scope
"plugins": None, # Prettier plugins
"plugins": "linter_list", # Prettier plugins - same pattern as linters
"file-extensions": "file_extensions", # File extension list
"file-pattern": None, # Glob pattern
"enable-linters": None, # Linter list
"disable-linters": None, # Linter list
"success-codes": None, # Exit code list
"retry-codes": None, # Exit code list
"ignore-paths": None, # Path patterns
"key-files": None, # Cache key files
"restore-keys": None, # Cache restore keys
"env-vars": None, # Environment variables
"file-pattern": "path_list", # Glob pattern for file paths
"enable-linters": "linter_list", # Linter list
"disable-linters": "linter_list", # Linter list
"success-codes": "exit_code_list", # Exit code list
"retry-codes": "exit_code_list", # Exit code list
"ignore-paths": "path_list", # Path patterns to ignore
"key-files": "path_list", # Cache key files (paths)
"restore-keys": "path_list", # Cache restore keys (paths)
"env-vars": "key_value_list", # Environment variables (KEY=VALUE format)
# Action-specific fields that need special handling
"type": None, # Cache type enum (npm, composer, go, etc.) - complex enum,
# skip validation
"paths": None, # File paths for caching (comma-separated) - complex format,
# skip validation
"paths": "path_list", # File paths for caching (comma-separated)
"command": None, # Shell command - complex format, skip validation for safety
"backoff-strategy": None, # Retry strategy enum - complex enum, skip validation
"shell": None, # Shell type enum - simple enum, skip validation
@@ -199,10 +199,13 @@ class ValidationRuleGenerator:
"retry-delay": "numeric_range_1_300", # Retry delay should support higher values
"max-warnings": "numeric_range_0_10000",
# version-file-parser specific fields
"language": None, # Simple enum (node, php, python, go, dotnet)
"tool-versions-key": None, # Simple string (nodejs, python, php, golang, dotnet)
"dockerfile-image": None, # Simple string (node, python, php, golang, dotnet)
"validation-regex": "regex_pattern", # Regex pattern - validate for ReDoS
# Docker network mode
"network": "network_mode", # Docker network mode (host, none, default)
# Language enum for version detection
"language": "language_enum", # Language type (php, python, go, dotnet)
}
def get_action_directories(self) -> list[str]:
@@ -307,14 +310,9 @@ class ValidationRuleGenerator:
"common-file-check": {
"file-pattern": "file_path",
},
"common-retry": {
"backoff-strategy": "backoff_strategy",
"shell": "shell_type",
},
"docker-publish": {
"registry": "registry_enum",
"cache-mode": "cache_mode",
"platforms": None, # Skip validation - complex platform format
},
"docker-publish-hub": {
"password": "docker_password",
@@ -354,26 +352,28 @@ class ValidationRuleGenerator:
"prettier-lint": {
"mode": "mode_enum",
},
"security-scan": {
"gitleaks-config": "file_path",
"trivy-severity": "severity_enum",
"trivy-scanners": "scanner_list",
"trivy-timeout": "timeout_with_unit",
"actionlint-enabled": "boolean",
"token": "github_token",
},
}
if action_name in action_overrides:
# Apply overrides for existing conventions
overrides.update(
{
input_name: override_value
for input_name, override_value in action_overrides[action_name].items()
if input_name in conventions
},
)
# Add missing inputs from overrides to conventions
for input_name, override_value in action_overrides[action_name].items():
if input_name not in conventions and input_name in action_data["inputs"]:
if input_name in action_data["inputs"]:
overrides[input_name] = override_value
# Update conventions to match override (or set to None if skipped)
conventions[input_name] = override_value
# Calculate statistics
total_inputs = len(action_data["inputs"])
validated_inputs = len(conventions)
skipped_inputs = sum(1 for v in overrides.values() if v is None)
validated_inputs = sum(1 for v in conventions.values() if v is not None)
skipped_inputs = sum(1 for v in conventions.values() if v is None)
coverage = round((validated_inputs / total_inputs) * 100) if total_inputs > 0 else 0
# Generate rules object with enhanced metadata
@@ -432,8 +432,20 @@ class ValidationRuleGenerator:
# Use a custom yaml dumper to ensure proper indentation
class CustomYamlDumper(yaml.SafeDumper):
def increase_indent(self, flow: bool = False, *, indentless: bool = False) -> None: # noqa: FBT001, FBT002
return super().increase_indent(flow, indentless=indentless)
def increase_indent(self, flow: bool = False, *, indentless: bool = False) -> None: # noqa: FBT001, FBT002, ARG002 # type: ignore[override]
return super().increase_indent(flow, False)
def choose_scalar_style(self):
"""Choose appropriate quote style based on string content."""
if hasattr(self, "event") and hasattr(self.event, "value") and self.event.value: # type: ignore[attr-defined]
value = self.event.value # type: ignore[attr-defined]
# Use literal block style for multiline strings
if "\n" in value:
return "|"
# Use double quotes for strings with single quotes
if "'" in value:
return '"'
return super().choose_scalar_style()
yaml_content = yaml.dump(
rules,

View File

@@ -1,74 +0,0 @@
"""Tests for common-retry custom validator.
Generated by generate-tests.py - Do not edit manually.
"""
# pylint: disable=invalid-name # Test file name matches action name
import sys
from pathlib import Path
# Add action directory to path to import custom validator
action_path = Path(__file__).parent.parent.parent / "common-retry"
sys.path.insert(0, str(action_path))
# pylint: disable=wrong-import-position
from CustomValidator import CustomValidator
class TestCustomCommonRetryValidator:
"""Test cases for common-retry custom validator."""
def setup_method(self):
"""Set up test fixtures."""
self.validator = CustomValidator("common-retry")
def teardown_method(self):
"""Clean up after tests."""
self.validator.clear_errors()
def test_validate_inputs_valid(self):
"""Test validation with valid inputs."""
# TODO: Add specific valid inputs for common-retry
inputs = {}
result = self.validator.validate_inputs(inputs)
# Adjust assertion based on required inputs
assert isinstance(result, bool)
def test_validate_inputs_invalid(self):
"""Test validation with invalid inputs."""
# TODO: Add specific invalid inputs for common-retry
inputs = {"invalid_key": "invalid_value"}
result = self.validator.validate_inputs(inputs)
# Custom validators may have specific validation rules
assert isinstance(result, bool)
def test_required_inputs(self):
"""Test required inputs detection."""
required = self.validator.get_required_inputs()
assert isinstance(required, list)
# TODO: Assert specific required inputs for common-retry
def test_validation_rules(self):
"""Test validation rules."""
rules = self.validator.get_validation_rules()
assert isinstance(rules, dict)
# TODO: Assert specific validation rules for common-retry
def test_github_expressions(self):
"""Test GitHub expression handling."""
inputs = {
"test_input": "${{ github.token }}",
}
result = self.validator.validate_inputs(inputs)
assert isinstance(result, bool)
# GitHub expressions should generally be accepted
def test_error_propagation(self):
"""Test error propagation from sub-validators."""
# Custom validators often use sub-validators
# Test that errors are properly propagated
inputs = {"test": "value"}
self.validator.validate_inputs(inputs)
# Check error handling
if self.validator.has_errors():
assert len(self.validator.errors) > 0

File diff suppressed because it is too large Load Diff

View File

@@ -274,6 +274,71 @@ class TestDockerValidator:
result = self.validator.validate_inputs(inputs)
assert isinstance(result, bool)
def test_validate_registry_valid(self):
"""Test registry enum validation with valid values."""
valid_registries = [
"dockerhub",
"github",
"both",
]
for registry in valid_registries:
self.validator.errors = []
result = self.validator.validate_registry(registry)
assert result is True, f"Should accept registry: {registry}"
def test_validate_registry_invalid(self):
"""Test registry enum validation with invalid values."""
invalid_registries = [
"", # Empty
" ", # Whitespace only
"docker", # Wrong value (should be dockerhub)
"hub", # Wrong value
"ghcr", # Wrong value
"gcr", # Wrong value
"both,github", # Comma-separated not allowed
"DOCKERHUB", # Uppercase
"DockerHub", # Mixed case
"docker hub", # Space
"github.com", # Full URL not allowed
]
for registry in invalid_registries:
self.validator.errors = []
result = self.validator.validate_registry(registry)
assert result is False, f"Should reject registry: {registry}"
def test_validate_sbom_format_valid(self):
"""Test SBOM format validation with valid values."""
valid_formats = [
"spdx-json",
"cyclonedx-json",
"", # Empty is optional
]
for sbom_format in valid_formats:
self.validator.errors = []
result = self.validator.validate_sbom_format(sbom_format)
assert result is True, f"Should accept SBOM format: {sbom_format}"
def test_validate_sbom_format_invalid(self):
"""Test SBOM format validation with invalid values."""
invalid_formats = [
"spdx", # Missing -json suffix
"cyclonedx", # Missing -json suffix
"json", # Just json
"spdx-xml", # Wrong format
"cyclonedx-xml", # Wrong format
"SPDX-JSON", # Uppercase
"spdx json", # Space
"invalid", # Invalid value
]
for sbom_format in invalid_formats:
self.validator.errors = []
result = self.validator.validate_sbom_format(sbom_format)
assert result is False, f"Should reject SBOM format: {sbom_format}"
def test_empty_values_handling(self):
"""Test that empty values are handled appropriately."""
# Some Docker fields might be required, others optional
@@ -281,3 +346,5 @@ class TestDockerValidator:
assert isinstance(self.validator.validate_docker_tag(""), bool)
assert isinstance(self.validator.validate_architectures(""), bool)
assert isinstance(self.validator.validate_prefix(""), bool)
# Registry should reject empty values
assert self.validator.validate_registry("") is False

View File

@@ -151,7 +151,7 @@ class TestValidationRuleGenerator:
generator = ValidationRuleGenerator()
# Test special cases from the mapping
assert generator.detect_validation_type("build-args", {}) is None
assert generator.detect_validation_type("build-args", {}) == "key_value_list"
assert generator.detect_validation_type("version", {}) == "flexible_version"
assert (
generator.detect_validation_type("dotnet-version", {}) == "dotnet_version"

Some files were not shown because too many files have changed in this diff Show More