Skip to content

Commit d103fc9

Browse files
committed
ci: make pyproject.toml the canonical release version source
Signed-off-by: lelia <2418071+lelia@users.noreply.github.com>
1 parent 7271046 commit d103fc9

6 files changed

Lines changed: 234 additions & 133 deletions

File tree

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@
2121

2222
<!-- Only fill this out if this PR is cutting a new release (e.g. v2.1.0). -->
2323

24+
- [ ] `pyproject.toml` `version:` field updated to new version
25+
- [ ] `python3 scripts/sync_release_version.py --write` run after updating `pyproject.toml`
2426
- [ ] `socket_basics/version.py` updated to new version
25-
- [ ] `pyproject.toml` `version:` field updated to match
26-
- [ ] `action.yml` `image:` ref updated to `docker://ghcr.io/socketdev/socket-basics:<new-version>` *(auto-updated by `publish-docker.yml`
27+
- [ ] `socket_basics/__init__.py` updated to the same version
28+
- [ ] `action.yml` `image:` ref updated to `docker://ghcr.io/socketdev/socket-basics:<new-version>`
2729
- [ ] `CHANGELOG.md` `[Unreleased]` section reviewed
28-
29-
> See [docs/releasing.md](../docs/releasing.md) for the full release process.
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
name: dependabot-review
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened, ready_for_review]
6+
7+
permissions:
8+
contents: read
9+
10+
concurrency:
11+
group: dependabot-review-${{ github.event.pull_request.number }}
12+
cancel-in-progress: true
13+
14+
jobs:
15+
inspect:
16+
if: github.actor == 'dependabot[bot]'
17+
runs-on: ubuntu-latest
18+
outputs:
19+
root_docker_changed: ${{ steps.diff.outputs.root_docker_changed }}
20+
app_tests_docker_changed: ${{ steps.diff.outputs.app_tests_docker_changed }}
21+
workflow_or_action_changed: ${{ steps.diff.outputs.workflow_or_action_changed }}
22+
steps:
23+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
24+
with:
25+
fetch-depth: 0
26+
persist-credentials: false
27+
28+
- name: Inspect changed files
29+
id: diff
30+
env:
31+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
32+
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
33+
run: |
34+
CHANGED_FILES="$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")"
35+
36+
echo "Changed files:" >> "$GITHUB_STEP_SUMMARY"
37+
echo '```' >> "$GITHUB_STEP_SUMMARY"
38+
printf '%s\n' "$CHANGED_FILES" >> "$GITHUB_STEP_SUMMARY"
39+
echo '```' >> "$GITHUB_STEP_SUMMARY"
40+
41+
has_file() {
42+
local pattern="$1"
43+
if printf '%s\n' "$CHANGED_FILES" | grep -Eq "$pattern"; then
44+
echo "true"
45+
else
46+
echo "false"
47+
fi
48+
}
49+
50+
echo "root_docker_changed=$(has_file '^Dockerfile$')" >> "$GITHUB_OUTPUT"
51+
echo "app_tests_docker_changed=$(has_file '^app_tests/Dockerfile$')" >> "$GITHUB_OUTPUT"
52+
echo "workflow_or_action_changed=$(has_file '^\\.github/workflows/|^action\\.yml$|^\\.github/dependabot\\.yml$')" >> "$GITHUB_OUTPUT"
53+
54+
- name: Summarize review expectations
55+
env:
56+
PR_URL: ${{ github.event.pull_request.html_url }}
57+
run: |
58+
{
59+
echo "## Dependabot Review Checklist"
60+
echo "- PR: $PR_URL"
61+
echo "- Confirm upstream release notes before merge"
62+
echo "- Confirm Docker/toolchain changes match the files in this PR"
63+
echo "- Do not treat a Dependabot PR as trusted solely because of the actor"
64+
echo "- This workflow runs in pull_request context only; no publish secrets are exposed"
65+
} >> "$GITHUB_STEP_SUMMARY"
66+
67+
docker-smoke-main:
68+
needs: inspect
69+
if: github.actor == 'dependabot[bot]' && needs.inspect.outputs.root_docker_changed == 'true'
70+
uses: ./.github/workflows/_docker-pipeline.yml
71+
permissions:
72+
contents: read
73+
with:
74+
name: socket-basics
75+
dockerfile: Dockerfile
76+
context: .
77+
check_set: main
78+
push: false
79+
80+
docker-smoke-app-tests:
81+
needs: inspect
82+
if: github.actor == 'dependabot[bot]' && needs.inspect.outputs.app_tests_docker_changed == 'true'
83+
uses: ./.github/workflows/_docker-pipeline.yml
84+
permissions:
85+
contents: read
86+
with:
87+
name: socket-basics-app-tests
88+
dockerfile: app_tests/Dockerfile
89+
context: .
90+
check_set: app-tests
91+
push: false
92+
93+
workflow-notice:
94+
needs: inspect
95+
if: github.actor == 'dependabot[bot]' && needs.inspect.outputs.workflow_or_action_changed == 'true'
96+
runs-on: ubuntu-latest
97+
steps:
98+
- name: Flag workflow-sensitive updates
99+
run: |
100+
{
101+
echo "## Sensitive File Notice"
102+
echo "This Dependabot PR changes workflow or action metadata files."
103+
echo "Require explicit human review before merge."
104+
} >> "$GITHUB_STEP_SUMMARY"

