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
9 changes: 9 additions & 0 deletions .github/release-please-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"release-type": "node",
"packages": {
".": {}
},
"bump-minor-pre-major": true,
"bump-patch-for-minor-pre-major": true
}
3 changes: 3 additions & 0 deletions .github/release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
".": "0.0.0"
}
99 changes: 99 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
name: Prepare Release

on:
push:
branches:
- main

permissions: {}

jobs:
release-please:
runs-on: ubuntu-latest
timeout-minutes: 15
environment: release
permissions:
contents: write
issues: write
pull-requests: write
outputs:
pr_branch: ${{ steps.pr-branch.outputs.branch }}
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write

- name: Run release-please action
id: release-please-action
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
with:
token: ${{ steps.app-token.outputs.token }}
config-file: .github/release-please-config.json
manifest-file: .github/release-please-manifest.json

- name: Extract PR branch
id: pr-branch
if: steps.release-please-action.outputs.pr
env:
PR_JSON: ${{ steps.release-please-action.outputs.pr }}
run: |
branch=$(echo "$PR_JSON" | jq -r '.headBranchName')
echo "branch=${branch}" >> $GITHUB_OUTPUT

build-action:
needs: release-please
if: needs.release-please.outputs.pr_branch
runs-on: ubuntu-latest
timeout-minutes: 15
environment: release
permissions:
contents: write
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write

- name: Checkout PR branch
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ needs.release-please.outputs.pr_branch }}
token: ${{ steps.app-token.outputs.token }}
persist-credentials: false

- name: Install pnpm

Check warning

Code scanning / CodeQL

Checkout of untrusted code in a trusted context Medium

Potential unsafe checkout of untrusted pull request on privileged workflow.
Comment thread
timokoessler marked this conversation as resolved.
Dismissed
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8

- name: Use Node.js 24.x
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24.x
cache: "pnpm"

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build
run: pnpm build

- name: Commit dist to PR branch
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
run: |
bot_id=$(gh api "/users/${APP_SLUG}[bot]" --jq .id)
git config user.name "${APP_SLUG}[bot]"
git config user.email "${bot_id}+${APP_SLUG}[bot]@users.noreply.github.com"
gh auth setup-git
git add dist/
git diff --staged --quiet || git commit -m "chore: build dist"
git push
43 changes: 43 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Tests

on:
push:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
pull-requests: read
id-token: write # For Codecov upload
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- name: Install pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8

- name: Use Node.js 24.x
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24.x
cache: "pnpm"

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Run tests
run: node --run test:coverage

