Skip to content

Commit 5b0642b

Browse files
authored
Merge pull request #36 from malkreide/claude/claude-md-conventions-c0v3rp
ci: Actions auf Commit-SHAs pinnen
2 parents 3c3b808 + 058be3b commit 5b0642b

5 files changed

Lines changed: 120 additions & 11 deletions

File tree

.github/dependabot.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Haelt die SHA-Pins der Actions in Bewegung.
2+
#
3+
# Ein Commit-SHA ist nachpruefbar, aber er altert. Ohne diese Datei bleibt der
4+
# Pin stehen, bis jemand von Hand nachsieht — und die Sicherheitsluecke, wegen
5+
# der man gepinnt hat, bleibt gleich mit stehen. Dependabot kennt die Form
6+
# `uses: owner/repo@<sha> # v1.2.3` und hebt SHA und Kommentar zusammen an.
7+
#
8+
# Monatlich, nicht woechentlich: Vier PRs im Jahr fuer drei Actions liest man,
9+
# zwoelf ignoriert man.
10+
#
11+
# `pip` steht hier bewusst NICHT: Die ruff-Version ist ein Gate
12+
# (`scripts/check_ruff_pin.py`, `tests/test_werkzeug_versionen.py`), und ein
13+
# automatischer Bump wuerde sie gegen die CI verschieben.
14+
version: 2
15+
updates:
16+
- package-ecosystem: "github-actions"
17+
directory: "/"
18+
schedule:
19+
interval: "monthly"
20+
commit-message:
21+
prefix: "ci"
22+
labels:
23+
- "dependencies"

.github/workflows/ci.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ jobs:
1414
python-version: ["3.11", "3.12", "3.13"]
1515

1616
steps:
17-
- uses: actions/checkout@v7
17+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
1818

1919
- name: Python ${{ matrix.python-version }} einrichten
20-
uses: actions/setup-python@v7
20+
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
2121
with:
2222
python-version: ${{ matrix.python-version }}
2323

@@ -55,8 +55,8 @@ jobs:
5555
lint:
5656
runs-on: ubuntu-latest
5757
steps:
58-
- uses: actions/checkout@v7
59-
- uses: actions/setup-python@v7
58+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
59+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
6060
with:
6161
python-version: "3.11"
6262

.github/workflows/live-tests.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ jobs:
3434
timeout-minutes: 15
3535

3636
steps:
37-
- uses: actions/checkout@v7
37+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
3838

3939
- name: Set up Python
40-
uses: actions/setup-python@v7
40+
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
4141
with:
4242
python-version: "3.11"
4343

@@ -75,7 +75,7 @@ jobs:
7575
} >> "$GITHUB_OUTPUT"
7676
7777
- name: Issue oeffnen, aktualisieren oder schliessen
78-
uses: actions/github-script@v9
78+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
7979
# Ueber `env`, nicht ueber `${{ }}` im Skript: Die pytest-Ausgabe ist
8080
# fremder Text, der sonst in einem JavaScript-Template-Literal landet.
8181
# Ein Backtick oder ein `${` darin bricht das Skript oder fuehrt aus,

.github/workflows/publish.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ jobs:
1616

1717
steps:
1818
- name: Checkout code
19-
uses: actions/checkout@v7
19+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
2020

2121
- name: Set up Python
22-
uses: actions/setup-python@v7
22+
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
2323
with:
2424
python-version: "3.11"
2525

@@ -30,7 +30,7 @@ jobs:
3030
run: python -m build
3131

3232
- name: Publish to PyPI
33-
uses: pypa/gh-action-pypi-publish@release/v1
33+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
3434

3535
publish-mcp:
3636
name: Publish to MCP Registry
@@ -41,7 +41,7 @@ jobs:
4141
contents: read
4242

4343
steps:
44-
- uses: actions/checkout@v7
44+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
4545

4646
- name: Sync server.json version to the release tag
4747
run: |

