Skip to content
Open
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
46 changes: 46 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"permissions": {
"allow": [
"Bash(pwd)",
"Bash(ls*)",
"Bash(dir*)",
"Bash(Get-ChildItem*)",
"Bash(Get-Content*)",
"Bash(Select-String*)",
"Bash(Test-Path*)",
"Bash(npm install*)",
"Bash(npm ci*)",
"Bash(npm run *)",
"Bash(npm test*)",
"Bash(npx *)",
"Bash(node *)",
"Bash(git status*)",
"Bash(git diff*)",
"Bash(git log*)",
"Bash(git branch*)",
"Bash(Start-Sleep -Seconds 5)",
"Bash(powershell -Command \"docker ps\")"
],
"deny": [
"Bash(git commit*)",
"Bash(git push*)",
"Bash(git tag*)",
"Bash(git reset --hard*)",
"Bash(rm -rf*)",
"Bash(Remove-Item * -Recurse*)",
"Bash(del *)",
"Bash(rmdir *)"
],
"ask": [
"Bash(git add*)",
"Bash(git restore*)",
"Bash(git checkout*)",
"Bash(git stash*)",
"Bash(mkdir*)",
"Bash(New-Item*)",
"Bash(Copy-Item*)",
"Bash(Move-Item*)"
],
"defaultMode": "default"
}
}
16 changes: 16 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,19 @@ jobs:
uses: ./
with:
dry-run: true

docker-smoke:
name: Docker Smoke Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Build Docker image
run: docker build --no-cache -t github-release-action:test .

- name: Run smoke test
run: |
output="$(docker run --rm github-release-action:test --smoke-test)"
echo "$output"
echo "$output" | grep -qF "GITHUB_RELEASE_ACTION_RUNTIME=typescript-v1"
echo "$output" | grep -qF "TypeScript Docker runtime smoke test passed."
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,13 @@ yarn-error.log

# GitHub Actions cache
.github/workflows/node_modules/

