Skip to content

Commit fa73d32

Browse files
zdrapelajohnmcollier
authored andcommitted
feat: add --tag-filter for targeted Quay tag queries
Replace pagination-based tag fetching with a smarter approach: - With --tag-filter: single Quay request using like:<filter> (fast, precise) - Without filter: dual-prefix fallback (like:1. + like:2.) covering current and upcoming RHDH major versions This is faster than pagination (~0.5s vs ~11s) and future-proof for RHDH 2.x without hardcoding version assumptions. Update SKILL.md to instruct agents to derive --tag-filter from the selected branch (e.g. release-1.10 -> --tag-filter 1.10). Assisted-by: OpenCode fix: paginate Quay tag API to find version tags without hardcoded prefix Without the server-side like:1. filter, version tags are buried among sha/sig/att artifacts. Paginate the Quay API (up to 10 pages, stopping after 2 consecutive pages with no version tags) and filter locally with the existing regex. The limit parameter now controls how many results to display (latest N), while JSON output returns all matches. Assisted-by: OpenCode fix: warn when --tag-filter is used without --list-tags Assisted-by: OpenCode docs: don't hardcode main branch to a specific version in SKILL.md Assisted-by: OpenCode
1 parent 6290b1a commit fa73d32

2 files changed

Lines changed: 75 additions & 17 deletions

File tree

skills/prow-trigger-nightly/SKILL.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,17 @@ Constraint: `--image-repo` requires `--tag`, but `--tag` works on its own.
9898

9999
### Follow-up based on selections
100100

101-
**If 2 or 3 selected (quay.io registry)** — fetch available tags and present as numbered options:
101+
**If 2 or 3 selected (quay.io registry)** — fetch available tags and present as numbered options. For `release-*` branches, derive `--tag-filter` by stripping the `release-` prefix. For `main`, omit `--tag-filter` to show all available versions:
102102

103103
```bash
104-
uv run scripts/trigger_nightly_job.py --list-tags [--image-repo <REPO>]
104+
# For release-1.10 branch:
105+
uv run scripts/trigger_nightly_job.py --list-tags --tag-filter 1.10
106+
107+
# For main branch (show all versions):
108+
uv run scripts/trigger_nightly_job.py --list-tags
105109
```
106110

107-
Default repo is `rhdh/rhdh-hub-rhel9`. Present the numbered results with a final option to enter a custom tag (e.g. `next`, `latest`). For option 3, also ask for the image repository.
111+
Use `--image-repo <REPO>` to query a different image repository (default: `rhdh/rhdh-hub-rhel9`). Present the numbered results with a final option to enter a custom tag (e.g. `next`, `latest`). For option 3, also ask for the image repository.
108112

109113
**If 4 selected (non-quay registry)** — ask for all three values (tag fetching not available):
110114
- Registry (e.g. `brew.registry.redhat.io`)
@@ -140,7 +144,7 @@ After execution, show the API response. If a job URL or ID is returned, display
140144

141145
## Reference
142146

143-
- Script flags: `-j/--job`, `-l/--list`, `-T/--list-tags`, `-I/--image-registry`, `-q/--image-repo`, `-t/--tag`, `-o/--org`, `-r/--repo`, `-b/--branch`, `-S/--send-alerts`, `-n/--dry-run`
147+
- Script flags: `-j/--job`, `-l/--list`, `-T/--list-tags`, `--tag-filter`, `-I/--image-registry`, `-q/--image-repo`, `-t/--tag`, `-o/--org`, `-r/--repo`, `-b/--branch`, `-S/--send-alerts`, `-n/--dry-run`, `--json`
144148
- Dedicated kubeconfig at `~/.config/openshift-ci/kubeconfig` — won't interfere with your current cluster context
145149
- If auth is needed, the script opens a browser for SSO login
146150
- RHDH jobs list: https://prow.ci.openshift.org/configured-jobs/redhat-developer/rhdh

skills/prow-trigger-nightly/scripts/trigger_nightly_job.py

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -212,35 +212,76 @@ def branch_sort_key(b: str) -> tuple[int, str]:
212212
DEFAULT_IMAGE_REPO = "rhdh/rhdh-hub-rhel9"
213213

214214