tests/test_action_pins.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""Jede GitHub-Action haengt an einem Commit-SHA, nicht an einem Tag.
2+
3+
Ein Tag ist beweglich. `actions/checkout@v7` und `pypa/...@release/v1` zeigen
4+
auf das, worauf der Herausgeber sie zeigen laesst — auch nachtraeglich, auch
5+
auf anderen Code als gestern. Bei `publish.yml` haengt daran ein Job mit
6+
`id-token: write`, der auf PyPI veroeffentlichen darf.
7+
8+
Der Rueckfall ist leise: Ein Tag-Pin macht kein Gate rot, er nimmt der
9+
Lieferkette nur die Nachpruefbarkeit. Deshalb steht hier ein Test.
10+
11+
Der Kommentar hinter dem SHA ist nicht Zierde: Ohne ihn steht in der Datei
12+
eine 40-stellige Zahl, und niemand sieht mehr, welche Version das ist oder ob
13+
sie alt ist.
14+
"""
15+
16+
from __future__ import annotations
17+
18+
import pathlib
19+
import re
20+
21+
_ROOT = pathlib.Path(__file__).resolve().parents[1]
22+
_WORKFLOWS = _ROOT / ".github" / "workflows"
23+
24+
# `uses:` mit Wert, Kommentarzeilen sind vorher raus.
25+
_USES = re.compile(r"^\s*-?\s*uses:\s*(?P<ref>\S+)\s*(?P<rest>.*)$")
26+
_SHA = re.compile(r"^[0-9a-f]{40}$")
27+
# `# v1.2.3` oder `# v1` — irgendeine lesbare Versionsangabe.
28+
_VERSIONSKOMMENTAR = re.compile(r"#\s*v?\d[\w.\-+]*")
29+
30+
31+
def _workflow_dateien() -> list[pathlib.Path]:
32+
"""Beide Endungen: GitHub laedt `*.yml` UND `*.yaml`."""
33+
return sorted([*_WORKFLOWS.glob("*.yml"), *_WORKFLOWS.glob("*.yaml")])
34+
35+
36+
def _verwendungen() -> list[tuple[pathlib.Path, str, str]]:
37+
"""(Datei, Referenz, Rest der Zeile) je `uses:`-Zeile."""
38+
gefunden = []
39+
for wf in _workflow_dateien():
40+
for zeile in wf.read_text(encoding="utf-8").splitlines():
41+
if zeile.lstrip().startswith("#"):
42+
continue
43+
treffer = _USES.match(zeile)
44+
if treffer:
45+
gefunden.append((wf, treffer.group("ref"), treffer.group("rest")))
46+
return gefunden
47+
48+
49+
def test_der_scan_findet_ueberhaupt_actions() -> None:
50+
"""Sichert die Zusicherungen unten gegen eine leere Liste ab.
51+
52+
Faende der Scan nichts — umbenanntes Verzeichnis, geaenderte Schreibweise —,
53+
waeren die Schleifen leer und beide Tests trivialerweise wahr.
54+
"""
55+
verwendungen = _verwendungen()
56+
assert len(verwendungen) >= 5, f"verdaechtig wenige `uses:` gefunden: {verwendungen}"
57+
58+
59+
def test_jede_action_haengt_an_einem_commit_sha() -> None:
60+
"""Ein Tag kann umgehaengt werden, ein Commit-SHA nicht."""
61+
lose = []
62+
for wf, ref, _ in _verwendungen():
63+
if ref.startswith("./"):
64+
continue # lokale Action im Repo, kein Fremdcode
65+
_, _, version = ref.partition("@")
66+
if not _SHA.match(version):
67+
lose.append(f"{wf.name}: {ref}")
68+
assert not lose, (
69+
f"nicht auf einen Commit-SHA gepinnt: {lose}. Ein Tag oder Branch zeigt auf "
70+
"das, worauf der Herausgeber ihn zeigen laesst — auch nachtraeglich. "
71+
"SHA ermitteln mit: git ls-remote https://github.com/<owner>/<repo> "
72+
"'refs/tags/<tag>^{}'"
73+
)
74+
75+
76+
def test_jeder_pin_sagt_welche_version_er_ist() -> None:
77+
"""Sonst steht da eine 40-stellige Zahl und niemand sieht, wie alt sie ist."""
78+
ohne = []
79+
for wf, ref, rest in _verwendungen():
80+
if ref.startswith("./"):
81+
continue
82+
if not _VERSIONSKOMMENTAR.search(rest):
83+
ohne.append(f"{wf.name}: {ref}")
84+
assert not ohne, (
85+
f"Pin ohne Versionskommentar: {ohne}. Erwartet wird `uses: owner/repo@<sha> # v1.2.3`."
86+
)

0 commit comments

Comments
 (0)