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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ ACTIONS_STEP_DEBUG=true
################################################################################

INPUT_LCOV-FILE-PATHS=coverage/lcov.info
# INPUT_REGION=eu
# INPUT_FAIL-ON-ERROR=true

################################################################################
Expand Down
104 changes: 91 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,59 @@ on:
push: {}

jobs:
build-and-test:
format:
runs-on: open-source-releaser
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: npm

- run: npm ci
- run: npm run format:check

lint:
runs-on: open-source-releaser
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: npm

- run: npm ci
- run: npm run lint

test:
runs-on: open-source-releaser
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: npm

- run: npm ci
- run: npm test

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: coverage
path: coverage/lcov.info
if-no-files-found: error

test-integration:
runs-on: open-source-releaser
permissions:
packages: write
contents: write
id-token: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
Expand All @@ -21,18 +68,49 @@ jobs:
cache: npm

- run: npm ci
- run: npm run test:integration

- name: Check formatting
run: npm run format:check
build:
runs-on: open-source-releaser
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- name: Lint
run: npm run lint
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: npm

- run: npm ci
- run: npm run build

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dist
path: dist/
if-no-files-found: error

test-action:
needs: [test, build]
runs-on: open-source-releaser
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- name: Test
run: npm test
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: coverage
path: coverage/

- name: Build
run: npm run build
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: dist
path: dist/

- name: Test Aikido Upload Code Coverage action in workflow with OIDC
uses: ./
Expand Down
18 changes: 10 additions & 8 deletions README.dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@ npm install

## npm scripts

| Script | Description |
| ---------------- | -------------------------------------------------- |
| `npm test` | Run unit tests with Jest |
| `npm run lint` | Lint `src/` and `__tests__/` with ESLint |
| `npm run format` | Format JavaScript files with Prettier |
| `npm run build` | Bundle `src/main.js` into `dist/index.js` with ncc |
| `npm run local` | Run the action locally via `@github/local-action` |
| `npm run all` | Format, lint, test, and build in one command |
| Script | Description |
| ------------------ | -------------------------------------------------- |
| `npm test` | Run unit and e2e tests with Jest |
| `npm run test:e2e` | Run e2e/integration tests only |
| `npm run lint` | Lint `src/` and `__tests__/` with ESLint |
| `npm run format` | Format JavaScript files with Prettier |
| `npm run build` | Bundle `src/main.js` into `dist/index.js` with ncc |
| `npm run local` | Run the action locally via `@github/local-action` |
| `npm run all` | Format, lint, test, and build in one command |

Before opening a pull request, run the full check:

Expand Down Expand Up @@ -57,6 +58,7 @@ GitHub Actions inputs are exposed as environment variables with an `INPUT_` pref
| Variable | Required | Description |
| ----------------------- | -------- | -------------------------------------------------- |
| `INPUT_LCOV-FILE-PATHS` | yes | Path(s) to LCOV file(s), e.g. `coverage/lcov.info` |
| `INPUT_REGION` | no | `eu` (default), `us`, `au`, or `us-gov` |
| `INPUT_FAIL-ON-ERROR` | no | Defaults to `true` |

The published action authenticates with GitHub OIDC (`core.getIDToken`). That only works
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,22 @@ the matrix test jobs.
| Input | Required | Default | Description |
| ----------------- | -------- | ------- | ------------------------------------------------------------------------------------- |
| `lcov-file-paths` | yes | — | Path(s) to the LCOV report file(s). |
| `region` | no | `eu` | Aikido region for upload and OIDC audience: `eu`, `us`, `au`, or `us-gov`. |
| `fail-on-error` | no | `true` | Fail the action if reading or upload fails. Set to `false` to emit a warning instead. |

### Region

Set `region` to match your Aikido workspace. The value selects both the API host and the OIDC
token audience.

```yaml
- name: Upload coverage to Aikido
uses: AikidoSec/code-coverage-github-action@v1
with:
lcov-file-paths: coverage/lcov.info
region: us
```

## Authentication

The action authenticates with GitHub OIDC. The workflow job must grant `id-token: write`
Expand Down
43 changes: 42 additions & 1 deletion __tests__/aikido.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jest.unstable_mockModule('@actions/http-client', () => ({
},
}));

const { getAuthHeaders, uploadCoverage } = await import('../src/aikido.js');
const { getAuthHeaders, getBaseUrl, uploadCoverage } = await import('../src/aikido.js');

