Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
{
"ignorePaths": ["**/node_modules/**", "**/vscode-extension/**", "**/.git/**", ".vscode", "megalinter", "package-lock.json", "report"],
"ignorePaths": [
"**/node_modules/**",
"**/vscode-extension/**",
"**/.git/**",
"**/.pnpm-lock.json",
".vscode",
"megalinter",
"package-lock.json",
"report"
],
"language": "en",
"noConfigSearch": true,
"words": ["megalinter", "oxsecurity"],
Expand Down
85 changes: 85 additions & 0 deletions .github/scripts/update-chart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
const fs = require('fs');
const path = require('path');
const YAML = require('yaml');

function updateCharts(imageName, newTag, workspacePath) {
const chartsDir = path.join(workspacePath, 'helm-charts', 'charts');
let updatedAnyChart = false;

if (!fs.existsSync(chartsDir)) {
throw new Error(`Charts directory not found at ${chartsDir}`);
}

const chartDirs = fs
.readdirSync(chartsDir, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);

for (const chartName of chartDirs) {
const targetPath = path.join(chartsDir, chartName);
const chartYamlPath = path.join(targetPath, 'Chart.yaml');
const valuesYamlPath = path.join(targetPath, 'values.yaml');

if (!fs.existsSync(chartYamlPath)) continue;

let chartDoc = YAML.parseDocument(fs.readFileSync(chartYamlPath, 'utf8'));
let valuesDoc = fs.existsSync(valuesYamlPath) ? YAML.parseDocument(fs.readFileSync(valuesYamlPath, 'utf8')) : null;

// Track file updates independently to avoid unnecessary disk writes
let chartDocUpdated = false;
let valuesDocUpdated = false;

// Strict exact matching
const isExactMatch = chartName === imageName;
const deps = chartDoc.get('dependencies');
const hasDependency = deps && YAML.isSeq(deps) && deps.items.some((dep) => dep.get('name') === imageName);

if (hasDependency) {
if (valuesDoc && valuesDoc.hasIn([imageName, 'image', 'tag'])) {
valuesDoc.setIn([imageName, 'image', 'tag'], newTag);
valuesDocUpdated = true;
} else {
chartDoc.set('appVersion', newTag);
chartDocUpdated = true;
}
} else if (isExactMatch) {
// IMPORTANT: 'else if' prevents dual-updating when both match
if (valuesDoc && valuesDoc.hasIn(['image', 'tag'])) {
valuesDoc.setIn(['image', 'tag'], newTag);
valuesDocUpdated = true;
} else {
chartDoc.set('appVersion', newTag);
chartDocUpdated = true;
}
}

// Only write the files that actually changed
if (valuesDocUpdated) {
fs.writeFileSync(valuesYamlPath, String(valuesDoc));
updatedAnyChart = true;
}
if (chartDocUpdated) {
fs.writeFileSync(chartYamlPath, String(chartDoc));
updatedAnyChart = true;
}
}

return updatedAnyChart;
}

// Only execute immediately if run directly via Node (e.g., in GitHub Actions)
if (require.main === module) {
const imageName = process.env.IMAGE_NAME;
const newTag = process.env.NEW_TAG;
const workspacePath = process.env.GITHUB_WORKSPACE;

try {
const success = updateCharts(imageName, newTag, workspacePath);
if (!success) console.log(`No charts required updates for '${imageName}'.`);
} catch (err) {
console.error(err.message);
process.exit(1);
}
}

module.exports = updateCharts; // Export for Jest
243 changes: 243 additions & 0 deletions .github/scripts/update-chart.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
const mock = require('mock-fs');
const fs = require('fs');
const path = require('path');
const YAML = require('yaml');
const updateCharts = require('./update-chart');

describe('updateCharts script', () => {
const workspacePath = '/fake/workspace';

afterEach(() => {
mock.restore();
});

test('if there are no dependencies, should update image.tag', () => {
// Arrange
mock({
'/fake/workspace/helm-charts/charts/grafana': {
'Chart.yaml': `
name: grafana
appVersion: 1.0.0
`,
'values.yaml': `
image:
tag: 1.0.0
`,
},
});

// Act
const result = updateCharts('grafana', 'v2.0.0', workspacePath);

const values = YAML.parse(fs.readFileSync('/fake/workspace/helm-charts/charts/grafana/values.yaml', 'utf8'));
const chart = YAML.parse(fs.readFileSync('/fake/workspace/helm-charts/charts/grafana/Chart.yaml', 'utf8'));
// Assert
expect(result).toBe(true);
expect(values.image.tag).toBe('v2.0.0');
expect(chart.appVersion).toBe('1.0.0');
});

test('if there are no dependencies and no tag in values, should update appVersion', () => {
// Arrange
mock({
'/fake/workspace/helm-charts/charts/my-app': {
'Chart.yaml': `
name: my-app
appVersion: 1.0.0
`,
'values.yaml': `# empty`,
},
});

// Act
const result = updateCharts('my-app', 'v2.0.0', workspacePath);

const chart = YAML.parse(fs.readFileSync('/fake/workspace/helm-charts/charts/my-app/Chart.yaml', 'utf8'));
const values = YAML.parse(fs.readFileSync('/fake/workspace/helm-charts/charts/my-app/values.yaml', 'utf8'));
// Assert
expect(result).toBe(true);
expect(chart.appVersion).toBe('v2.0.0');
expect(values).toEqual(null); // Ensure values.yaml remains unchanged
});

test('if image name matches a dependency, should update dependency tag', () => {
// Arrange
mock({
'/fake/workspace/helm-charts/charts/bla': {
'Chart.yaml': `
name: bla
version: 1.0.0
appVersion: 1.0.0
dependencies:
- name: sftpgo-node-exporter
version: 1.0.0
- name: sftpgo
version: 1.0.0
`,
'values.yaml': `
sftpgo-node-exporter:
image:
tag: "old-tag"
sftpgo:
image:
tag: "old-tag"
`,
},
});

// Act
const result = updateCharts('sftpgo-node-exporter', 'v2.0.0', workspacePath);

const values = YAML.parse(fs.readFileSync('/fake/workspace/helm-charts/charts/bla/values.yaml', 'utf8'));
const chart = YAML.parse(fs.readFileSync('/fake/workspace/helm-charts/charts/bla/Chart.yaml', 'utf8'));
// Assert
expect(result).toBe(true);
expect(values['sftpgo-node-exporter'].image.tag).toBe('v2.0.0');
expect(values.sftpgo.image.tag).toBe('old-tag');
expect(chart.appVersion).toBe('1.0.0');
});

test('if image name matches both dependency and chart name, should update only dependency tag', () => {
// Arrange
mock({
'/fake/workspace/helm-charts/charts/sftpgo': {
'Chart.yaml': `
name: sftpgo
version: 1.0.0
appVersion: "1.0.0"
dependencies:
- name: sftpgo-node-exporter
version: 1.0.0
- name: sftpgo
version: 1.0.0
`,
'values.yaml': `
sftpgo-node-exporter:
image:
tag: "old-tag"
sftpgo:
image:
tag: "old-tag"
`,
},
});

// Act
const result = updateCharts('sftpgo', 'v2.0.0', workspacePath);

const values = YAML.parse(fs.readFileSync('/fake/workspace/helm-charts/charts/sftpgo/values.yaml', 'utf8'));
const chart = YAML.parse(fs.readFileSync('/fake/workspace/helm-charts/charts/sftpgo/Chart.yaml', 'utf8'));
// Assert
expect(result).toBe(true);
expect(values['sftpgo'].image.tag).toBe('v2.0.0');
expect(values['sftpgo-node-exporter'].image.tag).toBe('old-tag');
expect(chart.appVersion).toBe('1.0.0');
});

test('if image name matches a dependency and no tag in values, should update root appVersion', () => {
// Arrange
mock({
'/fake/workspace/helm-charts/charts/sftpgo': {
'Chart.yaml': `
name: sftpgo
version: 1.0.0
appVersion: "1.0.0"
dependencies:
- name: sftpgo-node-exporter
version: 1.0.0
`,
'values.yaml': `
sftpgo:
image:
tag: "old-tag"
`,
},
});

// Act
const result = updateCharts('sftpgo-node-exporter', 'v2.0.0', workspacePath);

const chart = YAML.parse(fs.readFileSync('/fake/workspace/helm-charts/charts/sftpgo/Chart.yaml', 'utf8'));
const values = YAML.parse(fs.readFileSync('/fake/workspace/helm-charts/charts/sftpgo/values.yaml', 'utf8'));
// Assert
expect(result).toBe(true);
expect(chart.appVersion).toBe('v2.0.0');
expect(values.sftpgo.image.tag).toBe('old-tag');
});

test('if image name matches more than one dependency, should update all matching dependency tags', () => {
// Arrange
mock({
'/fake/workspace/helm-charts/charts/sftpgo': {
'Chart.yaml': `
name: sftpgo
version: 1.0.0
appVersion: "1.0.0"
dependencies:
- name: sftpgo-node-exporter
version: 1.0.0
`,
'values.yaml': `
sftpgo-node-exporter:
image:
tag: "old-tag"
`,
},
'/fake/workspace/helm-charts/charts/grafana': {
'Chart.yaml': `
name: grafana
version: 1.0.0
appVersion: "1.0.0"
dependencies:
- name: sftpgo-node-exporter
version: 1.0.0
`,
'values.yaml': `
sftpgo-node-exporter:
image:
tag: "old-tag"
`,
},
});

// Act
const result = updateCharts('sftpgo-node-exporter', 'v2.0.0', workspacePath);

const sftpgoChart = YAML.parse(fs.readFileSync('/fake/workspace/helm-charts/charts/sftpgo/Chart.yaml', 'utf8'));
const sftpgoValues = YAML.parse(fs.readFileSync('/fake/workspace/helm-charts/charts/sftpgo/values.yaml', 'utf8'));
const grafanaChart = YAML.parse(fs.readFileSync('/fake/workspace/helm-charts/charts/grafana/Chart.yaml', 'utf8'));
const grafanaValues = YAML.parse(fs.readFileSync('/fake/workspace/helm-charts/charts/grafana/values.yaml', 'utf8'));
// Assert
expect(result).toBe(true);
expect(sftpgoChart.appVersion).toBe('1.0.0');
expect(sftpgoValues['sftpgo-node-exporter'].image.tag).toBe('v2.0.0');
expect(grafanaChart.appVersion).toBe('1.0.0');
expect(grafanaValues['sftpgo-node-exporter'].image.tag).toBe('v2.0.0');
});

test('if image name does not match any chart or dependency, should not update anything', () => {
// Arrange
mock({
'/fake/workspace/helm-charts/charts/grafana': {
'Chart.yaml': `
name: grafana
appVersion: 1.0.0
`,
'values.yaml': `
image:
tag: 1.0.0
`,
},
});

// Act
const result = updateCharts('non-existent-image', 'v2.0.0', workspacePath);

const chart = YAML.parse(fs.readFileSync('/fake/workspace/helm-charts/charts/grafana/Chart.yaml', 'utf8'));
const values = YAML.parse(fs.readFileSync('/fake/workspace/helm-charts/charts/grafana/values.yaml', 'utf8'));
// Assert
expect(result).toBe(false);
expect(chart.appVersion).toBe('1.0.0');
expect(values.image.tag).toBe('1.0.0');
});
});
Loading
Loading