Skip to content

Use TS to fetch tags#940

Open
FyreByrd wants to merge 1 commit intomainfrom
fix/appbuilder-version-gha
Open

Use TS to fetch tags#940
FyreByrd wants to merge 1 commit 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 automated process for determining the latest app builder version in CI/CD pipelines. The version retrieval logic now leverages a dedicated script instead of manual shell commands, improving maintainability without changing user-facing functionality.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 12, 2026

📝 Walkthrough

Walkthrough

This refactors the GitHub workflow step responsible for fetching the latest AppBuilder version from GHCR. Previously implemented using shell commands with curl and jq, the logic is now delegated to a dedicated TypeScript script that performs token retrieval, tag fetching, filtering, and version sorting.

Changes

Cohort / File(s) Summary
Workflow Simplification
.github/workflows/main.yml
Replaces 8 lines of shell logic (curl, jq, token parsing, tag filtering) with a single call to npx ts-node convert/fetchTags, maintaining the same GITHUB_OUTPUT write behavior.
GHCR Tag Fetcher Script
convert/fetchTags.ts
New TypeScript script that retrieves a GHCR authentication token, fetches tags for sillsdev/app-builders, filters for numeric versions, sorts via compareVersions, and outputs the latest version as appbuilder_version=<tag> to stdout.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • chrisvire

Poem

🐰 A rabbit's workflow, once tangled in shell,
Now dances in TypeScript—a cleaner tale to tell!
With tokens and tags in a script so bright,
Version-fetching logic takes elegant flight. ✨

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: replacing shell-based tag fetching logic with a TypeScript script to fetch tags from GHCR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 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

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.

@@ -0,0 +1,22 @@
import { compareVersions } from './stringUtils';

const verNumRegExp = /\d+(\.\d+){0,2}/;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Anchor the regex to avoid matching non-version tags.

/\d+(\.\d+){0,2}/ matches any tag containing a digit sequence (e.g., a hypothetical "rc1-staging" tag would pass the filter). Since the goal is to match tags that are purely numeric versions, anchor the regex.

Proposed fix
-const verNumRegExp = /\d+(\.\d+){0,2}/;
+const verNumRegExp = /^\d+(\.\d+){0,2}$/;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const verNumRegExp = /\d+(\.\d+){0,2}/;
const verNumRegExp = /^\d+(\.\d+){0,2}$/;
🤖 Prompt for AI Agents
In `@convert/fetchTags.ts` at 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.

Comment on lines +5 to +22
(async () => {
const tokenJSON = await fetch(
'https://ghcr.io/token?scope=repository:sillsdev/app-builders:pull'
).then((r) => r.json());
const token = tokenJSON.token;
const tagJSON = await fetch('https://ghcr.io/v2/sillsdev/app-builders/tags/list', {
headers: { Authorization: `Bearer ${token}` }
}).then((r) => r.json());

const tags = tagJSON.tags as string[];

const latest = tags
.filter((t) => t.match(verNumRegExp))
.sort(compareVersions)
.at(-1);

console.log(`appbuilder_version=${latest}`);
})();
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unhandled async IIFE rejection may not fail the CI step.

The top-level async IIFE has no .catch(). If any await throws, the promise rejects silently. Depending on the Node.js version, this may or may not exit with a non-zero code (older Node versions only warn). Add a .catch() to ensure a non-zero exit.

Proposed fix
-(async () => {
+(async () => {
     // ... body unchanged ...
-})();
+})().catch((e) => {
+    console.error(e);
+    process.exit(1);
+});
🤖 Prompt for AI Agents
In `@convert/fetchTags.ts` around lines 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.

Comment on lines +6 to +12
const tokenJSON = await fetch(
'https://ghcr.io/token?scope=repository:sillsdev/app-builders:pull'
).then((r) => r.json());
const token = tokenJSON.token;
const tagJSON = await fetch('https://ghcr.io/v2/sillsdev/app-builders/tags/list', {
headers: { Authorization: `Bearer ${token}` }
}).then((r) => r.json());
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

No error handling on HTTP responses — failures will silently corrupt CI output.

If either fetch returns a non-2xx status (e.g., rate-limited, auth failure), .json() will either throw or return unexpected data. The script would then silently output appbuilder_version=undefined, and the workflow would continue with a bogus version string instead of failing the step. Per the fail-fast convention for convert/ files, check response.ok and throw on failure.

Proposed fix
-    const tokenJSON = await fetch(
-        'https://ghcr.io/token?scope=repository:sillsdev/app-builders:pull'
-    ).then((r) => r.json());
-    const token = tokenJSON.token;
-    const tagJSON = await fetch('https://ghcr.io/v2/sillsdev/app-builders/tags/list', {
-        headers: { Authorization: `Bearer ${token}` }
-    }).then((r) => r.json());
+    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}`);
+    const tokenJSON = await tokenRes.json();
+    const token = tokenJSON.token;
+
+    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}`);
+    const tagJSON = await tagRes.json();

Based on learnings: for conversion modules under convert/, adopt a fail-fast approach and allow runtime errors when data is invalid, rather than silently handling or recovering.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const tokenJSON = await fetch(
'https://ghcr.io/token?scope=repository:sillsdev/app-builders:pull'
).then((r) => r.json());
const token = tokenJSON.token;
const tagJSON = await fetch('https://ghcr.io/v2/sillsdev/app-builders/tags/list', {
headers: { Authorization: `Bearer ${token}` }
}).then((r) => r.json());
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}`);
const tokenJSON = await tokenRes.json();
const token = tokenJSON.token;
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}`);
const tagJSON = await tagRes.json();
🤖 Prompt for AI Agents
In `@convert/fetchTags.ts` around lines 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.

Comment on lines +16 to +21
const latest = tags
.filter((t) => t.match(verNumRegExp))
.sort(compareVersions)
.at(-1);

console.log(`appbuilder_version=${latest}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

latest can be undefined, producing appbuilder_version=undefined in CI.

If no tags match the filter (or the tags array is empty), .at(-1) returns undefined. The workflow step would silently set appbuilder_version=undefined and continue. Add a guard to fail explicitly.

Proposed fix
     const latest = tags
         .filter((t) => t.match(verNumRegExp))
         .sort(compareVersions)
         .at(-1);
 
+    if (!latest) {
+        throw new Error('No version tags found');
+    }
     console.log(`appbuilder_version=${latest}`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const latest = tags
.filter((t) => t.match(verNumRegExp))
.sort(compareVersions)
.at(-1);
console.log(`appbuilder_version=${latest}`);
const latest = tags
.filter((t) => t.match(verNumRegExp))
.sort(compareVersions)
.at(-1);
if (!latest) {
throw new Error('No version tags found');
}
console.log(`appbuilder_version=${latest}`);
🤖 Prompt for AI Agents
In `@convert/fetchTags.ts` around lines 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.

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