Skip to content

Commit 7950a38

Browse files
mtnbikencclaude
andcommitted
[hack] Address PR review feedback on create-release-tag.py
- Validate --commit flag against _HEX_RE at parse time to prevent git option injection (e.g. --upload-pack=evil-cmd or HEAD~1 references) - Extract resolve_commit_sha() and resolve_date() helpers from main() to reduce branch count and remove too-many-branches pylint suppress Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0f16e25 commit 7950a38

File tree

1 file changed

+140
-87
lines changed

1 file changed

+140
-87
lines changed

hack/create-release-tag.py

Lines changed: 140 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
The commit SHA is resolved from the bundle image in the Red Hat Container
66
Catalog, preferring the org.opencontainers.image.revision label (full SHA)
77
and falling back to the short-SHA image tag. The tag date is set to the
8-
operator image's push_date so the timestamp reflects the actual release date.
8+
bundle image's push_date so the timestamp reflects the actual release date.
99
1010
Usage:
1111
python3 hack/create-release-tag.py <version>
@@ -17,26 +17,41 @@
1717
--commit is required when the version has no entry in the bundle catalog
1818
(e.g. backport releases that were never shipped as a container image).
1919
20-
Note:
21-
Pushing tags to the upstream openshift/windows-machine-config-operator
22-
repository requires write access granted by the repository administrators.
23-
Contact the WMCO team to have your GitHub account added before pushing.
20+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
21+
PREREQUISITES
22+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
23+
24+
Runtime
25+
Python 3.9 or later
26+
git in PATH
27+
28+
Python packages
29+
requests (pip install requests)
30+
31+
Environment
32+
Must be run from within a clone of the WMCO git repository so that
33+
commit SHAs from the catalog can be verified against local history.
34+
Network access to catalog.redhat.com is required; the Red Hat Container
35+
Catalog API is publicly accessible and does not require authentication.
36+
37+
Permissions
38+
Creating the tag locally requires no special permissions beyond a
39+
working git clone. Pushing the tag to the upstream repository requires
40+
write access granted by the repository administrators — contact the
41+
WMCO team to have your GitHub account added before pushing.
2442
2543
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2644
DATA SOURCES
2745
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2846
2947
Red Hat Container Catalog (catalog.redhat.com)
3048
Bundle image: openshift4-wincw/windows-machine-config-operator-bundle
31-
Provides: build commit SHA. Newer images carry the full 40-character SHA
32-
in the org.opencontainers.image.revision OCI label. Older images that
33-
predate the label are handled by falling back to the short (7-character)
34-
hex SHA that Konflux publishes as an image tag alongside the version tag.
35-
36-
Operator image: openshift4-wincw/windows-machine-config-rhel9-operator
37-
Provides: push_date — the UTC timestamp when the image was published to
38-
the catalog. This is used as the tag date so the annotated tag timestamp
39-
reflects the actual release date rather than the date the tag was created.
49+
Provides: build commit SHA and publish date. Newer images carry the full
50+
40-character SHA in the org.opencontainers.image.revision OCI label.
51+
Older images that predate the label are handled by falling back to the
52+
short (7-character) hex SHA that Konflux publishes as an image tag
53+
alongside the version tag. The bundle's push_date provides the release
54+
date used for the tag timestamp.
4055
4156
Local git repository
4257
Provides: existing tag detection (prevents accidental overwrites), full
@@ -52,10 +67,15 @@
5267
Source: Bundle image catalog
5368
Data: org.opencontainers.image.revision label (preferred) or short hex
5469
image tag (fallback for images that predate the OCI label).
70+
Why bundle image: The bundle is built directly from a specific repo commit
71+
and represents the last commit that was actually shipped in a release.
72+
Tagging that commit creates a clear boundary — everything up to and
73+
including the tag has been released, everything after has not.
5574
Logic:
5675
• Scans the bundle catalog for the image whose tags include vX.Y.Z.
57-
• Prefers the full SHA from the OCI label; falls back to the short SHA
58-
tag.
76+
• Prefers the full SHA from the OCI label over the short SHA image tag.
77+
A full 40-character SHA is unambiguous; a 7-character short SHA carries
78+
collision risk as the repository grows.
5979
• Any SHA (full or short) is expanded to a full 40-character commit via
6080
git rev-parse and verified to exist locally. This catches catalog/repo
6181
mismatches (e.g. if the local clone is stale) before writing anything.
@@ -64,20 +84,26 @@
6484
image and therefore have no bundle catalog entry.
6585
6686
2. TAG DATE RESOLUTION
67-
Source: Operator image catalog
87+
Source: Bundle image catalog
6888
Data: push_date field from the repository entry for the matching
6989
version.
90+
Why bundle image: Since the tag points to the commit the bundle was built
91+
from, the bundle's publish date is the authoritative timestamp for when
92+
that code was released — it is consistent with the commit source.
7093
Logic:
71-
• The push_date is the UTC timestamp when the operator image was
72-
published.
94+
• The push_date is the UTC timestamp when the bundle image was published.
7395
• Only the date portion (YYYY-MM-DD) is used; the time is fixed to noon
74-
UTC (12:00:00+0000) to produce an unambiguous, timezone-neutral
75-
timestamp.
96+
UTC (12:00:00+0000) for consistency across all release tags — only the
97+
calendar date is meaningful.
7698
• The fixed noon time is set via GIT_COMMITTER_DATE when git tag is
7799
invoked, so it appears as the tag's creation date in git log and GitHub.
78100
• --date skips the catalog lookup and uses the provided date instead.
79101
80102
3. TAG CREATION
103+
Why annotated tag: Annotated tags are immutable git objects that carry
104+
tagger identity, date, and message, creating a verifiable audit trail in
105+
the repository history. Lightweight tags are just pointers and carry
106+
none of this metadata.
81107
Logic:
82108
• Tag name: vX.Y.Z
83109
• Tag message: "Windows Machine Config Operator vX.Y.Z"
@@ -88,6 +114,10 @@
88114
become permanent.
89115
90116
4. PUSH INSTRUCTION
117+
Why show command rather than push: This script runs interactively in a
118+
developer's local environment. Any action that modifies public,
119+
shared content is shown as a command for the developer to review and
120+
execute deliberately, rather than performed automatically.
91121
Logic:
92122
• After creation, scans git remote -v for a remote whose URL contains
93123
"openshift/windows-machine-config-operator" and uses its configured
@@ -108,11 +138,6 @@
108138

