Skip to content
Merged
7 changes: 7 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
14 changes: 9 additions & 5 deletions .github/workflows/ci-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
cache: pip
Expand All @@ -39,9 +41,11 @@ 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
with:
persist-credentials: false
- 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
Expand All @@ -55,7 +59,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
14 changes: 9 additions & 5 deletions .github/workflows/ci-stable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
cache: pip
Expand All @@ -39,9 +41,11 @@ 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
with:
persist-credentials: false
- 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
Expand All @@ -55,7 +59,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
5 changes: 3 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ 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:
persist-credentials: true # this job pushes
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"

Expand Down
2 changes: 1 addition & 1 deletion dev_requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
sphinx>=7.4.7
sphinx>=8.1.3
sphinx-rtd-theme
myst-parser
sphinxcontrib-mermaid
31 changes: 31 additions & 0 deletions docs/updates/2026-09.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,34 @@ 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.

## 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.

## 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/<owner>/<name>/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`.
6 changes: 5 additions & 1 deletion docs/updates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ 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) |
| 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) |
Expand All @@ -77,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 | 14 |
| [2026-09.md](2026-09.md) | 2026-09 | 18 |
98 changes: 98 additions & 0 deletions tests/test_workflow_actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""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)


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 == []
Loading