Skip to content

Commit 370fa5c

Browse files
committed
Add script to add ruby version to the matrix
1 parent 513ee66 commit 370fa5c

File tree

6 files changed

+307
-6
lines changed

6 files changed

+307
-6
lines changed

.devcontainer/devcontainer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
99
},
1010

11-
"updateContentCommand": "npm install -g @devcontainers/cli",
11+
"updateContentCommand": "npm install",
1212

1313
"customizations": {
1414
"vscode": {

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules/

CONTRIBUTING.md

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,37 @@ the **image version** (not the ruby version). Images will be published for all [
4242

4343
## Publishing new Ruby versions
4444

45-
When a new Ruby version is released, we can build and publish the existing image for the new ruby version, without
46-
needing to cut a new version of the image itself. To do this, we can run the Publish New Ruby Versions workflow
47-
manually. The workflow takes a list of a ruby versions and a list of image tags as inputs. They should be formatted
48-
as comma separated arrays. For example:
45+
When a new Ruby version is released, we need to add it to the build matrix and potentially update the default version.
46+
47+
### Step 1: Add the Ruby version to the build matrix
48+
49+
Use the automated script to update the configuration files:
50+
51+
```bash
52+
bin/add-ruby-version 3.4.5
53+
```
54+
55+
This script will:
56+
- Add the version to the build matrix in `.github/workflows/publish-new-image-version.yaml`
57+
- Update the default version in `features/src/ruby/devcontainer-feature.json` if the new version is newer
58+
- Maintain proper semantic version ordering
59+
- Prevent duplicate entries
60+
61+
After running the script, review the changes with `git diff`, commit them, and push.
62+
63+
### Step 2: Publish the Ruby version
64+
65+
You have two options:
66+
67+
#### Option A: Publish specific Ruby versions (common)
68+
69+
For immediate publishing of specific Ruby versions without cutting a new image version, run the **Publish New Ruby Versions** workflow manually. The workflow takes a list of ruby versions and image tags as inputs, formatted as comma separated arrays:
4970

5071
```
51-
ruby_versions: ["3.3.1","3.2.4","3.1.5"]
72+
ruby_versions: ["3.4.5"]
5273
image_versions: ["ruby-1.1.0"]
5374
```
75+
76+
#### Option B: Wait for next image release (automatic)
77+
78+
When a new image version is released (triggered by creating a `ruby-*.*.*` tag), the automatic workflow will build images for all Ruby versions in the matrix, including the newly added one.

bin/add-ruby-version

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
#!/usr/bin/env node
2+
3+
const fs = require('fs');
4+
const path = require('path');
5+
const yaml = require('js-yaml');
6+
const semver = require('semver');
7+
8+
// ANSI color codes for better output
9+
const colors = {
10+
reset: '\x1b[0m',
11+
green: '\x1b[32m',
12+
blue: '\x1b[34m',
13+
yellow: '\x1b[33m',
14+
red: '\x1b[31m',
15+
cyan: '\x1b[36m',
16+
magenta: '\x1b[35m'
17+
};
18+
19+
// Emoji helpers
20+
const emoji = {
21+
search: '🔍',
22+
edit: '📝',
23+
check: '✅',
24+
update: '🔄',
25+
info: 'ℹ️',
26+
party: '🎉',
27+
file: '📄',
28+
bulb: '💡',
29+
error: '❌'
30+
};
31+
32+
function log(message, color = 'reset') {
33+
console.log(colors[color] + message + colors.reset);
34+
}
35+
36+
function validateVersionFormat(version) {
37+
const versionRegex = /^[0-9]+\.[0-9]+\.[0-9]+$/;
38+
return versionRegex.test(version);
39+
}
40+
41+
function getCurrentDefaultVersion(jsonFile) {
42+
try {
43+
const content = fs.readFileSync(jsonFile, 'utf8');
44+
const data = JSON.parse(content);
45+
return data.options.version.default;
46+
} catch (error) {
47+
throw new Error(`Could not read current default version: ${error.message}`);
48+
}
49+
}
50+
51+
function versionExistsInYaml(version, yamlFile) {
52+
try {
53+
const content = fs.readFileSync(yamlFile, 'utf8');
54+
const data = yaml.load(content);
55+
const versions = data.jobs.build.strategy.matrix.RUBY_VERSION || [];
56+
return versions.includes(version);
57+
} catch (error) {
58+
throw new Error(`Could not check if version exists: ${error.message}`);
59+
}
60+
}
61+
62+
function addVersionToYaml(newVersion, yamlFile) {
63+
try {
64+
// Read and parse the YAML file
65+
const content = fs.readFileSync(yamlFile, 'utf8');
66+
const data = yaml.load(content);
67+
68+
// Get current versions and add new one
69+
const currentVersions = data.jobs.build.strategy.matrix.RUBY_VERSION || [];
70+
const allVersions = [...new Set([...currentVersions, newVersion])];
71+
const sortedVersions = allVersions.sort((a, b) => semver.rcompare(a, b));
72+
73+
// Update the data structure
74+
data.jobs.build.strategy.matrix.RUBY_VERSION = sortedVersions;
75+
76+
// Write back to file with proper YAML formatting
77+
const newContent = yaml.dump(data, {
78+
indent: 2,
79+
lineWidth: -1,
80+
noRefs: true,
81+
sortKeys: false,
82+
noArrayIndent: false,
83+
skipInvalid: false,
84+
flowLevel: -1,
85+
styles: {},
86+
schema: yaml.DEFAULT_SCHEMA,
87+
noCompatMode: false,
88+
condenseFlow: false,
89+
quotingType: '"',
90+
forceQuotes: false
91+
});
92+
93+
fs.writeFileSync(yamlFile, newContent);
94+
return true;
95+
} catch (error) {
96+
throw new Error(`Could not update YAML file: ${error.message}`);
97+
}
98+
}
99+
100+
function updateDefaultInJson(newVersion, jsonFile) {
101+
try {
102+
const content = fs.readFileSync(jsonFile, 'utf8');
103+
const data = JSON.parse(content);
104+
data.options.version.default = newVersion;
105+
fs.writeFileSync(jsonFile, JSON.stringify(data, null, 4) + '\n');
106+
return true;
107+
} catch (error) {
108+
throw new Error(`Could not update JSON file: ${error.message}`);
109+
}
110+
}
111+
112+
function main() {
113+
// Check command line arguments
114+
const args = process.argv.slice(2);
115+
116+
if (args.length !== 1) {
117+
log('Usage: bin/add-ruby-version <ruby-version>', 'red');
118+
log('Example: bin/add-ruby-version 3.4.5', 'yellow');
119+
process.exit(1);
120+
}
121+
122+
const newVersion = args[0];
123+
const yamlFile = '.github/workflows/publish-new-image-version.yaml';
124+
const jsonFile = 'features/src/ruby/devcontainer-feature.json';
125+
126+
// Validate version format
127+
if (!validateVersionFormat(newVersion)) {
128+
log(`${emoji.error} Error: Invalid version format. Expected format: x.y.z (e.g., 3.4.5)`, 'red');
129+
process.exit(1);
130+
}
131+
132+
// Check if files exist
133+
if (!fs.existsSync(yamlFile)) {
134+
log(`${emoji.error} Error: ${yamlFile} not found`, 'red');
135+
process.exit(1);
136+
}
137+
138+
if (!fs.existsSync(jsonFile)) {
139+
log(`${emoji.error} Error: ${jsonFile} not found`, 'red');
140+
process.exit(1);
141+
}
142+
143+
try {
144+
// Check if version already exists
145+
if (versionExistsInYaml(newVersion, yamlFile)) {
146+
log(`${emoji.error} Error: Version ${newVersion} already exists in ${yamlFile}`, 'red');
147+
process.exit(1);
148+
}
149+
150+
log(`${emoji.search} Checking current configuration...`, 'cyan');
151+
152+
// Get current default version
153+
const currentDefault = getCurrentDefaultVersion(jsonFile);
154+
log(`Current default version: ${currentDefault}`);
155+
log(`New version to add: ${newVersion}`);
156+
157+
// Add version to YAML file
158+
log('');
159+
log(`${emoji.edit} Adding ${newVersion} to ${yamlFile}...`, 'blue');
160+
addVersionToYaml(newVersion, yamlFile);
161+
log(`${emoji.check} Added to workflow matrix`, 'green');
162+
163+
// Check if new version should become the default
164+
const comparisonResult = semver.compare(newVersion, currentDefault);
165+
const filesModified = [yamlFile];
166+
167+
if (comparisonResult > 0) {
168+
log('');
169+
log(`${emoji.update} New version ${newVersion} is newer than current default ${currentDefault}`, 'yellow');
170+
log(`Updating default version in ${jsonFile}...`);
171+
updateDefaultInJson(newVersion, jsonFile);
172+
log(`${emoji.check} Updated default version to ${newVersion}`, 'green');
173+
filesModified.push(jsonFile);
174+
} else {
175+
log('');
176+
log(`${emoji.info} New version ${newVersion} is not newer than current default ${currentDefault}`, 'cyan');
177+
log('Default version remains unchanged');
178+
}
179+
180+
// Success message
181+
log('');
182+
log(`${emoji.party} Successfully added Ruby version ${newVersion}!`, 'green');
183+
log('');
184+
log(`${emoji.file} Files modified:`, 'blue');
185+
filesModified.forEach(file => {
186+
log(` • ${file}`);
187+
});
188+
log('');
189+
log(`${emoji.bulb} Next steps:`, 'magenta');
190+
log(` 1. Review the changes: git diff`);
191+
log(` 2. Commit the changes: git add . && git commit -m 'Add Ruby ${newVersion}'`);
192+
log(` 3. Push changes: git push`);
193+
194+
} catch (error) {
195+
log(`${emoji.error} Error: ${error.message}`, 'red');
196+
process.exit(1);
197+
}
198+
}
199+
200+
// Run the main function
201+
main();

package-lock.json

Lines changed: 63 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "ruby-version-script",
3+
"version": "1.0.0",
4+
"description": "Script dependencies for adding Ruby versions",
5+
"devDependencies": {
6+
"js-yaml": "^4.1.0",
7+
"semver": "^7.5.4",
8+
"@devcontainers/cli": "^0.56.0"
9+
},
10+
"private": true
11+
}

0 commit comments

Comments
 (0)