gitlab-badge-proxy 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -76,6 +76,18 @@ explicitly whitelisted. The ref segment is free-form (any branch of a whiteliste
76
76
  the fixed `GITLAB_REF` used by the flat `/pipeline.svg` / `/coverage.svg` routes above. The coverage
77
77
  route also accepts `?job=<job-name>`, same as the flat route.
78
78
 
79
+ ## Project-scoped badges beyond pipeline/coverage/release
80
+
81
+ The rest of the [Routes](#routes) above — stars, forks, contributors, issues, merge requests, last
82
+ commit, license, tag, languages, language count, labels, go.mod version — aren't part of GitLab's own
83
+ badge system, so there's no GitLab-native URL shape to mirror for them. This proxy has its own:
84
+ `GET /<group/project>/badges/<name>.svg`, e.g. `https://badges.yourdomain.tld/ardenslab/pm2-dash/badges/stars.svg`
85
+ or `.../badges/issues.svg?state=opened&labels=bug`. `<name>` is one of `stars`, `forks`,
86
+ `contributors`, `issues`, `merge-requests`, `last-commit`, `license`, `tag`, `languages`,
87
+ `languages-count`, `labels`, `go-mod` — same query params as the matching flat route (`labels.svg`
88
+ still needs `?name=`). Gated by the same `GITLAB_PROJECTS` whitelist as the GitLab-shaped routes
89
+ above; an unlisted project 404s the same way.
90
+
79
91
  ## Badge preview page
80
92
 
81
93
  `GET /debug/<group/project>?ref=<branch>` renders an HTML page with every badge this proxy can
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAgB,MAAM,MAAM,CAAC;AAE1C,OAAO,EAAc,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AAoBtD,OAAO,EACL,eAAe,EAkBhB,MAAM,YAAY,CAAC;AAuWpB,wBAAgB,SAAS,CAAC,SAAS,GAAE,MAAM,MAAmB,GAAG,IAAI,CAyNpE;AAED,OAAO,EAAE,eAAe,EAAE,CAAC;AAC3B,eAAO,MAAM,GAAG,4EAAc,CAAC"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAgB,MAAM,MAAM,CAAC;AAE1C,OAAO,EAAc,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AAoBtD,OAAO,EACL,eAAe,EAkBhB,MAAM,YAAY,CAAC;AAkcpB,wBAAgB,SAAS,CAAC,SAAS,GAAE,MAAM,MAAmB,GAAG,IAAI,CAoOpE;AAED,OAAO,EAAE,eAAe,EAAE,CAAC;AAC3B,eAAO,MAAM,GAAG,4EAAc,CAAC"}
package/dist/src/app.js CHANGED
@@ -12,6 +12,27 @@ const UNKNOWN = { label: "gitlab", message: "unknown", color: "lightgrey" };
12
12
  const BADGE_PATH_PATTERN = /^(.+)\/badges\/([^/]+)\/(pipeline|coverage)\.svg$/;
13
13
  /** Matches GitLab's latest-release badge shape: `<project path>/-/badges/release.svg` (no ref). */
14
14
  const RELEASE_PATH_PATTERN = /^(.+)\/-\/badges\/release\.svg$/;
15
+ /**
16
+ * This proxy's own badge shape (not one GitLab itself has): `<project path>/badges/<name>.svg`,
17
+ * for everything besides pipeline/coverage/release that isn't tied to a ref. Lets any whitelisted
18
+ * project use these the same way it already can use the GitLab-native ones above, instead of only
19
+ * the single fixed GITLAB_PROJECT getting them (via the flat /<name>.svg routes).
20
+ */
21
+ const SIMPLE_BADGE_NAMES = [
22
+ "stars",
23
+ "forks",
24
+ "contributors",
25
+ "issues",
26
+ "merge-requests",
27
+ "last-commit",
28
+ "license",
29
+ "tag",
30
+ "languages",
31
+ "languages-count",
32
+ "labels",
33
+ "go-mod",
34
+ ];
35
+ const SIMPLE_BADGE_PATH_PATTERN = new RegExp(`^(.+)\\/badges\\/(${SIMPLE_BADGE_NAMES.join("|")})\\.svg$`);
15
36
  /**
16
37
  * Known-safe `/api/v4/projects/:id/<subpath>` suffixes: just enough of the real GitLab REST API
17
38
  * for shields.io-style "point gitlab_url at a different host" badges (release/tag/pipeline/
@@ -98,6 +119,8 @@ async function proxyGitlabApi(config, projectPath, subpath, search) {
98
119
  const upstream = `${config.gitlabUrl}/api/v4/projects/${encodeURIComponent(projectPath)}${subpath ? `/${subpath}` : ""}${search}`;
99
120
  try {
100
121
  const response = await fetch(upstream, { headers: { "PRIVATE-TOKEN": config.gitlabToken } });
122
+ // Server-side only (stdout), never echoed back to the client.
123
+ console.log(`gitlab-badge-proxy: GET ${upstream} -> ${response.status}`);
101
124
  const text = await response.text();
102
125
  const maxAge = response.ok ? config.cacheMaxAgeSeconds : 30;
103
126
  let body = text;
@@ -164,6 +187,18 @@ function renderDebugPage(project, ref, origin) {
164
187
  { label: "pipeline", src: `/${project}/badges/${ref}/pipeline.svg` },
165
188
  { label: "coverage", src: `/${project}/badges/${ref}/coverage.svg` },
166
189
  { label: "release", src: `/${project}/-/badges/release.svg` },
190
+ { label: "stars", src: `/${project}/badges/stars.svg` },
191
+ { label: "forks", src: `/${project}/badges/forks.svg` },
192
+ { label: "contributors", src: `/${project}/badges/contributors.svg` },
193
+ { label: "issues", src: `/${project}/badges/issues.svg` },
194
+ { label: "merge requests", src: `/${project}/badges/merge-requests.svg` },
195
+ { label: "last commit", src: `/${project}/badges/last-commit.svg` },
196
+ { label: "license", src: `/${project}/badges/license.svg` },
197
+ { label: "tag", src: `/${project}/badges/tag.svg` },
198
+ { label: "top language", src: `/${project}/badges/languages.svg` },
199
+ { label: "language count", src: `/${project}/badges/languages-count.svg` },
200
+ { label: "label color (bug)", src: `/${project}/badges/labels.svg?name=bug` },
201
+ { label: "go.mod version", src: `/${project}/badges/go-mod.svg` },
167
202
  ];
168
203
  const nativeDefaultBadges = [
169
204
  { label: "pipeline", src: "/pipeline.svg" },
@@ -284,6 +319,54 @@ async function resolveReleaseSpec(config, projectPath) {
284
319
  return releaseToBadge(release);
285
320
  });
286
321
  }
322
+ /** Computes one of SIMPLE_BADGE_NAMES's badges for `projectPath` - shared by the flat `/<name>.svg`
323
+ * routes (always `config.project`) and the project-scoped `/:project/badges/<name>.svg` routes
324
+ * (any whitelisted project), so the two stay identical instead of duplicating this per route. */
325
+ async function computeSimpleBadge(config, projectPath, name, c) {
326
+ switch (name) {
327
+ case "stars":
328
+ return starsToBadge(await getStarsCount(config, projectPath));
329
+ case "forks":
330
+ return forksToBadge(await getForksCount(config, projectPath));
331
+ case "contributors":
332
+ return contributorsToBadge(await getContributorsCount(config, projectPath));
333
+ case "issues": {
334
+ const state = c.req.query("state") ?? "all";
335
+ const labels = c.req.query("labels");
336
+ return issuesToBadge(await getIssuesCount(config, projectPath, state, labels), state, labels);
337
+ }
338
+ case "merge-requests": {
339
+ const state = c.req.query("state") ?? "opened";
340
+ const labels = c.req.query("labels");
341
+ return mergeRequestsToBadge(await getMergeRequestsCount(config, projectPath, state, labels), state, labels);
342
+ }
343
+ case "last-commit": {
344
+ const ref = c.req.query("ref") ?? config.ref;
345
+ const path = c.req.query("path");
346
+ return lastCommitToBadge(await getLastCommitDate(config, projectPath, ref, path));
347
+ }
348
+ case "license":
349
+ return licenseToBadge(await getLicenseName(config, projectPath));
350
+ case "tag":
351
+ return tagToBadge(await getLatestTag(config, projectPath));
352
+ case "languages":
353
+ return topLanguageToBadge(await getLanguages(config, projectPath));
354
+ case "languages-count":
355
+ return languagesCountToBadge(await getLanguages(config, projectPath));
356
+ case "labels": {
357
+ const labelName = c.req.query("name");
358
+ if (!labelName) {
359
+ throw new Error("missing required ?name= query param");
360
+ }
361
+ return labelToBadge(labelName, await getLabelColor(config, projectPath, labelName));
362
+ }
363
+ case "go-mod": {
364
+ const ref = c.req.query("ref") ?? config.ref;
365
+ const filename = c.req.query("filename") ?? "go.mod";
366
+ return goModVersionToBadge(await getGoModVersion(config, projectPath, ref, filename));
367
+ }
368
+ }
369
+ }
287
370
  /** Registers a flat `/<path>.svg` route fixed to the configured default project (and ref, where relevant). */
288
371
  function registerBadgeRoute(app, getConfig, path, fallback, context, compute) {
289
372
  app.get(path, async (c) => {
@@ -351,40 +434,18 @@ export function createApp(getConfig = loadConfig) {
351
434
  const { spec, maxAge } = await resolveReleaseSpec(config, config.project);
352
435
  return svgResponse(await fetchBadgeSvg(spec), maxAge);
353
436
  });
354
- registerBadgeRoute(app, getConfig, "/stars.svg", UNKNOWN, "stars", async (config) => starsToBadge(await getStarsCount(config, config.project)));
355
- registerBadgeRoute(app, getConfig, "/forks.svg", UNKNOWN, "forks", async (config) => forksToBadge(await getForksCount(config, config.project)));
356
- registerBadgeRoute(app, getConfig, "/contributors.svg", UNKNOWN, "contributors", async (config) => contributorsToBadge(await getContributorsCount(config, config.project)));
357
- registerBadgeRoute(app, getConfig, "/issues.svg", UNKNOWN, "issues", async (config, c) => {
358
- const state = c.req.query("state") ?? "all";
359
- const labels = c.req.query("labels");
360
- return issuesToBadge(await getIssuesCount(config, config.project, state, labels), state, labels);
361
- });
362
- registerBadgeRoute(app, getConfig, "/merge-requests.svg", UNKNOWN, "merge requests", async (config, c) => {
363
- const state = c.req.query("state") ?? "opened";
364
- const labels = c.req.query("labels");
365
- return mergeRequestsToBadge(await getMergeRequestsCount(config, config.project, state, labels), state, labels);
366
- });
367
- registerBadgeRoute(app, getConfig, "/last-commit.svg", UNKNOWN, "last commit", async (config, c) => {
368
- const ref = c.req.query("ref") ?? config.ref;
369
- const path = c.req.query("path");
370
- return lastCommitToBadge(await getLastCommitDate(config, config.project, ref, path));
371
- });
372
- registerBadgeRoute(app, getConfig, "/license.svg", UNKNOWN, "license", async (config) => licenseToBadge(await getLicenseName(config, config.project)));
373
- registerBadgeRoute(app, getConfig, "/tag.svg", UNKNOWN, "tag", async (config) => tagToBadge(await getLatestTag(config, config.project)));
374
- registerBadgeRoute(app, getConfig, "/languages.svg", UNKNOWN, "top language", async (config) => topLanguageToBadge(await getLanguages(config, config.project)));
375
- registerBadgeRoute(app, getConfig, "/languages-count.svg", UNKNOWN, "language count", async (config) => languagesCountToBadge(await getLanguages(config, config.project)));
376
- registerBadgeRoute(app, getConfig, "/labels.svg", UNKNOWN, "label", async (config, c) => {
377
- const name = c.req.query("name");
378
- if (!name) {
379
- throw new Error("missing required ?name= query param");
380
- }
381
- return labelToBadge(name, await getLabelColor(config, config.project, name));
382
- });
383
- registerBadgeRoute(app, getConfig, "/go-mod.svg", UNKNOWN, "go.mod version", async (config, c) => {
384
- const ref = c.req.query("ref") ?? config.ref;
385
- const filename = c.req.query("filename") ?? "go.mod";
386
- return goModVersionToBadge(await getGoModVersion(config, config.project, ref, filename));
387
- });
437
+ registerBadgeRoute(app, getConfig, "/stars.svg", UNKNOWN, "stars", (config, c) => computeSimpleBadge(config, config.project, "stars", c));
438
+ registerBadgeRoute(app, getConfig, "/forks.svg", UNKNOWN, "forks", (config, c) => computeSimpleBadge(config, config.project, "forks", c));
439
+ registerBadgeRoute(app, getConfig, "/contributors.svg", UNKNOWN, "contributors", (config, c) => computeSimpleBadge(config, config.project, "contributors", c));
440
+ registerBadgeRoute(app, getConfig, "/issues.svg", UNKNOWN, "issues", (config, c) => computeSimpleBadge(config, config.project, "issues", c));
441
+ registerBadgeRoute(app, getConfig, "/merge-requests.svg", UNKNOWN, "merge requests", (config, c) => computeSimpleBadge(config, config.project, "merge-requests", c));
442
+ registerBadgeRoute(app, getConfig, "/last-commit.svg", UNKNOWN, "last commit", (config, c) => computeSimpleBadge(config, config.project, "last-commit", c));
443
+ registerBadgeRoute(app, getConfig, "/license.svg", UNKNOWN, "license", (config, c) => computeSimpleBadge(config, config.project, "license", c));
444
+ registerBadgeRoute(app, getConfig, "/tag.svg", UNKNOWN, "tag", (config, c) => computeSimpleBadge(config, config.project, "tag", c));
445
+ registerBadgeRoute(app, getConfig, "/languages.svg", UNKNOWN, "top language", (config, c) => computeSimpleBadge(config, config.project, "languages", c));
446
+ registerBadgeRoute(app, getConfig, "/languages-count.svg", UNKNOWN, "language count", (config, c) => computeSimpleBadge(config, config.project, "languages-count", c));
447
+ registerBadgeRoute(app, getConfig, "/labels.svg", UNKNOWN, "label", (config, c) => computeSimpleBadge(config, config.project, "labels", c));
448
+ registerBadgeRoute(app, getConfig, "/go-mod.svg", UNKNOWN, "go.mod version", (config, c) => computeSimpleBadge(config, config.project, "go-mod", c));
388
449
  // Visual test page: every native and shields.io badge shape this proxy can produce, for one
389
450
  // project/ref given in the URL. /debug alone falls back to the fixed default project.
390
451
  app.get("/debug", (c) => {
@@ -457,26 +518,43 @@ export function createApp(getConfig = loadConfig) {
457
518
  return svgResponse(await fetchBadgeSvg(spec), maxAge);
458
519
  }
459
520
  const match = BADGE_PATH_PATTERN.exec(rest);
460
- if (!match) {
461
- return c.notFound();
462
- }
463
- const [, projectPath, ref, kind] = match;
464
- let config;
465
- try {
466
- config = getConfig();
467
- }
468
- catch (error) {
469
- console.error("gitlab-badge-proxy: failed to load config", error);
470
- const fallback = kind === "pipeline" ? UNKNOWN_PIPELINE : UNKNOWN_COVERAGE;
471
- return svgResponse(await fetchBadgeSvg(fallback), 30);
521
+ if (match) {
522
+ const [, projectPath, ref, kind] = match;
523
+ let config;
524
+ try {
525
+ config = getConfig();
526
+ }
527
+ catch (error) {
528
+ console.error("gitlab-badge-proxy: failed to load config", error);
529
+ const fallback = kind === "pipeline" ? UNKNOWN_PIPELINE : UNKNOWN_COVERAGE;
530
+ return svgResponse(await fetchBadgeSvg(fallback), 30);
531
+ }
532
+ if (!config.allowedProjects.includes(projectPath)) {
533
+ return c.notFound();
534
+ }
535
+ const { spec, maxAge } = kind === "pipeline"
536
+ ? await resolvePipelineSpec(config, projectPath, ref)
537
+ : await resolveCoverageSpec(config, projectPath, ref, c.req.query("job"));
538
+ return svgResponse(await fetchBadgeSvg(spec), maxAge);
472
539
  }
473
- if (!config.allowedProjects.includes(projectPath)) {
474
- return c.notFound();
540
+ const simpleMatch = SIMPLE_BADGE_PATH_PATTERN.exec(rest);
541
+ if (simpleMatch) {
542
+ const [, projectPath, name] = simpleMatch;
543
+ let config;
544
+ try {
545
+ config = getConfig();
546
+ }
547
+ catch (error) {
548
+ console.error("gitlab-badge-proxy: failed to load config", error);
549
+ return svgResponse(await fetchBadgeSvg(UNKNOWN), 30);
550
+ }
551
+ if (!config.allowedProjects.includes(projectPath)) {
552
+ return c.notFound();
553
+ }
554
+ const { spec, maxAge } = await resolveSpec(config, UNKNOWN, name, () => computeSimpleBadge(config, projectPath, name, c));
555
+ return svgResponse(await fetchBadgeSvg(spec), maxAge);
475
556
  }
476
- const { spec, maxAge } = kind === "pipeline"
477
- ? await resolvePipelineSpec(config, projectPath, ref)
478
- : await resolveCoverageSpec(config, projectPath, ref, c.req.query("job"));
479
- return svgResponse(await fetchBadgeSvg(spec), maxAge);
557
+ return c.notFound();
480
558
  });
481
559
  return app;
482
560
  }
@@ -1 +1 @@
1
- {"version":3,"file":"gitlab.d.ts","sourceRoot":"","sources":["../../src/gitlab.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"gitlab.d.ts","sourceRoot":"","sources":["../../src/gitlab.ts"],"names":[],"mappings":"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"}
@@ -4,7 +4,16 @@ async function gitlabRequest(config, projectPath, path) {
4
4
  const response = await fetch(url, {
5
5
  headers: { "PRIVATE-TOKEN": config.gitlabToken },
6
6
  });
7
+ // Server-side only (stdout), never echoed back to the client - shows the exact upstream call this
8
+ // request triggered, for anyone watching the proxy's own logs to debug a GitLab-side issue.
9
+ console.log(`gitlab-badge-proxy: GET ${url} -> ${response.status}`);
7
10
  if (!response.ok) {
11
+ // GitLab's own error body (e.g. `{"message":"401 Unauthorized"}`) plus the token's length (never
12
+ // its value) are usually enough to tell "GITLAB_TOKEN isn't set" (length 0) apart from "it's set
13
+ // but wrong/expired/missing scope" (non-zero length, still 401/403) from the logs alone.
14
+ const body = await response.text().catch(() => "<unreadable response body>");
15
+ console.error(`gitlab-badge-proxy: GitLab API error ${response.status} for ${url} ` +
16
+ `(GITLAB_TOKEN length: ${config.gitlabToken.length}): ${body.slice(0, 300)}`);
8
17
  throw new Error(`GitLab API responded with ${response.status}`);
9
18
  }
10
19
  return response;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitlab-badge-proxy",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Server-side proxy that turns private GitLab pipeline/coverage status into public SVG badges, without ever exposing the GitLab instance's URL to clients.",
5
5
  "type": "module",
6
6
  "license": "MIT",