gitlab-badge-proxy 0.1.3 → 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;AAyWpB,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/
@@ -166,6 +187,18 @@ function renderDebugPage(project, ref, origin) {
166
187
  { label: "pipeline", src: `/${project}/badges/${ref}/pipeline.svg` },
167
188
  { label: "coverage", src: `/${project}/badges/${ref}/coverage.svg` },
168
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` },
169
202
  ];
170
203
  const nativeDefaultBadges = [
171
204
  { label: "pipeline", src: "/pipeline.svg" },
@@ -286,6 +319,54 @@ async function resolveReleaseSpec(config, projectPath) {
286
319
  return releaseToBadge(release);
287
320
  });
288
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
+ }
289
370
  /** Registers a flat `/<path>.svg` route fixed to the configured default project (and ref, where relevant). */
290
371
  function registerBadgeRoute(app, getConfig, path, fallback, context, compute) {
291
372
  app.get(path, async (c) => {
@@ -353,40 +434,18 @@ export function createApp(getConfig = loadConfig) {
353
434
  const { spec, maxAge } = await resolveReleaseSpec(config, config.project);
354
435
  return svgResponse(await fetchBadgeSvg(spec), maxAge);
355
436
  });
356
- registerBadgeRoute(app, getConfig, "/stars.svg", UNKNOWN, "stars", async (config) => starsToBadge(await getStarsCount(config, config.project)));
357
- registerBadgeRoute(app, getConfig, "/forks.svg", UNKNOWN, "forks", async (config) => forksToBadge(await getForksCount(config, config.project)));
358
- registerBadgeRoute(app, getConfig, "/contributors.svg", UNKNOWN, "contributors", async (config) => contributorsToBadge(await getContributorsCount(config, config.project)));
359
- registerBadgeRoute(app, getConfig, "/issues.svg", UNKNOWN, "issues", async (config, c) => {
360
- const state = c.req.query("state") ?? "all";
361
- const labels = c.req.query("labels");
362
- return issuesToBadge(await getIssuesCount(config, config.project, state, labels), state, labels);
363
- });
364
- registerBadgeRoute(app, getConfig, "/merge-requests.svg", UNKNOWN, "merge requests", async (config, c) => {
365
- const state = c.req.query("state") ?? "opened";
366
- const labels = c.req.query("labels");
367
- return mergeRequestsToBadge(await getMergeRequestsCount(config, config.project, state, labels), state, labels);
368
- });
369
- registerBadgeRoute(app, getConfig, "/last-commit.svg", UNKNOWN, "last commit", async (config, c) => {
370
- const ref = c.req.query("ref") ?? config.ref;
371
- const path = c.req.query("path");
372
- return lastCommitToBadge(await getLastCommitDate(config, config.project, ref, path));
373
- });
374
- registerBadgeRoute(app, getConfig, "/license.svg", UNKNOWN, "license", async (config) => licenseToBadge(await getLicenseName(config, config.project)));
375
- registerBadgeRoute(app, getConfig, "/tag.svg", UNKNOWN, "tag", async (config) => tagToBadge(await getLatestTag(config, config.project)));
376
- registerBadgeRoute(app, getConfig, "/languages.svg", UNKNOWN, "top language", async (config) => topLanguageToBadge(await getLanguages(config, config.project)));
377
- registerBadgeRoute(app, getConfig, "/languages-count.svg", UNKNOWN, "language count", async (config) => languagesCountToBadge(await getLanguages(config, config.project)));
378
- registerBadgeRoute(app, getConfig, "/labels.svg", UNKNOWN, "label", async (config, c) => {
379
- const name = c.req.query("name");
380
- if (!name) {
381
- throw new Error("missing required ?name= query param");
382
- }
383
- return labelToBadge(name, await getLabelColor(config, config.project, name));
384
- });
385
- registerBadgeRoute(app, getConfig, "/go-mod.svg", UNKNOWN, "go.mod version", async (config, c) => {
386
- const ref = c.req.query("ref") ?? config.ref;
387
- const filename = c.req.query("filename") ?? "go.mod";
388
- return goModVersionToBadge(await getGoModVersion(config, config.project, ref, filename));
389
- });
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));
390
449
  // Visual test page: every native and shields.io badge shape this proxy can produce, for one
391
450
  // project/ref given in the URL. /debug alone falls back to the fixed default project.
392
451
  app.get("/debug", (c) => {
@@ -459,26 +518,43 @@ export function createApp(getConfig = loadConfig) {
459
518
  return svgResponse(await fetchBadgeSvg(spec), maxAge);
460
519
  }
461
520
  const match = BADGE_PATH_PATTERN.exec(rest);
462
- if (!match) {
463
- return c.notFound();
464
- }
465
- const [, projectPath, ref, kind] = match;
466
- let config;
467
- try {
468
- config = getConfig();
469
- }
470
- catch (error) {
471
- console.error("gitlab-badge-proxy: failed to load config", error);
472
- const fallback = kind === "pipeline" ? UNKNOWN_PIPELINE : UNKNOWN_COVERAGE;
473
- 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);
474
539
  }
475
- if (!config.allowedProjects.includes(projectPath)) {
476
- 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);
477
556
  }
478
- const { spec, maxAge } = kind === "pipeline"
479
- ? await resolvePipelineSpec(config, projectPath, ref)
480
- : await resolveCoverageSpec(config, projectPath, ref, c.req.query("job"));
481
- return svgResponse(await fetchBadgeSvg(spec), maxAge);
557
+ return c.notFound();
482
558
  });
483
559
  return app;
484
560
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitlab-badge-proxy",
3
- "version": "0.1.3",
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",