fix: streamline pr and release workflow (#289)
Co-authored-by: sck_0 <samujackson1337@gmail.com>
This commit is contained in:
35
tools/scripts/generated_files.js
Normal file
35
tools/scripts/generated_files.js
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { getManagedFiles, loadWorkflowContract } = require("../lib/workflow-contract");
|
||||
|
||||
function parseArgs(argv) {
|
||||
return {
|
||||
includeMixed: argv.includes("--include-mixed"),
|
||||
includeReleaseManaged: argv.includes("--include-release-managed"),
|
||||
json: argv.includes("--json"),
|
||||
shell: argv.includes("--shell"),
|
||||
};
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const contract = loadWorkflowContract(__dirname);
|
||||
const files = getManagedFiles(contract, {
|
||||
includeMixed: args.includeMixed,
|
||||
includeReleaseManaged: args.includeReleaseManaged,
|
||||
});
|
||||
|
||||
if (args.json) {
|
||||
process.stdout.write(`${JSON.stringify(files, null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.shell) {
|
||||
process.stdout.write(`${files.join(" ")}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
process.stdout.write(`${files.join("\n")}\n`);
|
||||
}
|
||||
|
||||
main();
|
||||
250
tools/scripts/pr_preflight.js
Normal file
250
tools/scripts/pr_preflight.js
Normal file
@@ -0,0 +1,250 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { spawnSync } = require("child_process");
|
||||
|
||||
const { findProjectRoot } = require("../lib/project-root");
|
||||
const {
|
||||
classifyChangedFiles,
|
||||
getDirectDerivedChanges,
|
||||
hasIssueLink,
|
||||
hasQualityChecklist,
|
||||
loadWorkflowContract,
|
||||
normalizeRepoPath,
|
||||
requiresReferencesValidation,
|
||||
} = require("../lib/workflow-contract");
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {
|
||||
base: null,
|
||||
head: "HEAD",
|
||||
eventPath: null,
|
||||
checkPolicy: false,
|
||||
noRun: false,
|
||||
writeGithubOutput: false,
|
||||
writeStepSummary: false,
|
||||
json: false,
|
||||
};
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (arg === "--base") {
|
||||
args.base = argv[index + 1];
|
||||
index += 1;
|
||||
} else if (arg === "--head") {
|
||||
args.head = argv[index + 1];
|
||||
index += 1;
|
||||
} else if (arg === "--event-path") {
|
||||
args.eventPath = argv[index + 1];
|
||||
index += 1;
|
||||
} else if (arg === "--check-policy") {
|
||||
args.checkPolicy = true;
|
||||
} else if (arg === "--no-run") {
|
||||
args.noRun = true;
|
||||
} else if (arg === "--write-github-output") {
|
||||
args.writeGithubOutput = true;
|
||||
} else if (arg === "--write-step-summary") {
|
||||
args.writeStepSummary = true;
|
||||
} else if (arg === "--json") {
|
||||
args.json = true;
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
function runGit(args, options = {}) {
|
||||
const result = spawnSync("git", args, {
|
||||
cwd: options.cwd,
|
||||
encoding: "utf8",
|
||||
stdio: options.capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
if (typeof result.status !== "number" || result.status !== 0) {
|
||||
const stderr = options.capture ? result.stderr.trim() : "";
|
||||
throw new Error(stderr || `git ${args.join(" ")} failed with status ${result.status}`);
|
||||
}
|
||||
|
||||
return options.capture ? result.stdout.trim() : "";
|
||||
}
|
||||
|
||||
function runCommand(command, args, cwd) {
|
||||
console.log(`[pr:preflight] ${command} ${args.join(" ")}`);
|
||||
const result = spawnSync(command, args, {
|
||||
cwd,
|
||||
stdio: "inherit",
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
if (typeof result.status !== "number" || result.status !== 0) {
|
||||
process.exit(result.status || 1);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveBaseRef(projectRoot) {
|
||||
for (const candidate of ["origin/main", "main"]) {
|
||||
const result = spawnSync("git", ["rev-parse", "--verify", candidate], {
|
||||
cwd: projectRoot,
|
||||
stdio: "ignore",
|
||||
});
|
||||
if (result.status === 0) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return "HEAD";
|
||||
}
|
||||
|
||||
function getChangedFiles(projectRoot, baseRef, headRef) {
|
||||
if (baseRef === headRef) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const diffOutput = runGit(["diff", "--name-only", `${baseRef}...${headRef}`], {
|
||||
cwd: projectRoot,
|
||||
capture: true,
|
||||
});
|
||||
|
||||
return [...new Set(diffOutput.split(/\r?\n/).map(normalizeRepoPath).filter(Boolean))];
|
||||
}
|
||||
|
||||
function loadPullRequestBody(eventPath) {
|
||||
if (!eventPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawEvent = fs.readFileSync(path.resolve(eventPath), "utf8");
|
||||
const event = JSON.parse(rawEvent);
|
||||
return event.pull_request?.body || "";
|
||||
}
|
||||
|
||||
function appendGithubOutput(result) {
|
||||
const outputPath = process.env.GITHUB_OUTPUT;
|
||||
if (!outputPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = [
|
||||
`primary_category=${result.primaryCategory}`,
|
||||
`categories=${result.categories.join(",")}`,
|
||||
`requires_references=${String(result.requiresReferencesValidation)}`,
|
||||
`direct_derived_changes_count=${String(result.directDerivedChanges.length)}`,
|
||||
`direct_derived_changes=${JSON.stringify(result.directDerivedChanges)}`,
|
||||
`changed_files_count=${String(result.changedFiles.length)}`,
|
||||
`has_quality_checklist=${String(result.prBody.hasQualityChecklist)}`,
|
||||
`has_issue_link=${String(result.prBody.hasIssueLink)}`,
|
||||
];
|
||||
|
||||
fs.appendFileSync(outputPath, `${lines.join("\n")}\n`, "utf8");
|
||||
}
|
||||
|
||||
function appendStepSummary(result) {
|
||||
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
||||
if (!summaryPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const derivedSummary =
|
||||
result.directDerivedChanges.length === 0
|
||||
? "none"
|
||||
: result.directDerivedChanges.map((filePath) => `\`${filePath}\``).join(", ");
|
||||
|
||||
const lines = [
|
||||
"## PR Workflow Intake",
|
||||
"",
|
||||
`- Primary change: \`${result.primaryCategory}\``,
|
||||
`- Categories: ${result.categories.length > 0 ? result.categories.map((category) => `\`${category}\``).join(", ") : "\`none\`"}`,
|
||||
`- Changed files: ${result.changedFiles.length}`,
|
||||
`- Direct derived-file edits: ${derivedSummary}`,
|
||||
`- \`validate:references\` required: ${result.requiresReferencesValidation ? "yes" : "no"}`,
|
||||
`- PR template checklist: ${result.prBody.hasQualityChecklist ? "present" : "missing"}`,
|
||||
`- Issue auto-close link: ${result.prBody.hasIssueLink ? "detected" : "not detected"}`,
|
||||
"",
|
||||
"> Generated drift is reported separately in the artifact preview job and remains informational on pull requests.",
|
||||
];
|
||||
|
||||
fs.appendFileSync(summaryPath, `${lines.join("\n")}\n`, "utf8");
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const projectRoot = findProjectRoot(__dirname);
|
||||
const contract = loadWorkflowContract(__dirname);
|
||||
const baseRef = args.base || resolveBaseRef(projectRoot);
|
||||
const changedFiles = getChangedFiles(projectRoot, baseRef, args.head);
|
||||
const classification = classifyChangedFiles(changedFiles, contract);
|
||||
const directDerivedChanges = getDirectDerivedChanges(changedFiles, contract);
|
||||
const pullRequestBody = loadPullRequestBody(args.eventPath);
|
||||
|
||||
const result = {
|
||||
baseRef,
|
||||
headRef: args.head,
|
||||
changedFiles,
|
||||
categories: classification.categories,
|
||||
primaryCategory: classification.primaryCategory,
|
||||
directDerivedChanges,
|
||||
requiresReferencesValidation: requiresReferencesValidation(changedFiles, contract),
|
||||
prBody: {
|
||||
available: pullRequestBody !== null,
|
||||
hasQualityChecklist: hasQualityChecklist(pullRequestBody),
|
||||
hasIssueLink: hasIssueLink(pullRequestBody),
|
||||
},
|
||||
};
|
||||
|
||||
if (args.writeGithubOutput) {
|
||||
appendGithubOutput(result);
|
||||
}
|
||||
|
||||
if (args.writeStepSummary) {
|
||||
appendStepSummary(result);
|
||||
}
|
||||
|
||||
if (args.json) {
|
||||
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
||||
} else {
|
||||
console.log(`[pr:preflight] Base ref: ${baseRef}`);
|
||||
console.log(`[pr:preflight] Changed files: ${changedFiles.length}`);
|
||||
console.log(
|
||||
`[pr:preflight] Classification: ${result.categories.length > 0 ? result.categories.join(", ") : "none"}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (args.checkPolicy) {
|
||||
if (directDerivedChanges.length > 0) {
|
||||
console.error(
|
||||
[
|
||||
"Pull requests are source-only.",
|
||||
"Remove derived files from the PR and let main regenerate them after merge.",
|
||||
`Derived files detected: ${directDerivedChanges.join(", ")}`,
|
||||
].join(" "),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (pullRequestBody !== null && !result.prBody.hasQualityChecklist) {
|
||||
console.error("PR body must include the Quality Bar Checklist section from the template.");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!args.noRun) {
|
||||
runCommand("npm", ["run", "validate"], projectRoot);
|
||||
|
||||
if (result.requiresReferencesValidation) {
|
||||
runCommand("npm", ["run", "validate:references"], projectRoot);
|
||||
}
|
||||
|
||||
runCommand("npm", ["run", "test"], projectRoot);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,66 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Release Cycle Automation Script
|
||||
# Enforces protocols from .github/MAINTENANCE.md
|
||||
|
||||
set -e
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
echo "release_cycle.sh is now a thin wrapper around the scripted release workflow."
|
||||
echo "Use \`npm run release:preflight\` directly for the supported entrypoint."
|
||||
|
||||
echo -e "${YELLOW}🤖 Initiating Antigravity Release Protocol...${NC}"
|
||||
|
||||
# 1. Validation Chain
|
||||
echo -e "\n${YELLOW}Step 1: Running Validation Chain...${NC}"
|
||||
echo "Running validate_skills.py..."
|
||||
python3 tools/scripts/validate_skills.py
|
||||
echo "Running generate_index.py..."
|
||||
python3 tools/scripts/generate_index.py
|
||||
echo "Running update_readme.py..."
|
||||
python3 tools/scripts/update_readme.py
|
||||
|
||||
# 2. Catalog (required for CI)
|
||||
echo -e "\n${YELLOW}Step 2: Build catalog...${NC}"
|
||||
npm run catalog
|
||||
|
||||
# 3. Stats Consistency Check
|
||||
echo -e "\n${YELLOW}Step 3: Verifying Stats Consistency...${NC}"
|
||||
JSON_COUNT=$(python3 -c "import json; print(len(json.load(open('skills_index.json'))))")
|
||||
echo "Skills in Registry (JSON): $JSON_COUNT"
|
||||
|
||||
# Check README Intro
|
||||
README_CONTENT=$(cat README.md)
|
||||
if [[ "$README_CONTENT" != *"$JSON_COUNT high-performance"* ]]; then
|
||||
echo -e "${RED}❌ ERROR: README.md intro consistency failure!${NC}"
|
||||
echo "Expected: '$JSON_COUNT high-performance'"
|
||||
echo "Found mismatch. Please grep for 'high-performance' in README.md and fix it."
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}✅ Stats Consistent.${NC}"
|
||||
|
||||
# 4. Version check (package.json is source of truth for npm)
|
||||
echo -e "\n${YELLOW}Step 4: Version check${NC}"
|
||||
PKG_VERSION=$(node -p "require('./package.json').version")
|
||||
echo "package.json version: $PKG_VERSION"
|
||||
echo "Ensure this version is bumped before 'npm publish' (npm forbids republishing the same version)."
|
||||
|
||||
# 5. Contributor Check
|
||||
echo -e "\n${YELLOW}Step 5: Contributor Check${NC}"
|
||||
echo "Recent commits by author (check against README 'Repo Contributors'):"
|
||||
git shortlog -sn --since="1 month ago" --all --no-merges | head -n 10
|
||||
|
||||
echo -e "${YELLOW}⚠️ MANUAL VERIFICATION REQUIRED:${NC}"
|
||||
echo "1. Are all PR authors above listed in 'Repo Contributors'?"
|
||||
echo "2. Are all External Sources listed in 'Credits & Sources'?"
|
||||
read -p "Type 'yes' to confirm you have verified contributors: " CONFIRM_CONTRIB
|
||||
|
||||
if [ "$CONFIRM_CONTRIB" != "yes" ]; then
|
||||
echo -e "${RED}❌ Verification failed. Aborting.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "\n${GREEN}✅ Release Cycle Checks Passed. You may now commit and push.${NC}"
|
||||
echo -e "${YELLOW}After tagging a release: run \`npm publish\` from repo root (or use GitHub Release + NPM_TOKEN for CI).${NC}"
|
||||
exit 0
|
||||
node tools/scripts/release_workflow.js preflight
|
||||
|
||||
252
tools/scripts/release_workflow.js
Normal file
252
tools/scripts/release_workflow.js
Normal file
@@ -0,0 +1,252 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { spawnSync } = require("child_process");
|
||||
|
||||
const { findProjectRoot } = require("../lib/project-root");
|
||||
const {
|
||||
extractChangelogSection,
|
||||
getManagedFiles,
|
||||
loadWorkflowContract,
|
||||
} = require("../lib/workflow-contract");
|
||||
|
||||
function parseArgs(argv) {
|
||||
const [command, version] = argv;
|
||||
return {
|
||||
command,
|
||||
version: version || null,
|
||||
};
|
||||
}
|
||||
|
||||
function runCommand(command, args, cwd, options = {}) {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
stdio: options.capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
||||
shell: options.shell ?? process.platform === "win32",
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
if (typeof result.status !== "number" || result.status !== 0) {
|
||||
const stderr = options.capture ? result.stderr.trim() : "";
|
||||
throw new Error(stderr || `${command} ${args.join(" ")} failed with status ${result.status}`);
|
||||
}
|
||||
|
||||
return options.capture ? result.stdout.trim() : "";
|
||||
}
|
||||
|
||||
function ensureOnMain(projectRoot) {
|
||||
const currentBranch = runCommand("git", ["rev-parse", "--abbrev-ref", "HEAD"], projectRoot, {
|
||||
capture: true,
|
||||
});
|
||||
if (currentBranch !== "main") {
|
||||
throw new Error(`Release workflow must run from main. Current branch: ${currentBranch}`);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureCleanWorkingTree(projectRoot, message) {
|
||||
const status = runCommand("git", ["status", "--porcelain", "--untracked-files=no"], projectRoot, {
|
||||
capture: true,
|
||||
});
|
||||
|
||||
if (status) {
|
||||
throw new Error(message || "Working tree has tracked changes. Commit or stash them first.");
|
||||
}
|
||||
}
|
||||
|
||||
function ensureTagMissing(projectRoot, tagName) {
|
||||
const result = spawnSync("git", ["rev-parse", "--verify", tagName], {
|
||||
cwd: projectRoot,
|
||||
stdio: "ignore",
|
||||
});
|
||||
|
||||
if (result.status === 0) {
|
||||
throw new Error(`Tag ${tagName} already exists.`);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureTagExists(projectRoot, tagName) {
|
||||
const result = spawnSync("git", ["rev-parse", "--verify", tagName], {
|
||||
cwd: projectRoot,
|
||||
stdio: "ignore",
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`Tag ${tagName} does not exist. Run release:prepare first.`);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureGithubReleaseMissing(projectRoot, tagName) {
|
||||
const result = spawnSync("gh", ["release", "view", tagName], {
|
||||
cwd: projectRoot,
|
||||
stdio: "ignore",
|
||||
});
|
||||
|
||||
if (result.status === 0) {
|
||||
throw new Error(`GitHub release ${tagName} already exists.`);
|
||||
}
|
||||
}
|
||||
|
||||
function readPackageVersion(projectRoot) {
|
||||
const packagePath = path.join(projectRoot, "package.json");
|
||||
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
||||
return packageJson.version;
|
||||
}
|
||||
|
||||
function ensureChangelogSection(projectRoot, version) {
|
||||
const changelogPath = path.join(projectRoot, "CHANGELOG.md");
|
||||
const changelogContent = fs.readFileSync(changelogPath, "utf8");
|
||||
return extractChangelogSection(changelogContent, version);
|
||||
}
|
||||
|
||||
function writeReleaseNotes(projectRoot, version, sectionContent) {
|
||||
const releaseNotesDir = path.join(projectRoot, ".tmp", "releases");
|
||||
const notesPath = path.join(releaseNotesDir, `v${version}.md`);
|
||||
fs.mkdirSync(releaseNotesDir, { recursive: true });
|
||||
fs.writeFileSync(notesPath, sectionContent, "utf8");
|
||||
return notesPath;
|
||||
}
|
||||
|
||||
function runReleaseSuite(projectRoot) {
|
||||
runCommand("npm", ["run", "validate"], projectRoot);
|
||||
runCommand("npm", ["run", "validate:references"], projectRoot);
|
||||
runCommand("npm", ["run", "sync:all"], projectRoot);
|
||||
runCommand("npm", ["run", "test"], projectRoot);
|
||||
runCommand("npm", ["run", "app:build"], projectRoot);
|
||||
}
|
||||
|
||||
function runReleasePreflight(projectRoot) {
|
||||
ensureOnMain(projectRoot);
|
||||
ensureCleanWorkingTree(projectRoot, "release:preflight requires a clean tracked working tree.");
|
||||
const version = readPackageVersion(projectRoot);
|
||||
ensureChangelogSection(projectRoot, version);
|
||||
runReleaseSuite(projectRoot);
|
||||
ensureCleanWorkingTree(
|
||||
projectRoot,
|
||||
"release:preflight left tracked changes. Sync and commit them before releasing.",
|
||||
);
|
||||
console.log(`[release] Preflight passed for version ${version}.`);
|
||||
}
|
||||
|
||||
function stageReleaseFiles(projectRoot, contract) {
|
||||
const filesToStage = getManagedFiles(contract, {
|
||||
includeMixed: true,
|
||||
includeReleaseManaged: true,
|
||||
});
|
||||
runCommand("git", ["add", ...filesToStage], projectRoot);
|
||||
}
|
||||
|
||||
function prepareRelease(projectRoot, version) {
|
||||
if (!version) {
|
||||
throw new Error("Usage: npm run release:prepare -- X.Y.Z");
|
||||
}
|
||||
|
||||
ensureOnMain(projectRoot);
|
||||
ensureCleanWorkingTree(projectRoot, "release:prepare requires a clean tracked working tree.");
|
||||
ensureTagMissing(projectRoot, `v${version}`);
|
||||
ensureChangelogSection(projectRoot, version);
|
||||
|
||||
const currentVersion = readPackageVersion(projectRoot);
|
||||
if (currentVersion !== version) {
|
||||
runCommand("npm", ["version", version, "--no-git-tag-version"], projectRoot);
|
||||
} else {
|
||||
console.log(`[release] package.json already set to ${version}; keeping current version.`);
|
||||
}
|
||||
|
||||
runReleaseSuite(projectRoot);
|
||||
runCommand(
|
||||
"npm",
|
||||
["run", "sync:metadata", "--", "--refresh-volatile"],
|
||||
projectRoot,
|
||||
);
|
||||
|
||||
const refreshedReleaseNotes = ensureChangelogSection(projectRoot, version);
|
||||
const notesPath = writeReleaseNotes(projectRoot, version, refreshedReleaseNotes);
|
||||
const contract = loadWorkflowContract(projectRoot);
|
||||
stageReleaseFiles(projectRoot, contract);
|
||||
|
||||
const stagedFiles = runCommand("git", ["diff", "--cached", "--name-only"], projectRoot, {
|
||||
capture: true,
|
||||
});
|
||||
if (!stagedFiles) {
|
||||
throw new Error("release:prepare did not stage any files. Nothing to commit.");
|
||||
}
|
||||
|
||||
runCommand("git", ["commit", "-m", `chore: release v${version}`], projectRoot);
|
||||
runCommand("git", ["tag", `v${version}`], projectRoot);
|
||||
|
||||
console.log(`[release] Prepared v${version}.`);
|
||||
console.log(`[release] Notes file: ${notesPath}`);
|
||||
console.log(`[release] Next step: npm run release:publish -- ${version}`);
|
||||
}
|
||||
|
||||
function publishRelease(projectRoot, version) {
|
||||
if (!version) {
|
||||
throw new Error("Usage: npm run release:publish -- X.Y.Z");
|
||||
}
|
||||
|
||||
ensureOnMain(projectRoot);
|
||||
ensureCleanWorkingTree(projectRoot, "release:publish requires a clean tracked working tree.");
|
||||
|
||||
const packageVersion = readPackageVersion(projectRoot);
|
||||
if (packageVersion !== version) {
|
||||
throw new Error(`package.json version ${packageVersion} does not match requested release ${version}.`);
|
||||
}
|
||||
|
||||
const tagName = `v${version}`;
|
||||
ensureTagExists(projectRoot, tagName);
|
||||
ensureGithubReleaseMissing(projectRoot, tagName);
|
||||
|
||||
const tagCommit = runCommand("git", ["rev-list", "-n", "1", tagName], projectRoot, {
|
||||
capture: true,
|
||||
});
|
||||
const headCommit = runCommand("git", ["rev-parse", "HEAD"], projectRoot, {
|
||||
capture: true,
|
||||
});
|
||||
if (tagCommit !== headCommit) {
|
||||
throw new Error(`${tagName} does not point at HEAD. Refusing to publish.`);
|
||||
}
|
||||
|
||||
const notesPath = writeReleaseNotes(projectRoot, version, ensureChangelogSection(projectRoot, version));
|
||||
|
||||
runCommand("git", ["push", "origin", "main"], projectRoot);
|
||||
runCommand("git", ["push", "origin", tagName], projectRoot);
|
||||
runCommand("gh", ["release", "create", tagName, "--title", tagName, "--notes-file", notesPath], projectRoot);
|
||||
|
||||
console.log(`[release] Published ${tagName}.`);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const projectRoot = findProjectRoot(__dirname);
|
||||
|
||||
if (args.command === "preflight") {
|
||||
runReleasePreflight(projectRoot);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.command === "prepare") {
|
||||
prepareRelease(projectRoot, args.version);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.command === "publish") {
|
||||
publishRelease(projectRoot, args.version);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"Usage: node tools/scripts/release_workflow.js <preflight|prepare|publish> [X.Y.Z]",
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
console.error(`[release] ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ const TOOL_TESTS = path.join(TOOL_SCRIPTS, "tests");
|
||||
const LOCAL_TEST_COMMANDS = [
|
||||
[path.join(TOOL_TESTS, "jetski_gemini_loader.test.js")],
|
||||
[path.join(TOOL_TESTS, "validate_skills_headings.test.js")],
|
||||
[path.join(TOOL_TESTS, "workflow_contracts.test.js")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_validate_skills_headings.py")],
|
||||
];
|
||||
const NETWORK_TEST_COMMANDS = [
|
||||
|
||||
68
tools/scripts/tests/workflow_contracts.test.js
Normal file
68
tools/scripts/tests/workflow_contracts.test.js
Normal file
@@ -0,0 +1,68 @@
|
||||
const assert = require("assert");
|
||||
|
||||
const {
|
||||
classifyChangedFiles,
|
||||
extractChangelogSection,
|
||||
getDirectDerivedChanges,
|
||||
hasIssueLink,
|
||||
hasQualityChecklist,
|
||||
requiresReferencesValidation,
|
||||
} = require("../../lib/workflow-contract");
|
||||
|
||||
const contract = {
|
||||
derivedFiles: [
|
||||
"CATALOG.md",
|
||||
"skills_index.json",
|
||||
"data/skills_index.json",
|
||||
"data/catalog.json",
|
||||
"data/bundles.json",
|
||||
"data/aliases.json",
|
||||
],
|
||||
mixedFiles: ["README.md"],
|
||||
releaseManagedFiles: ["CHANGELOG.md", "package.json", "package-lock.json", "README.md"],
|
||||
};
|
||||
|
||||
const skillOnly = classifyChangedFiles(["skills/example/SKILL.md"], contract);
|
||||
assert.deepStrictEqual(skillOnly.categories, ["skill"]);
|
||||
assert.strictEqual(skillOnly.primaryCategory, "skill");
|
||||
assert.strictEqual(requiresReferencesValidation(["skills/example/SKILL.md"], contract), false);
|
||||
|
||||
const docsOnly = classifyChangedFiles(["README.md", "docs/users/faq.md"], contract);
|
||||
assert.deepStrictEqual(docsOnly.categories, ["docs"]);
|
||||
assert.strictEqual(docsOnly.primaryCategory, "docs");
|
||||
assert.strictEqual(requiresReferencesValidation(["README.md"], contract), true);
|
||||
|
||||
const infraChange = classifyChangedFiles([".github/workflows/ci.yml", "tools/scripts/pr_preflight.js"], contract);
|
||||
assert.deepStrictEqual(infraChange.categories, ["infra"]);
|
||||
assert.strictEqual(infraChange.primaryCategory, "infra");
|
||||
assert.strictEqual(requiresReferencesValidation(["tools/scripts/pr_preflight.js"], contract), true);
|
||||
|
||||
const mixedChange = classifyChangedFiles(["skills/example/SKILL.md", "README.md"], contract);
|
||||
assert.deepStrictEqual(mixedChange.categories, ["skill", "docs"]);
|
||||
assert.strictEqual(mixedChange.primaryCategory, "skill");
|
||||
|
||||
assert.deepStrictEqual(
|
||||
getDirectDerivedChanges(["skills/example/SKILL.md", "data/catalog.json"], contract),
|
||||
["data/catalog.json"],
|
||||
);
|
||||
|
||||
const changelog = [
|
||||
"## [7.7.0] - 2026-03-13 - \"Merge Friction Reduction\"",
|
||||
"",
|
||||
"- Line one",
|
||||
"",
|
||||
"## [7.6.0] - 2026-03-01 - \"Older Release\"",
|
||||
"",
|
||||
"- Older line",
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
assert.strictEqual(
|
||||
extractChangelogSection(changelog, "7.7.0"),
|
||||
"## [7.7.0] - 2026-03-13 - \"Merge Friction Reduction\"\n\n- Line one\n",
|
||||
);
|
||||
|
||||
assert.strictEqual(hasQualityChecklist("## Quality Bar Checklist\n- [x] Standards"), true);
|
||||
assert.strictEqual(hasQualityChecklist("No template here"), false);
|
||||
assert.strictEqual(hasIssueLink("Fixes #123"), true);
|
||||
assert.strictEqual(hasIssueLink("Related to #123"), false);
|
||||
Reference in New Issue
Block a user