109139
import requests
110140

111-
OPERATOR_CATALOG_API = (
112-
"https://catalog.redhat.com/api/containers/v1/repositories/"
113-
"registry/registry.access.redhat.com/repository/"
114-
"openshift4-wincw/windows-machine-config-rhel9-operator/images"
115-
)
116141
BUNDLE_CATALOG_API = (
117142
"https://catalog.redhat.com/api/containers/v1/repositories/"
118143
"registry/registry.access.redhat.com/repository/"
@@ -124,7 +149,7 @@
124149

125150

126151
# ---------------------------------------------------------------------------
127-
# Catalog helpers (shared pattern with verify-release.py)
152+
# Catalog helpers
128153
# ---------------------------------------------------------------------------
129154

130155
def _fetch_pages(api_url: str) -> list:
@@ -194,12 +219,12 @@ def fetch_bundle_info(version: str) -> tuple[str, str]:
194219
return "", ""
195220

196221

197-
def fetch_operator_push_date(version: str) -> str:
222+
def fetch_bundle_push_date(version: str) -> str:
198223
"""
199224
Return the push_date (YYYY-MM-DD) for the given version from the
200-
operator catalog, or '' if not found.
225+
bundle catalog, or '' if not found.
201226
"""
202-
for img in _fetch_pages(OPERATOR_CATALOG_API):
227+
for img in _fetch_pages(BUNDLE_CATALOG_API):
203228
for repo in img.get("repositories", []):
204229
for tag in repo.get("tags", []):
205230
m = _VERSION_TAG_RE.match(tag.get("name", ""))
@@ -277,7 +302,84 @@ def _find_upstream_remote() -> str:
277302
return ""
278303

279304

280-
# pylint: disable=too-many-locals,too-many-branches,too-many-statements
305+
def resolve_commit_sha(tag: str, version: str,
306+
override: str) -> tuple[str, str]:
307+
"""
308+
Return (commit_sha, source_description) for the given version.
309+
310+
Uses the manually provided *override* SHA when supplied; otherwise
311+
queries the bundle catalog. Calls sys.exit(1) on any failure.
312+
"""
313+
if override:
314+
return override, "provided manually"
315+
316+
try:
317+
commit_sha, commit_source = fetch_bundle_info(version)
318+
except Exception as err: # pylint: disable=broad-except
319+
print(
320+
f"\nERROR: fetch_bundle_info failed: {err}",
321+
file=sys.stderr,
322+
)
323+
sys.exit(1)
324+
325+
if not commit_sha:
326+
print(
327+
f"\nERROR: Could not resolve commit SHA for {tag}.",
328+
file=sys.stderr,
329+
)
330+
print(
331+
" The bundle image for this version "
332+
"may not be in the catalog.",
333+
file=sys.stderr,
334+
)
335+
print(
336+
" Provide the commit manually: --commit <SHA>",
337+
file=sys.stderr,
338+
)
339+
sys.exit(1)
340+
341+
return commit_sha, commit_source
342+
343+
344+
def resolve_date(tag: str, version: str, override: str) -> tuple[str, str]:
345+
"""
346+
Return (published_date, source_description) for the given version.
347+
348+
Uses the manually provided *override* date when supplied; otherwise
349+
queries the bundle catalog. Calls sys.exit(1) on any failure.
350+
"""
351+
if override:
352+
return override, "provided manually"
353+
354+
try:
355+
published_date = fetch_bundle_push_date(version)
356+
except Exception as err: # pylint: disable=broad-except
357+
print(
358+
f"\nERROR: fetch_bundle_push_date failed: {err}",
359+
file=sys.stderr,
360+
)
361+
sys.exit(1)
362+
363+
if not published_date:
364+
print(
365+
f"\nERROR: Could not resolve published date for {tag}.",
366+
file=sys.stderr,
367+
)
368+
print(
369+
" The bundle image for this version "
370+
"may not be in the catalog.",
371+
file=sys.stderr,
372+
)
373+
print(
374+
" Provide the date manually: --date YYYY-MM-DD",
375+
file=sys.stderr,
376+
)
377+
sys.exit(1)
378+
379+
return published_date, "bundle image push date"
380+
381+
382+
# pylint: disable=too-many-locals,too-many-statements
281383
def main():
282384
"""Parse args, resolve tag details, confirm with user, create the tag."""
283385
parser = argparse.ArgumentParser(
@@ -308,6 +410,11 @@ def main():
308410
if not re.fullmatch(r"\d+\.\d+\.\d+", args.version):
309411
parser.error(f"version must be X.Y.Z, got: {args.version!r}")
310412

413+
if args.commit and not _HEX_RE.match(args.commit):
414+
parser.error(
415+
f"--commit must be a hex SHA (7-40 characters), got: {args.commit!r}"
416+
)
417+
311418
if args.date:
312419
try:
313420
date.fromisoformat(args.date)
@@ -336,62 +443,8 @@ def main():
336443
flush=True,
337444
)
338445

339-
if args.commit:
340-
commit_sha = args.commit
341-
commit_source = "provided manually"
342-
else:
343-
try:
344-
commit_sha, commit_source = fetch_bundle_info(version)
345-
except Exception as err: # pylint: disable=broad-except
346-
print(
347-
f"\nERROR: fetch_bundle_info failed: {err}",
348-
file=sys.stderr,
349-
)
350-
sys.exit(1)
351-
if not commit_sha:
352-
print(
353-
f"\nERROR: Could not resolve commit SHA for {tag}.",
354-
file=sys.stderr,
355-
)
356-
print(
357-
" The bundle image for this version "
358-
"may not be in the catalog.",
359-
file=sys.stderr,
360-
)
361-
print(
362-
" Provide the commit manually: --commit <SHA>",
363-
file=sys.stderr,
364-
)
365-
sys.exit(1)
366-
367-
if args.date:
368-
published_date = args.date
369-
date_source = "provided manually"
370-
else:
371-
try:
372-
published_date = fetch_operator_push_date(version)
373-
except Exception as err: # pylint: disable=broad-except
374-
print(
375-
f"\nERROR: fetch_operator_push_date failed: {err}",
376-
file=sys.stderr,
377-
)
378-
sys.exit(1)
379-
date_source = "operator image push date"
380-
if not published_date:
381-
print(
382-
f"\nERROR: Could not resolve published date for {tag}.",
383-
file=sys.stderr,
384-
)
385-
print(
386-
" The operator image for this version "
387-
"may not be in the catalog.",
388-
file=sys.stderr,
389-
)
390-
print(
391-
" Provide the date manually: --date YYYY-MM-DD",
392-
file=sys.stderr,
393-
)
394-
sys.exit(1)
446+
commit_sha, commit_source = resolve_commit_sha(tag, version, args.commit)
447+
published_date, date_source = resolve_date(tag, version, args.date)
395448

396449
# Expand to full commit SHA and verify it exists locally
397450
try:

0 commit comments

Comments
 (0)