Skip to content

Commit 4167681

Browse files
committed
Add workflow to branch for next OSS release. Fixes #16
1 parent e7d0167 commit 4167681

15 files changed

Lines changed: 33672 additions & 0 deletions

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# add-branches-projects-json
2+
3+
Registers newly created release line branches in
4+
[`config/projects.json`](../../../config/projects.json), so scheduled builds start
5+
covering them.
6+
7+
The mirror image of
8+
[`retire-branch-projects-json`](../retire-branch-projects-json/README.md), and it
9+
resolves the project entry and section the same way. It takes the whole set of
10+
branches for a release train **at once**: a train rollover touches ~30 repositories,
11+
and doing this inside a matrix would have those jobs racing to push to this
12+
repository's `main`.
13+
14+
## Behaviour
15+
16+
For each addition, the project key comes from the repository name (org prefix and
17+
`-commercial` suffix stripped) and the section from the suffix — `commercial` for
18+
`-commercial` repositories, `oss` for everything else. Then:
19+
20+
1. The branch is added to the front of `<section>.branches.scheduled` if it is not
21+
already there.
22+
2. `<section>.jdkVersions[branch]` is set to a **copy** of the source branch's list,
23+
falling back to the section's `default`, then to the matching section of the
24+
global `defaults` entry, then to `["17", "21"]`.
25+
3. `branches.default` is left alone. A new release line branch is a maintenance
26+
line; the branch it was cut from stays the default.
27+
4. A project with no entry in `projects.json` gets one, deep-copied from
28+
`defaults.<section>`. It is copied rather than shared on purpose:
29+
`update-projects-json` hands out the shared `defaults` object and then mutates
30+
it, silently rewriting the fallback every other project relies on.
31+
32+
Every step is a no-op when it has already been applied, so a re-run of a partially
33+
successful rollout makes no commit. All the additions land as a single commit on
34+
`main`.
35+
36+
## Inputs
37+
38+
| Name | Required | Default | Description |
39+
|---|---|---|---|
40+
| `additions` | yes | | JSON array of `{"repo": …, "branch": …, "sourceBranch": …}`. `sourceBranch` defaults to `main` |
41+
| `commit-message` | no | `Update projects.json: register N new branch(es)` | Commit message |
42+
| `dry-run` | no | `false` | Write the file and capture the patch, commit and push nothing |
43+
| `token` | yes | | Token with `contents: write` on `spring-cloud-github-actions` |
44+
45+
## Outputs
46+
47+
| Name | Description |
48+
|---|---|
49+
| `changed` | `true` when `projects.json` was modified |
50+
| `patch` | The diff of the staged change, empty when nothing changed |
51+
52+
## Example
53+
54+
```yaml
55+
- uses: ./.github/actions/add-branches-projects-json
56+
with:
57+
additions: >-
58+
[{"repo":"spring-cloud/spring-cloud-config","branch":"5.0.x","sourceBranch":"main"}]
59+
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
60+
```
61+
62+
Used by [`setup-next-release-train.yml`](../../workflows/README-setup-next-release-train.md).
63+
64+
## Development
65+
66+
```bash
67+
npm ci
68+
npm test
69+
npm run build # rebuilds dist/index.js — verify-dist.yml fails if it is not committed
70+
```
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
'use strict';
2+
3+
jest.mock('@actions/core');
4+
jest.mock('@actions/exec');
5+
6+
const core = require('@actions/core');
7+
const { dumpsPretty, resolveJdkVersions, addBranch, addBranches } = require('../src/index');
8+
9+
beforeEach(() => {
10+
jest.clearAllMocks();
11+
core.info.mockImplementation(() => {});
12+
core.warning.mockImplementation(() => {});
13+
core.error.mockImplementation(() => {});
14+
core.setFailed.mockImplementation(() => {});
15+
core.setSecret.mockImplementation(() => {});
16+
});
17+
18+
function makeData() {
19+
return {
20+
defaults: {
21+
oss: {
22+
branches: { default: ['main'], scheduled: ['main'] },
23+
jdkVersions: { main: ['17', '21', '25'], default: ['17', '21', '25'] },
24+
},
25+
commercial: {
26+
branches: { default: ['4.3.x'], scheduled: ['4.3.x'] },
27+
jdkVersions: { '4.3.x': ['17', '21', '25'], default: ['17', '21'] },
28+
},
29+
},
30+
'spring-cloud-config': {
31+
oss: {
32+
branches: { default: ['main'], scheduled: ['main'] },
33+
jdkVersions: { main: ['17', '21', '25'] },
34+
},
35+
commercial: {
36+
branches: { default: ['3.3.x'], scheduled: ['3.3.x'] },
37+
jdkVersions: { '3.3.x': ['17', '21'] },
38+
},
39+
},
40+
};
41+
}
42+
43+
// ── dumpsPretty ─────────────────────────────────────────────────────────────
44+
45+
describe('dumpsPretty', () => {
46+
test('keeps string arrays on a single line', () => {
47+
expect(dumpsPretty({ scheduled: ['5.0.x', 'main'] })).toContain('["5.0.x", "main"]');
48+
});
49+
});
50+
51+
// ── addBranch ───────────────────────────────────────────────────────────────
52+
53+
describe('addBranch', () => {
54+
test('adds the branch to oss scheduled and copies the source branch JDKs', () => {
55+
const data = makeData();
56+
const changes = addBranch(data, 'spring-cloud/spring-cloud-config', '5.0.x', 'main');
57+
58+
expect(changes).toHaveLength(2);
59+
expect(data['spring-cloud-config'].oss.branches.scheduled).toEqual(['5.0.x', 'main']);
60+
expect(data['spring-cloud-config'].oss.jdkVersions['5.0.x']).toEqual(['17', '21', '25']);
61+
});
62+
63+
test('leaves branches.default alone — main stays the default line', () => {
64+
const data = makeData();
65+
addBranch(data, 'spring-cloud/spring-cloud-config', '5.0.x', 'main');
66+
expect(data['spring-cloud-config'].oss.branches.default).toEqual(['main']);
67+
});
68+
69+
test('copies the JDK list rather than aliasing the source branch entry', () => {
70+
const data = makeData();
71+
addBranch(data, 'spring-cloud/spring-cloud-config', '5.0.x', 'main');
72+
data['spring-cloud-config'].oss.jdkVersions['5.0.x'].push('26');
73+
expect(data['spring-cloud-config'].oss.jdkVersions.main).toEqual(['17', '21', '25']);
74+
});
75+
76+
test('modifies the commercial section for a -commercial repo', () => {
77+
const data = makeData();
78+
addBranch(data, 'spring-cloud/spring-cloud-config-commercial', '5.0.x', '3.3.x');
79+
expect(data['spring-cloud-config'].commercial.branches.scheduled).toEqual(['5.0.x', '3.3.x']);
80+
expect(data['spring-cloud-config'].commercial.jdkVersions['5.0.x']).toEqual(['17', '21']);
81+
expect(data['spring-cloud-config'].oss.branches.scheduled).toEqual(['main']);
82+
});
83+
84+
test('is idempotent — a second call reports no changes', () => {
85+
const data = makeData();
86+
addBranch(data, 'spring-cloud/spring-cloud-config', '5.0.x', 'main');
87+
const changes = addBranch(data, 'spring-cloud/spring-cloud-config', '5.0.x', 'main');
88+
expect(changes).toEqual([]);
89+
expect(data['spring-cloud-config'].oss.branches.scheduled).toEqual(['5.0.x', 'main']);
90+
});
91+
92+
test('seeds a missing project entry from defaults without mutating defaults', () => {
93+
const data = makeData();
94+
const changes = addBranch(data, 'spring-cloud/spring-cloud-new-thing', '5.0.x', 'main');
95+
96+
expect(changes[0]).toContain('seeded from defaults.oss');
97+
expect(data['spring-cloud-new-thing'].oss.branches.scheduled).toEqual(['5.0.x', 'main']);
98+
expect(data['spring-cloud-new-thing'].oss.jdkVersions['5.0.x']).toEqual(['17', '21', '25']);
99+
// The shared defaults entry must come back untouched.
100+
expect(data.defaults.oss.branches.scheduled).toEqual(['main']);
101+
expect(data.defaults.oss.jdkVersions['5.0.x']).toBeUndefined();
102+
});
103+
});
104+
105+
// ── resolveJdkVersions ──────────────────────────────────────────────────────
106+
107+
describe('resolveJdkVersions', () => {
108+
test('falls back to the section default when the source branch has no entry', () => {
109+
const data = makeData();
110+
const sec = { jdkVersions: { default: ['21'] } };
111+
expect(resolveJdkVersions(data, 'oss', sec, 'main')).toEqual(['21']);
112+
});
113+
114+
test('falls back to the global defaults entry when the section has nothing', () => {
115+
const data = makeData();
116+
expect(resolveJdkVersions(data, 'oss', { jdkVersions: {} }, 'main')).toEqual(['17', '21', '25']);
117+
});
118+
119+
test('falls back to 17/21 when projects.json has no defaults at all', () => {
120+
expect(resolveJdkVersions({}, 'oss', {}, 'main')).toEqual(['17', '21']);
121+
});
122+
});
123+
124+
// ── addBranches ─────────────────────────────────────────────────────────────
125+
126+
describe('addBranches', () => {
127+
test('applies every addition and returns one description per change', () => {
128+
const data = makeData();
129+
const changes = addBranches(data, [
130+
{ repo: 'spring-cloud/spring-cloud-config', branch: '5.0.x', sourceBranch: 'main' },
131+
{ repo: 'spring-cloud/spring-cloud-commons', branch: '5.0.x' },
132+
]);
133+
134+
expect(changes.filter((c) => c.startsWith('spring-cloud/spring-cloud-config:'))).toHaveLength(2);
135+
expect(data['spring-cloud-config'].oss.branches.scheduled).toContain('5.0.x');
136+
expect(data['spring-cloud-commons'].oss.branches.scheduled).toContain('5.0.x');
137+
});
138+
139+
test('defaults sourceBranch to main', () => {
140+
const data = makeData();
141+
addBranches(data, [{ repo: 'spring-cloud/spring-cloud-config', branch: '5.0.x' }]);
142+
expect(data['spring-cloud-config'].oss.jdkVersions['5.0.x']).toEqual(['17', '21', '25']);
143+
});
144+
145+
test('throws when an addition is missing a branch', () => {
146+
expect(() => addBranches(makeData(), [{ repo: 'spring-cloud/spring-cloud-config' }]))
147+
.toThrow(/needs a 'repo' and a 'branch'/);
148+
});
149+
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: 'Add Branches to Projects JSON'
2+
description: >
3+
Registers newly created release line branches in config/projects.json. Takes the whole
4+
set of branches for a release train at once so the entire rollout lands as a single
5+
commit instead of ~30 racing pushes.
6+
author: 'Spring Cloud Team'
7+
8+
inputs:
9+
additions:
10+
description: >
11+
JSON array of branches to register, e.g.
12+
[{"repo":"spring-cloud/spring-cloud-config","branch":"5.0.x","sourceBranch":"main"}].
13+
sourceBranch is the branch the new branch was cut from and is used to copy jdkVersions;
14+
it defaults to "main".
15+
required: true
16+
commit-message:
17+
description: 'Commit message for the projects.json commit'
18+
required: false
19+
default: ''
20+
dry-run:
21+
description: 'When true the file is written and the patch is captured, but nothing is committed or pushed'
22+
required: false
23+
default: 'false'
24+
token:
25+
description: 'GitHub token with contents:write on the spring-cloud-github-actions repository'
26+
required: true
27+
28+
outputs:
29+
changed:
30+
description: 'true when projects.json was modified'
31+
patch:
32+
description: 'The diff of the staged projects.json change, empty when nothing changed'
33+
34+
runs:
35+
using: node20
36+
main: dist/index.js
37+
38+
branding:
39+
icon: 'file-text'
40+
color: 'blue'

0 commit comments

Comments
 (0)