Skip to content

Use TS to fetch tags#940

Open
FyreByrd wants to merge 3 commits intomainfrom
fix/appbuilder-version-gha
Open

Use TS to fetch tags#940
FyreByrd wants to merge 3 commits intomainfrom
fix/appbuilder-version-gha

Conversation

@FyreByrd
Copy link
Collaborator

@FyreByrd FyreByrd commented Feb 12, 2026

Uses a script written in TS to find the latest AppBuilders version number. The GHCR recently added the tag staging to the list of tags, which invalidated prior assumptions about getting the latest version number tag from GHCR.

Summary by CodeRabbit

  • Chores
    • Streamlined the CI/CD process for detecting the latest app builder version by consolidating tag retrieval into a dedicated automation step. Improves maintainability and reliability of version discovery without changing user-facing behavior.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 12, 2026

📝 Walkthrough

Walkthrough

Replaces inline shell steps in the GitHub Actions workflow that fetched GHCR tags with a new TypeScript script (convert/fetchTags.ts) invoked via npx ts-node, preserving output of appbuilder_version while moving token/tag/digest logic into the script.

Changes

Cohort / File(s) Summary
Workflow Simplification
.github/workflows/main.yml
Replaces multi-step shell token/tag/digest parsing with a single call: npx ts-node convert/fetchTags.ts >> $GITHUB_OUTPUT (removes curl/jq/token parsing lines).
GHCR Tag Fetcher Script
convert/fetchTags.ts
Adds a new TypeScript script that authenticates to GHCR, lists sillsdev/app-builders tags, filters numeric/version-like tags, sorts via compareVersions, matches digest to latest, and prints appbuilder_version=<tag>.

Sequence Diagram(s)

sequenceDiagram
    participant WF as GitHub Actions Workflow
    participant Script as convert/fetchTags.ts
    participant GHCR as GitHub Container Registry API

    rect rgba(200,220,255,0.5)
    WF->>Script: invoke `npx ts-node convert/fetchTags.ts`
    end

    rect rgba(200,255,220,0.5)
    Script->>GHCR: request token (actions identity)
    GHCR-->>Script: return token
    Script->>GHCR: fetch tags for sillsdev/app-builders (Bearer token)
    GHCR-->>Script: return tags list
    Script->>Script: filter numeric tags, sort versions, fetch/latest digests
    Script->>GHCR: request digest for candidate tags
    GHCR-->>Script: return digest(s)
    Script->>WF: output `appbuilder_version=<latest_tag>` (stdout -> GITHUB_OUTPUT)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • chrisvire

Poem

