git-daemon 0.1.10 → 0.1.12
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 +12 -1
- package/config.schema.json +5 -0
- package/design.md +1 -1
- package/dist/config.js +18 -1
- package/healthchecks/example-suite/about-description/run.js +28 -7
- package/openapi.yaml +58 -1
- package/package.json +3 -1
- package/src/app.ts +6 -0
- package/src/config.ts +24 -1
- package/src/daemon.ts +4 -0
- package/src/healthchecks.ts +17 -0
- package/src/types.ts +1 -0
package/README.md
CHANGED
|
@@ -19,7 +19,9 @@ Git Daemon is a local Node.js service that exposes a small, authenticated HTTP A
|
|
|
19
19
|
Healthchecks are user-supplied executables/scripts stored in local suites (which can be
|
|
20
20
|
private repos). The daemon runs them as jobs, streams logs over SSE, and returns a
|
|
21
21
|
normalized status (`na`, `failed`, `pass-partial`, `pass-full`) plus details. Suites are
|
|
22
|
-
configured via local paths, and each check has a `healthcheck.json` manifest.
|
|
22
|
+
configured via local paths, and each check has a `healthcheck.json` manifest. For a
|
|
23
|
+
quick demo, set `healthchecks.demo: true` in config to auto-enable the bundled example
|
|
24
|
+
suite (when present).
|
|
23
25
|
|
|
24
26
|
## Security model (high level)
|
|
25
27
|
|
|
@@ -197,6 +199,14 @@ curl -H "Origin: https://app.example.com" \
|
|
|
197
199
|
http://127.0.0.1:8790/v1/healthchecks/jobs/<JOB_ID>/result
|
|
198
200
|
```
|
|
199
201
|
|
|
202
|
+
List healthchecks (flat):
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
curl -H "Origin: https://app.example.com" \
|
|
206
|
+
-H "Authorization: Bearer <TOKEN>" \
|
|
207
|
+
"http://127.0.0.1:8790/v1/healthchecks?flat=true"
|
|
208
|
+
```
|
|
209
|
+
|
|
200
210
|
## Configuration
|
|
201
211
|
|
|
202
212
|
Config is stored in OS-specific directories:
|
|
@@ -218,6 +228,7 @@ Key settings live in `config.json`:
|
|
|
218
228
|
- `deps.defaultSafer`: defaults to `true` for `--ignore-scripts`
|
|
219
229
|
- `jobs.maxConcurrent` and `jobs.timeoutSeconds`
|
|
220
230
|
- `healthchecks.suites`: array of absolute or config-relative suite paths
|
|
231
|
+
- `healthchecks.demo`: enable bundled example healthchecks if present
|
|
221
232
|
|
|
222
233
|
Tokens are stored (hashed) in `tokens.json`. Logs are written under the configured `logging.directory` with rotation.
|
|
223
234
|
|
package/config.schema.json
CHANGED
package/design.md
CHANGED
|
@@ -430,7 +430,7 @@ Recommendations:
|
|
|
430
430
|
|
|
431
431
|
### Healthcheck endpoints
|
|
432
432
|
|
|
433
|
-
* `GET /v1/healthchecks` → list installed suites and checks.
|
|
433
|
+
* `GET /v1/healthchecks` → list installed suites and checks (use `?flat=true` for a flattened list).
|
|
434
434
|
* `POST /v1/healthchecks/run` → job
|
|
435
435
|
|
|
436
436
|
* body: `{ repoPath, repoInfo?, suiteId?, checks?: [{ suiteId?, checkId, config?, cacheMode? }] }`
|
package/dist/config.js
CHANGED
|
@@ -47,6 +47,7 @@ const defaultConfig = () => ({
|
|
|
47
47
|
},
|
|
48
48
|
healthchecks: {
|
|
49
49
|
suites: [],
|
|
50
|
+
demo: false,
|
|
50
51
|
},
|
|
51
52
|
logging: {
|
|
52
53
|
directory: "logs",
|
|
@@ -68,7 +69,7 @@ const loadConfig = async (configDir) => {
|
|
|
68
69
|
try {
|
|
69
70
|
const raw = await fs_1.promises.readFile(configPath, "utf8");
|
|
70
71
|
const data = JSON.parse(raw);
|
|
71
|
-
|
|
72
|
+
const merged = {
|
|
72
73
|
...(0, exports.defaultConfig)(),
|
|
73
74
|
...data,
|
|
74
75
|
server: {
|
|
@@ -107,6 +108,22 @@ const loadConfig = async (configDir) => {
|
|
|
107
108
|
entries: data.approvals?.entries ?? [],
|
|
108
109
|
},
|
|
109
110
|
};
|
|
111
|
+
if (merged.healthchecks?.demo) {
|
|
112
|
+
const demoSuitePath = path_1.default.resolve(__dirname, "..", "healthchecks", "example-suite");
|
|
113
|
+
try {
|
|
114
|
+
await fs_1.promises.access(demoSuitePath);
|
|
115
|
+
if (!merged.healthchecks.suites.includes(demoSuitePath)) {
|
|
116
|
+
merged.healthchecks.suites = [
|
|
117
|
+
...merged.healthchecks.suites,
|
|
118
|
+
demoSuitePath,
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Ignore missing demo suite; config can still load.
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return merged;
|
|
110
127
|
}
|
|
111
128
|
catch (err) {
|
|
112
129
|
if (err.code !== "ENOENT") {
|
|
@@ -155,10 +155,16 @@ const description =
|
|
|
155
155
|
|
|
156
156
|
if (!description) {
|
|
157
157
|
emit({
|
|
158
|
-
status: "
|
|
158
|
+
status: "failed",
|
|
159
159
|
summary: "Missing repo description",
|
|
160
|
-
explanation:
|
|
161
|
-
|
|
160
|
+
explanation: "REPO_INFO.description is required for this healthcheck.",
|
|
161
|
+
details: [
|
|
162
|
+
{
|
|
163
|
+
code: "ABOUT_MISSING",
|
|
164
|
+
severity: "high",
|
|
165
|
+
message: "Provide a GitHub repo description to match the README.",
|
|
166
|
+
},
|
|
167
|
+
],
|
|
162
168
|
});
|
|
163
169
|
process.exit(0);
|
|
164
170
|
}
|
|
@@ -166,9 +172,16 @@ if (!description) {
|
|
|
166
172
|
const readmePath = findReadme(repoPath);
|
|
167
173
|
if (!readmePath) {
|
|
168
174
|
emit({
|
|
169
|
-
status: "
|
|
175
|
+
status: "failed",
|
|
170
176
|
summary: "README not found",
|
|
171
|
-
explanation: "
|
|
177
|
+
explanation: "A README is required to compare against the description.",
|
|
178
|
+
details: [
|
|
179
|
+
{
|
|
180
|
+
code: "README_MISSING",
|
|
181
|
+
severity: "high",
|
|
182
|
+
message: "Add a README with a clear first sentence.",
|
|
183
|
+
},
|
|
184
|
+
],
|
|
172
185
|
});
|
|
173
186
|
process.exit(0);
|
|
174
187
|
}
|
|
@@ -195,10 +208,18 @@ try {
|
|
|
195
208
|
const firstSentence = extractFirstSentence(readmeText);
|
|
196
209
|
if (!firstSentence) {
|
|
197
210
|
emit({
|
|
198
|
-
status: "
|
|
199
|
-
summary: "README
|
|
211
|
+
status: "failed",
|
|
212
|
+
summary: "README missing a first sentence",
|
|
200
213
|
explanation:
|
|
201
214
|
"Could not locate non-title text to compare against the description.",
|
|
215
|
+
details: [
|
|
216
|
+
{
|
|
217
|
+
code: "README_SENTENCE_MISSING",
|
|
218
|
+
severity: "high",
|
|
219
|
+
message:
|
|
220
|
+
"Add a first paragraph with a clear first sentence after the title.",
|
|
221
|
+
},
|
|
222
|
+
],
|
|
202
223
|
});
|
|
203
224
|
process.exit(0);
|
|
204
225
|
}
|
package/openapi.yaml
CHANGED
|
@@ -321,13 +321,16 @@ paths:
|
|
|
321
321
|
summary: List available healthchecks
|
|
322
322
|
parameters:
|
|
323
323
|
- $ref: '#/components/parameters/OriginHeader'
|
|
324
|
+
- $ref: '#/components/parameters/HealthcheckFlat'
|
|
324
325
|
responses:
|
|
325
326
|
'200':
|
|
326
327
|
description: Healthcheck catalog
|
|
327
328
|
content:
|
|
328
329
|
application/json:
|
|
329
330
|
schema:
|
|
330
|
-
|
|
331
|
+
oneOf:
|
|
332
|
+
- $ref: '#/components/schemas/HealthcheckCatalogResponse'
|
|
333
|
+
- $ref: '#/components/schemas/HealthcheckFlatResponse'
|
|
331
334
|
'401':
|
|
332
335
|
$ref: '#/components/responses/Unauthorized'
|
|
333
336
|
'403':
|
|
@@ -438,6 +441,14 @@ components:
|
|
|
438
441
|
schema:
|
|
439
442
|
type: boolean
|
|
440
443
|
default: true
|
|
444
|
+
HealthcheckFlat:
|
|
445
|
+
name: flat
|
|
446
|
+
in: query
|
|
447
|
+
required: false
|
|
448
|
+
schema:
|
|
449
|
+
type: boolean
|
|
450
|
+
default: false
|
|
451
|
+
description: Return a flattened list of checks instead of suites.
|
|
441
452
|
schemas:
|
|
442
453
|
MetaResponse:
|
|
443
454
|
type: object
|
|
@@ -790,6 +801,15 @@ components:
|
|
|
790
801
|
type: array
|
|
791
802
|
items:
|
|
792
803
|
$ref: '#/components/schemas/HealthcheckSuite'
|
|
804
|
+
HealthcheckFlatResponse:
|
|
805
|
+
type: object
|
|
806
|
+
required:
|
|
807
|
+
- checks
|
|
808
|
+
properties:
|
|
809
|
+
checks:
|
|
810
|
+
type: array
|
|
811
|
+
items:
|
|
812
|
+
$ref: '#/components/schemas/HealthcheckFlatCheck'
|
|
793
813
|
HealthcheckSuite:
|
|
794
814
|
type: object
|
|
795
815
|
required:
|
|
@@ -843,6 +863,43 @@ components:
|
|
|
843
863
|
additionalProperties: true
|
|
844
864
|
outputSchemaVersion:
|
|
845
865
|
type: integer
|
|
866
|
+
HealthcheckFlatCheck:
|
|
867
|
+
type: object
|
|
868
|
+
required:
|
|
869
|
+
- suiteId
|
|
870
|
+
- checkId
|
|
871
|
+
- name
|
|
872
|
+
- version
|
|
873
|
+
- description
|
|
874
|
+
- timeoutSeconds
|
|
875
|
+
- cacheable
|
|
876
|
+
properties:
|
|
877
|
+
suiteId:
|
|
878
|
+
type: string
|
|
879
|
+
checkId:
|
|
880
|
+
type: string
|
|
881
|
+
name:
|
|
882
|
+
type: string
|
|
883
|
+
version:
|
|
884
|
+
type: string
|
|
885
|
+
description:
|
|
886
|
+
type: string
|
|
887
|
+
timeoutSeconds:
|
|
888
|
+
type: integer
|
|
889
|
+
minimum: 1
|
|
890
|
+
cacheable:
|
|
891
|
+
type: boolean
|
|
892
|
+
cacheMaxAgeSeconds:
|
|
893
|
+
type: integer
|
|
894
|
+
minimum: 1
|
|
895
|
+
permissions:
|
|
896
|
+
$ref: '#/components/schemas/HealthcheckPermissions'
|
|
897
|
+
configSchema:
|
|
898
|
+
type: object
|
|
899
|
+
description: JSON schema for check config.
|
|
900
|
+
additionalProperties: true
|
|
901
|
+
outputSchemaVersion:
|
|
902
|
+
type: integer
|
|
846
903
|
HealthcheckPermissions:
|
|
847
904
|
type: object
|
|
848
905
|
properties:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-daemon",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "dist/daemon.js",
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
"build": "tsc",
|
|
9
9
|
"daemon": "tsx src/daemon.ts",
|
|
10
10
|
"daemon:setup": "tsx src/setup.ts",
|
|
11
|
+
"typecheck": "tsc --noEmit",
|
|
12
|
+
"verify": "npm run lint && npm run typecheck && npm test",
|
|
11
13
|
"test": "vitest run",
|
|
12
14
|
"test:watch": "vitest",
|
|
13
15
|
"test:clone": "bash scripts/test-clone.sh",
|
package/src/app.ts
CHANGED
|
@@ -54,6 +54,7 @@ import { installDeps } from "./deps";
|
|
|
54
54
|
import { openTarget } from "./os";
|
|
55
55
|
import {
|
|
56
56
|
HealthcheckResultStore,
|
|
57
|
+
flattenHealthchecks,
|
|
57
58
|
listHealthcheckSuites,
|
|
58
59
|
loadHealthcheckSuites,
|
|
59
60
|
resolveSelections,
|
|
@@ -530,6 +531,11 @@ export const createApp = (ctx: DaemonContext) => {
|
|
|
530
531
|
ctx.configDir,
|
|
531
532
|
ctx.config.healthchecks?.suites ?? [],
|
|
532
533
|
);
|
|
534
|
+
const flat = _req.query.flat === "true";
|
|
535
|
+
if (flat) {
|
|
536
|
+
res.json({ checks: flattenHealthchecks(suites) });
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
533
539
|
res.json({ suites: listHealthcheckSuites(suites) });
|
|
534
540
|
} catch (err) {
|
|
535
541
|
next(err);
|
package/src/config.ts
CHANGED
|
@@ -46,6 +46,7 @@ export const defaultConfig = (): AppConfig => ({
|
|
|
46
46
|
},
|
|
47
47
|
healthchecks: {
|
|
48
48
|
suites: [],
|
|
49
|
+
demo: false,
|
|
49
50
|
},
|
|
50
51
|
logging: {
|
|
51
52
|
directory: "logs",
|
|
@@ -67,7 +68,7 @@ export const loadConfig = async (configDir: string): Promise<AppConfig> => {
|
|
|
67
68
|
try {
|
|
68
69
|
const raw = await fs.readFile(configPath, "utf8");
|
|
69
70
|
const data = JSON.parse(raw) as AppConfig;
|
|
70
|
-
|
|
71
|
+
const merged: AppConfig = {
|
|
71
72
|
...defaultConfig(),
|
|
72
73
|
...data,
|
|
73
74
|
server: {
|
|
@@ -106,6 +107,28 @@ export const loadConfig = async (configDir: string): Promise<AppConfig> => {
|
|
|
106
107
|
entries: data.approvals?.entries ?? [],
|
|
107
108
|
},
|
|
108
109
|
};
|
|
110
|
+
|
|
111
|
+
if (merged.healthchecks?.demo) {
|
|
112
|
+
const demoSuitePath = path.resolve(
|
|
113
|
+
__dirname,
|
|
114
|
+
"..",
|
|
115
|
+
"healthchecks",
|
|
116
|
+
"example-suite",
|
|
117
|
+
);
|
|
118
|
+
try {
|
|
119
|
+
await fs.access(demoSuitePath);
|
|
120
|
+
if (!merged.healthchecks.suites.includes(demoSuitePath)) {
|
|
121
|
+
merged.healthchecks.suites = [
|
|
122
|
+
...merged.healthchecks.suites,
|
|
123
|
+
demoSuitePath,
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
// Ignore missing demo suite; config can still load.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return merged;
|
|
109
132
|
} catch (err) {
|
|
110
133
|
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
111
134
|
throw err;
|
package/src/daemon.ts
CHANGED
|
@@ -15,6 +15,7 @@ const start = async () => {
|
|
|
15
15
|
port: ctx.config.server.port,
|
|
16
16
|
workspaceRoot: ctx.config.workspaceRoot ?? "not configured",
|
|
17
17
|
originAllowlist: ctx.config.originAllowlist,
|
|
18
|
+
healthchecksDemo: ctx.config.healthchecks?.demo ?? false,
|
|
18
19
|
};
|
|
19
20
|
ctx.logger.info(startupSummary, "Git Daemon starting");
|
|
20
21
|
if (process.env.GIT_DAEMON_LOG_STDOUT !== "1") {
|
|
@@ -26,6 +27,9 @@ const start = async () => {
|
|
|
26
27
|
console.log(
|
|
27
28
|
` allowlist: ${startupSummary.originAllowlist.join(", ") || "none"}`,
|
|
28
29
|
);
|
|
30
|
+
if (startupSummary.healthchecksDemo) {
|
|
31
|
+
console.log(" healthchecks: demo mode enabled");
|
|
32
|
+
}
|
|
29
33
|
const httpsConfig = ctx.config.server.https;
|
|
30
34
|
if (httpsConfig?.enabled) {
|
|
31
35
|
console.log(
|
package/src/healthchecks.ts
CHANGED
|
@@ -279,6 +279,23 @@ export const listHealthcheckSuites = (suites: HealthcheckSuite[]) =>
|
|
|
279
279
|
})),
|
|
280
280
|
}));
|
|
281
281
|
|
|
282
|
+
export const flattenHealthchecks = (suites: HealthcheckSuite[]) =>
|
|
283
|
+
suites.flatMap((suite) =>
|
|
284
|
+
suite.checks.map((check) => ({
|
|
285
|
+
suiteId: suite.id,
|
|
286
|
+
checkId: check.id,
|
|
287
|
+
name: check.name,
|
|
288
|
+
version: check.version,
|
|
289
|
+
description: check.description,
|
|
290
|
+
timeoutSeconds: check.timeoutSeconds,
|
|
291
|
+
cacheable: check.cacheable,
|
|
292
|
+
cacheMaxAgeSeconds: check.cacheMaxAgeSeconds,
|
|
293
|
+
permissions: check.permissions,
|
|
294
|
+
configSchema: check.configSchema,
|
|
295
|
+
outputSchemaVersion: check.outputSchemaVersion,
|
|
296
|
+
})),
|
|
297
|
+
);
|
|
298
|
+
|
|
282
299
|
export const resolveSelections = (
|
|
283
300
|
suites: HealthcheckSuite[],
|
|
284
301
|
request: HealthcheckRunRequest,
|