.github/workflows/publish-docker.yml

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -129,25 +129,13 @@ jobs:
129129
--version "$VERSION" \
130130
--date "$DATE"
131131
132-
- name: 🔀 Commit CHANGELOG + action.yml back to main
132+
- name: 🔀 Commit CHANGELOG back to main
133133
env:
134134
BOT_TOKEN: ${{ steps.bot.outputs.token }}
135-
REF_NAME: ${{ github.ref_name }}
136135
run: |
137136
git config user.name "socket-release-bot[bot]"
138137
git config user.email "socket-release-bot[bot]@users.noreply.github.com"
139138
git remote set-url origin "https://x-access-token:${BOT_TOKEN}@github.com/SocketDev/socket-basics.git"
140-
141-
# Auto-update action.yml image ref to the new version.
142-
# No-op if action.yml still uses `image: "Dockerfile"` (handles the
143-
# chicken-and-egg on the initial v2.0.0 release).
144-
if grep -q 'docker://ghcr.io/socketdev/socket-basics:' action.yml; then
145-
sed -i "s|docker://ghcr.io/socketdev/socket-basics:[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*|docker://ghcr.io/socketdev/socket-basics:${VERSION}|" action.yml
146-
echo "Updated action.yml image ref to ${VERSION}"
147-
else
148-
echo "action.yml not yet using pre-built image — skipping version update"
149-
fi
150-
151-
git add CHANGELOG.md action.yml
152-
git diff --cached --quiet || git commit -m "chore: release ${REF_NAME} — update CHANGELOG and action.yml [skip ci]"
139+
git add CHANGELOG.md
140+
git diff --cached --quiet || git commit -m "chore: release ${github.ref_name} — update CHANGELOG [skip ci]"
153141
git push origin HEAD:main

.github/workflows/python-tests.yml

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -45,34 +45,7 @@ jobs:
4545
run: |
4646
python -m pip install --upgrade pip
4747
pip install -e ".[dev]"
48-
- name: 🔒 Assert version files in sync
49-
run: |
50-
V_PY=$(python -c "from socket_basics.version import __version__; print(__version__)")
51-
V_TOML=$(python -c "import tomllib; print(tomllib.loads(open('pyproject.toml').read())['project']['version'])")
52-
[ "$V_PY" = "$V_TOML" ] || (echo "Version mismatch: version.py=$V_PY pyproject.toml=$V_TOML" && exit 1)
53-
echo "Version in sync: $V_PY"
54-
55-
- name: 🔒 Assert action.yml image ref matches version (once switched to pre-built)
56-
run: |
57-
python3 - <<'EOF'
58-
import re, sys, tomllib
59-
from pathlib import Path
60-
61-
action = Path("action.yml").read_text()
62-
version = tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"]
63-
64-
match = re.search(r'image:\s*["\']docker://[^:]+:([^"\']+)["\']', action)
65-
if not match:
66-
print(f"SKIP: action.yml still uses Dockerfile — check will activate once switched to pre-built image")
67-
sys.exit(0)
68-
69-
action_version = match.group(1)
70-
if action_version != version:
71-
print(f"FAIL: action.yml refs {action_version} but version is {version}")
72-
print(f" Update action.yml image ref to docker://ghcr.io/socketdev/socket-basics:{version}")
73-
sys.exit(1)
74-
75-
print(f"OK: action.yml image ref matches version {version}")
76-
EOF
48+
- name: 🔒 Assert release version metadata is in sync
49+
run: python3 scripts/sync_release_version.py --check
7750
- name: 🧪 Run tests
7851
run: pytest -q tests/

