Skip to content

Commit a5fd142

Browse files
authored
Merge pull request #14 from APIOpsCycles/housekeeping3_4_1
Housekeeping3 4 1
2 parents 272de33 + db398cc commit a5fd142

8 files changed

Lines changed: 283 additions & 7 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: Publish create-apiops
2+
3+
on:
4+
workflow_dispatch:
5+
6+
permissions:
7+
contents: read
8+
9+
jobs:
10+
publish:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Checkout repository
14+
uses: actions/checkout@v4
15+
with:
16+
fetch-depth: 0
17+
18+
- name: Setup Node.js
19+
uses: actions/setup-node@v4
20+
with:
21+
node-version: 22
22+
registry-url: https://registry.npmjs.org
23+
cache: npm
24+
25+
- name: Install dependencies
26+
run: npm ci
27+
28+
- name: Inspect root package contents
29+
run: npm run check:package:contents
30+
31+
- name: Run create-apiops scaffold integration test
32+
run: npm run test:create-apiops
33+
34+
- name: Read scaffolded root package dependency version
35+
shell: bash
36+
run: |
37+
ROOT_VERSION=$(node --input-type=module -e "import fs from 'node:fs'; const pkg = JSON.parse(fs.readFileSync('packages/create-apiops/template/package.json', 'utf8')); const version = pkg.dependencies?.['apiops-cycles-method-data']; if (!version) { process.exit(1); } console.log(version.replace(/^[^0-9]*/, ''));")
38+
if [ -z "$ROOT_VERSION" ]; then
39+
echo "Failed to resolve apiops-cycles-method-data dependency version from template/package.json"
40+
exit 1
41+
fi
42+
echo "ROOT_VERSION=$ROOT_VERSION" >> "$GITHUB_ENV"
43+
echo "Resolved template dependency version: $ROOT_VERSION"
44+
45+
- name: Verify referenced root package version exists on npm
46+
run: npm view apiops-cycles-method-data@${ROOT_VERSION} version
47+
48+
- name: Publish create-apiops
49+
run: npm publish --workspace packages/create-apiops --access public
50+
env:
51+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "apiops-cycles-method-data",
3-
"version": "3.4.0",
3+
"version": "3.4.1",
44
"description": "APIOps Cycles Method data and canvases",
55
"license": "Apache-2.0",
66
"type": "module",
@@ -30,7 +30,7 @@
3030
"./snippets/*": "./src/snippets/*"
3131
},
3232
"scripts": {
33-
"test": "node scripts/validate.mjs && node scripts/test-method-stakeholders.mjs && node scripts/test-note-colors.mjs && node scripts/test-print-method-snippet.mjs",
33+
"test": "node scripts/validate.mjs && node scripts/test-method-stakeholders.mjs && node scripts/test-note-colors.mjs && node scripts/test-print-method-snippet.mjs && node scripts/test-method-content-integrity.mjs",
3434
"release:create-apiops:pack": "npm pack --workspace packages/create-apiops",
3535
"release:create-apiops:publish": "npm publish --workspace packages/create-apiops --access public",
3636
"check:packaging:skills": "node scripts/check-packaging-skills.mjs",

packages/create-apiops/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "create-apiops",
3-
"version": "1.1.0",
3+
"version": "1.1.1",
44
"description": "Scaffold a new APIOps project with an OpenAPI spec, audit scaffolding, and APIOps documentation templates.",
55
"license": "Apache-2.0",
66
"type": "module",

packages/create-apiops/template/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ In practice:
110110
- `scripts/run-design-audit.js` also selects the matching Spectral ruleset directly when generating coverage reports
111111
- `.spectral.yaml` is still useful as the default config for editor integrations or manual commands like `spectral lint specs/openapi/api.yaml`
112112

113+
Note: the Spectral toolchain currently relies on some transitive dependencies that may otherwise trigger npm audit findings. The `overrides` in `package.json` are intentional and are used to keep the scaffolded dependency tree in a working and lower-risk state.
114+
113115
The audit command writes multiple outputs so the same result can be used by developers, docs, and CI:
114116

115117
- `specs/audit/design-audit.<profile>.json` as the canonical machine-readable result

packages/create-apiops/template/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"node": ">=22"
77
},
88
"dependencies": {
9-
"apiops-cycles-method-data": "^3.4.0",
9+
"apiops-cycles-method-data": "^3.4.1",
1010
"canvascreator": "^1.7.3"
1111
},
1212
"devDependencies": {
@@ -19,7 +19,8 @@
1919
},
2020
"@stoplight/spectral-ruleset-bundler": {
2121
"rollup": "2.80.0"
22-
}
22+
},
23+
"lodash": "~4.18.0"
2324
},
2425
"scripts": {
2526
"preinstall": "node ./scripts/check-node-version.js",
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import { existsSync, readFileSync, readdirSync } from "node:fs";
2+
import path from "node:path";
3+
4+
function readJson(filePath) {
5+
return JSON.parse(readFileSync(filePath, "utf8"));
6+
}
7+
8+
const stationsJson = readJson("src/data/method/stations.json");
9+
const linesJson = readJson("src/data/method/lines.json");
10+
const resourcesJson = readJson("src/data/method/resources.json");
11+
const criteriaJson = readJson("src/data/method/criteria.json");
12+
const stakeholdersJson = readJson("src/data/method/stakeholders.json");
13+
const canvasDataJson = readJson("src/data/canvas/canvasData.json");
14+
const localizedCanvasDataJson = readJson("src/data/canvas/localizedData.json");
15+
const knownResourceIds = new Set((resourcesJson.resources || []).map((resource) => resource.id));
16+
const stationGroups = [
17+
...(stationsJson["core-stations"]?.items || []).map((station) => ({ ...station, group: "core-stations" })),
18+
...(stationsJson["sub-stations"]?.items || []).map((station) => ({ ...station, group: "sub-stations" }))
19+
];
20+
const knownStationIds = new Set(stationGroups.map((station) => station.id));
21+
const knownCanvasIds = new Set(Object.keys(canvasDataJson));
22+
const localeDirs = readdirSync("src/data/method", { withFileTypes: true })
23+
.filter((entry) => entry.isDirectory())
24+
.map((entry) => entry.name);
25+
const findings = [];
26+
27+
function collectMatchingStringValues(node, pattern, results = new Set()) {
28+
if (typeof node === "string") {
29+
if (pattern.test(node)) {
30+
results.add(node);
31+
}
32+
return results;
33+
}
34+
35+
if (Array.isArray(node)) {
36+
for (const item of node) {
37+
collectMatchingStringValues(item, pattern, results);
38+
}
39+
return results;
40+
}
41+
42+
if (node && typeof node === "object") {
43+
for (const value of Object.values(node)) {
44+
collectMatchingStringValues(value, pattern, results);
45+
}
46+
}
47+
48+
return results;
49+
}
50+
51+
function validateLabelFile(locale, filename, expectedKeys, allowedExtras = []) {
52+
const filePath = path.join("src", "data", "method", locale, filename);
53+
const labels = readJson(filePath);
54+
const actualKeys = new Set(Object.keys(labels));
55+
const allowedExtrasSet = new Set(allowedExtras);
56+
57+
for (const expectedKey of expectedKeys) {
58+
if (!actualKeys.has(expectedKey)) {
59+
findings.push(`Locale ${locale} is missing label "${expectedKey}" in ${filename}.`);
60+
}
61+
}
62+
63+
for (const actualKey of actualKeys) {
64+
if (!expectedKeys.has(actualKey) && !allowedExtrasSet.has(actualKey)) {
65+
findings.push(`Locale ${locale} has unused label "${actualKey}" in ${filename}.`);
66+
}
67+
}
68+
}
69+
70+
for (const station of stationGroups) {
71+
const steps = station.how_it_works || station["how-it-works"] || [];
72+
const seenStepKeys = new Map();
73+
const seenResources = new Map();
74+
75+
for (const [index, step] of steps.entries()) {
76+
const stepKey = String(step?.step || "").trim();
77+
const resourceId = String(step?.resource || "").trim();
78+
79+
if (!stepKey) {
80+
findings.push(`Station ${station.id} has an empty step value at index ${index}.`);
81+
} else if (seenStepKeys.has(stepKey)) {
82+
findings.push(
83+
`Station ${station.id} contains duplicate step key "${stepKey}" at indexes ${seenStepKeys.get(stepKey)} and ${index}.`
84+
);
85+
} else {
86+
seenStepKeys.set(stepKey, index);
87+
}
88+
89+
if (!resourceId) {
90+
continue;
91+
}
92+
93+
if (seenResources.has(resourceId)) {
94+
findings.push(
95+
`Station ${station.id} contains duplicate resource reference "${resourceId}" at indexes ${seenResources.get(resourceId)} and ${index}.`
96+
);
97+
} else {
98+
seenResources.set(resourceId, index);
99+
}
100+
101+
if (!knownResourceIds.has(resourceId)) {
102+
findings.push(
103+
`Station ${station.id} references unknown resource "${resourceId}" at index ${index}.`
104+
);
105+
}
106+
}
107+
}
108+
109+
for (const line of linesJson.lines?.items || []) {
110+
const seenStations = new Map();
111+
112+
for (const [index, stationId] of (line.stations || []).entries()) {
113+
if (seenStations.has(stationId)) {
114+
findings.push(
115+
`Line ${line.id} contains duplicate station "${stationId}" at indexes ${seenStations.get(stationId)} and ${index}.`
116+
);
117+
} else {
118+
seenStations.set(stationId, index);
119+
}
120+
121+
if (!knownStationIds.has(stationId)) {
122+
findings.push(`Line ${line.id} references unknown station "${stationId}" at index ${index}.`);
123+
}
124+
}
125+
}
126+
127+
const stationsUsedInLines = new Set(
128+
(linesJson.lines?.items || []).flatMap((line) => line.stations || [])
129+
);
130+
for (const station of stationGroups) {
131+
if (!stationsUsedInLines.has(station.id)) {
132+
findings.push(`Station ${station.id} does not belong to any line.`);
133+
}
134+
}
135+
136+
for (const resource of resourcesJson.resources || []) {
137+
if (resource.category === "canvas") {
138+
if (!resource.canvas) {
139+
findings.push(`Canvas resource ${resource.id} is missing its canvas id.`);
140+
} else if (!knownCanvasIds.has(resource.canvas)) {
141+
findings.push(`Canvas resource ${resource.id} references unknown canvas "${resource.canvas}".`);
142+
}
143+
}
144+
145+
if (resource.snippet) {
146+
const snippetPath = path.join("src", "snippets", resource.snippet);
147+
if (!existsSync(snippetPath)) {
148+
findings.push(`Resource ${resource.id} references missing snippet "${resource.snippet}".`);
149+
}
150+
}
151+
}
152+
153+
const expectedStationLabels = collectMatchingStringValues(stationsJson, /^(group|station)\./);
154+
const expectedLineLabels = collectMatchingStringValues(linesJson, /^(lines|line)\./);
155+
const expectedResourceLabels = collectMatchingStringValues(resourcesJson, /^resource\./);
156+
const expectedCriteriaLabels = new Set(criteriaJson.map((criterion) => `criterion.${criterion.id}`));
157+
const expectedStakeholderLabels = collectMatchingStringValues(stakeholdersJson, /^stakeholder\./);
158+
159+
for (const locale of localeDirs) {
160+
validateLabelFile(locale, "labels.stations.json", expectedStationLabels);
161+
validateLabelFile(locale, "labels.lines.json", expectedLineLabels);
162+
validateLabelFile(locale, "labels.resources.json", expectedResourceLabels);
163+
validateLabelFile(locale, "labels.criteria.json", expectedCriteriaLabels, ["entry_criteria", "exit_criteria"]);
164+
validateLabelFile(
165+
locale,
166+
"labels.stakeholders.json",
167+
expectedStakeholderLabels,
168+
[
169+
"stakeholder.involvement.lead",
170+
"stakeholder.involvement.core",
171+
"stakeholder.involvement.consulted"
172+
]
173+
);
174+
}
175+
176+
for (const locale of Object.keys(localizedCanvasDataJson)) {
177+
const localizedCanvases = localizedCanvasDataJson[locale] || {};
178+
179+
for (const [canvasId, canvas] of Object.entries(canvasDataJson)) {
180+
const localizedCanvas = localizedCanvases[canvasId];
181+
if (!localizedCanvas) {
182+
findings.push(`Locale ${locale} is missing canvas localization for "${canvasId}".`);
183+
continue;
184+
}
185+
186+
if (!String(localizedCanvas.title || "").trim()) {
187+
findings.push(`Locale ${locale} is missing canvas title for "${canvasId}".`);
188+
}
189+
if (!String(localizedCanvas.purpose || "").trim()) {
190+
findings.push(`Locale ${locale} is missing canvas purpose for "${canvasId}".`);
191+
}
192+
if (!String(localizedCanvas.howToUse || "").trim()) {
193+
findings.push(`Locale ${locale} is missing canvas howToUse for "${canvasId}".`);
194+
}
195+
196+
for (const section of canvas.sections || []) {
197+
const localizedSection = localizedCanvas.sections?.[section.id];
198+
if (!localizedSection) {
199+
findings.push(`Locale ${locale} is missing section localization for "${canvasId}.${section.id}".`);
200+
continue;
201+
}
202+
203+
if (!String(localizedSection.section || "").trim()) {
204+
findings.push(`Locale ${locale} is missing section title for "${canvasId}.${section.id}".`);
205+
}
206+
if (!String(localizedSection.description || "").trim()) {
207+
findings.push(`Locale ${locale} is missing section description for "${canvasId}.${section.id}".`);
208+
}
209+
}
210+
}
211+
}
212+
213+
if (findings.length > 0) {
214+
console.error("Method content validation failed:");
215+
for (const finding of findings) {
216+
console.error(`- ${finding}`);
217+
}
218+
process.exit(1);
219+
}
220+
221+
console.log("Method content validation passed.");

src/data/method/fr/labels.criteria.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"criterion.architecture-patterns-validated": "Les modèles d'architecture et de plateforme API choisis ont été validés avec les parties prenantes d'architecture, de sécurité et de plateforme pertinentes.",
1515
"criterion.design-reflects-business-value": "La conception de l'API et ses capacités exposées reflètent clairement la valeur commerciale et les besoins des utilisateurs.",
1616
"criterion.api-consistency": "La conception de l'API suit nos conventions partagées pour le produit API et la conception.",
17+
"criterion.api-contract-tested": "Le contrat d'API est testé et répond aux exigences fonctionnelles et non fonctionnelles.",
1718
"criterion.api-description-available": "L'API et ses capacités exposées sont décrites de manière suffisante pour être examinées, auditées et intégrées.",
1819
"criterion.audit-passed": "L'API passe avec succès les contrôles de conformité, de sécurité et d'audit.",
1920
"criterion.audit-reports-shared": "Les résultats de l'audit et les décisions de correction sont partagés avec les parties prenantes pertinentes.",

0 commit comments

Comments
 (0)