Skip to content

Commit 565dd5e

Browse files
committed
feat(helm): add new action 'update-chart-values'
Signed-off-by: Emilien Escalle <emilien.escalle@escemi.com>
1 parent cb3b9dd commit 565dd5e

6 files changed

Lines changed: 378 additions & 95 deletions

File tree

.github/workflows/__shared-ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ jobs:
5151
permissions:
5252
contents: read
5353

54+
test-action-helm-update-chart-values:
55+
needs: linter
56+
uses: ./.github/workflows/__test-action-helm-update-chart-values.yml
57+
permissions:
58+
contents: read
59+
5460
test-action-helm-release-chart:
5561
needs: linter
5662
uses: ./.github/workflows/__test-action-helm-release-chart.yml
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
---
2+
name: Test for "helm/update-chart-values" action
3+
run-name: Test for "actions/helm/update-chart-values" action
4+
5+
on: # yamllint disable-line rule:truthy
6+
workflow_call:
7+
8+
permissions: {}
9+
10+
jobs:
11+
test-simple-chart:
12+
name: Test for "helm/update-chart-values" action with simple chart
13+
runs-on: ubuntu-latest
14+
permissions:
15+
contents: read
16+
steps:
17+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
18+
with:
19+
persist-credentials: false
20+
21+
- name: Act
22+
uses: ./actions/helm/update-chart-values
23+
with:
24+
path: tests/charts/application
25+
values: |
26+
[
27+
{ "path": ".image.registry", "value": "ghcr.io" },
28+
{
29+
"path": ".image.repository",
30+
"value": "hoverkraft-tech/ci-github-container/application"
31+
},
32+
{ "path": ".image.tag", "value": "0.2.0" }
33+
]
34+
35+
- name: Assert - Check updated chart files
36+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
37+
with:
38+
script: |
39+
const assert = require('node:assert');
40+
const fs = require('node:fs');
41+
42+
const chartContent = fs.readFileSync('tests/charts/application/Chart.yaml', 'utf8');
43+
assert.match(chartContent, /name:\s+"test-application"/, 'root chart name should stay unchanged');
44+
assert.match(chartContent, /version:\s+0\.0\.0/, 'root chart version should stay unchanged');
45+
assert.match(chartContent, /appVersion:\s+"0\.0\.0"/, 'root chart appVersion should stay unchanged');
46+
47+
const valuesContent = fs.readFileSync('tests/charts/application/values.yaml', 'utf8');
48+
assert.match(valuesContent, /registry:\s+ghcr\.io/, 'image registry should be updated');
49+
assert.match(valuesContent, /repository:\s+hoverkraft-tech\/ci-github-container\/application/, 'image repository should be updated');
50+
assert.match(valuesContent, /tag:\s+0\.2\.0/, 'image tag should be updated');
51+
52+
test-umbrella-chart:
53+
name: Test for "helm/update-chart-values" action with umbrella chart
54+
runs-on: ubuntu-latest
55+
permissions:
56+
contents: read
57+
steps:
58+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
59+
with:
60+
persist-credentials: false
61+
62+
- name: Act
63+
uses: ./actions/helm/update-chart-values
64+
with:
65+
path: tests/charts/umbrella-application
66+
values: |
67+
[
68+
{ "file": "charts/app/values.yaml", "path": ".image.registry", "value": "ghcr.io" },
69+
{
70+
"file": "charts/app/values.yaml",
71+
"path": ".image.repository",
72+
"value": "hoverkraft-tech/ci-github-container/umbrella-application"
73+
},
74+
{ "file": "charts/app/values.yaml", "path": ".image.tag", "value": "0.2.0" }
75+
]
76+
77+
- name: Assert - Check updated umbrella chart files
78+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
79+
with:
80+
script: |
81+
const assert = require('node:assert');
82+
const fs = require('node:fs');
83+
84+
const rootChartContent = fs.readFileSync('tests/charts/umbrella-application/Chart.yaml', 'utf8');
85+
assert.match(rootChartContent, /name:\s+test-umbrella-application/, 'root chart name should stay unchanged');
86+
assert.match(rootChartContent, /version:\s+0\.1\.0/, 'root chart version should stay unchanged');
87+
assert.match(rootChartContent, /appVersion:\s+"0\.1\.0"/, 'root chart appVersion should stay unchanged');
88+
assert.match(rootChartContent, /version:\s+0\.0\.0\s+condition:\s+app\.enabled/s, 'local dependency version in Chart.yaml should stay unchanged');
89+
90+
const childChartContent = fs.readFileSync('tests/charts/umbrella-application/charts/app/Chart.yaml', 'utf8');
91+
assert.match(childChartContent, /version:\s+0\.0\.0/, 'child chart version should stay unchanged');
92+
assert.match(childChartContent, /appVersion:\s+"0\.0\.0"/, 'child chart appVersion should stay unchanged');
93+
94+
const childValuesContent = fs.readFileSync('tests/charts/umbrella-application/charts/app/values.yaml', 'utf8');
95+
assert.match(childValuesContent, /registry:\s+ghcr\.io/, 'child image registry should be updated');
96+
assert.match(childValuesContent, /repository:\s+hoverkraft-tech\/ci-github-container\/umbrella-application/, 'child image repository should be updated');
97+
assert.match(childValuesContent, /tag:\s+0\.2\.0/, 'child image tag should be updated');
98+
99+
const chartLockContent = fs.readFileSync('tests/charts/umbrella-application/Chart.lock', 'utf8');
100+
assert.match(chartLockContent, /version:\s+0\.0\.0/, 'local dependency version in Chart.lock should stay unchanged');

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ _Actions dedicated to packaging, validating, and publishing Helm charts for Kube
4848

