Skip to content
Open
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
88 changes: 88 additions & 0 deletions .github/workflows/e2e-publishing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
@'
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>setup-java.test</groupId>
<artifactId>mirror-settings</artifactId>
<version>1.0</version>
<repositories>
<repository>
<id>pom-dependencies</id>
<url>https://dependencies.example.invalid/maven</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>pom-plugins</id>
<url>https://plugins.example.invalid/maven</url>
</pluginRepository>
</pluginRepositories>
</project>
'@ | 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 }}
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down Expand Up @@ -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
Expand Down
175 changes: 175 additions & 0 deletions __tests__/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,181 @@ 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([
'https://repo.example.com:8443',
'https://repo.example.com:8443/maven',
'https://[2001:db8::1]',
'https://[2001:db8::1]/maven',
'https://[2001:db8::1]:8443',
'https://[2001:db8::1]:8443/maven'
])('preserves the complete mirror URL %s with an explicit selector', url => {
expect(
auth.parseMavenMirrors([`private:${url}:external:http:*,!internal`])
).toEqual([{id: 'private', url, mirrorOf: 'external:http:*,!internal'}]);
});

it.each([
'private',
'private:https://repo.example.com',
'private:https://repo.example.com:8443',
'private:https://repo.example.com:8443/maven',
'private:https://[2001:db8::1]',
'private:https://[2001:db8::1]/maven',
'private:https://[2001:db8::1]:8443',
'private:https://[2001:db8::1]:8443/maven',
':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('<mirrors>');
});

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('<profiles>');
expect(xml).not.toContain('<activeProfiles>');
});

it('escapes all mirror fields as XML text', () => {
const mirror = {
id: 'private<&>',
url: 'https://repo.example.com/maven?a=1&b=<value>',
mirrorOf: '*,!internal<&>'
};
const xml = auth.generate([], undefined, undefined, [mirror]);
expect(xml).toContain('<id>private&lt;&amp;&gt;</id>');
expect(xml).toContain('a=1&amp;b=&lt;value&gt;');
expect(xml).toContain('<mirrorOf>*,!internal&lt;&amp;&gt;</mirrorOf>');
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<string, string> = {
'settings-path': directory,
'overwrite-settings': String(overwrite),
'mvn-repositories-include-central': 'false',
...(combined ? {'gpg-passphrase-env-var': 'GPG_PASSPHRASE'} : {})
};
const multilineInputs: Record<string, string[]> = {
'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('<repositories>');
expect(xml).toContain('<pluginRepositories>');
expect(xml).toContain('<gpg.passphraseEnvName>GPG_PASSPHRASE');
} else {
expect(xml).not.toContain('<profiles>');
expect(core.getInput).not.toHaveBeenCalledWith(
'mvn-repositories-include-central'
);
}
}
);

it.each(repositoryInputs)(
'parses %s while preserving colons in URLs',
inputName => {
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion dist/cleanup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down
Loading
Loading