# Claude Code local state (worktrees, shell snapshots, plans, session data)
.claude/worktrees/
.claude/shell-snapshots/
.claude/plans/
.claude/projects/
.claude/todos/
.claude/history/
.claude/*.local.json
.claude/settings.local.json
9 changes: 5 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Stage 1: Build
FROM node:24.16.0-alpine AS build
FROM node:24.17.0-alpine AS build

WORKDIR /app

Expand All @@ -14,6 +14,7 @@ COPY . .

# Compile TypeScript files
RUN npm run build
RUN test -f /app/dist/src/main.js

# Remove unnecessary files after build
RUN npm prune --production
Expand All @@ -24,7 +25,7 @@ RUN rm -rf ./src \
./tsconfig.prod.json

# Stage 2: Production
FROM node:24.16.0-alpine
FROM node:24.17.0-alpine

WORKDIR /app

Expand All @@ -38,5 +39,5 @@ RUN apk add --no-cache git jq curl
RUN chmod +x /app/scripts/*.sh
RUN chmod +x /app/dist/src/release.js

# Set the entrypoint script
ENTRYPOINT ["/bin/sh", "/app/scripts/entrypoint.sh"]
# Set the entrypoint to the compiled TypeScript runtime
ENTRYPOINT ["node", "/app/dist/src/main.js"]
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ inputs:
required: false
default: "auto"

outputs:
release-url:
description: "URL of the created GitHub release."

runs:
using: "docker"
image: "Dockerfile"
224 changes: 224 additions & 0 deletions src/__tests__/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import { describe, expect, test } from "@jest/globals";
import { readActionConfig } from "../config.js";

const BASE_ENV = {
"INPUT_GITHUB-TOKEN": "ghp_test_token",
GITHUB_SERVER_URL: "https://github.com",
GITHUB_API_URL: "https://api.github.com",
GITHUB_REPOSITORY: "owner/repo",
GITHUB_ACTOR: "octocat",
GITHUB_WORKSPACE: "/github/workspace",
};

describe("readActionConfig", () => {
describe("defaults", () => {
test("applies all default values when no optional inputs are set", () => {
const config = readActionConfig(BASE_ENV);
expect(config.dryRun).toBe(false);
expect(config.releaseDraft).toBe(false);
expect(config.releasePrerelease).toBe(false);
expect(config.releaseTitlePrefix).toBe("");
expect(config.tagTemplate).toBe("v<version>");
expect(config.changelogFilePath).toBe("CHANGELOG.md");
expect(config.versionOverride).toBeUndefined();
});

test("reads GitHub token from INPUT_GITHUB-TOKEN", () => {
const config = readActionConfig(BASE_ENV);
expect(config.githubToken).toBe("ghp_test_token");
});

test("falls back to GITHUB_TOKEN when INPUT_GITHUB-TOKEN is not set", () => {
const env = { ...BASE_ENV, GITHUB_TOKEN: "fallback_token" };
delete (env as Record<string, string | undefined>)["INPUT_GITHUB-TOKEN"];
const config = readActionConfig(env);
expect(config.githubToken).toBe("fallback_token");
});

test("throws when neither token env var is set", () => {
const env: Record<string, string> = { ...BASE_ENV };
delete (env as Record<string, string | undefined>)["INPUT_GITHUB-TOKEN"];
expect(() => readActionConfig(env)).toThrow(
"GITHUB_TOKEN is required but not set.",
);
});

test("throws when INPUT_GITHUB-TOKEN is empty and GITHUB_TOKEN is not set", () => {
const env = { ...BASE_ENV, "INPUT_GITHUB-TOKEN": "" };
expect(() => readActionConfig(env)).toThrow(
"GITHUB_TOKEN is required but not set.",
);
});
});

describe("ciWorkflows parsing", () => {
test("defaults to auto when INPUT_CI-WORKFLOWS is not set", () => {
const config = readActionConfig(BASE_ENV);
expect(config.ciWorkflows).toEqual({ mode: "auto" });
});

test("returns auto for empty string", () => {
const config = readActionConfig({
...BASE_ENV,
"INPUT_CI-WORKFLOWS": "",
});
expect(config.ciWorkflows).toEqual({ mode: "auto" });
});

test("returns auto for explicit 'auto' value", () => {
const config = readActionConfig({
...BASE_ENV,
"INPUT_CI-WORKFLOWS": "auto",
});
expect(config.ciWorkflows).toEqual({ mode: "auto" });
});

test("returns disabled for 'none'", () => {
const config = readActionConfig({
...BASE_ENV,
"INPUT_CI-WORKFLOWS": "none",
});
expect(config.ciWorkflows).toEqual({ mode: "disabled" });
});

test("returns disabled for 'false'", () => {
const config = readActionConfig({
...BASE_ENV,
"INPUT_CI-WORKFLOWS": "false",
});
expect(config.ciWorkflows).toEqual({ mode: "disabled" });
});

test("returns explicit list for comma-separated values", () => {
const config = readActionConfig({
...BASE_ENV,
"INPUT_CI-WORKFLOWS": "ci.yml,test.yml",
});
expect(config.ciWorkflows).toEqual({
mode: "explicit",
workflows: ["ci.yml", "test.yml"],
});
});

test("trims whitespace around workflow names", () => {
const config = readActionConfig({
...BASE_ENV,
"INPUT_CI-WORKFLOWS": " ci.yml , test.yml ",
});
expect(config.ciWorkflows).toEqual({
mode: "explicit",
workflows: ["ci.yml", "test.yml"],
});
});

test("filters empty entries from comma-separated list", () => {
const config = readActionConfig({
...BASE_ENV,
"INPUT_CI-WORKFLOWS": "ci.yml,,test.yml",
});
expect(config.ciWorkflows).toEqual({
mode: "explicit",
workflows: ["ci.yml", "test.yml"],
});
});
});

describe("boolean parsing", () => {
test("only exact 'true' enables dryRun", () => {
expect(
readActionConfig({ ...BASE_ENV, "INPUT_DRY-RUN": "true" }).dryRun,
).toBe(true);
expect(
readActionConfig({ ...BASE_ENV, "INPUT_DRY-RUN": "TRUE" }).dryRun,
).toBe(false);
expect(
readActionConfig({ ...BASE_ENV, "INPUT_DRY-RUN": "1" }).dryRun,
).toBe(false);
});

test("only exact 'true' enables releaseDraft", () => {
expect(
readActionConfig({ ...BASE_ENV, "INPUT_RELEASE-DRAFT": "true" })
.releaseDraft,
).toBe(true);
expect(
readActionConfig({ ...BASE_ENV, "INPUT_RELEASE-DRAFT": "TRUE" })
.releaseDraft,
).toBe(false);
});

test("only exact 'true' enables releasePrerelease", () => {
expect(
readActionConfig({ ...BASE_ENV, "INPUT_RELEASE-PRERELEASE": "true" })
.releasePrerelease,
).toBe(true);
expect(
readActionConfig({ ...BASE_ENV, "INPUT_RELEASE-PRERELEASE": "1" })
.releasePrerelease,
).toBe(false);
});
});

describe("GitHub Enterprise support", () => {
test("preserves custom server and API URLs", () => {
const config = readActionConfig({
...BASE_ENV,
GITHUB_SERVER_URL: "https://ghe.example.com",
GITHUB_API_URL: "https://ghe.example.com/api/v3",
});
expect(config.githubServerUrl).toBe("https://ghe.example.com");
expect(config.githubApiUrl).toBe("https://ghe.example.com/api/v3");
});

test("throws when GITHUB_SERVER_URL is missing", () => {
const env: Record<string, string> = { ...BASE_ENV };
delete (env as Record<string, string | undefined>).GITHUB_SERVER_URL;
expect(() => readActionConfig(env)).toThrow(
"GITHUB_SERVER_URL is required but not set.",
);
});

test("throws when GITHUB_API_URL is missing", () => {
const env: Record<string, string> = { ...BASE_ENV };
delete (env as Record<string, string | undefined>).GITHUB_API_URL;
expect(() => readActionConfig(env)).toThrow(
"GITHUB_API_URL is required but not set.",
);
});

test("preserves GITHUB_REPOSITORY", () => {
const config = readActionConfig({
...BASE_ENV,
GITHUB_REPOSITORY: "myorg/myrepo",
});
expect(config.githubRepository).toBe("myorg/myrepo");
});
});

describe("optional context fields", () => {
test("versionOverride is set when INPUT_VERSION is provided", () => {
const config = readActionConfig({
...BASE_ENV,
INPUT_VERSION: "2.3.4",
});
expect(config.versionOverride).toBe("2.3.4");
});

test("githubRefName and githubBaseRef are undefined when not set", () => {
const config = readActionConfig(BASE_ENV);
expect(config.githubRefName).toBeUndefined();
expect(config.githubBaseRef).toBeUndefined();
});

test("githubWorkflowRef is set when present", () => {
const config = readActionConfig({
...BASE_ENV,
GITHUB_WORKFLOW_REF:
"owner/repo/.github/workflows/release.yml@refs/heads/main",
});
expect(config.githubWorkflowRef).toBe(
"owner/repo/.github/workflows/release.yml@refs/heads/main",
);
});
});
});
Loading
Loading