@savvy-web/github-action-builder 0.7.3 → 0.7.5
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/bin/github-action-builder.d.ts +1 -0
- package/bin/github-action-builder.js +39 -307
- package/cli/commands/build.js +91 -0
- package/cli/commands/index.js +5 -0
- package/cli/commands/init.js +247 -0
- package/cli/commands/validate.js +42 -0
- package/errors.js +284 -0
- package/github-action.js +302 -0
- package/index.d.ts +1567 -1820
- package/index.js +10 -134
- package/layers/app.js +83 -0
- package/package.json +74 -90
- package/schemas/action-yml.js +110 -0
- package/schemas/config.js +190 -0
- package/schemas/path.js +43 -0
- package/services/build-live.js +223 -0
- package/services/build.js +63 -0
- package/services/config-live.js +111 -0
- package/services/config.js +63 -0
- package/services/persist-local-live.js +210 -0
- package/services/persist-local.js +37 -0
- package/services/validation-live.js +216 -0
- package/services/validation.js +77 -0
- package/tsdoc-metadata.json +11 -11
- package/231.js +0 -5
- package/612.js +0 -5
- package/948.js +0 -931
- /package/{tsconfig → public/tsconfig}/action.json +0 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { ActionYmlPathError, PersistLocalError } from "../errors.js";
|
|
2
|
+
import { PersistLocalService } from "./persist-local.js";
|
|
3
|
+
import { Effect, Layer } from "effect";
|
|
4
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { parse } from "yaml-effect";
|
|
8
|
+
|
|
9
|
+
//#region src/services/persist-local-live.ts
|
|
10
|
+
/**
|
|
11
|
+
* PersistLocalService Layer implementation.
|
|
12
|
+
*
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Compute SHA-256 hash of a file's contents.
|
|
16
|
+
*/
|
|
17
|
+
function fileHash(filePath) {
|
|
18
|
+
const content = readFileSync(filePath);
|
|
19
|
+
return createHash("sha256").update(content).digest("hex");
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Sync a single file from src to dest using hash comparison.
|
|
23
|
+
* Returns true if the file was copied, false if skipped.
|
|
24
|
+
*/
|
|
25
|
+
function syncFile(src, dest) {
|
|
26
|
+
if (existsSync(dest)) {
|
|
27
|
+
if (fileHash(src) === fileHash(dest)) return false;
|
|
28
|
+
}
|
|
29
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
30
|
+
copyFileSync(src, dest);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Recursively collect all file paths relative to a base directory.
|
|
35
|
+
*/
|
|
36
|
+
function walkDirectory(dir, base = dir) {
|
|
37
|
+
const files = [];
|
|
38
|
+
if (!existsSync(dir)) return files;
|
|
39
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
40
|
+
const fullPath = join(dir, entry.name);
|
|
41
|
+
if (entry.isDirectory()) files.push(...walkDirectory(fullPath, base));
|
|
42
|
+
else files.push(relative(base, fullPath));
|
|
43
|
+
}
|
|
44
|
+
return files;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Smart-sync a directory: copy changed files, remove stale dest files.
|
|
48
|
+
*/
|
|
49
|
+
function syncDirectory(srcDir, destDir) {
|
|
50
|
+
const stats = {
|
|
51
|
+
copied: 0,
|
|
52
|
+
skipped: 0
|
|
53
|
+
};
|
|
54
|
+
const srcFiles = walkDirectory(srcDir);
|
|
55
|
+
for (const relPath of srcFiles) if (syncFile(join(srcDir, relPath), join(destDir, relPath))) stats.copied++;
|
|
56
|
+
else stats.skipped++;
|
|
57
|
+
const srcFileSet = new Set(srcFiles);
|
|
58
|
+
const destFiles = walkDirectory(destDir);
|
|
59
|
+
for (const relPath of destFiles) if (!srcFileSet.has(relPath)) {
|
|
60
|
+
rmSync(join(destDir, relPath), { force: true });
|
|
61
|
+
let parent = dirname(join(destDir, relPath));
|
|
62
|
+
while (parent !== destDir && existsSync(parent)) if (readdirSync(parent).length === 0) {
|
|
63
|
+
rmSync(parent, { recursive: true });
|
|
64
|
+
parent = dirname(parent);
|
|
65
|
+
} else break;
|
|
66
|
+
}
|
|
67
|
+
return stats;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Validate that action.yml runs paths resolve correctly relative to the destination.
|
|
71
|
+
*/
|
|
72
|
+
function validateActionYmlPaths(actionYmlPath, destDir) {
|
|
73
|
+
return Effect.gen(function* () {
|
|
74
|
+
if (!existsSync(actionYmlPath)) return;
|
|
75
|
+
const parsed = yield* parse(readFileSync(actionYmlPath, "utf8")).pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
76
|
+
if (!parsed?.runs) return;
|
|
77
|
+
for (const entryType of [
|
|
78
|
+
"main",
|
|
79
|
+
"pre",
|
|
80
|
+
"post"
|
|
81
|
+
]) {
|
|
82
|
+
const specifiedPath = parsed.runs[entryType];
|
|
83
|
+
if (!specifiedPath) continue;
|
|
84
|
+
const expectedPath = resolve(destDir, specifiedPath);
|
|
85
|
+
if (!existsSync(expectedPath)) return yield* Effect.fail(new ActionYmlPathError({
|
|
86
|
+
entryType,
|
|
87
|
+
specifiedPath,
|
|
88
|
+
expectedPath
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const ACTRC_CONTENT = `--container-architecture linux/amd64
|
|
94
|
+
-W .github/workflows/act-test.yml
|
|
95
|
+
`;
|
|
96
|
+
const ACT_WORKFLOW_CONTENT = `name: Local Test
|
|
97
|
+
on:
|
|
98
|
+
workflow_dispatch:
|
|
99
|
+
|
|
100
|
+
jobs:
|
|
101
|
+
test:
|
|
102
|
+
runs-on: ubuntu-latest
|
|
103
|
+
steps:
|
|
104
|
+
- uses: actions/checkout@v6
|
|
105
|
+
- uses: ./.github/actions/local
|
|
106
|
+
`;
|
|
107
|
+
function formatPersistResult(result) {
|
|
108
|
+
const lines = [];
|
|
109
|
+
if (result.success) {
|
|
110
|
+
lines.push("Persist Local Summary:");
|
|
111
|
+
lines.push(` Output: ${result.outputPath}`);
|
|
112
|
+
lines.push(` Files copied: ${result.filesCopied}`);
|
|
113
|
+
lines.push(` Files skipped (unchanged): ${result.filesSkipped}`);
|
|
114
|
+
if (result.actTemplateGenerated) lines.push(" Act template files generated");
|
|
115
|
+
} else lines.push(`Persist Local Failed: ${result.error}`);
|
|
116
|
+
return lines.join("\n");
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Live implementation of PersistLocalService.
|
|
120
|
+
*/
|
|
121
|
+
const PersistLocalServiceLive = Layer.succeed(PersistLocalService, {
|
|
122
|
+
persist: (config, options = {}) => Effect.gen(function* () {
|
|
123
|
+
const cwd = options.cwd ?? process.cwd();
|
|
124
|
+
const outputPath = resolve(cwd, config.persistLocal.path);
|
|
125
|
+
if (!config.persistLocal.enabled) return {
|
|
126
|
+
success: true,
|
|
127
|
+
filesCopied: 0,
|
|
128
|
+
filesSkipped: 0,
|
|
129
|
+
actTemplateGenerated: false,
|
|
130
|
+
outputPath
|
|
131
|
+
};
|
|
132
|
+
yield* Effect.try({
|
|
133
|
+
try: () => mkdirSync(outputPath, { recursive: true }),
|
|
134
|
+
/* v8 ignore next 5 - error branch requires fs permission failures */
|
|
135
|
+
catch: (error) => new PersistLocalError({
|
|
136
|
+
path: outputPath,
|
|
137
|
+
cause: error
|
|
138
|
+
})
|
|
139
|
+
});
|
|
140
|
+
let totalCopied = 0;
|
|
141
|
+
let totalSkipped = 0;
|
|
142
|
+
const actionYmlSrc = resolve(cwd, "action.yml");
|
|
143
|
+
const actionYmlDest = resolve(outputPath, "action.yml");
|
|
144
|
+
if (existsSync(actionYmlSrc)) if (yield* Effect.try({
|
|
145
|
+
try: () => syncFile(actionYmlSrc, actionYmlDest),
|
|
146
|
+
/* v8 ignore next 5 - error branch requires fs permission failures */
|
|
147
|
+
catch: (error) => new PersistLocalError({
|
|
148
|
+
path: actionYmlSrc,
|
|
149
|
+
cause: error
|
|
150
|
+
})
|
|
151
|
+
})) totalCopied++;
|
|
152
|
+
else totalSkipped++;
|
|
153
|
+
else if (existsSync(actionYmlDest)) rmSync(actionYmlDest, { force: true });
|
|
154
|
+
const distSrc = resolve(cwd, "dist");
|
|
155
|
+
if (existsSync(distSrc) && statSync(distSrc).isDirectory()) {
|
|
156
|
+
const distStats = yield* Effect.try({
|
|
157
|
+
try: () => syncDirectory(distSrc, resolve(outputPath, "dist")),
|
|
158
|
+
/* v8 ignore next 5 - error branch requires fs permission failures */
|
|
159
|
+
catch: (error) => new PersistLocalError({
|
|
160
|
+
path: distSrc,
|
|
161
|
+
cause: error
|
|
162
|
+
})
|
|
163
|
+
});
|
|
164
|
+
totalCopied += distStats.copied;
|
|
165
|
+
totalSkipped += distStats.skipped;
|
|
166
|
+
}
|
|
167
|
+
yield* validateActionYmlPaths(resolve(outputPath, "action.yml"), outputPath);
|
|
168
|
+
let actTemplateGenerated = false;
|
|
169
|
+
if (config.persistLocal.actTemplate) {
|
|
170
|
+
const actrcPath = resolve(cwd, ".actrc");
|
|
171
|
+
const actWorkflowPath = resolve(cwd, ".github/workflows/act-test.yml");
|
|
172
|
+
if (!existsSync(actrcPath)) {
|
|
173
|
+
yield* Effect.try({
|
|
174
|
+
try: () => writeFileSync(actrcPath, ACTRC_CONTENT, "utf8"),
|
|
175
|
+
/* v8 ignore next 5 - error branch requires fs permission failures */
|
|
176
|
+
catch: (error) => new PersistLocalError({
|
|
177
|
+
path: actrcPath,
|
|
178
|
+
cause: error
|
|
179
|
+
})
|
|
180
|
+
});
|
|
181
|
+
actTemplateGenerated = true;
|
|
182
|
+
}
|
|
183
|
+
if (!existsSync(actWorkflowPath)) {
|
|
184
|
+
yield* Effect.try({
|
|
185
|
+
try: () => {
|
|
186
|
+
mkdirSync(dirname(actWorkflowPath), { recursive: true });
|
|
187
|
+
writeFileSync(actWorkflowPath, ACT_WORKFLOW_CONTENT, "utf8");
|
|
188
|
+
},
|
|
189
|
+
/* v8 ignore next 5 - error branch requires fs permission failures */
|
|
190
|
+
catch: (error) => new PersistLocalError({
|
|
191
|
+
path: actWorkflowPath,
|
|
192
|
+
cause: error
|
|
193
|
+
})
|
|
194
|
+
});
|
|
195
|
+
actTemplateGenerated = true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
success: true,
|
|
200
|
+
filesCopied: totalCopied,
|
|
201
|
+
filesSkipped: totalSkipped,
|
|
202
|
+
actTemplateGenerated,
|
|
203
|
+
outputPath
|
|
204
|
+
};
|
|
205
|
+
}),
|
|
206
|
+
formatResult: formatPersistResult
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
//#endregion
|
|
210
|
+
export { PersistLocalServiceLive };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Context, Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/services/persist-local.ts
|
|
4
|
+
/**
|
|
5
|
+
* Options for the persist operation.
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
const PersistLocalRunnerOptionsSchema = Schema.Struct({
|
|
9
|
+
/** Working directory. Accepts string. */
|
|
10
|
+
cwd: Schema.optional(Schema.String) });
|
|
11
|
+
/**
|
|
12
|
+
* Result of the persist-local operation.
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
const PersistLocalResultSchema = Schema.Struct({
|
|
16
|
+
/** Whether the operation completed successfully. */
|
|
17
|
+
success: Schema.Boolean,
|
|
18
|
+
/** Number of files copied (changed or new). */
|
|
19
|
+
filesCopied: Schema.Number,
|
|
20
|
+
/** Number of files skipped (unchanged). */
|
|
21
|
+
filesSkipped: Schema.Number,
|
|
22
|
+
/** Whether act template files were generated. */
|
|
23
|
+
actTemplateGenerated: Schema.Boolean,
|
|
24
|
+
/** Output path where files were persisted. */
|
|
25
|
+
outputPath: Schema.String,
|
|
26
|
+
/** Error message if failed. */
|
|
27
|
+
error: Schema.optional(Schema.String)
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* PersistLocalService tag for dependency injection.
|
|
31
|
+
*
|
|
32
|
+
* @public
|
|
33
|
+
*/
|
|
34
|
+
const PersistLocalService = Context.GenericTag("PersistLocalService");
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
export { PersistLocalResultSchema, PersistLocalRunnerOptionsSchema, PersistLocalService };
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { ActionYmlMissing, ActionYmlSchemaError, ActionYmlSyntaxError, MainEntryMissing, ValidationFailed } from "../errors.js";
|
|
2
|
+
import { ConfigService } from "./config.js";
|
|
3
|
+
import { ActionYml } from "../schemas/action-yml.js";
|
|
4
|
+
import { ValidationService } from "./validation.js";
|
|
5
|
+
import { Effect, Layer, ParseResult, Schema } from "effect";
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
import { parse } from "yaml-effect";
|
|
9
|
+
|
|
10
|
+
//#region src/services/validation-live.ts
|
|
11
|
+
/**
|
|
12
|
+
* ValidationService Layer implementation.
|
|
13
|
+
*/
|
|
14
|
+
/* v8 ignore start - CI environment detection has multiple env var formats */
|
|
15
|
+
/** Check if running in CI environment. */
|
|
16
|
+
const isCI = () => process.env.CI === "true" || process.env.CI === "1" || process.env.GITHUB_ACTIONS === "true";
|
|
17
|
+
/** Resolve strict mode from config or environment. */
|
|
18
|
+
const resolveStrict = (configStrict) => configStrict ?? isCI();
|
|
19
|
+
/* v8 ignore stop */
|
|
20
|
+
/** Create a validation warning, omitting undefined file. */
|
|
21
|
+
const makeWarning = (code, message, suggestion, file) => file !== void 0 ? {
|
|
22
|
+
code,
|
|
23
|
+
message,
|
|
24
|
+
suggestion,
|
|
25
|
+
file
|
|
26
|
+
} : {
|
|
27
|
+
code,
|
|
28
|
+
message,
|
|
29
|
+
suggestion
|
|
30
|
+
};
|
|
31
|
+
/** Format schema parse errors. */
|
|
32
|
+
/* v8 ignore start - only called for schema validation errors */
|
|
33
|
+
const formatSchemaErrors = (error, filePath) => [{
|
|
34
|
+
path: filePath,
|
|
35
|
+
message: ParseResult.TreeFormatter.formatErrorSync(error)
|
|
36
|
+
}];
|
|
37
|
+
/* v8 ignore stop */
|
|
38
|
+
/**
|
|
39
|
+
* Live implementation of ValidationService.
|
|
40
|
+
*/
|
|
41
|
+
const ValidationServiceLive = Layer.effect(ValidationService, Effect.gen(function* () {
|
|
42
|
+
const configService = yield* ConfigService;
|
|
43
|
+
/** Read and parse action.yml file. */
|
|
44
|
+
const readActionYml = (path) => Effect.gen(function* () {
|
|
45
|
+
if (!existsSync(path)) return yield* new ActionYmlMissing({ cwd: path });
|
|
46
|
+
const parsed = yield* parse(yield* Effect.try({
|
|
47
|
+
try: () => readFileSync(path, "utf8"),
|
|
48
|
+
/* v8 ignore next */
|
|
49
|
+
catch: () => new ActionYmlSyntaxError({
|
|
50
|
+
path,
|
|
51
|
+
message: "Failed to read file"
|
|
52
|
+
})
|
|
53
|
+
})).pipe(
|
|
54
|
+
/* v8 ignore next 5 - requires malformed YAML */
|
|
55
|
+
Effect.mapError((error) => new ActionYmlSyntaxError({
|
|
56
|
+
path,
|
|
57
|
+
message: error.message
|
|
58
|
+
}))
|
|
59
|
+
);
|
|
60
|
+
/* v8 ignore start - requires non-object YAML (e.g., scalar or array) */
|
|
61
|
+
if (!parsed || typeof parsed !== "object") return yield* new ActionYmlSyntaxError({
|
|
62
|
+
path,
|
|
63
|
+
message: "action.yml must be an object"
|
|
64
|
+
});
|
|
65
|
+
/* v8 ignore stop */
|
|
66
|
+
return parsed;
|
|
67
|
+
});
|
|
68
|
+
/** Validate parsed content against ActionYml schema. */
|
|
69
|
+
/* v8 ignore start - schema validation error branch */
|
|
70
|
+
const validateSchema = (parsed, path) => Effect.gen(function* () {
|
|
71
|
+
const result = Schema.decodeUnknownEither(ActionYml)(parsed);
|
|
72
|
+
if (result._tag === "Left") return yield* new ActionYmlSchemaError({
|
|
73
|
+
path,
|
|
74
|
+
errors: formatSchemaErrors(result.left, path)
|
|
75
|
+
});
|
|
76
|
+
return result.right;
|
|
77
|
+
});
|
|
78
|
+
/* v8 ignore stop */
|
|
79
|
+
/** Check for recommended fields and generate warnings. */
|
|
80
|
+
/* v8 ignore start - recommendation checks have many branches */
|
|
81
|
+
const checkRecommendations = (content, filePath) => {
|
|
82
|
+
const warnings = [];
|
|
83
|
+
if (!content.branding) warnings.push(makeWarning("ACTION_YML_NO_BRANDING", "No branding configuration found", "Add branding.icon and branding.color for better marketplace visibility", filePath));
|
|
84
|
+
else {
|
|
85
|
+
const branding = content.branding;
|
|
86
|
+
if (!branding.icon) warnings.push(makeWarning("ACTION_YML_NO_BRANDING_ICON", "Branding icon not specified", "Add branding.icon for better marketplace visibility", filePath));
|
|
87
|
+
if (!branding.color) warnings.push(makeWarning("ACTION_YML_NO_BRANDING_COLOR", "Branding color not specified", "Add branding.color for better marketplace visibility", filePath));
|
|
88
|
+
}
|
|
89
|
+
if (content.inputs) {
|
|
90
|
+
const inputs = content.inputs;
|
|
91
|
+
for (const [name, input] of Object.entries(inputs)) if (!input.description) warnings.push(makeWarning("ACTION_YML_INPUT_NO_DESCRIPTION", `Input '${name}' has no description`, `Add a description for the '${name}' input`, filePath));
|
|
92
|
+
}
|
|
93
|
+
if (content.outputs) {
|
|
94
|
+
const outputs = content.outputs;
|
|
95
|
+
for (const [name, output] of Object.entries(outputs)) if (!output.description) warnings.push(makeWarning("ACTION_YML_OUTPUT_NO_DESCRIPTION", `Output '${name}' has no description`, `Add a description for the '${name}' output`, filePath));
|
|
96
|
+
}
|
|
97
|
+
return warnings;
|
|
98
|
+
};
|
|
99
|
+
/* v8 ignore stop */
|
|
100
|
+
/** Validate action.yml file completely. */
|
|
101
|
+
const validateActionYml = (path) => Effect.gen(function* () {
|
|
102
|
+
const parsed = yield* readActionYml(path);
|
|
103
|
+
return {
|
|
104
|
+
valid: true,
|
|
105
|
+
content: yield* validateSchema(parsed, path),
|
|
106
|
+
errors: [],
|
|
107
|
+
warnings: checkRecommendations(parsed, path)
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
/** Check entry points exist. */
|
|
111
|
+
const checkEntries = (config, cwd) => Effect.gen(function* () {
|
|
112
|
+
const errors = [];
|
|
113
|
+
const entriesConfig = { main: config.entries.main };
|
|
114
|
+
if (config.entries.pre) entriesConfig.pre = config.entries.pre;
|
|
115
|
+
if (config.entries.post) entriesConfig.post = config.entries.post;
|
|
116
|
+
const result = yield* Effect.either(configService.detectEntries(cwd, entriesConfig));
|
|
117
|
+
/* v8 ignore start - error branch requires missing main entry */
|
|
118
|
+
if (result._tag === "Left" && result.left instanceof MainEntryMissing) errors.push({
|
|
119
|
+
code: "MAIN_ENTRY_MISSING",
|
|
120
|
+
message: `Main entry point not found: ${result.left.expectedPath}`,
|
|
121
|
+
file: result.left.expectedPath,
|
|
122
|
+
suggestion: "Create src/main.ts or specify a different path in config"
|
|
123
|
+
});
|
|
124
|
+
/* v8 ignore stop */
|
|
125
|
+
return errors;
|
|
126
|
+
});
|
|
127
|
+
/** Check action.yml and collect errors/warnings. */
|
|
128
|
+
/* v8 ignore start - action.yml validation has many error branches */
|
|
129
|
+
const checkActionYml = (config, cwd) => Effect.gen(function* () {
|
|
130
|
+
const errors = [];
|
|
131
|
+
const warnings = [];
|
|
132
|
+
if (!config.validation.requireActionYml) return {
|
|
133
|
+
errors,
|
|
134
|
+
warnings
|
|
135
|
+
};
|
|
136
|
+
const actionYmlPath = resolve(cwd, "action.yml");
|
|
137
|
+
const result = yield* Effect.either(validateActionYml(actionYmlPath));
|
|
138
|
+
if (result._tag === "Left") {
|
|
139
|
+
const error = result.left;
|
|
140
|
+
if (error instanceof ActionYmlMissing) warnings.push({
|
|
141
|
+
code: "ACTION_YML_MISSING",
|
|
142
|
+
message: "action.yml not found",
|
|
143
|
+
file: actionYmlPath,
|
|
144
|
+
suggestion: "Create action.yml to define your action metadata"
|
|
145
|
+
});
|
|
146
|
+
else if (error instanceof ActionYmlSyntaxError) errors.push({
|
|
147
|
+
code: "ACTION_YML_SYNTAX_ERROR",
|
|
148
|
+
message: error.message,
|
|
149
|
+
file: error.path
|
|
150
|
+
});
|
|
151
|
+
else if (error instanceof ActionYmlSchemaError) for (const schemaError of error.errors) errors.push({
|
|
152
|
+
code: "ACTION_YML_SCHEMA_ERROR",
|
|
153
|
+
message: schemaError.message,
|
|
154
|
+
file: error.path
|
|
155
|
+
});
|
|
156
|
+
} else warnings.push(...result.right.warnings);
|
|
157
|
+
return {
|
|
158
|
+
errors,
|
|
159
|
+
warnings
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
/* v8 ignore stop */
|
|
163
|
+
return {
|
|
164
|
+
validate: (config, options = {}) => Effect.gen(function* () {
|
|
165
|
+
const cwd = options.cwd ?? process.cwd();
|
|
166
|
+
const strict = resolveStrict(options.strict ?? config.validation.strict);
|
|
167
|
+
const entryErrors = yield* checkEntries(config, cwd);
|
|
168
|
+
const actionYmlResult = yield* checkActionYml(config, cwd);
|
|
169
|
+
const errors = [...entryErrors, ...actionYmlResult.errors];
|
|
170
|
+
const warnings = [...actionYmlResult.warnings];
|
|
171
|
+
const valid = errors.length === 0 && (!strict || warnings.length === 0);
|
|
172
|
+
/* v8 ignore start - strict mode branch requires CI environment */
|
|
173
|
+
if (strict && warnings.length > 0 && errors.length === 0) return yield* new ValidationFailed({
|
|
174
|
+
errorCount: 0,
|
|
175
|
+
warningCount: warnings.length,
|
|
176
|
+
message: "Warnings treated as errors in strict mode"
|
|
177
|
+
});
|
|
178
|
+
/* v8 ignore stop */
|
|
179
|
+
return {
|
|
180
|
+
valid,
|
|
181
|
+
errors,
|
|
182
|
+
warnings
|
|
183
|
+
};
|
|
184
|
+
}),
|
|
185
|
+
validateActionYml,
|
|
186
|
+
/* v8 ignore start - formatting function tested via integration */
|
|
187
|
+
formatResult: (result) => {
|
|
188
|
+
const lines = [];
|
|
189
|
+
if (result.errors.length > 0) {
|
|
190
|
+
lines.push("Errors:");
|
|
191
|
+
for (const error of result.errors) {
|
|
192
|
+
lines.push(` \u2717 ${error.message}`);
|
|
193
|
+
if (error.suggestion) lines.push(` \u2192 ${error.suggestion}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (result.warnings.length > 0) {
|
|
197
|
+
if (lines.length > 0) lines.push("");
|
|
198
|
+
lines.push("Warnings:");
|
|
199
|
+
for (const warning of result.warnings) {
|
|
200
|
+
lines.push(` \u26A0 ${warning.message}`);
|
|
201
|
+
if (warning.suggestion) lines.push(` \u2192 ${warning.suggestion}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (result.valid && result.errors.length === 0 && result.warnings.length === 0) lines.push("✓ All checks passed");
|
|
205
|
+
return lines.join("\n");
|
|
206
|
+
},
|
|
207
|
+
/* v8 ignore stop */
|
|
208
|
+
/* v8 ignore next 2 - environment detection */
|
|
209
|
+
isCI: () => Effect.succeed(isCI()),
|
|
210
|
+
/* v8 ignore next */
|
|
211
|
+
isStrict: (configStrict) => Effect.succeed(resolveStrict(configStrict))
|
|
212
|
+
};
|
|
213
|
+
}));
|
|
214
|
+
|
|
215
|
+
//#endregion
|
|
216
|
+
export { ValidationServiceLive };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { OptionalPathLikeSchema } from "../schemas/path.js";
|
|
2
|
+
import { Context, Schema } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/services/validation.ts
|
|
5
|
+
/**
|
|
6
|
+
* Options for validation.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
const ValidateOptionsSchema = Schema.Struct({
|
|
10
|
+
/** Working directory for file operations. Accepts string, Buffer, or URL. */
|
|
11
|
+
cwd: OptionalPathLikeSchema,
|
|
12
|
+
/** Force strict mode regardless of environment. Auto-detects from CI when undefined. */
|
|
13
|
+
strict: Schema.optional(Schema.Boolean)
|
|
14
|
+
});
|
|
15
|
+
/**
|
|
16
|
+
* A validation error item.
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
const ValidationErrorSchema = Schema.Struct({
|
|
20
|
+
/** Error code for categorization. */
|
|
21
|
+
code: Schema.String,
|
|
22
|
+
/** Human-readable error message. */
|
|
23
|
+
message: Schema.String,
|
|
24
|
+
/** File path where error occurred. */
|
|
25
|
+
file: Schema.optional(Schema.String),
|
|
26
|
+
/** Suggestion for fixing the error. */
|
|
27
|
+
suggestion: Schema.optional(Schema.String)
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* A validation warning.
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
const ValidationWarningSchema = Schema.Struct({
|
|
34
|
+
/** Warning code for categorization. */
|
|
35
|
+
code: Schema.String,
|
|
36
|
+
/** Human-readable warning message. */
|
|
37
|
+
message: Schema.String,
|
|
38
|
+
/** File path where warning occurred. */
|
|
39
|
+
file: Schema.optional(Schema.String),
|
|
40
|
+
/** Suggestion for addressing the warning. */
|
|
41
|
+
suggestion: Schema.optional(Schema.String)
|
|
42
|
+
});
|
|
43
|
+
/**
|
|
44
|
+
* Validation result with errors and warnings.
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
const ValidationResultSchema = Schema.Struct({
|
|
48
|
+
/** Whether validation passed (no errors, or only warnings in non-strict mode). */
|
|
49
|
+
valid: Schema.Boolean,
|
|
50
|
+
/** Validation errors. */
|
|
51
|
+
errors: Schema.Array(ValidationErrorSchema),
|
|
52
|
+
/** Validation warnings. */
|
|
53
|
+
warnings: Schema.Array(ValidationWarningSchema)
|
|
54
|
+
});
|
|
55
|
+
/**
|
|
56
|
+
* Result of action.yml validation.
|
|
57
|
+
* @internal
|
|
58
|
+
*/
|
|
59
|
+
const ActionYmlResultSchema = Schema.Struct({
|
|
60
|
+
/** Whether the action.yml is valid. */
|
|
61
|
+
valid: Schema.Boolean,
|
|
62
|
+
/** Parsed action.yml content if valid. */
|
|
63
|
+
content: Schema.optional(Schema.Any),
|
|
64
|
+
/** Validation errors. */
|
|
65
|
+
errors: Schema.Array(ValidationErrorSchema),
|
|
66
|
+
/** Validation warnings. */
|
|
67
|
+
warnings: Schema.Array(ValidationWarningSchema)
|
|
68
|
+
});
|
|
69
|
+
/**
|
|
70
|
+
* ValidationService tag for dependency injection.
|
|
71
|
+
*
|
|
72
|
+
* @public
|
|
73
|
+
*/
|
|
74
|
+
const ValidationService = Context.GenericTag("ValidationService");
|
|
75
|
+
|
|
76
|
+
//#endregion
|
|
77
|
+
export { ActionYmlResultSchema, ValidateOptionsSchema, ValidationErrorSchema, ValidationResultSchema, ValidationService, ValidationWarningSchema };
|
package/tsdoc-metadata.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
// This file is read by tools that parse documentation comments conforming to the TSDoc standard.
|
|
2
|
-
// It should be published with your NPM package. It should not be tracked by Git.
|
|
3
|
-
{
|
|
4
|
-
"tsdocVersion": "0.12",
|
|
5
|
-
"toolPackages": [
|
|
6
|
-
{
|
|
7
|
-
"packageName": "@microsoft/api-extractor",
|
|
8
|
-
"packageVersion": "7.58.7"
|
|
9
|
-
}
|
|
10
|
-
]
|
|
11
|
-
}
|
|
1
|
+
// This file is read by tools that parse documentation comments conforming to the TSDoc standard.
|
|
2
|
+
// It should be published with your NPM package. It should not be tracked by Git.
|
|
3
|
+
{
|
|
4
|
+
"tsdocVersion": "0.12",
|
|
5
|
+
"toolPackages": [
|
|
6
|
+
{
|
|
7
|
+
"packageName": "@microsoft/api-extractor",
|
|
8
|
+
"packageVersion": "7.58.7"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
}
|
package/231.js
DELETED