Skip to content

Commit dfe985c

Browse files
committed
Add validation for select metadata fields.
1 parent c75faac commit dfe985c

3 files changed

Lines changed: 95 additions & 9 deletions

File tree

scripts/api_md_workflow/adapters/python.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,15 @@ function generateApiForPackage({
121121
});
122122
}
123123

124+
// Fields in API.metadata.yml that must match between working tree and committed version.
125+
// pythonVersion is excluded because it varies across CI environments.
126+
const metadataFieldsToValidate = ["apiMdSha256", "parserVersion"];
127+
124128
module.exports = {
125129
name: "python",
126130
isPackageDir,
127131
findPackageDir,
128132
readVersion,
129133
generateApiForPackage,
134+
metadataFieldsToValidate,
130135
};

scripts/api_md_workflow/create_api_review_pr.js

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,14 @@ function apiMdRel(packageDir) {
173173
return `${packageRelDir(packageDir)}/API.md`;
174174
}
175175

176+
function metadataPath(packageDir) {
177+
return path.join(packageDir, "API.metadata.yml");
178+
}
179+
180+
function metadataRel(packageDir) {
181+
return `${packageRelDir(packageDir)}/API.metadata.yml`;
182+
}
183+
176184
function findRealGitExe() {
177185
if (process.platform !== "win32") {
178186
return null;
@@ -389,7 +397,14 @@ function generateApiBytesForPackage({
389397
throw new Error(`ERROR: did not produce ${outputPath}`);
390398
}
391399

392-
return fs.readFileSync(outputPath);
400+
const result = { apiMd: fs.readFileSync(outputPath), metadata: null };
401+
402+
const metaPath = metadataPath(packageDir);
403+
if (fs.existsSync(metaPath)) {
404+
result.metadata = fs.readFileSync(metaPath);
405+
}
406+
407+
return result;
393408
}
394409

395410
function main() {
@@ -415,11 +430,11 @@ function main() {
415430
const targetRef = args.target ? resolveTargetRef(args.target) : MAIN_REF;
416431

417432
try {
418-
let baseApiBytes = null;
433+
let baseResult = null;
419434
if (args.base) {
420435
logInfo(`\n=== Capturing baseline API.md from tag ${args.base} ===`);
421436
git(["checkout", "--detach", args.base]);
422-
baseApiBytes = generateApiBytesForPackage({
437+
baseResult = generateApiBytesForPackage({
423438
adapter,
424439
repoRoot: REPO_ROOT,
425440
packageName: args.packageName,
@@ -434,7 +449,7 @@ function main() {
434449
logInfo(`\n=== Capturing target API.md from ${targetRef} ===`);
435450
git(["checkout", "--detach", targetRef]);
436451
const targetVersion = adapter.readVersion(packageDir);
437-
const targetApiBytes = generateApiBytesForPackage({
452+
const targetResult = generateApiBytesForPackage({
438453
adapter,
439454
repoRoot: REPO_ROOT,
440455
packageName: args.packageName,
@@ -453,10 +468,16 @@ function main() {
453468

454469
const apiPath = apiMdPath(packageDir);
455470
const apiRelative = apiMdRel(packageDir);
471+
const metaFilePath = metadataPath(packageDir);
472+
const metaRelative = metadataRel(packageDir);
456473

457-
if (baseApiBytes !== null) {
458-
writeBytes(apiPath, baseApiBytes);
474+
if (baseResult !== null) {
475+
writeBytes(apiPath, baseResult.apiMd);
459476
git(["add", apiRelative]);
477+
if (baseResult.metadata) {
478+
writeBytes(metaFilePath, baseResult.metadata);
479+
git(["add", metaRelative]);
480+
}
460481
git(["commit", "-m", `[API Review] Baseline API.md for ${args.packageName} ${baseVersion}`]);
461482
} else {
462483
const tracked = git(["ls-files", "--error-unmatch", apiRelative], {
@@ -466,11 +487,21 @@ function main() {
466487

467488
if (tracked.status === 0) {
468489
git(["rm", apiRelative]);
490+
const metaTracked = git(["ls-files", "--error-unmatch", metaRelative], {
491+
capture: true,
492+
check: false,
493+
});
494+
if (metaTracked.status === 0) {
495+
git(["rm", metaRelative]);
496+
}
469497
git(["commit", "-m", `[API Review] Remove API.md for ${args.packageName} (empty baseline)`]);
470498
} else {
471499
if (fs.existsSync(apiPath)) {
472500
fs.unlinkSync(apiPath);
473501
}
502+
if (fs.existsSync(metaFilePath)) {
503+
fs.unlinkSync(metaFilePath);
504+
}
474505
git(["commit", "--allow-empty", "-m", `[API Review] Empty baseline for ${args.packageName}`]);
475506
}
476507
}
@@ -479,8 +510,12 @@ function main() {
479510

480511
logInfo(`\n=== Creating review branch ${reviewBranch} ===`);
481512
git(["checkout", "-B", reviewBranch, baseBranch]);
482-
writeBytes(apiPath, targetApiBytes);
513+
writeBytes(apiPath, targetResult.apiMd);
483514
git(["add", apiRelative]);
515+
if (targetResult.metadata) {
516+
writeBytes(metaFilePath, targetResult.metadata);
517+
git(["add", metaRelative]);
518+
}
484519

485520
const diff = git(["diff", "--cached", "--quiet"], {
486521
capture: true,

scripts/api_md_workflow/find_mismatches.js

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,31 @@
33
const fs = require("fs");
44

55
const { appendGithubOutput, envPath, getDefaultLogger, readLines, runAsync, writeLines } = require("./common");
6+
const { loadAdapter, loadWorkflowConfig } = require("./adapter_config");
7+
8+
/**
9+
* Parse a simple key: value YAML file into an object.
10+
* Only handles flat scalar mappings (no nesting, no multi-line values).
11+
*/
12+
function parseSimpleYaml(text) {
13+
const result = {};
14+
for (const line of text.split(/\r?\n/)) {
15+
const match = line.match(/^(\w+)\s*:\s*(.*)$/);
16+
if (match) {
17+
result[match[1]] = match[2].trim();
18+
}
19+
}
20+
return result;
21+
}
622

723
async function main() {
24+
const config = loadWorkflowConfig();
25+
const adapter = loadAdapter(config.adapter);
26+
27+
// Fields to compare in API.metadata.yml. If the adapter doesn't specify,
28+
// compare all fields (strict default for languages that don't opt out).
29+
const fieldsToValidate = adapter.metadataFieldsToValidate || null;
30+
831
const packagesFile = envPath("API_MD_PACKAGES_FILE", ".artifacts/affected_package_dirs.txt");
932
const mismatchesFile = envPath("API_MD_MISMATCHES_FILE", ".artifacts/mismatched_api_files.txt");
1033
const missingFile = envPath("API_MD_MISSING_FILE", ".artifacts/missing_api_files.txt");
@@ -13,9 +36,8 @@ async function main() {
1336
const mismatches = [];
1437
const missing = [];
1538
for (const pkgDir of packages) {
16-
// Deliberately scope consistency gating to API.md only.
17-
// API.metadata.yml is generated sidecar metadata and is not diff-gated here.
1839
const apiFile = `${pkgDir}/API.md`;
40+
const metadataFile = `${pkgDir}/API.metadata.yml`;
1941

2042
// Enforce that each affected package has a committed API.md file.
2143
if (!fs.existsSync(apiFile) || !fs.statSync(apiFile).isFile()) {
@@ -31,6 +53,30 @@ async function main() {
3153
continue;
3254
}
3355

56+
// API.metadata.yml must be present alongside API.md.
57+
if (!fs.existsSync(metadataFile) || !fs.statSync(metadataFile).isFile()) {
58+
missing.push(metadataFile);
59+
} else {
60+
const committedMeta = await runAsync("git", ["show", `HEAD:${metadataFile}`], {
61+
check: false,
62+
});
63+
if (committedMeta.status !== 0) {
64+
// Not yet committed — treat as missing
65+
missing.push(metadataFile);
66+
} else {
67+
const current = parseSimpleYaml(fs.readFileSync(metadataFile, "utf-8"));
68+
const committed = parseSimpleYaml(committedMeta.stdout);
69+
70+
// Compare only adapter-specified fields, or all fields if not specified.
71+
const keys = fieldsToValidate || Object.keys({ ...committed, ...current });
72+
const mismatch = keys.some((key) => current[key] !== committed[key]);
73+
if (mismatch) {
74+
mismatches.push(metadataFile);
75+
}
76+
}
77+
}
78+
79+
// Diff-gate only API.md; metadata content differences are acceptable.
3480
const quietDiffResult = await runAsync("git", ["diff", "--quiet", "--", apiFile], {
3581
check: false,
3682
});

0 commit comments

Comments
 (0)