diff --git a/src/artifact-permissions.test.ts b/src/artifact-permissions.test.ts index 014a44a3b..26b8ca373 100644 --- a/src/artifact-permissions.test.ts +++ b/src/artifact-permissions.test.ts @@ -72,6 +72,25 @@ describe('artifact-permissions', () => { } }); + it('logs stdout when stderr is empty and command writes only to stdout', () => { + const auditDir = makeTempDir(); + let warnSpy: jest.SpyInstance | undefined; + try { + getuidSpy = jest.spyOn(process, 'getuid').mockReturnValue(1001); + warnSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + mockExecaSync.mockReturnValue({ + stdout: '[entrypoint] Timed out waiting for iptables init container after 30s', + stderr: '', + exitCode: 1, + }); + fixArtifactPermissionsForRootless([auditDir], undefined, undefined, undefined, undefined); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Timed out waiting for iptables init container')); + } finally { + warnSpy?.mockRestore(); + fs.rmSync(auditDir, { recursive: true, force: true }); + } + }); + it('does not warn for benign permission errors on restricted runners', () => { const auditDir = makeTempDir(); let errorSpy: jest.SpyInstance | undefined; @@ -113,9 +132,13 @@ describe('artifact-permissions', () => { 'run', '--pull', 'never', + '--entrypoint', + 'sh', '-v', `/host${path.resolve(auditDir)}:/fix:rw`, 'ghcr.io/github/gh-aw-firewall/agent:latest', + '-c', + 'chown -R "$TUID:$TGID" /fix 2>/dev/null; chmod -R a+rwX /fix', ]), expect.objectContaining({ reject: false }), ); diff --git a/src/artifact-permissions.ts b/src/artifact-permissions.ts index de82d34a6..4b0aea4c4 100644 --- a/src/artifact-permissions.ts +++ b/src/artifact-permissions.ts @@ -64,6 +64,8 @@ export function fixArtifactPermissionsForRootless( 'DAC_OVERRIDE', '--cap-add', 'FOWNER', + '--entrypoint', + 'sh', '-e', `TUID=${uid}`, '-e', @@ -71,7 +73,6 @@ export function fixArtifactPermissionsForRootless( '-v', mount, imageRef, - 'sh', '-c', 'chown -R "$TUID:$TGID" /fix 2>/dev/null; chmod -R a+rwX /fix', ], @@ -80,6 +81,8 @@ export function fixArtifactPermissionsForRootless( if (typeof result.exitCode === 'number' && result.exitCode !== 0) { const stderr = result.stderr?.trim(); + const stdout = result.stdout?.trim(); + const errorDetail = stderr || stdout; // Ownership/permission repair is best-effort: the agent has already // finished and its artifacts are still readable by the owning user. // On rootless or restricted runners (e.g. ARC/DinD with a non-root @@ -88,8 +91,8 @@ export function fixArtifactPermissionsForRootless( // expected and non-fatal, so log them at debug to avoid alarming users // who otherwise see a scary WARN for a benign, non-blocking condition. const isBenignPermissionError = - !!stderr && /(?:^|\n)(?:chown|chmod):.*(?:operation not permitted|permission denied|EPERM|EACCES)/i.test(stderr); - const detail = `for ${dir} (exit ${result.exitCode})` + (stderr ? `: ${stderr}` : ''); + !!errorDetail && /(?:^|\n)(?:chown|chmod):.*(?:operation not permitted|permission denied|EPERM|EACCES)/i.test(errorDetail); + const detail = `for ${dir} (exit ${result.exitCode})` + (errorDetail ? `: ${errorDetail}` : ''); if (isBenignPermissionError) { logger.debug( `Rootless artifact permission repair skipped ${detail}. ` + diff --git a/src/artifact-preservation-errors.test.ts b/src/artifact-preservation-errors.test.ts index 078d7eba2..4587b3d67 100644 --- a/src/artifact-preservation-errors.test.ts +++ b/src/artifact-preservation-errors.test.ts @@ -198,9 +198,13 @@ describe('artifact-preservation – error paths', () => { 'run', '--pull', 'never', + '--entrypoint', + 'sh', '-v', `/host${path.resolve(auditDir)}:/fix:rw`, 'ghcr.io/github/gh-aw-firewall/agent:latest', + '-c', + 'chown -R "$TUID:$TGID" /fix 2>/dev/null; chmod -R a+rwX /fix', ]), expect.objectContaining({ reject: false }), );