@grimoire-rs/indexer 0.3.2 → 0.4.0

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.
Files changed (89) hide show
  1. package/README.md +137 -2
  2. package/dist/ci.d.ts +6 -1
  3. package/dist/ci.d.ts.map +1 -1
  4. package/dist/ci.js +32 -1
  5. package/dist/ci.js.map +1 -1
  6. package/dist/cli/build.d.ts.map +1 -1
  7. package/dist/cli/build.js +3 -1
  8. package/dist/cli/build.js.map +1 -1
  9. package/dist/cli/ci.d.ts.map +1 -1
  10. package/dist/cli/ci.js +5 -1
  11. package/dist/cli/ci.js.map +1 -1
  12. package/dist/cli/init.d.ts.map +1 -1
  13. package/dist/cli/init.js +17 -3
  14. package/dist/cli/init.js.map +1 -1
  15. package/dist/cli/main.d.ts.map +1 -1
  16. package/dist/cli/main.js +8 -0
  17. package/dist/cli/main.js.map +1 -1
  18. package/dist/cli/ratings.d.ts +3 -0
  19. package/dist/cli/ratings.d.ts.map +1 -0
  20. package/dist/cli/ratings.js +160 -0
  21. package/dist/cli/ratings.js.map +1 -0
  22. package/dist/config.d.ts +19 -0
  23. package/dist/config.d.ts.map +1 -1
  24. package/dist/config.js +47 -1
  25. package/dist/config.js.map +1 -1
  26. package/dist/enrich/index.d.ts.map +1 -1
  27. package/dist/enrich/index.js +74 -1
  28. package/dist/enrich/index.js.map +1 -1
  29. package/dist/ratings/budget.d.ts +16 -0
  30. package/dist/ratings/budget.d.ts.map +1 -0
  31. package/dist/ratings/budget.js +7 -0
  32. package/dist/ratings/budget.js.map +1 -0
  33. package/dist/ratings/config.d.ts +56 -0
  34. package/dist/ratings/config.d.ts.map +1 -0
  35. package/dist/ratings/config.js +86 -0
  36. package/dist/ratings/config.js.map +1 -0
  37. package/dist/ratings/marker.d.ts +105 -0
  38. package/dist/ratings/marker.d.ts.map +1 -0
  39. package/dist/ratings/marker.js +141 -0
  40. package/dist/ratings/marker.js.map +1 -0
  41. package/dist/ratings/provider.d.ts +108 -0
  42. package/dist/ratings/provider.d.ts.map +1 -0
  43. package/dist/ratings/provider.js +202 -0
  44. package/dist/ratings/provider.js.map +1 -0
  45. package/dist/ratings/provider_github.d.ts +3 -0
  46. package/dist/ratings/provider_github.d.ts.map +1 -0
  47. package/dist/ratings/provider_github.js +134 -0
  48. package/dist/ratings/provider_github.js.map +1 -0
  49. package/dist/ratings/provider_gitlab.d.ts +3 -0
  50. package/dist/ratings/provider_gitlab.d.ts.map +1 -0
  51. package/dist/ratings/provider_gitlab.js +152 -0
  52. package/dist/ratings/provider_gitlab.js.map +1 -0
  53. package/dist/ratings/provider_memory.d.ts +28 -0
  54. package/dist/ratings/provider_memory.d.ts.map +1 -0
  55. package/dist/ratings/provider_memory.js +49 -0
  56. package/dist/ratings/provider_memory.js.map +1 -0
  57. package/dist/ratings/reconcile.d.ts +47 -0
  58. package/dist/ratings/reconcile.d.ts.map +1 -0
  59. package/dist/ratings/reconcile.js +133 -0
  60. package/dist/ratings/reconcile.js.map +1 -0
  61. package/dist/ratings/seed.d.ts +60 -0
  62. package/dist/ratings/seed.d.ts.map +1 -0
  63. package/dist/ratings/seed.js +132 -0
  64. package/dist/ratings/seed.js.map +1 -0
  65. package/dist/renderer/astro/components/Catalog.d.ts +2 -0
  66. package/dist/renderer/astro/components/Catalog.js +96 -28
  67. package/dist/renderer/astro/components/Catalog.tsx +136 -32
  68. package/dist/renderer/astro/layouts/Base.astro +15 -5
  69. package/dist/renderer/astro/lib/catalog.d.ts +25 -0
  70. package/dist/renderer/astro/lib/catalog.js +46 -0
  71. package/dist/renderer/astro/lib/catalog.ts +49 -0
  72. package/dist/renderer/astro/pages/p/[...slug].astro +146 -3
  73. package/dist/renderer/index.d.ts.map +1 -1
  74. package/dist/renderer/index.js +101 -2
  75. package/dist/renderer/index.js.map +1 -1
  76. package/dist/renderer/types.d.ts +41 -0
  77. package/dist/renderer/types.d.ts.map +1 -1
  78. package/dist/validate/adapters/http.d.ts +6 -1
  79. package/dist/validate/adapters/http.d.ts.map +1 -1
  80. package/dist/validate/adapters/http.js +5 -1
  81. package/dist/validate/adapters/http.js.map +1 -1
  82. package/package.json +2 -1
  83. package/templates/ci/github-pages.yml +11 -5
  84. package/templates/ci/github-ratings-seed.yml +88 -0
  85. package/templates/ci/github-ratings.yml +51 -0
  86. package/templates/ci/gitlab-ci.yml +3 -3
  87. package/templates/ci/gitlab-ratings-seed.sh +83 -0
  88. package/templates/ci/gitlab-ratings.yml +37 -0
  89. package/templates/gitignore +8 -0
