From 72f11f320c9539282de64ad4af8d2c18db4fc8a1 Mon Sep 17 00:00:00 2001 From: Vlad Mocanu Date: Wed, 25 Feb 2026 16:03:51 +0200 Subject: [PATCH] feat(prd-1): implement M1 - platform detection and action.yml modifications - Add src/platform/detector.ts with 3-tier detection (explicit input, FORGEJO env var, GITHUB_SERVER_URL heuristic) - Add 'platform' input to action.yml (auto/github/forgejo) - Add 'Detect Platform' shell step as first composite action step - Split bun installation: oven-sh/setup-bun for GitHub, curl fallback for Forgejo runners - Condition 'Revoke app token' step to skip on Forgejo - Pass resolved PLATFORM env var to run step - Add 14 unit tests covering all detection paths Progress: M1 complete (1/9 milestones) - M2 and M5 now unblocked Co-Authored-By: Claude Opus 4.6 --- action.yml | 43 +++++++++++++- prds/1-forgejo-platform-support.md | 2 +- src/platform/detector.ts | 29 +++++++++ test/platform-detector.test.ts | 94 ++++++++++++++++++++++++++++++ 4 files changed, 165 insertions(+), 3 deletions(-) create mode 100644 src/platform/detector.ts create mode 100644 test/platform-detector.test.ts diff --git a/action.yml b/action.yml index 34633cf..df95004 100644 --- a/action.yml +++ b/action.yml @@ -138,6 +138,12 @@ inputs: required: false default: "" + # Platform configuration + platform: + description: "Platform to run on: 'auto' (default auto-detects), 'github', or 'forgejo'" + required: false + default: "auto" + outputs: execution_file: description: "Path to the Claude Code execution output file" @@ -158,13 +164,43 @@ outputs: runs: using: "composite" steps: + - name: Detect Platform + id: detect-platform + shell: bash + run: | + if [ "${{ inputs.platform }}" = "forgejo" ]; then + DETECTED="forgejo" + elif [ "${{ inputs.platform }}" = "github" ]; then + DETECTED="github" + elif [ "${FORGEJO}" = "true" ]; then + DETECTED="forgejo" + elif [ -n "${GITHUB_SERVER_URL}" ] && echo "${GITHUB_SERVER_URL}" | grep -qv "github.com"; then + DETECTED="forgejo" + else + DETECTED="github" + fi + echo "platform=${DETECTED}" >> "$GITHUB_OUTPUT" + echo "Detected platform: ${DETECTED}" + - name: Install Bun - if: inputs.path_to_bun_executable == '' + if: inputs.path_to_bun_executable == '' && steps.detect-platform.outputs.platform != 'forgejo' uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # https://github.com/oven-sh/setup-bun/releases/tag/v2.1.2 with: bun-version: 1.3.6 token: ${{ inputs.github_token || github.token }} + - name: Install Bun (Forgejo) + if: inputs.path_to_bun_executable == '' && steps.detect-platform.outputs.platform == 'forgejo' + shell: bash + run: | + if command -v bun &> /dev/null; then + echo "Bun is already installed: $(bun --version)" + else + echo "Installing Bun via curl..." + curl -fsSL https://bun.sh/install | bash + echo "$HOME/.bun/bin" >> "$GITHUB_PATH" + fi + - name: Setup Custom Bun Path if: inputs.path_to_bun_executable != '' shell: bash @@ -188,6 +224,9 @@ runs: run: | bun run ${GITHUB_ACTION_PATH}/src/entrypoints/run.ts env: + # Platform detection + PLATFORM: ${{ steps.detect-platform.outputs.platform }} + # Prepare inputs MODE: ${{ inputs.mode }} PROMPT: ${{ inputs.prompt }} @@ -283,7 +322,7 @@ runs: bun run ${GITHUB_ACTION_PATH}/src/entrypoints/cleanup-ssh-signing.ts - name: Revoke app token - if: always() && inputs.github_token == '' && steps.run.outputs.skipped_due_to_workflow_validation_mismatch != 'true' + if: always() && inputs.github_token == '' && steps.run.outputs.skipped_due_to_workflow_validation_mismatch != 'true' && steps.detect-platform.outputs.platform != 'forgejo' shell: bash run: | curl -L \ diff --git a/prds/1-forgejo-platform-support.md b/prds/1-forgejo-platform-support.md index 0cb050d..fc00cb0 100644 --- a/prds/1-forgejo-platform-support.md +++ b/prds/1-forgejo-platform-support.md @@ -228,7 +228,7 @@ The upstream repo has 13 workflows in `.github/workflows/`. Strategy: ### Phase 1 - Foundation (parallel, no dependencies) -- [ ] **M1: Platform detection + `action.yml` modifications** — Add `platform` input to `action.yml`. Create `src/platform/detector.ts` with explicit input + env-var auto-detection. Add conditional logic to skip "Revoke app token" step on Forgejo. Fix bun installation to handle Forgejo runners (direct curl fallback). +- [x] **M1: Platform detection + `action.yml` modifications** — Add `platform` input to `action.yml`. Create `src/platform/detector.ts` with explicit input + env-var auto-detection. Add conditional logic to skip "Revoke app token" step on Forgejo. Fix bun installation to handle Forgejo runners (direct curl fallback). - Files: `action.yml` (MODIFIED), `src/platform/detector.ts` (NEW) - Success: `detectPlatform()` returns "forgejo" in Forgejo runner, "github" in GitHub runner. `action.yml` doesn't error on Forgejo. diff --git a/src/platform/detector.ts b/src/platform/detector.ts new file mode 100644 index 0000000..a40a30f --- /dev/null +++ b/src/platform/detector.ts @@ -0,0 +1,29 @@ +/** + * Platform detection for GitHub vs Forgejo environments. + * + * Detection priority: + * 1. PLATFORM env var (set by action.yml detect step or explicit input) + * 2. FORGEJO env var (set by Forgejo runner v7.0+) + * 3. GITHUB_SERVER_URL heuristic (non-github.com = Forgejo) + * 4. Default to GitHub + */ + +export type Platform = "github" | "forgejo"; + +export function detectPlatform(): Platform { + // 1. Explicit platform from action.yml detect step or user input + const platformEnv = process.env.PLATFORM; + if (platformEnv === "forgejo") return "forgejo"; + if (platformEnv === "github") return "github"; + + // 2. FORGEJO env var (set by Forgejo runner v7.0+) + if (process.env.FORGEJO === "true") return "forgejo"; + + // 3. GITHUB_SERVER_URL heuristic — if not github.com, likely Forgejo/Gitea + const serverUrl = process.env.GITHUB_SERVER_URL || ""; + if (serverUrl && !serverUrl.includes("github.com")) { + return "forgejo"; + } + + return "github"; +} diff --git a/test/platform-detector.test.ts b/test/platform-detector.test.ts new file mode 100644 index 0000000..961dc5b --- /dev/null +++ b/test/platform-detector.test.ts @@ -0,0 +1,94 @@ +import { detectPlatform } from "../src/platform/detector"; +import { describe, it, expect, beforeEach } from "bun:test"; + +describe("detectPlatform", () => { + beforeEach(() => { + // Reset relevant env vars before each test + delete process.env.PLATFORM; + delete process.env.FORGEJO; + delete process.env.GITHUB_SERVER_URL; + }); + + // Restore original env after all tests in this file + // (bun:test doesn't have afterAll cleanup issues like jest, but let's be safe) + + describe("explicit PLATFORM env var", () => { + it("should return 'forgejo' when PLATFORM is 'forgejo'", () => { + process.env.PLATFORM = "forgejo"; + expect(detectPlatform()).toBe("forgejo"); + }); + + it("should return 'github' when PLATFORM is 'github'", () => { + process.env.PLATFORM = "github"; + expect(detectPlatform()).toBe("github"); + }); + + it("should take precedence over FORGEJO env var", () => { + process.env.PLATFORM = "github"; + process.env.FORGEJO = "true"; + expect(detectPlatform()).toBe("github"); + }); + + it("should take precedence over GITHUB_SERVER_URL", () => { + process.env.PLATFORM = "github"; + process.env.GITHUB_SERVER_URL = "https://git.example.com"; + expect(detectPlatform()).toBe("github"); + }); + + it("should ignore unrecognized PLATFORM values and fall through", () => { + process.env.PLATFORM = "gitlab"; + // No other env vars set, should default to github + expect(detectPlatform()).toBe("github"); + }); + }); + + describe("FORGEJO env var", () => { + it("should return 'forgejo' when FORGEJO is 'true'", () => { + process.env.FORGEJO = "true"; + expect(detectPlatform()).toBe("forgejo"); + }); + + it("should not detect forgejo when FORGEJO is 'false'", () => { + process.env.FORGEJO = "false"; + expect(detectPlatform()).toBe("github"); + }); + + it("should not detect forgejo when FORGEJO is empty", () => { + process.env.FORGEJO = ""; + expect(detectPlatform()).toBe("github"); + }); + }); + + describe("GITHUB_SERVER_URL heuristic", () => { + it("should return 'forgejo' for non-github.com server URLs", () => { + process.env.GITHUB_SERVER_URL = "https://git.wxs.ro"; + expect(detectPlatform()).toBe("forgejo"); + }); + + it("should return 'forgejo' for gitea/forgejo instances", () => { + process.env.GITHUB_SERVER_URL = "https://forgejo.example.org"; + expect(detectPlatform()).toBe("forgejo"); + }); + + it("should return 'github' for github.com", () => { + process.env.GITHUB_SERVER_URL = "https://github.com"; + expect(detectPlatform()).toBe("github"); + }); + + it("should return 'github' for GitHub Enterprise (contains github.com)", () => { + process.env.GITHUB_SERVER_URL = "https://github.com/enterprise"; + expect(detectPlatform()).toBe("github"); + }); + + it("should return 'github' when GITHUB_SERVER_URL is empty", () => { + process.env.GITHUB_SERVER_URL = ""; + expect(detectPlatform()).toBe("github"); + }); + }); + + describe("default behavior", () => { + it("should return 'github' when no env vars are set", () => { + expect(detectPlatform()).toBe("github"); + }); + }); +});