|
5 | 5 | The commit SHA is resolved from the bundle image in the Red Hat Container |
6 | 6 | Catalog, preferring the org.opencontainers.image.revision label (full SHA) |
7 | 7 | 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. |
9 | 9 |
|
10 | 10 | Usage: |
11 | 11 | python3 hack/create-release-tag.py <version> |
|
17 | 17 | --commit is required when the version has no entry in the bundle catalog |
18 | 18 | (e.g. backport releases that were never shipped as a container image). |
19 | 19 |
|
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. |
24 | 42 |
|
25 | 43 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
26 | 44 | DATA SOURCES |
27 | 45 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
28 | 46 |
|
29 | 47 | Red Hat Container Catalog (catalog.redhat.com) |
30 | 48 | 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. |
40 | 55 |
|
41 | 56 | Local git repository |
42 | 57 | Provides: existing tag detection (prevents accidental overwrites), full |
|
52 | 67 | Source: Bundle image catalog |
53 | 68 | Data: org.opencontainers.image.revision label (preferred) or short hex |
54 | 69 | 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. |
55 | 74 | Logic: |
56 | 75 | • 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. |
59 | 79 | • Any SHA (full or short) is expanded to a full 40-character commit via |
60 | 80 | git rev-parse and verified to exist locally. This catches catalog/repo |
61 | 81 | mismatches (e.g. if the local clone is stale) before writing anything. |
|
64 | 84 | image and therefore have no bundle catalog entry. |
65 | 85 |
|
66 | 86 | 2. TAG DATE RESOLUTION |
67 | | - Source: Operator image catalog |
| 87 | + Source: Bundle image catalog |
68 | 88 | Data: push_date field from the repository entry for the matching |
69 | 89 | 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. |
70 | 93 | 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. |
73 | 95 | • 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. |
76 | 98 | • The fixed noon time is set via GIT_COMMITTER_DATE when git tag is |
77 | 99 | invoked, so it appears as the tag's creation date in git log and GitHub. |
78 | 100 | • --date skips the catalog lookup and uses the provided date instead. |
79 | 101 |
|
80 | 102 | 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. |
81 | 107 | Logic: |
82 | 108 | • Tag name: vX.Y.Z |
83 | 109 | • Tag message: "Windows Machine Config Operator vX.Y.Z" |
|
88 | 114 | become permanent. |
89 | 115 |
|
90 | 116 | 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. |
91 | 121 | Logic: |
92 | 122 | • After creation, scans git remote -v for a remote whose URL contains |
93 | 123 | "openshift/windows-machine-config-operator" and uses its configured |
|
108 | 138 |
|
109 | 139 | import requests |
110 | 140 |
|
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 | | -) |
116 | 141 | BUNDLE_CATALOG_API = ( |
117 | 142 | "https://catalog.redhat.com/api/containers/v1/repositories/" |
118 | 143 | "registry/registry.access.redhat.com/repository/" |
|
124 | 149 |
|
125 | 150 |
|
126 | 151 | # --------------------------------------------------------------------------- |
127 | | -# Catalog helpers (shared pattern with verify-release.py) |
| 152 | +# Catalog helpers |
128 | 153 | # --------------------------------------------------------------------------- |
129 | 154 |
|
130 | 155 | def _fetch_pages(api_url: str) -> list: |
@@ -194,12 +219,12 @@ def fetch_bundle_info(version: str) -> tuple[str, str]: |
194 | 219 | return "", "" |
195 | 220 |
|
196 | 221 |
|
197 | | -def fetch_operator_push_date(version: str) -> str: |
| 222 | +def fetch_bundle_push_date(version: str) -> str: |
198 | 223 | """ |
199 | 224 | 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. |
201 | 226 | """ |
202 | | - for img in _fetch_pages(OPERATOR_CATALOG_API): |
| 227 | + for img in _fetch_pages(BUNDLE_CATALOG_API): |
203 | 228 | for repo in img.get("repositories", []): |
204 | 229 | for tag in repo.get("tags", []): |
205 | 230 | m = _VERSION_TAG_RE.match(tag.get("name", "")) |
@@ -277,7 +302,84 @@ def _find_upstream_remote() -> str: |
277 | 302 | return "" |
278 | 303 |
|
279 | 304 |
|
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 |
281 | 383 | def main(): |
282 | 384 | """Parse args, resolve tag details, confirm with user, create the tag.""" |
283 | 385 | parser = argparse.ArgumentParser( |
@@ -308,6 +410,11 @@ def main(): |
308 | 410 | if not re.fullmatch(r"\d+\.\d+\.\d+", args.version): |
309 | 411 | parser.error(f"version must be X.Y.Z, got: {args.version!r}") |
310 | 412 |
|
| 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 | + |
311 | 418 | if args.date: |
312 | 419 | try: |
313 | 420 | date.fromisoformat(args.date) |
@@ -336,62 +443,8 @@ def main(): |
336 | 443 | flush=True, |
337 | 444 | ) |
338 | 445 |
|
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) |
395 | 448 |
|
396 | 449 | # Expand to full commit SHA and verify it exists locally |
397 | 450 | try: |
|
0 commit comments