Skip to content

Commit 43f2c72

Browse files
authored
Merge pull request #183 from morri-son/add-cv-creation-for-cli
Add cv creation for cli
2 parents f39cf80 + afe024d commit 43f2c72

File tree

14 files changed

+1264
-115
lines changed

14 files changed

+1264
-115
lines changed

.github/scripts/component-constructor.js

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// @ts-check
22
import fs from "fs";
3+
import path from "path";
34
import yaml from "js-yaml";
45

56
/**
@@ -76,6 +77,9 @@ export function patchCliConstructor(constructor, imageRef, imageTag) {
7677
resource.name === "cli" &&
7778
resource.input?.type === "file"
7879
) {
80+
if (typeof resource.input.path !== "string" || resource.input.path.length === 0) {
81+
throw new Error("CLI file resource is missing required field 'input.path'");
82+
}
7983
const parts = resource.input.path.split("/");
8084
const filename = parts[parts.length - 1];
8185
resource.input.path = `resources/bin/${filename}`;
@@ -103,6 +107,179 @@ export function patchCliConstructor(constructor, imageRef, imageTag) {
103107
return constructor;
104108
}
105109

110+
/**
111+
* GitHub Actions entrypoint: patch the CLI component constructor for publishing.
112+
*
113+
* Verifies that CLI binaries exist, parses the constructor YAML, patches paths
114+
* and image access, and writes the result to the target location.
115+
*
116+
* Environment variables:
117+
* - CONSTRUCTOR_SOURCE: Path to the source component-constructor.yaml (required)
118+
* - TARGET_CONSTRUCTOR: Path to write the patched constructor (required)
119+
* - IMAGE_REF: Full OCI image reference, e.g. "ghcr.io/owner/cli:tag" (required)
120+
* - IMAGE_TAG: Image tag / version string (required)
121+
*
122+
* @param {import('@actions/github-script').AsyncFunctionArguments} args
123+
*/
124+
export async function patchCliConstructorAction({ core }) {
125+
const constructorSource = process.env.CONSTRUCTOR_SOURCE;
126+
const targetConstructor = process.env.TARGET_CONSTRUCTOR;
127+
const imageRef = process.env.IMAGE_REF;
128+
const imageTag = process.env.IMAGE_TAG;
129+
130+
if (!constructorSource) {
131+
core.setFailed("CONSTRUCTOR_SOURCE environment variable is required");
132+
return;
133+
}
134+
if (!targetConstructor) {
135+
core.setFailed("TARGET_CONSTRUCTOR environment variable is required");
136+
return;
137+
}
138+
if (!imageRef) {
139+
core.setFailed("IMAGE_REF environment variable is required");
140+
return;
141+
}
142+
if (!imageTag) {
143+
core.setFailed("IMAGE_TAG environment variable is required");
144+
return;
145+
}
146+
147+
try {
148+
// Verify CLI binaries exist
149+
const binDir = "bin";
150+
const entries = fs.readdirSync(binDir).filter(f => f.startsWith("ocm-"));
151+
if (entries.length === 0) {
152+
throw new Error(`No CLI binaries found under ./${binDir}`);
153+
}
154+
core.info(`✅ Found ${entries.length} CLI binary(ies): ${entries.join(", ")}`);
155+
156+
// Verify source constructor exists
157+
if (!fs.existsSync(constructorSource)) {
158+
throw new Error(`Constructor source not found: ${constructorSource}`);
159+
}
160+
161+
// Parse, patch, and write
162+
const constructor = parseConstructorFile(constructorSource);
163+
patchCliConstructor(constructor, imageRef, imageTag);
164+
165+
const targetDir = path.dirname(targetConstructor);
166+
fs.mkdirSync(targetDir, { recursive: true });
167+
fs.writeFileSync(targetConstructor, yaml.dump(constructor), "utf8");
168+
169+
core.info(`✅ Patched constructor written to ${targetConstructor}`);
170+
171+
// Validate round-trip
172+
const written = parseConstructorFile(targetConstructor);
173+
const imageResource = written.resources.find(r => r.name === "image");
174+
if (!imageResource || imageResource.access?.imageReference !== imageRef) {
175+
throw new Error("Validation failed: patched constructor does not contain expected image reference");
176+
}
177+
core.info(`✅ Validated image reference: ${imageRef}`);
178+
} catch (error) {
179+
core.setFailed(error.message);
180+
}
181+
}
182+
183+
/**
184+
* Promote a constructor from RC to final version:
185+
* - Replace the top-level version
186+
* - Replace all resource versions
187+
* - Update the image resource's access.imageReference
188+
*
189+
* @param {Object} constructor - Parsed constructor YAML object (mutated in place)
190+
* @param {string} finalVersion - Final version string (e.g. "0.17.0")
191+
* @param {string} imageRef - Full OCI image reference with final tag (e.g. "ghcr.io/owner/cli:0.17.0")
192+
* @returns {Object} The mutated constructor object
193+
* @throws {Error} If expected resources or access fields are not found
194+
*/
195+
export function promoteConstructorVersion(constructor, finalVersion, imageRef) {
196+
if (!Array.isArray(constructor.resources)) {
197+
throw new Error("Constructor has no 'resources' array");
198+
}
199+
200+
constructor.version = finalVersion;
201+
202+
for (const resource of constructor.resources) {
203+
resource.version = finalVersion;
204+
}
205+
206+
const imageResource = constructor.resources.find(r => r.name === "image");
207+
if (!imageResource || !imageResource.access) {
208+
throw new Error("No image resource with 'access' found — was the constructor patched by patchCliConstructor first?");
209+
}
210+
imageResource.access.imageReference = imageRef;
211+
212+
return constructor;
213+
}
214+
215+
/**
216+
* GitHub Actions entrypoint: promote an RC constructor to final version.
217+
*
218+
* Reads the RC constructor, replaces all version fields with the final version,
219+
* updates the image reference, validates the result, and writes it back.
220+
*
221+
* Environment variables:
222+
* - CONSTRUCTOR: Path to the component-constructor.yaml (read and written in place) (required)
223+
* - FINAL_VERSION: Final version string, e.g. "0.17.0" (required)
224+
* - IMAGE_REF: Full OCI image reference with final tag (required)
225+
*
226+
* @param {import('@actions/github-script').AsyncFunctionArguments} args
227+
*/
228+
export async function promoteConstructorVersionAction({ core }) {
229+
const constructorPath = process.env.CONSTRUCTOR;
230+
const finalVersion = process.env.FINAL_VERSION;
231+
const imageRef = process.env.IMAGE_REF;
232+
233+
if (!constructorPath) {
234+
core.setFailed("CONSTRUCTOR environment variable is required");
235+
return;
236+
}
237+
if (!finalVersion) {
238+
core.setFailed("FINAL_VERSION environment variable is required");
239+
return;
240+
}
241+
if (!imageRef) {
242+
core.setFailed("IMAGE_REF environment variable is required");
243+
return;
244+
}
245+
246+
try {
247+
if (!fs.existsSync(constructorPath)) {
248+
throw new Error(`Constructor not found: ${constructorPath}`);
249+
}
250+
251+
const constructor = parseConstructorFile(constructorPath);
252+
const rcVersion = constructor.version;
253+
core.info(`Promoting constructor from ${rcVersion} to ${finalVersion}...`);
254+
255+
promoteConstructorVersion(constructor, finalVersion, imageRef);
256+
257+
fs.writeFileSync(constructorPath, yaml.dump(constructor), "utf8");
258+
core.info(`✅ Patched constructor written to ${constructorPath}`);
259+
260+
// Validate round-trip
261+
const written = parseConstructorFile(constructorPath);
262+
263+
if (written.version !== finalVersion) {
264+
throw new Error(`Validation failed: .version is '${written.version}', expected '${finalVersion}'`);
265+
}
266+
267+
const resourceVersions = [...new Set(written.resources.map(r => r.version))];
268+
if (resourceVersions.length !== 1 || resourceVersions[0] !== finalVersion) {
269+
throw new Error(`Validation failed: resource versions are [${resourceVersions}], expected all '${finalVersion}'`);
270+
}
271+
272+
const imageResource = written.resources.find(r => r.name === "image");
273+
if (!imageResource || imageResource.access?.imageReference !== imageRef) {
274+
throw new Error(`Validation failed: image reference is '${imageResource?.access?.imageReference}', expected '${imageRef}'`);
275+
}
276+
277+
core.info(`✅ Validated: version=${finalVersion}, image=${imageRef}`);
278+
} catch (error) {
279+
core.setFailed(error.message);
280+
}
281+
}
282+
106283
/**
107284
* GitHub Actions entrypoint: summarize a published component version in the step summary.
108285
*

.github/scripts/component-constructor.test.js

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
buildPackageUrl,
99
patchCliConstructor,
1010
parseConstructorFile,
11+
promoteConstructorVersion,
1112
} from "./component-constructor.js";
1213

1314
// ----------------------------------------------------------
@@ -148,6 +149,30 @@ assert.throws(
148149
"Should throw when resources array is missing"
149150
);
150151

152+
// patchCliConstructor: missing input.path on CLI file resource
153+
assert.throws(
154+
() => patchCliConstructor({
155+
resources: [
156+
{ name: "cli", input: { type: "file" } },
157+
{ name: "image", type: "ociImage", relation: "local", input: { type: "file", path: "x" } },
158+
],
159+
}, "ref", "tag"),
160+
/missing required field 'input\.path'/,
161+
"Should throw when CLI file resource has no input.path"
162+
);
163+
164+
// patchCliConstructor: empty input.path on CLI file resource
165+
assert.throws(
166+
() => patchCliConstructor({
167+
resources: [
168+
{ name: "cli", input: { type: "file", path: "" } },
169+
{ name: "image", type: "ociImage", relation: "local", input: { type: "file", path: "x" } },
170+
],
171+
}, "ref", "tag"),
172+
/missing required field 'input\.path'/,
173+
"Should throw when CLI file resource has empty input.path"
174+
);
175+
151176
// patchCliConstructor: non-file CLI resources are left untouched
152177
{
153178
const constructor = {
@@ -208,4 +233,112 @@ assert.throws(
208233
fs.rmdirSync(tmpDir);
209234
}
210235

211-
console.log("✅ All component-constructor tests passed.");
236+
// ----------------------------------------------------------
237+
// promoteConstructorVersion
238+
// ----------------------------------------------------------
239+
console.log("Testing promoteConstructorVersion...");
240+
241+
// Happy path: promotes version, resource versions, and image reference
242+
{
243+
const constructor = {
244+
name: "ocm.software/cli",
245+
version: "0.17.0-rc.1",
246+
resources: [
247+
{
248+
name: "cli",
249+
type: "executable",
250+
version: "0.17.0-rc.1",
251+
input: { type: "file", path: "resources/bin/ocm-linux-amd64" },
252+
},
253+
{
254+
name: "cli",
255+
type: "executable",
256+
version: "0.17.0-rc.1",
257+
input: { type: "file", path: "resources/bin/ocm-darwin-arm64" },
258+
},
259+
{
260+
name: "image",
261+
type: "ociImage",
262+
version: "0.17.0-rc.1",
263+
access: {
264+
type: "ociArtifact",
265+
imageReference: "ghcr.io/ocm/cli:0.17.0-rc.1",
266+
},
267+
},
268+
],
269+
};
270+
271+
const result = promoteConstructorVersion(constructor, "0.17.0", "ghcr.io/ocm/cli:0.17.0");
272+
273+
assert.strictEqual(result.version, "0.17.0", "Top-level version should be updated");
274+
assert.strictEqual(result.resources[0].version, "0.17.0", "First CLI resource version should be updated");
275+
assert.strictEqual(result.resources[1].version, "0.17.0", "Second CLI resource version should be updated");
276+
assert.strictEqual(result.resources[2].version, "0.17.0", "Image resource version should be updated");
277+
assert.strictEqual(
278+
result.resources[2].access.imageReference,
279+
"ghcr.io/ocm/cli:0.17.0",
280+
"Image reference should be updated"
281+
);
282+
// Ensure non-image fields are untouched
283+
assert.strictEqual(result.resources[0].input.path, "resources/bin/ocm-linux-amd64", "CLI path should be unchanged");
284+
assert.strictEqual(result.name, "ocm.software/cli", "Name should be unchanged");
285+
}
286+
287+
// Mutates in place and returns the same object
288+
{
289+
const constructor = {
290+
version: "1.0.0-rc.1",
291+
resources: [
292+
{ name: "image", version: "1.0.0-rc.1", access: { type: "ociArtifact", imageReference: "old" } },
293+
],
294+
};
295+
const result = promoteConstructorVersion(constructor, "1.0.0", "new-ref");
296+
assert.strictEqual(result, constructor, "Should return the same object (mutate in place)");
297+
}
298+
299+
// Error: missing resources array
300+
assert.throws(
301+
() => promoteConstructorVersion({ name: "test", version: "1.0.0" }, "2.0.0", "ref"),
302+
/no 'resources' array/,
303+
"Should throw when resources array is missing"
304+
);
305+
306+
// Error: no image resource
307+
assert.throws(
308+
() => promoteConstructorVersion({
309+
version: "1.0.0-rc.1",
310+
resources: [
311+
{ name: "cli", version: "1.0.0-rc.1", input: { type: "file", path: "bin/ocm" } },
312+
],
313+
}, "1.0.0", "ref"),
314+
/No image resource with 'access' found/,
315+
"Should throw when no image resource exists"
316+
);
317+
318+
// Error: image resource exists but has no access field
319+
assert.throws(
320+
() => promoteConstructorVersion({
321+
version: "1.0.0-rc.1",
322+
resources: [
323+
{ name: "image", version: "1.0.0-rc.1" },
324+
],
325+
}, "1.0.0", "ref"),
326+
/No image resource with 'access' found/,
327+
"Should throw when image resource has no access field"
328+
);
329+
330+
// Edge: single resource that is the image
331+
{
332+
const constructor = {
333+
version: "0.1.0-rc.2",
334+
resources: [
335+
{ name: "image", version: "0.1.0-rc.2", access: { type: "ociArtifact", imageReference: "old:rc2" } },
336+
],
337+
};
338+
promoteConstructorVersion(constructor, "0.1.0", "new:final");
339+
assert.strictEqual(constructor.version, "0.1.0");
340+
assert.strictEqual(constructor.resources[0].version, "0.1.0");
341+
assert.strictEqual(constructor.resources[0].access.imageReference, "new:final");
342+
}
343+
344+
console.log("✅ All component-constructor tests passed.");

.github/workflows/ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -338,15 +338,15 @@ jobs:
338338
.github/workflows/ci.yml
339339
${{ matrix.project }}
340340
- name: Initialize CodeQL
341-
uses: github/codeql-action/init@ae9ef3a1d2e3413523c3741725c30064970cc0d4 # v3
341+
uses: github/codeql-action/init@820e3160e279568db735cee8ed8f8e77a6da7818 # v3
342342
with:
343343
languages: go
344344
queries: security-extended
345345
- name: Autobuild
346-
uses: github/codeql-action/autobuild@ae9ef3a1d2e3413523c3741725c30064970cc0d4 # v3
346+
uses: github/codeql-action/autobuild@820e3160e279568db735cee8ed8f8e77a6da7818 # v3
347347

348348
- name: Perform CodeQL Analysis
349-
uses: github/codeql-action/analyze@ae9ef3a1d2e3413523c3741725c30064970cc0d4 # v3
349+
uses: github/codeql-action/analyze@820e3160e279568db735cee8ed8f8e77a6da7818 # v3
350350
with:
351351
category: "/language:go"
352352

0 commit comments

Comments
 (0)