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
4 changes: 4 additions & 0 deletions .github/actions/update-commercial-repositories/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Any `<repositories>` or `<pluginRepositories>` block in a POM file, and any `rep

Each `<repositories>` and `<pluginRepositories>` block containing old `repo.spring.io` URLs is replaced in its entirety with the four commercial entries above, preserving the surrounding indentation. Blocks that do not reference old `repo.spring.io` URLs are left untouched.

With `keep-oss-repositories: 'true'`, the existing old `repo.spring.io` entries (snapshots, milestones, release) are kept after the four commercial ones instead of being dropped, so a build can resolve both commercial and OSS-only artifacts. Entries that are neither commercial nor `repo.spring.io` are still dropped. If a kept entry has the same `<id>` as one of the four commercial repositories, the action fails, naming the file, the id and the URL, and commits nothing: Maven rejects a block that declares an id twice, and the commercial repository's credentials are matched by id. This is for hotfix branches, whose dependencies mix commercial hotfix releases with OSS-only releases.

### Gradle files (`.gradle`, `.gradle.kts`)

Within each `repositories { }` block, individual `maven { url '...' }` entries referencing old `repo.spring.io` URLs are removed and replaced with the four commercial entries. Other entries (e.g. `mavenCentral()`, `gradlePluginPortal()`) are preserved.
Expand Down Expand Up @@ -74,6 +76,8 @@ After editing `src/index.js`, rebuild the bundle with `npm run build` and commit
| `repository` | yes | — | Repository to update (`org/repo-name`) |
| `branch` | yes | — | Branch to update |
| `token` | yes | — | GitHub token with `contents: write` permission on the repository |
| `add-snapshot-repo` | no | `false` | Also add `https://repo.spring.io/snapshot` as a snapshot-only repository |
| `keep-oss-repositories` | no | `false` | Keep existing old `repo.spring.io` entries in POM blocks after the commercial ones. No effect on Gradle files |
| `commit-message` | no | `Updating repositories to commercial Broadcom repositories` | Commit message |
| `git-user-name` | no | `Spring Builds` | Git author name |
| `git-user-email` | no | `svc.spring-builds@broadcom.com` | Git author email |
Expand Down
119 changes: 119 additions & 0 deletions .github/actions/update-commercial-repositories/__tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,109 @@ describe('updatePom', () => {
expect(result).toContain('<pluginRepository>');
COMMERCIAL_REPOS.forEach(r => expect(result).toContain(r.url));
});

describe('keepOssRepositories', () => {
const mixed = `<project>
<repositories>
<repository>
<id>spring-snapshots</id>
<url>https://repo.spring.io/libs-snapshot-local</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<url>https://repo.spring.io/libs-milestone-local</url>
</repository>
<repository>
<id>internal-mirror</id>
<url>https://nexus.example.com/maven</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<url>https://repo.spring.io/libs-snapshot-local</url>
</pluginRepository>
</pluginRepositories>
</project>`;

test('is off by default: OSS repo.spring.io entries are dropped', () => {
const result = updatePom(mixed);
expect(result).not.toContain('libs-snapshot-local');
expect(result).not.toContain('libs-milestone-local');
});

test('keeps the old repo.spring.io entries in both block types', () => {
const result = updatePom(mixed, new Set(), false, true);
expect(result.match(/libs-snapshot-local/g)).toHaveLength(2);
expect(result.match(/libs-milestone-local/g)).toHaveLength(1);
COMMERCIAL_REPOS.forEach(r => expect(result).toContain(r.url));
});

test('puts the commercial repos before the kept OSS ones', () => {
const result = updatePom(mixed, new Set(), false, true);
const firstBlock = result.slice(0, result.indexOf('</repositories>'));
expect(firstBlock.indexOf('spring-enterprise-maven-prod-local'))
.toBeLessThan(firstBlock.indexOf('libs-snapshot-local'));
});

test('does not keep entries that are not repo.spring.io', () => {
const result = updatePom(mixed, new Set(), false, true);
expect(result).not.toContain('nexus.example.com');
});

test('is idempotent', () => {
const once = updatePom(mixed, new Set(), false, true);
expect(updatePom(once, new Set(), false, true)).toBe(once);
});

test('does not declare spring-snapshots twice alongside includeSnapshotRepo', () => {
const result = updatePom(mixed, new Set(), true, true);
const firstBlock = result.slice(0, result.indexOf('</repositories>'));
expect(firstBlock.match(/<id>spring-snapshots<\/id>/g)).toHaveLength(1);
expect(firstBlock).toContain('https://repo.spring.io/snapshot');
expect(firstBlock).not.toContain('libs-snapshot-local');
});

describe('id collision with a commercial repository', () => {
const clash = `<project>
<repositories>
<repository>
<id>spring-commercial-release</id>
<url>https://repo.spring.io/release</url>
</repository>
</repositories>
</project>`;

test('throws, naming the id, the block and the URL', () => {
expect(() => updatePom(clash, new Set(), false, true))
.toThrow(/<repositories> entry 'spring-commercial-release' \(https:\/\/repo\.spring\.io\/release\)/);
});

test('throws for a pluginRepository too', () => {
const plugin = clash
.replace(/repositories>/g, 'pluginRepositories>')
.replace(/repository>/g, 'pluginRepository>');
expect(() => updatePom(plugin, new Set(), false, true)).toThrow(/<pluginRepositories> entry/);
});

test('does not throw when the OSS entries are being dropped anyway', () => {
expect(() => updatePom(clash)).not.toThrow();
});

test('does not throw for the same id on a non-OSS URL, which is dropped', () => {
const nonOss = clash.replace('https://repo.spring.io/release', 'https://nexus.example.com/maven');
expect(() => updatePom(nonOss, new Set(), false, true)).not.toThrow();
});
});

test('does not duplicate an entry that is also in preserveIds', () => {
const result = updatePom(mixed, new Set(['spring-milestones']), false, true);
expect(result.match(/<id>spring-milestones<\/id>/g)).toHaveLength(1);
});
});
});

