Import Maven signing keys into an isolated GPG home (#1214) · actions/setup-java@634b0f0 · GitHub
Skip to content

Commit 634b0f0

Browse files
jduboisCopilotbrunoborges
authored
Import Maven signing keys into an isolated GPG home (#1214)
* Isolate Maven signing keys Import signing keys into an action-owned temporary GPG home, export GNUPGHOME, and remove the owned directory in the post action. Cover import failure, multiple keys and invocations, unrelated keyrings, missing state, and Windows path conversion. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Fix cleanup state assertion Account for isolated GPG-home cleanup when cache saving is disabled. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Update generated action bundles Apply repository formatting and commit the setup and cleanup bundles produced by the validated Node 24 build. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Address isolated GPG home review feedback Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Bruno Borges <brborges@microsoft.com>
1 parent f4bfb3d commit 634b0f0

17 files changed

Lines changed: 705 additions & 299 deletions

‎README.md‎

Lines changed: 1 addition & 1 deletion

‎__tests__/auth.test.ts‎

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,18 @@ jest.unstable_mockModule('@actions/core', () => ({
4141
toPosixPath: jest.fn((p: string) => p)
4242
}));
4343

44+
jest.unstable_mockModule('../src/gpg.js', () => ({
45+
importKey: jest.fn(),
46+
removeGpgHome: jest.fn(),
47+
toGpgPath: jest.fn()
48+
}));
49+
4450
// Dynamic imports after mocking
4551
const core = await import('@actions/core');
52+
const gpg = await import('../src/gpg.js');
4653
const auth = await import('../src/auth.js');
47-
const {M2_DIR, MVN_SETTINGS_FILE} = await import('../src/constants.js');
54+
const {M2_DIR, MVN_SETTINGS_FILE, STATE_GPG_HOME} =
55+
await import('../src/constants.js');
4856

4957
const __dirname = path.dirname(fileURLToPath(import.meta.url));
5058
const m2Dir = path.join(__dirname, M2_DIR);
@@ -60,8 +68,17 @@ describe('auth tests', () => {
6068
spyOSHomedir.mockReturnValue(__dirname);
6169
spyInfo = core.info as jest.Mock;
6270
spyInfo.mockImplementation(() => null);
71+
(gpg.toGpgPath as jest.Mock<any>).mockImplementation((p: string) => p);
6372
}, 300000);
6473

74+
afterEach(() => {
75+
(core.getInput as jest.Mock).mockReset();
76+
(core.exportVariable as jest.Mock).mockReset();
77+
(gpg.importKey as jest.Mock).mockReset();
78+
(gpg.removeGpgHome as jest.Mock).mockReset();
79+
(gpg.toGpgPath as jest.Mock).mockReset();
80+
});
81+
6582
afterAll(async () => {
6683
try {
6784
await io.rmRF(m2Dir);
@@ -144,6 +161,52 @@ describe('auth tests', () => {
144161
);
145162
}, 100000);
146163

164+
it('exports a GPG-compatible path and persists the native GPG home', async () => {
165+
const gpgHome = 'D:\\a\\_temp\\setup-java-gpg-1';
166+
const exportedGpgHome = '/d/a/_temp/setup-java-gpg-1';
167+
(gpg.importKey as jest.Mock<any>).mockResolvedValue(gpgHome);
168+
(gpg.toGpgPath as jest.Mock<any>).mockReturnValue(exportedGpgHome);
169+
(core.getInput as jest.Mock<any>).mockImplementation((name: string) => {
170+
const inputs: Record<string, string> = {
171+
'server-id': 'packages',
172+
'server-username-env-var': 'USERNAME',
173+
'server-password-env-var': 'PASSWORD',
174+
'settings-path': m2Dir,
175+
'gpg-private-key': 'KEY ONE\nKEY TWO'
176+
};
177+
return inputs[name] ?? '';
178+
});
179+
180+
await auth.configureAuthentication();
181+
182+
expect(gpg.importKey).toHaveBeenCalledWith('KEY ONE\nKEY TWO');
183+
expect(core.saveState).toHaveBeenCalledWith(STATE_GPG_HOME, gpgHome);
184+
expect(gpg.toGpgPath).toHaveBeenCalledWith(gpgHome);
185+
expect(core.exportVariable).toHaveBeenCalledWith(
186+
'GNUPGHOME',
187+
exportedGpgHome
188+
);
189+
});
190+
191+
it('removes the isolated GPG home when environment export fails', async () => {
192+
const gpgHome = path.join(__dirname, 'runner', 'temp', 'setup-java-gpg-2');
193+
(gpg.importKey as jest.Mock<any>).mockResolvedValue(gpgHome);
194+
(core.exportVariable as jest.Mock<any>).mockImplementation(() => {
195+
throw new Error('environment file unavailable');
196+
});
197+
(core.getInput as jest.Mock<any>).mockImplementation((name: string) => {
198+
if (name === 'gpg-private-key') return 'KEY CONTENTS';
199+
if (name === 'settings-path') return m2Dir;
200+
return '';
201+
});
202+
203+
await expect(auth.configureAuthentication()).rejects.toThrow(
204+
'environment file unavailable'
205+
);
206+
207+
expect(gpg.removeGpgHome).toHaveBeenCalledWith(gpgHome);
208+
});
209+
147210
it('overwrites existing settings.xml files', async () => {
148211
const id = 'packages';
149212
const username = 'USERNAME';

‎__tests__/cleanup-java.test.ts‎

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ const core = await import('@actions/core');
6363
const cache = await import('@actions/cache');
6464
const {run: cleanup} = await import('../src/cleanup-java.js');
6565
const util = await import('../src/util.js');
66+
const constants = await import('../src/constants.js');
67+
const {GPG_HOME_PREFIX} = await import('../src/gpg.js');
6668
const {registerJdk, buildJdkCacheKey} = await import('../src/jdk-cache.js');
6769

6870
const jdkTempRoots: string[] = [];
@@ -114,11 +116,65 @@ describe('cleanup', () => {
114116
(core.getInput as jest.Mock<any>).mockImplementation((name: string) => {
115117
return name === 'cache' ? 'gradle' : '';
116118
});
119+
117120
await cleanup();
118121
expect(spyCacheSave).toHaveBeenCalled();
119122
expect(spyWarning).not.toHaveBeenCalled();
120123
});
121124

125+
it('removes the isolated GPG home without touching unrelated key material', async () => {
126+
const tempDir = util.getTempDir();
127+
fs.mkdirSync(tempDir, {recursive: true});
128+
const gpgHome = fs.mkdtempSync(path.join(tempDir, GPG_HOME_PREFIX));
129+
const unrelatedGpgHome = fs.mkdtempSync(
130+
path.join(tempDir, 'user-gpg-home-')
131+
);
132+
fs.writeFileSync(
133+
path.join(unrelatedGpgHome, 'private.key'),
134+
'pre-existing'
135+
);
136+
(core.getInput as jest.Mock<any>).mockReturnValue('');
137+
(core.getState as jest.Mock<any>).mockImplementation((name: string) =>
138+
name === constants.STATE_GPG_HOME ? gpgHome : ''
139+
);
140+
141+
await cleanup();
142+
143+
expect(fs.existsSync(gpgHome)).toBe(false);
144+
expect(
145+
fs.readFileSync(path.join(unrelatedGpgHome, 'private.key'), 'utf8')
146+
).toBe('pre-existing');
147+
fs.rmSync(unrelatedGpgHome, {recursive: true, force: true});
148+
});
149+
150+
it('makes repeated cleanup of the same GPG home idempotent', async () => {
151+
const tempDir = util.getTempDir();
152+
fs.mkdirSync(tempDir, {recursive: true});
153+
const gpgHome = fs.mkdtempSync(path.join(tempDir, GPG_HOME_PREFIX));
154+
(core.getInput as jest.Mock<any>).mockReturnValue('');
155+
(core.getState as jest.Mock<any>).mockImplementation((name: string) =>
156+
name === constants.STATE_GPG_HOME ? gpgHome : ''
157+
);
158+
159+
await cleanup();
160+
await cleanup();
161+
162+
expect(fs.existsSync(gpgHome)).toBe(false);
163+
expect(core.setFailed).not.toHaveBeenCalled();
164+
});
165+
166+
it('skips GPG cleanup when no home was persisted', async () => {
167+
(core.getInput as jest.Mock<any>).mockReturnValue('');
168+
(core.getState as jest.Mock<any>).mockReturnValue('');
169+
170+
await cleanup();
171+
172+
expect(spyInfo).not.toHaveBeenCalledWith(
173+
'Removing private key from isolated GPG home'
174+
);
175+
expect(core.setFailed).not.toHaveBeenCalled();
176+
});
177+
122178
it('does not fail even though the save process throws error', async () => {
123179
spyCacheSave.mockImplementation((paths: string[], key: string) =>
124180
Promise.reject(new Error('Unexpected error'))
@@ -148,7 +204,8 @@ describe('cleanup', () => {
148204
await cleanup();
149205

150206
expect(spyCacheSave).not.toHaveBeenCalled();
151-
expect(core.getState).not.toHaveBeenCalled();
207+
expect(core.getState).toHaveBeenCalledTimes(1);
208+
expect(core.getState).toHaveBeenCalledWith(constants.STATE_GPG_HOME);
152209
expect(spyInfo).toHaveBeenCalledWith(
153210
'Cache saving is skipped because cache-read-only is enabled.'
154211
);

‎__tests__/distributors/microsoft-installer.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ jest.unstable_mockModule('../../src/util.js', () => ({
8686

8787
jest.unstable_mockModule('../../src/gpg.js', () => ({
8888
importKey: jest.fn(),
89-
deleteKey: jest.fn(),
89+
removeGpgHome: jest.fn(),
9090
verifyPackageSignature: jest.fn()
9191
}));
9292

‎__tests__/distributors/temurin-installer.test.ts‎

Lines changed: 1 addition & 1 deletion

0 commit comments

Comments
 (0)