Skip to content

Commit f9a4a3e

Browse files
authored
Merge pull request #165 from morri-son/enhance-controller-release
Enhance controller release
2 parents c52ee88 + b7e40d1 commit f9a4a3e

File tree

8 files changed

+1132
-164
lines changed

8 files changed

+1132
-164
lines changed

.github/scripts/compute-version.js

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,20 @@
1010
*
1111
* Rules:
1212
* - Tag refs (matching tagPrefix pattern): Extract version from tag name
13-
* - Branch/other refs: Generate pseudo-version (0.0.0-<sanitized-ref>)
13+
* - Branch/other refs: Generate unique pseudo-version
14+
* (0.0.0-<sanitized-ref>.<yyyymmddHHMMSS>.<shortsha>)
1415
*
1516
* @param {string} ref - Git ref (branch name, tag name, or other ref)
1617
* @param {string} tagPrefix - Tag prefix pattern (e.g., "cli/v" or "bindings/go/helm/v")
18+
* @param {{ now?: Date, gitSha?: string }} [options] - Optional deterministic inputs for testing or overrides
1719
* @returns {string} Computed version string
1820
*
1921
* @example
2022
* computeVersion("cli/v1.2.3", "cli/v") // returns "1.2.3"
2123
* computeVersion("bindings/go/helm/v2.0.0-alpha1", "bindings/go/helm/v") // returns "2.0.0-alpha1"
22-
* computeVersion("main", "cli/v") // returns "0.0.0-main"
23-
* computeVersion("releases/v0.1", "cli/v") // returns "0.0.0-releases-v0.1"
24+
* computeVersion("main", "cli/v", { now: new Date("2026-03-03T12:34:56Z"), gitSha: "abcdef1234567890" }) // returns "0.0.0-main.20260303123456.abcdef123456"
2425
*/
25-
export function computeVersion(ref, tagPrefix) {
26+
export function computeVersion(ref, tagPrefix, options = {}) {
2627
if (!ref) {
2728
throw new Error("ref is required");
2829
}
@@ -43,12 +44,49 @@ export function computeVersion(ref, tagPrefix) {
4344
return ref.replace(tagPrefix, "");
4445
} else {
4546
// Convert ref to semver-safe pseudo version
46-
// Replace slashes and other problematic chars with hyphens
47-
const sanitized = ref.replace(/[\/+#?_^%$]/g, "-").toLocaleLowerCase();
48-
return `0.0.0-${sanitized}`;
47+
// using current timestamp and git SHA for uniqueness
48+
const timestamp = toUtcCompactTimestamp(options.now ?? new Date());
49+
const sanitizedRef = sanitizePrereleaseIdentifier(ref) || "ref";
50+
const shortSha = normalizeSha(options.gitSha ?? "unknown") || "unknown";
51+
52+
return `0.0.0-${sanitizedRef}.${timestamp}.${shortSha}`;
4953
}
5054
}
5155

56+
/**
57+
* Convert a value into a semver-safe prerelease identifier segment.
58+
*
59+
* @param {string} value
60+
* @returns {string}
61+
*/
62+
function sanitizePrereleaseIdentifier(value) {
63+
return value
64+
.toLocaleLowerCase()
65+
.replace(/[^0-9a-z.-]/g, "-")
66+
.replace(/-+/g, "-")
67+
.replace(/^-|-$/g, "");
68+
}
69+
70+
/**
71+
* Format a Date as UTC timestamp (yyyymmddHHMMSS).
72+
*
73+
* @param {Date} date
74+
* @returns {string}
75+
*/
76+
function toUtcCompactTimestamp(date) {
77+
return date.toISOString().replace(/[-:TZ.]/g, "").slice(0, 14);
78+
}
79+
80+
/**
81+
* Normalize a git SHA to lower-case hex and trim to 12 chars.
82+
*
83+
* @param {string} sha
84+
* @returns {string}
85+
*/
86+
function normalizeSha(sha) {
87+
return sha.toLocaleLowerCase().replace(/[^0-9a-f]/g, "").slice(0, 12);
88+
}
89+
5290
/**
5391
* Escape special regex characters in a string.
5492
*
@@ -71,6 +109,7 @@ function escapeRegex(str) {
71109
export default async function computeVersionAction({ core }) {
72110
const ref = process.env.REF;
73111
const tagPrefix = process.env.TAG_PREFIX;
112+
const gitSha = process.env.GITHUB_SHA;
74113

75114
if (!ref) {
76115
core.setFailed("REF environment variable is required");
@@ -83,7 +122,7 @@ export default async function computeVersionAction({ core }) {
83122
}
84123

85124
try {
86-
const version = computeVersion(ref, tagPrefix);
125+
const version = computeVersion(ref, tagPrefix, { gitSha });
87126

88127
core.exportVariable("VERSION", version);
89128
core.setOutput("version", version);

.github/scripts/compute-version.test.js

Lines changed: 67 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,29 @@ assert.strictEqual(
2525
);
2626

2727
assert.strictEqual(
28-
computeVersion("main", "cli/v"),
29-
"0.0.0-main",
30-
"CLI main branch should create pseudo version"
28+
computeVersion("main", "cli/v", {
29+
now: new Date("2026-03-03T12:34:56Z"),
30+
gitSha: "abcdef1234567890",
31+
}),
32+
"0.0.0-main.20260303123456.abcdef123456",
33+
"CLI main branch should create unique pseudo version"
3134
);
3235

3336
assert.strictEqual(
34-
computeVersion("releases/v0.1", "cli/v"),
35-
"0.0.0-releases-v0.1",
37+
computeVersion("releases/v0.1", "cli/v", {
38+
now: new Date("2026-03-03T12:34:56Z"),
39+
gitSha: "abcdef1234567890",
40+
}),
41+
"0.0.0-releases-v0.1.20260303123456.abcdef123456",
3642
"CLI release branch should create pseudo version"
3743
);
3844

3945
assert.strictEqual(
40-
computeVersion("feature/my-branch", "cli/v"),
41-
"0.0.0-feature-my-branch",
46+
computeVersion("feature/my-branch", "cli/v", {
47+
now: new Date("2026-03-03T12:34:56Z"),
48+
gitSha: "abcdef1234567890",
49+
}),
50+
"0.0.0-feature-my-branch.20260303123456.abcdef123456",
4251
"CLI feature branch should sanitize slashes"
4352
);
4453

@@ -60,14 +69,20 @@ assert.strictEqual(
6069
);
6170

6271
assert.strictEqual(
63-
computeVersion("main", "bindings/go/helm/v"),
64-
"0.0.0-main",
72+
computeVersion("main", "bindings/go/helm/v", {
73+
now: new Date("2026-03-03T12:34:56Z"),
74+
gitSha: "abcdef1234567890",
75+
}),
76+
"0.0.0-main.20260303123456.abcdef123456",
6577
"Helm plugin main branch should create pseudo version"
6678
);
6779

6880
assert.strictEqual(
69-
computeVersion("feat/new-feature", "bindings/go/helm/v"),
70-
"0.0.0-feat-new-feature",
81+
computeVersion("feat/new-feature", "bindings/go/helm/v", {
82+
now: new Date("2026-03-03T12:34:56Z"),
83+
gitSha: "abcdef1234567890",
84+
}),
85+
"0.0.0-feat-new-feature.20260303123456.abcdef123456",
7186
"Helm plugin feature branch should sanitize slashes"
7287
);
7388

@@ -95,27 +110,39 @@ assert.strictEqual(
95110
);
96111

97112
assert.strictEqual(
98-
computeVersion("pr/123/merge", "cli/v"),
99-
"0.0.0-pr-123-merge",
113+
computeVersion("pr/123/merge", "cli/v", {
114+
now: new Date("2026-03-03T12:34:56Z"),
115+
gitSha: "abcdef1234567890",
116+
}),
117+
"0.0.0-pr-123-merge.20260303123456.abcdef123456",
100118
"PR refs should be sanitized"
101119
);
102120

103121
assert.strictEqual(
104-
computeVersion("refs/heads/main", "cli/v"),
105-
"0.0.0-refs-heads-main",
122+
computeVersion("refs/heads/main", "cli/v", {
123+
now: new Date("2026-03-03T12:34:56Z"),
124+
gitSha: "abcdef1234567890",
125+
}),
126+
"0.0.0-refs-heads-main.20260303123456.abcdef123456",
106127
"Full ref paths should be sanitized"
107128
);
108129

109130
// Special characters in branch names
110131
assert.strictEqual(
111-
computeVersion("feature/issue#123", "cli/v"),
112-
"0.0.0-feature-issue-123",
132+
computeVersion("feature/issue#123", "cli/v", {
133+
now: new Date("2026-03-03T12:34:56Z"),
134+
gitSha: "abcdef1234567890",
135+
}),
136+
"0.0.0-feature-issue-123.20260303123456.abcdef123456",
113137
"Branch with # should be preserved"
114138
);
115139

116140
assert.strictEqual(
117-
computeVersion("hotfix/v1.2.3-fix", "cli/v"),
118-
"0.0.0-hotfix-v1.2.3-fix",
141+
computeVersion("hotfix/v1.2.3-fix", "cli/v", {
142+
now: new Date("2026-03-03T12:34:56Z"),
143+
gitSha: "abcdef1234567890",
144+
}),
145+
"0.0.0-hotfix-v1.2.3-fix.20260303123456.abcdef123456",
119146
"Branch that looks like version should not be treated as tag"
120147
);
121148

@@ -170,14 +197,20 @@ console.log("Testing regex escaping for security...");
170197

171198
// These should NOT match as tags even though they contain regex special chars
172199
assert.strictEqual(
173-
computeVersion("cli.*v1.2.3", "cli/v"),
174-
"0.0.0-cli.*v1.2.3",
200+
computeVersion("cli.*v1.2.3", "cli/v", {
201+
now: new Date("2026-03-03T12:34:56Z"),
202+
gitSha: "abcdef1234567890",
203+
}),
204+
"0.0.0-cli.-v1.2.3.20260303123456.abcdef123456",
175205
"Ref with regex chars should not match tag pattern"
176206
);
177207

178208
assert.strictEqual(
179-
computeVersion("v1.2.3", "cli/v"),
180-
"0.0.0-v1.2.3",
209+
computeVersion("v1.2.3", "cli/v", {
210+
now: new Date("2026-03-03T12:34:56Z"),
211+
gitSha: "abcdef1234567890",
212+
}),
213+
"0.0.0-v1.2.3.20260303123456.abcdef123456",
181214
"Ref without correct prefix should not match"
182215
);
183216

@@ -193,6 +226,17 @@ const result1a = computeVersion(ref1, prefix1);
193226
const result1b = computeVersion(ref1, prefix1);
194227
assert.strictEqual(result1a, result1b, "Same inputs should produce same output");
195228

229+
// Same non-tag input should produce same output when deterministic options are provided
230+
const branchResultA = computeVersion("main", "cli/v", {
231+
now: new Date("2026-03-03T12:34:56Z"),
232+
gitSha: "abcdef1234567890",
233+
});
234+
const branchResultB = computeVersion("main", "cli/v", {
235+
now: new Date("2026-03-03T12:34:56Z"),
236+
gitSha: "abcdef1234567890",
237+
});
238+
assert.strictEqual(branchResultA, branchResultB, "Deterministic inputs should produce same output");
239+
196240
// Different prefixes should not interfere
197241
assert.notStrictEqual(
198242
computeVersion("cli/v1.2.3", "cli/v"),

.github/workflows/cli.yml

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ on:
1717
paths:
1818
- 'cli/**'
1919
- '.github/workflows/cli.yml'
20+
- '.github/workflows/cli-release.yml'
21+
- '.github/workflows/publish-ocm-component-version.yml'
2022
workflow_call:
2123
inputs:
2224
artifact_name:
@@ -144,7 +146,8 @@ jobs:
144146
# OCI layout files
145147
path: |
146148
${{ env.LOCATION }}/tmp/bin/ocm-*
147-
${{ env.LOCATION }}/tmp/oci/*
149+
${{ env.LOCATION }}/tmp/oci/*
150+
${{ env.LOCATION }}/tmp/component-constructor.yaml
148151
149152
publish:
150153
name: Publish and Attest
@@ -207,3 +210,71 @@ jobs:
207210
subject-digest: ${{ steps.digest.outputs.digest }}
208211
subject-name: ${{ env.TARGET_REPO }}
209212
push-to-registry: true
213+
214+
# Reuse generated constructor from build artifacts, normalize file paths, and patch only image access
215+
- name: Generate OCM component constructor
216+
shell: bash
217+
run: |
218+
set -euo pipefail
219+
220+
TARGET_CONSTRUCTOR="ocm-publish/cli-component-constructor.yaml"
221+
222+
echo "Verifying downloaded CLI binaries..."
223+
shopt -s nullglob
224+
files=(bin/ocm-*)
225+
(( ${#files[@]} > 0 )) || { echo "❌ No CLI binaries found under ./bin"; ls -la; exit 1; }
226+
227+
CONSTRUCTOR_SOURCE="$(find . -type f -name component-constructor.yaml | head -n 1)"
228+
test -n "${CONSTRUCTOR_SOURCE}" || { echo "❌ component-constructor.yaml not found in downloaded artifacts"; ls -la; exit 1; }
229+
echo "✅ Required artifacts are present"
230+
231+
mkdir -p ocm-publish
232+
cp "${CONSTRUCTOR_SOURCE}" "${TARGET_CONSTRUCTOR}"
233+
234+
IMAGE_REF="${REGISTRY}/${GITHUB_REPOSITORY_OWNER}/cli:${TAG}"
235+
export IMAGE_REF
236+
237+
yq -e '.resources[] | select(.name == "image")' "${TARGET_CONSTRUCTOR}" >/dev/null
238+
239+
yq -i '
240+
(.resources[] | select(.name == "cli" and .input.type == "file") | .input.path) |= ("bin/" + (split("/") | .[-1])) |
241+
(.resources[] | select(.name == "image") | .type) = "ociImage" |
242+
(.resources[] | select(.name == "image") | .version) = env(TAG) |
243+
(.resources[] | select(.name == "image") | .access.type) = "ociArtifact" |
244+
(.resources[] | select(.name == "image") | .access.imageReference) = env(IMAGE_REF) |
245+
del(.resources[] | select(.name == "image") | .relation) |
246+
del(.resources[] | select(.name == "image") | .input)
247+
' "${TARGET_CONSTRUCTOR}"
248+
249+
yq -e '.resources[] | select(.name == "image") | .access.imageReference == env(IMAGE_REF)' "${TARGET_CONSTRUCTOR}" >/dev/null
250+
251+
- name: Upload component constructor as artifact
252+
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
253+
with:
254+
name: cli-constructor-artifact
255+
if-no-files-found: error
256+
path: ocm-publish/cli-component-constructor.yaml
257+
258+
publish-component:
259+
name: Publish Component Version
260+
needs: [build, publish]
261+
if: ${{ needs.build.outputs.should_push_oci_image == 'true' }}
262+
uses: ./.github/workflows/publish-ocm-component-version.yml
263+
secrets: inherit
264+
with:
265+
constructor_artifact_name: cli-constructor-artifact
266+
constructor_filename: cli-component-constructor.yaml
267+
build_artifact_name: ${{ needs.build.outputs.artifact_name }}
268+
ocm_repository: ghcr.io/${{ github.repository_owner }}
269+
270+
status-check:
271+
name: Status Check
272+
runs-on: ubuntu-latest
273+
needs: [publish, publish-component]
274+
if: always()
275+
steps:
276+
- name: Fail if OCM publish did not succeed
277+
if: ${{ needs.publish.result == 'success' && needs.publish-component.result != 'success' }}
278+
run: |
279+
echo "::error::OCM component publish did not pass (result: ${{ needs.publish-component.result }})"
280+
exit 1

0 commit comments

Comments
 (0)