Files
claude-code-action/.forgejo/kcl-ci/main.k
T
Vlad MocanuandClaude Opus 4.6 90a9904e9f feat: mirror to Codeberg via SSH deploy key
Push clean copy (without .forgejo/ and prds/) to Codeberg on
each push to main. Uses SSH deploy key for authentication.

Also bumps kcl-ci to v1.40.0.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 16:52:39 +02:00

689 lines
27 KiB
Plaintext

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.
## 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<<DIFF_EOF"
echo "${"$"}DIFF_STAT"
echo "DIFF_EOF"
} >> "${"$"}GITHUB_OUTPUT"
{
echo "commit_log<<LOG_EOF"
git log main..upstream/main --oneline | head -30
echo "LOG_EOF"
} >> "${"$"}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 <<BODY_EOF
## Upstream Sync Required
Our \`main\` branch is **${"$"}{BEHIND} commits behind** upstream (\`anthropics/claude-code-action\`).
We are **${"$"}{AHEAD} commits ahead** (our Forgejo-specific changes).
### New upstream commits (latest ${"$"}{BEHIND}, max 30 shown)
\`\`\`
${"$"}{COMMIT_LOG}
\`\`\`
### Files changed
\`\`\`
${"$"}{DIFF_STAT}
\`\`\`
### How to merge
\`\`\`bash
git fetch upstream main
git merge upstream/main
# Resolve any conflicts in the 3-4 modified files (run.ts, tag/index.ts, etc.)
bun test
git push origin main
\`\`\`
### Conflict-prone files
These files have Forgejo modifications and may conflict:
- \`src/entrypoints/run.ts\`
- \`src/modes/tag/index.ts\`
- \`src/mcp/install-mcp-server.ts\`
- \`action.yml\`
_Auto-generated by upstream-sync workflow on ${"$"}(date -u +%Y-%m-%d)_
BODY_EOF
)
TITLE="Upstream sync: ${"$"}{BEHIND} new commits from anthropics/claude-code-action"
if [ "${h.gha_steps("existing", "issue_exists")}" = "true" ]; then
ISSUE_NUMBER="${h.gha_steps("existing", "issue_number")}"
curl -s -o /dev/null \\
-X PATCH -H "${"$"}AUTH" -H "Content-Type: application/json" \\
-d "${"$"}(jq -n --arg title "${"$"}TITLE" --arg body "${"$"}BODY" '{title: ${"$"}title, body: ${"$"}body}')" \\
"${"$"}API/issues/${"$"}{ISSUE_NUMBER}"
echo "Updated existing issue #${"$"}{ISSUE_NUMBER}"
echo "issue_number=${"$"}{ISSUE_NUMBER}" >> "${"$"}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<<EOF" >> "${"$"}GITHUB_OUTPUT"
echo "${"$"}FORK_FILES" >> "${"$"}GITHUB_OUTPUT"
echo "EOF" >> "${"$"}GITHUB_OUTPUT"
echo "affected_files<<EOF" >> "${"$"}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")