@@ -15,7 +15,12 @@ export interface HttpResponse {
15
15
  body: string;
16
16
  header(name: string): string;
17
17
  }
18
- export declare function request(url: string, headers?: Record<string, string>): Promise<HttpResponse>;
18
+ /** Everything past a plain GET. Omitted leaves `fetch`'s own defaults. */
19
+ export interface RequestOptions {
20
+ method?: string;
21
+ body?: string;
22
+ }
23
+ export declare function request(url: string, headers?: Record<string, string>, options?: RequestOptions): Promise<HttpResponse>;
19
24
  /** `request` plus a `JSON.parse` that yields `null` instead of throwing. */
20
25
  export declare function requestJson(url: string, headers?: Record<string, string>): Promise<{
21
26
  status: number;
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../../src/validate/adapters/http.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AAEH,eAAO,MAAM,UAAU,qBAAqB,CAAC;AAC7C,eAAO,MAAM,UAAU,QAAS,CAAC;AACjC,eAAO,MAAM,kBAAkB,QAAc,CAAC;AAE9C,MAAM,WAAW,YAAY;IAC3B,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;CAC9B;AA4BD,wBAAsB,OAAO,CAC3B,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GACnC,OAAO,CAAC,YAAY,CAAC,CAoBvB;AAED,4EAA4E;AAC5E,wBAAsB,WAAW,CAC/B,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GACnC,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC,CAQ5C"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../../src/validate/adapters/http.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AAEH,eAAO,MAAM,UAAU,qBAAqB,CAAC;AAC7C,eAAO,MAAM,UAAU,QAAS,CAAC;AACjC,eAAO,MAAM,kBAAkB,QAAc,CAAC;AAE9C,MAAM,WAAW,YAAY;IAC3B,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;CAC9B;AA4BD,0EAA0E;AAC1E,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wBAAsB,OAAO,CAC3B,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,EACpC,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,YAAY,CAAC,CAwBvB;AAED,4EAA4E;AAC5E,wBAAsB,WAAW,CAC/B,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GACnC,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC,CAQ5C"}
@@ -37,11 +37,15 @@ async function readCapped(response, cap) {
37
37
  }
38
38
  return Buffer.concat(chunks).toString("utf8");
39
39
  }