215-
def list_tags(image_repo: str, limit: int = 20, *, json_output: bool = False) -> None:
216-
"""Fetch and print available image tags from quay.io."""
215+
def _fetch_quay_tags(image_repo: str, like_filter: str) -> list[dict]:
216+
"""Fetch tags from the Quay API with a ``like:`` substring filter."""
217217
encoded_repo = urllib.request.quote(image_repo, safe="")
218-
url = f"https://quay.io/api/v1/repository/{encoded_repo}/tag/?limit={limit}&onlyActiveTags=true"
218+
encoded_filter = urllib.request.quote(like_filter, safe="")
219+
url = (
220+
f"https://quay.io/api/v1/repository/{encoded_repo}/tag/"
221+
f"?limit=100&onlyActiveTags=true&filter_tag_name=like:{encoded_filter}"
222+
)
219223
req = urllib.request.Request(url, headers={"User-Agent": "rhdh-skill"})
220224
try:
221225
with urllib.request.urlopen(req, timeout=30) as resp:
222226
data = json.loads(resp.read().decode("utf-8"))
223227
except (urllib.error.URLError, OSError) as exc:
224-
log_error(f"Failed to fetch tags from quay.io: {exc}")
225-
sys.exit(1)
228+
log_warn(f"Failed to fetch tags (filter={like_filter!r}): {exc}")
229+
return []
230+
return data.get("tags", [])
231+
232+
233+
def _fetch_version_tags(image_repo: str, tag_filter: str = "") -> list[str]:
234+
"""Fetch version tags from quay.io for the given image repo.
235+
236+
If *tag_filter* is provided (e.g. ``1.10``), a single Quay request with
237+
``filter_tag_name=like:<tag_filter>`` is issued — fast and precise.
238+
239+
Without a filter, two requests are made for the ``1.`` and ``2.`` major
240+
version prefixes and the results are merged, covering current and upcoming
241+
RHDH releases.
242+
243+
In both cases the results are filtered locally by a strict version regex
244+
(``MAJOR.MINOR`` or ``MAJOR.MINOR-BUILD``) to discard digest artifacts
245+
that happen to match the substring filter.
246+
"""
247+
version_re = re.compile(r"^[0-9]+\.[0-9]+(-[0-9]+)?$")
248+
249+
if tag_filter:
250+
prefixes = [tag_filter]
251+
else:
252+
prefixes = ["1.", "2."]
253+
254+
seen: set[str] = set()
255+
tags: list[str] = []
256+
for prefix in prefixes:
257+
for t in _fetch_quay_tags(image_repo, prefix):
258+
name = t.get("name", "")
259+
if name not in seen and version_re.match(name):
260+
seen.add(name)
261+
tags.append(name)
226262

227-
tags = [
228-
t["name"]
229-
for t in data.get("tags", [])
230-
if re.match(r"^[0-9]+\.[0-9]+(-[0-9]+)?$", t.get("name", ""))
231-
]
232263
tags.sort(key=lambda t: [int(x) for x in re.split(r"[-.]", t)])
264+
return tags
265+
266+
267+
def list_tags(
268+
image_repo: str, limit: int = 20, *, tag_filter: str = "", json_output: bool = False
269+
) -> None:
270+
"""Fetch and print available image tags from quay.io."""
271+
tags = _fetch_version_tags(image_repo, tag_filter=tag_filter)
233272

234273
if not tags:
235274
log_error(f"No matching tags found for {image_repo}")
236275
sys.exit(1)
237276

277+
# JSON always returns all tags; human output shows the latest N.
238278
if json_output:
239279
print(json.dumps({"image_repo": image_repo, "tags": tags}, indent=2))
240280
return
241281

242-
log_info(f"Available tags for {image_repo}:")
243-
for i, tag in enumerate(tags, 1):
282+
display_tags = tags[-limit:]
283+
log_info(f"Available tags for {image_repo} (latest {len(display_tags)}):")
284+
for i, tag in enumerate(display_tags, 1):
244285
print(f" {i}. {tag}")
245286

246287

@@ -483,6 +524,12 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
483524
dest="list_tags",
484525
help="List available image tags from quay.io. Use --image-repo to specify the repo.",
485526
)
527+
parser.add_argument(
528+
"--tag-filter",
529+
dest="tag_filter",
530+
default="",
531+
help="Filter tags by version prefix (e.g. '1.10'). Used with --list-tags.",
532+
)
486533

487534
overrides = parser.add_argument_group("RHDH job overrides (not supported for overlay jobs)")
488535
overrides.add_argument(
@@ -557,6 +604,9 @@ def validate_args(args: argparse.Namespace) -> None:
557604
log_error("Either --list, --list-tags, or --job is required.")
558605
sys.exit(1)
559606

607+
if args.tag_filter and not args.list_tags:
608+
log_warn("--tag-filter is only used with --list-tags, ignoring.")
609+
560610
if args.job:
561611
if not args.job.startswith("periodic-ci-"):
562612
log_error(f"Job name must start with 'periodic-ci-', got: {args.job}")
@@ -602,7 +652,11 @@ def main(argv: list[str] | None = None) -> None:
602652
return
603653

604654
if args.list_tags:
605-
list_tags(args.image_repo or DEFAULT_IMAGE_REPO, json_output=args.json_output)
655+
list_tags(
656+
args.image_repo or DEFAULT_IMAGE_REPO,
657+
tag_filter=args.tag_filter,
658+
json_output=args.json_output,
659+
)
606660
return
607661

608662
payload = build_payload(args)

0 commit comments

Comments
 (0)