diff --git a/.github/workflows/e2e-publishing.yml b/.github/workflows/e2e-publishing.yml
index 35c1596c..f85b5bd1 100644
--- a/.github/workflows/e2e-publishing.yml
+++ b/.github/workflows/e2e-publishing.yml
@@ -175,6 +175,94 @@ jobs:
throw "settings.xml file is not found in expected location"
}
+ test-maven-mirrors:
+ name: Maven mirrors (${{ matrix.mirror-of }})
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [macos-latest, windows-latest, ubuntu-latest]
+ mirror-of: ['*', 'external:*']
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v7
+ with:
+ persist-credentials: false
+ - name: Configure Maven mirrors without repository inputs
+ uses: ./
+ with:
+ distribution: 'temurin'
+ java-version: '21'
+ settings-path: ${{ runner.temp }}/maven-mirror-test
+ mvn-server-credentials: |
+ private:PRIVATE_USERNAME:PRIVATE_TOKEN
+ mvn-mirrors: |
+ private:https://mirror.example.invalid/maven:${{ matrix.mirror-of }}
+ - name: Validate generated settings and Maven mirror selection
+ env:
+ MIRROR_OF: ${{ matrix.mirror-of }}
+ PRIVATE_USERNAME: mirror-test-user
+ PRIVATE_TOKEN: mirror-test-token
+ run: |
+ $directory = Join-Path $env:RUNNER_TEMP "maven-mirror-test"
+ $settingsPath = Join-Path $directory "settings.xml"
+ [xml]$settings = Get-Content -Path $settingsPath -Raw
+ $mirrors = @($settings.settings.mirrors.mirror)
+ if ($mirrors.Count -ne 1 -or $mirrors[0].id -ne "private" -or $mirrors[0].url -ne "https://mirror.example.invalid/maven" -or $mirrors[0].mirrorOf -ne $env:MIRROR_OF) {
+ throw "Incorrect generated mirror"
+ }
+ if ($settings.settings.profiles -or $settings.settings.activeProfiles) {
+ throw "Mirror-only settings must not add repository profiles"
+ }
+ $server = $settings.settings.servers.server
+ if ($server.id -ne "private" -or $server.username -ne '${env.PRIVATE_USERNAME}' -or $server.password -ne '${env.PRIVATE_TOKEN}') {
+ throw "Mirror credentials were not preserved"
+ }
+
+ $pomPath = Join-Path $directory "pom.xml"
+ @'
+
+ 4.0.0
+ setup-java.test
+ mirror-settings
+ 1.0
+
+
+ pom-dependencies
+ https://dependencies.example.invalid/maven
+
+
+
+
+ pom-plugins
+ https://plugins.example.invalid/maven
+
+
+
+ '@ | Set-Content -Path $pomPath
+
+ # validate requires no plugins or dependencies, so mirror selection can
+ # be inspected offline without contacting the deliberately invalid URLs.
+ $output = & mvn --batch-mode --no-transfer-progress --offline --debug `
+ --settings $settingsPath --file $pomPath `
+ "-Dmaven.repo.local=$directory/repository" validate 2>&1
+ if ($LASTEXITCODE -ne 0) {
+ throw "Could not inspect Maven mirror selection: $output"
+ }
+ $log = $output -join "`n"
+ foreach ($id in @("central", "pom-dependencies")) {
+ $expected = "Using mirror private (https://mirror.example.invalid/maven) for $id ("
+ if (-not $log.Contains($expected)) {
+ throw "Maven did not select the mirror for ${id}: $log"
+ }
+ }
+ foreach ($type in @("dependencies", "plugins")) {
+ $pattern = 'Repositories \(' + $type + '\)\s*: \[private \(https://mirror\.example\.invalid/maven, [^\]]+\)\]'
+ if ($log -notmatch $pattern) {
+ throw "Maven did not redirect all $type repositories: $log"
+ }
+ }
+
test-maven-repositories:
name: Maven ${{ matrix.repositories }} repositories
runs-on: ${{ matrix.os }}
diff --git a/README.md b/README.md
index 58822e68..b3740931 100644
--- a/README.md
+++ b/README.md
@@ -171,6 +171,7 @@ steps:
| `mvn-server-repository-origins` | Multiline Maven credential origins in the format `server-id:repository-origin`. Each origin must belong to a configured server ID. Requires Maven 3.10+. | |
| `mvn-repositories` | Multiline Maven dependency repositories in the format `repository-id:repository-url:snapshots-enabled`. | |
| `mvn-plugin-repositories` | Multiline Maven plugin repositories in the format `repository-id:repository-url:snapshots-enabled`. Can be used independently of `mvn-repositories`. | |
+| `mvn-mirrors` | Multiline Maven mirrors in the format `mirror-id:mirror-url:mirror-of`. Redirect matching dependency and plugin repositories; use a matching server ID for authentication. | |
| `mvn-repositories-include-central` | Include Maven Central for dependency and plugin repositories in the generated profile. When `false`, Central is disabled for both types unless that type explicitly declares `central`. Has no effect when neither repository list is configured. | `true` |
| `mvn-repositories-prioritize-central` | Place Maven Central before custom dependency and plugin repositories. Has no effect when Central is excluded or neither repository list is configured. | `true` |
| `settings-path` | Directory where `settings.xml` is written. | `~/.m2` |
@@ -490,6 +491,11 @@ match `mvn-server-credentials` IDs when authentication is required.
See [Resolving Maven dependencies from custom repositories](docs/advanced-usage.md#resolving-maven-dependencies-from-custom-repositories)
and [Resolving Maven plugins from custom repositories](docs/advanced-usage.md#resolving-maven-plugins-from-custom-repositories).
+To redirect repositories, including Central and repositories declared in project
+or parent POMs, use [`mvn-mirrors`](docs/advanced-usage.md#redirecting-maven-repositories-with-mirrors).
+It works independently of the repository inputs; a `mirrorOf` pattern of `*`
+routes all dependency and plugin repositories through the configured mirror.
+
> [!WARNING]
> `mvn-repositories-include-central: false` now disables inherited Maven Central
> for **both dependencies and plugins**, even in existing workflows that only
diff --git a/__tests__/auth.test.ts b/__tests__/auth.test.ts
index 7af82c8f..4500de15 100644
--- a/__tests__/auth.test.ts
+++ b/__tests__/auth.test.ts
@@ -882,6 +882,162 @@ describe('auth tests', () => {
]);
});
+ it.each([
+ '*',
+ 'central',
+ 'external:*',
+ 'external:http:*',
+ '*,!internal',
+ 'external:*,!internal',
+ '!internal,external:http:*',
+ 'external:*,!external:http:*'
+ ])('preserves the mirror selector %s and URL colons', mirrorOf => {
+ expect(
+ auth.parseMavenMirrors([
+ '',
+ ' \t ',
+ ` private : https://[2001:db8::1]:8443/maven : ${mirrorOf} `,
+ 'public:https://repo.example.com:443/releases:central'
+ ])
+ ).toEqual([
+ {
+ id: 'private',
+ url: 'https://[2001:db8::1]:8443/maven',
+ mirrorOf
+ },
+ {
+ id: 'public',
+ url: 'https://repo.example.com:443/releases',
+ mirrorOf: 'central'
+ }
+ ]);
+ });
+
+ it.each([
+ 'private',
+ 'private:https://repo.example.com',
+ ':https://repo.example.com:*',
+ 'private::*',
+ 'private:https://repo.example.com:',
+ 'private: :*',
+ 'private:https://repo.example.com: '
+ ])('rejects malformed mirror entry %s with a line number', entry => {
+ expect(() => auth.parseMavenMirrors(['', entry])).toThrow(
+ 'Invalid mvn-mirrors entry at line 2.'
+ );
+ });
+
+ it('rejects duplicate mirror IDs after trimming', () => {
+ expect(() =>
+ auth.parseMavenMirrors([
+ 'private:https://first.example.com:*',
+ ' private :https://second.example.com:central'
+ ])
+ ).toThrow("Duplicate mirror-id 'private' in mvn-mirrors input at line 2");
+ });
+
+ it('leaves settings unchanged for absent or blank mirrors', () => {
+ const servers = credentials('github', 'USERNAME', 'PASSWORD');
+ expect(auth.parseMavenMirrors(['', ' ', '\t'])).toEqual([]);
+ expect(auth.generate(servers, undefined, undefined, [])).toBe(
+ auth.generate(servers)
+ );
+ expect(auth.generate(servers)).not.toContain('');
+ });
+
+ it('generates public mirrors without credentials or repository profiles', () => {
+ const mirrors = auth.parseMavenMirrors([
+ 'public:https://repo.example.com:external:*',
+ 'fallback:https://other.example.com:central'
+ ]);
+ const xml = auth.generate([], undefined, undefined, mirrors);
+ expect(parseXmlObject(xml)).toMatchObject({
+ settings: {mirrors: {mirror: mirrors}}
+ });
+ expect(xml).not.toContain('');
+ expect(xml).not.toContain('');
+ });
+
+ it('escapes all mirror fields as XML text', () => {
+ const mirror = {
+ id: 'private<&>',
+ url: 'https://repo.example.com/maven?a=1&b=',
+ mirrorOf: '*,!internal<&>'
+ };
+ const xml = auth.generate([], undefined, undefined, [mirror]);
+ expect(xml).toContain('private<&>');
+ expect(xml).toContain('a=1&b=<value>');
+ expect(xml).toContain('*,!internal<&>');
+ expect(parseXmlObject(xml)).toMatchObject({
+ settings: {mirrors: {mirror}}
+ });
+ });
+
+ it.each([
+ {combined: false, overwrite: true},
+ {combined: true, overwrite: true},
+ {combined: true, overwrite: false}
+ ])(
+ 'configures mirrors with combined=$combined and overwrite=$overwrite',
+ async ({combined, overwrite}) => {
+ const directory = path.join(m2Dir, 'mirrors');
+ const location = path.join(directory, MVN_SETTINGS_FILE);
+ fs.mkdirSync(directory, {recursive: true});
+ fs.writeFileSync(location, 'EXISTING SETTINGS');
+ const url = 'https://maven.example.com/releases';
+ const inputs: Record = {
+ 'settings-path': directory,
+ 'overwrite-settings': String(overwrite),
+ 'mvn-repositories-include-central': 'false',
+ ...(combined ? {'gpg-passphrase-env-var': 'GPG_PASSPHRASE'} : {})
+ };
+ const multilineInputs: Record = {
+ 'mvn-server-credentials': ['private:PRIVATE_USERNAME:PRIVATE_TOKEN'],
+ 'mvn-mirrors': [`private:${url}:*`],
+ ...(combined
+ ? {
+ 'mvn-repositories': [`private:${url}:false`],
+ 'mvn-plugin-repositories': [`private:${url}:false`]
+ }
+ : {})
+ };
+ jest.mocked(core.getInput).mockImplementation(name => inputs[name] ?? '');
+ jest
+ .mocked(core.getMultilineInput)
+ .mockImplementation(name => multilineInputs[name] ?? []);
+
+ await auth.configureAuthentication();
+
+ const xml = fs.readFileSync(location, 'utf8');
+ if (!overwrite) {
+ expect(xml).toBe('EXISTING SETTINGS');
+ return;
+ }
+ expect(parseXmlObject(xml)).toMatchObject({
+ settings: {
+ mirrors: {mirror: {id: 'private', url, mirrorOf: '*'}},
+ servers: {
+ server: {
+ id: 'private',
+ username: '${env.PRIVATE_USERNAME}',
+ password: '${env.PRIVATE_TOKEN}'
+ }
+ }
+ }
+ });
+ if (combined) {
+ expect(xml).toContain('');
+ expect(xml).toContain('');
+ expect(xml).toContain('GPG_PASSPHRASE');
+ } else {
+ expect(xml).not.toContain('');
+ expect(core.getInput).not.toHaveBeenCalledWith(
+ 'mvn-repositories-include-central'
+ );
+ }
+ }
+ );
+
it.each(repositoryInputs)(
'parses %s while preserving colons in URLs',
inputName => {
diff --git a/action.yml b/action.yml
index eb32ec42..adf85a59 100644
--- a/action.yml
+++ b/action.yml
@@ -75,6 +75,9 @@ inputs:
mvn-plugin-repositories:
description: 'Multiline list of Maven plugin repositories in the format `repository-id:repository-url:snapshots-enabled`.'
required: false
+ mvn-mirrors:
+ description: 'Multiline list of Maven mirrors in the format `mirror-id:mirror-url:mirror-of`. Use a matching server ID for authentication. Can be used independently of repository inputs.'
+ required: false
mvn-repositories-include-central:
description: 'Include Maven Central for dependency and plugin repositories in the generated profile. When false, Central is disabled for both types unless that type explicitly declares ID `central`. Has no effect when neither repository list is configured.'
required: false
diff --git a/dist/cleanup/index.js b/dist/cleanup/index.js
index 03ab6fe1..349e3d01 100644
--- a/dist/cleanup/index.js
+++ b/dist/cleanup/index.js
@@ -31040,7 +31040,7 @@ module.exports = {
/* harmony export */ gk: () => (/* binding */ INPUT_CACHE),
/* harmony export */ wG: () => (/* binding */ INPUT_JOB_STATUS)
/* harmony export */ });
-/* unused harmony exports MACOS_JAVA_CONTENT_POSTFIX, INPUT_JAVA_VERSION, INPUT_JAVA_VERSION_FILE, INPUT_ARCHITECTURE, INPUT_JAVA_PACKAGE, INPUT_DISTRIBUTION, INPUT_JDK_FILE, INPUT_JDK_FILE_DEPRECATED, INPUT_CHECK_LATEST, INPUT_FORCE_DOWNLOAD, INPUT_SET_DEFAULT, INPUT_PROBLEM_MATCHER, INPUT_VERIFY_SIGNATURE, INPUT_VERIFY_SIGNATURE_PUBLIC_KEY, SIGNATURE_VERIFICATION_DOCUMENTATION_URL, SIGNATURE_VERIFICATION_FAILURE_HELP, INPUT_MVN_SERVER_CREDENTIALS, INPUT_MVN_SERVER_REPOSITORY_ORIGINS, INPUT_MVN_REPOSITORIES, INPUT_MVN_PLUGIN_REPOSITORIES, INPUT_MVN_REPOSITORIES_INCLUDE_CENTRAL, INPUT_MVN_REPOSITORIES_PRIORITIZE_CENTRAL, INPUT_SERVER_ID, INPUT_SERVER_USERNAME_ENV_VAR, INPUT_SERVER_PASSWORD_ENV_VAR, INPUT_SERVER_USERNAME_DEPRECATED, INPUT_SERVER_PASSWORD_DEPRECATED, INPUT_SETTINGS_PATH, INPUT_OVERWRITE_SETTINGS, INPUT_GPG_PRIVATE_KEY, INPUT_GPG_PASSPHRASE_ENV_VAR, INPUT_GPG_PASSPHRASE_DEPRECATED, INPUT_DEFAULT_SERVER_USERNAME, INPUT_DEFAULT_SERVER_PASSWORD, INPUT_DEFAULT_GPG_PRIVATE_KEY, INPUT_DEFAULT_GPG_PASSPHRASE, MAVEN_GPG_PASSPHRASE_DEFAULT_ENV, GPG_PASSPHRASE_PROFILE_ID, MAVEN_REPOSITORIES_PROFILE_ID, MAVEN_CENTRAL_REPOSITORY_ID, MAVEN_CENTRAL_REPOSITORY_URL, INPUT_CACHE_DEPENDENCY_PATH, INPUT_CACHE_PATH, M2_DIR, MVN_SETTINGS_FILE, MVN_TOOLCHAINS_FILE, INPUT_MVN_TOOLCHAIN_ID, INPUT_MVN_TOOLCHAIN_VENDOR, INPUT_SHOW_DOWNLOAD_PROGRESS, MAVEN_ARGS_ENV, MAVEN_NO_TRANSFER_PROGRESS_FLAG, MAVEN_NO_TRANSFER_PROGRESS_LONG_FLAG, DISTRIBUTIONS_ONLY_MAJOR_VERSION */
+/* unused harmony exports MACOS_JAVA_CONTENT_POSTFIX, INPUT_JAVA_VERSION, INPUT_JAVA_VERSION_FILE, INPUT_ARCHITECTURE, INPUT_JAVA_PACKAGE, INPUT_DISTRIBUTION, INPUT_JDK_FILE, INPUT_JDK_FILE_DEPRECATED, INPUT_CHECK_LATEST, INPUT_FORCE_DOWNLOAD, INPUT_SET_DEFAULT, INPUT_PROBLEM_MATCHER, INPUT_VERIFY_SIGNATURE, INPUT_VERIFY_SIGNATURE_PUBLIC_KEY, SIGNATURE_VERIFICATION_DOCUMENTATION_URL, SIGNATURE_VERIFICATION_FAILURE_HELP, INPUT_MVN_SERVER_CREDENTIALS, INPUT_MVN_SERVER_REPOSITORY_ORIGINS, INPUT_MVN_REPOSITORIES, INPUT_MVN_PLUGIN_REPOSITORIES, INPUT_MVN_MIRRORS, INPUT_MVN_REPOSITORIES_INCLUDE_CENTRAL, INPUT_MVN_REPOSITORIES_PRIORITIZE_CENTRAL, INPUT_SERVER_ID, INPUT_SERVER_USERNAME_ENV_VAR, INPUT_SERVER_PASSWORD_ENV_VAR, INPUT_SERVER_USERNAME_DEPRECATED, INPUT_SERVER_PASSWORD_DEPRECATED, INPUT_SETTINGS_PATH, INPUT_OVERWRITE_SETTINGS, INPUT_GPG_PRIVATE_KEY, INPUT_GPG_PASSPHRASE_ENV_VAR, INPUT_GPG_PASSPHRASE_DEPRECATED, INPUT_DEFAULT_SERVER_USERNAME, INPUT_DEFAULT_SERVER_PASSWORD, INPUT_DEFAULT_GPG_PRIVATE_KEY, INPUT_DEFAULT_GPG_PASSPHRASE, MAVEN_GPG_PASSPHRASE_DEFAULT_ENV, GPG_PASSPHRASE_PROFILE_ID, MAVEN_REPOSITORIES_PROFILE_ID, MAVEN_CENTRAL_REPOSITORY_ID, MAVEN_CENTRAL_REPOSITORY_URL, INPUT_CACHE_DEPENDENCY_PATH, INPUT_CACHE_PATH, M2_DIR, MVN_SETTINGS_FILE, MVN_TOOLCHAINS_FILE, INPUT_MVN_TOOLCHAIN_ID, INPUT_MVN_TOOLCHAIN_VENDOR, INPUT_SHOW_DOWNLOAD_PROGRESS, MAVEN_ARGS_ENV, MAVEN_NO_TRANSFER_PROGRESS_FLAG, MAVEN_NO_TRANSFER_PROGRESS_LONG_FLAG, DISTRIBUTIONS_ONLY_MAJOR_VERSION */
const MACOS_JAVA_CONTENT_POSTFIX = 'Contents/Home';
const INPUT_JAVA_VERSION = 'java-version';
const INPUT_JAVA_VERSION_FILE = 'java-version-file';
@@ -31061,6 +31061,7 @@ const INPUT_MVN_SERVER_CREDENTIALS = 'mvn-server-credentials';
const INPUT_MVN_SERVER_REPOSITORY_ORIGINS = 'mvn-server-repository-origins';
const INPUT_MVN_REPOSITORIES = 'mvn-repositories';
const INPUT_MVN_PLUGIN_REPOSITORIES = 'mvn-plugin-repositories';
+const INPUT_MVN_MIRRORS = 'mvn-mirrors';
const INPUT_MVN_REPOSITORIES_INCLUDE_CENTRAL = 'mvn-repositories-include-central';
const INPUT_MVN_REPOSITORIES_PRIORITIZE_CENTRAL = 'mvn-repositories-prioritize-central';
const INPUT_SERVER_ID = 'server-id';
diff --git a/dist/setup/81.index.js b/dist/setup/81.index.js
index 39f98e37..28c50048 100644
--- a/dist/setup/81.index.js
+++ b/dist/setup/81.index.js
@@ -14,6 +14,7 @@ __webpack_require__.r(__webpack_exports__);
/* harmony export */ getInputWithDeprecatedAlias: () => (/* binding */ getInputWithDeprecatedAlias),
/* harmony export */ getMavenRepositorySettings: () => (/* binding */ getMavenRepositorySettings),
/* harmony export */ getMavenServerSettings: () => (/* binding */ getMavenServerSettings),
+/* harmony export */ parseMavenMirrors: () => (/* binding */ parseMavenMirrors),
/* harmony export */ parseMavenRepositories: () => (/* binding */ parseMavenRepositories),
/* harmony export */ parseMavenServerCredentials: () => (/* binding */ parseMavenServerCredentials)
/* harmony export */ });
@@ -42,6 +43,7 @@ const MAVEN_REPOSITORY_ORIGIN = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^/?#@]+\/?$/;
async function configureAuthentication() {
const servers = getMavenServerSettings();
const repositorySettings = getMavenRepositorySettings();
+ const mirrors = parseMavenMirrors(_actions_core__WEBPACK_IMPORTED_MODULE_1__/* .getMultilineInput */ .q3(_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_MVN_MIRRORS */ .K));
const settingsDirectory = _actions_core__WEBPACK_IMPORTED_MODULE_1__/* .getInput */ .V4(_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_SETTINGS_PATH */ .Xh) ||
path__WEBPACK_IMPORTED_MODULE_0__.join(os__WEBPACK_IMPORTED_MODULE_4__.homedir(), _constants_js__WEBPACK_IMPORTED_MODULE_7__/* .M2_DIR */ .iT);
const overwriteSettings = (0,_util_js__WEBPACK_IMPORTED_MODULE_6__/* .getBooleanInput */ .Vt)(_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .INPUT_OVERWRITE_SETTINGS */ .TS, true);
@@ -51,7 +53,7 @@ async function configureAuthentication() {
if (gpgPrivateKey) {
_actions_core__WEBPACK_IMPORTED_MODULE_1__/* .setSecret */ .Pq(gpgPrivateKey);
}
- await createAuthenticationSettings(servers, settingsDirectory, overwriteSettings, gpgPassphraseEnvVar, repositorySettings);
+ await createAuthenticationSettings(servers, settingsDirectory, overwriteSettings, gpgPassphraseEnvVar, repositorySettings, mirrors);
if (gpgPrivateKey) {
_actions_core__WEBPACK_IMPORTED_MODULE_1__/* .info */ .pq('Importing private gpg key');
const gpgHome = await _gpg_js__WEBPACK_IMPORTED_MODULE_5__/* .importKey */ .Fh(gpgPrivateKey);
@@ -222,6 +224,37 @@ function parseMavenRepositories(entries, includeCentral, inputName = _constants_
});
return repositories;
}
+// only exported for testing purposes
+function parseMavenMirrors(entries) {
+ const mirrors = [];
+ const mirrorIds = new Set();
+ entries.forEach((entry, index) => {
+ if (!entry.trim()) {
+ return;
+ }
+ const separator = entry.indexOf(':');
+ const remainder = entry.slice(separator + 1).trim();
+ // Consume the URL scheme before looking for the selector delimiter. Selector
+ // tokens may themselves contain colons (external:* and external:http:*).
+ const scheme = /^[A-Za-z][A-Za-z0-9+.-]*:/.exec(remainder)?.[0] ?? '';
+ const fields = /^(.*?):((?:\s*!?external:(?:http:)?\*\s*|[^:,]*)(?:,(?:\s*!?external:(?:http:)?\*\s*|[^:,]*))*)$/.exec(remainder.slice(scheme.length));
+ if (separator < 0 || !fields) {
+ throw new Error(`Invalid mvn-mirrors entry at line ${index + 1}. Expected format: mirror-id:mirror-url:mirror-of`);
+ }
+ const id = entry.slice(0, separator).trim();
+ const url = `${scheme}${fields[1]}`.trim();
+ const mirrorOf = fields[2].trim();
+ if (!id || !url || !mirrorOf) {
+ throw new Error(`Invalid mvn-mirrors entry at line ${index + 1}. mirror-id, mirror URL, and mirror-of are required`);
+ }
+ if (mirrorIds.has(id)) {
+ throw new Error(`Duplicate mirror-id '${id}' in mvn-mirrors input at line ${index + 1}`);
+ }
+ mirrorIds.add(id);
+ mirrors.push({ id, url, mirrorOf });
+ });
+ return mirrors;
+}
function generateRepositorySection(customRepositories, settings, element) {
if (!customRepositories.length && settings.includeCentral) {
return [];
@@ -253,15 +286,15 @@ function generateRepositorySection(customRepositories, settings, element) {
lines.push(` ${container}>`);
return lines;
}
-async function createAuthenticationSettings(servers, settingsDirectory, overwriteSettings, gpgPassphraseEnvVar = undefined, repositorySettings = undefined) {
+async function createAuthenticationSettings(servers, settingsDirectory, overwriteSettings, gpgPassphraseEnvVar = undefined, repositorySettings = undefined, mirrors = []) {
_actions_core__WEBPACK_IMPORTED_MODULE_1__/* .info */ .pq(`Creating ${_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .MVN_SETTINGS_FILE */ .vO} with server-id: ${servers.map(server => server.id).join(', ')}`);
// when an alternate m2 location is specified use only that location (no .m2 directory)
// otherwise use the home/.m2/ path
await _actions_io__WEBPACK_IMPORTED_MODULE_2__/* .mkdirP */ .U$(settingsDirectory);
- await write(settingsDirectory, generate(servers, gpgPassphraseEnvVar, repositorySettings), overwriteSettings);
+ await write(settingsDirectory, generate(servers, gpgPassphraseEnvVar, repositorySettings, mirrors), overwriteSettings);
}
// only exported for testing purposes
-function generate(servers, gpgPassphraseEnvVar, repositorySettings) {
+function generate(servers, gpgPassphraseEnvVar, repositorySettings, mirrors = []) {
// The maven-gpg-plugin reads the passphrase from the environment variable
// named by the `gpg.passphraseEnvName` property (default MAVEN_GPG_PASSPHRASE).
// Only configure it when the requested env var name differs from that default;
@@ -289,6 +322,13 @@ function generate(servers, gpgPassphraseEnvVar, repositorySettings) {
lines.push(' ');
}
lines.push(' ');
+ if (mirrors.length) {
+ lines.push(' ');
+ for (const mirror of mirrors) {
+ lines.push(' ', ` ${(0,_xml_js__WEBPACK_IMPORTED_MODULE_8__/* .escapeXmlText */ .I)(mirror.id)}`, ` ${(0,_xml_js__WEBPACK_IMPORTED_MODULE_8__/* .escapeXmlText */ .I)(mirror.url)}`, ` ${(0,_xml_js__WEBPACK_IMPORTED_MODULE_8__/* .escapeXmlText */ .I)(mirror.mirrorOf)}`, ' ');
+ }
+ lines.push(' ');
+ }
if (repositorySettings || includeGpgPassphraseProfile) {
lines.push(' ');
if (repositorySettings) {
diff --git a/dist/setup/index.js b/dist/setup/index.js
index 5c053664..bc543700 100644
--- a/dist/setup/index.js
+++ b/dist/setup/index.js
@@ -31040,6 +31040,7 @@ module.exports = {
/* harmony export */ GL: () => (/* binding */ INPUT_CACHE_JDK),
/* harmony export */ H5: () => (/* binding */ INPUT_MVN_REPOSITORIES_INCLUDE_CENTRAL),
/* harmony export */ I9: () => (/* binding */ INPUT_FORCE_DOWNLOAD),
+/* harmony export */ K: () => (/* binding */ INPUT_MVN_MIRRORS),
/* harmony export */ K$: () => (/* binding */ GPG_PASSPHRASE_PROFILE_ID),
/* harmony export */ LS: () => (/* binding */ INPUT_ARCHITECTURE),
/* harmony export */ MM: () => (/* binding */ INPUT_MVN_SERVER_CREDENTIALS),
@@ -31111,6 +31112,7 @@ const INPUT_MVN_SERVER_CREDENTIALS = 'mvn-server-credentials';
const INPUT_MVN_SERVER_REPOSITORY_ORIGINS = 'mvn-server-repository-origins';
const INPUT_MVN_REPOSITORIES = 'mvn-repositories';
const INPUT_MVN_PLUGIN_REPOSITORIES = 'mvn-plugin-repositories';
+const INPUT_MVN_MIRRORS = 'mvn-mirrors';
const INPUT_MVN_REPOSITORIES_INCLUDE_CENTRAL = 'mvn-repositories-include-central';
const INPUT_MVN_REPOSITORIES_PRIORITIZE_CENTRAL = 'mvn-repositories-prioritize-central';
const INPUT_SERVER_ID = 'server-id';
diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md
index dedca4ed..488d1efb 100644
--- a/docs/advanced-usage.md
+++ b/docs/advanced-usage.md
@@ -28,6 +28,7 @@
- [Testing against different platforms](#Testing-against-different-platforms)
- [Publishing using Apache Maven](#Publishing-using-Apache-Maven)
- [Publishing to multiple Maven servers](#publishing-to-multiple-maven-servers)
+- [Redirecting Maven repositories with mirrors](#redirecting-maven-repositories-with-mirrors)
- [Apache Maven with a settings path](#apache-maven-with-a-settings-path)
- [Maven transfer progress (download logs)](#Maven-transfer-progress-download-logs)
- [Java problem matcher (compiler annotations)](#java-problem-matcher-compiler-annotations)
@@ -1090,6 +1091,72 @@ default; `mvn-repositories-prioritize-central: false` places it after them.
All repository generation respects `settings-path` and `overwrite-settings`;
an existing settings file is left untouched when `overwrite-settings: false`.
+### Redirecting Maven repositories with mirrors
+
+Use `mvn-mirrors` to redirect matching dependency and plugin repositories,
+including Maven Central and repositories declared in project or parent POMs.
+Unlike repository profiles, mirrors replace the destination Maven contacts.
+The Central controls described above do not redirect arbitrary POM repositories.
+
+Each nonblank line has the format `mirror-id:mirror-url:mirror-of`. IDs must be
+unique within this input. URL schemes and ports, and colons in `mirrorOf`
+selectors, are preserved. For example:
+
+```yaml
+steps:
+ - uses: actions/checkout@v7
+ - uses: actions/setup-java@v6
+ with:
+ distribution: 'temurin'
+ java-version: '21'
+ mvn-server-credentials: |
+ private:PRIVATE_REPOSITORY_USERNAME:PRIVATE_REPOSITORY_TOKEN
+ mvn-mirrors: |
+ private:https://maven.example.com/releases:*
+ - run: mvn --batch-mode verify
+ env:
+ PRIVATE_REPOSITORY_USERNAME: ${{ secrets.PRIVATE_REPOSITORY_USERNAME }}
+ PRIVATE_REPOSITORY_TOKEN: ${{ secrets.PRIVATE_REPOSITORY_TOKEN }}
+```
+
+This adds the following directly under ``, not inside a profile:
+
+```xml
+
+
+ private
+ https://maven.example.com/releases
+ *
+
+
+```
+
+For authentication, the mirror ID must match a server ID configured through
+`mvn-server-credentials` or the single-server inputs. Public mirrors do not need
+matching credentials. Credentials remain environment-variable references in the
+generated settings; do not put credential values in `mvn-mirrors`.
+
+Mirrors can be used alone, as above, or alongside `mvn-repositories` and
+`mvn-plugin-repositories`. They do not change those inputs or the Central
+controls. Omitting `mvn-mirrors`, or supplying only blank lines, adds no
+`` section or repository profile.
+
+Maven interprets `mirrorOf`: `*` matches every repository, `central` matches
+only that repository ID, `external:*` matches external repositories, and
+`external:http:*` matches external HTTP repositories. Comma-separated patterns
+such as `external:*,!internal` exclude specific IDs. Selector text and mirror
+order are retained; Maven applies its own matching and precedence rules.
+For example, a public mirror needs no additional repository or credential input:
+
+```yaml
+mvn-mirrors: |
+ public:https://mirror.example.com:8443/maven:external:*,!internal
+```
+
+Mirror generation respects `settings-path` and `overwrite-settings`. An existing
+settings file is left untouched when `overwrite-settings: false`; this input
+does not merge mirrors into that file.
+
### GPG
The example above uses the [Maven GPG Plugin](https://maven.apache.org/plugins/maven-gpg-plugin/)'s Bouncy Castle signer (`-Dgpg.signer=bc`, available since `maven-gpg-plugin` 3.2.0). It is a pure-Java signer that reads the key directly from the `MAVEN_GPG_KEY` environment variable, so it does **not** require the `gpg` executable, importing the key into a GPG keychain, or the `--pinentry-mode loopback` workaround in your `pom.xml`. The key must be an ASCII-armored secret key (transferable secret key format).
diff --git a/src/auth.ts b/src/auth.ts
index 91480f76..c3c25112 100644
--- a/src/auth.ts
+++ b/src/auth.ts
@@ -30,11 +30,20 @@ export interface MavenRepositorySettings {
prioritizeCentral: boolean;
}
+export interface MavenMirror {
+ id: string;
+ url: string;
+ mirrorOf: string;
+}
+
const MAVEN_REPOSITORY_ORIGIN = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^/?#@]+\/?$/;
export async function configureAuthentication() {
const servers = getMavenServerSettings();
const repositorySettings = getMavenRepositorySettings();
+ const mirrors = parseMavenMirrors(
+ core.getMultilineInput(constants.INPUT_MVN_MIRRORS)
+ );
const settingsDirectory =
core.getInput(constants.INPUT_SETTINGS_PATH) ||
path.join(os.homedir(), constants.M2_DIR);
@@ -60,7 +69,8 @@ export async function configureAuthentication() {
settingsDirectory,
overwriteSettings,
gpgPassphraseEnvVar,
- repositorySettings
+ repositorySettings,
+ mirrors
);
if (gpgPrivateKey) {
@@ -333,6 +343,52 @@ export function parseMavenRepositories(
return repositories;
}
+// only exported for testing purposes
+export function parseMavenMirrors(entries: string[]): MavenMirror[] {
+ const mirrors: MavenMirror[] = [];
+ const mirrorIds = new Set();
+
+ entries.forEach((entry, index) => {
+ if (!entry.trim()) {
+ return;
+ }
+
+ const separator = entry.indexOf(':');
+ const remainder = entry.slice(separator + 1).trim();
+ // Consume the URL scheme before looking for the selector delimiter. Selector
+ // tokens may themselves contain colons (external:* and external:http:*).
+ const scheme = /^[A-Za-z][A-Za-z0-9+.-]*:/.exec(remainder)?.[0] ?? '';
+ const fields =
+ /^(.*?):((?:\s*!?external:(?:http:)?\*\s*|[^:,]*)(?:,(?:\s*!?external:(?:http:)?\*\s*|[^:,]*))*)$/.exec(
+ remainder.slice(scheme.length)
+ );
+ if (separator < 0 || !fields) {
+ throw new Error(
+ `Invalid mvn-mirrors entry at line ${index + 1}. Expected format: mirror-id:mirror-url:mirror-of`
+ );
+ }
+
+ const id = entry.slice(0, separator).trim();
+ const url = `${scheme}${fields[1]}`.trim();
+ const mirrorOf = fields[2].trim();
+ if (!id || !url || !mirrorOf) {
+ throw new Error(
+ `Invalid mvn-mirrors entry at line ${index + 1}. mirror-id, mirror URL, and mirror-of are required`
+ );
+ }
+ if (mirrorIds.has(id)) {
+ throw new Error(
+ `Duplicate mirror-id '${id}' in mvn-mirrors input at line ${index + 1}`
+ );
+ }
+
+ mirrorIds.add(id);
+ mirrors.push({id, url, mirrorOf});
+ });
+
+ return mirrors;
+}
+
function generateRepositorySection(
customRepositories: MavenRepository[],
settings: MavenRepositorySettings,
@@ -388,7 +444,8 @@ export async function createAuthenticationSettings(
settingsDirectory: string,
overwriteSettings: boolean,
gpgPassphraseEnvVar: string | undefined = undefined,
- repositorySettings: MavenRepositorySettings | undefined = undefined
+ repositorySettings: MavenRepositorySettings | undefined = undefined,
+ mirrors: MavenMirror[] = []
) {
core.info(
`Creating ${constants.MVN_SETTINGS_FILE} with server-id: ${servers.map(server => server.id).join(', ')}`
@@ -398,7 +455,7 @@ export async function createAuthenticationSettings(
await io.mkdirP(settingsDirectory);
await write(
settingsDirectory,
- generate(servers, gpgPassphraseEnvVar, repositorySettings),
+ generate(servers, gpgPassphraseEnvVar, repositorySettings, mirrors),
overwriteSettings
);
}
@@ -407,7 +464,8 @@ export async function createAuthenticationSettings(
export function generate(
servers: MavenServerCredentials[],
gpgPassphraseEnvVar?: string | undefined,
- repositorySettings?: MavenRepositorySettings | undefined
+ repositorySettings?: MavenRepositorySettings | undefined,
+ mirrors: MavenMirror[] = []
) {
// The maven-gpg-plugin reads the passphrase from the environment variable
// named by the `gpg.passphraseEnvName` property (default MAVEN_GPG_PASSPHRASE).
@@ -447,6 +505,20 @@ export function generate(
}
lines.push(' ');
+ if (mirrors.length) {
+ lines.push(' ');
+ for (const mirror of mirrors) {
+ lines.push(
+ ' ',
+ ` ${escapeXmlText(mirror.id)}`,
+ ` ${escapeXmlText(mirror.url)}`,
+ ` ${escapeXmlText(mirror.mirrorOf)}`,
+ ' '
+ );
+ }
+ lines.push(' ');
+ }
+
if (repositorySettings || includeGpgPassphraseProfile) {
lines.push(' ');
if (repositorySettings) {
diff --git a/src/constants.ts b/src/constants.ts
index bdba9224..03942193 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -20,6 +20,7 @@ export const INPUT_MVN_SERVER_REPOSITORY_ORIGINS =
'mvn-server-repository-origins';
export const INPUT_MVN_REPOSITORIES = 'mvn-repositories';
export const INPUT_MVN_PLUGIN_REPOSITORIES = 'mvn-plugin-repositories';
+export const INPUT_MVN_MIRRORS = 'mvn-mirrors';
export const INPUT_MVN_REPOSITORIES_INCLUDE_CENTRAL =
'mvn-repositories-include-central';
export const INPUT_MVN_REPOSITORIES_PRIORITIZE_CENTRAL =