4949
#### - [Parse chart URI](actions/helm/parse-chart-uri/README.md)
5050

51+
#### - [Update chart values](actions/helm/update-chart-values/README.md)
52+
5153
#### - [Release chart](actions/helm/release-chart/README.md)
5254

5355
#### - [Test chart](actions/helm/test-chart/README.md)

actions/helm/release-chart/action.yml

Lines changed: 48 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -78,18 +78,18 @@ runs:
7878
steps:
7979
- uses: hoverkraft-tech/ci-github-common/actions/checkout@4c9d51717dc04d823dac2dc9ac2857e7b3069454 # 0.35.0
8080

81-
- id: chart-values-updates
81+
- id: chart-tag-updates
8282
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
8383
env:
8484
INPUT_PATH: ${{ inputs.path }}
8585
INPUT_TAG: ${{ inputs.tag }}
8686
INPUT_UPDATE_TAG_PATHS: ${{ inputs.update-tag-paths }}
87+
INPUT_VALUES: ${{ inputs.values }}
8788
REPOSITORY_NAME: ${{ github.event.repository.name }}
8889
with:
8990
script: |
9091
const path = require('node:path');
9192
92-
const yqUpdates = {};
9393
const basePath = process.env.INPUT_PATH;
9494
if (!basePath) {
9595
throw new Error(`"path" input is missing`);
@@ -100,153 +100,111 @@ runs:
100100
throw new Error(`"tag" input is missing`);
101101
}
102102
103-
const updateTagPaths = process.env.INPUT_UPDATE_TAG_PATHS.trim().split(',').map(p => p.trim()).filter(p => p);
104-
105-
// Chart.yml files
106-
const globber = await glob.create(`${basePath}/**/Chart.yaml`, {followSymbolicLinks: false})
107-
for await (const chartFile of globber.globGenerator()) {
108-
const filePath = path.relative(process.env.GITHUB_WORKSPACE, chartFile);
109-
if (!yqUpdates[filePath]) {
110-
yqUpdates[filePath] = [];
111-
}
112-
113-
const isRootChart = filePath === path.join(basePath, "Chart.yaml");
114-
if (isRootChart) {
115-
// Update name for root chart
116-
yqUpdates[filePath].push(`.name = "${process.env.REPOSITORY_NAME}"`);
117-
118-
// Update dependencies version where repository starts with file://
119-
if (updateTagPaths.includes('.version')) {
120-
yqUpdates[filePath].push(
121-
`(. as $doc | (select(has("dependencies")) | (.dependencies[] | select(.repository == "file://*")).version = "${tag}") // $doc)`
122-
);
123-
}
124-
}
125-
126-
// Update tag fields
127-
for (const path of updateTagPaths) {
128-
yqUpdates[filePath].push(`${path} = "${tag}"`);
129-
}
130-
}
131-
132-
// values.yml files
133-
const chartValuesInput = `${{ inputs.values }}`;
134-
if (chartValuesInput) {
135-
136-
// Check if is valid Json
137-
let chartValues = null;
103+
const inputValues = process.env.INPUT_VALUES;
104+
let chartValues = [];
105+
if (inputValues) {
138106
try {
139-
chartValues = JSON.parse(chartValuesInput);
107+
chartValues = JSON.parse(inputValues);
140108
} catch (error) {
141109
throw new Error(`"values" input is not a valid JSON: ${error}`);
142110
}
143111
144-
// Check if is an array
145112
if (!Array.isArray(chartValues)) {
146113
throw new Error(`"values" input is not an array`);
147114
}
115+
}
148116
149-
if (chartValues.length) {
150-
const defaultValuesPath = "values.yaml";
151-
152-
// Check each item
153-
for (const key in chartValues) {
154-
const chartValue = chartValues[key];
155-
if (typeof chartValue !== 'object') {
156-
throw new Error(`"values[${key}]" input is not an object`);
157-
}
158-
159-
// Check mandatory properties
160-
for (const property of ['path', 'value']) {
161-
if (!chartValue.hasOwnProperty(property)) {
162-
throw new Error(`"values[${key}].${property}" input is missing`);
163-
}
164-
}
117+
const updateTagPaths = process.env.INPUT_UPDATE_TAG_PATHS.trim().split(',').map(p => p.trim()).filter(p => p);
118+
const chartRootPath = path.resolve(process.env.GITHUB_WORKSPACE ?? '.', basePath);
119+
const generatedValues = [];
165120
166-
const valueFilePath = chartValue['file'] ? chartValue['file'] : defaultValuesPath;
167-
const filePath = `${basePath}/${valueFilePath}`;
121+
const globber = await glob.create(`${basePath}/**/Chart.yaml`, { followSymbolicLinks: false });
122+
for await (const chartFile of globber.globGenerator()) {
123+
const filePath = path.relative(chartRootPath, chartFile);
168124
169-
if (!yqUpdates[filePath]) {
170-
yqUpdates[filePath] = [];
171-
}
125+
const isRootChart = filePath === 'Chart.yaml';
126+
if (isRootChart) {
127+
generatedValues.push({
128+
file: filePath,
129+
path: '.name',
130+
value: process.env.REPOSITORY_NAME,
131+
});
172132
173-
yqUpdates[filePath].push(`${chartValue.path} = "${chartValue.value}"`);
133+
if (updateTagPaths.includes('.version')) {
134+
generatedValues.push({
135+
file: filePath,
136+
path: '(.dependencies[]? | select(.repository | test("^file://")).version)',
137+
value: tag,
138+
});
174139
}
175140
}
176-
}
177141
178-
// Build yq commands
179-
const yqCommands = Object.entries(yqUpdates).map(([filePath, updates]) => {
180-
return `yq -i '${updates.join(' | ')}' ${filePath}`;
181-
});
142+
for (const path of updateTagPaths) {
143+
generatedValues.push({
144+
file: filePath,
145+
path,
146+
value: tag,
147+
});
148+
}
149+
}
182150
183-
core.setOutput('yq-command', yqCommands.join('\n'));
151+
core.setOutput('values', JSON.stringify([...generatedValues, ...chartValues]));
184152
185-
- uses: mikefarah/yq@751d8ad57b84f1794661bc70c0afb92a22ad7b3c # v4.53.2
153+
- uses: ./actions/helm/update-chart-values
186154
with:
187-
cmd: |
188-
${{ steps.chart-values-updates.outputs.yq-command }}
155+
path: ${{ inputs.path }}
156+
values: ${{ steps.chart-tag-updates.outputs.values }}
189157

190158
- name: Setup Node.js
191159
uses: hoverkraft-tech/ci-github-nodejs/actions/setup-node@a10d5e32daef8e060c49fe617833fb0d53476f22 # 0.24.0
192160
with:
193161
working-directory: ${{ github.action_path }}
194162

195-
- name: Rewrite the Chart.lock to match with updated ombrella dependencies if any
163+
- name: Rewrite the Chart.lock to match with updated umbrella dependencies if any
196164
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
197165
env:
166+
INPUT_PATH: ${{ inputs.path }}
167+
INPUT_TAG: ${{ inputs.tag }}
198168
NODE_PATH: ${{ github.action_path }}/node_modules
199169
with:
200170
script: |
201171
const fs = require('node:fs');
202-
const path = require('node:path');
203172
const crypto = require('node:crypto');
204-
const yaml = require("yaml");
173+
const yaml = require('yaml');
205174
206-
const rootChartFile = `${{ inputs.path }}/Chart.yaml`;
175+
const rootChartFile = process.env.INPUT_PATH + '/Chart.yaml';
207176
const rootChartFileContent = yaml.parse(fs.readFileSync(rootChartFile, 'utf8'));
208177
209-
// Check if the root chart has dependencies
210178
if (!rootChartFileContent.dependencies || rootChartFileContent.dependencies.length === 0) {
211179
return;
212180
}
213181
214-
const chartLockFile = `${{ inputs.path }}/Chart.lock`;
182+
const chartLockFile = process.env.INPUT_PATH + '/Chart.lock';
215183
if (!fs.existsSync(chartLockFile)) {
216184
return core.setFailed(`Chart.lock file not found: ${chartLockFile}`);
217185
}
218186
219187
const chartLockFileContent = yaml.parse(fs.readFileSync(chartLockFile, 'utf8'));
220188
221-
// Update ombrella dependencies versions
222189
let hasLocalDependencies = false;
223190
const dependencies = chartLockFileContent.dependencies;
224191
225-
// Check if dependencies are empt
226192
for (const dependency of dependencies) {
227-
const isLocalDependency = dependency.repository.startsWith("file://") && dependency.version === "0.0.0";
193+
const isLocalDependency = dependency.repository.startsWith('file://') && dependency.version === '0.0.0';
228194
229-
// Check if the dependency is a local file
230195
if (isLocalDependency) {
231-
// Update the version to the tag
232-
dependency.version = `${{ inputs.tag }}`;
196+
dependency.version = process.env.INPUT_TAG;
233197
hasLocalDependencies = true;
234198
}
235199
}
236200
237-
// If no local dependencies, exit
238201
if (!hasLocalDependencies) {
239202
return;
240203
}
241204
242-
// Update generated
243205
chartLockFileContent.generated = new Date().toISOString();
244206
245-
// Update global digest.
246-
247-
// See Helm hashReq function: https://github.com/helm/helm/blob/99c065789ef8c45bade24d4bc2d33432595de956/internal/resolver/resolver.go#L214
248207
function hashReq(req, lock) {
249-
// Sort the dependencies
250208
req = req.map(sortDependencyFields);
251209
lock = lock.map(sortDependencyFields);
252210
@@ -261,8 +219,6 @@ runs:
261219
return hash.digest('hex');
262220
}
263221
264-
// Should respect the Helm struct order
265-
// See https://github.com/helm/helm/blob/99c065789ef8c45bade24d4bc2d33432595de956/pkg/chart/v2/dependency.go#L24
266222
function sortDependencyFields(dependency) {
267223
const fieldOrder = [
268224
'name',
@@ -274,10 +230,9 @@ runs:
274230
'import-values',
275231
'alias',
276232
];
277-
// Sort the dependency fields
278233
const sortedDependency = {};
279234
for (const field of fieldOrder) {
280-
if (dependency.hasOwnProperty(field)) {
235+
if (Object.prototype.hasOwnProperty.call(dependency, field)) {
281236
sortedDependency[field] = dependency[field];
282237
}
283238
}
@@ -288,13 +243,11 @@ runs:
288243
const req = rootChartFileContent.dependencies;
289244
const lock = dependencies;
290245
291-
const hash = hashReq(req, lock);
292-
chartLockFileContent.digest = hash;
246+
chartLockFileContent.digest = hashReq(req, lock);
293247
294248
const updatedChartLockFileContent = yaml.stringify(chartLockFileContent);
295249
core.debug(`Updated Chart.lock file content:\n${updatedChartLockFileContent}`);
296250
297-
// Update Chart.lock file
298251
fs.writeFileSync(chartLockFile, updatedChartLockFileContent, 'utf8');
299252
300253
- uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0

0 commit comments

Comments
 (0)