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 # ============================================================================= # Check name constants # Forgejo check names format: "WorkflowName / JobName (event)" # Keep these in sync with job names in the workflows below # ============================================================================= _CI_WORKFLOW_NAME = "CI" _CI_JOB_TEST = "Tests" _CI_JOB_PRETTIER = "Prettier" _CI_JOB_TYPECHECK = "Type Check" _CHECK_CI_TEST = "{} / {} (push)".format(_CI_WORKFLOW_NAME, _CI_JOB_TEST) _CHECK_CI_PRETTIER = "{} / {} (push)".format(_CI_WORKFLOW_NAME, _CI_JOB_PRETTIER) _CHECK_CI_TYPECHECK = "{} / {} (push)".format(_CI_WORKFLOW_NAME, _CI_JOB_TYPECHECK) # ============================================================================= # 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(claude_mod.ClaudeConfig { anthropic_api_key_secret = "ANTHROPIC_API_KEY" }) # ============================================================================= # 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(kcl_build_mod.KclCiBuildConfig { wait_for_checks = [_CHECK_CI_TEST, _CHECK_CI_PRETTIER, _CHECK_CI_TYPECHECK] }) # ============================================================================= # 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" 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" } env = { ANTHROPIC_API_KEY = h.gha_secret("ANTHROPIC_API_KEY") } } { 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) # ============================================================================= _upstream_sync_workflow = { name = "Upstream Sync Check" on = { schedule = [ # Weekly Monday at 08:00 UTC {cron = "0 8 * * 1"} ] workflow_dispatch = {} } jobs = { "check-upstream" = { name = "Check for upstream updates" "runs-on" = "grunt" 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 = "Check for existing sync issue" "if" = h.gha_steps_eq("compare", "needs_sync", "true") id = "existing" run = """\ RESPONSE=${"$"}(curl -s \\ -H "Authorization: token ${h.gha_github("token")}" \\ -H "Content-Type: application/json" \\ "${h.gha_env("GITHUB_API_URL")}/repos/${h.gha_github("repository")}/issues?state=open&labels=upstream-sync&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 upstream-sync label" "if" = h.gha_steps_eq("compare", "needs_sync", "true") run = """\ curl -s -o /dev/null -w "%{http_code}" \\ -X POST \\ -H "Authorization: token ${h.gha_github("token")}" \\ -H "Content-Type: application/json" \\ -d '{"name":"upstream-sync","color":"#0075ca","description":"Upstream sync tracking"}' \\ "${h.gha_env("GITHUB_API_URL")}/repos/${h.gha_github("repository")}/labels" || true""" } { name = "Create or update sync issue" "if" = h.gha_steps_eq("compare", "needs_sync", "true") run = """\ 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 <