Skip to content
Merged
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
758 changes: 3 additions & 755 deletions action.yml

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
"format": "biome check --write ."
},
"dependencies": {
"@actions/artifact": "^6.2.1",
"@actions/cache": "^6.0.0",
"@actions/core": "3.0.0",
"@actions/exec": "^3.0.0",
"@actions/tool-cache": "^4.0.0",
"@aws-sdk/client-secrets-manager": "3.972.0",
"@aws-sdk/credential-provider-web-identity": "3.972.0",
"@octokit/auth-app": "8.2.0",
Expand Down
1,124 changes: 1,124 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ describe('checkOrgMembership', () => {
Object.assign(new Error('Unauthorized'), { status: 401 }),
);

await expect(checkOrgMembership(ORG_TOKEN, ORG, USERNAME)).rejects.toThrow(/HTTP 401/);
const err = await checkOrgMembership(ORG_TOKEN, ORG, USERNAME).catch((e: unknown) => e);
expect(err).toBeInstanceOf(Error);
expect((err as Error).message).toMatch(/HTTP 401/);
expect((err as { status?: number }).status).toBe(401);
});

it('re-throws unexpected errors', async () => {
Expand Down
9 changes: 6 additions & 3 deletions src/check-org-membership/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@ export async function checkOrgMembership(
const status = (err as { status?: number }).status;
if (status === 404 || status === 302) return false;
if (status === 401) {
throw new Error(
'Org membership token is missing or invalid (HTTP 401). ' +
"Ensure the job has 'id-token: write' permission and OIDC is configured.",
throw Object.assign(
new Error(
'Org membership token is missing or invalid (HTTP 401). ' +
"Ensure the job has 'id-token: write' permission and OIDC is configured.",
),
{ status: 401 },
);
}
throw err;
Expand Down
128 changes: 128 additions & 0 deletions src/main/__tests__/artifact.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**
* Unit tests for src/main/artifact.ts
*
* Tests makeArtifactName (pure) and uploadVerboseLog (mocked DefaultArtifactClient).
* Uses real temp files to avoid mocking node:fs.
*/

import * as fsSync from 'node:fs';
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

vi.mock('@actions/core');

// ── Mock @actions/artifact ────────────────────────────────────────────────────

const { mockUploadArtifact, MockDefaultArtifactClient } = vi.hoisted(() => {
const mockUploadArtifact = vi.fn().mockResolvedValue({ id: 42 });
class MockDefaultArtifactClient {
uploadArtifact = mockUploadArtifact;
}
return { mockUploadArtifact, MockDefaultArtifactClient };
});

vi.mock('@actions/artifact', () => ({
DefaultArtifactClient: MockDefaultArtifactClient,
}));

import { makeArtifactName, uploadVerboseLog } from '../artifact.js';

// ── Helpers ─────────────────────────────────────────────────────────────────

let tmpDir: string;

beforeEach(async () => {
tmpDir = await mkdtemp(join(tmpdir(), 'artifact-test-'));
vi.clearAllMocks();
mockUploadArtifact.mockResolvedValue({ id: 42 });
});

afterEach(async () => {
await rm(tmpDir, { recursive: true, force: true });
});

// ── makeArtifactName ─────────────────────────────────────────────────────────

describe('makeArtifactName', () => {
it('builds the expected name from all components', () => {
const name = makeArtifactName('12345', '2', 'build', '/tmp/verbose-abc.log');
expect(name).toBe('docker-agent-verbose-log-12345-2-build-verbose-abc.log');
});

it('uses only the basename of the log file path', () => {
const name = makeArtifactName('1', '1', 'test', '/some/deep/path/to/logfile.txt');
expect(name).toBe('docker-agent-verbose-log-1-1-test-logfile.txt');
});

it('handles job names with hyphens', () => {
const name = makeArtifactName('99', '3', 'pr-review', '/tmp/verbose.log');
expect(name).toBe('docker-agent-verbose-log-99-3-pr-review-verbose.log');
});
});

// ── uploadVerboseLog ─────────────────────────────────────────────────────────

describe('uploadVerboseLog', () => {
it('uploads a real file successfully', async () => {
const filePath = join(tmpDir, 'verbose.log');
await writeFile(filePath, 'Agent output content', 'utf-8');

await uploadVerboseLog({ name: 'test-artifact', filePath, retentionDays: 7 });

expect(mockUploadArtifact).toHaveBeenCalledOnce();
expect(mockUploadArtifact).toHaveBeenCalledWith(
'test-artifact',
[filePath],
tmpDir, // rootDir = dirname(filePath)
{ retentionDays: 7 },
);
});

it('uses default retentionDays=14 when not specified', async () => {
const filePath = join(tmpDir, 'verbose.log');
await writeFile(filePath, 'content', 'utf-8');

await uploadVerboseLog({ name: 'test-artifact', filePath });

expect(mockUploadArtifact).toHaveBeenCalledWith(
expect.any(String),
expect.any(Array),
expect.any(String),
{ retentionDays: 14 },
);
});

it('warns and skips when file does not exist', async () => {
const { warning } = await import('@actions/core');
const filePath = join(tmpDir, 'nonexistent.log');

await uploadVerboseLog({ name: 'test-artifact', filePath });

expect(mockUploadArtifact).not.toHaveBeenCalled();
expect(vi.mocked(warning)).toHaveBeenCalledWith(expect.stringContaining('not found'));
});

it('warns and skips when path is a directory', async () => {
const { warning } = await import('@actions/core');
const dirPath = join(tmpDir, 'subdir');
fsSync.mkdirSync(dirPath);

await uploadVerboseLog({ name: 'test-artifact', filePath: dirPath });

expect(mockUploadArtifact).not.toHaveBeenCalled();
expect(vi.mocked(warning)).toHaveBeenCalledWith(expect.stringContaining('not a file'));
});

it('warns but does not throw when upload fails', async () => {
const { warning } = await import('@actions/core');
const filePath = join(tmpDir, 'verbose.log');
await writeFile(filePath, 'content', 'utf-8');

mockUploadArtifact.mockRejectedValue(new Error('Network timeout'));

await expect(uploadVerboseLog({ name: 'test-artifact', filePath })).resolves.toBeUndefined();
expect(vi.mocked(warning)).toHaveBeenCalledWith(expect.stringContaining('Network timeout'));
});
});
Loading
Loading