mirror of
https://github.com/actions/setup-java.git
synced 2026-08-01 07:31:38 +08:00
* Optimize Maven configuration warm path Avoid eager Maven XML initialization on warm JDK runs by using deterministic serializers for new Maven settings/toolchains files, lazy-loading xmlbuilder2 for existing toolchains merges, and deferring Maven configuration modules until after Java setup. Add targeted tests for XML escaping, lazy xmlbuilder2 loading, concurrent Maven configuration, and a manual benchmark workflow for warm-path validation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4dc58426-5e20-44cb-af16-8da0965fac3b * Address Maven optimization PR feedback Make the toolchain XML generator consistently async, remove redundant Maven configuration await handling, and reuse the existing XML test helper. Configure CodeQL to skip generated dist output so newly split vendored chunks do not report duplicate generated-code alerts. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4dc58426-5e20-44cb-af16-8da0965fac3b * Apply rubber duck review suggestions Document XML attribute escaping, simplify Maven configuration awaiting, and add a regression test that feeds fast-path toolchains output into the merge path. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4dc58426-5e20-44cb-af16-8da0965fac3b * Delete .github/codeql/codeql-config.yml * Update codeql-analysis.yml * Replace xmlbuilder2 in Maven toolchain merge Use fast-xml-parser for existing toolchains.xml parsing and serialize merged Maven toolchains deterministically. This removes the bundled xmlbuilder2 DOM/XML builder chunk from dist while preserving merge behavior for custom attributes, custom toolchains, partial entries, duplicate filtering, and escaping. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4dc58426-5e20-44cb-af16-8da0965fac3b * Move Maven benchmark out of setup-java Remove the Maven warm-path benchmark workflow and helper script from setup-java. Benchmark coverage is being moved to actions/setup-java-benchmarks so this action repository only carries the runtime optimization, tests, and generated distribution artifacts. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4dc58426-5e20-44cb-af16-8da0965fac3b --------- Copilot-Session: 4dc58426-5e20-44cb-af16-8da0965fac3b
361 lines
10 KiB
TypeScript
361 lines
10 KiB
TypeScript
import {
|
|
jest,
|
|
describe,
|
|
it,
|
|
expect,
|
|
beforeEach,
|
|
afterEach,
|
|
beforeAll,
|
|
afterAll
|
|
} from '@jest/globals';
|
|
import {fileURLToPath} from 'url';
|
|
import * as io from '@actions/io';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import os from 'os';
|
|
import {XMLParser} from 'fast-xml-parser';
|
|
|
|
// Mock @actions/core before importing source modules that depend on it
|
|
jest.unstable_mockModule('@actions/core', () => ({
|
|
info: jest.fn(),
|
|
warning: jest.fn(),
|
|
debug: jest.fn(),
|
|
error: jest.fn(),
|
|
notice: jest.fn(),
|
|
setFailed: jest.fn(),
|
|
setOutput: jest.fn(),
|
|
getInput: jest.fn(),
|
|
getBooleanInput: jest.fn(),
|
|
getMultilineInput: jest.fn(),
|
|
addPath: jest.fn(),
|
|
exportVariable: jest.fn(),
|
|
saveState: jest.fn(),
|
|
getState: jest.fn(),
|
|
setSecret: jest.fn(),
|
|
isDebug: jest.fn(() => false),
|
|
startGroup: jest.fn(),
|
|
endGroup: jest.fn(),
|
|
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
|
|
toPlatformPath: jest.fn((p: string) => p),
|
|
toWin32Path: jest.fn((p: string) => p),
|
|
toPosixPath: jest.fn((p: string) => p)
|
|
}));
|
|
|
|
// Dynamic imports after mocking
|
|
const core = await import('@actions/core');
|
|
const auth = await import('../src/auth.js');
|
|
const {M2_DIR, MVN_SETTINGS_FILE} = await import('../src/constants.js');
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const m2Dir = path.join(__dirname, M2_DIR);
|
|
const settingsFile = path.join(m2Dir, MVN_SETTINGS_FILE);
|
|
|
|
describe('auth tests', () => {
|
|
let spyOSHomedir: any;
|
|
let spyInfo: any;
|
|
|
|
beforeEach(async () => {
|
|
await io.rmRF(m2Dir);
|
|
spyOSHomedir = jest.spyOn(os, 'homedir');
|
|
spyOSHomedir.mockReturnValue(__dirname);
|
|
spyInfo = core.info as jest.Mock;
|
|
spyInfo.mockImplementation(() => null);
|
|
}, 300000);
|
|
|
|
afterAll(async () => {
|
|
try {
|
|
await io.rmRF(m2Dir);
|
|
} catch {
|
|
console.log('Failed to remove test directories');
|
|
}
|
|
jest.resetAllMocks();
|
|
jest.clearAllMocks();
|
|
jest.restoreAllMocks();
|
|
}, 100000);
|
|
|
|
it('creates settings.xml in alternate locations', async () => {
|
|
const id = 'packages';
|
|
const username = 'UNAMI';
|
|
const password = 'TOLKIEN';
|
|
|
|
const altHome = path.join(__dirname, 'runner', 'settings');
|
|
const altSettingsFile = path.join(altHome, MVN_SETTINGS_FILE);
|
|
await io.rmRF(altHome); // ensure it doesn't already exist
|
|
|
|
await auth.createAuthenticationSettings(
|
|
id,
|
|
username,
|
|
password,
|
|
altHome,
|
|
true
|
|
);
|
|
|
|
expect(fs.existsSync(m2Dir)).toBe(false);
|
|
expect(fs.existsSync(settingsFile)).toBe(false);
|
|
|
|
expect(fs.existsSync(altHome)).toBe(true);
|
|
expect(fs.existsSync(altSettingsFile)).toBe(true);
|
|
expect(fs.readFileSync(altSettingsFile, 'utf-8')).toEqual(
|
|
auth.generate(id, username, password)
|
|
);
|
|
|
|
await io.rmRF(altHome);
|
|
}, 100000);
|
|
|
|
it('creates settings.xml with minimal configuration', async () => {
|
|
const id = 'packages';
|
|
const username = 'UNAME';
|
|
const password = 'TOKEN';
|
|
|
|
await auth.createAuthenticationSettings(
|
|
id,
|
|
username,
|
|
password,
|
|
m2Dir,
|
|
true
|
|
);
|
|
|
|
expect(fs.existsSync(m2Dir)).toBe(true);
|
|
expect(fs.existsSync(settingsFile)).toBe(true);
|
|
expect(fs.readFileSync(settingsFile, 'utf-8')).toEqual(
|
|
auth.generate(id, username, password)
|
|
);
|
|
}, 100000);
|
|
|
|
it('creates settings.xml with additional configuration', async () => {
|
|
const id = 'packages';
|
|
const username = 'UNAME';
|
|
const password = 'TOKEN';
|
|
const gpgPassphrase = 'GPG';
|
|
|
|
await auth.createAuthenticationSettings(
|
|
id,
|
|
username,
|
|
password,
|
|
m2Dir,
|
|
true,
|
|
gpgPassphrase
|
|
);
|
|
|
|
expect(fs.existsSync(m2Dir)).toBe(true);
|
|
expect(fs.existsSync(settingsFile)).toBe(true);
|
|
expect(fs.readFileSync(settingsFile, 'utf-8')).toEqual(
|
|
auth.generate(id, username, password, gpgPassphrase)
|
|
);
|
|
}, 100000);
|
|
|
|
it('overwrites existing settings.xml files', async () => {
|
|
const id = 'packages';
|
|
const username = 'USERNAME';
|
|
const password = 'PASSWORD';
|
|
|
|
fs.mkdirSync(m2Dir, {recursive: true});
|
|
fs.writeFileSync(settingsFile, 'FAKE FILE');
|
|
expect(fs.existsSync(m2Dir)).toBe(true);
|
|
expect(fs.existsSync(settingsFile)).toBe(true);
|
|
|
|
await auth.createAuthenticationSettings(
|
|
id,
|
|
username,
|
|
password,
|
|
m2Dir,
|
|
true
|
|
);
|
|
|
|
expect(fs.existsSync(m2Dir)).toBe(true);
|
|
expect(fs.existsSync(settingsFile)).toBe(true);
|
|
expect(fs.readFileSync(settingsFile, 'utf-8')).toEqual(
|
|
auth.generate(id, username, password)
|
|
);
|
|
}, 100000);
|
|
|
|
it('does not overwrite existing settings.xml files', async () => {
|
|
const id = 'packages';
|
|
const username = 'USERNAME';
|
|
const password = 'PASSWORD';
|
|
|
|
fs.mkdirSync(m2Dir, {recursive: true});
|
|
fs.writeFileSync(settingsFile, 'FAKE FILE');
|
|
expect(fs.existsSync(m2Dir)).toBe(true);
|
|
expect(fs.existsSync(settingsFile)).toBe(true);
|
|
|
|
await auth.createAuthenticationSettings(
|
|
id,
|
|
username,
|
|
password,
|
|
m2Dir,
|
|
false
|
|
);
|
|
|
|
expect(fs.existsSync(m2Dir)).toBe(true);
|
|
expect(fs.existsSync(settingsFile)).toBe(true);
|
|
expect(fs.readFileSync(settingsFile, 'utf-8')).toEqual('FAKE FILE');
|
|
}, 100000);
|
|
|
|
it('generates valid settings.xml with minimal configuration', () => {
|
|
const id = 'packages';
|
|
const username = 'USER';
|
|
const password = '&<>"\'\'"><&';
|
|
|
|
const expectedSettings = `<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">
|
|
<interactiveMode>false</interactiveMode>
|
|
<servers>
|
|
<server>
|
|
<id>${id}</id>
|
|
<username>\${env.${username}}</username>
|
|
<password>\${env.&<>"''"><&}</password>
|
|
</server>
|
|
</servers>
|
|
</settings>`;
|
|
|
|
expect(auth.generate(id, username, password)).toEqual(expectedSettings);
|
|
});
|
|
|
|
it('generates valid settings.xml with additional configuration', () => {
|
|
const id = 'packages';
|
|
const username = 'USER';
|
|
const password = '&<>"\'\'"><&';
|
|
const gpgPassphrase = 'PASSPHRASE';
|
|
|
|
const expectedSettings = `<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">
|
|
<interactiveMode>false</interactiveMode>
|
|
<servers>
|
|
<server>
|
|
<id>${id}</id>
|
|
<username>\${env.${username}}</username>
|
|
<password>\${env.&<>"''"><&}</password>
|
|
</server>
|
|
</servers>
|
|
<profiles>
|
|
<profile>
|
|
<id>setup-java-gpg</id>
|
|
<properties>
|
|
<gpg.passphraseEnvName>${gpgPassphrase}</gpg.passphraseEnvName>
|
|
</properties>
|
|
</profile>
|
|
</profiles>
|
|
<activeProfiles>
|
|
<activeProfile>setup-java-gpg</activeProfile>
|
|
</activeProfiles>
|
|
</settings>`;
|
|
|
|
expect(auth.generate(id, username, password, gpgPassphrase)).toEqual(
|
|
expectedSettings
|
|
);
|
|
});
|
|
|
|
it('does not add a gpg profile when the passphrase env var is the maven-gpg-plugin default', () => {
|
|
const id = 'packages';
|
|
const username = 'USER';
|
|
const password = '&<>"\'\'"><&';
|
|
const gpgPassphrase = 'MAVEN_GPG_PASSPHRASE';
|
|
|
|
const expectedSettings = `<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">
|
|
<interactiveMode>false</interactiveMode>
|
|
<servers>
|
|
<server>
|
|
<id>${id}</id>
|
|
<username>\${env.${username}}</username>
|
|
<password>\${env.&<>"''"><&}</password>
|
|
</server>
|
|
</servers>
|
|
</settings>`;
|
|
|
|
expect(auth.generate(id, username, password, gpgPassphrase)).toEqual(
|
|
expectedSettings
|
|
);
|
|
});
|
|
|
|
it('escapes settings.xml values while preserving parsed semantics', () => {
|
|
const id = `packages&<>"'é`;
|
|
const username = `USER&<>"'é`;
|
|
const password = `TOKEN&<>"'é`;
|
|
const gpgPassphrase = `GPG&<>"'é`;
|
|
|
|
const xml = auth.generate(id, username, password, gpgPassphrase);
|
|
const parsed = parseXmlObject(xml) as any;
|
|
|
|
expect(parsed.settings.interactiveMode).toBe('false');
|
|
expect(xmlElementText(xml, 'id')).toBe(id);
|
|
expect(xmlElementText(xml, 'username')).toBe(`\${env.${username}}`);
|
|
expect(xmlElementText(xml, 'password')).toBe(`\${env.${password}}`);
|
|
expect(xmlElementText(xml, 'gpg.passphraseEnvName')).toBe(gpgPassphrase);
|
|
expect(parsed.settings.activeProfiles.activeProfile).toBe('setup-java-gpg');
|
|
});
|
|
|
|
function xmlElementText(xml: string, tagName: string): string {
|
|
const match = new RegExp(`<${tagName}>([\\s\\S]*?)</${tagName}>`).exec(xml);
|
|
expect(match).not.toBeNull();
|
|
return (parseXmlObject(`<value>${match?.[1]}</value>`) as {value: string})
|
|
.value;
|
|
}
|
|
|
|
function parseXmlObject(xml: string): unknown {
|
|
const parser = new XMLParser({
|
|
ignoreAttributes: false,
|
|
attributeNamePrefix: '@',
|
|
parseAttributeValue: false,
|
|
parseTagValue: false,
|
|
trimValues: true
|
|
});
|
|
return parser.parse(xml);
|
|
}
|
|
|
|
it('uses deprecated input aliases and warns', () => {
|
|
const mockGetInput = core.getInput as jest.MockedFunction<
|
|
typeof core.getInput
|
|
>;
|
|
const mockWarning = core.warning as jest.MockedFunction<
|
|
typeof core.warning
|
|
>;
|
|
mockGetInput.mockImplementation(name =>
|
|
name === 'server-username' ? 'LEGACY_USERNAME' : ''
|
|
);
|
|
|
|
expect(
|
|
auth.getInputWithDeprecatedAlias(
|
|
'server-username-env-var',
|
|
'server-username',
|
|
'GITHUB_ACTOR'
|
|
)
|
|
).toBe('LEGACY_USERNAME');
|
|
expect(mockWarning).toHaveBeenCalledWith(
|
|
"The 'server-username' input is deprecated and may be removed in a future release. Please use 'server-username-env-var' instead."
|
|
);
|
|
|
|
mockGetInput.mockReset();
|
|
mockWarning.mockReset();
|
|
});
|
|
|
|
it('prefers the replacement input over its deprecated alias', () => {
|
|
const mockGetInput = core.getInput as jest.MockedFunction<
|
|
typeof core.getInput
|
|
>;
|
|
mockGetInput.mockImplementation(name => {
|
|
const inputs: Record<string, string> = {
|
|
'server-password-env-var': 'NEW_PASSWORD',
|
|
'server-password': 'LEGACY_PASSWORD'
|
|
};
|
|
return inputs[name] || '';
|
|
});
|
|
|
|
expect(
|
|
auth.getInputWithDeprecatedAlias(
|
|
'server-password-env-var',
|
|
'server-password',
|
|
'GITHUB_TOKEN'
|
|
)
|
|
).toBe('NEW_PASSWORD');
|
|
expect(core.warning).toHaveBeenCalled();
|
|
|
|
mockGetInput.mockReset();
|
|
(core.warning as jest.Mock).mockReset();
|
|
});
|
|
});
|