@grimoire-rs/indexer 0.1.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.
- package/LICENSE +201 -0
- package/README.md +48 -0
- package/dist/cli/build.d.ts +6 -0
- package/dist/cli/build.d.ts.map +1 -0
- package/dist/cli/build.js +29 -0
- package/dist/cli/build.js.map +1 -0
- package/dist/cli/exit.d.ts +28 -0
- package/dist/cli/exit.d.ts.map +1 -0
- package/dist/cli/exit.js +33 -0
- package/dist/cli/exit.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +8 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +40 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +317 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +112 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/validate.d.ts +18 -0
- package/dist/cli/validate.d.ts.map +1 -0
- package/dist/cli/validate.js +139 -0
- package/dist/cli/validate.js.map +1 -0
- package/dist/config.d.ts +76 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +134 -0
- package/dist/config.js.map +1 -0
- package/dist/data/index.d.ts +40 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/index.js +103 -0
- package/dist/data/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/integration.d.ts +2 -0
- package/dist/integration.d.ts.map +1 -0
- package/dist/integration.js +4 -0
- package/dist/integration.js.map +1 -0
- package/dist/renderer/astro/components/Catalog.d.ts +6 -0
- package/dist/renderer/astro/components/Catalog.d.ts.map +1 -0
- package/dist/renderer/astro/components/Catalog.js +73 -0
- package/dist/renderer/astro/components/Catalog.js.map +1 -0
- package/dist/renderer/astro/components/Catalog.tsx +256 -0
- package/dist/renderer/astro/components/CopyButton.astro +24 -0
- package/dist/renderer/astro/components/VersionMenu.astro +105 -0
- package/dist/renderer/astro/content.config.ts +30 -0
- package/dist/renderer/astro/layouts/Base.astro +623 -0
- package/dist/renderer/astro/lib/catalog.d.ts +7 -0
- package/dist/renderer/astro/lib/catalog.d.ts.map +1 -0
- package/dist/renderer/astro/lib/catalog.js +61 -0
- package/dist/renderer/astro/lib/catalog.js.map +1 -0
- package/dist/renderer/astro/lib/catalog.ts +69 -0
- package/dist/renderer/astro/lib/data.d.ts +3 -0
- package/dist/renderer/astro/lib/data.d.ts.map +1 -0
- package/dist/renderer/astro/lib/data.js +4 -0
- package/dist/renderer/astro/lib/data.js.map +1 -0
- package/dist/renderer/astro/lib/data.ts +12 -0
- package/dist/renderer/astro/pages/index.astro +69 -0
- package/dist/renderer/astro/pages/p/[...slug].astro +367 -0
- package/dist/renderer/index.d.ts +20 -0
- package/dist/renderer/index.d.ts.map +1 -0
- package/dist/renderer/index.js +192 -0
- package/dist/renderer/index.js.map +1 -0
- package/dist/renderer/public/favicon.svg +4 -0
- package/dist/renderer/types.d.ts +34 -0
- package/dist/renderer/types.d.ts.map +1 -0
- package/dist/renderer/types.js +4 -0
- package/dist/renderer/types.js.map +1 -0
- package/dist/validate/adapters/files.d.ts +17 -0
- package/dist/validate/adapters/files.d.ts.map +1 -0
- package/dist/validate/adapters/files.js +50 -0
- package/dist/validate/adapters/files.js.map +1 -0
- package/dist/validate/adapters/forge.d.ts +25 -0
- package/dist/validate/adapters/forge.d.ts.map +1 -0
- package/dist/validate/adapters/forge.js +100 -0
- package/dist/validate/adapters/forge.js.map +1 -0
- package/dist/validate/adapters/http.d.ts +24 -0
- package/dist/validate/adapters/http.d.ts.map +1 -0
- package/dist/validate/adapters/http.js +75 -0
- package/dist/validate/adapters/http.js.map +1 -0
- package/dist/validate/adapters/registry.d.ts +16 -0
- package/dist/validate/adapters/registry.d.ts.map +1 -0
- package/dist/validate/adapters/registry.js +91 -0
- package/dist/validate/adapters/registry.js.map +1 -0
- package/dist/validate/core/metadata.d.ts +67 -0
- package/dist/validate/core/metadata.d.ts.map +1 -0
- package/dist/validate/core/metadata.js +149 -0
- package/dist/validate/core/metadata.js.map +1 -0
- package/dist/validate/core/ownership.d.ts +43 -0
- package/dist/validate/core/ownership.d.ts.map +1 -0
- package/dist/validate/core/ownership.js +33 -0
- package/dist/validate/core/ownership.js.map +1 -0
- package/dist/validate/core/paths.d.ts +21 -0
- package/dist/validate/core/paths.d.ts.map +1 -0
- package/dist/validate/core/paths.js +34 -0
- package/dist/validate/core/paths.js.map +1 -0
- package/dist/validate/index.d.ts +40 -0
- package/dist/validate/index.d.ts.map +1 -0
- package/dist/validate/index.js +206 -0
- package/dist/validate/index.js.map +1 -0
- package/package.json +50 -0
- package/templates/README.md +48 -0
- package/templates/github-pages.yml +24 -0
- package/templates/github-validate.yml +36 -0
- package/templates/gitignore +9 -0
- package/templates/gitlab-ci.yml +13 -0
- package/templates/publish.toml +17 -0
- package/templates/reusable/gitlab-index-ci.yml +61 -0
- package/templates/reusable/index-pages.yml +80 -0
- package/templates/reusable/index-validate.yml +88 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 The Grimoire Authors
|
|
3
|
+
/**
|
|
4
|
+
* The index contribution gate.
|
|
5
|
+
*
|
|
6
|
+
* Runs from the TRUSTED base checkout (GitHub `pull_request_target`, or a
|
|
7
|
+
* GitLab pipeline with `scripts/` re-checked-out from the target branch). PR
|
|
8
|
+
* content is consumed as data only — never executed, never imported, never
|
|
9
|
+
* interpolated into a command.
|
|
10
|
+
*
|
|
11
|
+
* `eligible: true` means the change may be auto-merged. Anything else means a
|
|
12
|
+
* human looks at it: this gate never rejects a contribution, it only declines
|
|
13
|
+
* to wave it through. Callers map that to the exit contract — 0 for eligible,
|
|
14
|
+
* non-zero for manual review.
|
|
15
|
+
*
|
|
16
|
+
* Three separated concerns make auto-merging an untrusted PR safe:
|
|
17
|
+
* identity — the forge account that authored the PR; the forge proves it
|
|
18
|
+
* authorization — the *numeric* namespace owner id, never the login string
|
|
19
|
+
* verification — the OCI ref re-checked against registry truth, not trusted
|
|
20
|
+
*/
|
|
21
|
+
import { createForge } from "./adapters/forge.js";
|
|
22
|
+
import { readIndexFile } from "./adapters/files.js";
|
|
23
|
+
import { refReachable } from "./adapters/registry.js";
|
|
24
|
+
import { ownerLogin, parseMetadataJson, parseRef, registryHostReason, validateMetadata, } from "./core/metadata.js";
|
|
25
|
+
import { parseIndexPath } from "./core/paths.js";
|
|
26
|
+
import { botOwnsNamespace, findTrustedBot, namespaceOwned, } from "./core/ownership.js";
|
|
27
|
+
const env = (name) => process.env[name] || undefined;
|
|
28
|
+
function forgeHost(options) {
|
|
29
|
+
if (options.host)
|
|
30
|
+
return options.host;
|
|
31
|
+
return options.forge === "github" ? "github.com" : env("CI_SERVER_HOST");
|
|
32
|
+
}
|
|
33
|
+
function apiBase(options) {
|
|
34
|
+
if (options.apiBase)
|
|
35
|
+
return options.apiBase;
|
|
36
|
+
return options.forge === "github"
|
|
37
|
+
? env("GITHUB_API_URL") ?? "https://api.github.com"
|
|
38
|
+
: env("CI_API_V4_URL");
|
|
39
|
+
}
|
|
40
|
+
function apiToken(options) {
|
|
41
|
+
if (options.token)
|
|
42
|
+
return options.token;
|
|
43
|
+
return options.forge === "github"
|
|
44
|
+
? env("GH_TOKEN") ?? env("GITHUB_TOKEN")
|
|
45
|
+
: env("GRIM_INDEX_BOT_TOKEN");
|
|
46
|
+
}
|
|
47
|
+
function isHttps(url) {
|
|
48
|
+
try {
|
|
49
|
+
return new URL(url).protocol === "https:";
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export async function runValidate(options) {
|
|
56
|
+
try {
|
|
57
|
+
const reasons = await collectReasons(options);
|
|
58
|
+
return { eligible: reasons.length === 0, reasons };
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
// Any unexpected failure is a manual review, never an approval.
|
|
62
|
+
return {
|
|
63
|
+
eligible: false,
|
|
64
|
+
reasons: [`unexpected error: ${error.message}`],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function collectReasons(options) {
|
|
69
|
+
const host = forgeHost(options);
|
|
70
|
+
const api = apiBase(options);
|
|
71
|
+
if (!host)
|
|
72
|
+
return ["forge host is not configured"];
|
|
73
|
+
if (!api)
|
|
74
|
+
return ["forge API base URL is not configured"];
|
|
75
|
+
if (!isHttps(api))
|
|
76
|
+
return [`forge API base URL ${api} is not https`];
|
|
77
|
+
const changed = [...new Set(options.changedFiles.filter((file) => file !== ""))];
|
|
78
|
+
if (changed.length === 0)
|
|
79
|
+
return ["empty change set"];
|
|
80
|
+
// 1. Path gate. Nothing else runs until every path is in bounds — no forge
|
|
81
|
+
// call, no registry call, no read of PR content.
|
|
82
|
+
const paths = [];
|
|
83
|
+
const pathReasons = [];
|
|
84
|
+
for (const file of changed) {
|
|
85
|
+
const parsed = parseIndexPath(file, options.forge, host);
|
|
86
|
+
if (parsed === null) {
|
|
87
|
+
pathReasons.push(`${file}: outside index/${host}/<ns>/<pkg>/metadata.json — manual review`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
paths.push(parsed);
|
|
91
|
+
}
|
|
92
|
+
if (pathReasons.length > 0)
|
|
93
|
+
return pathReasons;
|
|
94
|
+
// 2. Namespace ownership, once per namespace.
|
|
95
|
+
const forge = createForge(options.forge, {
|
|
96
|
+
apiBase: api,
|
|
97
|
+
token: apiToken(options),
|
|
98
|
+
minAccessLevel: options.minAccessLevel,
|
|
99
|
+
});
|
|
100
|
+
const identities = new Map();
|
|
101
|
+
const ownershipReasons = [];
|
|
102
|
+
const reserved = new Set((options.reservedNamespaces ?? []).map((name) => name.toLowerCase()));
|
|
103
|
+
for (const namespace of [...new Set(paths.map((p) => p.namespace))].sort()) {
|
|
104
|
+
if (reserved.has(namespace.toLowerCase())) {
|
|
105
|
+
ownershipReasons.push(`namespace ${JSON.stringify(namespace)} is reserved`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const identity = await forge.namespaceIdentity(namespace);
|
|
109
|
+
if (identity === null) {
|
|
110
|
+
ownershipReasons.push(`namespace ${JSON.stringify(namespace)} does not exist on ${host}`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
identities.set(namespace, identity);
|
|
114
|
+
const bot = findTrustedBot(options.trustedBots, options.author.login);
|
|
115
|
+
const owned = bot !== null
|
|
116
|
+
? // A login matching a trusted bot is never re-checked as a human: an
|
|
117
|
+
// impostor on that login is refused outright, not given a second path.
|
|
118
|
+
botOwnsNamespace(bot, namespace, options.author.id)
|
|
119
|
+
: namespaceOwned(namespace, identity, options.author, identity.kind === "user"
|
|
120
|
+
? false
|
|
121
|
+
: await forge.isMember(namespace, options.author));
|
|
122
|
+
if (!owned) {
|
|
123
|
+
ownershipReasons.push(`namespace ${JSON.stringify(namespace)} is not owned by ${JSON.stringify(options.author.login)}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (ownershipReasons.length > 0)
|
|
127
|
+
return ownershipReasons;
|
|
128
|
+
// 3. Per-file content checks.
|
|
129
|
+
const reasons = [];
|
|
130
|
+
for (const path of paths) {
|
|
131
|
+
const reason = await checkFile(path, identities, options);
|
|
132
|
+
if (reason !== null)
|
|
133
|
+
reasons.push(`${path.file}: ${reason}`);
|
|
134
|
+
}
|
|
135
|
+
return reasons;
|
|
136
|
+
}
|
|
137
|
+
async function checkFile(path, identities, options) {
|
|
138
|
+
const read = await readIndexFile(options.prTree, path.file);
|
|
139
|
+
// A path with no file in the PR tree is a deletion by the namespace owner,
|
|
140
|
+
// whose ownership was already established above.
|
|
141
|
+
if (read.kind === "missing")
|
|
142
|
+
return null;
|
|
143
|
+
if (read.kind === "error")
|
|
144
|
+
return read.reason;
|
|
145
|
+
const parsed = parseMetadataJson(read.text);
|
|
146
|
+
if (!parsed.ok)
|
|
147
|
+
return parsed.reason;
|
|
148
|
+
const checked = validateMetadata(parsed.value, path.pkg);
|
|
149
|
+
if (!checked.ok)
|
|
150
|
+
return checked.reason;
|
|
151
|
+
const meta = checked.value;
|
|
152
|
+
const identity = identities.get(path.namespace);
|
|
153
|
+
if (identity === undefined)
|
|
154
|
+
return "namespace identity is unknown";
|
|
155
|
+
const login = ownerLogin(meta.owner);
|
|
156
|
+
if (login.toLowerCase() !== path.namespace.toLowerCase()) {
|
|
157
|
+
return `owner login ${JSON.stringify(login)} != namespace ${JSON.stringify(path.namespace)}`;
|
|
158
|
+
}
|
|
159
|
+
if (String(meta.owner.id) !== identity.id) {
|
|
160
|
+
return `owner.id ${JSON.stringify(meta.owner.id)} != ${options.forge} id ${identity.id}`;
|
|
161
|
+
}
|
|
162
|
+
const continuity = await committedOwnerReason(path, meta, options.root);
|
|
163
|
+
if (continuity !== null)
|
|
164
|
+
return continuity;
|
|
165
|
+
const ref = parseRef(meta.ref);
|
|
166
|
+
if (!ref.ok)
|
|
167
|
+
return ref.reason;
|
|
168
|
+
// Host policy before the request — the ref is attacker-chosen input.
|
|
169
|
+
const hostReason = registryHostReason(ref.value.host, options.registryHosts);
|
|
170
|
+
if (hostReason !== null)
|
|
171
|
+
return hostReason;
|
|
172
|
+
const reachable = await refReachable(ref.value.host, ref.value.repository, {
|
|
173
|
+
auth: options.registryAuth ?? env("GRIM_INDEX_REGISTRY_AUTH"),
|
|
174
|
+
allowlist: options.registryHosts,
|
|
175
|
+
});
|
|
176
|
+
if (!reachable) {
|
|
177
|
+
return `ref ${JSON.stringify(meta.ref)} has no reachable tags — publish first`;
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The committed entry is the authorization record: an existing package keeps
|
|
183
|
+
* the owner id it was published under. A namespace whose login was freed and
|
|
184
|
+
* re-registered resolves to a *different* account id on the forge, so this is
|
|
185
|
+
* what stops the new holder from inheriting the old holder's packages.
|
|
186
|
+
*/
|
|
187
|
+
async function committedOwnerReason(path, submitted, root) {
|
|
188
|
+
const read = await readIndexFile(root, path.file);
|
|
189
|
+
if (read.kind === "missing")
|
|
190
|
+
return null; // new package
|
|
191
|
+
if (read.kind === "error") {
|
|
192
|
+
return `cannot read the committed entry (${read.reason}) — manual review`;
|
|
193
|
+
}
|
|
194
|
+
const parsed = parseMetadataJson(read.text);
|
|
195
|
+
if (!parsed.ok)
|
|
196
|
+
return `committed entry is unreadable (${parsed.reason})`;
|
|
197
|
+
const committed = validateMetadata(parsed.value, path.pkg);
|
|
198
|
+
if (!committed.ok) {
|
|
199
|
+
return `committed entry is invalid (${committed.reason}) — manual review`;
|
|
200
|
+
}
|
|
201
|
+
if (committed.value.owner.id !== submitted.owner.id) {
|
|
202
|
+
return `owner.id ${JSON.stringify(submitted.owner.id)} != committed owner.id ${JSON.stringify(committed.value.owner.id)}`;
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/validate/index.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,sCAAsC;AAEtC;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,WAAW,EAA0B,MAAM,qBAAqB,CAAC;AAC1E,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EACL,UAAU,EACV,iBAAiB,EACjB,QAAQ,EACR,kBAAkB,EAClB,gBAAgB,GAEjB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,cAAc,EAAkB,MAAM,iBAAiB,CAAC;AACjE,OAAO,EACL,gBAAgB,EAChB,cAAc,EACd,cAAc,GAEf,MAAM,qBAAqB,CAAC;AAuC7B,MAAM,GAAG,GAAG,CAAC,IAAY,EAAsB,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC;AAEjF,SAAS,SAAS,CAAC,OAAwB;IACzC,IAAI,OAAO,CAAC,IAAI;QAAE,OAAO,OAAO,CAAC,IAAI,CAAC;IACtC,OAAO,OAAO,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;AAC3E,CAAC;AAED,SAAS,OAAO,CAAC,OAAwB;IACvC,IAAI,OAAO,CAAC,OAAO;QAAE,OAAO,OAAO,CAAC,OAAO,CAAC;IAC5C,OAAO,OAAO,CAAC,KAAK,KAAK,QAAQ;QAC/B,CAAC,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,wBAAwB;QACnD,CAAC,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;AAC3B,CAAC;AAED,SAAS,QAAQ,CAAC,OAAwB;IACxC,IAAI,OAAO,CAAC,KAAK;QAAE,OAAO,OAAO,CAAC,KAAK,CAAC;IACxC,OAAO,OAAO,CAAC,KAAK,KAAK,QAAQ;QAC/B,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,GAAG,CAAC,cAAc,CAAC;QACxC,CAAC,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC;AAClC,CAAC;AAED,SAAS,OAAO,CAAC,GAAW;IAC1B,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC;IAC5C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,OAAwB;IAExB,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,OAAO,CAAC,CAAC;QAC9C,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IACrD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,gEAAgE;QAChE,OAAO;YACL,QAAQ,EAAE,KAAK;YACf,OAAO,EAAE,CAAC,qBAAsB,KAAe,CAAC,OAAO,EAAE,CAAC;SAC3D,CAAC;IACJ,CAAC;AACH,CAAC;AAED,KAAK,UAAU,cAAc,CAAC,OAAwB;IACpD,MAAM,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;IAChC,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7B,IAAI,CAAC,IAAI;QAAE,OAAO,CAAC,8BAA8B,CAAC,CAAC;IACnD,IAAI,CAAC,GAAG;QAAE,OAAO,CAAC,sCAAsC,CAAC,CAAC;IAC1D,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC;QAAE,OAAO,CAAC,sBAAsB,GAAG,eAAe,CAAC,CAAC;IAErE,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;IACjF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAEtD,2EAA2E;IAC3E,oDAAoD;IACpD,MAAM,KAAK,GAAgB,EAAE,CAAC;IAC9B,MAAM,WAAW,GAAa,EAAE,CAAC;IACjC,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3B,MAAM,MAAM,GAAG,cAAc,CAAC,IAAI,EAAE,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACzD,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;YACpB,WAAW,CAAC,IAAI,CACd,GAAG,IAAI,mBAAmB,IAAI,2CAA2C,CAC1E,CAAC;YACF,SAAS;QACX,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACrB,CAAC;IACD,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,WAAW,CAAC;IAE/C,8CAA8C;IAC9C,MAAM,KAAK,GAAG,WAAW,CAAC,OAAO,CAAC,KAAK,EAAE;QACvC,OAAO,EAAE,GAAG;QACZ,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC;QACxB,cAAc,EAAE,OAAO,CAAC,cAAc;KACvC,CAAC,CAAC;IACH,MAAM,UAAU,GAAG,IAAI,GAAG,EAA6B,CAAC;IACxD,MAAM,gBAAgB,GAAa,EAAE,CAAC;IACtC,MAAM,QAAQ,GAAG,IAAI,GAAG,CACtB,CAAC,OAAO,CAAC,kBAAkB,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CACrE,CAAC;IACF,KAAK,MAAM,SAAS,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;QAC3E,IAAI,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;YAC1C,gBAAgB,CAAC,IAAI,CAAC,aAAa,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;YAC5E,SAAS;QACX,CAAC;QACD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;QAC1D,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;YACtB,gBAAgB,CAAC,IAAI,CACnB,aAAa,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,sBAAsB,IAAI,EAAE,CACnE,CAAC;YACF,SAAS;QACX,CAAC;QACD,UAAU,CAAC,GAAG,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QAEpC,MAAM,GAAG,GAAG,cAAc,CAAC,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACtE,MAAM,KAAK,GACT,GAAG,KAAK,IAAI;YACV,CAAC,CAAC,oEAAoE;gBACpE,uEAAuE;gBACvE,gBAAgB,CAAC,GAAG,EAAE,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YACrD,CAAC,CAAC,cAAc,CACZ,SAAS,EACT,QAAQ,EACR,OAAO,CAAC,MAAM,EACd,QAAQ,CAAC,IAAI,KAAK,MAAM;gBACtB,CAAC,CAAC,KAAK;gBACP,CAAC,CAAC,MAAM,KAAK,CAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,CACpD,CAAC;QACR,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,gBAAgB,CAAC,IAAI,CACnB,aAAa,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,oBAAoB,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CACjG,CAAC;QACJ,CAAC;IACH,CAAC;IACD,IAAI,gBAAgB,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,gBAAgB,CAAC;IAEzD,8BAA8B;IAC9B,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;QAC1D,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC,CAAC;IAC/D,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,KAAK,UAAU,SAAS,CACtB,IAAe,EACf,UAA0C,EAC1C,OAAwB;IAExB,MAAM,IAAI,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5D,2EAA2E;IAC3E,iDAAiD;IACjD,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACzC,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,IAAI,CAAC,MAAM,CAAC;IAE9C,MAAM,MAAM,GAAG,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5C,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,OAAO,MAAM,CAAC,MAAM,CAAC;IACrC,MAAM,OAAO,GAAG,gBAAgB,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IACzD,IAAI,CAAC,OAAO,CAAC,EAAE;QAAE,OAAO,OAAO,CAAC,MAAM,CAAC;IACvC,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC;IAE3B,MAAM,QAAQ,GAAG,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAChD,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,+BAA+B,CAAC;IAEnE,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrC,IAAI,KAAK,CAAC,WAAW,EAAE,KAAK,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE,EAAE,CAAC;QACzD,OAAO,eAAe,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,iBAAiB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;IAC/F,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,QAAQ,CAAC,EAAE,EAAE,CAAC;QAC1C,OAAO,YAAY,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,OAAO,CAAC,KAAK,OAAO,QAAQ,CAAC,EAAE,EAAE,CAAC;IAC3F,CAAC;IAED,MAAM,UAAU,GAAG,MAAM,oBAAoB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,IAAI,UAAU,KAAK,IAAI;QAAE,OAAO,UAAU,CAAC;IAE3C,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC/B,IAAI,CAAC,GAAG,CAAC,EAAE;QAAE,OAAO,GAAG,CAAC,MAAM,CAAC;IAC/B,qEAAqE;IACrE,MAAM,UAAU,GAAG,kBAAkB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;IAC7E,IAAI,UAAU,KAAK,IAAI;QAAE,OAAO,UAAU,CAAC;IAE3C,MAAM,SAAS,GAAG,MAAM,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,UAAU,EAAE;QACzE,IAAI,EAAE,OAAO,CAAC,YAAY,IAAI,GAAG,CAAC,0BAA0B,CAAC;QAC7D,SAAS,EAAE,OAAO,CAAC,aAAa;KACjC,CAAC,CAAC;IACH,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,wCAAwC,CAAC;IACjF,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,oBAAoB,CACjC,IAAe,EACf,SAAmB,EACnB,IAAY;IAEZ,MAAM,IAAI,GAAG,MAAM,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IAClD,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC,CAAC,cAAc;IACxD,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QAC1B,OAAO,oCAAoC,IAAI,CAAC,MAAM,mBAAmB,CAAC;IAC5E,CAAC;IACD,MAAM,MAAM,GAAG,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5C,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,OAAO,kCAAkC,MAAM,CAAC,MAAM,GAAG,CAAC;IAC1E,MAAM,SAAS,GAAG,gBAAgB,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IAC3D,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC;QAClB,OAAO,+BAA+B,SAAS,CAAC,MAAM,mBAAmB,CAAC;IAC5E,CAAC;IACD,IAAI,SAAS,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,KAAK,SAAS,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC;QACpD,OAAO,YAAY,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,0BAA0B,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC;IAC5H,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@grimoire-rs/indexer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI + Astro integration for running your own grim package index",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.14.0"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"grim-indexer": "./dist/cli/index.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"templates"
|
|
16
|
+
],
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./integration": {
|
|
23
|
+
"types": "./dist/integration.d.ts",
|
|
24
|
+
"import": "./dist/integration.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc -p tsconfig.json && mkdir -p dist/renderer/astro dist/renderer/public && cp -r src/renderer/astro/. dist/renderer/astro/ && cp -r src/renderer/public/. dist/renderer/public/",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"lint": "eslint .",
|
|
31
|
+
"typecheck": "tsc --noEmit"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/js-yaml": "^4.0.9",
|
|
35
|
+
"@types/node": "^22.14.0",
|
|
36
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
37
|
+
"eslint": "^10.8.0",
|
|
38
|
+
"js-yaml": "^4.1.0",
|
|
39
|
+
"typescript": "^6.0.3",
|
|
40
|
+
"typescript-eslint": "^8.65.0",
|
|
41
|
+
"vitest": "^4.1.10"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@astrojs/preact": "^6.0.1",
|
|
45
|
+
"@clack/prompts": "^1.7.0",
|
|
46
|
+
"astro": "^7.1.4",
|
|
47
|
+
"commander": "^15.0.0",
|
|
48
|
+
"preact": "^10.29.7"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# {{title}}
|
|
2
|
+
|
|
3
|
+
A [Grimoire](https://github.com/grimoire-rs/grimoire) package index — a
|
|
4
|
+
static site listing the skills, rules, agents, mcp servers, and bundles
|
|
5
|
+
available in `{{registryHost}}`, served at {{baseUrl}}.
|
|
6
|
+
|
|
7
|
+
## Layout
|
|
8
|
+
|
|
9
|
+
| Path | Purpose |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `index/<namespace>/<package>/metadata.json` | One entry per package — the source of truth |
|
|
12
|
+
| `index.config.json` | Site identity: title, base URL, registry, branding |
|
|
13
|
+
| `index-policy.json` | Committed allowlist of registry hosts contributions may point at |
|
|
14
|
+
| `dist/` | Build output (`all.json`, per-path copies, the rendered site) — not committed |
|
|
15
|
+
|
|
16
|
+
## Contributing a package
|
|
17
|
+
|
|
18
|
+
Add `index/<namespace>/<package>/metadata.json`, then open a pull request.
|
|
19
|
+
CI runs `grim-indexer validate` on it: a clean run means the entry is
|
|
20
|
+
eligible for auto-merge, a failing one means a maintainer takes a look.
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"schema": 1,
|
|
25
|
+
"name": "{{name}}-example",
|
|
26
|
+
"kind": "skill",
|
|
27
|
+
"ref": "{{registryHost}}/{{registryAlias}}/skills/{{name}}-example:1.0.0",
|
|
28
|
+
"description": "One line describing what this package does.",
|
|
29
|
+
"owner": { "id": 0, "github": "your-github-login" }
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The directory name must equal `name`, and `kind` is one of `skill`,
|
|
34
|
+
`rule`, `agent`, `mcp`, `bundle`.
|
|
35
|
+
|
|
36
|
+
## Local development
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
npx @grimoire-rs/indexer build # index/** -> dist/
|
|
40
|
+
npx @grimoire-rs/indexer validate # run the contribution gate locally
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Upgrading CI
|
|
44
|
+
|
|
45
|
+
The workflows in this repo are thin callers of reusable workflows that
|
|
46
|
+
[grim-indexer](https://github.com/grimoire-rs/indexer) owns. To
|
|
47
|
+
pick up CI fixes, bump the pinned ref in the `uses:` line — nothing needs
|
|
48
|
+
re-scaffolding, and your local edits are never overwritten.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Builds this index and deploys it to GitHub Pages.
|
|
2
|
+
#
|
|
3
|
+
# Deliberately a thin caller: the job logic lives in a reusable workflow
|
|
4
|
+
# grim-indexer owns, so upgrades arrive by bumping the pinned ref below
|
|
5
|
+
# instead of re-scaffolding this repo over your edits. Renovate's
|
|
6
|
+
# `github-actions` manager bumps it for you.
|
|
7
|
+
name: pages
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
push:
|
|
11
|
+
branches: [main]
|
|
12
|
+
workflow_dispatch:
|
|
13
|
+
|
|
14
|
+
permissions: {}
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
pages:
|
|
18
|
+
uses: grimoire-rs/indexer/.github/workflows/index-pages.yml@{{ref}}
|
|
19
|
+
permissions:
|
|
20
|
+
contents: read
|
|
21
|
+
pages: write
|
|
22
|
+
id-token: write
|
|
23
|
+
with:
|
|
24
|
+
grim-indexer-version: "{{version}}"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Contribution gate for pull requests against this index.
|
|
2
|
+
#
|
|
3
|
+
# Exit 0 = eligible for auto-merge, non-zero = manual review required.
|
|
4
|
+
# Make `validate` a required status check in branch protection to enforce it.
|
|
5
|
+
#
|
|
6
|
+
# `pull_request_target` is deliberate and safe HERE, and only because of
|
|
7
|
+
# what the called workflow does: it runs grim-indexer from npm against the
|
|
8
|
+
# trusted base checkout, and reads the PR head as data in a separate
|
|
9
|
+
# directory without ever executing, importing, or installing from it. The
|
|
10
|
+
# gate needs a forge token to resolve numeric namespace-owner ids, which an
|
|
11
|
+
# unprivileged `pull_request` job does not have.
|
|
12
|
+
#
|
|
13
|
+
# Do not switch this to run contributor code, and do not widen
|
|
14
|
+
# `permissions:` here.
|
|
15
|
+
#
|
|
16
|
+
# Deliberately a thin caller: the job logic lives in a reusable workflow
|
|
17
|
+
# grim-indexer owns, so upgrades arrive by bumping the pinned ref below
|
|
18
|
+
# instead of re-scaffolding this repo over your edits.
|
|
19
|
+
name: validate
|
|
20
|
+
|
|
21
|
+
on:
|
|
22
|
+
pull_request_target:
|
|
23
|
+
paths:
|
|
24
|
+
- "index/**"
|
|
25
|
+
- "index-policy.json"
|
|
26
|
+
|
|
27
|
+
permissions: {}
|
|
28
|
+
|
|
29
|
+
jobs:
|
|
30
|
+
validate:
|
|
31
|
+
uses: grimoire-rs/indexer/.github/workflows/index-validate.yml@{{ref}}
|
|
32
|
+
permissions:
|
|
33
|
+
contents: read
|
|
34
|
+
pull-requests: read
|
|
35
|
+
with:
|
|
36
|
+
grim-indexer-version: "{{version}}"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Builds this index for GitLab Pages and gates merge requests.
|
|
2
|
+
#
|
|
3
|
+
# Deliberately a thin caller: the job logic lives in a file grim-indexer
|
|
4
|
+
# owns, so upgrades arrive by bumping the pinned ref in the URL below
|
|
5
|
+
# instead of re-scaffolding this repo over your edits.
|
|
6
|
+
#
|
|
7
|
+
# If your GitLab instance blocks remote includes, vendor the file locally
|
|
8
|
+
# and switch to `- local: "ci/grim-indexer.yml"`.
|
|
9
|
+
include:
|
|
10
|
+
- remote: "https://raw.githubusercontent.com/grimoire-rs/indexer/{{ref}}/templates/reusable/gitlab-index-ci.yml"
|
|
11
|
+
|
|
12
|
+
variables:
|
|
13
|
+
GRIM_INDEXER_VERSION: "{{version}}"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# `grim publish` manifest — this repo holds both its own skills (under
|
|
2
|
+
# `skills/`) and the index that lists them, the combined layout
|
|
3
|
+
# `grim-indexer init --with-skills` scaffolds.
|
|
4
|
+
#
|
|
5
|
+
# The default layout is a standalone index repo instead: an index is a
|
|
6
|
+
# distribution database that N separate skill repos announce into. Use
|
|
7
|
+
# this combined form when one team owns both sides.
|
|
8
|
+
#
|
|
9
|
+
# Publish with `grim publish`, then announce into this index. Schema:
|
|
10
|
+
# `grim schema --kind publish`.
|
|
11
|
+
registry = "{{registryHost}}"
|
|
12
|
+
version = "0.1.0"
|
|
13
|
+
|
|
14
|
+
# One table per package. `repository` is the path under the registry host.
|
|
15
|
+
#
|
|
16
|
+
# [skills.example]
|
|
17
|
+
# repository = "{{registryAlias}}/skills/example"
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2026 The Grimoire Authors
|
|
3
|
+
#
|
|
4
|
+
# GitLab counterpart of the two reusable GitHub workflows: scaffolded index
|
|
5
|
+
# repos pull this in with `include: remote:` so a fix here reaches them
|
|
6
|
+
# without a re-scaffold. Override any variable below in the including
|
|
7
|
+
# `.gitlab-ci.yml`.
|
|
8
|
+
#
|
|
9
|
+
# A private GitLab instance may refuse to fetch a remote include. The
|
|
10
|
+
# fallback is to vendor this file into the index repo and switch the
|
|
11
|
+
# include to `local:`.
|
|
12
|
+
variables:
|
|
13
|
+
GRIM_INDEXER_VERSION: "latest"
|
|
14
|
+
GRIM_INDEXER_NODE_IMAGE: "node:22-alpine"
|
|
15
|
+
GRIM_INDEXER_OUT_DIR: "public"
|
|
16
|
+
|
|
17
|
+
stages:
|
|
18
|
+
- test
|
|
19
|
+
- deploy
|
|
20
|
+
|
|
21
|
+
# Contribution gate. Exit 0 = eligible for auto-merge, non-zero = manual
|
|
22
|
+
# review required, so this job's status IS the gate.
|
|
23
|
+
#
|
|
24
|
+
# The gate must judge the change against the COMMITTED policy, not the
|
|
25
|
+
# policy the merge request proposes — otherwise a contributor allowlists
|
|
26
|
+
# their own registry host in the same MR. So the target branch is checked
|
|
27
|
+
# out into a sibling worktree and passed as `--root`, with the MR head
|
|
28
|
+
# passed as `--pr-tree` and read as data only.
|
|
29
|
+
#
|
|
30
|
+
# RESIDUAL, and it is not fixable from this file: on GitLab the merge
|
|
31
|
+
# request supplies its own `.gitlab-ci.yml`, so a contributor can delete
|
|
32
|
+
# this job outright. Enforce it instance-side — a compliance pipeline, a
|
|
33
|
+
# required merge check, or protected-branch approval rules — or treat the
|
|
34
|
+
# GitLab gate as advisory.
|
|
35
|
+
grim-indexer:validate:
|
|
36
|
+
stage: test
|
|
37
|
+
image: $GRIM_INDEXER_NODE_IMAGE
|
|
38
|
+
variables:
|
|
39
|
+
GIT_DEPTH: 0
|
|
40
|
+
rules:
|
|
41
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
42
|
+
script:
|
|
43
|
+
- |
|
|
44
|
+
set -eu
|
|
45
|
+
git fetch --quiet origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
|
|
46
|
+
git worktree add --detach ../base "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
|
|
47
|
+
git diff --name-only --diff-filter=ACMR "$CI_MERGE_REQUEST_DIFF_BASE_SHA" HEAD -- index > changed.txt
|
|
48
|
+
xargs -a changed.txt npx --yes "@grimoire-rs/indexer@$GRIM_INDEXER_VERSION" validate \
|
|
49
|
+
--forge gitlab --root ../base --pr-tree .
|
|
50
|
+
|
|
51
|
+
# GitLab Pages serves whatever the `pages` job leaves in `public/`.
|
|
52
|
+
pages:
|
|
53
|
+
stage: deploy
|
|
54
|
+
image: $GRIM_INDEXER_NODE_IMAGE
|
|
55
|
+
rules:
|
|
56
|
+
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
57
|
+
script:
|
|
58
|
+
- npx --yes "@grimoire-rs/indexer@$GRIM_INDEXER_VERSION" build --out-dir "$GRIM_INDEXER_OUT_DIR"
|
|
59
|
+
artifacts:
|
|
60
|
+
paths:
|
|
61
|
+
- $GRIM_INDEXER_OUT_DIR
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2026 The Grimoire Authors
|
|
3
|
+
#
|
|
4
|
+
# Reusable workflow: build a grimoire index repo and publish it to GitHub
|
|
5
|
+
# Pages. Scaffolded index repos call this by ref rather than inlining the
|
|
6
|
+
# job logic, so a fix here reaches every downstream index without anyone
|
|
7
|
+
# re-running `grim-indexer init` over their edits. Renovate's built-in
|
|
8
|
+
# `github-actions` manager bumps the pinned ref in each caller.
|
|
9
|
+
#
|
|
10
|
+
# Copy this file to `.github/workflows/index-pages.yml` in this repo — a
|
|
11
|
+
# reusable workflow must live directly in `.github/workflows/`, GitHub does
|
|
12
|
+
# not resolve subdirectories.
|
|
13
|
+
name: grim-indexer pages
|
|
14
|
+
|
|
15
|
+
on:
|
|
16
|
+
workflow_call:
|
|
17
|
+
inputs:
|
|
18
|
+
node-version:
|
|
19
|
+
description: Node.js version used to run grim-indexer.
|
|
20
|
+
type: string
|
|
21
|
+
required: false
|
|
22
|
+
default: "22"
|
|
23
|
+
grim-indexer-version:
|
|
24
|
+
description: npm version or range of grim-indexer to run.
|
|
25
|
+
type: string
|
|
26
|
+
required: false
|
|
27
|
+
default: latest
|
|
28
|
+
out-dir:
|
|
29
|
+
description: Directory `grim-indexer build` writes the site into.
|
|
30
|
+
type: string
|
|
31
|
+
required: false
|
|
32
|
+
default: dist
|
|
33
|
+
|
|
34
|
+
permissions: {}
|
|
35
|
+
|
|
36
|
+
concurrency:
|
|
37
|
+
group: grim-indexer-pages
|
|
38
|
+
cancel-in-progress: false
|
|
39
|
+
|
|
40
|
+
jobs:
|
|
41
|
+
build:
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
permissions:
|
|
44
|
+
contents: read
|
|
45
|
+
steps:
|
|
46
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
47
|
+
with:
|
|
48
|
+
persist-credentials: false
|
|
49
|
+
|
|
50
|
+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
51
|
+
with:
|
|
52
|
+
node-version: ${{ inputs.node-version }}
|
|
53
|
+
|
|
54
|
+
# Inputs reach the shell through env vars, never through `run:`
|
|
55
|
+
# interpolation — a caller-supplied value must not be able to close
|
|
56
|
+
# the quoting and append a command.
|
|
57
|
+
- name: Build index
|
|
58
|
+
env:
|
|
59
|
+
VERSION: ${{ inputs.grim-indexer-version }}
|
|
60
|
+
OUT_DIR: ${{ inputs.out-dir }}
|
|
61
|
+
run: npx --yes "@grimoire-rs/indexer@${VERSION}" build --out-dir "${OUT_DIR}"
|
|
62
|
+
|
|
63
|
+
- uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
|
|
64
|
+
|
|
65
|
+
- uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
|
|
66
|
+
with:
|
|
67
|
+
path: ${{ inputs.out-dir }}
|
|
68
|
+
|
|
69
|
+
deploy:
|
|
70
|
+
needs: build
|
|
71
|
+
runs-on: ubuntu-latest
|
|
72
|
+
permissions:
|
|
73
|
+
pages: write
|
|
74
|
+
id-token: write
|
|
75
|
+
environment:
|
|
76
|
+
name: github-pages
|
|
77
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
78
|
+
steps:
|
|
79
|
+
- id: deployment
|
|
80
|
+
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2026 The Grimoire Authors
|
|
3
|
+
#
|
|
4
|
+
# Reusable workflow: the contribution gate for an index repo.
|
|
5
|
+
#
|
|
6
|
+
# Runs from the TRUSTED base checkout under `pull_request_target`, because
|
|
7
|
+
# the gate needs a forge token to resolve numeric namespace-owner ids. The
|
|
8
|
+
# PR head is checked out into a SEPARATE directory and consumed as DATA
|
|
9
|
+
# ONLY — nothing in it is executed, imported, installed, or interpolated
|
|
10
|
+
# into a command. `@grimoire-rs/indexer` itself comes from npm, never from the PR.
|
|
11
|
+
#
|
|
12
|
+
# Do not add a `run:` step that touches `pr-tree/`, and never add
|
|
13
|
+
# `npm ci`/`npm install` — that would execute contributor code with a
|
|
14
|
+
# write-capable token.
|
|
15
|
+
#
|
|
16
|
+
# The gate contract is the exit code of `grim-indexer validate`:
|
|
17
|
+
# 0 -> eligible for auto-merge
|
|
18
|
+
# non-zero -> manual review required
|
|
19
|
+
# so this job's success/failure IS the gate. Branch protection on the
|
|
20
|
+
# calling repo turns that into a merge decision.
|
|
21
|
+
#
|
|
22
|
+
# Copy this file to `.github/workflows/index-validate.yml` in this repo — a
|
|
23
|
+
# reusable workflow must live directly in `.github/workflows/`, GitHub does
|
|
24
|
+
# not resolve subdirectories.
|
|
25
|
+
name: grim-indexer validate
|
|
26
|
+
|
|
27
|
+
on:
|
|
28
|
+
workflow_call:
|
|
29
|
+
inputs:
|
|
30
|
+
node-version:
|
|
31
|
+
description: Node.js version used to run grim-indexer.
|
|
32
|
+
type: string
|
|
33
|
+
required: false
|
|
34
|
+
default: "22"
|
|
35
|
+
grim-indexer-version:
|
|
36
|
+
description: npm version or range of grim-indexer to run.
|
|
37
|
+
type: string
|
|
38
|
+
required: false
|
|
39
|
+
default: latest
|
|
40
|
+
|
|
41
|
+
permissions: {}
|
|
42
|
+
|
|
43
|
+
jobs:
|
|
44
|
+
validate:
|
|
45
|
+
runs-on: ubuntu-latest
|
|
46
|
+
permissions:
|
|
47
|
+
contents: read
|
|
48
|
+
pull-requests: read
|
|
49
|
+
steps:
|
|
50
|
+
# Trusted: the committed state the PR proposes to change.
|
|
51
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
52
|
+
with:
|
|
53
|
+
persist-credentials: false
|
|
54
|
+
|
|
55
|
+
# Untrusted: PR head, as data. `persist-credentials: false` keeps a
|
|
56
|
+
# token out of the tree the contributor controls.
|
|
57
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
58
|
+
with:
|
|
59
|
+
ref: ${{ github.event.pull_request.head.sha }}
|
|
60
|
+
path: pr-tree
|
|
61
|
+
persist-credentials: false
|
|
62
|
+
|
|
63
|
+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
64
|
+
with:
|
|
65
|
+
node-version: ${{ inputs.node-version }}
|
|
66
|
+
|
|
67
|
+
# Changed paths come from the API, not from a diff of the untrusted
|
|
68
|
+
# tree. `jq -r` output is written to a file and passed via `xargs -a`
|
|
69
|
+
# so a crafted filename can never reach the shell as syntax.
|
|
70
|
+
- name: Collect changed paths
|
|
71
|
+
env:
|
|
72
|
+
GH_TOKEN: ${{ github.token }}
|
|
73
|
+
REPO: ${{ github.repository }}
|
|
74
|
+
PR: ${{ github.event.pull_request.number }}
|
|
75
|
+
run: |
|
|
76
|
+
set -euo pipefail
|
|
77
|
+
gh api --paginate "repos/${REPO}/pulls/${PR}/files" \
|
|
78
|
+
--jq '.[] | select(.status != "removed") | .filename' \
|
|
79
|
+
| grep -E '^index/' > changed.txt || : > changed.txt
|
|
80
|
+
|
|
81
|
+
- name: Validate contribution
|
|
82
|
+
env:
|
|
83
|
+
VERSION: ${{ inputs.grim-indexer-version }}
|
|
84
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
85
|
+
run: |
|
|
86
|
+
set -euo pipefail
|
|
87
|
+
xargs -a changed.txt npx --yes "@grimoire-rs/indexer@${VERSION}" validate \
|
|
88
|
+
--forge github --root . --pr-tree pr-tree
|