Skip to content

Commit 5966695

Browse files
authored
Merge pull request #99 from stack127labs/research/traffic-growth-causes
Fix GitLab and GlitchTip star-count refresh
2 parents 79a10ea + fdd2165 commit 5966695

2 files changed

Lines changed: 59 additions & 5 deletions

File tree

‎scripts/self-host-apps/refresh-source-host.mjs‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@ async function fetchJson(url, token) {
4343
}
4444
}
4545

46-
async function optionalJson(url, token) {
46+
async function optionalJson(url, token, unavailableStatuses = [404]) {
4747
try {
4848
return await fetchJson(url, token);
4949
} catch (error) {
50-
if (error.status === 404) return null;
50+
if (unavailableStatuses.includes(error.status)) return null;
5151
throw error;
5252
}
5353
}
@@ -73,12 +73,15 @@ export async function gitlabFacts(row) {
7373
const origin = new URL(row.source_code_url).origin;
7474
const token = origin === "https://gitlab.com" ? process.env.GITLAB_TOKEN ?? "" : "";
7575
const base = `${origin}/api/v4/projects/${id}`;
76-
const [project, languages, release, commits] = await Promise.all([
76+
const [project, languages, releases, commits] = await Promise.all([
7777
fetchJson(`${base}?license=true`, token),
7878
optionalJson(`${base}/languages`, token),
79-
optionalJson(`${base}/releases/permalink/latest`, token),
79+
// The latest permalink redirects. Releases can also be disabled (403)
80+
// on a public project; neither should prevent refreshing its stars.
81+
optionalJson(`${base}/releases?per_page=1&order_by=released_at&sort=desc`, token, [403, 404]),
8082
optionalJson(`${base}/repository/commits?per_page=1`, token),
8183
]);
84+
const release = releases?.[0] ?? null;
8285
const lastCommitAt = commits?.[0]?.committed_date ?? commits?.[0]?.created_at ?? null;
8386
const license = normalizedLicense(
8487
project.license?.key ?? project.license?.spdx_identifier ?? project.license?.name,

‎tests/self-host-apps/refresh-automations.test.mjs‎

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ test("nested GitLab repositories use their own instance without the gitlab.com t
3030
license: { spdx_identifier: "MPL-2.0" },
3131
});
3232
if (url.endsWith("/languages")) return Response.json({ C: 100 });
33-
if (url.endsWith("/releases/permalink/latest")) return Response.json({}, { status: 404 });
33+
if (url.includes("/releases?")) return Response.json({}, { status: 404 });
3434
return Response.json([{ committed_date: "2026-10-31T00:00:00Z" }]);
3535
});
3636
const facts = await gitlabFacts({ source_code_url: "https://gitlab.torproject.org/tpo/applications/mullvad-browser.git/-/tree/main" });
@@ -48,6 +48,57 @@ test("nested GitLab repositories use their own instance without the gitlab.com t
4848
assert.ok(calls.every(({ options }) => options.headers.Authorization === "Bearer gitlab-com-test-token"));
4949
});
5050

51+
test("GitLab release list refreshes stars and the latest release without following redirects", async (t) => {
52+
const calls = [];
53+
t.mock.method(globalThis, "fetch", async (url, options) => {
54+
calls.push({ url, options });
55+
if (url.endsWith("?license=true")) return Response.json({ id: 1, star_count: 42 });
56+
if (url.endsWith("/languages")) return Response.json({ Python: 100 });
57+
if (url.endsWith("/releases?per_page=1&order_by=released_at&sort=desc")) {
58+
return Response.json([{ tag_name: "v6.0.0", released_at: "2026-11-01T00:00:00Z" }]);
59+
}
60+
if (url.endsWith("/repository/commits?per_page=1")) return Response.json([]);
61+
throw new Error(`Unexpected request: ${url}`);
62+
});
63+
const facts = await gitlabFacts({ source_code_url: "https://gitlab.com/glitchtip/glitchtip" });
64+
assert.equal(facts.source_stars, 42);
65+
assert.equal(facts.source_latest_release, "v6.0.0");
66+
assert.equal(facts.source_latest_release_at, "2026-11-01T00:00:00Z");
67+
assert.equal(facts.raw.release.tag_name, "v6.0.0");
68+
assert.ok(calls.every(({ options }) => options.redirect === "error"));
69+
});
70+
71+
for (const status of [200, 403, 404]) {
72+
test(`GitLab stars are written when releases are unavailable or empty (${status})`, async (t) => {
73+
t.mock.method(globalThis, "fetch", async (url) => {
74+
if (url.endsWith("?license=true")) return Response.json({ id: 1, star_count: 123 });
75+
if (url.endsWith("/languages")) return Response.json({ Ruby: 100 });
76+
if (url.includes("/releases?")) return Response.json(status === 200 ? [] : {}, { status });
77+
return Response.json([]);
78+
});
79+
const writes = [];
80+
const client = { async query(sql, values) {
81+
if (sql.includes("select id, slug")) return { rows: [{ id: "app-id", slug: "gitlab", source_code_url: "https://gitlab.com/gitlab-org/gitlab" }] };
82+
writes.push(values);
83+
return { rows: [] };
84+
} };
85+
const report = await refreshSourceHost(client, { host: "gitlab" });
86+
assert.equal(report.refreshed, 1);
87+
assert.equal(report.failed, 0);
88+
assert.equal(writes.length, 1);
89+
assert.equal(writes[0][3], 123);
90+
assert.equal(writes[0][19], null);
91+
});
92+
}
93+
94+
test("GitLab project permission errors still fail the refresh", async (t) => {
95+
t.mock.method(globalThis, "fetch", async (url) => {
96+
if (url.endsWith("?license=true")) return Response.json({}, { status: 403 });
97+
return Response.json([]);
98+
});
99+
await assert.rejects(gitlabFacts({ source_code_url: "https://gitlab.com/private/app" }), { status: 403 });
100+
});
101+
51102
test("catalog requests send a GitHub credential only to the GitHub API", async (t) => {
52103
env(t, "GITHUB_TOKEN", "github-test-token");
53104
const calls = [];

0 commit comments

Comments
 (0)