Skip to content
This repository was archived by the owner on Sep 11, 2025. It is now read-only.

Commit c2a1295

Browse files
chore: replace GitHub API calls with git ls-remote in release info script (#819)
1 parent 5bbca81 commit c2a1295

File tree

1 file changed

+78
-48
lines changed

1 file changed

+78
-48
lines changed

tools/release-info/versioninfo.ts

Lines changed: 78 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import semver from "semver";
1111
import * as globals from "./constants.js";
12+
import { execSync } from "child_process";
1213

1314
export function isPrerelease(version: string): boolean {
1415
if (version.startsWith("v")) {
@@ -77,70 +78,99 @@ async function getAllVersions(owner: string, repo: string, prefix: string, inclu
7778
}
7879
}
7980

80-
const headers = {
81-
Accept: "application/vnd.github.v3+json",
82-
"X-GitHub-Api-Version": "2022-11-28",
83-
"User-Agent": "Modus CLI",
84-
};
81+
function execGitCommand(command: string): string {
82+
try {
83+
return execSync(command, {
84+
encoding: "utf8",
85+
timeout: 10000,
86+
}).trim();
87+
} catch (error) {
88+
console.error(`Error executing git command: ${command}`, error);
89+
throw error;
90+
}
91+
}
8592

86-
async function findLatestReleaseTag(owner: string, repo: string, prefix: string, includePrerelease: boolean): Promise<string | undefined> {
87-
let page = 1;
88-
while (true) {
89-
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases?page=${page}`, { headers });
93+
// Parse git ls-remote output, where each line is of the form
94+
// <commit-hash>\trefs/tags/<tag-name>
95+
function parseGitTagsOutput(output: string, normalizedPrefix: string): string[] {
96+
if (!output) {
97+
return [];
98+
}
9099

91-
if (!response.ok) {
92-
throw new Error(`Error fetching releases: ${response.statusText}`);
93-
}
100+
return output
101+
.split("\n")
102+
.map((line) => {
103+
const parts = line.split("\t");
104+
if (parts.length !== 2) return null;
94105

95-
const releases = await response.json();
96-
if (releases.length === 0) {
97-
return;
98-
}
106+
const tagName = parts[1].replace("refs/tags/", "");
99107

100-
for (const release of releases) {
101-
if (!includePrerelease && release.prerelease) {
102-
continue;
108+
if (!tagName.startsWith(normalizedPrefix)) {
109+
return null;
103110
}
104111

105-
if (prefix && !release.tag_name.startsWith(prefix)) {
106-
continue;
107-
}
112+
return tagName;
113+
})
114+
.filter((tag) => tag !== null) as string[];
115+
}
108116

109-
return release.tag_name;
110-
}
117+
async function findLatestReleaseTag(owner: string, repo: string, prefix: string, includePrerelease: boolean): Promise<string | undefined> {
118+
const repoUrl = `https://github.com/${owner}/${repo}`;
119+
const normalizedPrefix = prefix.endsWith("/") ? prefix : prefix + "/";
120+
const pattern = `refs/tags/${normalizedPrefix}*`;
111121

112-
page++;
113-
}
114-
}
122+
const command = `git ls-remote --refs ${repoUrl} ${pattern}`;
123+
const output = execGitCommand(command);
115124

116-
async function getAllReleaseTags(owner: string, repo: string, prefix: string, includePrerelease: boolean): Promise<string[]> {
117-
const results: string[] = [];
125+
const tags = parseGitTagsOutput(output, normalizedPrefix);
126+
if (tags.length === 0) {
127+
return undefined;
128+
}
118129

119-
let page = 1;
120-
while (true) {
121-
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases?per_page=100&page=${page}`, { headers });
130+
let filteredTags = tags;
131+
if (!includePrerelease) {
132+
const stableVersions = tags.filter((tag) => {
133+
const version = tag.slice(normalizedPrefix.length);
134+
return !isPrerelease(version);
135+
});
122136

123-
if (!response.ok) {
124-
throw new Error(`Error fetching releases: ${response.statusText}`);
137+
if (stableVersions.length > 0) {
138+
filteredTags = stableVersions;
125139
}
140+
}
126141

127-
const releases = await response.json();
128-
if (releases.length === 0) {
129-
return results;
130-
}
142+
const versions = filteredTags.map((tag) => {
143+
const version = tag.slice(normalizedPrefix.length);
144+
return version.startsWith("v") ? version.slice(1) : version;
145+
});
131146

132-
for (const release of releases) {
133-
if (!includePrerelease && release.prerelease) {
134-
continue;
135-
}
147+
if (versions.length === 0) {
148+
return undefined;
149+
}
136150

137-
if (prefix && !release.tag_name.startsWith(prefix)) {
138-
continue;
139-
}
151+
const latestVersion = semver.rsort(versions)[0];
152+
return normalizedPrefix + (latestVersion.startsWith("v") ? latestVersion : "v" + latestVersion);
153+
}
140154

141-
results.push(release.tag_name);
142-
}
155+
async function getAllReleaseTags(owner: string, repo: string, prefix: string, includePrerelease: boolean): Promise<string[]> {
156+
const repoUrl = `https://github.com/${owner}/${repo}`;
157+
const normalizedPrefix = prefix.endsWith("/") ? prefix : prefix + "/";
158+
const pattern = `refs/tags/${normalizedPrefix}*`;
159+
160+
const command = `git ls-remote --refs ${repoUrl} ${pattern}`;
161+
const output = execGitCommand(command);
162+
163+
const tags = parseGitTagsOutput(output, normalizedPrefix);
164+
if (!includePrerelease) {
165+
const stableVersions = tags.filter((tag) => {
166+
const version = tag.slice(normalizedPrefix.length);
167+
return !isPrerelease(version);
168+
});
143169

144-
page++;
170+
if (stableVersions.length > 0) {
171+
return stableVersions;
172+
}
145173
}
174+
175+
return tags;
146176
}

0 commit comments

Comments
 (0)