function mockResponse(statusCode, rawBody = '') {
return {
Expand All @@ -31,6 +31,34 @@ function decodeCoverageContent(encoded) {
return gunzipSync(Buffer.from(encoded, 'base64')).toString('utf8');
}

describe('getBaseUrl', () => {
beforeEach(() => {
delete process.env.DEVELOPMENT;
});

it.each([
['', 'https://bg.aikido.dev'],
['eu', 'https://bg.aikido.dev'],
['EU', 'https://bg.aikido.dev'],
['us', 'https://bg.us.aikido.dev'],
['au', 'https://bg.au.aikido.dev'],
['us-gov', 'https://bg.aikidogov.us'],
])('maps region %j to %s', (region, url) => {
expect(getBaseUrl(region)).toBe(url);
});

it('throws for an unknown region', () => {
expect(() => getBaseUrl('mars')).toThrow(
'Unknown region "mars". Supported regions: eu, us, au, us-gov',
);
});

it('uses the development URL when DEVELOPMENT is set', () => {
process.env.DEVELOPMENT = 'true';
expect(getBaseUrl('us')).toBe('https://app.test.aikido.dev');
});
});

describe('getAuthHeaders', () => {
beforeEach(() => {
delete process.env.DEVELOPMENT;
Expand All @@ -48,13 +76,26 @@ describe('getAuthHeaders', () => {
expect(mockSetSecret).toHaveBeenCalledWith('oidc-jwt');
});

it('uses the region base URL as the OIDC audience', async () => {
mockGetIDToken.mockResolvedValue('oidc-jwt');

await getAuthHeaders('us');

expect(mockGetIDToken).toHaveBeenCalledWith('https://bg.us.aikido.dev');
});

it('throws a friendly error when OIDC is unavailable', async () => {
mockGetIDToken.mockRejectedValue(new Error('OIDC not available'));

await expect(getAuthHeaders()).rejects.toThrow(
'This action uses OIDC to authenticate with Aikido. Add to your workflow job:\n permissions:\n id-token: write',
);
});

it('rethrows unknown region errors', async () => {
await expect(getAuthHeaders('mars')).rejects.toThrow('Unknown region "mars"');
expect(mockGetIDToken).not.toHaveBeenCalled();
});
});

describe('uploadCoverage', () => {
Expand Down
36 changes: 34 additions & 2 deletions __tests__/inputs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,60 @@ const { readInputs } = await import('../src/inputs.js');

describe('readInputs', () => {
beforeEach(() => {
mockGetInput.mockReturnValue('coverage/lcov.info');
mockGetInput.mockImplementation((name) => {
if (name === 'lcov-file-paths') {
return 'coverage/lcov.info';
}
if (name === 'region') {
return '';
}
return '';
});
mockGetBooleanInput.mockReturnValue(true);
});

it('reads action inputs', () => {
expect(readInputs()).toEqual({
lcovFilePaths: ['coverage/lcov.info'],
failOnError: true,
region: 'eu',
});
expect(mockGetInput).toHaveBeenCalledWith('lcov-file-paths', {
required: true,
trimWhitespace: true,
});
expect(mockGetInput).toHaveBeenCalledWith('region', {
required: false,
trimWhitespace: true,
});
expect(mockGetBooleanInput).toHaveBeenCalledWith('fail-on-error');
});

it('reads an explicit region', () => {
mockGetInput.mockImplementation((name) => {
if (name === 'lcov-file-paths') {
return 'coverage/lcov.info';
}
if (name === 'region') {
return 'us';
}
return '';
});

expect(readInputs().region).toBe('us');
});

it.each([
['newlines', 'packages/a/coverage/lcov.info\npackages/b/coverage/lcov.info'],
['commas', 'packages/a/coverage/lcov.info,packages/b/coverage/lcov.info'],
['spaces', 'packages/a/coverage/lcov.info packages/b/coverage/lcov.info'],
])('splits lcov paths on %s', (_label, input) => {
mockGetInput.mockReturnValue(input);
mockGetInput.mockImplementation((name) => {
if (name === 'lcov-file-paths') {
return input;
}
return '';
});

expect(readInputs().lcovFilePaths).toEqual([
'packages/a/coverage/lcov.info',
Expand Down
Loading