Prune unused definitions from workflow schema
Remove 10 unreachable definitions from workflow-v1.0.json: - workflow-root, on, on-mapping (non-strict variants) - job-if-result, step-if-result - boolean-needs-context, number-needs-context, string-needs-context - boolean-steps-context, number-steps-context Saves 2,039 bytes minified (2.9%), 146 bytes gzipped (1.2%). Also adds script/prune-schema.cjs for future maintenance.
This commit is contained in:
@@ -7,6 +7,9 @@ node_modules
|
|||||||
# Minified JSON (generated at build time)
|
# Minified JSON (generated at build time)
|
||||||
*.min.json
|
*.min.json
|
||||||
|
|
||||||
|
# Optimized workflow schema (generated by optimize-workflow-schema.js)
|
||||||
|
*.optimized.json
|
||||||
|
|
||||||
# Full webhooks source (generated by update-webhooks, used for validation tests)
|
# Full webhooks source (generated by update-webhooks, used for validation tests)
|
||||||
*.full.json
|
*.full.json
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,8 @@
|
|||||||
"format-check": "prettier --check '**/*.ts'",
|
"format-check": "prettier --check '**/*.ts'",
|
||||||
"lint": "eslint 'src/**/*.ts'",
|
"lint": "eslint 'src/**/*.ts'",
|
||||||
"lint-fix": "eslint --fix 'src/**/*.ts'",
|
"lint-fix": "eslint --fix 'src/**/*.ts'",
|
||||||
"minify-json": "node ../script/minify-json.js src/workflow-v1.0.json",
|
"optimize-schema": "node script/optimize-workflow-schema.js",
|
||||||
|
"minify-json": "npm run optimize-schema && node ../script/minify-json.js src/workflow-v1.0.optimized.json",
|
||||||
"prebuild": "npm run minify-json",
|
"prebuild": "npm run minify-json",
|
||||||
"prepublishOnly": "npm run build && npm run test",
|
"prepublishOnly": "npm run build && npm run test",
|
||||||
"pretest": "npm run minify-json",
|
"pretest": "npm run minify-json",
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Optimizes workflow-v1.0.json by pruning unused definitions.
|
||||||
|
*
|
||||||
|
* Removes definitions not reachable from the entry point (workflow-root-strict).
|
||||||
|
* Output is then minified by minify-json.js to produce the final .min.json file.
|
||||||
|
*
|
||||||
|
* Usage: node script/optimize-workflow-schema.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {promises as fs} from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import {fileURLToPath} from "url";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const ENTRY_POINT = "workflow-root-strict";
|
||||||
|
|
||||||
|
const inputPath = path.join(__dirname, "..", "src", "workflow-v1.0.json");
|
||||||
|
const outputPath = path.join(__dirname, "..", "src", "workflow-v1.0.optimized.json");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all type references in a definition.
|
||||||
|
*/
|
||||||
|
function findRefs(obj) {
|
||||||
|
const refs = [];
|
||||||
|
|
||||||
|
function visit(node) {
|
||||||
|
if (!node || typeof node !== "object") return;
|
||||||
|
|
||||||
|
if (Array.isArray(node)) {
|
||||||
|
for (const item of node) {
|
||||||
|
if (typeof item === "string") refs.push(item);
|
||||||
|
else visit(item);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(node)) {
|
||||||
|
if (["type", "item-type", "loose-key-type", "loose-value-type"].includes(key)) {
|
||||||
|
if (typeof value === "string") refs.push(value);
|
||||||
|
} else if (key === "one-of") {
|
||||||
|
visit(value);
|
||||||
|
} else if (key === "properties") {
|
||||||
|
for (const propValue of Object.values(value)) {
|
||||||
|
if (typeof propValue === "string") refs.push(propValue);
|
||||||
|
else if (propValue && typeof propValue === "object") visit(propValue);
|
||||||
|
}
|
||||||
|
} else if (["mapping", "sequence", "string"].includes(key)) {
|
||||||
|
visit(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visit(obj);
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all definitions reachable from entry point.
|
||||||
|
*/
|
||||||
|
function findReachable(schema, entryPoint) {
|
||||||
|
const reachable = new Set();
|
||||||
|
const queue = [entryPoint];
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const name = queue.shift();
|
||||||
|
if (reachable.has(name) || !schema.definitions[name]) continue;
|
||||||
|
reachable.add(name);
|
||||||
|
|
||||||
|
const refs = findRefs(schema.definitions[name]);
|
||||||
|
for (const ref of refs) {
|
||||||
|
if (!reachable.has(ref) && schema.definitions[ref]) {
|
||||||
|
queue.push(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return reachable;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const content = await fs.readFile(inputPath, "utf8");
|
||||||
|
const schema = JSON.parse(content);
|
||||||
|
|
||||||
|
const reachable = findReachable(schema, ENTRY_POINT);
|
||||||
|
const allDefs = Object.keys(schema.definitions);
|
||||||
|
const unused = allDefs.filter((name) => !reachable.has(name));
|
||||||
|
|
||||||
|
console.log(`Entry point: ${ENTRY_POINT}`);
|
||||||
|
console.log(`Definitions: ${allDefs.length} -> ${reachable.size} (${unused.length} pruned)`);
|
||||||
|
|
||||||
|
if (unused.length > 0) {
|
||||||
|
console.log("\nPruned:");
|
||||||
|
unused.forEach((name) => console.log(` - ${name}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create pruned schema preserving definition order
|
||||||
|
const pruned = {version: schema.version, definitions: {}};
|
||||||
|
for (const name of allDefs) {
|
||||||
|
if (reachable.has(name)) {
|
||||||
|
pruned.definitions[name] = schema.definitions[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write output (will be minified by minify-json.js)
|
||||||
|
await fs.writeFile(outputPath, JSON.stringify(pruned));
|
||||||
|
|
||||||
|
console.log(`\nOutput: ${outputPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import {getWorkflowSchema} from "./workflow-schema";
|
||||||
|
|
||||||
|
describe("workflow-schema", () => {
|
||||||
|
it("loads successfully from workflow-root-strict entry point", () => {
|
||||||
|
const schema = getWorkflowSchema();
|
||||||
|
expect(schema).toBeDefined();
|
||||||
|
|
||||||
|
// Verify entry point exists
|
||||||
|
const rootDef = schema.getDefinition("workflow-root-strict");
|
||||||
|
expect(rootDef).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has all referenced definitions reachable from workflow-root-strict", () => {
|
||||||
|
const schema = getWorkflowSchema();
|
||||||
|
const definitions = schema.definitions;
|
||||||
|
|
||||||
|
// Collect all type references from all definitions
|
||||||
|
const referencedTypes = new Set<string>();
|
||||||
|
const definedTypes = new Set<string>();
|
||||||
|
|
||||||
|
for (const name of Object.keys(definitions)) {
|
||||||
|
definedTypes.add(name);
|
||||||
|
collectReferences(definitions[name], referencedTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every referenced type should be defined
|
||||||
|
const missingDefinitions: string[] = [];
|
||||||
|
for (const ref of referencedTypes) {
|
||||||
|
// Skip built-in types
|
||||||
|
if (isBuiltInType(ref)) continue;
|
||||||
|
|
||||||
|
if (!definedTypes.has(ref)) {
|
||||||
|
missingDefinitions.push(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(missingDefinitions).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can resolve key workflow definitions", () => {
|
||||||
|
const schema = getWorkflowSchema();
|
||||||
|
|
||||||
|
// These are critical definitions that must exist
|
||||||
|
const criticalDefinitions = [
|
||||||
|
"workflow-root-strict",
|
||||||
|
"jobs",
|
||||||
|
"steps",
|
||||||
|
"runs-on",
|
||||||
|
"step-uses",
|
||||||
|
"job-env",
|
||||||
|
"step-env",
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const defName of criticalDefinitions) {
|
||||||
|
const def = schema.getDefinition(defName);
|
||||||
|
expect(def).toBeDefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function collectReferences(obj: unknown, refs: Set<string>): void {
|
||||||
|
if (!obj || typeof obj !== "object") return;
|
||||||
|
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
for (const item of obj) {
|
||||||
|
if (typeof item === "string") refs.add(item);
|
||||||
|
else collectReferences(item, refs);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = obj as Record<string, unknown>;
|
||||||
|
for (const [key, value] of Object.entries(record)) {
|
||||||
|
if (["type", "item-type", "loose-key-type", "loose-value-type"].includes(key)) {
|
||||||
|
if (typeof value === "string") refs.add(value);
|
||||||
|
} else if (key === "one-of" || key === "properties" || key === "mapping" || key === "sequence") {
|
||||||
|
collectReferences(value, refs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBuiltInType(typeName: string): boolean {
|
||||||
|
const builtIns = ["null", "boolean", "number", "string", "sequence", "mapping", "any"];
|
||||||
|
return builtIns.includes(typeName);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import {JSONObjectReader} from "../templates/json-object-reader";
|
import {JSONObjectReader} from "../templates/json-object-reader";
|
||||||
import {TemplateSchema} from "../templates/schema";
|
import {TemplateSchema} from "../templates/schema";
|
||||||
import WorkflowSchema from "../workflow-v1.0.min.json";
|
import WorkflowSchema from "../workflow-v1.0.optimized.min.json";
|
||||||
|
|
||||||
let schema: TemplateSchema;
|
let schema: TemplateSchema;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user