docs/releasing.md

Lines changed: 0 additions & 85 deletions
This file was deleted.

scripts/sync_release_version.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#!/usr/bin/env python3
2+
"""Sync release version metadata from pyproject.toml.
3+
4+
This script treats pyproject.toml as the canonical source of truth and keeps
5+
the duplicated version fields in sync.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import argparse
11+
import re
12+
import sys
13+
import tomllib
14+
from pathlib import Path
15+
16+
17+
REPO_ROOT = Path(__file__).resolve().parent.parent
18+
PYPROJECT_PATH = REPO_ROOT / "pyproject.toml"
19+
VERSION_PY_PATH = REPO_ROOT / "socket_basics" / "version.py"
20+
INIT_PY_PATH = REPO_ROOT / "socket_basics" / "__init__.py"
21+
ACTION_YML_PATH = REPO_ROOT / "action.yml"
22+
23+
24+
def read_canonical_version() -> str:
25+
data = tomllib.loads(PYPROJECT_PATH.read_text())
26+
return data["project"]["version"]
27+
28+
29+
def replace_first(pattern: str, replacement: str, content: str, path: Path) -> str:
30+
updated, count = re.subn(pattern, replacement, content, count=1, flags=re.MULTILINE)
31+
if count != 1:
32+
raise ValueError(f"Could not update expected version field in {path}")
33+
return updated
34+
35+
36+
def build_expected_files(version: str) -> dict[Path, str]:
37+
expected: dict[Path, str] = {}
38+
39+
version_py = VERSION_PY_PATH.read_text()
40+
expected[VERSION_PY_PATH] = replace_first(
41+
r'^__version__ = "[^"]+"$',
42+
f'__version__ = "{version}"',
43+
version_py,
44+
VERSION_PY_PATH,
45+
)
46+
47+
init_py = INIT_PY_PATH.read_text()
48+
expected[INIT_PY_PATH] = replace_first(
49+
r'^__version__ = "[^"]+"$',
50+
f'__version__ = "{version}"',
51+
init_py,
52+
INIT_PY_PATH,
53+
)
54+
55+
action_yml = ACTION_YML_PATH.read_text()
56+
expected[ACTION_YML_PATH] = replace_first(
57+
r'^( image: "docker://ghcr\.io/socketdev/socket-basics:)[^"]+(")$',
58+
rf'\g<1>{version}\2',
59+
action_yml,
60+
ACTION_YML_PATH,
61+
)
62+
63+
return expected
64+
65+
66+
def check_files(expected: dict[Path, str]) -> list[str]:
67+
mismatches: list[str] = []
68+
for path, rendered in expected.items():
69+
current = path.read_text()
70+
if current != rendered:
71+
mismatches.append(str(path.relative_to(REPO_ROOT)))
72+
return mismatches
73+
74+
75+
def write_files(expected: dict[Path, str]) -> None:
76+
for path, rendered in expected.items():
77+
path.write_text(rendered)
78+
79+
80+
def parse_args() -> argparse.Namespace:
81+
parser = argparse.ArgumentParser(
82+
description="Sync socket-basics version metadata from pyproject.toml"
83+
)
84+
group = parser.add_mutually_exclusive_group(required=True)
85+
group.add_argument(
86+
"--check",
87+
action="store_true",
88+
help="Fail if any derived version files differ from pyproject.toml",
89+
)
90+
group.add_argument(
91+
"--write",
92+
action="store_true",
93+
help="Rewrite derived version files to match pyproject.toml",
94+
)
95+
return parser.parse_args()
96+
97+
98+
def main() -> int:
99+
args = parse_args()
100+
version = read_canonical_version()
101+
expected = build_expected_files(version)
102+
103+
if args.write:
104+
write_files(expected)
105+
print(f"Synchronized release version metadata to {version}")
106+
return 0
107+
108+
mismatches = check_files(expected)
109+
if mismatches:
110+
print(f"Release version metadata is out of sync with pyproject.toml ({version}):")
111+
for mismatch in mismatches:
112+
print(f" - {mismatch}")
113+
print("Run: python3 scripts/sync_release_version.py --write")
114+
return 1
115+
116+
print(f"Release version metadata is in sync: {version}")
117+
return 0
118+
119+
120+
if __name__ == "__main__":
121+
sys.exit(main())

0 commit comments

Comments
 (0)