🐰 I hopped through lines of shell and strife,
Now TypeScript hums and tidies life.
Tokens fetched and tags aligned,
A cleaner output now to find —
Hooray! — my workflow’s feeling fine. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: replacing multi-step shell logic with a TypeScript script to fetch GHCR tags and determine the latest version.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/appbuilder-version-gha

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@FyreByrd FyreByrd requested a review from chrisvire February 12, 2026 20:39
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@convert/fetchTags.ts`:
- Line 3: The regex verNumRegExp currently matches any string containing a
numeric version fragment; change it to anchor the pattern so it only matches
entire tags that are purely numeric versions (update verNumRegExp to use start
and end anchors, e.g., require the whole string to match digits and up to two
dot-separated groups) and ensure any code using verNumRegExp (the filter/match
logic in fetchTags) continues to test the whole tag against this anchored
pattern.
- Around line 5-22: The top-level async IIFE that fetches tokenJSON/tagJSON and
prints latest can reject silently; wrap the IIFE invocation in a .catch(...)
handler so any thrown error is logged (e.g., processLogger or console.error) and
the process exits non‑zero (process.exit(1)). Specifically update the immediate
call to the async function that contains tokenJSON, token, tagJSON, tags, latest
and console.log to append a .catch(block) that logs the error and calls
process.exit(1) to ensure CI fails on errors.
- Around line 16-21: The variable `latest` (computed from
`tags.filter(...).sort(compareVersions).at(-1)`) can be undefined when no tags
match `verNumRegExp`, so add an explicit guard immediately after computing
`latest` to fail the process instead of printing `appbuilder_version=undefined`;
for example, check `if (!latest) { console.error("No matching tags found for
verNumRegExp"); process.exit(1); }` (or throw an Error) before the existing
`console.log`, referencing `latest`, `tags`, `verNumRegExp`, and
`compareVersions` so the workflow step fails clearly when no version is found.
- Around line 6-12: The fetch calls in convert/fetchTags.ts don't check HTTP
responses and can produce undefined outputs; change both requests to await the
Response (e.g., respToken and respTags), verify resp.ok and throw a descriptive
Error including status and statusText when not ok, then call resp.json(); after
parsing, validate that tokenJSON.token exists before using it and that tagJSON
has the expected shape (e.g., tagJSON.tags array) and throw if not — update the
tokenJSON/token and tagJSON usages accordingly so failures fail-fast instead of
producing undefined values.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
convert/fetchTags.ts (2)

6-11: ⚠️ Potential issue | 🟠 Major

Add response-status checks and payload validation before using token/tags/digest.

Right now fetch(...).json() is used directly; if GHCR returns a non-2xx response or unexpected JSON shape, the script may fail later with less context. Please fail explicitly on !res.ok and validate required fields so CI fails fast with a clear error.

Suggested fix
 async function getDigest(token: string, ref: string) {
-    return (
-        await fetch(`https://ghcr.io/v2/sillsdev/app-builders/manifests/${ref}`, {
-            headers: { Authorization: `Bearer ${token}` }
-        }).then((r) => r.json())
-    ).config.digest as string;
+    const res = await fetch(`https://ghcr.io/v2/sillsdev/app-builders/manifests/${ref}`, {
+        headers: { Authorization: `Bearer ${token}` }
+    });
+    if (!res.ok) {
+        throw new Error(`Manifest fetch failed for ${ref}: ${res.status} ${res.statusText}`);
+    }
+    const json = await res.json();
+    if (!json?.config?.digest) {
+        throw new Error(`Missing config.digest for ${ref}`);
+    }
+    return json.config.digest as string;
 }
 
 (async () => {
     try {
-        const token = (
-            await fetch('https://ghcr.io/token?scope=repository:sillsdev/app-builders:pull').then(
-                (r) => r.json()
-            )
-        ).token as string;
+        const tokenRes = await fetch(
+            'https://ghcr.io/token?scope=repository:sillsdev/app-builders:pull'
+        );
+        if (!tokenRes.ok) {
+            throw new Error(`Token fetch failed: ${tokenRes.status} ${tokenRes.statusText}`);
+        }
+        const tokenJSON = await tokenRes.json();
+        if (!tokenJSON?.token) {
+            throw new Error('Token response missing token');
+        }
+        const token = tokenJSON.token as string;
 
-        const tags = (
-            await fetch('https://ghcr.io/v2/sillsdev/app-builders/tags/list', {
-                headers: { Authorization: `Bearer ${token}` }
-            }).then((r) => r.json())
-        ).tags as string[];
+        const tagRes = await fetch('https://ghcr.io/v2/sillsdev/app-builders/tags/list', {
+            headers: { Authorization: `Bearer ${token}` }
+        });
+        if (!tagRes.ok) {
+            throw new Error(`Tag fetch failed: ${tagRes.status} ${tagRes.statusText}`);
+        }
+        const tagJSON = await tagRes.json();
+        if (!Array.isArray(tagJSON?.tags)) {
+            throw new Error('Tag response missing tags array');
+        }
+        const tags = tagJSON.tags as string[];

Based on learnings: For conversion modules (e.g., files under convert/), adopt a fail-fast approach: use non-null assertions and allow runtime errors when source XML data is invalid or missing attributes, rather than silently handling or recovering from such errors. This ensures data issues are surfaced and fixed upstream. Apply this guideline to all TypeScript conversion files under convert/.

Also applies to: 16-26

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convert/fetchTags.ts` around lines 6 - 11, getDigest currently calls
fetch(...).json() directly which can hide HTTP errors and unexpected payloads;
change it to await fetch(...) into a response variable (e.g., const res = await
fetch(...)), check if (!res.ok) and throw a descriptive error including
res.status and await res.text(), then parse const payload = await res.json();
validate that payload.config and payload.config.digest exist and are strings
before returning payload.config.digest as string (throw a clear error if
validation fails). Apply the same pattern to any other tag/token/digest lookups
in this file (and related convert/* modules) so failures are explicit and
fail-fast.

28-39: ⚠️ Potential issue | 🟠 Major

Fail if no version tag matches the latest digest.

The current do/while always prints sortedTags[i] even when no digest match exists (e.g., latest points to a non-version tag). That can silently emit the wrong version. Add an explicit guard for “no match” and “no version tags.”

Suggested fix
         const sortedTags = tags.filter((t) => t.match(verNumRegExp)).sort(compareVersions);
 
+        if (sortedTags.length === 0) {
+            throw new Error('No version tags found');
+        }
+
         const latestDigest = await getDigest(token, 'latest');
 
-        let i = sortedTags.length;
-        let digest = '';
-        do {
-            i--;
-            digest = await getDigest(token, sortedTags[i]);
-        } while (i > 0 && latestDigest !== digest);
-
-        console.log(`appbuilder_version=${sortedTags[i]}`);
+        let matchedTag: string | undefined;
+        for (let i = sortedTags.length - 1; i >= 0; i--) {
+            const digest = await getDigest(token, sortedTags[i]);
+            if (latestDigest === digest) {
+                matchedTag = sortedTags[i];
+                break;
+            }
+        }
+
+        if (!matchedTag) {
+            throw new Error('No version tag matches latest digest');
+        }
+        console.log(`appbuilder_version=${matchedTag}`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convert/fetchTags.ts` around lines 28 - 39, The code prints sortedTags[i]
even when no version tags exist or none match the latest digest; update the
logic in the block that uses sortedTags, verNumRegExp, compareVersions and
getDigest to explicitly handle these cases: first guard against an empty
sortedTags and fail/exit with a clear error if none; then when iterating
(do/while) track whether a matching digest was found (or after the loop compare
latestDigest to digest) and if no match was found, fail/exit or emit an explicit
error instead of printing sortedTags[i]; only console.log the matched tag when a
valid match exists.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@convert/fetchTags.ts`:
- Around line 6-11: getDigest currently calls fetch(...).json() directly which
can hide HTTP errors and unexpected payloads; change it to await fetch(...) into
a response variable (e.g., const res = await fetch(...)), check if (!res.ok) and
throw a descriptive error including res.status and await res.text(), then parse
const payload = await res.json(); validate that payload.config and
payload.config.digest exist and are strings before returning
payload.config.digest as string (throw a clear error if validation fails). Apply
the same pattern to any other tag/token/digest lookups in this file (and related
convert/* modules) so failures are explicit and fail-fast.
- Around line 28-39: The code prints sortedTags[i] even when no version tags
exist or none match the latest digest; update the logic in the block that uses
sortedTags, verNumRegExp, compareVersions and getDigest to explicitly handle
these cases: first guard against an empty sortedTags and fail/exit with a clear
error if none; then when iterating (do/while) track whether a matching digest
was found (or after the loop compare latestDigest to digest) and if no match was
found, fail/exit or emit an explicit error instead of printing sortedTags[i];
only console.log the matched tag when a valid match exists.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 468ff13 and 7a30e3a.

📒 Files selected for processing (2)
  • .github/workflows/main.yml
  • convert/fetchTags.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant