From 5f37ee01b74d7bf0e6dab5969a74594251edae11 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 04:48:34 +0000 Subject: [PATCH 1/7] Update cryptography requirement from >=47.0.0 to >=50.0.1 Updates the requirements on [cryptography](https://github.com/pyca/cryptography) to permit the latest version. - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/47.0.0...50.0.1) --- updated-dependencies: - dependency-name: cryptography dependency-version: 50.0.1 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- dev_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index 1469cc6..aa1b519 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -3,7 +3,7 @@ google-api-python-client google-auth-httplib2 google-auth-oauthlib APScheduler -cryptography>=47.0.0 +cryptography>=50.0.1 prometheus_client>=0.25.0 defusedxml>=0.7.1 twine From 2f5083a59d47cca671e87b4563b9a805924e9719 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Wed, 23 Sep 2026 12:48:40 +0800 Subject: [PATCH 2/7] Record the 0.0.48 release --- docs/updates/2026-09.md | 9 +++++++++ docs/updates/README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/updates/2026-09.md b/docs/updates/2026-09.md index 64c6826..4e877c1 100644 --- a/docs/updates/2026-09.md +++ b/docs/updates/2026-09.md @@ -121,3 +121,12 @@ Index and query commands: [README.md](README.md). New entries go at the end. - **Checked**: the local venv was upgraded to those versions, `uv pip check` is clean, and the suite gives 759 passed, 8 skipped. - **PRs**: #91–#95 are closed with a comment pointing at the `dev` commit. - **Open items**: none. + +## U-20260923-10 · 2026-09-23 · Release 0.0.48 after Codacy's six findings · #done #release #ci + +- **What**: Codacy failed release PR #96 on six new issues. + - Four lines were over 100 columns: two docstrings in `automation_file/logging_config.py`, one in `automation_file/remote/box/client.py` and the module docstring of `tests/test_log_location.py`. They were shortened, and the file handler's docstring now has a summary line. + - Two Semgrep subprocess findings in `tests/test_log_location.py` and `tests/test_legacy_cli_contract.py` were on calls that run the fixed interpreter with test-controlled arguments. Both are marked `nosemgrep`. +- **Checked**: `ruff check` and `ruff format --check` pass. The log, CLI-contract and Box tests pass (41 in total). +- **Release**: PR #96 then passed all 14 checks and was merged into `main` (`9f8cbac`). The publish run passed and automation_file 0.0.48 is on PyPI. The release contains today's box_sdk_gen move, the log location change, the dependency floors and the CI fixes. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 5fd76ff..6fcab59 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260923-10 | 2026-09-23 | Release 0.0.48 after Codacy's six findings | #done #release #ci | [2026-09](2026-09.md) | | U-20260923-09 | 2026-09-23 | Five floors from the Dependabot PRs on main; the PRs closed | #done #deps | [2026-09](2026-09.md) | | U-20260923-08 | 2026-09-23 | dev CI green again: format check and mypy | #done #ci | [2026-09](2026-09.md) | | U-20260923-07 | 2026-09-23 | Stale release/bump-v0.0.32 branch deleted | #done #housekeeping | [2026-09](2026-09.md) | @@ -77,4 +78,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| -| [2026-09.md](2026-09.md) | 2026-09 | 14 | +| [2026-09.md](2026-09.md) | 2026-09 | 15 | From 3fe8e87a33dd59e17b7d4fb1ac4eb844eab80735 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 04:48:43 +0000 Subject: [PATCH 3/7] Update sphinx requirement from >=7.4.7 to >=8.1.3 Updates the requirements on [sphinx](https://github.com/sphinx-doc/sphinx) to permit the latest version. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/v8.1.3/CHANGES.rst) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v7.4.7...v8.1.3) --- updated-dependencies: - dependency-name: sphinx dependency-version: 8.1.3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index c3d2bd2..1dd7fa8 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ -sphinx>=7.4.7 +sphinx>=8.1.3 sphinx-rtd-theme myst-parser sphinxcontrib-mermaid From 5f43aa10a11e5f77c4b0fafedbf54d93fa6645a9 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Wed, 23 Sep 2026 18:21:34 +0800 Subject: [PATCH 4/7] Record the cryptography and sphinx floor merges --- docs/updates/2026-09.md | 7 +++++++ docs/updates/README.md | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/updates/2026-09.md b/docs/updates/2026-09.md index 4e877c1..1304e41 100644 --- a/docs/updates/2026-09.md +++ b/docs/updates/2026-09.md @@ -130,3 +130,10 @@ Index and query commands: [README.md](README.md). New entries go at the end. - **Checked**: `ruff check` and `ruff format --check` pass. The log, CLI-contract and Box tests pass (41 in total). - **Release**: PR #96 then passed all 14 checks and was merged into `main` (`9f8cbac`). The publish run passed and automation_file 0.0.48 is on PyPI. The release contains today's box_sdk_gen move, the log location change, the dependency floors and the CI fixes. - **Open items**: none. + +## U-20260923-11 · 2026-09-23 · cryptography and sphinx floors from Dependabot · #done #deps + +- **What**: merged two Dependabot PRs into `dev` after all 8 checks passed on each. + - #97: `dev_requirements.txt` raises cryptography from `>=47.0.0` to `>=50.0.1`. `stable.toml` and `dev.toml` already require `>=50.0.0`, so the dev file no longer allows an older release than the package does. + - #98: `docs/requirements.txt` raises sphinx from `>=7.4.7` to `>=8.1.3`. Sphinx 8.1 needs Python 3.10, which is the project's floor. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 6fcab59..5488406 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260923-11 | 2026-09-23 | cryptography and sphinx floors from Dependabot | #done #deps | [2026-09](2026-09.md) | | U-20260923-10 | 2026-09-23 | Release 0.0.48 after Codacy's six findings | #done #release #ci | [2026-09](2026-09.md) | | U-20260923-09 | 2026-09-23 | Five floors from the Dependabot PRs on main; the PRs closed | #done #deps | [2026-09](2026-09.md) | | U-20260923-08 | 2026-09-23 | dev CI green again: format check and mypy | #done #ci | [2026-09](2026-09.md) | @@ -78,4 +79,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| -| [2026-09.md](2026-09.md) | 2026-09 | 15 | +| [2026-09.md](2026-09.md) | 2026-09 | 16 | From acaea4d3486e8129f2f1cb51e9d0a8c6070e4ba1 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 11:56:46 +0800 Subject: [PATCH 5/7] Move CI to Node 24 actions pinned by commit; Dependabot tracks actions on dev --- .github/dependabot.yml | 7 ++++ .github/workflows/ci-dev.yml | 10 ++--- .github/workflows/ci-stable.yml | 10 ++--- .github/workflows/publish.yml | 4 +- docs/updates/2026-09.md | 8 ++++ docs/updates/README.md | 3 +- tests/test_workflow_actions.py | 65 +++++++++++++++++++++++++++++++++ 7 files changed, 94 insertions(+), 13 deletions(-) create mode 100644 tests/test_workflow_actions.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 309370b..8058307 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,3 +12,10 @@ updates: target-branch: "dev" schedule: interval: "daily" + # Workflow actions are pinned to commit SHAs with the version as a comment + # (test_workflow_actions.py); Dependabot moves both together. + - package-ecosystem: "github-actions" + directory: "/" + target-branch: "dev" + schedule: + interval: "weekly" diff --git a/.github/workflows/ci-dev.yml b/.github/workflows/ci-dev.yml index 2ec3e14..79d78da 100644 --- a/.github/workflows/ci-dev.yml +++ b/.github/workflows/ci-dev.yml @@ -15,8 +15,8 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" cache: pip @@ -39,9 +39,9 @@ jobs: matrix: python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ matrix.python-version }} cache: pip @@ -55,7 +55,7 @@ jobs: run: python -m pytest tests/ -v --tb=short --cov=automation_file --cov-report=term-missing --cov-report=xml - name: Upload coverage artifact if: matrix.python-version == '3.12' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: coverage-xml path: coverage.xml diff --git a/.github/workflows/ci-stable.yml b/.github/workflows/ci-stable.yml index 5c8543d..30ff378 100644 --- a/.github/workflows/ci-stable.yml +++ b/.github/workflows/ci-stable.yml @@ -15,8 +15,8 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" cache: pip @@ -39,9 +39,9 @@ jobs: matrix: python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ matrix.python-version }} cache: pip @@ -55,7 +55,7 @@ jobs: run: python -m pytest tests/ -v --tb=short --cov=automation_file --cov-report=term-missing --cov-report=xml - name: Upload coverage artifact if: matrix.python-version == '3.12' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: coverage-xml path: coverage.xml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fa59aa4..fb88584 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -16,12 +16,12 @@ jobs: runs-on: ubuntu-latest if: "!contains(github.event.head_commit.message, 'chore: bump version')" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" diff --git a/docs/updates/2026-09.md b/docs/updates/2026-09.md index 1304e41..a6f8d27 100644 --- a/docs/updates/2026-09.md +++ b/docs/updates/2026-09.md @@ -137,3 +137,11 @@ Index and query commands: [README.md](README.md). New entries go at the end. - #97: `dev_requirements.txt` raises cryptography from `>=47.0.0` to `>=50.0.1`. `stable.toml` and `dev.toml` already require `>=50.0.0`, so the dev file no longer allows an older release than the package does. - #98: `docs/requirements.txt` raises sphinx from `>=7.4.7` to `>=8.1.3`. Sphinx 8.1 needs Python 3.10, which is the project's floor. - **Open items**: none. + +## U-20260924-01 · 2026-09-24 · Move CI to Node 24 actions pinned by commit · #ci #security #deps + +- **What**: the workflows still used Node 20 actions, and GitHub removed Node 20 from its runners on 2026-09-23. They were also referenced by mutable tags, which the 2025 tj-actions/changed-files compromise showed can be repointed. All 12 references in `ci-dev.yml`, `ci-stable.yml`, `publish.yml` now name the latest release, whose `action.yml` declares `node24` (or is composite), pinned to its commit SHA with the version as a comment: checkout v7.0.1, setup-python v7.0.0, upload-artifact v7.0.1. No breaking change in the release notes applies: checkout v6 keeps the credentials in a separate included file that later git commands still read, and v7 blocks fork checkouts under `pull_request_target` / `workflow_run`, which are not used; setup-python v7 removed the unused `pip-install` input. `.github/dependabot.yml` gains a weekly `github-actions` entry on `dev`, so Dependabot moves each pin and its comment together (workspace `progress.md` X-21). +- **Tests**: `tests/test_workflow_actions.py` requires every remote `uses:` to name a 40-hex commit followed by a `# vX.Y.Z` comment, each action at a single commit across workflows, and `dependabot.yml` to cover pip and GitHub Actions on `dev`. The previous workflows fail it. +- **Result / numbers**: the new tests pass. The workflows take effect on the next push. +- **Sources**: https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/ and each action's release notes (`gh api repos///releases`). +- **Files**: `.github/workflows/ci-dev.yml`, `.github/workflows/ci-stable.yml`, `.github/workflows/publish.yml`, `.github/dependabot.yml`, `tests/test_workflow_actions.py` (new). diff --git a/docs/updates/README.md b/docs/updates/README.md index 5488406..428a57e 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-01 | 2026-09-24 | Move CI to Node 24 actions pinned by commit | #ci #security #deps | [2026-09](2026-09.md) | | U-20260923-11 | 2026-09-23 | cryptography and sphinx floors from Dependabot | #done #deps | [2026-09](2026-09.md) | | U-20260923-10 | 2026-09-23 | Release 0.0.48 after Codacy's six findings | #done #release #ci | [2026-09](2026-09.md) | | U-20260923-09 | 2026-09-23 | Five floors from the Dependabot PRs on main; the PRs closed | #done #deps | [2026-09](2026-09.md) | @@ -79,4 +80,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| -| [2026-09.md](2026-09.md) | 2026-09 | 16 | +| [2026-09.md](2026-09.md) | 2026-09 | 17 | diff --git a/tests/test_workflow_actions.py b/tests/test_workflow_actions.py new file mode 100644 index 0000000..e5e8a8b --- /dev/null +++ b/tests/test_workflow_actions.py @@ -0,0 +1,65 @@ +"""Every GitHub Actions step pins its action to a commit SHA. + +A tag such as ``@v4`` can be moved to new code at any time (the 2025 +tj-actions/changed-files compromise rewrote tags), so each ``uses:`` names a +full 40-hex commit and carries the release it corresponds to as a comment, +which is what Dependabot reads and updates. Pinning also keeps Node 20 actions +from lingering unnoticed: GitHub removed Node 20 from its runners on 2026-09-23. +""" +from __future__ import annotations + +import re +from pathlib import Path + +import pytest + +_ROOT = next(p for p in Path(__file__).resolve().parents if (p / ".github" / "workflows").is_dir()) +_WORKFLOWS = sorted((_ROOT / ".github" / "workflows").glob("*.yml")) +_USES = re.compile(r"^\s*(?:-\s*)?uses:\s*(\S+)(.*)$") +_PINNED = re.compile(r"^[\w.-]+/[\w./-]+@[0-9a-f]{40}$") +_LOCAL = re.compile(r"^\./") +_VERSION_COMMENT = re.compile(r"^\s+#\s*v\d+(\.\d+)*\s*$") + + +def _uses(path: Path) -> list[tuple[int, str, str]]: + """Return ``(line number, action reference, rest of line)`` for each remote ``uses:``.""" + found = [] + for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + match = _USES.match(line) + if match and not _LOCAL.match(match.group(1)): + found.append((number, match.group(1), match.group(2))) + return found + + +def test_workflows_exist(): + assert _WORKFLOWS + + +@pytest.mark.parametrize("workflow", _WORKFLOWS, ids=lambda p: p.name) +def test_every_action_is_pinned_to_a_commit_with_its_version(workflow): + bad = [f"{workflow.name}:{number} {ref}{rest}" + for number, ref, rest in _uses(workflow) + if not (_PINNED.match(ref) and _VERSION_COMMENT.match(rest))] + assert bad == [] + + +def test_one_version_per_action(): + # The same action at two different commits means a partial upgrade. + seen: dict[str, set[str]] = {} + for workflow in _WORKFLOWS: + for _number, ref, _rest in _uses(workflow): + action, _, sha = ref.partition("@") + seen.setdefault(action, set()).add(sha) + assert {action: shas for action, shas in seen.items() if len(shas) > 1} == {} + + +def test_dependabot_keeps_pins_current_on_dev(): + # Pinned SHAs only stay current if something bumps them; every update + # goes to dev because main is the release branch. Parsed as text: PyYAML + # is not a test dependency. + text = (_ROOT / ".github" / "dependabot.yml").read_text(encoding="utf-8") + blocks = re.split(r"^\s*-\s*package-ecosystem:", text, flags=re.MULTILINE)[1:] + ecosystems = {block.split()[0].strip("\"'") for block in blocks} + assert {"pip", "github-actions"} <= ecosystems + assert all(re.search(r"^\s*target-branch:\s*\"dev\"", block, re.MULTILINE) + for block in blocks) From cb3e0bd301e02693eee9adbca8ed5aa7e4d5fbcd Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 12:07:09 +0800 Subject: [PATCH 6/7] Keep checkout credentials only in the job that pushes --- .github/workflows/ci-dev.yml | 4 ++++ .github/workflows/ci-stable.yml | 4 ++++ .github/workflows/publish.yml | 1 + docs/updates/2026-09.md | 7 +++++++ docs/updates/README.md | 3 ++- tests/test_workflow_actions.py | 28 ++++++++++++++++++++++++++++ 6 files changed, 46 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-dev.yml b/.github/workflows/ci-dev.yml index 79d78da..e1a005d 100644 --- a/.github/workflows/ci-dev.yml +++ b/.github/workflows/ci-dev.yml @@ -16,6 +16,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" @@ -40,6 +42,8 @@ jobs: python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: diff --git a/.github/workflows/ci-stable.yml b/.github/workflows/ci-stable.yml index 30ff378..56840d4 100644 --- a/.github/workflows/ci-stable.yml +++ b/.github/workflows/ci-stable.yml @@ -16,6 +16,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" @@ -40,6 +42,8 @@ jobs: python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fb88584..d4e47f7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,6 +18,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + persist-credentials: true # this job pushes fetch-depth: 0 - name: Set up Python diff --git a/docs/updates/2026-09.md b/docs/updates/2026-09.md index a6f8d27..68e06c7 100644 --- a/docs/updates/2026-09.md +++ b/docs/updates/2026-09.md @@ -145,3 +145,10 @@ Index and query commands: [README.md](README.md). New entries go at the end. - **Result / numbers**: the new tests pass. The workflows take effect on the next push. - **Sources**: https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/ and each action's release notes (`gh api repos///releases`). - **Files**: `.github/workflows/ci-dev.yml`, `.github/workflows/ci-stable.yml`, `.github/workflows/publish.yml`, `.github/dependabot.yml`, `tests/test_workflow_actions.py` (new). + +## U-20260924-02 · 2026-09-24 · Keep checkout credentials only in the job that pushes · #ci #security + +- **What**: zizmor 1.30.1 (`zizmor --offline .github/workflows`) reported that `actions/checkout` leaves the job token in `.git/config` (artipacked), where every later step can read it. The 4 checkouts in jobs that never push now set `persist-credentials: false`. The release job that pushes the version bump and tag (1 checkout) now says `persist-credentials: true # this job pushes`, so the exception is visible. +- **Tests**: `tests/test_workflow_actions.py` gains a check that every checkout step sets `persist-credentials` explicitly. The previous workflows fail it. +- **Result / numbers**: zizmor reports no warnings or errors for these workflows any more; the test file passes. +- **Files**: `.github/workflows/ci-dev.yml`, `.github/workflows/ci-stable.yml`, `.github/workflows/publish.yml`, `tests/test_workflow_actions.py`. diff --git a/docs/updates/README.md b/docs/updates/README.md index 428a57e..1c49f1f 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-02 | 2026-09-24 | Keep checkout credentials only in the job that pushes | #ci #security | [2026-09](2026-09.md) | | U-20260924-01 | 2026-09-24 | Move CI to Node 24 actions pinned by commit | #ci #security #deps | [2026-09](2026-09.md) | | U-20260923-11 | 2026-09-23 | cryptography and sphinx floors from Dependabot | #done #deps | [2026-09](2026-09.md) | | U-20260923-10 | 2026-09-23 | Release 0.0.48 after Codacy's six findings | #done #release #ci | [2026-09](2026-09.md) | @@ -80,4 +81,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| -| [2026-09.md](2026-09.md) | 2026-09 | 17 | +| [2026-09.md](2026-09.md) | 2026-09 | 18 | diff --git a/tests/test_workflow_actions.py b/tests/test_workflow_actions.py index e5e8a8b..46b9b4b 100644 --- a/tests/test_workflow_actions.py +++ b/tests/test_workflow_actions.py @@ -63,3 +63,31 @@ def test_dependabot_keeps_pins_current_on_dev(): assert {"pip", "github-actions"} <= ecosystems assert all(re.search(r"^\s*target-branch:\s*\"dev\"", block, re.MULTILINE) for block in blocks) + + +def _checkout_steps(path: Path) -> list[tuple[int, str]]: + """Return ``(line number, step text)`` for each ``actions/checkout`` step.""" + lines = path.read_text(encoding="utf-8").splitlines() + steps = [] + for index, line in enumerate(lines): + if not re.search(r"uses:\s*actions/checkout@", line): + continue + column = line.index("uses:") + body = [line] + for following in lines[index + 1:]: + indent = len(following) - len(following.lstrip()) + if following.strip() and (indent < column or following.lstrip().startswith("- ")): + break + body.append(following) + steps.append((index + 1, "\n".join(body))) + return steps + + +@pytest.mark.parametrize("workflow", _WORKFLOWS, ids=lambda p: p.name) +def test_every_checkout_decides_on_persisted_credentials(workflow): + # actions/checkout leaves the job token in .git/config unless told not + # to, where every later step (and any uploaded workspace) can read it. + # Only jobs that push keep it, and they say so. + bad = [f"{workflow.name}:{number}" for number, step in _checkout_steps(workflow) + if not re.search(r"^\s*persist-credentials:\s*(true|false)\b", step, re.MULTILINE)] + assert bad == [] From 00f9965d5c9ec84dfffa678d4da08fee545615d4 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 13:12:15 +0800 Subject: [PATCH 7/7] style: apply ruff format to workflow actions test --- tests/test_workflow_actions.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/tests/test_workflow_actions.py b/tests/test_workflow_actions.py index 46b9b4b..d8fdba8 100644 --- a/tests/test_workflow_actions.py +++ b/tests/test_workflow_actions.py @@ -6,6 +6,7 @@ which is what Dependabot reads and updates. Pinning also keeps Node 20 actions from lingering unnoticed: GitHub removed Node 20 from its runners on 2026-09-23. """ + from __future__ import annotations import re @@ -37,9 +38,11 @@ def test_workflows_exist(): @pytest.mark.parametrize("workflow", _WORKFLOWS, ids=lambda p: p.name) def test_every_action_is_pinned_to_a_commit_with_its_version(workflow): - bad = [f"{workflow.name}:{number} {ref}{rest}" - for number, ref, rest in _uses(workflow) - if not (_PINNED.match(ref) and _VERSION_COMMENT.match(rest))] + bad = [ + f"{workflow.name}:{number} {ref}{rest}" + for number, ref, rest in _uses(workflow) + if not (_PINNED.match(ref) and _VERSION_COMMENT.match(rest)) + ] assert bad == [] @@ -61,8 +64,7 @@ def test_dependabot_keeps_pins_current_on_dev(): blocks = re.split(r"^\s*-\s*package-ecosystem:", text, flags=re.MULTILINE)[1:] ecosystems = {block.split()[0].strip("\"'") for block in blocks} assert {"pip", "github-actions"} <= ecosystems - assert all(re.search(r"^\s*target-branch:\s*\"dev\"", block, re.MULTILINE) - for block in blocks) + assert all(re.search(r"^\s*target-branch:\s*\"dev\"", block, re.MULTILINE) for block in blocks) def _checkout_steps(path: Path) -> list[tuple[int, str]]: @@ -74,7 +76,7 @@ def _checkout_steps(path: Path) -> list[tuple[int, str]]: continue column = line.index("uses:") body = [line] - for following in lines[index + 1:]: + for following in lines[index + 1 :]: indent = len(following) - len(following.lstrip()) if following.strip() and (indent < column or following.lstrip().startswith("- ")): break @@ -88,6 +90,9 @@ def test_every_checkout_decides_on_persisted_credentials(workflow): # actions/checkout leaves the job token in .git/config unless told not # to, where every later step (and any uploaded workspace) can read it. # Only jobs that push keep it, and they say so. - bad = [f"{workflow.name}:{number}" for number, step in _checkout_steps(workflow) - if not re.search(r"^\s*persist-credentials:\s*(true|false)\b", step, re.MULTILINE)] + bad = [ + f"{workflow.name}:{number}" + for number, step in _checkout_steps(workflow) + if not re.search(r"^\s*persist-credentials:\s*(true|false)\b", step, re.MULTILINE) + ] assert bad == []