- name: Upload coverage
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
use_oidc: true
fail_ci_if_error: true
files: ./lcov.info
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules/
lcov.info
7 changes: 6 additions & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,10 @@
"no-duplicate-imports": "error"
},
"ignorePatterns": ["dist/", "node_modules/"],
"overrides": [{ "files": ["**/*.test.ts"], "rules": { "no-floating-promises": "off" } }]
"overrides": [
{
"files": ["**/*.test.ts"],
"rules": { "no-floating-promises": "off", "require-await": "off" }
}
]
}
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
"format": "oxfmt --write .",
"check:types": "tsc --noEmit",
"check:format": "oxfmt --check .",
"build": "rolldown -c rolldown.config.ts"
"build": "rolldown -c rolldown.config.ts",
"test": "node --experimental-test-module-mocks --test src/**/*.test.ts",
"test:coverage": "node --experimental-test-module-mocks --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info --test src/**/*.test.ts"
},
"devDependencies": {
"@actions/core": "^3.0.1",
Expand Down
184 changes: 184 additions & 0 deletions src/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { after, beforeEach, describe, mock, test } from "node:test";
import assert from "node:assert/strict";

const getInputMock = mock.fn<(name: string, options?: { required?: boolean }) => string>();
const getBooleanInputMock = mock.fn<(name: string) => boolean>();

mock.module("@actions/core", {
exports: {
getInput: getInputMock,
getBooleanInput: getBooleanInputMock,
},
});

const { getConfig } = await import("./config.ts");

function setHub(value: string) {
getInputMock.mock.mockImplementation(() => value);
}

function setFlags(syncRepo: boolean, pullImages: boolean) {
getBooleanInputMock.mock.mockImplementation((name: string) => {
if (name === "syncRepo") {
return syncRepo;
}
if (name === "pullImages") {
return pullImages;
}
return false;
});
}

beforeEach(() => {
getInputMock.mock.resetCalls();
getBooleanInputMock.mock.resetCalls();
setHub("https://orca.example.com");
setFlags(true, false);
});

after(() => {
mock.restoreAll();
});

describe("getConfig()", () => {
describe("URL normalization: trailing slashes", () => {
test("strips a single trailing slash", () => {
setHub("https://orca.example.com/");
const result = getConfig();
assert.equal(result.hubUrl, "https://orca.example.com");
});

test("strips multiple trailing slashes", () => {
setHub("https://orca.example.com///");
const result = getConfig();
assert.equal(result.hubUrl, "https://orca.example.com");
});

test("leaves URL without trailing slash unchanged", () => {
setHub("https://orca.example.com");
const result = getConfig();
assert.equal(result.hubUrl, "https://orca.example.com");
});
});

describe("URL normalization: protocol", () => {
test("prepends https:// when no protocol is given", () => {
setHub("orca.example.com");
const result = getConfig();
assert.equal(result.hubUrl, "https://orca.example.com");
});

test("prepends https:// and strips trailing slash together", () => {
setHub("orca.example.com/");
const result = getConfig();
assert.equal(result.hubUrl, "https://orca.example.com");
});

test("preserves explicit https:// protocol", () => {
setHub("https://orca.example.com");
const result = getConfig();
assert.equal(result.hubUrl, "https://orca.example.com");
});

test("preserves explicit http:// protocol", () => {
setHub("http://orca.example.com");
const result = getConfig();
assert.equal(result.hubUrl, "http://orca.example.com");
});
});

describe("URL validation: path component", () => {
test("throws when URL contains a path component", () => {
setHub("https://orca.example.com/some/path");
assert.throws(
() => getConfig(),
(err: Error) => {
assert(err instanceof Error);
assert(err.message.includes("contains a path"));
return true;
},
);
});

test("throws when bare hostname has a path component", () => {
setHub("orca.example.com/path");
assert.throws(
() => getConfig(),
(err: Error) => {
assert(err instanceof Error);
assert(err.message.includes("contains a path"));
return true;
},
);
});
});

describe("URL validation: invalid URL", () => {
test("throws TypeError for a syntactically invalid URL", () => {
setHub("not a valid url with spaces");
assert.throws(() => getConfig(), TypeError);
});
});

describe("flag validation", () => {
test("throws when both syncRepo and pullImages are false", () => {
setFlags(false, false);
assert.throws(
() => getConfig(),
(err: Error) => {
assert(err instanceof Error);
assert.equal(err.message, "At least one of syncRepo or pullImages must be set to true.");
return true;
},
);
});

test("succeeds when only syncRepo is true", () => {
setFlags(true, false);
const result = getConfig();
assert.equal(result.syncRepo, true);
assert.equal(result.pullImages, false);
});

test("succeeds when only pullImages is true", () => {
setFlags(false, true);
const result = getConfig();
assert.equal(result.syncRepo, false);
assert.equal(result.pullImages, true);
});

test("succeeds when both are true", () => {
setFlags(true, true);
const result = getConfig();
assert.equal(result.syncRepo, true);
assert.equal(result.pullImages, true);
});
});

describe("return value shape", () => {
test("constructs endpoint from hubUrl", () => {
setHub("https://orca.example.com");
const result = getConfig();
assert.equal(result.endpoint, "https://orca.example.com/api/v1/github-actions");
});

test("endpoint uses the normalized hubUrl (no trailing slash)", () => {
setHub("orca.example.com/");
const result = getConfig();
assert.equal(result.endpoint, "https://orca.example.com/api/v1/github-actions");
});

test("calls getInput once with 'hub'", () => {
getConfig();
assert.equal(getInputMock.mock.callCount(), 1);
assert.equal(getInputMock.mock.calls[0]?.arguments[0], "hub");
});

test("calls getBooleanInput for 'syncRepo' and 'pullImages'", () => {
getConfig();
const names = getBooleanInputMock.mock.calls.map((c) => c.arguments[0]);
assert(names.includes("syncRepo"));
assert(names.includes("pullImages"));
});
});
});
Loading