Add idempotency to the merge-upstream job:
1. Check for existing open PR from chore/upstream-sync-* branch
2. If PR exists and branch is up to date with upstream → skip
3. If PR exists but upstream has new commits → checkout existing
branch, merge new upstream commits, run Claude to review/test
4. If no PR exists → create new branch (existing behavior)
Also update Claude's prompt to handle both create and update PR
cases, so it knows to push to the existing PR branch instead of
creating a duplicate.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three fixes in the upstream-sync workflow:
1. Replace github.token with CLAUDE_BOT_TOKEN in the check-upstream
job. The default runner token on Forgejo doesn't have issue-write
permissions, so issues were silently failing to create.
2. Output the tracking issue number from check-upstream and pass it
to the merge-upstream job. Claude's prompt now includes the issue
reference and instructs it to add "Closes #N" in the PR body.
3. Capture the issue number from both create and update paths so
the merge job always has it available.
Also cleaned up 6 duplicate upstream-sync labels created by previous
runs that used the unprivileged token.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two fixes for the upstream-sync Claude workflow:
1. Add Bun install + bun install (deps) steps before Claude runs,
so tsc, prettier, and other devDependencies are available.
Previously node_modules/.bin/ didn't exist and typecheck/format
commands failed with "command not found".
2. Add Bash(bunx:*), Bash(npx:*), Bash(./node_modules/.bin/*:*)
to allowed_tools. The existing Bash(bun:*) only covers the `bun`
binary, not `bunx` which is a separate binary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- CLAUDE.md: add "Things That Will Bite You" entry about bare Bash
requiring interactive approval and needing sub-patterns in CI
- docs/forgejo.md: update allowed_tools section with sub-pattern
examples, agent mode fix note, and common patterns reference
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Agent mode never passed ALLOWED_TOOLS_INPUT to the Claude SDK —
only tag mode did (via --allowedTools in claudeArgs). This meant
the allowed_tools action input was silently ignored in agent mode,
causing all Bash commands to require interactive approval in CI.
Pass ALLOWED_TOOLS_INPUT directly as options.allowedTools, which
the SDK merges with any tools from claudeArgs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace raw JSON dumps with concise, scannable log lines when
show_full_output is enabled. Adds an onMessage callback to the SDK
runner that formats each message type into 1-2 line summaries with
emoji prefixes, tool-specific parameter display, result correlation
for parallel tool calls, and subagent progress tracking.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Picks up the allowed_tools fix from kcl-ci — claude.yml tag-mode
workflow now uses Bash(git:*),Bash(bun:*),Bash(curl:*) sub-patterns
instead of bare Bash, enabling auto-approval in CI.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bare `Bash` in allowed_tools makes the tool available but individual
commands still require interactive approval. In CI there's nobody to
approve, so git push, bun test, and curl all fail with
"Error: This command requires approval".
Changes:
- Switch to Bash(git:*), Bash(bun:*), Bash(curl:*) sub-patterns
- Add GITHUB_SERVER_URL env var so Claude doesn't need `export`
- Update prompt to reflect env var is pre-set
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add show_full_output: true to both claude.yml and upstream-sync.yml
- Add allowed_tools (Bash, Read, Write, etc.) to upstream-sync merge step
- Upgrade kcl-ci to v1.37.0 (defaults show_full_output and full tools)
Fixes: Claude was burning 38 turns hitting sandbox permission denials
trying to run bun test and git push during upstream sync.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove wait-for-checks from kcl-ci-build (not needed for this repo).
Add .forgejo/workflows/ to .prettierignore since KCL controls formatting.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New merge-upstream job: creates branch, merges upstream, runs Claude
agent mode to review, resolve conflicts, run tests, and create a PR
- Dynamic fork impact analysis: computes overlap between fork-modified
files and upstream-changed files to assess conflict risk
- Claude reviews ALL upstream changes for fork compatibility (renamed
imports, changed APIs, GitHub-only assumptions) not just conflicts
- Maintainability principle: minimize fork divergence, prefer adapters
- Remove ANTHROPIC_API_KEY (using OAuth token only)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add .forgejo/kcl-ci/ with kcl.mod, main.k, Taskfile.yaml
- claude.yml: use claude.k module (fixes #comment YAML validation error,
removes duplicate pull_request_comment trigger)
- ci.yml: add paths-ignore for kcl-ci paths, named jobs for check constants
- test-action.yml: regenerated from KCL (functionally identical)
- upstream-sync.yml: regenerated from KCL (functionally identical)
- kcl-ci-build.yml: new auto-regeneration workflow (waits for CI checks)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ignore .github/, base-action/, and node_modules/ — these are
upstream-owned and managed via upstream-sync, not Renovate.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The disallowed tools env var read in run.ts was DISALLOWED_TOOLS_INPUT
(raw user input) instead of DISALLOWED_TOOLS (computed value including
WebSearch/WebFetch defaults). This meant default tool disallowance was
never enforced. Also removes orphaned ALLOWED_TOOLS env var export from
createPrompt() (tag mode passes allowed tools via claudeArgs instead),
extracts shared parseToolsInput utility to eliminate duplication, and
adds tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Forgejo REST API returns state as lowercase ("closed"/"open") but
branch.ts compares against GraphQL-style uppercase ("CLOSED"/"MERGED").
This caused @claude on closed/merged PRs to take the open-PR checkout
path, failing with "fatal: invalid reference: refs/pull/N/head".
Normalize state to uppercase in the Forgejo data fetcher, matching the
existing pattern used for review state (line 153).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Forgejo's pull_request_comment event uses an issue_comment-like payload
with issue.number instead of pull_request.number. Add fallback to
extract entity number from issue field when pull_request is absent.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Removed unused re-exports (CommentUpdateInput, ExecutionDetails,
ensureProperlyEncodedUrl) from forgejo/comment-logic.ts
- Verified no `any` type escapes in Forgejo-specific code
- Confirmed naming conventions consistent with upstream
- No dead code, unused imports, or unreachable branches
- All complex decisions documented with comments
- Typecheck, format, and 834 tests pass clean
Progress: 83% complete — M6 (documentation accuracy) remaining
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- F1: Replace process.exit(1) with throw in branch.ts (cleanup was bypassed)
- F2: Replace process.exit(1) with throw in install-mcp-server.ts (same issue)
- F2b: Replace process.exit(1) with throw in create-prompt/index.ts (same issue)
- F3: Use getServerUrl() instead of GITHUB_SERVER_URL constant in branch-cleanup.ts
- F4: Use updated_at for review TOCTOU filtering in Forgejo data fetcher
- Accepted notes: no pagination >100, no retry, unconditional trigger, image no-op
Progress: 33% complete — M2 done, M3 (security review) next
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Branch creation, commit push, bot attribution all verified (run #198)
- 1 bug found: prompt template used quick_pull=1 instead of expand=1
- Fix committed in f08b1b4, validates after merge to main
Progress: 87% complete — M7 (docs update) remaining
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The prompt template hardcoded quick_pull=1 (GitHub-specific) in the
"Create a PR" URL. On Forgejo, this parameter is ignored so the
compare page doesn't auto-expand the PR creation form. Forgejo uses
expand=1 for this purpose.
Since Claude writes the PR URL into the comment body, and
updateForgejoCommentBody() extracts it (prLinkFromContent) with
priority over the URL generated by update-comment-link.ts, the
prompt template is the correct place to fix this.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add inline comments explaining:
- Forgejo never fires pull_request_review (sends pull_request_comment)
- Why workflow if-condition cannot gate on @claude for these events
(payload fields are empty, confirmed by run 12084 being skipped)
- The @claude check is handled in trigger.ts via payload inspection
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Forgejo's pull_request_comment payload does not populate
github.event.comment.body or github.event.review.body in a way
the workflow contains() expression can read, causing legitimate
@claude reviews to be skipped (run 12084).
Revert to unconditional pass at the workflow level. The @claude
check is handled properly in trigger.ts which inspects the actual
payload fields with fallback logic.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously, all pull_request_comment events on Forgejo triggered the
full Claude pipeline unconditionally — both in the workflow if-condition
and in trigger.ts. This wasted CI resources and API credits on reviews
without @claude mentions (e.g. plain "LGTM" reviews).
Workflow: add @claude check via contains() on comment.body and review.body.
Trigger validation: replace blind auto-trigger with payload-aware check
that inspects available body fields, falling back only when no body exists.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Forgejo's issues/assigned webhook payload doesn't include a top-level
assignee object. The assignee is only available under issue.assignee.
Fall back to that field when payload.assignee is missing.
Also removes M5 debug logging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Forgejo runner's expression parser rejects '#' characters inside
if: | blocks (run 52778: "unexpected character '#' while lexing
expression"). Move all comments above the if: key and switch from
block scalar (|) to folded scalar (>-) to avoid multiline issues.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add inline comments explaining:
- Forgejo never fires pull_request_review (sends pull_request_comment)
- Why workflow if-condition cannot gate on @claude for these events
(payload fields are empty, confirmed by run 12084 being skipped)
- The @claude check is handled in trigger.ts via payload inspection
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Forgejo's pull_request_comment payload does not populate
github.event.comment.body or github.event.review.body in a way
the workflow contains() expression can read, causing legitimate
@claude reviews to be skipped (run 12084).
Revert to unconditional pass at the workflow level. The @claude
check is handled properly in trigger.ts which inspects the actual
payload fields with fallback logic.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously, all pull_request_comment events on Forgejo triggered the
full Claude pipeline unconditionally — both in the workflow if-condition
and in trigger.ts. This wasted CI resources and API credits on reviews
without @claude mentions (e.g. plain "LGTM" reviews).
Workflow: add @claude check via contains() on comment.body and review.body.
Trigger validation: replace blind auto-trigger with payload-aware check
that inspects available body fields, falling back only when no body exists.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
E2E test confirmed: Claude correctly responds to @claude mentions in
PR review comments on Forgejo (run #12075, PR #15). Three bugs were
found and fixed during validation:
1. Missing comment object in Forgejo webhook payload
2. Event name mismatch (pull_request_comment vs pull_request_review_comment)
3. MCP server routing updates to wrong API (critical — silently dropped responses)
Progress: 3/8 milestones complete (37.5%)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On Forgejo, the tracking comment for pull_request_review_comment events
is always created as a regular issue comment (because Forgejo's webhook
payload lacks the top-level comment object). The MCP server was
incorrectly setting isPullRequestReviewComment=true based on the event
name, causing every update to try pulls.updateReviewComment first —
which 404'd and silently swallowed Claude's responses.
Three fixes:
- MCP server: detect Forgejo and always use issue comment path
- update-comment-link: skip pulls.getReviewComment on Forgejo (saves
one wasted 404 API call)
- update-claude-comment: expand fallback to also catch 422 (defense
in depth for Forgejo edge cases)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Both mock.module and spyOn cause cross-file test pollution in Bun's
single-process runner — mock.module poisons the module registry permanently,
and spyOn doesn't correctly restore ESM live bindings across files on Linux.
The real downloadCommentImages is safe to call because test comment bodies
contain no GitHub image URLs, so the function finds zero images and returns
an empty Map naturally. The only side effect is mkdir("/tmp/github-images")
which is harmless on CI runners.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root cause was mock.module cross-file pollution in Bun's test runner,
not Linux-specific mock tracking bugs as previously theorized.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mock.module("image-downloader") in forgejo-data-fetcher.test.ts replaced
the module in Bun's registry globally for the entire process. When Bun's
non-deterministic file ordering ran this file BEFORE image-downloader.test.ts,
the image-downloader tests imported a stubbed module instead of the real one,
breaking all 18 tests. This was the root cause of the CI failures that
persisted through 5 previous fix attempts.
spyOn patches the live module export per-test and is auto-restored by Bun
after each test — no cross-file pollution regardless of execution order.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The DI fix for image-downloader tests passed on CI (all 18 tests
green). Removing the temporary mock tracking diagnostic file whose
type errors caused the typecheck job to fail.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DI fix passes locally in grunt-runner Docker but still fails on
actual Forgejo runner. Adding diagnostic tests to isolate whether
mock() tracking itself is broken, or something else in the runner
environment causes it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mock.module("fs/promises") on Linux Bun poisons the entire mock
tracking system — not just fs mocks, but ALL mock/spy assertions
(octokit mocks, spyOn(global, "fetch"), etc.) stop recording calls.
Replace with dependency injection: downloadCommentImages() now accepts
an optional fsDeps parameter. Tests pass mock mkdir/writeFile directly.
Production callers omit it and get real fs. No mock.module anywhere.
Fixes 18 test failures blocking CI on Forgejo Linux runners.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Forgejo fires both issues/opened AND issues/label_updated when an
issue is created with a label. Having the label check on opened
caused duplicate Claude runs. Remove it — the label_updated event
handles label triggers correctly now that the workflow gate and
action normalization are fixed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When an issue is created with the trigger label already applied,
Forgejo only fires issues/opened (not a separate labeled event).
Check payload.issue.labels for the trigger label during opened
events so creating an issue with the claude label works in one step.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Debug logging confirmed Forgejo sends 'label_updated' as eventAction
for issues/labeled events. Normalization fix in context.ts handles it.
Remove temporary debug lines.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Forgejo sends eventAction 'label_updated' instead of 'labeled' for
issue label events. Add FORGEJO_EVENT_ACTION_MAP in context.ts to
normalize at parse time (same pattern as event name normalization),
so all downstream code (trigger.ts, create-prompt) works unchanged.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Forgejo sends eventAction 'label_updated' instead of 'labeled'
for issue label events (discovered via debug logging, run 52786).
Accept both values in trigger.ts label check.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Temporary debug logging to inspect eventAction and payload.label
on Forgejo for issues/labeled events.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Forgejo runner's expression parser rejects '#' characters inside
if: | blocks (run 52778: "unexpected character '#' while lexing
expression"). Move all comments above the if: key and switch from
block scalar (|) to folded scalar (>-) to avoid multiline issues.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Forgejo does not populate github.event.action in the workflow if:
context for issues events (run 12061 was skipped despite action
being 'labeled'). Same pattern as pull_request_comment (M3).
Remove the action/body/title gating from the workflow condition
and let trigger.ts handle all validation (label, assignee, mention).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the _fsForTest/DI patterns with Bun's native mock.module() API.
This mocks fs/promises at the module loader level, which works reliably
on Linux where jest.fn() call tracking fails cross-module. The source
code is now clean with no testing artifacts.
Also switches from jest.fn() to mock() (Bun's native API) for Octokit
mocks in these tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The _fsForTest mutable object pattern fails on Linux Bun because
test and source files get different module instances. Switch to
passing fs operations as an optional parameter with defaults.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On Forgejo, pull_request_review_comment events create the tracking
comment as a regular issue comment (because the payload lacks
comment.id for reply-to-review). The update-comment-link step then
tried pulls.getReviewComment() first, which 404'd and threw before
reaching the issues.getComment() fallback.
Wrap the PR review comment fetch in its own try/catch so it falls
through to the issue comment fetch on failure.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The separate debug-event job ran on every event, wasting a runner.
Merged the event payload dump into the existing "Debug on failure"
step so it only runs when the claude job actually fails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Forgejo's pull_request_review_comment payload doesn't include the
comment body. The Forgejo data fetcher retrieves it via REST instead.
Skip the COMMENT_BODY required check on Forgejo to avoid crashing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Forgejo's pull_request_review_comment event does NOT include a top-level
`comment` object in the webhook payload. This caused crashes at:
- create-initial.ts: context.payload.comment.id (for reply-to-review)
- fetcher.ts: context.payload.comment.created_at (for trigger timestamp)
Fix: use optional chaining and fall back gracefully — reply-to-review
becomes a regular issue comment, trigger timestamp returns undefined.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Forgejo's pull_request_review_comment event does not include a
top-level `comment` object in the payload (it's undefined), causing
a crash: "undefined is not an object (evaluating 'context.payload.comment.body')".
- trigger.ts: On Forgejo, always trigger for pull_request_review_comment
events since the workflow already gates on event name alone
- create-prompt/index.ts: Use optional chaining for comment fields,
fall back to actor/empty values (data fetcher provides full data)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Forgejo's pull_request_review_comment event does not populate
github.event.comment.body in the workflow context (it's always
empty), so the contains() check never matches. The action code
does its own trigger phrase validation, so it's safe to let these
events through based on event name alone.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- All milestones delivered, E2E validated on PR #8
- Deferred: commit authorship E2E, cross-repo test
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Forgejo uses the per-repo sequential run index (GITHUB_RUN_NUMBER) in
action URLs, not the internal DB run ID (GITHUB_RUN_ID). This caused
"View job" links to return 404. Fixed in all 4 URL construction sites:
create-initial comment, update-comment-link, and both prompt generators.
Also pins Bun to 1.2.21 in CI — Bun 1.3.9 broke spyOn(fs, ...) which
failed 18 upstream image-downloader tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace hardcoded bot_id "48" and bot_email "claude@wxs.ro" with
generic placeholders in Quick Start and setup examples so readers
don't copy instance-specific values.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add bot_email action input for custom git commit email on Forgejo
- Update claude.yml to use CLAUDE_BOT_TOKEN with bot_id/bot_name/bot_email
- Update git-config.ts to use provided email over noreply format
- Add "Dedicated Bot User" setup guide to docs/forgejo.md
- Update CLAUDE.md with bot identity documentation
Infrastructure (done outside code):
- claude-bot user created (ID 48, non-admin, with avatar)
- Claude team with write access in all 12 orgs
- CLAUDE_BOT_TOKEN org-level secret in all 12 orgs
Progress: ~85% complete - pending E2E validation after merge
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace hardcoded bot_id "48" and bot_email "claude@wxs.ro" with
generic placeholders in Quick Start and setup examples so readers
don't copy instance-specific values.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Forgejo doesn't have the flat GET /pulls/{number}/comments endpoint
(returns 404). Replace pulls.listReviewComments() with a 2-phase
approach: fetch reviews first, then pulls.listCommentsForReview()
per-review in parallel. Updates tests to mock the new per-review API.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use CLAUDE_BOT_TOKEN (dedicated bot user) instead of FORGEJO_CLAUDE_TOKEN
(personal token). Comments and commits will now appear as claude-bot.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- PRD #5: Dedicated Forgejo user for Claude bot (dedicated identity, scoped token)
- PRD #6: E2E test all 5 untested Forgejo trigger scenarios
- PRD #7: Senior developer review of all Forgejo platform support code
- Defined 5, 7, and 6 milestones respectively
- All three can be parallelized (#7 is read-only, runs alongside #5)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add branch.ts to key integration points (repos.getBranch on Forgejo)
- Add claude.yml to CI workflows list
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add E2E Test Results section with validated capabilities
- Document git ref API difference (refs vs ref) in Known Limitations
- Document user display name 403 fallback
- Add git ref lookup row to API differences table
- Add claude.yml workflow to CI workflows section
- Remove outdated "not yet E2E tested" note from tag mode
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Forgejo uses /git/refs (plural) not /git/ref (singular), causing
git.getRef() to 404. Use repos.getBranch() on Forgejo instead,
which returns the same SHA data via a compatible endpoint.
Also adds the tag mode workflow (.forgejo/workflows/claude.yml)
for @claude mentions on issues, PR reviews, and label triggers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Enables @claude mentions on issues, PR review comments,
issue assignment, and label triggers on Forgejo.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PRD #1 (Forgejo Platform Support) is 100% complete.
All milestones M1-M9 implemented and validated.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Skip checkHumanActor on Forgejo (no bot accounts, avoids read:user scope)
- Replace oven-sh/setup-bun action with curl install in action.yml
(Forgejo runner pre-fetches all uses: refs even when if: skips them)
- Mirror oven-sh/setup-bun created at xternal/setup-bun for future use
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Forgejo runner re-injects GITHUB_SERVER_URL after step-level env is
set, so the env: key approach doesn't override it. Using inline export
in the run command ensures the override takes effect before bun loads
modules (config.ts module-level constants read process.env at import).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Forgejo's user API doesn't return a `type` field (GitHub returns
"User" or "Bot"). The undefined type was incorrectly treated as
non-human, blocking all Forgejo workflows. Now treat undefined/empty
type as human — Forgejo doesn't have GitHub-style bot accounts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two issues found during M8 E2E testing on Forgejo:
1. CI test failures (31 tests): Unit tests assume GitHub context but
Forgejo runner sets GITHUB_SERVER_URL=git.wxs.ro, causing
detectPlatform() to return "forgejo" and module-level constants in
config.ts to use Forgejo URLs. Fixed by setting GITHUB_SERVER_URL
in the test step env. Forgejo-specific tests already override with
PLATFORM=forgejo in their beforeEach.
2. Permission check 403: Forgejo's getCollaboratorPermissionLevel
requires admin access. Added checkForgejoPermissions() that uses
GET /repos/{owner}/{repo} instead — returns a permissions object
with { admin, push, pull } booleans for the authenticated token.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>