From 421d59c59f4c2c8a614ffb9b66c9a3c4e92c0fa8 Mon Sep 17 00:00:00 2001 From: "forgejo-actions[bot]" Date: Mon, 16 Mar 2026 08:34:46 +0000 Subject: [PATCH] mirror: f2339f8 style: fix prettier formatting on upstream file --- .forgejo/kcl-ci/Taskfile.yaml | 16 - .forgejo/kcl-ci/kcl.mod | 7 - .forgejo/kcl-ci/kcl.mod.lock | 7 - .forgejo/kcl-ci/main.k | 693 ----------------------- .forgejo/workflows/ci.yml | 101 ---- .forgejo/workflows/claude.yml | 56 -- .forgejo/workflows/kcl-ci-build.yml | 31 - .forgejo/workflows/mirror-branch.yml | 60 -- .forgejo/workflows/test-action.yml | 74 --- .forgejo/workflows/upstream-sync.yml | 410 -------------- prds/done/1-forgejo-platform-support.md | 353 ------------ prds/done/5-dedicated-forgejo-user.md | 109 ---- prds/done/6-e2e-test-forgejo-triggers.md | 144 ----- prds/done/7-senior-review-forgejo.md | 130 ----- 14 files changed, 2191 deletions(-) delete mode 100644 .forgejo/kcl-ci/Taskfile.yaml delete mode 100644 .forgejo/kcl-ci/kcl.mod delete mode 100644 .forgejo/kcl-ci/kcl.mod.lock delete mode 100644 .forgejo/kcl-ci/main.k delete mode 100644 .forgejo/workflows/ci.yml delete mode 100644 .forgejo/workflows/claude.yml delete mode 100644 .forgejo/workflows/kcl-ci-build.yml delete mode 100644 .forgejo/workflows/mirror-branch.yml delete mode 100644 .forgejo/workflows/test-action.yml delete mode 100644 .forgejo/workflows/upstream-sync.yml delete mode 100644 prds/done/1-forgejo-platform-support.md delete mode 100644 prds/done/5-dedicated-forgejo-user.md delete mode 100644 prds/done/6-e2e-test-forgejo-triggers.md delete mode 100644 prds/done/7-senior-review-forgejo.md diff --git a/.forgejo/kcl-ci/Taskfile.yaml b/.forgejo/kcl-ci/Taskfile.yaml deleted file mode 100644 index 04d0e20..0000000 --- a/.forgejo/kcl-ci/Taskfile.yaml +++ /dev/null @@ -1,16 +0,0 @@ ---- -version: "3" - -vars: - TASK_REPO_LOCAL: ~/stuff/gitrepos/wxs/task - TASK_REPO_BASE: git.wxs.ro/wxs/task/raw/branch/main - -includes: - kcl: - taskfile: "{{if .CI}}https://token:{{.READ_REPO_TOKEN}}@{{.TASK_REPO_BASE}}/kcl-ci-consumer.yml{{else}}{{.TASK_REPO_LOCAL}}/kcl-ci-consumer.yml{{end}}" - -tasks: - default: - desc: Show available tasks - cmds: - - task --list diff --git a/.forgejo/kcl-ci/kcl.mod b/.forgejo/kcl-ci/kcl.mod deleted file mode 100644 index b358014..0000000 --- a/.forgejo/kcl-ci/kcl.mod +++ /dev/null @@ -1,7 +0,0 @@ -[package] -name = "claude_code_action_workflows" -edition = "v0.12.3" -version = "0.0.1" - -[dependencies] -kcl-ci = { git = "https://git.wxs.ro/actions/kcl-ci", tag = "v1.43.3" } diff --git a/.forgejo/kcl-ci/kcl.mod.lock b/.forgejo/kcl-ci/kcl.mod.lock deleted file mode 100644 index d0575bb..0000000 --- a/.forgejo/kcl-ci/kcl.mod.lock +++ /dev/null @@ -1,7 +0,0 @@ -[dependencies] - [dependencies.kcl-ci] - name = "kcl-ci" - full_name = "kcl-ci_0.0.1" - version = "0.0.1" - url = "https://git.wxs.ro/actions/kcl-ci" - git_tag = "v1.43.0" diff --git a/.forgejo/kcl-ci/main.k b/.forgejo/kcl-ci/main.k deleted file mode 100644 index 9e68513..0000000 --- a/.forgejo/kcl-ci/main.k +++ /dev/null @@ -1,693 +0,0 @@ -import kcl_ci.schemas.helpers as h -import kcl_ci.schemas.config as cfg -import kcl_ci.steps.checkout as checkout_mod -import kcl_ci.workflows.kcl_ci_build as kcl_build_mod -import kcl_ci.workflows.claude as claude_mod -import kcl_ci.workflows.mirror_branch as mirror_mod -import kcl_ci.versions - -# ============================================================================= -# CI workflow job name constants -# ============================================================================= -_CI_WORKFLOW_NAME = "CI" -_CI_JOB_TEST = "Tests" -_CI_JOB_PRETTIER = "Prettier" -_CI_JOB_TYPECHECK = "Type Check" - -# ============================================================================= -# Bun version pinned across all CI jobs -# Bun 1.3.x broke spyOn(fs, ...) in tests. -# Bump this when upstream tests are updated for newer Bun versions. -# ============================================================================= -_BUN_VERSION = "1.2.21" - -# Bun install step — checks installed version before reinstalling -_bun_install = lambda -> {str:} { - { - name = "Install Bun" - run = """\ -if command -v bun &> /dev/null; then - INSTALLED=${"$"}(bun --version) - if [ "${"$"}INSTALLED" = "${"$"}BUN_VERSION" ]; then - echo "Bun ${"$"}BUN_VERSION already installed" - else - echo "Bun ${"$"}INSTALLED installed, need ${"$"}BUN_VERSION — reinstalling" - curl -fsSL https://bun.sh/install | bash -s "bun-v${"$"}{BUN_VERSION}" - echo "${"$"}HOME/.bun/bin" >> "${"$"}GITHUB_PATH" - fi -else - curl -fsSL https://bun.sh/install | bash -s "bun-v${"$"}{BUN_VERSION}" - echo "${"$"}HOME/.bun/bin" >> "${"$"}GITHUB_PATH" -fi""" - } -} - -# bun install dependencies step -_bun_deps = lambda -> {str:} { - { - name = "Install dependencies" - run = "bun install" - } -} - -# ============================================================================= -# Claude Code workflow (tag mode — responds to @claude mentions) -# Uses the standard claude.k module from kcl-ci -# ============================================================================= -_claude_workflow = claude_mod.claude_workflow() - -# ============================================================================= -# KCL CI Build workflow (auto-regenerates workflows from KCL) -# Waits for CI checks before building -# ============================================================================= -_kcl_build_workflow = kcl_build_mod.kcl_ci_build_workflow() - -# ============================================================================= -# CI workflow (test, prettier, typecheck) -# ============================================================================= -_ci_workflow = { - name = _CI_WORKFLOW_NAME - on = { - push = { - branches = ["main"] - "paths-ignore" = cfg.KCL_CI_DEFAULT_PATHS_IGNORE - } - pull_request = { - branches = ["main"] - "paths-ignore" = cfg.KCL_CI_DEFAULT_PATHS_IGNORE - } - workflow_dispatch = {} - } - concurrency = { - group = "ci-" + h.gha_github("ref") - "cancel-in-progress" = True - } - env = { - BUN_VERSION = _BUN_VERSION - } - jobs = { - test = { - name = _CI_JOB_TEST - "runs-on" = "grunt" - steps = [ - checkout_mod.checkout() - _bun_install() - _bun_deps() - { - name = "Run tests" - # Unit tests assume GitHub context. Override Forgejo runner's - # GITHUB_SERVER_URL so module-level constants (config.ts) and - # detectPlatform() default to GitHub. Forgejo-specific tests - # explicitly set PLATFORM=forgejo in their own beforeEach. - # Inline export because Forgejo runner re-injects GITHUB_SERVER_URL - # after step-level env:, overriding it. - run = """\ -export GITHUB_SERVER_URL=https://github.com -bun test""" - } - ] - } - prettier = { - name = _CI_JOB_PRETTIER - "runs-on" = "grunt" - steps = [ - checkout_mod.checkout() - _bun_install() - _bun_deps() - { - name = "Run prettier check" - run = "bun run format:check" - } - ] - } - typecheck = { - name = _CI_JOB_TYPECHECK - "runs-on" = "grunt" - steps = [ - checkout_mod.checkout() - _bun_install() - _bun_deps() - { - name = "Run TypeScript type check" - run = "bun run typecheck" - } - ] - } - } -} - -# ============================================================================= -# Test Action workflow (integration test — agent mode on Forgejo) -# Uses ./ self-reference since this IS the claude-code-action repo -# ============================================================================= -_test_action_workflow = { - name = "Test Action (Forgejo)" - on = { - schedule = [ - # Weekly Monday at 06:00 UTC - {cron = "0 6 * * 1"} - ] - pull_request = { - branches = ["main"] - paths = ["src/**", "action.yml", ".forgejo/**", "base-action/**"] - } - workflow_dispatch = {} - } - concurrency = { - group = "test-action-" + h.gha_github("ref") - "cancel-in-progress" = True - } - jobs = { - "test-agent-mode" = { - name = "Test Agent Mode on Forgejo" - "runs-on" = "grunt" - # Skip claude bot on issue_comment/issues events (tag mode loop risk). - # Allow on pull_request events (e.g. upstream-sync PRs) — no loop risk there. - "if" = "github.actor != 'claude' || github.event_name == 'pull_request'" - steps = [ - checkout_mod.checkout() - { - name = "Run Claude Code Action (agent mode)" - id = "claude" - uses = "./" - "with" = { - prompt = "Respond with exactly the text FORGEJO_PLATFORM_TEST_OK and nothing else. Do not use any tools." - claude_code_oauth_token = h.gha_secret("CLAUDE_CODE_OAUTH_TOKEN") - github_token = h.gha_secret("FORGEJO_CLAUDE_TOKEN") - platform = "forgejo" - } - } - { - name = "Verify execution output exists" - run = """\ -EXEC_FILE="${h.gha_steps("claude", "execution_file")}" -if [ -z "${"$"}EXEC_FILE" ]; then - echo "FAIL: execution_file output is empty" - exit 1 -fi -if [ ! -f "${"$"}EXEC_FILE" ]; then - echo "FAIL: execution file does not exist at: ${"$"}EXEC_FILE" - exit 1 -fi -echo "OK: execution file exists at: ${"$"}EXEC_FILE" - -if grep -q "FORGEJO_PLATFORM_TEST_OK" "${"$"}EXEC_FILE"; then - echo "OK: Output contains expected test marker" -else - echo "WARN: Test marker not found in output (Claude may have formatted differently)" - echo "--- Output contents (first 50 lines) ---" - head -50 "${"$"}EXEC_FILE" -fi""" - } - { - name = "Debug on failure" - "if" = "failure()" - run = """\ -echo "=== Debug Info ===" -echo "Platform env: PLATFORM=${"$"}PLATFORM FORGEJO=${"$"}FORGEJO" -echo "Server URL: ${"$"}GITHUB_SERVER_URL" -echo "API URL: ${"$"}GITHUB_API_URL" -echo "Event: ${h.gha_github("event_name")}" -echo "Actor: ${h.gha_github("actor")}" -echo "" -EXEC_FILE="${h.gha_steps("claude", "execution_file")}" -if [ -f "${"$"}EXEC_FILE" ]; then - echo "--- Execution file contents ---" - cat "${"$"}EXEC_FILE" -else - echo "No execution file found" -fi""" - } - ] - } - } -} - -# ============================================================================= -# Upstream Sync Check workflow (weekly check for upstream updates) -# Detects upstream changes, creates/updates tracking issue, and triggers -# Claude to auto-merge and create a PR if changes are found. -# ============================================================================= - -# Claude action URL for agent mode (merge job) -_claude_action_url = h.gha_action_url("actions", "claude-code-action", versions.CLAUDE_CODE_ACTION_VERSION, "READ_REPO_TOKEN") - -# GHA expression refs for the merge prompt -_merge_branch_ref = h.gha_steps("prepare", "branch") -_merge_behind_ref = h.gha_needs("check-upstream", "behind") -_merge_ahead_ref = h.gha_needs("check-upstream", "ahead") -_merge_issue_ref = h.gha_needs("check-upstream", "issue_number") -_merge_existing_pr_ref = h.gha_steps("existing-pr", "pr_exists") -_merge_existing_pr_number_ref = h.gha_steps("existing-pr", "pr_number") -_merge_clean_ref = h.gha_steps("prepare", "merge_clean") -_merge_affected_ref = h.gha_steps("prepare", "affected_files") - -_merge_prompt = """\ -You are automating an upstream sync for our Forgejo fork of anthropics/claude-code-action. - -## CRITICAL: Do NOT stop early -You MUST complete ALL steps below before stopping. Resolving conflicts alone is NOT sufficient. -Your job is NOT done until you have pushed the branch AND created (or updated) the PR. -If you stop before pushing and creating the PR, the entire sync fails. - -## Current state -- Branch: ${_merge_branch_ref} -- ${_merge_behind_ref} commits behind upstream, ${_merge_ahead_ref} commits ahead (our Forgejo changes) -- Tracking issue: #${_merge_issue_ref} -- Existing PR: ${_merge_existing_pr_ref} (if true, PR #${_merge_existing_pr_number_ref} already exists — update it instead of creating a new one) -- Merge was attempted. Status: ${_merge_clean_ref} (true = clean, false = has conflicts) - -## Impact analysis -- ${"$"}FORK_MODIFIED_FILES — all files our fork has changed vs upstream -- ${"$"}AFFECTED_FORK_FILES — subset of fork files that upstream ALSO changed (overlap = conflict risk) -- If AFFECTED_FORK_FILES is empty, upstream only touched files we haven't modified — low risk, merge should be clean. -- If AFFECTED_FORK_FILES is non-empty, those files need careful review since both sides changed them. - -## If merge has conflicts (merge_clean=false) -Resolve all conflicts. For files in ${"$"}FORK_MODIFIED_FILES, our fork's changes take PRIORITY — they contain Forgejo-specific adaptations. - -## Review ALL upstream changes for fork compatibility -Read CLAUDE.md for context on our Forgejo adapter pattern and platform-specific code. -Even files we haven't modified can break our fork. Check upstream changes for: -- Renamed/moved imports or exports that our Forgejo-specific files depend on -- Changed function signatures, types, or interfaces used by our adapter code -- New assumptions (e.g. GitHub-only APIs) that don't hold on Forgejo -- Removed or restructured code that our platform branches reference -If you find incompatibilities, fix them to maintain Forgejo support. - -## Maintainability principle -When fixing incompatibilities, minimize fork divergence to keep future merges easy. -Prefer adapter patterns (separate Forgejo files) over inline modifications to upstream files. -The fewer upstream files we modify, the fewer conflicts in the next sync. - -## IMPORTANT: Command execution rules -- GITHUB_SERVER_URL=https://github.com is ALREADY exported in your environment. Run commands directly WITHOUT env var prefixes. - CORRECT: `bun test` - WRONG: `GITHUB_SERVER_URL=https://github.com bun test` (this will be blocked by permissions) -- Do NOT use `2>&1` redirections, subshells (`sh -c`), or `export` commands — they will be blocked. -- For inspecting env vars, use: `bun -e "console.log(process.env.VAR_NAME)"` -- Available commands: git, bun, bunx, npx, curl, echo, and anything in ./node_modules/.bin/ - -## Known pre-existing test failures -Some tests in comment-logic.test.ts and create-prompt.test.ts have pre-existing failures related to Forgejo URL patterns. -These are NOT caused by the upstream merge. When running `bun test`: -- If only comment-logic.test.ts and/or create-prompt.test.ts fail, that's expected — proceed. -- If OTHER test files fail that weren't failing before, investigate those as potential merge regressions. -- Do NOT spend time trying to fix pre-existing failures or trying different env var approaches. - -## After merge is resolved and reviewed -1. Run tests: `bun test` — see "Known pre-existing test failures" above -2. Run typecheck: `bun run typecheck` -3. Check formatting: `bun run format:check` — if issues, fix with `bun run format` -4. Commit any remaining changes and push the branch -5. Push the branch, then create or update the PR: - - If existing PR is "true": PR #${_merge_existing_pr_number_ref} already exists. Just push the branch — the PR updates automatically. - Optionally update the PR body via PATCH ${"$"}GITHUB_API_URL/repos/${"$"}GITHUB_REPOSITORY/pulls/${_merge_existing_pr_number_ref} - - If existing PR is "false": Create a new PR via POST ${"$"}GITHUB_API_URL/repos/${"$"}GITHUB_REPOSITORY/pulls - Auth: token ${"$"}GITHUB_TOKEN - Title: "chore: merge ${_merge_behind_ref} upstream commits from anthropics/claude-code-action" - Body should include: - - "Closes #${_merge_issue_ref}" on its own line (links PR to the tracking issue) - - Summary of notable upstream changes - - Impact assessment: which fork files (if any) were affected - - List of conflicts resolved (if any) - - If no fork files were affected, note that this is a low-risk merge""" - -_upstream_sync_workflow = { - name = "Upstream Sync Check" - on = { - schedule = [ - # Weekly Monday at 08:00 UTC - {cron = "0 8 * * 1"} - ] - workflow_dispatch = {} - } - env = { - BUN_VERSION = _BUN_VERSION - } - jobs = { - "check-upstream" = { - name = "Check for upstream updates" - "runs-on" = "grunt" - outputs = { - needs_sync = h.gha_steps("compare", "needs_sync") - behind = h.gha_steps("compare", "behind") - ahead = h.gha_steps("compare", "ahead") - issue_number = h.gha_steps("sync-issue", "issue_number") - } - steps = [ - checkout_mod.checkout() | {"with" = {"fetch-depth" = "0"}} - { - name = "Configure upstream remote" - run = """\ -if ! git remote get-url upstream &> /dev/null; then - git remote add upstream https://github.com/anthropics/claude-code-action.git -fi""" - } - { - name = "Fetch upstream" - run = "git fetch upstream main --quiet" - } - { - name = "Compare with upstream" - id = "compare" - run = """\ -BEHIND_COUNT=${"$"}(git rev-list main..upstream/main --count) -AHEAD_COUNT=${"$"}(git rev-list upstream/main..main --count) - -echo "behind=${"$"}BEHIND_COUNT" >> "${"$"}GITHUB_OUTPUT" -echo "ahead=${"$"}AHEAD_COUNT" >> "${"$"}GITHUB_OUTPUT" - -echo "Our main is ${"$"}BEHIND_COUNT commits behind and ${"$"}AHEAD_COUNT commits ahead of upstream" - -if [ "${"$"}BEHIND_COUNT" -gt 0 ]; then - echo "needs_sync=true" >> "${"$"}GITHUB_OUTPUT" - - echo "--- New upstream commits ---" - git log main..upstream/main --oneline | head -30 - - echo "" - echo "--- Files changed ---" - DIFF_STAT=${"$"}(git diff --stat main...upstream/main) - echo "${"$"}DIFF_STAT" - - { - echo "diff_stat<> "${"$"}GITHUB_OUTPUT" - - { - echo "commit_log<> "${"$"}GITHUB_OUTPUT" -else - echo "needs_sync=false" >> "${"$"}GITHUB_OUTPUT" - echo "Already up to date with upstream" -fi""" - } - { - name = "Ensure upstream-sync label exists" - "if" = h.gha_steps_eq("compare", "needs_sync", "true") - id = "label" - run = """\ -API="${h.gha_env("GITHUB_API_URL")}/repos/${h.gha_github("repository")}" -AUTH="Authorization: token ${h.gha_secret("CLAUDE_BOT_TOKEN")}" - -# Look up the label ID first (Forgejo needs integer IDs, not names) -LABEL_ID=${"$"}(curl -s -H "${"$"}AUTH" "${"$"}API/labels" | jq -r '[.[] | select(.name == "upstream-sync")] | .[0].id // ""') - -# Only create label if it doesn't exist yet -if [ -z "${"$"}LABEL_ID" ]; then - curl -s -o /dev/null \\ - -X POST -H "${"$"}AUTH" -H "Content-Type: application/json" \\ - -d '{"name":"upstream-sync","color":"#0075ca","description":"Upstream sync tracking"}' \\ - "${"$"}API/labels" - LABEL_ID=${"$"}(curl -s -H "${"$"}AUTH" "${"$"}API/labels" | jq -r '[.[] | select(.name == "upstream-sync")] | .[0].id // ""') - echo "Created upstream-sync label with ID: ${"$"}LABEL_ID" -else - echo "Found existing upstream-sync label ID: ${"$"}LABEL_ID" -fi - -echo "label_id=${"$"}LABEL_ID" >> "${"$"}GITHUB_OUTPUT" -""" - } - { - name = "Check for existing sync issue" - "if" = h.gha_steps_eq("compare", "needs_sync", "true") - id = "existing" - run = """\ -# Forgejo's labels filter requires integer IDs, not names -RESPONSE=${"$"}(curl -s \\ - -H "Authorization: token ${h.gha_secret("CLAUDE_BOT_TOKEN")}" \\ - -H "Content-Type: application/json" \\ - "${h.gha_env("GITHUB_API_URL")}/repos/${h.gha_github("repository")}/issues?state=open&labels=${h.gha_steps("label", "label_id")}&limit=1") - -ISSUE_COUNT=${"$"}(echo "${"$"}RESPONSE" | jq 'length') - -if [ "${"$"}ISSUE_COUNT" -gt 0 ]; then - ISSUE_NUMBER=${"$"}(echo "${"$"}RESPONSE" | jq '.[0].number') - echo "issue_exists=true" >> "${"$"}GITHUB_OUTPUT" - echo "issue_number=${"$"}ISSUE_NUMBER" >> "${"$"}GITHUB_OUTPUT" - echo "Found existing sync issue #${"$"}ISSUE_NUMBER" -else - echo "issue_exists=false" >> "${"$"}GITHUB_OUTPUT" - echo "No existing sync issue found" -fi""" - } - { - name = "Create or update sync issue" - "if" = h.gha_steps_eq("compare", "needs_sync", "true") - id = "sync-issue" - run = """\ -API="${h.gha_env("GITHUB_API_URL")}/repos/${h.gha_github("repository")}" -AUTH="Authorization: token ${h.gha_secret("CLAUDE_BOT_TOKEN")}" -LABEL_ID="${h.gha_steps("label", "label_id")}" - -BEHIND="${h.gha_steps("compare", "behind")}" -AHEAD="${h.gha_steps("compare", "ahead")}" -COMMIT_LOG=${"$"}(cat <<'LOG_EOF' -${h.gha_steps("compare", "commit_log")} -LOG_EOF -) -DIFF_STAT=${"$"}(cat <<'DIFF_EOF' -${h.gha_steps("compare", "diff_stat")} -DIFF_EOF -) - -BODY=${"$"}(cat <> "${"$"}GITHUB_OUTPUT" -else - # Forgejo expects labels as integer IDs, not objects with names - RESPONSE=${"$"}(curl -s \\ - -X POST -H "${"$"}AUTH" -H "Content-Type: application/json" \\ - -d "${"$"}(jq -n --arg title "${"$"}TITLE" --arg body "${"$"}BODY" --argjson labels "[${"$"}{LABEL_ID:-0}]" '{title: ${"$"}title, body: ${"$"}body, labels: ${"$"}labels}')" \\ - "${"$"}API/issues") - ISSUE_NUMBER=${"$"}(echo "${"$"}RESPONSE" | jq '.number') - if [ "${"$"}ISSUE_NUMBER" = "null" ] || [ -z "${"$"}ISSUE_NUMBER" ]; then - echo "::error::Failed to create issue. API response:" - echo "${"$"}RESPONSE" | jq '.' - else - echo "Created new upstream sync issue #${"$"}{ISSUE_NUMBER}" - fi - echo "issue_number=${"$"}{ISSUE_NUMBER}" >> "${"$"}GITHUB_OUTPUT" -fi""" - } - ] - } - # ================================================================= - # Auto-merge job: creates branch, merges upstream, runs Claude - # to review/fix conflicts/test/create PR - # ================================================================= - "merge-upstream" = { - name = "Auto-merge Upstream" - "runs-on" = "grunt" - needs = ["check-upstream"] - "if" = h.gha_needs_eq("check-upstream", "needs_sync", "true") - steps = [ - { - name = "Checkout" - uses = h.gha_action_url("xternal", "checkout", versions.CHECKOUT_VERSION, "READ_REPO_TOKEN") - "with" = { - "fetch-depth" = "0" - token = h.gha_secret("CLAUDE_BOT_TOKEN") - } - } - { - name = "Check for existing sync PR" - id = "existing-pr" - run = """\ -# Search for open PRs from chore/upstream-sync-* branches -RESPONSE=${"$"}(curl -s \\ - -H "Authorization: token ${h.gha_secret("CLAUDE_BOT_TOKEN")}" \\ - -H "Content-Type: application/json" \\ - "${h.gha_env("GITHUB_API_URL")}/repos/${h.gha_github("repository")}/pulls?state=open&limit=50") - -PR_BRANCH=${"$"}(echo "${"$"}RESPONSE" | jq -r '[.[] | select(.head.ref | startswith("chore/upstream-sync-"))] | .[0].head.ref // ""') - -if [ -n "${"$"}PR_BRANCH" ]; then - PR_NUMBER=${"$"}(echo "${"$"}RESPONSE" | jq -r '[.[] | select(.head.ref | startswith("chore/upstream-sync-"))] | .[0].number') - echo "Found existing sync PR #${"$"}PR_NUMBER on branch ${"$"}PR_BRANCH" - echo "pr_exists=true" >> "${"$"}GITHUB_OUTPUT" - echo "pr_branch=${"$"}PR_BRANCH" >> "${"$"}GITHUB_OUTPUT" - echo "pr_number=${"$"}PR_NUMBER" >> "${"$"}GITHUB_OUTPUT" -else - echo "No existing sync PR found" - echo "pr_exists=false" >> "${"$"}GITHUB_OUTPUT" -fi""" - } - { - name = "Prepare upstream merge" - id = "prepare" - run = """\ -git config user.name "claude" -git config user.email "claude@wxs.ro" - -git remote add upstream https://github.com/anthropics/claude-code-action.git -git fetch upstream main --quiet - -# Check if existing PR branch is already up to date with upstream -if [ "${h.gha_steps("existing-pr", "pr_exists")}" = "true" ]; then - PR_BRANCH="${h.gha_steps("existing-pr", "pr_branch")}" - git checkout "${"$"}PR_BRANCH" - - # Check if upstream/main is already merged into the PR branch - BEHIND=${"$"}(git rev-list "${"$"}PR_BRANCH"..upstream/main --count) - if [ "${"$"}BEHIND" -eq 0 ]; then - echo "Existing PR branch ${"$"}PR_BRANCH is already up to date with upstream" - echo "skip=true" >> "${"$"}GITHUB_OUTPUT" - exit 0 - fi - - echo "Existing PR branch ${"$"}PR_BRANCH is ${"$"}BEHIND commits behind upstream — updating" - BRANCH="${"$"}PR_BRANCH" -else - BRANCH="chore/upstream-sync-${"$"}(date +%Y%m%d)" - - # Delete stale remote branch if it exists (from a previous run with no PR) - if git ls-remote --heads origin "${"$"}BRANCH" | grep -q "${"$"}BRANCH"; then - echo "Deleting stale remote branch ${"$"}BRANCH (no PR references it)" - git push origin --delete "${"$"}BRANCH" || true - fi - - git checkout -b "${"$"}BRANCH" -fi - -echo "skip=false" >> "${"$"}GITHUB_OUTPUT" -echo "branch=${"$"}BRANCH" >> "${"$"}GITHUB_OUTPUT" - -# Files our fork has modified vs upstream -FORK_FILES=${"$"}(git log upstream/main..main --name-only --pretty=format: | sort -u | grep -v '^${"$"}') -echo "=== Fork-modified files ===" -echo "${"$"}FORK_FILES" - -# Files upstream has changed since we last synced -UPSTREAM_FILES=${"$"}(git diff --name-only main...upstream/main | sort -u) -echo "" -echo "=== Upstream-changed files ===" -echo "${"$"}UPSTREAM_FILES" - -# Intersection: fork files that upstream also changed (conflict risk) -AFFECTED=${"$"}(comm -12 <(echo "${"$"}FORK_FILES") <(echo "${"$"}UPSTREAM_FILES")) -echo "" -echo "=== Affected fork files (overlap) ===" -if [ -z "${"$"}AFFECTED" ]; then - echo "(none — low risk merge)" -else - echo "${"$"}AFFECTED" -fi - -# Export all lists as multiline outputs -echo "fork_files<> "${"$"}GITHUB_OUTPUT" -echo "${"$"}FORK_FILES" >> "${"$"}GITHUB_OUTPUT" -echo "EOF" >> "${"$"}GITHUB_OUTPUT" - -echo "affected_files<> "${"$"}GITHUB_OUTPUT" -echo "${"$"}{AFFECTED:-(none)}" >> "${"$"}GITHUB_OUTPUT" -echo "EOF" >> "${"$"}GITHUB_OUTPUT" - -if git merge upstream/main --no-edit; then - echo "merge_clean=true" >> "${"$"}GITHUB_OUTPUT" - echo "Merge completed cleanly" -else - echo "merge_clean=false" >> "${"$"}GITHUB_OUTPUT" - echo "Merge has conflicts — Claude will resolve them" -fi""" - } - _bun_install() | {"if" = "steps.prepare.outputs.skip != 'true'"} - _bun_deps() | {"if" = "steps.prepare.outputs.skip != 'true'"} - { - name = "Claude: review merge and create PR" - "if" = "steps.prepare.outputs.skip != 'true'" - uses = _claude_action_url - "with" = { - prompt = _merge_prompt - claude_code_oauth_token = h.gha_secret("CLAUDE_CODE_OAUTH_TOKEN") - github_token = h.gha_secret("CLAUDE_BOT_TOKEN") - platform = "forgejo" - show_full_output = "true" - allowed_tools = "Bash(git:*),Bash(bun:*),Bash(bunx:*),Bash(npx:*),Bash(./node_modules/.bin/*:*),Bash(curl:*),Bash(echo:*),Bash(cat:*),Read,Write,Edit,MultiEdit,Glob,Grep,WebFetch,WebSearch" - } - env = { - GITHUB_TOKEN = h.gha_secret("CLAUDE_BOT_TOKEN") - GITHUB_SERVER_URL = "https://github.com" - FORK_MODIFIED_FILES = h.gha_steps("prepare", "fork_files") - AFFECTED_FORK_FILES = h.gha_steps("prepare", "affected_files") - } - } - ] - } - } -} - -# ============================================================================= -# Mirror branch workflow (syncs clean copy to Codeberg via push mirror) -# Excludes internal CI config and development history -# ============================================================================= -_mirror_config = mirror_mod.MirrorBranchConfig { - remote_url = "git@codeberg.org:vtmocanu/claude-code-action.git" - ssh_key_secret = "CODEBERG_MIRROR_SSH_KEY" - ssh_host = "codeberg.org" - exclude_paths = [".forgejo", "prds"] - paths_ignore = [".forgejo/kcl-ci/**", "prds/**"] -} -_mirror_workflow = mirror_mod.mirror_branch_workflow(_mirror_config) - -# ============================================================================= -# Write workflows -# ============================================================================= -h.dump_workflow(_claude_workflow, "../workflows/claude.yml") -h.dump_workflow(_kcl_build_workflow, "../workflows/kcl-ci-build.yml") -h.dump_workflow(_ci_workflow, "../workflows/ci.yml") -h.dump_workflow(_test_action_workflow, "../workflows/test-action.yml") -h.dump_workflow(_upstream_sync_workflow, "../workflows/upstream-sync.yml") -h.dump_workflow(_mirror_workflow, "../workflows/mirror-branch.yml") diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml deleted file mode 100644 index ed0fb5d..0000000 --- a/.forgejo/workflows/ci.yml +++ /dev/null @@ -1,101 +0,0 @@ -# Generated by KCL v1.43.3 - DO NOT EDIT -# Source: .forgejo/kcl-ci/main.k -# kcl-ci local: ~/stuff/gitrepos/wxs/actions/kcl-ci -# kcl-ci remote: https://git.wxs.ro/actions/kcl-ci - -name: CI -'on': - push: - branches: - - main - paths-ignore: - - '.forgejo/kcl-ci/**' - pull_request: - branches: - - main - paths-ignore: - - '.forgejo/kcl-ci/**' - workflow_dispatch: {} -concurrency: - group: ci-${{ github.ref }} - cancel-in-progress: true -env: - BUN_VERSION: '1.2.21' -jobs: - test: - name: Tests - runs-on: grunt - steps: - - name: Checkout - uses: https://${{ secrets.READ_REPO_TOKEN }}@git.wxs.ro/xternal/checkout@v6.0.2 - - name: Install Bun - run: |- - if command -v bun &> /dev/null; then - INSTALLED=$(bun --version) - if [ "$INSTALLED" = "$BUN_VERSION" ]; then - echo "Bun $BUN_VERSION already installed" - else - echo "Bun $INSTALLED installed, need $BUN_VERSION — reinstalling" - curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" - echo "$HOME/.bun/bin" >> "$GITHUB_PATH" - fi - else - curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" - echo "$HOME/.bun/bin" >> "$GITHUB_PATH" - fi - - name: Install dependencies - run: bun install - - name: Run tests - run: |- - export GITHUB_SERVER_URL=https://github.com - bun test - prettier: - name: Prettier - runs-on: grunt - steps: - - name: Checkout - uses: https://${{ secrets.READ_REPO_TOKEN }}@git.wxs.ro/xternal/checkout@v6.0.2 - - name: Install Bun - run: |- - if command -v bun &> /dev/null; then - INSTALLED=$(bun --version) - if [ "$INSTALLED" = "$BUN_VERSION" ]; then - echo "Bun $BUN_VERSION already installed" - else - echo "Bun $INSTALLED installed, need $BUN_VERSION — reinstalling" - curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" - echo "$HOME/.bun/bin" >> "$GITHUB_PATH" - fi - else - curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" - echo "$HOME/.bun/bin" >> "$GITHUB_PATH" - fi - - name: Install dependencies - run: bun install - - name: Run prettier check - run: bun run format:check - typecheck: - name: Type Check - runs-on: grunt - steps: - - name: Checkout - uses: https://${{ secrets.READ_REPO_TOKEN }}@git.wxs.ro/xternal/checkout@v6.0.2 - - name: Install Bun - run: |- - if command -v bun &> /dev/null; then - INSTALLED=$(bun --version) - if [ "$INSTALLED" = "$BUN_VERSION" ]; then - echo "Bun $BUN_VERSION already installed" - else - echo "Bun $INSTALLED installed, need $BUN_VERSION — reinstalling" - curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" - echo "$HOME/.bun/bin" >> "$GITHUB_PATH" - fi - else - curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" - echo "$HOME/.bun/bin" >> "$GITHUB_PATH" - fi - - name: Install dependencies - run: bun install - - name: Run TypeScript type check - run: bun run typecheck diff --git a/.forgejo/workflows/claude.yml b/.forgejo/workflows/claude.yml deleted file mode 100644 index 15b10c9..0000000 --- a/.forgejo/workflows/claude.yml +++ /dev/null @@ -1,56 +0,0 @@ -# Generated by KCL v1.43.3 - DO NOT EDIT -# Source: .forgejo/kcl-ci/main.k -# kcl-ci local: ~/stuff/gitrepos/wxs/actions/kcl-ci -# kcl-ci remote: https://git.wxs.ro/actions/kcl-ci - -name: Claude Code -'on': - issue_comment: - types: - - created - pull_request_review_comment: - types: - - created - issues: - types: - - opened - - assigned - - labeled - pull_request_review: - types: - - submitted -concurrency: - group: claude-${{ github.event.issue.number || github.event.pull_request.number }} -jobs: - claude: - if: ((github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') && contains(github.event.comment.body, '@claude')) || (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || (github.event_name == 'issues') - runs-on: grunt - steps: - - name: Checkout - uses: https://${{ secrets.READ_REPO_TOKEN }}@git.wxs.ro/xternal/checkout@v6.0.2 - - name: Run Claude Code - id: claude - uses: https://${{ secrets.READ_REPO_TOKEN }}@git.wxs.ro/actions/claude-code-action@main - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - github_token: ${{ secrets.CLAUDE_BOT_TOKEN }} - bot_id: '48' - bot_name: claude - bot_email: claude@wxs.ro - platform: forgejo - assignee_trigger: claude - show_full_output: 'true' - allowed_tools: Bash(git:*),Bash(bun:*),Bash(bunx:*),Bash(npx:*),Bash(./node_modules/.bin/*:*),Bash(curl:*),Read,Write,Edit,MultiEdit,Glob,Grep,WebFetch,WebSearch - - name: Debug on failure - if: failure() - run: | - echo "=== Debug Info ===" - echo "Event: ${{ github.event_name }} / ${{ github.event.action }}" - echo "Actor: ${{ github.actor }}" - EXEC_FILE="${{ steps.claude.outputs.execution_file }}" - if [ -f "$EXEC_FILE" ]; then - echo "--- Execution file ---" - cat "$EXEC_FILE" - else - echo "No execution file" - fi diff --git a/.forgejo/workflows/kcl-ci-build.yml b/.forgejo/workflows/kcl-ci-build.yml deleted file mode 100644 index 278cce8..0000000 --- a/.forgejo/workflows/kcl-ci-build.yml +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by KCL v1.43.3 - DO NOT EDIT -# Source: .forgejo/kcl-ci/main.k -# kcl-ci local: ~/stuff/gitrepos/wxs/actions/kcl-ci -# kcl-ci remote: https://git.wxs.ro/actions/kcl-ci - -name: KCL CI Build -'on': - push: - branches: - - main - paths: - - '.forgejo/kcl-ci/**' - - '.forgejo/workflows/kcl-ci-build.yml' - workflow_dispatch: {} -concurrency: - group: kcl-ci-${{ github.ref }} - cancel-in-progress: true -jobs: - build: - name: Build KCL Workflows - runs-on: grunt - steps: - - name: Build KCL workflows - uses: https://${{ secrets.READ_REPO_TOKEN }}@git.wxs.ro/actions/kcl-ci@v1.43.3 - with: - read-repo-token: ${{ secrets.READ_REPO_TOKEN }} - ci-bot-token: ${{ secrets.CI_BOT_TOKEN }} - working-dir: '.forgejo/kcl-ci' - commit-changes: 'true' - file-pattern: '.forgejo/workflows/*.yml' - commit-message: 'chore: regenerate workflows from KCL [skip ci]' diff --git a/.forgejo/workflows/mirror-branch.yml b/.forgejo/workflows/mirror-branch.yml deleted file mode 100644 index 92a7b5c..0000000 --- a/.forgejo/workflows/mirror-branch.yml +++ /dev/null @@ -1,60 +0,0 @@ -# Generated by KCL v1.43.3 - DO NOT EDIT -# Source: .forgejo/kcl-ci/main.k -# kcl-ci local: ~/stuff/gitrepos/wxs/actions/kcl-ci -# kcl-ci remote: https://git.wxs.ro/actions/kcl-ci - -name: Mirror to Remote -'on': - push: - branches: - - main - paths-ignore: - - '.forgejo/kcl-ci/**' - - prds/** - workflow_dispatch: {} -concurrency: - group: mirror-${{ github.ref }} - cancel-in-progress: true -env: - READ_REPO_TOKEN: ${{ secrets.READ_REPO_TOKEN }} -jobs: - push-mirror: - name: Push to Mirror - runs-on: grunt - steps: - - name: Checkout code - uses: https://${{ secrets.READ_REPO_TOKEN }}@git.wxs.ro/xternal/checkout@v6.0.2 - with: - fetch-depth: '0' - token: ${{ secrets.CI_BOT_TOKEN }} - - name: Configure SSH for mirror - run: |- - mkdir -p ~/.ssh - echo "${MIRROR_SSH_KEY}" > ~/.ssh/mirror_key - chmod 600 ~/.ssh/mirror_key - ssh-keyscan codeberg.org >> ~/.ssh/known_hosts 2>/dev/null - export GIT_SSH_COMMAND="ssh -i ~/.ssh/mirror_key -o StrictHostKeyChecking=no" - env: - MIRROR_SSH_KEY: ${{ secrets.CODEBERG_MIRROR_SSH_KEY }} - - name: Prepare and push to mirror - run: |- - # Configure git identity for the staging commit - git config user.name "forgejo-actions[bot]" - git config user.email "noreply@forgejo-actions" - - # Remove excluded paths - rm -rf .forgejo - rm -rf prds - - # Commit cleaned tree (detached from source history) - git checkout -B mirror-staging - git add -A - git diff --cached --quiet || git commit -m "mirror: $(git log -1 --format='%h %s' main)" - - # Push to remote via SSH - export GIT_SSH_COMMAND="ssh -i ~/.ssh/mirror_key -o StrictHostKeyChecking=no" - git remote add mirror git@codeberg.org:vtmocanu/claude-code-action.git - git push mirror mirror-staging:main --force - - # Cleanup - rm -f ~/.ssh/mirror_key diff --git a/.forgejo/workflows/test-action.yml b/.forgejo/workflows/test-action.yml deleted file mode 100644 index 48dea5f..0000000 --- a/.forgejo/workflows/test-action.yml +++ /dev/null @@ -1,74 +0,0 @@ -# Generated by KCL v1.43.3 - DO NOT EDIT -# Source: .forgejo/kcl-ci/main.k -# kcl-ci local: ~/stuff/gitrepos/wxs/actions/kcl-ci -# kcl-ci remote: https://git.wxs.ro/actions/kcl-ci - -name: Test Action (Forgejo) -'on': - schedule: - - cron: '0 6 * * 1' - pull_request: - branches: - - main - paths: - - src/** - - action.yml - - '.forgejo/**' - - base-action/** - workflow_dispatch: {} -concurrency: - group: test-action-${{ github.ref }} - cancel-in-progress: true -jobs: - test-agent-mode: - name: Test Agent Mode on Forgejo - runs-on: grunt - if: github.actor != 'claude' || github.event_name == 'pull_request' - steps: - - name: Checkout - uses: https://${{ secrets.READ_REPO_TOKEN }}@git.wxs.ro/xternal/checkout@v6.0.2 - - name: Run Claude Code Action (agent mode) - id: claude - uses: './' - with: - prompt: Respond with exactly the text FORGEJO_PLATFORM_TEST_OK and nothing else. Do not use any tools. - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - github_token: ${{ secrets.FORGEJO_CLAUDE_TOKEN }} - platform: forgejo - - name: Verify execution output exists - run: |- - EXEC_FILE="${{ steps.claude.outputs.execution_file }}" - if [ -z "$EXEC_FILE" ]; then - echo "FAIL: execution_file output is empty" - exit 1 - fi - if [ ! -f "$EXEC_FILE" ]; then - echo "FAIL: execution file does not exist at: $EXEC_FILE" - exit 1 - fi - echo "OK: execution file exists at: $EXEC_FILE" - - if grep -q "FORGEJO_PLATFORM_TEST_OK" "$EXEC_FILE"; then - echo "OK: Output contains expected test marker" - else - echo "WARN: Test marker not found in output (Claude may have formatted differently)" - echo "--- Output contents (first 50 lines) ---" - head -50 "$EXEC_FILE" - fi - - name: Debug on failure - if: failure() - run: |- - echo "=== Debug Info ===" - echo "Platform env: PLATFORM=$PLATFORM FORGEJO=$FORGEJO" - echo "Server URL: $GITHUB_SERVER_URL" - echo "API URL: $GITHUB_API_URL" - echo "Event: ${{ github.event_name }}" - echo "Actor: ${{ github.actor }}" - echo "" - EXEC_FILE="${{ steps.claude.outputs.execution_file }}" - if [ -f "$EXEC_FILE" ]; then - echo "--- Execution file contents ---" - cat "$EXEC_FILE" - else - echo "No execution file found" - fi diff --git a/.forgejo/workflows/upstream-sync.yml b/.forgejo/workflows/upstream-sync.yml deleted file mode 100644 index 3bbb42f..0000000 --- a/.forgejo/workflows/upstream-sync.yml +++ /dev/null @@ -1,410 +0,0 @@ -# Generated by KCL v1.43.3 - DO NOT EDIT -# Source: .forgejo/kcl-ci/main.k -# kcl-ci local: ~/stuff/gitrepos/wxs/actions/kcl-ci -# kcl-ci remote: https://git.wxs.ro/actions/kcl-ci - -name: Upstream Sync Check -'on': - schedule: - - cron: '0 8 * * 1' - workflow_dispatch: {} -env: - BUN_VERSION: '1.2.21' -jobs: - check-upstream: - name: Check for upstream updates - runs-on: grunt - outputs: - needs_sync: ${{ steps.compare.outputs.needs_sync }} - behind: ${{ steps.compare.outputs.behind }} - ahead: ${{ steps.compare.outputs.ahead }} - issue_number: ${{ steps.sync-issue.outputs.issue_number }} - steps: - - name: Checkout - uses: https://${{ secrets.READ_REPO_TOKEN }}@git.wxs.ro/xternal/checkout@v6.0.2 - with: - fetch-depth: '0' - - name: Configure upstream remote - run: |- - if ! git remote get-url upstream &> /dev/null; then - git remote add upstream https://github.com/anthropics/claude-code-action.git - fi - - name: Fetch upstream - run: git fetch upstream main --quiet - - name: Compare with upstream - id: compare - run: |- - BEHIND_COUNT=$(git rev-list main..upstream/main --count) - AHEAD_COUNT=$(git rev-list upstream/main..main --count) - - echo "behind=$BEHIND_COUNT" >> "$GITHUB_OUTPUT" - echo "ahead=$AHEAD_COUNT" >> "$GITHUB_OUTPUT" - - echo "Our main is $BEHIND_COUNT commits behind and $AHEAD_COUNT commits ahead of upstream" - - if [ "$BEHIND_COUNT" -gt 0 ]; then - echo "needs_sync=true" >> "$GITHUB_OUTPUT" - - echo "--- New upstream commits ---" - git log main..upstream/main --oneline | head -30 - - echo "" - echo "--- Files changed ---" - DIFF_STAT=$(git diff --stat main...upstream/main) - echo "$DIFF_STAT" - - { - echo "diff_stat<> "$GITHUB_OUTPUT" - - { - echo "commit_log<> "$GITHUB_OUTPUT" - else - echo "needs_sync=false" >> "$GITHUB_OUTPUT" - echo "Already up to date with upstream" - fi - - name: Ensure upstream-sync label exists - if: steps.compare.outputs.needs_sync == 'true' - id: label - run: | - API="${{ env.GITHUB_API_URL }}/repos/${{ github.repository }}" - AUTH="Authorization: token ${{ secrets.CLAUDE_BOT_TOKEN }}" - - # Look up the label ID first (Forgejo needs integer IDs, not names) - LABEL_ID=$(curl -s -H "$AUTH" "$API/labels" | jq -r '[.[] | select(.name == "upstream-sync")] | .[0].id // ""') - - # Only create label if it doesn't exist yet - if [ -z "$LABEL_ID" ]; then - curl -s -o /dev/null \ - -X POST -H "$AUTH" -H "Content-Type: application/json" \ - -d '{"name":"upstream-sync","color":"#0075ca","description":"Upstream sync tracking"}' \ - "$API/labels" - LABEL_ID=$(curl -s -H "$AUTH" "$API/labels" | jq -r '[.[] | select(.name == "upstream-sync")] | .[0].id // ""') - echo "Created upstream-sync label with ID: $LABEL_ID" - else - echo "Found existing upstream-sync label ID: $LABEL_ID" - fi - - echo "label_id=$LABEL_ID" >> "$GITHUB_OUTPUT" - - name: Check for existing sync issue - if: steps.compare.outputs.needs_sync == 'true' - id: existing - run: |- - # Forgejo's labels filter requires integer IDs, not names - RESPONSE=$(curl -s \ - -H "Authorization: token ${{ secrets.CLAUDE_BOT_TOKEN }}" \ - -H "Content-Type: application/json" \ - "${{ env.GITHUB_API_URL }}/repos/${{ github.repository }}/issues?state=open&labels=${{ steps.label.outputs.label_id }}&limit=1") - - ISSUE_COUNT=$(echo "$RESPONSE" | jq 'length') - - if [ "$ISSUE_COUNT" -gt 0 ]; then - ISSUE_NUMBER=$(echo "$RESPONSE" | jq '.[0].number') - echo "issue_exists=true" >> "$GITHUB_OUTPUT" - echo "issue_number=$ISSUE_NUMBER" >> "$GITHUB_OUTPUT" - echo "Found existing sync issue #$ISSUE_NUMBER" - else - echo "issue_exists=false" >> "$GITHUB_OUTPUT" - echo "No existing sync issue found" - fi - - name: Create or update sync issue - if: steps.compare.outputs.needs_sync == 'true' - id: sync-issue - run: |- - API="${{ env.GITHUB_API_URL }}/repos/${{ github.repository }}" - AUTH="Authorization: token ${{ secrets.CLAUDE_BOT_TOKEN }}" - LABEL_ID="${{ steps.label.outputs.label_id }}" - - BEHIND="${{ steps.compare.outputs.behind }}" - AHEAD="${{ steps.compare.outputs.ahead }}" - COMMIT_LOG=$(cat <<'LOG_EOF' - ${{ steps.compare.outputs.commit_log }} - LOG_EOF - ) - DIFF_STAT=$(cat <<'DIFF_EOF' - ${{ steps.compare.outputs.diff_stat }} - DIFF_EOF - ) - - BODY=$(cat <> "$GITHUB_OUTPUT" - else - # Forgejo expects labels as integer IDs, not objects with names - RESPONSE=$(curl -s \ - -X POST -H "$AUTH" -H "Content-Type: application/json" \ - -d "$(jq -n --arg title "$TITLE" --arg body "$BODY" --argjson labels "[${LABEL_ID:-0}]" '{title: $title, body: $body, labels: $labels}')" \ - "$API/issues") - ISSUE_NUMBER=$(echo "$RESPONSE" | jq '.number') - if [ "$ISSUE_NUMBER" = "null" ] || [ -z "$ISSUE_NUMBER" ]; then - echo "::error::Failed to create issue. API response:" - echo "$RESPONSE" | jq '.' - else - echo "Created new upstream sync issue #${ISSUE_NUMBER}" - fi - echo "issue_number=${ISSUE_NUMBER}" >> "$GITHUB_OUTPUT" - fi - merge-upstream: - name: Auto-merge Upstream - runs-on: grunt - needs: - - check-upstream - if: needs.check-upstream.outputs.needs_sync == 'true' - steps: - - name: Checkout - uses: https://${{ secrets.READ_REPO_TOKEN }}@git.wxs.ro/xternal/checkout@v6.0.2 - with: - fetch-depth: '0' - token: ${{ secrets.CLAUDE_BOT_TOKEN }} - - name: Check for existing sync PR - id: existing-pr - run: |- - # Search for open PRs from chore/upstream-sync-* branches - RESPONSE=$(curl -s \ - -H "Authorization: token ${{ secrets.CLAUDE_BOT_TOKEN }}" \ - -H "Content-Type: application/json" \ - "${{ env.GITHUB_API_URL }}/repos/${{ github.repository }}/pulls?state=open&limit=50") - - PR_BRANCH=$(echo "$RESPONSE" | jq -r '[.[] | select(.head.ref | startswith("chore/upstream-sync-"))] | .[0].head.ref // ""') - - if [ -n "$PR_BRANCH" ]; then - PR_NUMBER=$(echo "$RESPONSE" | jq -r '[.[] | select(.head.ref | startswith("chore/upstream-sync-"))] | .[0].number') - echo "Found existing sync PR #$PR_NUMBER on branch $PR_BRANCH" - echo "pr_exists=true" >> "$GITHUB_OUTPUT" - echo "pr_branch=$PR_BRANCH" >> "$GITHUB_OUTPUT" - echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT" - else - echo "No existing sync PR found" - echo "pr_exists=false" >> "$GITHUB_OUTPUT" - fi - - name: Prepare upstream merge - id: prepare - run: |- - git config user.name "claude" - git config user.email "claude@wxs.ro" - - git remote add upstream https://github.com/anthropics/claude-code-action.git - git fetch upstream main --quiet - - # Check if existing PR branch is already up to date with upstream - if [ "${{ steps.existing-pr.outputs.pr_exists }}" = "true" ]; then - PR_BRANCH="${{ steps.existing-pr.outputs.pr_branch }}" - git checkout "$PR_BRANCH" - - # Check if upstream/main is already merged into the PR branch - BEHIND=$(git rev-list "$PR_BRANCH"..upstream/main --count) - if [ "$BEHIND" -eq 0 ]; then - echo "Existing PR branch $PR_BRANCH is already up to date with upstream" - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - - echo "Existing PR branch $PR_BRANCH is $BEHIND commits behind upstream — updating" - BRANCH="$PR_BRANCH" - else - BRANCH="chore/upstream-sync-$(date +%Y%m%d)" - - # Delete stale remote branch if it exists (from a previous run with no PR) - if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then - echo "Deleting stale remote branch $BRANCH (no PR references it)" - git push origin --delete "$BRANCH" || true - fi - - git checkout -b "$BRANCH" - fi - - echo "skip=false" >> "$GITHUB_OUTPUT" - echo "branch=$BRANCH" >> "$GITHUB_OUTPUT" - - # Files our fork has modified vs upstream - FORK_FILES=$(git log upstream/main..main --name-only --pretty=format: | sort -u | grep -v '^$') - echo "=== Fork-modified files ===" - echo "$FORK_FILES" - - # Files upstream has changed since we last synced - UPSTREAM_FILES=$(git diff --name-only main...upstream/main | sort -u) - echo "" - echo "=== Upstream-changed files ===" - echo "$UPSTREAM_FILES" - - # Intersection: fork files that upstream also changed (conflict risk) - AFFECTED=$(comm -12 <(echo "$FORK_FILES") <(echo "$UPSTREAM_FILES")) - echo "" - echo "=== Affected fork files (overlap) ===" - if [ -z "$AFFECTED" ]; then - echo "(none — low risk merge)" - else - echo "$AFFECTED" - fi - - # Export all lists as multiline outputs - echo "fork_files<> "$GITHUB_OUTPUT" - echo "$FORK_FILES" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - - echo "affected_files<> "$GITHUB_OUTPUT" - echo "${AFFECTED:-(none)}" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - - if git merge upstream/main --no-edit; then - echo "merge_clean=true" >> "$GITHUB_OUTPUT" - echo "Merge completed cleanly" - else - echo "merge_clean=false" >> "$GITHUB_OUTPUT" - echo "Merge has conflicts — Claude will resolve them" - fi - - name: Install Bun - run: |- - if command -v bun &> /dev/null; then - INSTALLED=$(bun --version) - if [ "$INSTALLED" = "$BUN_VERSION" ]; then - echo "Bun $BUN_VERSION already installed" - else - echo "Bun $INSTALLED installed, need $BUN_VERSION — reinstalling" - curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" - echo "$HOME/.bun/bin" >> "$GITHUB_PATH" - fi - else - curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" - echo "$HOME/.bun/bin" >> "$GITHUB_PATH" - fi - if: steps.prepare.outputs.skip != 'true' - - name: Install dependencies - run: bun install - if: steps.prepare.outputs.skip != 'true' - - name: 'Claude: review merge and create PR' - if: steps.prepare.outputs.skip != 'true' - uses: https://${{ secrets.READ_REPO_TOKEN }}@git.wxs.ro/actions/claude-code-action@main - with: - prompt: |- - You are automating an upstream sync for our Forgejo fork of anthropics/claude-code-action. - - ## CRITICAL: Do NOT stop early - You MUST complete ALL steps below before stopping. Resolving conflicts alone is NOT sufficient. - Your job is NOT done until you have pushed the branch AND created (or updated) the PR. - If you stop before pushing and creating the PR, the entire sync fails. - - ## Current state - - Branch: ${{ steps.prepare.outputs.branch }} - - ${{ needs.check-upstream.outputs.behind }} commits behind upstream, ${{ needs.check-upstream.outputs.ahead }} commits ahead (our Forgejo changes) - - Tracking issue: #${{ needs.check-upstream.outputs.issue_number }} - - Existing PR: ${{ steps.existing-pr.outputs.pr_exists }} (if true, PR #${{ steps.existing-pr.outputs.pr_number }} already exists — update it instead of creating a new one) - - Merge was attempted. Status: ${{ steps.prepare.outputs.merge_clean }} (true = clean, false = has conflicts) - - ## Impact analysis - - $FORK_MODIFIED_FILES — all files our fork has changed vs upstream - - $AFFECTED_FORK_FILES — subset of fork files that upstream ALSO changed (overlap = conflict risk) - - If AFFECTED_FORK_FILES is empty, upstream only touched files we haven't modified — low risk, merge should be clean. - - If AFFECTED_FORK_FILES is non-empty, those files need careful review since both sides changed them. - - ## If merge has conflicts (merge_clean=false) - Resolve all conflicts. For files in $FORK_MODIFIED_FILES, our fork's changes take PRIORITY — they contain Forgejo-specific adaptations. - - ## Review ALL upstream changes for fork compatibility - Read CLAUDE.md for context on our Forgejo adapter pattern and platform-specific code. - Even files we haven't modified can break our fork. Check upstream changes for: - - Renamed/moved imports or exports that our Forgejo-specific files depend on - - Changed function signatures, types, or interfaces used by our adapter code - - New assumptions (e.g. GitHub-only APIs) that don't hold on Forgejo - - Removed or restructured code that our platform branches reference - If you find incompatibilities, fix them to maintain Forgejo support. - - ## Maintainability principle - When fixing incompatibilities, minimize fork divergence to keep future merges easy. - Prefer adapter patterns (separate Forgejo files) over inline modifications to upstream files. - The fewer upstream files we modify, the fewer conflicts in the next sync. - - ## IMPORTANT: Command execution rules - - GITHUB_SERVER_URL=https://github.com is ALREADY exported in your environment. Run commands directly WITHOUT env var prefixes. - CORRECT: `bun test` - WRONG: `GITHUB_SERVER_URL=https://github.com bun test` (this will be blocked by permissions) - - Do NOT use `2>&1` redirections, subshells (`sh -c`), or `export` commands — they will be blocked. - - For inspecting env vars, use: `bun -e "console.log(process.env.VAR_NAME)"` - - Available commands: git, bun, bunx, npx, curl, echo, and anything in ./node_modules/.bin/ - - ## Known pre-existing test failures - Some tests in comment-logic.test.ts and create-prompt.test.ts have pre-existing failures related to Forgejo URL patterns. - These are NOT caused by the upstream merge. When running `bun test`: - - If only comment-logic.test.ts and/or create-prompt.test.ts fail, that's expected — proceed. - - If OTHER test files fail that weren't failing before, investigate those as potential merge regressions. - - Do NOT spend time trying to fix pre-existing failures or trying different env var approaches. - - ## After merge is resolved and reviewed - 1. Run tests: `bun test` — see "Known pre-existing test failures" above - 2. Run typecheck: `bun run typecheck` - 3. Check formatting: `bun run format:check` — if issues, fix with `bun run format` - 4. Commit any remaining changes and push the branch - 5. Push the branch, then create or update the PR: - - If existing PR is "true": PR #${{ steps.existing-pr.outputs.pr_number }} already exists. Just push the branch — the PR updates automatically. - Optionally update the PR body via PATCH $GITHUB_API_URL/repos/$GITHUB_REPOSITORY/pulls/${{ steps.existing-pr.outputs.pr_number }} - - If existing PR is "false": Create a new PR via POST $GITHUB_API_URL/repos/$GITHUB_REPOSITORY/pulls - Auth: token $GITHUB_TOKEN - Title: "chore: merge ${{ needs.check-upstream.outputs.behind }} upstream commits from anthropics/claude-code-action" - Body should include: - - "Closes #${{ needs.check-upstream.outputs.issue_number }}" on its own line (links PR to the tracking issue) - - Summary of notable upstream changes - - Impact assessment: which fork files (if any) were affected - - List of conflicts resolved (if any) - - If no fork files were affected, note that this is a low-risk merge - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - github_token: ${{ secrets.CLAUDE_BOT_TOKEN }} - platform: forgejo - show_full_output: 'true' - allowed_tools: Bash(git:*),Bash(bun:*),Bash(bunx:*),Bash(npx:*),Bash(./node_modules/.bin/*:*),Bash(curl:*),Bash(echo:*),Bash(cat:*),Read,Write,Edit,MultiEdit,Glob,Grep,WebFetch,WebSearch - env: - GITHUB_TOKEN: ${{ secrets.CLAUDE_BOT_TOKEN }} - GITHUB_SERVER_URL: https://github.com - FORK_MODIFIED_FILES: ${{ steps.prepare.outputs.fork_files }} - AFFECTED_FORK_FILES: ${{ steps.prepare.outputs.affected_files }} diff --git a/prds/done/1-forgejo-platform-support.md b/prds/done/1-forgejo-platform-support.md deleted file mode 100644 index 8484db9..0000000 --- a/prds/done/1-forgejo-platform-support.md +++ /dev/null @@ -1,353 +0,0 @@ -# PRD #1: Forgejo Platform Support with Upstream-Compatible Architecture - -**Issue**: [#1](https://git.wxs.ro/actions/claude-code-action/issues/1) -**Priority**: High -**Created**: 2026-02-25 -**Revised**: 2026-02-25 (post senior engineer review) - ---- - -## Problem - -The `claude-code-action` is tightly coupled to GitHub APIs (Octokit REST, GraphQL, `@actions/github`, OIDC token exchange). This makes it unusable with Forgejo Actions, despite Forgejo being workflow-compatible with GitHub Actions. We need Forgejo support while keeping upstream merges as painless as possible. - -## Solution - -Adopt a **lean adapter approach**: only create new files for functionality that is genuinely different on Forgejo, and **re-use existing `src/github/` code wherever Octokit REST is already compatible** with Forgejo's API. Make minimal, surgical modifications to entry points and `action.yml`. - -### Key Insight: Octokit REST Works with Forgejo - -Octokit REST methods (comments, permissions, branches, actor validation) work with Forgejo when `baseUrl` is set to the Forgejo instance's `/api/v1` endpoint. Forgejo mirrors GitHub's REST API paths. This means most of `src/github/` can be re-used directly — **we do NOT need to duplicate 20+ files**. - -### What Truly Needs Forgejo-Specific Code - -Only **4 areas** require new Forgejo implementations: - -| Area | Why It's Different | New File | -| --------------------------- | -------------------------------------------------------------------------------- | ----------------------------------- | -| **Token handling** | No OIDC exchange with Anthropic; use `GITHUB_TOKEN`/`FORGEJO_TOKEN` directly | `src/forgejo/token.ts` | -| **Data fetching** | Forgejo has no GraphQL; must replace 3 GraphQL queries with multiple REST calls | `src/forgejo/data/fetcher.ts` | -| **Comment URL formatting** | Forgejo URLs differ from GitHub URLs (job links, PR links, branch links) | `src/forgejo/comment-logic.ts` | -| **CI status / Actions API** | Forgejo Actions API is less mature; `downloadJobLogForWorkflowRun` may not exist | `src/mcp/forgejo-actions-server.ts` | - -### What Re-Uses Existing Code (No Duplication) - -These `src/github/` modules work with Forgejo via Octokit REST as-is: - -| Module | Why It Works | -| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `validation/permissions.ts` | `octokit.repos.getCollaboratorPermissionLevel()` exists in Forgejo API | -| `validation/actor.ts` | **Skipped entirely on Forgejo** — `read:user` scope required for `getByUsername()` but unavailable; Forgejo has no bot/app accounts so check is unnecessary | -| `validation/trigger.ts` | Pure logic — reads webhook payload fields, no API calls | -| `operations/comments/create-initial.ts` | `octokit.rest.issues.createComment()` works on Forgejo | -| `operations/comments/update-claude-comment.ts` | `octokit.rest.issues.updateComment()` works on Forgejo | -| `operations/branch.ts` | `octokit.rest.git.getRef()`, `octokit.rest.repos.get()` work on Forgejo | -| `operations/git-config.ts` | Uses `GITHUB_SERVER_URL` for noreply domain — works since Forgejo sets this var | -| `utils/sanitizer.ts` | Pure logic, no API dependency | -| `utils/image-downloader.ts` | Uses Octokit REST for fetching — should work | -| `context.ts` | `@actions/github` reads `GITHUB_EVENT_PATH` — Forgejo sets this | -| `data/formatter.ts` | Pure formatting logic | -| `api/config.ts` | Reads `GITHUB_API_URL` / `GITHUB_SERVER_URL` — Forgejo sets these | - -### Architecture: Lean Adapter - -``` -src/ -├── platform/ -│ └── detector.ts # NEW - Platform detection (+ action.yml `platform` input) -├── forgejo/ -│ ├── token.ts # NEW - Direct token (skip OIDC) -│ ├── data/ -│ │ └── fetcher.ts # NEW - REST-based data fetching (replaces GraphQL) -│ └── comment-logic.ts # NEW - Forgejo URL formatting for comments -├── mcp/ -│ ├── forgejo-actions-server.ts # NEW - Forgejo CI status MCP server -│ └── install-mcp-server.ts # MODIFIED - platform routing for MCP config -├── github/ # REUSED - existing code, no modifications -│ └── ... -├── entrypoints/ -│ └── run.ts # MODIFIED - platform branch for token setup -├── modes/ -│ ├── tag/index.ts # MODIFIED - swap data fetcher + comment logic -│ └── agent/index.ts # MODIFIED - swap token setup -action.yml # MODIFIED - platform input, conditional steps -``` - -**~8 new/modified files** instead of ~25. The existing `src/github/` code is re-used, not duplicated. - -### Why This Approach - -| Approach | Upstream Merge Effort | Maintenance | Chosen? | -| -------------------------------------------- | ---------------------------------------------------------------- | ----------- | ------- | -| **Lean adapter (reuse + minimal new files)** | Low (3-4 conflict-prone files) | Low | **Yes** | -| Full abstraction layer (20+ new files) | Near-zero conflicts but high maintenance syncing duplicated code | High | No | -| Modify GitHub files in-place | High conflict rate on every upstream pull | Low | No | -| Complete fork with divergence | No merges possible after a while | Very high | No | - -### Honest Merge-Conflict Assessment - -These files **will be modified** and are the merge-conflict surface when pulling upstream: - -| File | What We Change | Conflict Risk | -| ------------------------------- | ------------------------------------------------------------- | ---------------------------------------- | -| `src/entrypoints/run.ts` | Platform branch at top for token/client setup | **Medium** - upstream's most active file | -| `src/modes/tag/index.ts` | Swap `fetchGitHubData` call + comment logic based on platform | **Medium** | -| `src/modes/agent/index.ts` | Swap token setup based on platform | **Low** - simpler file | -| `src/mcp/install-mcp-server.ts` | Platform routing for MCP server selection | **Low-Medium** | -| `action.yml` | Add `platform` input, conditional Forgejo steps | **Low** - additive changes | - -**Mitigation strategy**: Keep all platform changes at the **top** of functions (early return / branch pattern). Avoid restructuring existing code flow. When upstream modifies these files, conflicts will be localized to our small additions, not interleaved throughout. - -## Technical Analysis - -### Platform Detection Strategy - -Use an **explicit `platform` input** (most reliable) with env-var auto-detection as fallback: - -```typescript -// src/platform/detector.ts -export function detectPlatform(): "github" | "forgejo" { - // 1. Explicit input (most reliable) - const platformInput = process.env.PLATFORM; - if (platformInput === "forgejo") return "forgejo"; - if (platformInput === "github") return "github"; - - // 2. Auto-detect from environment - // FORGEJO is set by Forgejo runner v7.0+ (not in older runners) - if (process.env.FORGEJO === "true") return "forgejo"; - - // Check GITHUB_SERVER_URL — if not github.com, likely Forgejo/Gitea - const serverUrl = process.env.GITHUB_SERVER_URL || ""; - if (serverUrl && !serverUrl.includes("github.com")) { - return "forgejo"; - } - - return "github"; -} -``` - -New `action.yml` input: - -```yaml -platform: - description: "Platform to run on: 'auto' (default), 'github', or 'forgejo'" - required: false - default: "auto" -``` - -### Key Technical Differences - -1. **No GraphQL**: Forgejo is REST-only. The 3 GraphQL queries in `src/github/api/queries/github.ts` (PR_QUERY, ISSUE_QUERY, USER_QUERY) must be replaced with **5-8 separate paginated REST calls**: - - - `GET /repos/{owner}/{repo}/pulls/{index}` - PR details - - `GET /repos/{owner}/{repo}/pulls/{index}/files` - changed files (paginated) - - `GET /repos/{owner}/{repo}/pulls/{index}/commits` - commits (paginated) - - `GET /repos/{owner}/{repo}/issues/{index}/comments` - issue-level comments (paginated) - - `GET /repos/{owner}/{repo}/pulls/{index}/reviews` - reviews (paginated) - - For each review: `GET /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments` - - `GET /repos/{owner}/{repo}/issues/{index}` - issue details - - Use `Promise.all()` to run independent calls in parallel and mitigate latency. - -2. **API base URL**: Forgejo uses `/api/v1` (vs GitHub's `/api/v3`). Octokit REST methods work when `baseUrl` is set correctly since Forgejo mirrors GitHub's endpoint paths. - -3. **Token handling**: No OIDC token exchange with Anthropic. The existing `OVERRIDE_GITHUB_TOKEN` code path in `token.ts` (line 130-135) already short-circuits the OIDC flow — for Forgejo we just need to use `GITHUB_TOKEN` or `FORGEJO_TOKEN` directly. - -4. **Bot identity**: No GitHub App bot. The action runs as the token owner. Default `bot_id` (41898282) is meaningless on Forgejo — need sensible defaults. - -5. **`pull_request_target` event**: May not exist in Forgejo. The existing `context.ts` maps it to `pull_request` (line 188), so it degrades gracefully. - -6. **GitHub MCP Server**: The `ghcr.io/github/github-mcp-server` Docker image referenced in `install-mcp-server.ts` (line 214) won't work. Skip on Forgejo — it's only included when `hasGitHubMcpTools` is true. - -7. **Forgejo Actions API gaps**: `actions.downloadJobLogForWorkflowRun()` used in the CI MCP server may not exist. Need to verify and implement alternative (direct API call or skip). - -### `action.yml` Required Modifications - -| Line(s) | Current | Problem | Fix | -| ------- | ----------------------------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | -| 103 | `default: "41898282"` (bot_id) | Meaningless on Forgejo | Make platform-aware; use token owner identity on Forgejo | -| 107 | `default: "claude[bot]"` (bot_name) | Doesn't exist on Forgejo | Same — detect from token or use configurable default | -| 163-166 | `uses: oven-sh/setup-bun@...` | References GitHub release; may fail on isolated Forgejo runners | Add conditional: if bun already available, skip; otherwise use direct curl install | -| 166 | `token: ${{ inputs.github_token \|\| github.token }}` | Used to fetch bun release from GitHub | On Forgejo, `github.token` is the Forgejo token — may not auth to GitHub; provide fallback | -| 285-294 | "Revoke app token" step | Calls `${GITHUB_API_URL}/installation/token` which doesn't exist on Forgejo | Make conditional: `if: platform != 'forgejo'` or detect and skip | -| N/A | Missing | No `platform` input | Add `platform` input (auto/github/forgejo) | - -### `base-action/` Directory - -`run.ts` imports directly from `base-action/src/`: - -- `validate-env.ts` - checks env vars for Claude CLI (platform-agnostic) -- `setup-claude-code-settings.ts` - writes Claude settings (platform-agnostic) -- `install-plugins.ts` - installs Claude plugins (platform-agnostic) -- `prepare-prompt.ts` - prepares prompt file (platform-agnostic) -- `run-claude.ts` / `run-claude-sdk.ts` - runs Claude CLI (platform-agnostic) - -**Conclusion**: `base-action/` is platform-independent. No modifications needed. - -### MCP Server Naming Strategy - -**Keep existing server names** (`github_comment`, `github_ci`, `github_file_ops`, `github_inline_comment`) even for Forgejo implementations. These are internal identifiers referenced in hardcoded tool name lists in `src/modes/tag/index.ts` (lines 127-152): - -```typescript -"mcp__github_comment__update_claude_comment", -"mcp__github_ci__get_ci_status", -"mcp__github_file_ops__commit_files", -``` - -Renaming would require modifying the tool name lists — more merge-conflict surface for zero user benefit. The MCP server config in `install-mcp-server.ts` will point these names to either GitHub or Forgejo server implementations based on platform. - -## CI/CD Workflows - -### Handling Upstream `.github/workflows/` - -The upstream repo has 13 workflows in `.github/workflows/`. Strategy: - -- **Do NOT delete or modify** `.github/workflows/` files (merge conflicts) -- Create `.forgejo/workflows/` with our custom workflows -- **Configure Forgejo runner** to only read from `.forgejo/workflows/` (runner-level `DEFAULT_ACTIONS_URL` and workflow directory settings) -- **Document this as a setup requirement** — if runners aren't configured, upstream workflows will attempt to run and fail loudly (missing secrets, OIDC endpoints, etc.) - -### Custom Forgejo Workflows - -#### 1. CI Test Workflow (`.forgejo/workflows/ci.yml`) - -- Triggers on push to main and PRs -- Runs `bun install` and `bun test` -- Lints with biome (matching upstream's toolchain) - -#### 2. Action Self-Test Workflow (`.forgejo/workflows/test-action.yml`) - -- **Tests the action itself** on Forgejo -- Creates a test issue via API, triggers the action with a simple prompt -- Verifies: platform detection returns "forgejo", comment is created on the issue, action completes successfully -- Validates end-to-end: token handling, Octokit REST calls, comment posting -- Runs on a schedule (weekly) and on PRs that modify `src/`, `action.yml`, or `.forgejo/` - -#### 3. Upstream Sync Check (`.forgejo/workflows/upstream-sync.yml`) - -- Scheduled workflow (weekly) + manual trigger -- Fetches `upstream/main`, compares with our `main` using `git rev-list` -- Creates an issue if upstream has new commits we haven't merged -- Includes commit count and summary of changed files to help assess merge effort - -## Milestones - -### Phase 1 - Foundation (parallel, no dependencies) - -- [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. - -- [x] **M2: Forgejo token handling** — Create `src/forgejo/token.ts` that returns `GITHUB_TOKEN` / `FORGEJO_TOKEN` directly (no OIDC, no Anthropic exchange). Wire into `run.ts` with an early platform branch before the existing `setupGitHubToken()` call. - - - Files: `src/forgejo/token.ts` (NEW), `src/entrypoints/run.ts` (MODIFIED — surgical addition at top of `run()`) - - Depends on: M1 - - Success: Token is available without OIDC on Forgejo. GitHub path unchanged. - -- [x] **M3: Forgejo CI workflows** — Created `.forgejo/workflows/` with three workflows adapted for Forgejo `grunt` runner: `ci.yml` (3 separate jobs: test, prettier, typecheck using curl-based bun install + `xternal/checkout`), `test-action.yml` (agent-mode self-test with `FORGEJO_PLATFORM_TEST_OK` marker, runs weekly + on PRs to src/action.yml/.forgejo), `upstream-sync.yml` (weekly upstream check, creates/updates issue with `upstream-sync` label including commit log, diff stat, and merge instructions). - - - Files: `.forgejo/workflows/ci.yml`, `.forgejo/workflows/test-action.yml`, `.forgejo/workflows/upstream-sync.yml` (all NEW) - - Success: CI runs tests on push. Upstream `.github/workflows/` don't interfere. - -### Phase 2 - Core Data Fetching (the hard part) - -- [x] **M4a: Forgejo REST data fetching — PR/Issue basics** — Implement REST-based fetching for PR details, issue details, changed files, and commits. Produce output matching the `FetchDataResult` type that existing code expects. - - - Files: `src/forgejo/data/fetcher.ts` (NEW) - - Depends on: M1, M2 - - Success: Can fetch PR/issue title, body, author, state, files, commits from Forgejo REST API. - -- [x] **M4b: Forgejo REST data fetching — Comments, reviews, inline comments** — Extend the fetcher with paginated comment/review fetching, including nested inline review comments (requires per-review API calls). Implement TOCTOU filtering using the same `filterCommentsToTriggerTime` / `filterReviewsToTriggerTime` functions (reuse from `src/github/data/fetcher.ts` — these are pure logic). Use `Promise.all()` for parallel API calls. - - - Files: `src/forgejo/data/fetcher.ts` (extend from M4a) - - Depends on: M4a - - Success: Full parity with GitHub GraphQL data fetch. All comments, reviews, and inline comments included with proper TOCTOU filtering. - -- [x] **M5: Forgejo comment URL formatting** — Create `src/forgejo/comment-logic.ts` with Forgejo-specific URL patterns for job links, branch links, and PR creation links (used in tracking comment updates). - - Files: `src/forgejo/comment-logic.ts` (NEW) - - Depends on: M1 - - Success: Comment formatting produces correct Forgejo URLs. - -### Phase 3 - Integration (wiring it together) - -- [x] **M6: MCP server platform routing** — Modified `install-mcp-server.ts` to add platform detection. Skips GitHub public MCP server (`ghcr.io/github/github-mcp-server`) on Forgejo. Skips `checkActionsReadPermission` on Forgejo (workflow token has full repo access). Did NOT create `src/mcp/forgejo-actions-server.ts` — existing `github-actions-server.ts` works on Forgejo via Octokit REST + `GITHUB_API_URL` (zero duplication, easier maintenance). Server names remain `github_comment`, `github_ci`, etc. - - - Files: `src/mcp/install-mcp-server.ts` (MODIFIED), `test/install-mcp-server.test.ts` (MODIFIED — 6 new Forgejo tests) - - Depends on: M4b, M5 - - Success: All MCP servers route correctly on both platforms. 772 tests pass. - -- [x] **M7: Entry point + mode handler integration** — Wired platform detection into mode handlers. In `tag/index.ts`: platform branch routes to `fetchForgejoData` vs `fetchGitHubData`. `createInitialComment` auto-detects platform for Forgejo spinner/URL format. `update-comment-link.ts` uses `updateForgejoCommentBody` + Forgejo PR compare URL on Forgejo. Agent mode needed NO changes (token already in `run.ts`, rest uses Octokit REST). 11 new integration tests in `test/m7-integration.test.ts`. - - - Files: `src/modes/tag/index.ts` (MODIFIED), `src/entrypoints/update-comment-link.ts` (MODIFIED), `src/github/operations/comments/create-initial.ts` (MODIFIED), `src/github/operations/comments/update-with-branch.ts` (MODIFIED), `test/m7-integration.test.ts` (NEW) - - Depends on: M4b, M5, M6 - - Success: All platform routing correct on both platforms. 785 tests pass. - -### Phase 4 - Validation & Polish - -- [x] **M8: End-to-end testing on Forgejo** — Tested agent mode (workflow_dispatch) end-to-end on Forgejo runner. Fixed multiple issues discovered during E2E: permission check switched to `repos.get()` (Forgejo lacks collaborator permission endpoint for workflow tokens), actor check skipped entirely on Forgejo (no bot accounts + `read:user` scope unavailable), `oven-sh/setup-bun` replaced with universal curl install in `action.yml` (Forgejo runner pre-fetches all `uses:` refs even when `if:` would skip them), CI test env override for `GITHUB_SERVER_URL` (module-level constants evaluated at import time). Self-test workflow passes: platform detection, token auth, actor skip, Claude Code execution, output validation all green. CI tests: 794 pass, 0 fail. - - - Files: `action.yml` (MODIFIED — curl bun install), `src/github/validation/actor.ts` (MODIFIED — Forgejo skip), `test/actor.test.ts` (MODIFIED — Forgejo tests), `.forgejo/workflows/ci.yml` (MODIFIED — inline export fix) - - Depends on: M7 - - Success: Agent mode works on Forgejo. Self-test workflow passes. CI tests pass. Note: tag mode (issue trigger, PR review) not yet E2E tested — code paths are wired but await real-world validation. - -- [x] **M9: Documentation & upstream merge validation** — Created comprehensive `docs/forgejo.md` setup guide covering prerequisites, quick start (tag + agent mode), platform detection, GitHub vs Forgejo differences (auth, API, bun install, comment URLs), CI workflows, upstream merge process with conflict mitigation, known limitations, and troubleshooting. Updated `README.md` (Forgejo in description, feature bullet, docs link), `CLAUDE.md` (Forgejo architecture section with key files and integration points), `docs/usage.md` (`platform` input in table), `docs/faq.md` (5 Forgejo FAQ entries). Upstream merge validation: `git merge upstream/main` → zero conflicts — branch based on latest upstream `ade221f`. All 794 tests pass. - - - Files: `docs/forgejo.md` (NEW), `README.md` (MODIFIED), `CLAUDE.md` (MODIFIED), `docs/usage.md` (MODIFIED), `docs/faq.md` (MODIFIED) - - Depends on: M8 - - Success: Zero-conflict upstream merge. Complete Forgejo setup documentation with troubleshooting guide. - -### Milestone Dependency Graph - -``` -M1 ──> M2 ──> M4a ──> M4b ──┬──> M6 ──> M7 ──> M8 ──> M9 -M1 ──> M5 ───────────────────┘ -M3 (independent, anytime) -``` - -**Parallel opportunities:** - -- M3 is fully independent — can run anytime -- M5 and M2 can run in parallel (both only depend on M1) -- M4a can start as soon as M2 is done -- M6 and M7 are sequential (M7 depends on M6) - -## Execution Plan - -| Phase | Milestones | Files Touched | Parallel? | -| ----- | ------------ | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | -| 1 | M1, M2, M3 | `action.yml`, `src/platform/detector.ts`, `src/forgejo/token.ts`, `src/entrypoints/run.ts`, `.forgejo/workflows/*` | M1 first, then M2+M3 in parallel | -| 2 | M4a, M4b, M5 | `src/forgejo/data/fetcher.ts`, `src/forgejo/comment-logic.ts` | M5 parallel with M4a/M4b | -| 3 | M6, M7 | `src/mcp/install-mcp-server.ts`, `src/mcp/forgejo-actions-server.ts`, `src/modes/tag/index.ts`, `src/modes/agent/index.ts` | Sequential | -| 4 | M8, M9 | Tests, docs, `CLAUDE.md` | Sequential | - -## Risk Assessment - -| Risk | Likelihood | Impact | Mitigation | -| -------------------------------------------------------------- | ------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Upstream refactors `run.ts` or mode handlers | Medium | High | Keep entry point changes to <10 lines; use early-return pattern; platform branch at top of functions | -| Octokit REST methods behave differently on Forgejo | Low | Medium | Test each used method early in M4a; fall back to raw `fetch()` if needed | -| Forgejo webhook payloads differ from `@octokit/webhooks-types` | Medium | Medium | Test with real Forgejo webhooks; key check: `payload.issue.pull_request` field for `is_pull` detection | -| `pull_request_target` event missing in Forgejo | High | Low | Already handled — `context.ts:188` maps it to `pull_request` | -| Forgejo Actions API missing endpoints (downloadJobLog) | Medium | Medium | Degrade gracefully — CI MCP server may offer fewer tools on Forgejo | -| `oven-sh/setup-bun` action fails on isolated Forgejo runners | **RESOLVED** | Medium | **Fixed**: Replaced `oven-sh/setup-bun` entirely with universal curl install in `action.yml`. Forgejo runner pre-fetches all `uses:` refs before evaluating `if:` conditions, so conditional skip was insufficient. Mirror created at `xternal/setup-bun` for future reference. | -| Upstream `.github/workflows/` run and fail on Forgejo | Medium | Low | Document runner configuration; provide `.forgejo/workflows/` overrides | -| Upstream adds new features requiring new GraphQL fields | Certain | Low | New features won't have Forgejo support until we add REST equivalents to `forgejo/data/fetcher.ts` | - -## Success Criteria - -1. Action runs on Forgejo for issue, PR, and workflow_dispatch triggers -2. Claude can read issue/PR context, post comments, create branches, and commit files on Forgejo -3. `git fetch upstream && git merge upstream/main` produces conflicts only in the 3-4 files we modified, and those conflicts are resolvable in <15 minutes -4. Self-test workflow validates the action works on every PR -5. Upstream sync workflow alerts when new upstream changes are available -6. Total new files: ~6-8 (not 25+) - -## Out of Scope (for now) - -- GitHub Enterprise Server support -- Gitea support (similar to Forgejo but may diverge) -- `pull_request_target` event support -- SSH signing on Forgejo (can be added later) -- GitHub MCP Server equivalent for Forgejo (the `ghcr.io/github/github-mcp-server` image) -- Full Forgejo Actions API parity (downloadJobLog may be missing) diff --git a/prds/done/5-dedicated-forgejo-user.md b/prds/done/5-dedicated-forgejo-user.md deleted file mode 100644 index b042763..0000000 --- a/prds/done/5-dedicated-forgejo-user.md +++ /dev/null @@ -1,109 +0,0 @@ -# PRD #5: Dedicated Forgejo User for Claude Bot - -**Issue**: [#5](https://git.wxs.ro/actions/claude-code-action/issues/5) -**Priority**: High -**Created**: 2026-02-25 -**Status**: Complete (2026-02-25) - ---- - -## Problem - -Claude currently runs under the human operator's personal Forgejo token (`FORGEJO_TOKEN` / `github_token`). This causes: - -1. **Attribution confusion**: Commits and comments appear as the human user, not as a bot -2. **Over-privileged access**: The bot inherits all permissions of the human account -3. **Audit trail**: No clear separation between human actions and bot actions in repo history -4. **Security**: If the action is compromised, the human's full token is exposed - -On GitHub, this is solved by the GitHub App OIDC token exchange (bot gets its own `claude[bot]` identity). On Forgejo, there's no such mechanism — we need a manual equivalent. - -## Solution - -Create a dedicated `claude-bot` user on the Forgejo instance (git.wxs.ro), generate a scoped API token for it, and configure the action workflow to use that token. This gives Claude its own identity for commits, comments, and API operations. - -## Milestones - -### Phase 1: Forgejo Admin Setup - -- [x] **M1: Create dedicated `claude-bot` user on git.wxs.ro** - - - Create user via Forgejo admin panel or API (`POST /admin/users`) - - Set appropriate display name (e.g., "Claude Bot") - - Set avatar to distinguish from human users - - User should NOT have admin privileges - - **Validation**: User exists and can log in - - **Evidence**: User ID 48, `is_admin: false`, avatar set to Claude logo - -- [x] **M2: Configure bot user permissions and token** - - Add `claude-bot` to the `actions` org (and any other orgs where the action runs) - - Grant `write` permission on repos where Claude needs to operate - - Generate a scoped API token for the bot user with minimal required permissions: - - `repo` (read/write issues, PRs, comments, branches, commits) - - `packages` (if needed for container operations) - - NO admin scope, NO user scope beyond what's needed - - **Validation**: Token can create comments and push branches on a test repo - - **Evidence**: `Claude` team with `write` permission created in all 12 orgs, token validated (comment create/delete on issue #5, `push: true` on k8s/flux, crossplane/harbor-resources) - -### Phase 2: Action Configuration - -- [x] **M3: Update workflow to use bot token** - - - Store the bot token as a Forgejo Actions secret (e.g., `CLAUDE_BOT_TOKEN`) - - Update `claude.yml` workflow to pass `github_token: ${{ secrets.CLAUDE_BOT_TOKEN }}` instead of the human token - - Update `bot_id` and `bot_name` inputs to match the new Forgejo user's ID and username - - Document the token rotation procedure - - **Validation**: Action runs with bot token, comments appear as `claude-bot` - - **Evidence**: `CLAUDE_BOT_TOKEN` org secret in all 12 orgs, `claude.yml` updated with `bot_id: "48"`, `bot_name: "claude-bot"`, `bot_email: "claude@wxs.ro"`. New `bot_email` input added to `action.yml`. - - [x] **E2E validated**: `@claude` mention on PR #8 — claude-bot responded with full review, tracking comment updated correctly - -- [x] **M4: Verify git commit attribution** - - Ensure `git config user.name` and `user.email` use the bot user's identity - - Verify that `src/github/operations/git-config.ts` correctly picks up the bot user info from the token - - Test that pushed commits show the bot user as author in Forgejo UI - - **Validation**: A tag-mode response that creates a branch shows `claude-bot` as commit author - - **Evidence**: `git-config.ts` uses `user.email` when provided (falls back to noreply), `context.ts` reads `BOT_EMAIL` env var, all call sites pass `botEmail`. 797 tests pass. - - [ ] **Deferred**: Branch creation + commit authorship E2E (will be validated naturally on next code-changing `@claude` request) - -### Phase 3: Documentation & Hardening - -- [x] **M5: Update documentation and test across repos** - - [x] Update `docs/forgejo.md` with bot user setup instructions (full 6-step guide added) - - [x] Document token rotation procedure (tokens expire, need renewal plan) - - [~] Test the bot user setup on at least one additional repo beyond `claude-code-action` (deferred — bot token is org-level, will work on first use) - - [x] Update `CLAUDE.md` if any workflow changes affect development - - **Validation**: Another developer can follow the docs to set up the bot user on a new Forgejo instance - -## Dependencies - -- Forgejo admin access to git.wxs.ro (to create the user) -- Understanding of Forgejo's permission model (org membership, repo access) - -## Risks - -| Risk | Mitigation | -| --------------------------------------- | ---------------------------------------------------------------------- | -| Token expiration | Document rotation procedure, set long-lived token or calendar reminder | -| Bot user locked out | Keep admin access to reset password/token | -| Permission too narrow | Start with `write` on repos, expand only if needed | -| Bot user confused with human in reviews | Set clear display name and avatar | - -## Success Criteria - -- All Claude comments and commits on Forgejo repos appear as `claude-bot`, not the human user -- The bot token has minimal scoped permissions (no admin access) -- Documentation allows reproducing the setup on a fresh Forgejo instance - -## Implementation Notes - -### New `bot_email` Action Input - -Added `bot_email` input to `action.yml` — when set, overrides the default noreply email format (`{id}+{name}@users.noreply.{host}`). On Forgejo, setting this to the bot user's registered email links commits to the bot's profile in the UI. - -### Org-Level Secrets - -`CLAUDE_BOT_TOKEN` is stored as an org-level secret (not repo-level) in all 12 orgs with `visibility: "all"`. This means any new repo in any org will automatically have access. - -### Team Structure - -Each org has a `Claude` team with `write` permission on all repositories (`includes_all_repositories: true`). New repos are automatically covered. diff --git a/prds/done/6-e2e-test-forgejo-triggers.md b/prds/done/6-e2e-test-forgejo-triggers.md deleted file mode 100644 index 7553e6c..0000000 --- a/prds/done/6-e2e-test-forgejo-triggers.md +++ /dev/null @@ -1,144 +0,0 @@ -# PRD #6: E2E Test All Untested Forgejo Trigger Scenarios - -**Issue**: [#6](https://git.wxs.ro/actions/claude-code-action/issues/6) -**Priority**: High -**Created**: 2026-02-25 - ---- - -## Problem - -The Forgejo platform support (PRD #1) was validated for the two most common scenarios (agent mode via `workflow_dispatch` and tag mode via `issue_comment`). However, 5 trigger scenarios remain untested in a live environment: - -1. PR review comment triggers (`pull_request_review_comment`) -2. PR review triggers (`pull_request_review`) -3. Label triggers (`issues` with `labeled` action) -4. Assignee triggers (`issues` with `assigned` action) -5. Branch creation + code changes from tag mode (the initial test only asked a question, no code was generated) - -These untested paths could have silent failures — the code may look correct but break on Forgejo's subtly different webhook payloads, API responses, or event structures. - -## Solution - -Systematically exercise each untested scenario on the live Forgejo instance (git.wxs.ro), document results, fix any issues discovered, and update the E2E status table in `docs/forgejo.md`. - -## Milestones - -- [x] **M1: Ensure workflow covers all trigger types** - - - Review `.forgejo/workflows/claude.yml` to confirm all 5 trigger types are listed - - Add any missing triggers to the workflow file - - Ensure the workflow has the correct permissions for each trigger type - - **Validation**: Workflow YAML includes all required `on:` triggers - -- [x] **M1.5: Fix image-downloader test failures blocking CI** - - - **Problem**: All 18 `downloadCommentImages` tests failed on Forgejo CI (Linux Bun 1.2.21 in `harbor.wxs.ro/wxs/grunt-runner:v1.9.0`) while passing locally on macOS. This blocked all PRs. - - **Root cause**: `mock.module("../src/github/utils/image-downloader")` in `test/forgejo-data-fetcher.test.ts` replaced the image-downloader module **globally** in Bun's module registry for the entire process. When Bun's non-deterministic test file ordering ran forgejo-data-fetcher.test.ts BEFORE image-downloader.test.ts, the latter imported the stubbed module instead of the real one, breaking all mock tracking. The failure was intermittent because file ordering changed when other files were added/removed. - - **Fix (2 parts)**: - 1. `c6ca8a7`: Added DI (`FsDeps` interface + optional `fsDeps` param) to `downloadCommentImages()` — removed `mock.module("fs/promises")` from the image-downloader test itself - 2. `49eedd5`: Removed all image-downloader mocking from forgejo-data-fetcher.test.ts entirely — the real function is safe to call because test comments have no GitHub image URLs, so it returns an empty Map naturally. Both `mock.module` and `spyOn` caused cross-file pollution in Bun's single-process runner. - - **Key learning**: Never use `mock.module()` in Bun test files — it poisons the module registry for all files in the process. `spyOn` on ESM namespace exports also fails to restore correctly across files on Linux Bun. Best approach: avoid mocking entirely when the real function is safe to call. - - **Validation**: CI passed (all 3 jobs: test, prettier, typecheck) on commit `49eedd5` - -- [x] **M2: Test PR review comment trigger** - - - Create a test PR on the repo ✓ (PR #15) - - Post a review comment with `@claude` mention ✓ (review comments on `test/e2e-prd6-m2.txt`) - - Verify: platform detection, context fetching (PR diff, review comment body), tracking comment, Claude response ✓ - - Document any issues and fix them ✓ (3 bugs found and fixed, see below) - - **Validation**: Claude responds correctly to `@claude` in a PR review comment ✓ (run #12075, 32s, full PR summary posted) - - **Bugs found & fixed**: - 1. `9d8c96f`–`ac8e22e`: Forgejo `pull_request_review_comment` payload lacks the top-level `comment` object — added fallback to issue comment creation and 404 fallback for review comment fetch - 2. `e73c881`: Forgejo sends event name `pull_request_comment` (not `pull_request_review_comment`) — skipped `commentBody` validation for this event - 3. `c3c3eb5`: **Critical** — MCP comment server set `isPullRequestReviewComment=true` on Forgejo, causing every Claude update to try `pulls.updateReviewComment` (404) and silently fail. Fix: detect Forgejo via `GITHUB_API_URL` and route to issue comment API. Also hardened `update-claude-comment.ts` fallback to catch 422 alongside 404, and skipped the wasted `pulls.getReviewComment` call in `update-comment-link.ts` on Forgejo. - - **Key learning**: On Forgejo, the tracking comment for review comment events is always an issue comment (not a PR review comment reply) because the payload lacks `comment.id`. All update paths must account for this. - -- [x] **M3: Test PR review trigger** - - - Reused test PR #15 from M2 ✓ - - Submitted reviews with `@claude` mention in the review body ✓ (review IDs 20, 21, 22) - - Verify: platform detection, context fetching, tracking comment, Claude response ✓ - - Document any issues and fix them ✓ (1 finding, 1 fix, see below) - - **Validation**: Claude responds correctly to `@claude` in a PR review ✓ (runs 12080, 12087) - - **Finding**: Forgejo sends `pull_request_comment` instead of `pull_request_review` for review submissions — same pattern as M2. The `pull_request_review` trigger and workflow condition (line 21) are dead code on Forgejo. - - **Fix** (`e514bf5`→`b21dbd6`): Replaced blind Forgejo auto-trigger in `trigger.ts` with payload-aware `@claude` check that inspects `comment.body` and `review.body` with fallback. Attempted workflow `if:` gating but reverted — Forgejo's `pull_request_comment` payload does not populate `github.event.comment.body` or `github.event.review.body` (run 12084 was skipped). - - **Key learning**: On Forgejo, the workflow `if:` condition cannot reliably gate on payload body fields for `pull_request_comment` events. The `@claude` check must happen in application code (`trigger.ts`), not in the workflow condition. - -- [x] **M4: Test label trigger** - - - Created `claude` label (id: 125) on the repo ✓ - - Applied label to test issues (#17, #18, #19) ✓ - - Verify: trigger detection, context fetching (issue body/title), Claude response ✓ - - Document any issues and fix them ✓ (3 bugs found and fixed, see below) - - **Validation**: Claude responds when the configured label is applied to an issue ✓ (run 52795, 20s, 2 turns, $0.12) - - Also verified: creating issue with label in one go fires exactly 1 run (issue #19) - - **Bugs found & fixed**: - 1. `a864b16`: Workflow `if:` expression parser rejects `#` comments — Forgejo runner's expression lexer treats `#` as invalid token. Moved all comments outside the `if:` block, switched from `|` to `>-` scalar. - 2. `414c705`→`a864b16`: Forgejo does not populate `github.event.action` in workflow `if:` context for `issues` events (run 12061 skipped despite action being `labeled`). Let all `issues` events through the gate; `trigger.ts` validates. - 3. `baa007a`: Forgejo sends `label_updated` instead of `labeled` as `eventAction` for issue label events. Added `FORGEJO_EVENT_ACTION_MAP` in `context.ts` to normalize at parse time (same pattern as event name normalization). - - **Key learning**: Forgejo's `issues` event action names differ from GitHub — `label_updated` not `labeled`. Normalize in `context.ts` at parse time so all downstream code works unchanged. Creating an issue with a label fires both `opened` and `label_updated` as separate events — do NOT check labels on `opened` to avoid duplicate runs. - -- [x] **M5: Test assignee trigger** - - - Created test issue #20, assigned `claude` user via API ✓ - - Verify: trigger detection, context fetching, Claude response ✓ - - Document any issues and fix them ✓ (1 bug found and fixed, see below) - - **Validation**: Claude responds when assigned to an issue ✓ (run 52842, 2 turns, $0.09) - - **Bug found & fixed**: - 1. `99624ef`: Forgejo's `issues/assigned` webhook payload lacks the top-level `assignee` object — `payload.assignee` is `undefined`. The assignee is only available at `payload.issue.assignee`. Added fallback in `trigger.ts` to check `issue.assignee.login` when `payload.assignee.login` is missing. - - **Also discovered**: Forgejo sends `assigned` as the event action (matching GitHub) — no normalization needed, unlike M4's `label_updated`. - - **Also discovered**: Forgejo's API `PATCH /issues/{index}` with `{"assignees": ["user"]}` (plural array) returns 404. Must use deprecated singular `{"assignee": "user"}` field instead. - - **Key learning**: On Forgejo, the `issues/assigned` webhook payload structure differs from GitHub — the assigned user is nested under `issue.assignee`, not at the top-level `assignee` field. Same pattern as M2's missing top-level `comment` object. - -- [x] **M6: Test tag mode with code changes (branch creation)** - - - Created test issue #21 requesting creation of `test/e2e-prd6-m6.txt` ✓ - - Triggered Claude via `@claude` mention in issue comment ✓ - - Verify: Claude creates a branch, pushes commits, links the branch in tracking comment ✓ - - Branch: `claude/issue-21-20260226-0917` - - Commit: `59f7e4e` by `claude ` (bot identity correct) - - File content verified via API: matches requested content - - Verify the Forgejo-specific PR compare URL format is correct ✓ (after fix, see below) - - **Validation**: Claude successfully creates a branch with code changes on Forgejo ✓ (run #198, 41s) - - **Bug found & fixed**: - 1. `f08b1b4`: Prompt template in `src/create-prompt/index.ts` (lines 604, 794) hardcoded `quick_pull=1` (GitHub-specific) in the "Create a PR" URL. On Forgejo, this param is ignored so the compare page doesn't auto-expand the PR creation form. Forgejo uses `expand=1`. Fix: made prompt template platform-aware via `detectPlatform()`. Note: `updateForgejoCommentBody()` extracts PR URLs from comment content (`prLinkFromContent`) with priority over the URL from `update-comment-link.ts`, so the prompt template is the correct fix location. - - **Key learning**: The PR compare URL appears in two places — the prompt template (used by Claude to write the URL into comment body) and `update-comment-link.ts` (post-execution update). Because `updateForgejoCommentBody()` prioritizes URLs found in existing content, both must use the correct platform-specific query parameter. Fix validated after merge to `main` (workflow runs from default branch for `issue_comment` events). - -- [x] **M7: Update documentation with E2E results** - - Updated `docs/forgejo.md` E2E status table with all 5 validated trigger scenarios ✓ - - Removed "Not Yet E2E Tested" section entirely — all items validated ✓ - - Added new "Webhook & Event Differences" section with 12-row comparison table documenting all Forgejo payload/event divergences ✓ - - Added 4 new Known Limitations entries (dead `pull_request_review` trigger, review comments as issue comments, unreliable workflow `if:`, assignees API) ✓ - - Added 5 new Troubleshooting entries (review reply tracking, `if:` skipping, `#` parse errors, duplicate label fires, `mock.module()` pollution) ✓ - - Expanded "Tag Mode Verified Capabilities" from 6 to 10 bullet points ✓ - - **Validation**: `docs/forgejo.md` accurately reflects the current E2E validation status ✓ - -## Dependencies - -- PRD #5 (Dedicated Forgejo User) is recommended but not strictly required — tests can run with the current token setup -- A test repository on git.wxs.ro with the action workflow configured - -## Risks - -| Risk | Mitigation | -| -------------------------------------------------------- | ------------------------------------------------------------ | -| Forgejo webhook payloads differ from expected | Log raw payloads, compare with GitHub format, adapt parsing | -| Trigger type not supported by Forgejo Actions | Document as known limitation, skip gracefully | -| Action creates unwanted branches/comments during testing | Use a dedicated test repo or clean up after each test | -| Bun mock.module breaks mock tracking on Linux | RESOLVED: use spyOn on module namespace, never mock.module | -| MCP comment updates silently fail on Forgejo | RESOLVED: route to issue comment API when platform=forgejo | -| Workflow if: cannot gate on payload body for Forgejo | RESOLVED: @claude check moved to trigger.ts application code | -| Workflow if: expression parser rejects # comments | RESOLVED: moved comments outside if: block, use >- scalar | -| Forgejo event action names differ from GitHub | RESOLVED: FORGEJO_EVENT_ACTION_MAP normalizes at parse time | -| Creating issue with label fires duplicate runs | RESOLVED: label check only on labeled event, not on opened | -| Forgejo assigned payload lacks top-level assignee | RESOLVED: fallback to issue.assignee.login in trigger.ts | -| Forgejo API assignees array field returns 404 | RESOLVED: use deprecated singular assignee field instead | -| Prompt template hardcodes quick_pull=1 in PR compare URL | RESOLVED: platform-aware expand=1 for Forgejo in prompt | - -## Success Criteria - -- All 5 previously untested scenarios have been exercised on a live Forgejo instance -- Any bugs discovered are fixed and tested -- `docs/forgejo.md` E2E status table is fully up-to-date -- "Not Yet E2E Tested" section is empty (all items moved to validated or documented as unsupported) diff --git a/prds/done/7-senior-review-forgejo.md b/prds/done/7-senior-review-forgejo.md deleted file mode 100644 index a198701..0000000 --- a/prds/done/7-senior-review-forgejo.md +++ /dev/null @@ -1,130 +0,0 @@ -# PRD #7: Senior Developer Review of Forgejo Platform Support - -**Issue**: [#7](https://git.wxs.ro/actions/claude-code-action/issues/7) -**Priority**: Medium -**Created**: 2026-02-25 - ---- - -## Problem - -The Forgejo platform support was implemented across multiple sessions (PRD #1). While it works for the validated scenarios, a comprehensive senior-level review is needed to: - -- Catch architectural issues that may cause problems at scale or during upstream merges -- Identify edge cases and error handling gaps -- Assess test coverage adequacy -- Verify security posture (token handling, input sanitization, permission checks) -- Ensure maintainability and code quality standards - -This review should treat the codebase as if it were being submitted for a production readiness review. - -## Solution - -Perform a thorough, documented code review of all Forgejo-related changes. The review should cover every new file, every modified file, and the overall architecture. Findings should be categorized by severity and each issue should be either fixed or documented as an accepted risk. - -## Review Scope - -### New Files (Forgejo-Specific) - -- `src/platform/detector.ts` — Platform detection logic -- `src/forgejo/token.ts` — Token handling -- `src/forgejo/data/fetcher.ts` — REST data fetching (replaces GraphQL) -- `src/forgejo/comment-logic.ts` — URL patterns for tracking comments - -### Modified Files (Platform Branches) - -- `src/entrypoints/run.ts` — Token setup platform branch -- `src/modes/tag/index.ts` — Data fetcher swap -- `src/github/operations/comments/create-initial.ts` — Platform-aware spinner/URL -- `src/github/operations/comments/update-with-branch.ts` — PR compare URL -- `src/github/operations/branch.ts` — `repos.getBranch()` on Forgejo -- `src/github/validation/actor.ts` — Skipped on Forgejo -- `src/mcp/install-mcp-server.ts` — Skip GitHub MCP on Forgejo -- `action.yml` — New inputs, env vars, platform support - -### Test Files - -- All `__tests__/` files related to Forgejo functionality -- Test coverage gaps - -### CI/CD - -- `.forgejo/workflows/` — All workflow files - -### Documentation - -- `docs/forgejo.md` — Accuracy and completeness -- `CLAUDE.md` — Forgejo section accuracy - -## Milestones - -- [x] **M1: Architecture & design review** - - - Review the lean adapter pattern: is it the right abstraction level? - - Assess merge-conflict surface with upstream: are platform branches minimal? - - Check for code duplication that could be consolidated - - Review the platform detection logic (3-tier: explicit > env > heuristic) - - Evaluate whether the `src/forgejo/` directory structure scales - - **Deliverable**: Architecture findings document with severity ratings - -- [x] **M2: Error handling & edge case review** - - - Review every `try/catch` block in Forgejo code paths - - Check for unhandled promise rejections - - Verify graceful degradation when Forgejo APIs behave differently - - Check error messages — are they actionable for users? - - Review fallback behavior (e.g., `repos.get()` fallback for permissions) - - **Deliverable**: List of error handling gaps with fixes - -- [x] **M3: Security review** - - - Token handling: is the token properly scoped and never logged? - - Input sanitization: can webhook payloads inject content? - - Permission checks: are they sufficient on Forgejo? - - Secrets in logs: review all `core.info()` / `core.debug()` calls - - **Deliverable**: Security findings with severity ratings - -- [x] **M4: Test coverage review & improvements** - - - Assess existing unit test coverage for Forgejo code paths - - Identify critical paths without test coverage - - Write or improve tests for the most critical gaps - - Verify mocks accurately represent Forgejo API responses - - **Deliverable**: Test coverage improved, gaps documented - -- [x] **M5: Code quality & maintainability review** - - - TypeScript strictness: are types accurate or using `any` / type assertions? - - Naming conventions: consistent with the rest of the codebase? - - Dead code: any unused imports, unreachable branches, commented-out code? - - Magic strings/numbers: should any be extracted to constants? - - Code comments: are complex decisions documented? - - **Deliverable**: Code quality fixes applied - -- [x] **M6: Documentation accuracy review** - - Cross-check `docs/forgejo.md` against actual code behavior - - Verify all "Known Limitations" are still accurate - - Check that code examples and configuration snippets work - - Ensure `CLAUDE.md` Forgejo section matches current state - - **Deliverable**: Documentation corrections applied - -## Deliverables - -Each milestone produces a findings document (can be inline in a PR or as comments on issue #7) with: - -- **Critical**: Must fix before production use -- **Major**: Should fix, risk of user-facing issues -- **Minor**: Nice to have, code quality improvements -- **Note**: Observations, not necessarily actionable - -## Dependencies - -- None — this is a review of existing code - -## Success Criteria - -- All Forgejo-related code has been reviewed by the criteria above -- All Critical and Major findings are resolved -- Minor findings are either fixed or documented as accepted -- Test coverage is adequate for critical paths -- Documentation accurately reflects the implementation