// ── findBraceEnd ──────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -244,6 +347,22 @@ describe('processFile', () => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});

test('reports the file and leaves it unwritten when a kept OSS id collides with a commercial one', () => {
const content = `<project>
<repositories>
<repository>
<id>spring-commercial-snapshot</id>
<url>https://repo.spring.io/libs-snapshot-local</url>
</repository>
</repositories>
</project>`;
const filePath = path.join(tmpDir, 'pom.xml');
fs.writeFileSync(filePath, content);
expect(() => processFile(filePath, new Set(), new Set(), false, false, true))
.toThrow(new RegExp(`${filePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}: <repositories> entry 'spring-commercial-snapshot'`));
expect(fs.readFileSync(filePath, 'utf-8')).toBe(content);
});

test('updates a pom.xml with old spring.io repos', () => {
const content = `<project>
<repositories>
Expand Down
4 changes: 4 additions & 0 deletions .github/actions/update-commercial-repositories/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ inputs:
description: 'When true, also add https://repo.spring.io/snapshot as a snapshot-only repository (no releases). Use when creating an OSS release branch that needs snapshot dependencies.'
required: false
default: 'false'
keep-oss-repositories:
description: 'When true, POM <repositories>/<pluginRepositories> blocks keep their existing old repo.spring.io entries (snapshots, milestones, release) after the commercial ones, so a build can resolve both commercial and OSS-only artifacts. Fails if a kept entry has the same id as a commercial repository. Has no effect on Gradle files. Use for hotfix branches, whose dependencies mix commercial hotfix releases with OSS-only releases.'
required: false
default: 'false'
commit-message:
description: 'Commit message for the repository updates'
required: false
Expand Down
50 changes: 39 additions & 11 deletions .github/actions/update-commercial-repositories/dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25689,6 +25689,7 @@ const COMMERCIAL_REPOS = [
];

const COMMERCIAL_URLS = new Set(COMMERCIAL_REPOS.map(r => r.url));
const COMMERCIAL_IDS = new Set(COMMERCIAL_REPOS.map(r => r.id));

const SPRING_SNAPSHOT_REPO = {
id: 'spring-snapshots',
Expand Down Expand Up @@ -25767,20 +25768,37 @@ function buildPomBlock(tag, indent, preservedEntries = [], includeSnapshotRepo =
* Updates <repositories> and <pluginRepositories> blocks in POM content.
* preserveIds is a Set of repository IDs to keep alongside the commercial repos.
* includeSnapshotRepo adds https://repo.spring.io/snapshot as a snapshot-only entry.
* keepOssRepositories keeps every existing entry whose URL is an old (non-commercial)
* repo.spring.io one, after the commercial repos, so a build can resolve both commercial
* and OSS-only artifacts. Skipped for an entry with id spring-snapshots when
* includeSnapshotRepo is set, which would otherwise be declared twice.
* Throws if a kept entry has the same id as one of the commercial repos: Maven rejects a
* block that declares an id twice, and credentials are matched by id, so the clash can
* only be resolved by a person.
* Exported for unit testing.
*/
function updatePom(content, preserveIds = new Set(), includeSnapshotRepo = false) {
function updatePom(content, preserveIds = new Set(), includeSnapshotRepo = false, keepOssRepositories = false) {
const replaceBlock = (match, indent, body, tag) => {
if (!OLD_SPRING_IO_RE.test(match)) return match;
if ([...COMMERCIAL_URLS].some(u => match.includes(u))) return match;

let preserved = [];
if (preserveIds.size > 0) {
if (preserveIds.size > 0 || keepOssRepositories) {
const entryTag = tag === 'repositories' ? 'repository' : 'pluginRepository';
const entryRe = new RegExp(`[ \\t]*<${entryTag}>[\\s\\S]*?<\\/${entryTag}>`, 'g');
for (const m of match.matchAll(entryRe)) {
const idMatch = m[0].match(/<id>(.*?)<\/id>/);
if (idMatch && preserveIds.has(idMatch[1].trim())) {
const id = (m[0].match(/<id>(.*?)<\/id>/) || [])[1]?.trim();
const url = (m[0].match(/<url>(.*?)<\/url>/) || [])[1]?.trim();
const isOss = keepOssRepositories && url && OLD_SPRING_IO_RE.test(url)
&& !(includeSnapshotRepo && id === SPRING_SNAPSHOT_REPO.id);
if (isOss && id && COMMERCIAL_IDS.has(id)) {
throw new Error(
`<${tag}> entry '${id}' (${url}) uses the same id as a commercial repository. ` +
`Keeping both would declare the id twice, which Maven rejects, and the credentials ` +
`for the commercial repository would apply to the wrong URL. Rename the entry.`
);
}
if ((id && preserveIds.has(id)) || isOss) {
preserved.push(m[0].trim());
}
}
Expand Down Expand Up @@ -25978,7 +25996,7 @@ function updateGradle(content, isKotlin, withCredentials = false, includeSnapsho
* Returns true when the file was modified.
* Exported for unit testing.
*/
function processFile(filePath, updatedSet = new Set(), preserveIds = new Set(), withCredentials = false, includeSnapshotRepo = false) {
function processFile(filePath, updatedSet = new Set(), preserveIds = new Set(), withCredentials = false, includeSnapshotRepo = false, keepOssRepositories = false) {
const ext = path.extname(filePath).toLowerCase();
const isKts = filePath.endsWith('.gradle.kts');
const isGradle = ext === '.gradle' || isKts;
Expand All @@ -26001,7 +26019,11 @@ function processFile(filePath, updatedSet = new Set(), preserveIds = new Set(),
let changed = false;

if (isXml) {
newContent = updatePom(content, preserveIds, includeSnapshotRepo);
try {
newContent = updatePom(content, preserveIds, includeSnapshotRepo, keepOssRepositories);
} catch (e) {
throw new Error(`${filePath}: ${e.message}`);
}
changed = newContent !== content;
} else {
[newContent, changed] = updateGradle(content, isKts, withCredentials, includeSnapshotRepo);
Expand Down Expand Up @@ -26031,7 +26053,7 @@ const PROJECT_OVERRIDES = {
* POM: replaces repo.spring.io repos while preserving the spring-milestones entry.
* Gradle: replaces with named maven {} blocks that include credentials.
*/
function springCloudContractUpdate(root) {
function springCloudContractUpdate(root, { keepOssRepositories = false } = {}) {
const updated = new Set();
const preserveIds = new Set(['spring-milestones']);

Expand All @@ -26057,7 +26079,11 @@ function springCloudContractUpdate(root) {
let changed = false;

if (isXml) {
newContent = updatePom(content, preserveIds);
try {
newContent = updatePom(content, preserveIds, false, keepOssRepositories);
} catch (e) {
throw new Error(`${filePath}: ${e.message}`);
}
changed = newContent !== content;
} else {
[newContent, changed] = updateGradle(content, isKts, true);
Expand Down Expand Up @@ -26101,7 +26127,8 @@ async function run() {
const branch = core.getInput('branch', { required: true });
const token = core.getInput('token', { required: true });
const addSnapshotRepo = (core.getInput('add-snapshot-repo') || 'false').toLowerCase() === 'true';
const commitMsg = core.getInput('commit-message') || 'Updating repositories to commercial Broadcom repositories';
const keepOssRepositories = (core.getInput('keep-oss-repositories') || 'false').toLowerCase() === 'true';
const commitMsg = core.getInput('commit-message') || 'Updating repositories to commercial Broadcom repositories';
const gitName = core.getInput('git-user-name') || 'Spring Builds';
const gitEmail = core.getInput('git-user-email') || 'svc.spring-builds@broadcom.com';
core.setSecret(token);
Expand All @@ -26110,6 +26137,7 @@ async function run() {
core.info(`Repository: ${repository}`);
core.info(`Branch: ${branch}`);
core.info(`Add snapshot repo: ${addSnapshotRepo}`);
core.info(`Keep OSS repos: ${keepOssRepositories}`);
core.info('');

const repoDir = path.join(os.tmpdir(), '_repos_repo');
Expand All @@ -26132,14 +26160,14 @@ async function run() {

if (override) {
core.info(`Running project-specific override for: ${repoName}`);
updatedByOverride = override(repoDir);
updatedByOverride = override(repoDir, { keepOssRepositories });
core.info('');
}

core.info('Scanning for old repo.spring.io repository references...');
let changed = 0;
walkDir(repoDir, (filePath) => {
if (processFile(filePath, updatedByOverride, new Set(), false, addSnapshotRepo)) changed++;
if (processFile(filePath, updatedByOverride, new Set(), false, addSnapshotRepo, keepOssRepositories)) changed++;
});
core.info(`\nUpdated repository references in ${changed} file(s).`);

Expand Down
Loading
Loading