40
- export async function request(url, headers = {}) {
40
+ export async function request(url, headers = {}, options = {}) {
41
41
  const none = { status: 0, body: "", header: () => "" };
42
42
  try {
43
43
  const response = await fetch(url, {
44
44
  headers: { "User-Agent": USER_AGENT, ...headers },
45
+ // Both `undefined` for a two-argument call, which is what `fetch` sees
46
+ // when neither is passed — the existing GET call sites are untouched.
47
+ method: options.method,
48
+ body: options.body,
45
49
  // ponytail: redirects are refused, not followed. If a real registry ever
46
50
  // needs one, re-run the host policy on the Location and follow one hop.
47
51
  redirect: "manual",
@@ -1 +1 @@
1
- {"version":3,"file":"http.js","sourceRoot":"","sources":["../../../src/validate/adapters/http.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../../../src/validate/adapters/http.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grimoire-rs/indexer",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "CLI + Astro integration for running your own grim package index",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -38,6 +38,7 @@
38
38
  "@vitest/coverage-v8": "^4.1.10",
39
39
  "eslint": "^10.8.0",
40
40
  "js-yaml": "^4.1.0",
41
+ "jsdom": "^29.1.1",
41
42
  "typescript": "^6.0.3",
42
43
  "typescript-eslint": "^8.65.0",
43
44
  "vitest": "^4.1.10"
@@ -7,7 +7,7 @@ name: pages
7
7
 
8
8
  on:
9
9
  push:
10
- branches: [{{defaultBranch}}]
10
+ branches: [{{defaultBranch}}]{{githubRatingsSchedule}}
11
11
  workflow_dispatch:
12
12
 
13
13
  # Default-deny at the top; every job pays for exactly what it needs.
@@ -19,7 +19,7 @@ concurrency:
19
19
 
20
20
  jobs:
21
21
  build:
22
- runs-on: ubuntu-latest
22
+ runs-on: ubuntu-latest{{githubRatingsBuildKeys}}
23
23
  permissions:
24
24
  contents: read
25
25
  steps:
@@ -40,12 +40,17 @@ jobs:
40
40
  # here, in the job that publishes the site.
41
41
  - name: Install
42
42
  run: npm ci --ignore-scripts
43
- {{githubEnrichSteps}}
43
+
44
+ # Ahead of the build, not after it: it only resolves this repository's
45
+ # Pages URL and sets outputs, and a step that runs before the build - the
46
+ # ratings seed - cross-checks `site` against that URL before it reads the
47
+ # published sidecar.
48
+ - id: pages
49
+ uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
50
+ {{githubEnrichSteps}}{{githubRatingsSeedSteps}}
44
51
  - name: Build index
45
52
  run: npm run build
46
53
 
47
- - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
48
-
49
54
  - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
50
55
  with:
51
56
  path: dist
@@ -62,3 +67,4 @@ jobs:
62
67
  steps:
63
68
  - id: deployment
64
69
  uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
70
+ {{githubRatingsJob}}
@@ -0,0 +1,88 @@
1
+
2
+ # Invariant R-2 lives in these two steps and in their order.
3
+ #
4
+ # A completed tally has already merged itself over this same published
5
+ # copy, per stat key, so it wins outright. Only when there is no fresh
6
+ # tally does the published copy get carried forward untouched - which is
7
+ # exactly what must happen when the `ratings` job failed, was skipped, or
8
+ # was never enabled on this run.
9
+ - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
10
+ # A failed or skipped tally produced no artifact. That must not fail
11
+ # the deploy: carrying the published sidecar forward is the whole point.
12
+ continue-on-error: true
13
+ with:
14
+ name: grim-stats
15
+
16
+ # `|| true` is forbidden here. It cannot tell a genuine 404 from a DNS
17
+ # failure, a TLS error, a 5xx or a truncated body - and if the tally also
18
+ # failed, the deploy would then publish a site with no sidecar at all,
19
+ # the exact wipe R-2 exists to prevent. So the status code is captured
20
+ # and branched on: 404 is an empty seed, a 2xx that parses is the merge
21
+ # base, anything else fails the job.
22
+ - name: Seed from the published sidecar
23
+ env:
24
+ PAGES_URL: ${{ steps.pages.outputs.base_url }}
25
+ run: |
26
+ set -euo pipefail
27
+
28
+ # One source. The seed URL is `site` from index.config.json, read
29
+ # out of the checkout here - the same key, the same file and the
30
+ # same run as the tally that produced the artifact above. Not baked
31
+ # in at render time on purpose: a workflow rendered before `site`
32
+ # was last edited would seed from one URL while the tally read the
33
+ # other, which is the divergence this step exists to close.
34
+ SITE_URL=$(node -p 'JSON.parse(require("fs").readFileSync("index.config.json","utf8")).site ?? ""')
35
+ rstrip() { u=$1; while [ "${u%/}" != "$u" ]; do u=${u%/}; done; printf '%s' "$u"; }
36
+ SITE_URL=$(rstrip "$SITE_URL")
37
+ case "$SITE_URL" in
38
+ https://*) ;;
39
+ # Not defaulted: the built-in default names the first-party index,
40
+ # and seeding from someone else's published stats would merge
41
+ # their ratings into this one's sidecar. TLS because the fetched
42
+ # document decides every published rating - `--proto '=https'`
43
+ # below would refuse it anyway, in curl's words instead of these.
44
+ *) echo "::error::index.config.json needs an explicit https \`site\` - the seed is read from <site>/stats.json (got '$SITE_URL')"; exit 1 ;;
45
+ esac
46
+
47
+ # The Pages URL is a cross-check, never a second source: while it was
48
+ # the source, this step fetched one URL and the tally fetched the
49
+ # other, and a 404 there is a legal empty seed - so a disagreement
50
+ # published a sidecar built from nothing, at exit 0.
51
+ #
52
+ # Absent is not a disagreement: a fork, or a deploy that is not
53
+ # Pages, has no such URL, and `site` stands alone. Neither is a
54
+ # trailing slash, the scheme, or host case - hostnames are
55
+ # case-insensitive where the path is not. A real disagreement fails
56
+ # ahead of the freshness check below: the tally read `site` too, so
57
+ # a fresh artifact is no evidence that this URL is the right one.
58
+ lower() { u=${1#*://}; h=${u%%/*}; printf '%s%s' "${h,,}" "${u#"$h"}"; }
59
+ if [ -n "${PAGES_URL:-}" ] && [ "$(lower "$(rstrip "$PAGES_URL")")" != "$(lower "$SITE_URL")" ]; then
60
+ echo "::error::index.config.json publishes to $SITE_URL but this pipeline deploys $PAGES_URL - the seed would be read from one and the sidecar published to the other"
61
+ exit 1
62
+ fi
63
+
64
+ if [ -f .stats.json ]; then
65
+ echo "using the tally this run produced"
66
+ exit 0
67
+ fi
68
+ code=$(curl -sS --proto '=https' --tlsv1.2 --max-time 30 \
69
+ -o .stats.json.seed -w '%{http_code}' "$SITE_URL/stats.json") || {
70
+ echo "::error::could not read the published stats.json - refusing to deploy a site that would empty every published rating"
71
+ exit 1
72
+ }
73
+ case "$code" in
74
+ 404)
75
+ echo "no stats.json published yet - starting from an empty seed"
76
+ ;;
77
+ 2??)
78
+ node -e 'JSON.parse(require("fs").readFileSync(".stats.json.seed","utf8"))' || {
79
+ echo "::error::the published stats.json did not parse - refusing to treat an unreadable seed as an empty one"
80
+ exit 1
81
+ }
82
+ mv -f .stats.json.seed .stats.json
83
+ ;;
84
+ *)
85
+ echo "::error::the published stats.json returned HTTP $code"
86
+ exit 1
87
+ ;;
88
+ esac
@@ -0,0 +1,51 @@
1
+
2
+ # Tallies the forge's own upvote counters into `.stats.json` - the sidecar
3
+ # `build` joins onto the catalog and the deploy publishes beside all.json.
4
+ #
5
+ # Its own job, ahead of `build`, because `build` has to run even when this
6
+ # one fails. A tally that cannot reach the forge must never empty a published
7
+ # rating set (R-2), and the only way to keep the previous one is to deploy
8
+ # without a fresh tally - see the seed step in `build`.
9
+ #
10
+ # Turn ratings off by deleting the `ratings` block from index.config.json and
11
+ # re-rendering. That stops the tally, but the last one stays published:
12
+ # deleting `stats.json` from the deploy is the other half of the rollback.
13
+ ratings:
14
+ runs-on: ubuntu-latest
15
+ permissions:
16
+ contents: read
17
+ # The only write anything in this pipeline is granted, and it is what a
18
+ # vote is: threads live on the forge, nothing is committed here.
19
+ discussions: write
20
+ steps:
21
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
22
+ with:
23
+ persist-credentials: false
24
+
25
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
26
+ with:
27
+ node-version: "{{nodeVersion}}"
28
+ cache: npm
29
+
30
+ - name: Install
31
+ run: npm ci --ignore-scripts
32
+
33
+ # `npx --no` refuses to fetch anything `npm ci` did not install, so the
34
+ # renderer that tallies is the one package-lock.json pins - the same
35
+ # reviewed-fact rule every other job here follows. Spelled as `npx`
36
+ # rather than an npm script so an index scaffolded before ratings existed
37
+ # turns them on by re-rendering CI, with no package.json edit.
38
+ - name: Tally ratings
39
+ env:
40
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
41
+ run: npx --no grim-indexer ratings
42
+
43
+ # `error` rather than the default warning: a run that reported success
44
+ # and wrote nothing is a bug, and the failure mode it would otherwise
45
+ # produce - `build` silently falling back to the seed - looks exactly
46
+ # like a healthy carry-forward.
47
+ - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
48
+ with:
49
+ name: grim-stats
50
+ path: .stats.json
51
+ if-no-files-found: error
@@ -129,13 +129,13 @@ grim-indexer:validate:
129
129
  pages:
130
130
  stage: deploy
131
131
  rules:
132
- - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
132
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH{{gitlabRatingsPagesRule}}
133
133
  before_script:
134
134
  - npm ci --cache .npm --prefer-offline --ignore-scripts
135
135
  script:
136
136
  - |
137
137
  set -eu
138
- {{gitlabEnrichScript}}
138
+ {{gitlabEnrichScript}}{{gitlabRatingsSeedScript}}
139
139
  # Built into `dist/`, then moved. Building straight into `public/` was a
140
140
  # data-loss bug: the compile step clears its output directory first, so it
141
141
  # deleted this index's own committed asset layer (logo, favicon) before
@@ -146,4 +146,4 @@ pages:
146
146
  artifacts:
147
147
  paths:
148
148
  - public
149
- {{gitlabPublishJob}}
149
+ {{gitlabRatingsJob}}{{gitlabPublishJob}}
@@ -0,0 +1,83 @@
1
+
2
+ # Invariant R-2. GitLab hands this job the ratings artifact before the
3
+ # script runs, so `.stats.json` being present means a tally completed -
4
+ # and a completed tally has already merged itself over this same
5
+ # published copy, per stat key. Only when there is none does the
6
+ # published copy get carried forward untouched, which is what must happen
7
+ # when the tally failed or was skipped.
8
+ #
9
+ # `|| true` is forbidden here. It cannot tell a genuine 404 from a DNS
10
+ # failure, a TLS error, a 5xx or a truncated body - and if the tally also
11
+ # failed, the deploy would then publish a site with no sidecar at all,
12
+ # the exact wipe R-2 exists to prevent. So the status code is captured
13
+ # and branched on: 404 is an empty seed, a 2xx that parses is the merge
14
+ # base, anything else fails the job.
15
+ #
16
+ # One source. The seed URL is `site` from index.config.json, read out of
17
+ # the checkout here - the same key, the same file and the same run as
18
+ # the tally that produced the artifact this job was handed. Not baked in
19
+ # at render time on purpose: a pipeline rendered before `site` was last
20
+ # edited would seed from one URL while the tally read the other, which
21
+ # is the divergence this step exists to close.
22
+ #
23
+ # Deliberately NOT cross-checked against `CI_PAGES_URL`. That variable
24
+ # is always a subdomain of `CI_PAGES_DOMAIN` and never reflects a custom
25
+ # domain, and unique domains (16.7+) and `path_prefix` (17.9+) move it
26
+ # again - so it disagrees with a correct `site` as the normal case, and
27
+ # comparing them would fail every such deploy. The GitHub arm keeps its
28
+ # cross-check because `steps.pages.outputs.base_url` really is a
29
+ # statement about where the site deploys.
30
+ SITE_URL=$(node -p 'JSON.parse(require("fs").readFileSync("index.config.json","utf8")).site ?? ""')
31
+ rstrip() { u=$1; while [ "${u%/}" != "$u" ]; do u=${u%/}; done; printf '%s' "$u"; }
32
+ SITE_URL=$(rstrip "$SITE_URL")
33
+ case "$SITE_URL" in
34
+ https://*) ;;
35
+ # Not defaulted: the built-in default names the first-party index, and
36
+ # seeding from someone else's published stats would merge their
37
+ # ratings into this one's sidecar. TLS because the fetched document
38
+ # decides every published rating - `--proto '=https'` below would
39
+ # refuse it anyway, in curl's words instead of these.
40
+ *)
41
+ echo "grim-indexer: index.config.json needs an explicit https \`site\` - the seed is read from <site>/stats.json (got '$SITE_URL')" >&2
42
+ exit 1
43
+ ;;
44
+ esac
45
+
46
+ # `node:*-alpine` ships no curl, and the runner's clone helper is a
47
+ # separate image, so nothing else puts one here. Install it for whichever
48
+ # package manager this image has, then insist it is really there - a
49
+ # missing tool must fail the job, never silently skip the seed.
50
+ if [ ! -f .stats.json ]; then
51
+ if ! command -v curl >/dev/null 2>&1; then
52
+ if command -v apk >/dev/null 2>&1; then
53
+ apk add --no-cache curl
54
+ elif command -v apt-get >/dev/null 2>&1; then
55
+ apt-get update -qq && apt-get install -y -qq --no-install-recommends curl
56
+ fi
57
+ fi
58
+ command -v curl >/dev/null 2>&1 || {
59
+ echo "grim-indexer: seeding stats.json needs curl, and this image has none" >&2
60
+ exit 1
61
+ }
62
+ code=$(curl -sS --proto '=https' --tlsv1.2 --max-time 30 \
63
+ -o .stats.json.seed -w '%{http_code}' "$SITE_URL/stats.json") || {
64
+ echo "grim-indexer: could not read the published stats.json - refusing to deploy a site that would empty every published rating" >&2
65
+ exit 1
66
+ }
67
+ case "$code" in
68
+ 404)
69
+ echo "grim-indexer: no stats.json published yet - starting from an empty seed"
70
+ ;;
71
+ 2??)
72
+ node -e 'JSON.parse(require("fs").readFileSync(".stats.json.seed","utf8"))' || {
73
+ echo "grim-indexer: the published stats.json did not parse - refusing to treat an unreadable seed as an empty one" >&2
74
+ exit 1
75
+ }
76
+ mv -f .stats.json.seed .stats.json
77
+ ;;
78
+ *)
79
+ echo "grim-indexer: the published stats.json returned HTTP $code" >&2
80
+ exit 1
81
+ ;;
82
+ esac
83
+ fi
@@ -0,0 +1,37 @@
1
+
2
+ # Tallies the forge's own emoji reactions into `.stats.json` - the sidecar the
3
+ # `pages` job joins onto the catalog and publishes beside all.json. (GitLab
4
+ # renamed this feature Award Emoji to Emoji Reactions in 16.0; the GraphQL
5
+ # surface is unchanged.)
6
+ #
7
+ # Its own job, ahead of `pages`, because `pages` has to run even when this one
8
+ # fails. A tally that cannot reach GitLab must never empty a published rating
9
+ # set (R-2), and the only way to keep the previous one is to deploy without a
10
+ # fresh tally - hence `allow_failure`, which still reds the pipeline and still
11
+ # lets the deploy through, and hence `optional: true` on the `pages` job's
12
+ # `needs`.
13
+ #
14
+ # `resource_group` is a correctness requirement, not tuning: two overlapping
15
+ # runs can interleave a stale tally over a fresh one. It serializes them. Set
16
+ # the group's process mode to `oldest_first` once in Settings -> CI/CD, so a
17
+ # queued run cannot overtake the one ahead of it - that part has no YAML.
18
+ #
19
+ # The tally needs a token that may write reactions: set GRIM_RATINGS_TOKEN as a
20
+ # masked CI/CD variable (a project access token with `api` scope is enough,
21
+ # since the threads live in this same project). CI_JOB_TOKEN cannot do it.
22
+ grim-indexer:ratings:
23
+ stage: test
24
+ resource_group: grim-ratings
25
+ allow_failure: true
26
+ rules:
27
+ - if: $CI_PIPELINE_SOURCE == "schedule"
28
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
29
+ before_script:
30
+ - npm ci --cache .npm --prefer-offline --ignore-scripts
31
+ script:
32
+ # `npx --no` refuses to fetch anything `npm ci` did not install, so the
33
+ # renderer that tallies is the one package-lock.json pins.
34
+ - npx --no grim-indexer ratings
35
+ artifacts:
36
+ paths:
37
+ - .stats.json
@@ -8,6 +8,14 @@ dist/
8
8
  # Renderer scratch dir - removed on a clean build, left behind by a killed one.
9
9
  .index-*/
10
10
 
11
+ # The ratings sidecar. CI seeds it from the live site, the tally merges over
12
+ # it, and the deploy publishes it as `stats.json` - it is a build input, never
13
+ # a source of truth. Deliberately not committed: it is regenerated from the
14
+ # forge every run, so a committed copy is only ever a stale one, and it would
15
+ # keep being published after the `ratings` block was removed, defeating the
16
+ # rollback.
17
+ .stats.json
18
+
11
19
  node_modules/
12
20
  .npm/
13
21