|
1 | 1 | // @ts-check |
2 | 2 | import fs from "fs"; |
| 3 | +import path from "path"; |
3 | 4 | import yaml from "js-yaml"; |
4 | 5 |
|
5 | 6 | /** |
@@ -76,6 +77,9 @@ export function patchCliConstructor(constructor, imageRef, imageTag) { |
76 | 77 | resource.name === "cli" && |
77 | 78 | resource.input?.type === "file" |
78 | 79 | ) { |
| 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 | + } |
79 | 83 | const parts = resource.input.path.split("/"); |
80 | 84 | const filename = parts[parts.length - 1]; |
81 | 85 | resource.input.path = `resources/bin/${filename}`; |
@@ -103,6 +107,179 @@ export function patchCliConstructor(constructor, imageRef, imageTag) { |
103 | 107 | return constructor; |
104 | 108 | } |
105 | 109 |
|
| 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 | + |
106 | 283 | /** |
107 | 284 | * GitHub Actions entrypoint: summarize a published component version in the step summary. |
108 | 285 | * |
|
0 commit comments