@patronage/factory-ci 0.1.2 → 0.1.3
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 +37 -4
- package/dist/index.d.ts +25 -1
- package/dist/index.js +132 -1
- package/package.json +2 -2
- package/src/hosted-docs-only-detector.ts +192 -0
- package/src/index.ts +8 -3
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ It is a **pure library**. There is no `bin`: anything invocable belongs to `psf`
|
|
|
12
12
|
|
|
13
13
|
Three repositories independently built the same scaffolding and then drifted: three wordings of the same setup block, the same `@v4` action tags pinned to different commits because they were generated months apart, two esbuild pre-bundles, two Alchemy CLI wrappers, and one disposable-stage grammar invented twice.
|
|
14
14
|
|
|
15
|
-
The admission rule is **upstream on repetition**: nothing enters this package until it already repeats across at least two projects. Experiments live in one repository first. The public surface is made of
|
|
15
|
+
The admission rule is **upstream on repetition**: nothing enters this package until it already repeats across at least two projects. Experiments live in one repository first. The public surface is made of four deep modules, not a workflow framework.
|
|
16
16
|
|
|
17
17
|
## Exports
|
|
18
18
|
|
|
@@ -38,8 +38,8 @@ const generated = factoryWorkflow({
|
|
|
38
38
|
actionFamily: NODE_PNPM_ACTION_FAMILY_NODE24,
|
|
39
39
|
additionalActions: {
|
|
40
40
|
pathsFilter: {
|
|
41
|
-
tag: "
|
|
42
|
-
uses: "dorny/paths-filter@
|
|
41
|
+
tag: "v4.0.2",
|
|
42
|
+
uses: "dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706",
|
|
43
43
|
},
|
|
44
44
|
},
|
|
45
45
|
generated: {
|
|
@@ -59,6 +59,39 @@ workflow({/* caller-owned jobs and topology */}).writeOrLint({
|
|
|
59
59
|
|
|
60
60
|
The **runner is not this package's business**. Jobs, runners, permissions, workflow topology, and deploy policy remain with the caller. The returned values are plain structural objects; this package does not depend on gagen.
|
|
61
61
|
|
|
62
|
+
### Hosted docs-only classification
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import {
|
|
66
|
+
hostedDocsOnlyDetectorStep,
|
|
67
|
+
NODE_PNPM_ACTION_FAMILY_NODE24,
|
|
68
|
+
} from "@patronage/factory-ci";
|
|
69
|
+
import { job, step } from "gagen";
|
|
70
|
+
|
|
71
|
+
import profile from "../../software-factory.profile.json" with { type: "json" };
|
|
72
|
+
|
|
73
|
+
const detector = step(
|
|
74
|
+
hostedDocsOnlyDetectorStep(profile.proof.classificationPolicy)
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const changes = job("changes", {
|
|
78
|
+
runsOn: "ubuntu-latest",
|
|
79
|
+
steps: [
|
|
80
|
+
{
|
|
81
|
+
name: "Checkout",
|
|
82
|
+
uses: NODE_PNPM_ACTION_FAMILY_NODE24.checkout.uses,
|
|
83
|
+
with: { "fetch-depth": 0 },
|
|
84
|
+
},
|
|
85
|
+
detector,
|
|
86
|
+
],
|
|
87
|
+
outputs: { docs_only: detector.outputs.docs_only },
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`hostedDocsOnlyDetectorStep()` takes the consumer's canonical `proof.classificationPolicy` object directly. It emits a small Bash step structurally accepted by gagen, matches the factory classifier's exact-file, prefix, and case-sensitive `.md` semantics, checks both paths of rename and copy records, and forces full CI when `software-factory.profile.json` changes. The package understands the stable classification-policy vocabulary without loading JSON or depending on the rest of the profile schema.
|
|
92
|
+
|
|
93
|
+
The emitted step needs only checkout, Bash, and Git. It does not install dependencies or invoke `psf`. Missing or zero SHAs, empty diffs, malformed policy, unknown Git change records, and detector errors all leave `docs_only` false (or unset), so the consumer runs full CI. Hosted classification may therefore conservatively run more work when detection fails, but it never carries a second path policy that can drift from the profile.
|
|
94
|
+
|
|
62
95
|
### Alchemy entries
|
|
63
96
|
|
|
64
97
|
```ts
|
|
@@ -89,7 +122,7 @@ The tests pack the actual tarball, extract it into a throwaway external consumer
|
|
|
89
122
|
|
|
90
123
|
## No configuration surface
|
|
91
124
|
|
|
92
|
-
Functions take plain typed
|
|
125
|
+
Functions take plain typed objects. Proof and preview configuration lives in the canonical `software-factory.profile.json` schema, never in this package (ADR 0021); consumers pass the relevant profile policy directly at generation time.
|
|
93
126
|
|
|
94
127
|
## Secrets
|
|
95
128
|
|
package/dist/index.d.ts
CHANGED
|
@@ -194,6 +194,30 @@ interface FactoryWorkflowArtifact<Additional extends Readonly<Record<string, Pin
|
|
|
194
194
|
*/
|
|
195
195
|
declare const factoryWorkflow: <const Additional extends Readonly<Record<string, PinnedAction>> = Record<never, never>>(options: FactoryWorkflowOptions<Additional>) => FactoryWorkflowArtifact<Additional>;
|
|
196
196
|
//#endregion
|
|
197
|
+
//#region src/hosted-docs-only-detector.d.ts
|
|
198
|
+
interface HostedDocsOnlyClassificationPolicy {
|
|
199
|
+
readonly docsOnlyFiles?: readonly string[];
|
|
200
|
+
readonly docsOnlyMarkdownPrefixes?: readonly string[];
|
|
201
|
+
readonly docsOnlyPrefixes?: readonly string[];
|
|
202
|
+
}
|
|
203
|
+
interface HostedDocsOnlyDetectorStep {
|
|
204
|
+
readonly continueOnError: true;
|
|
205
|
+
readonly env: Readonly<{
|
|
206
|
+
BASE_SHA: string;
|
|
207
|
+
HEAD_SHA: string;
|
|
208
|
+
}>;
|
|
209
|
+
readonly id: "docs-only";
|
|
210
|
+
readonly name: "Detect docs-only changes";
|
|
211
|
+
readonly outputs: readonly ["docs_only"];
|
|
212
|
+
readonly run: string;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Generate the pre-install hosted detector from a consumer's canonical
|
|
216
|
+
* classification policy. The returned object is structurally accepted by
|
|
217
|
+
* gagen's `step()` without adding a gagen runtime dependency.
|
|
218
|
+
*/
|
|
219
|
+
declare const hostedDocsOnlyDetectorStep: (policy: HostedDocsOnlyClassificationPolicy) => HostedDocsOnlyDetectorStep;
|
|
220
|
+
//#endregion
|
|
197
221
|
//#region src/execute-alchemy-entry.d.ts
|
|
198
222
|
interface ExecuteAlchemyEntryOptions {
|
|
199
223
|
/**
|
|
@@ -225,4 +249,4 @@ interface ExecuteAlchemyEntryResult {
|
|
|
225
249
|
*/
|
|
226
250
|
declare const executeAlchemyEntry: (options: ExecuteAlchemyEntryOptions) => Promise<ExecuteAlchemyEntryResult>;
|
|
227
251
|
//#endregion
|
|
228
|
-
export { type BundleAlchemyEntryOptions, type CheckoutStepOptions, type ExecuteAlchemyEntryOptions, type ExecuteAlchemyEntryResult, type FactoryWorkflowArtifact, type FactoryWorkflowOptions, type FactoryWorkflowSetupOptions, type InstallStepOptions, type LocalPreviewStage, type LocalPreviewStageOptions, NODE_PNPM_ACTION_FAMILY_NODE24, type NodePnpmActionFamily, type ParseLocalPreviewStageExpected, type ParsedLocalPreviewStage, type PinnedAction, type SetupNodeStepOptions, type WorkflowStep, bundleAlchemyEntry, executeAlchemyEntry, factoryWorkflow, isLocalPreviewStage, localPreviewStage, parseLocalPreviewStage };
|
|
252
|
+
export { type BundleAlchemyEntryOptions, type CheckoutStepOptions, type ExecuteAlchemyEntryOptions, type ExecuteAlchemyEntryResult, type FactoryWorkflowArtifact, type FactoryWorkflowOptions, type FactoryWorkflowSetupOptions, type HostedDocsOnlyClassificationPolicy, type HostedDocsOnlyDetectorStep, type InstallStepOptions, type LocalPreviewStage, type LocalPreviewStageOptions, NODE_PNPM_ACTION_FAMILY_NODE24, type NodePnpmActionFamily, type ParseLocalPreviewStageExpected, type ParsedLocalPreviewStage, type PinnedAction, type SetupNodeStepOptions, type WorkflowStep, bundleAlchemyEntry, executeAlchemyEntry, factoryWorkflow, hostedDocsOnlyDetectorStep, isLocalPreviewStage, localPreviewStage, parseLocalPreviewStage };
|
package/dist/index.js
CHANGED
|
@@ -210,6 +210,137 @@ const factoryWorkflow = (options) => {
|
|
|
210
210
|
});
|
|
211
211
|
};
|
|
212
212
|
//#endregion
|
|
213
|
+
//#region src/hosted-docs-only-detector.ts
|
|
214
|
+
const githubExpression = (expression) => `\${{ ${expression} }}`;
|
|
215
|
+
const BASE_SHA_EXPRESSION = githubExpression("github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before");
|
|
216
|
+
const HEAD_SHA_EXPRESSION = githubExpression("github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha");
|
|
217
|
+
const stringArray = (value) => {
|
|
218
|
+
if (value === void 0) return [];
|
|
219
|
+
if (Array.isArray(value) && value.every((entry) => typeof entry === "string" && entry.length > 0)) return value;
|
|
220
|
+
};
|
|
221
|
+
const normalizePolicy = (value) => {
|
|
222
|
+
if (!(value && typeof value === "object" && !Array.isArray(value))) return;
|
|
223
|
+
const policy = value;
|
|
224
|
+
const docsOnlyFiles = stringArray(policy.docsOnlyFiles);
|
|
225
|
+
const docsOnlyMarkdownPrefixes = stringArray(policy.docsOnlyMarkdownPrefixes);
|
|
226
|
+
const docsOnlyPrefixes = stringArray(policy.docsOnlyPrefixes);
|
|
227
|
+
if (!(docsOnlyFiles && docsOnlyMarkdownPrefixes && docsOnlyPrefixes)) return;
|
|
228
|
+
return {
|
|
229
|
+
docsOnlyFiles,
|
|
230
|
+
docsOnlyMarkdownPrefixes,
|
|
231
|
+
docsOnlyPrefixes
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
const shellQuoteSegment = (value) => `'${value.replaceAll("'", String.raw`'\''`)}'`;
|
|
235
|
+
const shellQuote = (value) => value.split("$").map(shellQuoteSegment).join("'$'");
|
|
236
|
+
const shellArray = (values) => `(${values.map(shellQuote).join(" ")})`;
|
|
237
|
+
const shellExpansion = (name) => `\${${name}}`;
|
|
238
|
+
const malformedPolicyScript = String.raw`printf 'docs_only=false\n' >> "$GITHUB_OUTPUT"`;
|
|
239
|
+
const detectorScript = (policy) => String.raw`docs_only=false
|
|
240
|
+
changed_entries_file=""
|
|
241
|
+
|
|
242
|
+
finish() {
|
|
243
|
+
detector_status=$?
|
|
244
|
+
if [ -n "$changed_entries_file" ]; then
|
|
245
|
+
rm -f "$changed_entries_file" || true
|
|
246
|
+
fi
|
|
247
|
+
if [ "$detector_status" -ne 0 ]; then
|
|
248
|
+
docs_only=false
|
|
249
|
+
fi
|
|
250
|
+
printf 'docs_only=%s\n' "$docs_only" >> "$GITHUB_OUTPUT"
|
|
251
|
+
exit 0
|
|
252
|
+
}
|
|
253
|
+
trap finish EXIT
|
|
254
|
+
set -eo pipefail
|
|
255
|
+
|
|
256
|
+
if [ -z "$BASE_SHA" ] || [ -z "$HEAD_SHA" ] ||
|
|
257
|
+
[ "$BASE_SHA" = "0000000000000000000000000000000000000000" ] ||
|
|
258
|
+
[ "$HEAD_SHA" = "0000000000000000000000000000000000000000" ]; then
|
|
259
|
+
exit 0
|
|
260
|
+
fi
|
|
261
|
+
|
|
262
|
+
changed_entries_file="$(mktemp)"
|
|
263
|
+
git diff --name-status -z --find-renames --find-copies-harder \
|
|
264
|
+
"$BASE_SHA...$HEAD_SHA" -- > "$changed_entries_file"
|
|
265
|
+
if [ ! -s "$changed_entries_file" ]; then
|
|
266
|
+
exit 0
|
|
267
|
+
fi
|
|
268
|
+
|
|
269
|
+
docs_only_files=${shellArray(policy.docsOnlyFiles)}
|
|
270
|
+
docs_only_prefixes=${shellArray(policy.docsOnlyPrefixes)}
|
|
271
|
+
docs_only_markdown_prefixes=${shellArray(policy.docsOnlyMarkdownPrefixes)}
|
|
272
|
+
|
|
273
|
+
is_docs_only_path() {
|
|
274
|
+
candidate="$1"
|
|
275
|
+
if [ "$candidate" = "software-factory.profile.json" ]; then
|
|
276
|
+
return 1
|
|
277
|
+
fi
|
|
278
|
+
for policy_path in "${shellExpansion("docs_only_files[@]")}"; do
|
|
279
|
+
if [ "$candidate" = "$policy_path" ]; then
|
|
280
|
+
return 0
|
|
281
|
+
fi
|
|
282
|
+
done
|
|
283
|
+
for policy_path in "${shellExpansion("docs_only_prefixes[@]")}"; do
|
|
284
|
+
if [[ "$candidate" == "$policy_path"* ]]; then
|
|
285
|
+
return 0
|
|
286
|
+
fi
|
|
287
|
+
done
|
|
288
|
+
if [[ "$candidate" == *.md ]]; then
|
|
289
|
+
for policy_path in "${shellExpansion("docs_only_markdown_prefixes[@]")}"; do
|
|
290
|
+
if [[ "$candidate" == "$policy_path"* ]]; then
|
|
291
|
+
return 0
|
|
292
|
+
fi
|
|
293
|
+
done
|
|
294
|
+
fi
|
|
295
|
+
return 1
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
entry_count=0
|
|
299
|
+
while IFS= read -r -d '' change_status <&3; do
|
|
300
|
+
entry_count=$((entry_count + 1))
|
|
301
|
+
case "$change_status" in
|
|
302
|
+
A|D|M)
|
|
303
|
+
IFS= read -r -d '' changed_path <&3 || exit 0
|
|
304
|
+
is_docs_only_path "$changed_path" || exit 0
|
|
305
|
+
;;
|
|
306
|
+
R*|C*)
|
|
307
|
+
if [[ ! "$change_status" =~ ^[RC][0-9]{1,3}$ ]]; then
|
|
308
|
+
exit 0
|
|
309
|
+
fi
|
|
310
|
+
IFS= read -r -d '' source_path <&3 || exit 0
|
|
311
|
+
IFS= read -r -d '' destination_path <&3 || exit 0
|
|
312
|
+
is_docs_only_path "$source_path" || exit 0
|
|
313
|
+
is_docs_only_path "$destination_path" || exit 0
|
|
314
|
+
;;
|
|
315
|
+
*)
|
|
316
|
+
exit 0
|
|
317
|
+
;;
|
|
318
|
+
esac
|
|
319
|
+
done 3< "$changed_entries_file"
|
|
320
|
+
|
|
321
|
+
if [ "$entry_count" -gt 0 ]; then
|
|
322
|
+
docs_only=true
|
|
323
|
+
fi`;
|
|
324
|
+
/**
|
|
325
|
+
* Generate the pre-install hosted detector from a consumer's canonical
|
|
326
|
+
* classification policy. The returned object is structurally accepted by
|
|
327
|
+
* gagen's `step()` without adding a gagen runtime dependency.
|
|
328
|
+
*/
|
|
329
|
+
const hostedDocsOnlyDetectorStep = (policy) => {
|
|
330
|
+
const normalized = normalizePolicy(policy);
|
|
331
|
+
return Object.freeze({
|
|
332
|
+
continueOnError: true,
|
|
333
|
+
env: Object.freeze({
|
|
334
|
+
BASE_SHA: BASE_SHA_EXPRESSION,
|
|
335
|
+
HEAD_SHA: HEAD_SHA_EXPRESSION
|
|
336
|
+
}),
|
|
337
|
+
id: "docs-only",
|
|
338
|
+
name: "Detect docs-only changes",
|
|
339
|
+
outputs: ["docs_only"],
|
|
340
|
+
run: normalized ? detectorScript(normalized) : malformedPolicyScript
|
|
341
|
+
});
|
|
342
|
+
};
|
|
343
|
+
//#endregion
|
|
213
344
|
//#region src/execute-alchemy-entry.ts
|
|
214
345
|
/**
|
|
215
346
|
* Bundle a consumer-owned Alchemy entry, resolve that consumer's Alchemy CLI,
|
|
@@ -241,4 +372,4 @@ const executeAlchemyEntry = async (options) => {
|
|
|
241
372
|
};
|
|
242
373
|
};
|
|
243
374
|
//#endregion
|
|
244
|
-
export { NODE_PNPM_ACTION_FAMILY_NODE24, bundleAlchemyEntry, executeAlchemyEntry, factoryWorkflow, isLocalPreviewStage, localPreviewStage, parseLocalPreviewStage };
|
|
375
|
+
export { NODE_PNPM_ACTION_FAMILY_NODE24, bundleAlchemyEntry, executeAlchemyEntry, factoryWorkflow, hostedDocsOnlyDetectorStep, isLocalPreviewStage, localPreviewStage, parseLocalPreviewStage };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patronage/factory-ci",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Deep CI and deploy building blocks for Patronage factory projects: workflow source artifacts, Alchemy entry execution, and disposable-stage semantics",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Deep CI and deploy building blocks for Patronage factory projects: workflow source artifacts, hosted diff classification, Alchemy entry execution, and disposable-stage semantics",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"alchemy",
|
|
7
7
|
"cloudflare",
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
const githubExpression = (expression: string): string =>
|
|
2
|
+
`\${{ ${expression} }}`;
|
|
3
|
+
|
|
4
|
+
const BASE_SHA_EXPRESSION = githubExpression(
|
|
5
|
+
"github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before"
|
|
6
|
+
);
|
|
7
|
+
const HEAD_SHA_EXPRESSION = githubExpression(
|
|
8
|
+
"github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha"
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
export interface HostedDocsOnlyClassificationPolicy {
|
|
12
|
+
readonly docsOnlyFiles?: readonly string[];
|
|
13
|
+
readonly docsOnlyMarkdownPrefixes?: readonly string[];
|
|
14
|
+
readonly docsOnlyPrefixes?: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface NormalizedHostedDocsOnlyClassificationPolicy {
|
|
18
|
+
readonly docsOnlyFiles: readonly string[];
|
|
19
|
+
readonly docsOnlyMarkdownPrefixes: readonly string[];
|
|
20
|
+
readonly docsOnlyPrefixes: readonly string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface HostedDocsOnlyDetectorStep {
|
|
24
|
+
readonly continueOnError: true;
|
|
25
|
+
readonly env: Readonly<{
|
|
26
|
+
BASE_SHA: string;
|
|
27
|
+
HEAD_SHA: string;
|
|
28
|
+
}>;
|
|
29
|
+
readonly id: "docs-only";
|
|
30
|
+
readonly name: "Detect docs-only changes";
|
|
31
|
+
readonly outputs: readonly ["docs_only"];
|
|
32
|
+
readonly run: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const stringArray = (value: unknown): readonly string[] | undefined => {
|
|
36
|
+
if (value === undefined) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
if (
|
|
40
|
+
Array.isArray(value) &&
|
|
41
|
+
value.every((entry) => typeof entry === "string" && entry.length > 0)
|
|
42
|
+
) {
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const normalizePolicy = (
|
|
48
|
+
value: unknown
|
|
49
|
+
): NormalizedHostedDocsOnlyClassificationPolicy | undefined => {
|
|
50
|
+
if (!(value && typeof value === "object" && !Array.isArray(value))) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const policy = value as Record<string, unknown>;
|
|
55
|
+
const docsOnlyFiles = stringArray(policy.docsOnlyFiles);
|
|
56
|
+
const docsOnlyMarkdownPrefixes = stringArray(policy.docsOnlyMarkdownPrefixes);
|
|
57
|
+
const docsOnlyPrefixes = stringArray(policy.docsOnlyPrefixes);
|
|
58
|
+
|
|
59
|
+
if (!(docsOnlyFiles && docsOnlyMarkdownPrefixes && docsOnlyPrefixes)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
docsOnlyFiles,
|
|
65
|
+
docsOnlyMarkdownPrefixes,
|
|
66
|
+
docsOnlyPrefixes,
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const shellQuoteSegment = (value: string): string =>
|
|
71
|
+
`'${value.replaceAll("'", String.raw`'\''`)}'`;
|
|
72
|
+
|
|
73
|
+
const shellQuote = (value: string): string =>
|
|
74
|
+
value.split("$").map(shellQuoteSegment).join("'$'");
|
|
75
|
+
|
|
76
|
+
const shellArray = (values: readonly string[]): string =>
|
|
77
|
+
`(${values.map(shellQuote).join(" ")})`;
|
|
78
|
+
|
|
79
|
+
const shellExpansion = (name: string): string => `\${${name}}`;
|
|
80
|
+
|
|
81
|
+
const malformedPolicyScript = String.raw`printf 'docs_only=false\n' >> "$GITHUB_OUTPUT"`;
|
|
82
|
+
|
|
83
|
+
const detectorScript = (
|
|
84
|
+
policy: NormalizedHostedDocsOnlyClassificationPolicy
|
|
85
|
+
): string => String.raw`docs_only=false
|
|
86
|
+
changed_entries_file=""
|
|
87
|
+
|
|
88
|
+
finish() {
|
|
89
|
+
detector_status=$?
|
|
90
|
+
if [ -n "$changed_entries_file" ]; then
|
|
91
|
+
rm -f "$changed_entries_file" || true
|
|
92
|
+
fi
|
|
93
|
+
if [ "$detector_status" -ne 0 ]; then
|
|
94
|
+
docs_only=false
|
|
95
|
+
fi
|
|
96
|
+
printf 'docs_only=%s\n' "$docs_only" >> "$GITHUB_OUTPUT"
|
|
97
|
+
exit 0
|
|
98
|
+
}
|
|
99
|
+
trap finish EXIT
|
|
100
|
+
set -eo pipefail
|
|
101
|
+
|
|
102
|
+
if [ -z "$BASE_SHA" ] || [ -z "$HEAD_SHA" ] ||
|
|
103
|
+
[ "$BASE_SHA" = "0000000000000000000000000000000000000000" ] ||
|
|
104
|
+
[ "$HEAD_SHA" = "0000000000000000000000000000000000000000" ]; then
|
|
105
|
+
exit 0
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
changed_entries_file="$(mktemp)"
|
|
109
|
+
git diff --name-status -z --find-renames --find-copies-harder \
|
|
110
|
+
"$BASE_SHA...$HEAD_SHA" -- > "$changed_entries_file"
|
|
111
|
+
if [ ! -s "$changed_entries_file" ]; then
|
|
112
|
+
exit 0
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
docs_only_files=${shellArray(policy.docsOnlyFiles)}
|
|
116
|
+
docs_only_prefixes=${shellArray(policy.docsOnlyPrefixes)}
|
|
117
|
+
docs_only_markdown_prefixes=${shellArray(policy.docsOnlyMarkdownPrefixes)}
|
|
118
|
+
|
|
119
|
+
is_docs_only_path() {
|
|
120
|
+
candidate="$1"
|
|
121
|
+
if [ "$candidate" = "software-factory.profile.json" ]; then
|
|
122
|
+
return 1
|
|
123
|
+
fi
|
|
124
|
+
for policy_path in "${shellExpansion("docs_only_files[@]")}"; do
|
|
125
|
+
if [ "$candidate" = "$policy_path" ]; then
|
|
126
|
+
return 0
|
|
127
|
+
fi
|
|
128
|
+
done
|
|
129
|
+
for policy_path in "${shellExpansion("docs_only_prefixes[@]")}"; do
|
|
130
|
+
if [[ "$candidate" == "$policy_path"* ]]; then
|
|
131
|
+
return 0
|
|
132
|
+
fi
|
|
133
|
+
done
|
|
134
|
+
if [[ "$candidate" == *.md ]]; then
|
|
135
|
+
for policy_path in "${shellExpansion("docs_only_markdown_prefixes[@]")}"; do
|
|
136
|
+
if [[ "$candidate" == "$policy_path"* ]]; then
|
|
137
|
+
return 0
|
|
138
|
+
fi
|
|
139
|
+
done
|
|
140
|
+
fi
|
|
141
|
+
return 1
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
entry_count=0
|
|
145
|
+
while IFS= read -r -d '' change_status <&3; do
|
|
146
|
+
entry_count=$((entry_count + 1))
|
|
147
|
+
case "$change_status" in
|
|
148
|
+
A|D|M)
|
|
149
|
+
IFS= read -r -d '' changed_path <&3 || exit 0
|
|
150
|
+
is_docs_only_path "$changed_path" || exit 0
|
|
151
|
+
;;
|
|
152
|
+
R*|C*)
|
|
153
|
+
if [[ ! "$change_status" =~ ^[RC][0-9]{1,3}$ ]]; then
|
|
154
|
+
exit 0
|
|
155
|
+
fi
|
|
156
|
+
IFS= read -r -d '' source_path <&3 || exit 0
|
|
157
|
+
IFS= read -r -d '' destination_path <&3 || exit 0
|
|
158
|
+
is_docs_only_path "$source_path" || exit 0
|
|
159
|
+
is_docs_only_path "$destination_path" || exit 0
|
|
160
|
+
;;
|
|
161
|
+
*)
|
|
162
|
+
exit 0
|
|
163
|
+
;;
|
|
164
|
+
esac
|
|
165
|
+
done 3< "$changed_entries_file"
|
|
166
|
+
|
|
167
|
+
if [ "$entry_count" -gt 0 ]; then
|
|
168
|
+
docs_only=true
|
|
169
|
+
fi`;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Generate the pre-install hosted detector from a consumer's canonical
|
|
173
|
+
* classification policy. The returned object is structurally accepted by
|
|
174
|
+
* gagen's `step()` without adding a gagen runtime dependency.
|
|
175
|
+
*/
|
|
176
|
+
export const hostedDocsOnlyDetectorStep = (
|
|
177
|
+
policy: HostedDocsOnlyClassificationPolicy
|
|
178
|
+
): HostedDocsOnlyDetectorStep => {
|
|
179
|
+
const normalized = normalizePolicy(policy);
|
|
180
|
+
|
|
181
|
+
return Object.freeze({
|
|
182
|
+
continueOnError: true as const,
|
|
183
|
+
env: Object.freeze({
|
|
184
|
+
BASE_SHA: BASE_SHA_EXPRESSION,
|
|
185
|
+
HEAD_SHA: HEAD_SHA_EXPRESSION,
|
|
186
|
+
}),
|
|
187
|
+
id: "docs-only" as const,
|
|
188
|
+
name: "Detect docs-only changes" as const,
|
|
189
|
+
outputs: ["docs_only"] as const,
|
|
190
|
+
run: normalized ? detectorScript(normalized) : malformedPolicyScript,
|
|
191
|
+
});
|
|
192
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* repeat across Patronage factory projects (#266, #268).
|
|
4
4
|
*
|
|
5
5
|
* A pure library: no `bin`, no `alchemy` or `effect` dependency, no config
|
|
6
|
-
* surface of its own. Everything takes
|
|
7
|
-
* `software-factory.profile.json`
|
|
8
|
-
*
|
|
6
|
+
* surface of its own. Everything takes plain typed values; callers pass
|
|
7
|
+
* canonical policy from `software-factory.profile.json` without this package
|
|
8
|
+
* loading or owning that profile (ADR 0021).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
export {
|
|
@@ -36,6 +36,11 @@ export {
|
|
|
36
36
|
type SetupNodeStepOptions,
|
|
37
37
|
type WorkflowStep,
|
|
38
38
|
} from "./factory-workflow.ts";
|
|
39
|
+
export {
|
|
40
|
+
type HostedDocsOnlyClassificationPolicy,
|
|
41
|
+
type HostedDocsOnlyDetectorStep,
|
|
42
|
+
hostedDocsOnlyDetectorStep,
|
|
43
|
+
} from "./hosted-docs-only-detector.ts";
|
|
39
44
|
export {
|
|
40
45
|
type ExecuteAlchemyEntryOptions,
|
|
41
46
|
type ExecuteAlchemyEntryResult,
|