@savvy-web/github-action-builder 2.1.1 → 2.2.1
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.js +1 -1
- package/cli/commands/build.js +1 -1
- package/cli/commands/init.js +3 -2
- package/github-action.js +1 -1
- package/index.d.ts +38 -4
- package/index.js +1 -1
- package/layers/app.js +9 -9
- package/package.json +2 -2
- package/services/build.js +286 -2
- package/services/config.js +141 -3
- package/services/persist-local.js +214 -2
- package/services/validation.js +223 -2
- package/services/build-live.js +0 -282
- package/services/config-live.js +0 -138
- package/services/persist-local-live.js +0 -211
- package/services/validation-live.js +0 -217
|
@@ -1,7 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ActionYmlPathError, PersistLocalError } from "../errors.js";
|
|
2
|
+
import { Context, Effect, Layer, Schema } from "effect";
|
|
3
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { Yaml } from "@effected/yaml";
|
|
2
7
|
|
|
3
8
|
//#region src/services/persist-local.ts
|
|
4
9
|
/**
|
|
10
|
+
* PersistLocalService - Effect service for copying build output to a local action directory.
|
|
11
|
+
*
|
|
12
|
+
* @remarks
|
|
13
|
+
* Provides smart-sync of action.yml and dist/ to a local directory
|
|
14
|
+
* for testing with nektos/act.
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
5
19
|
* Options for the persist operation.
|
|
6
20
|
* @public
|
|
7
21
|
*/
|
|
@@ -27,11 +41,209 @@ const PersistLocalResultSchema = Schema.Struct({
|
|
|
27
41
|
error: Schema.optional(Schema.String)
|
|
28
42
|
});
|
|
29
43
|
/**
|
|
44
|
+
* Compute SHA-256 hash of a file's contents.
|
|
45
|
+
*/
|
|
46
|
+
function fileHash(filePath) {
|
|
47
|
+
const content = readFileSync(filePath);
|
|
48
|
+
return createHash("sha256").update(content).digest("hex");
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Sync a single file from src to dest using hash comparison.
|
|
52
|
+
* Returns true if the file was copied, false if skipped.
|
|
53
|
+
*/
|
|
54
|
+
function syncFile(src, dest) {
|
|
55
|
+
if (existsSync(dest)) {
|
|
56
|
+
if (fileHash(src) === fileHash(dest)) return false;
|
|
57
|
+
}
|
|
58
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
59
|
+
copyFileSync(src, dest);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Recursively collect all file paths relative to a base directory.
|
|
64
|
+
*/
|
|
65
|
+
function walkDirectory(dir, base = dir) {
|
|
66
|
+
const files = [];
|
|
67
|
+
if (!existsSync(dir)) return files;
|
|
68
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
69
|
+
const fullPath = join(dir, entry.name);
|
|
70
|
+
if (entry.isDirectory()) files.push(...walkDirectory(fullPath, base));
|
|
71
|
+
else files.push(relative(base, fullPath));
|
|
72
|
+
}
|
|
73
|
+
return files;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Smart-sync a directory: copy changed files, remove stale dest files.
|
|
77
|
+
*/
|
|
78
|
+
function syncDirectory(srcDir, destDir) {
|
|
79
|
+
const stats = {
|
|
80
|
+
copied: 0,
|
|
81
|
+
skipped: 0
|
|
82
|
+
};
|
|
83
|
+
const srcFiles = walkDirectory(srcDir);
|
|
84
|
+
for (const relPath of srcFiles) if (syncFile(join(srcDir, relPath), join(destDir, relPath))) stats.copied++;
|
|
85
|
+
else stats.skipped++;
|
|
86
|
+
const srcFileSet = new Set(srcFiles);
|
|
87
|
+
const destFiles = walkDirectory(destDir);
|
|
88
|
+
for (const relPath of destFiles) if (!srcFileSet.has(relPath)) {
|
|
89
|
+
rmSync(join(destDir, relPath), { force: true });
|
|
90
|
+
let parent = dirname(join(destDir, relPath));
|
|
91
|
+
while (parent !== destDir && existsSync(parent)) if (readdirSync(parent).length === 0) {
|
|
92
|
+
rmSync(parent, { recursive: true });
|
|
93
|
+
parent = dirname(parent);
|
|
94
|
+
} else break;
|
|
95
|
+
}
|
|
96
|
+
return stats;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Validate that action.yml runs paths resolve correctly relative to the destination.
|
|
100
|
+
*/
|
|
101
|
+
function validateActionYmlPaths(actionYmlPath, destDir) {
|
|
102
|
+
return Effect.gen(function* () {
|
|
103
|
+
if (!existsSync(actionYmlPath)) return;
|
|
104
|
+
const content = readFileSync(actionYmlPath, "utf8");
|
|
105
|
+
const parsed = yield* Yaml.parse(content).pipe(Effect.catch(() => Effect.succeed(null)));
|
|
106
|
+
if (!parsed?.runs) return;
|
|
107
|
+
for (const entryType of [
|
|
108
|
+
"main",
|
|
109
|
+
"pre",
|
|
110
|
+
"post"
|
|
111
|
+
]) {
|
|
112
|
+
const specifiedPath = parsed.runs[entryType];
|
|
113
|
+
if (!specifiedPath) continue;
|
|
114
|
+
const expectedPath = resolve(destDir, specifiedPath);
|
|
115
|
+
if (!existsSync(expectedPath)) return yield* Effect.fail(new ActionYmlPathError({
|
|
116
|
+
entryType,
|
|
117
|
+
specifiedPath,
|
|
118
|
+
expectedPath
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
const ACTRC_CONTENT = `--container-architecture linux/amd64
|
|
124
|
+
-W .github/workflows/act-test.yml
|
|
125
|
+
`;
|
|
126
|
+
const ACT_WORKFLOW_CONTENT = `name: Local Test
|
|
127
|
+
on:
|
|
128
|
+
workflow_dispatch:
|
|
129
|
+
|
|
130
|
+
jobs:
|
|
131
|
+
test:
|
|
132
|
+
runs-on: ubuntu-latest
|
|
133
|
+
steps:
|
|
134
|
+
- uses: actions/checkout@v7
|
|
135
|
+
- uses: ./.github/actions/local
|
|
136
|
+
`;
|
|
137
|
+
function formatPersistResult(result) {
|
|
138
|
+
const lines = [];
|
|
139
|
+
if (result.success) {
|
|
140
|
+
lines.push("Persist Local Summary:");
|
|
141
|
+
lines.push(` Output: ${result.outputPath}`);
|
|
142
|
+
lines.push(` Files copied: ${result.filesCopied}`);
|
|
143
|
+
lines.push(` Files skipped (unchanged): ${result.filesSkipped}`);
|
|
144
|
+
if (result.actTemplateGenerated) lines.push(" Act template files generated");
|
|
145
|
+
} else lines.push(`Persist Local Failed: ${result.error}`);
|
|
146
|
+
return lines.join("\n");
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
30
149
|
* PersistLocalService key for dependency injection.
|
|
31
150
|
*
|
|
32
151
|
* @public
|
|
33
152
|
*/
|
|
34
|
-
var PersistLocalService = class extends Context.Service()("PersistLocalService") {
|
|
153
|
+
var PersistLocalService = class extends Context.Service()("PersistLocalService") {
|
|
154
|
+
/**
|
|
155
|
+
* Production implementation of {@link PersistLocalService}.
|
|
156
|
+
*
|
|
157
|
+
* @public
|
|
158
|
+
*/
|
|
159
|
+
static layer = Layer.succeed(this, {
|
|
160
|
+
persist: (config, options = {}) => Effect.gen(function* () {
|
|
161
|
+
const cwd = options.cwd ?? process.cwd();
|
|
162
|
+
const outputPath = resolve(cwd, config.persistLocal.path);
|
|
163
|
+
if (!config.persistLocal.enabled) return {
|
|
164
|
+
success: true,
|
|
165
|
+
filesCopied: 0,
|
|
166
|
+
filesSkipped: 0,
|
|
167
|
+
actTemplateGenerated: false,
|
|
168
|
+
outputPath
|
|
169
|
+
};
|
|
170
|
+
yield* Effect.try({
|
|
171
|
+
try: () => mkdirSync(outputPath, { recursive: true }),
|
|
172
|
+
/* v8 ignore next 5 - error branch requires fs permission failures */
|
|
173
|
+
catch: (error) => new PersistLocalError({
|
|
174
|
+
path: outputPath,
|
|
175
|
+
cause: error
|
|
176
|
+
})
|
|
177
|
+
});
|
|
178
|
+
let totalCopied = 0;
|
|
179
|
+
let totalSkipped = 0;
|
|
180
|
+
const actionYmlSrc = resolve(cwd, "action.yml");
|
|
181
|
+
const actionYmlDest = resolve(outputPath, "action.yml");
|
|
182
|
+
if (existsSync(actionYmlSrc)) if (yield* Effect.try({
|
|
183
|
+
try: () => syncFile(actionYmlSrc, actionYmlDest),
|
|
184
|
+
/* v8 ignore next 5 - error branch requires fs permission failures */
|
|
185
|
+
catch: (error) => new PersistLocalError({
|
|
186
|
+
path: actionYmlSrc,
|
|
187
|
+
cause: error
|
|
188
|
+
})
|
|
189
|
+
})) totalCopied++;
|
|
190
|
+
else totalSkipped++;
|
|
191
|
+
else if (existsSync(actionYmlDest)) rmSync(actionYmlDest, { force: true });
|
|
192
|
+
const distSrc = resolve(cwd, "dist");
|
|
193
|
+
if (existsSync(distSrc) && statSync(distSrc).isDirectory()) {
|
|
194
|
+
const distStats = yield* Effect.try({
|
|
195
|
+
try: () => syncDirectory(distSrc, resolve(outputPath, "dist")),
|
|
196
|
+
/* v8 ignore next 5 - error branch requires fs permission failures */
|
|
197
|
+
catch: (error) => new PersistLocalError({
|
|
198
|
+
path: distSrc,
|
|
199
|
+
cause: error
|
|
200
|
+
})
|
|
201
|
+
});
|
|
202
|
+
totalCopied += distStats.copied;
|
|
203
|
+
totalSkipped += distStats.skipped;
|
|
204
|
+
}
|
|
205
|
+
yield* validateActionYmlPaths(resolve(outputPath, "action.yml"), outputPath);
|
|
206
|
+
let actTemplateGenerated = false;
|
|
207
|
+
if (config.persistLocal.actTemplate) {
|
|
208
|
+
const actrcPath = resolve(cwd, ".actrc");
|
|
209
|
+
const actWorkflowPath = resolve(cwd, ".github/workflows/act-test.yml");
|
|
210
|
+
if (!existsSync(actrcPath)) {
|
|
211
|
+
yield* Effect.try({
|
|
212
|
+
try: () => writeFileSync(actrcPath, ACTRC_CONTENT, "utf8"),
|
|
213
|
+
/* v8 ignore next 5 - error branch requires fs permission failures */
|
|
214
|
+
catch: (error) => new PersistLocalError({
|
|
215
|
+
path: actrcPath,
|
|
216
|
+
cause: error
|
|
217
|
+
})
|
|
218
|
+
});
|
|
219
|
+
actTemplateGenerated = true;
|
|
220
|
+
}
|
|
221
|
+
if (!existsSync(actWorkflowPath)) {
|
|
222
|
+
yield* Effect.try({
|
|
223
|
+
try: () => {
|
|
224
|
+
mkdirSync(dirname(actWorkflowPath), { recursive: true });
|
|
225
|
+
writeFileSync(actWorkflowPath, ACT_WORKFLOW_CONTENT, "utf8");
|
|
226
|
+
},
|
|
227
|
+
/* v8 ignore next 5 - error branch requires fs permission failures */
|
|
228
|
+
catch: (error) => new PersistLocalError({
|
|
229
|
+
path: actWorkflowPath,
|
|
230
|
+
cause: error
|
|
231
|
+
})
|
|
232
|
+
});
|
|
233
|
+
actTemplateGenerated = true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
success: true,
|
|
238
|
+
filesCopied: totalCopied,
|
|
239
|
+
filesSkipped: totalSkipped,
|
|
240
|
+
actTemplateGenerated,
|
|
241
|
+
outputPath
|
|
242
|
+
};
|
|
243
|
+
}),
|
|
244
|
+
formatResult: formatPersistResult
|
|
245
|
+
});
|
|
246
|
+
};
|
|
35
247
|
|
|
36
248
|
//#endregion
|
|
37
249
|
export { PersistLocalResultSchema, PersistLocalRunnerOptionsSchema, PersistLocalService };
|
package/services/validation.js
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
|
+
import { ActionYmlMissing, ActionYmlSchemaError, ActionYmlSyntaxError, MainEntryMissing, ValidationFailed } from "../errors.js";
|
|
1
2
|
import { OptionalPathLikeSchema } from "../schemas/path.js";
|
|
2
|
-
import {
|
|
3
|
+
import { ConfigService } from "./config.js";
|
|
4
|
+
import { ActionYml } from "../schemas/action-yml.js";
|
|
5
|
+
import { Context, Effect, Layer, Result, Schema } from "effect";
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
import { Yaml } from "@effected/yaml";
|
|
3
9
|
|
|
4
10
|
//#region src/services/validation.ts
|
|
5
11
|
/**
|
|
12
|
+
* ValidationService - Effect service for validation.
|
|
13
|
+
*
|
|
14
|
+
* @remarks
|
|
15
|
+
* Provides validation of configuration, entry points, and action.yml
|
|
16
|
+
* using Effect's service pattern with Context.Service.
|
|
17
|
+
*
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
6
21
|
* Options for validation.
|
|
7
22
|
* @public
|
|
8
23
|
*/
|
|
@@ -66,6 +81,29 @@ const ActionYmlResultSchema = Schema.Struct({
|
|
|
66
81
|
/** Validation warnings. */
|
|
67
82
|
warnings: Schema.Array(ValidationWarningSchema)
|
|
68
83
|
});
|
|
84
|
+
/* v8 ignore start - CI environment detection has multiple env var formats */
|
|
85
|
+
/** Check if running in CI environment. */
|
|
86
|
+
const isCI = () => process.env.CI === "true" || process.env.CI === "1" || process.env.GITHUB_ACTIONS === "true";
|
|
87
|
+
/** Resolve strict mode from config or environment. */
|
|
88
|
+
const resolveStrict = (configStrict) => configStrict ?? isCI();
|
|
89
|
+
/* v8 ignore stop */
|
|
90
|
+
/** Create a validation warning, omitting undefined file. */
|
|
91
|
+
const makeWarning = (code, message, suggestion, file) => file !== void 0 ? {
|
|
92
|
+
code,
|
|
93
|
+
message,
|
|
94
|
+
suggestion,
|
|
95
|
+
file
|
|
96
|
+
} : {
|
|
97
|
+
code,
|
|
98
|
+
message,
|
|
99
|
+
suggestion
|
|
100
|
+
};
|
|
101
|
+
/** Format schema parse errors. */
|
|
102
|
+
/* v8 ignore start - only called for schema validation errors */
|
|
103
|
+
const formatSchemaErrors = (error, filePath) => [{
|
|
104
|
+
path: filePath,
|
|
105
|
+
message: error.message
|
|
106
|
+
}];
|
|
69
107
|
/**
|
|
70
108
|
* ValidationService key for dependency injection.
|
|
71
109
|
*
|
|
@@ -91,7 +129,190 @@ const ActionYmlResultSchema = Schema.Struct({
|
|
|
91
129
|
*
|
|
92
130
|
* @public
|
|
93
131
|
*/
|
|
94
|
-
var ValidationService = class extends Context.Service()("ValidationService") {
|
|
132
|
+
var ValidationService = class extends Context.Service()("ValidationService") {
|
|
133
|
+
/**
|
|
134
|
+
* Production implementation of {@link ValidationService}.
|
|
135
|
+
*
|
|
136
|
+
* @remarks
|
|
137
|
+
* Depends on {@link ConfigService}.
|
|
138
|
+
*
|
|
139
|
+
* @public
|
|
140
|
+
*/
|
|
141
|
+
static layer = Layer.effect(this, Effect.gen(function* () {
|
|
142
|
+
const configService = yield* ConfigService;
|
|
143
|
+
/** Read and parse action.yml file. */
|
|
144
|
+
const readActionYml = (path) => Effect.gen(function* () {
|
|
145
|
+
if (!existsSync(path)) return yield* new ActionYmlMissing({ cwd: path });
|
|
146
|
+
const content = yield* Effect.try({
|
|
147
|
+
try: () => readFileSync(path, "utf8"),
|
|
148
|
+
/* v8 ignore next */
|
|
149
|
+
catch: () => new ActionYmlSyntaxError({
|
|
150
|
+
path,
|
|
151
|
+
message: "Failed to read file"
|
|
152
|
+
})
|
|
153
|
+
});
|
|
154
|
+
const parsed = yield* Yaml.parse(content).pipe(
|
|
155
|
+
/* v8 ignore next 5 - requires malformed YAML */
|
|
156
|
+
Effect.mapError((error) => new ActionYmlSyntaxError({
|
|
157
|
+
path,
|
|
158
|
+
message: error.message
|
|
159
|
+
}))
|
|
160
|
+
);
|
|
161
|
+
/* v8 ignore start - requires non-object YAML (e.g., scalar or array) */
|
|
162
|
+
if (!parsed || typeof parsed !== "object") return yield* new ActionYmlSyntaxError({
|
|
163
|
+
path,
|
|
164
|
+
message: "action.yml must be an object"
|
|
165
|
+
});
|
|
166
|
+
/* v8 ignore stop */
|
|
167
|
+
return parsed;
|
|
168
|
+
});
|
|
169
|
+
/** Validate parsed content against ActionYml schema. */
|
|
170
|
+
/* v8 ignore start - schema validation error branch */
|
|
171
|
+
const validateSchema = (parsed, path) => Effect.gen(function* () {
|
|
172
|
+
const result = yield* Effect.result(Schema.decodeUnknownEffect(ActionYml)(parsed));
|
|
173
|
+
if (Result.isFailure(result)) return yield* new ActionYmlSchemaError({
|
|
174
|
+
path,
|
|
175
|
+
errors: formatSchemaErrors(result.failure, path)
|
|
176
|
+
});
|
|
177
|
+
return result.success;
|
|
178
|
+
});
|
|
179
|
+
/* v8 ignore stop */
|
|
180
|
+
/** Check for recommended fields and generate warnings. */
|
|
181
|
+
/* v8 ignore start - recommendation checks have many branches */
|
|
182
|
+
const checkRecommendations = (content, filePath) => {
|
|
183
|
+
const warnings = [];
|
|
184
|
+
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));
|
|
185
|
+
else {
|
|
186
|
+
const branding = content.branding;
|
|
187
|
+
if (!branding.icon) warnings.push(makeWarning("ACTION_YML_NO_BRANDING_ICON", "Branding icon not specified", "Add branding.icon for better marketplace visibility", filePath));
|
|
188
|
+
if (!branding.color) warnings.push(makeWarning("ACTION_YML_NO_BRANDING_COLOR", "Branding color not specified", "Add branding.color for better marketplace visibility", filePath));
|
|
189
|
+
}
|
|
190
|
+
if (content.inputs) {
|
|
191
|
+
const inputs = content.inputs;
|
|
192
|
+
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));
|
|
193
|
+
}
|
|
194
|
+
if (content.outputs) {
|
|
195
|
+
const outputs = content.outputs;
|
|
196
|
+
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));
|
|
197
|
+
}
|
|
198
|
+
return warnings;
|
|
199
|
+
};
|
|
200
|
+
/* v8 ignore stop */
|
|
201
|
+
/** Validate action.yml file completely. */
|
|
202
|
+
const validateActionYml = (path) => Effect.gen(function* () {
|
|
203
|
+
const parsed = yield* readActionYml(path);
|
|
204
|
+
return {
|
|
205
|
+
valid: true,
|
|
206
|
+
content: yield* validateSchema(parsed, path),
|
|
207
|
+
errors: [],
|
|
208
|
+
warnings: checkRecommendations(parsed, path)
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
/** Check entry points exist. */
|
|
212
|
+
const checkEntries = (config, cwd) => Effect.gen(function* () {
|
|
213
|
+
const errors = [];
|
|
214
|
+
const entriesConfig = { main: config.entries.main };
|
|
215
|
+
if (config.entries.pre) entriesConfig.pre = config.entries.pre;
|
|
216
|
+
if (config.entries.post) entriesConfig.post = config.entries.post;
|
|
217
|
+
const result = yield* Effect.result(configService.detectEntries(cwd, entriesConfig));
|
|
218
|
+
/* v8 ignore start - error branch requires missing main entry */
|
|
219
|
+
if (Result.isFailure(result) && result.failure instanceof MainEntryMissing) errors.push({
|
|
220
|
+
code: "MAIN_ENTRY_MISSING",
|
|
221
|
+
message: `Main entry point not found: ${result.failure.expectedPath}`,
|
|
222
|
+
file: result.failure.expectedPath,
|
|
223
|
+
suggestion: "Create src/main.ts or specify a different path in config"
|
|
224
|
+
});
|
|
225
|
+
/* v8 ignore stop */
|
|
226
|
+
return errors;
|
|
227
|
+
});
|
|
228
|
+
/** Check action.yml and collect errors/warnings. */
|
|
229
|
+
/* v8 ignore start - action.yml validation has many error branches */
|
|
230
|
+
const checkActionYml = (config, cwd) => Effect.gen(function* () {
|
|
231
|
+
const errors = [];
|
|
232
|
+
const warnings = [];
|
|
233
|
+
if (!config.validation.requireActionYml) return {
|
|
234
|
+
errors,
|
|
235
|
+
warnings
|
|
236
|
+
};
|
|
237
|
+
const actionYmlPath = resolve(cwd, "action.yml");
|
|
238
|
+
const result = yield* Effect.result(validateActionYml(actionYmlPath));
|
|
239
|
+
if (Result.isFailure(result)) {
|
|
240
|
+
const error = result.failure;
|
|
241
|
+
if (error instanceof ActionYmlMissing) warnings.push({
|
|
242
|
+
code: "ACTION_YML_MISSING",
|
|
243
|
+
message: "action.yml not found",
|
|
244
|
+
file: actionYmlPath,
|
|
245
|
+
suggestion: "Create action.yml to define your action metadata"
|
|
246
|
+
});
|
|
247
|
+
else if (error instanceof ActionYmlSyntaxError) errors.push({
|
|
248
|
+
code: "ACTION_YML_SYNTAX_ERROR",
|
|
249
|
+
message: error.message,
|
|
250
|
+
file: error.path
|
|
251
|
+
});
|
|
252
|
+
else if (error instanceof ActionYmlSchemaError) for (const schemaError of error.errors) errors.push({
|
|
253
|
+
code: "ACTION_YML_SCHEMA_ERROR",
|
|
254
|
+
message: schemaError.message,
|
|
255
|
+
file: error.path
|
|
256
|
+
});
|
|
257
|
+
} else warnings.push(...result.success.warnings);
|
|
258
|
+
return {
|
|
259
|
+
errors,
|
|
260
|
+
warnings
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
/* v8 ignore stop */
|
|
264
|
+
return {
|
|
265
|
+
validate: (config, options = {}) => Effect.gen(function* () {
|
|
266
|
+
const cwd = options.cwd ?? process.cwd();
|
|
267
|
+
const strict = resolveStrict(options.strict ?? config.validation.strict);
|
|
268
|
+
const entryErrors = yield* checkEntries(config, cwd);
|
|
269
|
+
const actionYmlResult = yield* checkActionYml(config, cwd);
|
|
270
|
+
const errors = [...entryErrors, ...actionYmlResult.errors];
|
|
271
|
+
const warnings = [...actionYmlResult.warnings];
|
|
272
|
+
const valid = errors.length === 0 && (!strict || warnings.length === 0);
|
|
273
|
+
/* v8 ignore start - strict mode branch requires CI environment */
|
|
274
|
+
if (strict && warnings.length > 0 && errors.length === 0) return yield* new ValidationFailed({
|
|
275
|
+
errorCount: 0,
|
|
276
|
+
warningCount: warnings.length,
|
|
277
|
+
message: "Warnings treated as errors in strict mode"
|
|
278
|
+
});
|
|
279
|
+
/* v8 ignore stop */
|
|
280
|
+
return {
|
|
281
|
+
valid,
|
|
282
|
+
errors,
|
|
283
|
+
warnings
|
|
284
|
+
};
|
|
285
|
+
}),
|
|
286
|
+
validateActionYml,
|
|
287
|
+
/* v8 ignore start - formatting function tested via integration */
|
|
288
|
+
formatResult: (result) => {
|
|
289
|
+
const lines = [];
|
|
290
|
+
if (result.errors.length > 0) {
|
|
291
|
+
lines.push("Errors:");
|
|
292
|
+
for (const error of result.errors) {
|
|
293
|
+
lines.push(` ✗ ${error.message}`);
|
|
294
|
+
if (error.suggestion) lines.push(` → ${error.suggestion}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (result.warnings.length > 0) {
|
|
298
|
+
if (lines.length > 0) lines.push("");
|
|
299
|
+
lines.push("Warnings:");
|
|
300
|
+
for (const warning of result.warnings) {
|
|
301
|
+
lines.push(` ⚠ ${warning.message}`);
|
|
302
|
+
if (warning.suggestion) lines.push(` → ${warning.suggestion}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (result.valid && result.errors.length === 0 && result.warnings.length === 0) lines.push("✓ All checks passed");
|
|
306
|
+
return lines.join("\n");
|
|
307
|
+
},
|
|
308
|
+
/* v8 ignore stop */
|
|
309
|
+
/* v8 ignore next 2 - environment detection */
|
|
310
|
+
isCI: () => Effect.succeed(isCI()),
|
|
311
|
+
/* v8 ignore next */
|
|
312
|
+
isStrict: (configStrict) => Effect.succeed(resolveStrict(configStrict))
|
|
313
|
+
};
|
|
314
|
+
}));
|
|
315
|
+
};
|
|
95
316
|
|
|
96
317
|
//#endregion
|
|
97
318
|
export { ActionYmlResultSchema, ValidateOptionsSchema, ValidationErrorSchema, ValidationResultSchema, ValidationService, ValidationWarningSchema };
|