feat: add Claude auto-merge to upstream-sync workflow
- 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>
This commit is contained in:
1 parent
5c014823e0
commit
4954e96cc5
4 files changed
+274
-10
No files matched your search
+153
-6
@@ -3,6 +3,7 @@ import kcl_ci.schemas.config as cfg
|
|||||||
import kcl_ci.steps.checkout as checkout_mod
|
import kcl_ci.steps.checkout as checkout_mod
|
||||||
import kcl_ci.workflows.kcl_ci_build as kcl_build_mod
|
import kcl_ci.workflows.kcl_ci_build as kcl_build_mod
|
||||||
import kcl_ci.workflows.claude as claude_mod
|
import kcl_ci.workflows.claude as claude_mod
|
||||||
|
import kcl_ci.versions
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Check name constants
|
# Check name constants
|
||||||
@@ -58,9 +59,7 @@ _bun_deps = lambda -> {str:} {
|
|||||||
# Claude Code workflow (tag mode — responds to @claude mentions)
|
# Claude Code workflow (tag mode — responds to @claude mentions)
|
||||||
# Uses the standard claude.k module from kcl-ci
|
# Uses the standard claude.k module from kcl-ci
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
_claude_workflow = claude_mod.claude_workflow(claude_mod.ClaudeConfig {
|
_claude_workflow = claude_mod.claude_workflow()
|
||||||
anthropic_api_key_secret = "ANTHROPIC_API_KEY"
|
|
||||||
})
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# KCL CI Build workflow (auto-regenerates workflows from KCL)
|
# KCL CI Build workflow (auto-regenerates workflows from KCL)
|
||||||
@@ -181,9 +180,6 @@ _test_action_workflow = {
|
|||||||
github_token = h.gha_secret("FORGEJO_CLAUDE_TOKEN")
|
github_token = h.gha_secret("FORGEJO_CLAUDE_TOKEN")
|
||||||
platform = "forgejo"
|
platform = "forgejo"
|
||||||
}
|
}
|
||||||
env = {
|
|
||||||
ANTHROPIC_API_KEY = h.gha_secret("ANTHROPIC_API_KEY")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
name = "Verify execution output exists"
|
name = "Verify execution output exists"
|
||||||
@@ -233,7 +229,66 @@ fi"""
|
|||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Upstream Sync Check workflow (weekly check for upstream updates)
|
# 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_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)
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
## After merge is resolved and reviewed
|
||||||
|
1. Run tests: export GITHUB_SERVER_URL=https://github.com && bun test
|
||||||
|
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. Create a PR targeting main via the Forgejo API:
|
||||||
|
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:
|
||||||
|
- 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 = {
|
_upstream_sync_workflow = {
|
||||||
name = "Upstream Sync Check"
|
name = "Upstream Sync Check"
|
||||||
on = {
|
on = {
|
||||||
@@ -247,6 +302,11 @@ _upstream_sync_workflow = {
|
|||||||
"check-upstream" = {
|
"check-upstream" = {
|
||||||
name = "Check for upstream updates"
|
name = "Check for upstream updates"
|
||||||
"runs-on" = "grunt"
|
"runs-on" = "grunt"
|
||||||
|
outputs = {
|
||||||
|
needs_sync = h.gha_steps("compare", "needs_sync")
|
||||||
|
behind = h.gha_steps("compare", "behind")
|
||||||
|
ahead = h.gha_steps("compare", "ahead")
|
||||||
|
}
|
||||||
steps = [
|
steps = [
|
||||||
checkout_mod.checkout() | {"with" = {"fetch-depth" = "0"}}
|
checkout_mod.checkout() | {"with" = {"fetch-depth" = "0"}}
|
||||||
{
|
{
|
||||||
@@ -410,6 +470,93 @@ 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 = "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
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
BRANCH="chore/upstream-sync-${"$"}(date +%Y%m%d)"
|
||||||
|
git checkout -b "${"$"}BRANCH"
|
||||||
|
echo "branch=${"$"}BRANCH" >> "${"$"}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 = "Claude: review merge and create PR"
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
env = {
|
||||||
|
GITHUB_TOKEN = h.gha_secret("CLAUDE_BOT_TOKEN")
|
||||||
|
FORK_MODIFIED_FILES = h.gha_steps("prepare", "fork_files")
|
||||||
|
AFFECTED_FORK_FILES = h.gha_steps("prepare", "affected_files")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,8 +39,6 @@ jobs:
|
|||||||
bot_email: claude@wxs.ro
|
bot_email: claude@wxs.ro
|
||||||
platform: forgejo
|
platform: forgejo
|
||||||
assignee_trigger: claude
|
assignee_trigger: claude
|
||||||
env:
|
|
||||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
||||||
- name: Debug on failure
|
- name: Debug on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -34,8 +34,6 @@ jobs:
|
|||||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||||
github_token: ${{ secrets.FORGEJO_CLAUDE_TOKEN }}
|
github_token: ${{ secrets.FORGEJO_CLAUDE_TOKEN }}
|
||||||
platform: forgejo
|
platform: forgejo
|
||||||
env:
|
|
||||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
||||||
- name: Verify execution output exists
|
- name: Verify execution output exists
|
||||||
run: |-
|
run: |-
|
||||||
EXEC_FILE="${{ steps.claude.outputs.execution_file }}"
|
EXEC_FILE="${{ steps.claude.outputs.execution_file }}"
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ jobs:
|
|||||||
check-upstream:
|
check-upstream:
|
||||||
name: Check for upstream updates
|
name: Check for upstream updates
|
||||||
runs-on: grunt
|
runs-on: grunt
|
||||||
|
outputs:
|
||||||
|
needs_sync: ${{ steps.compare.outputs.needs_sync }}
|
||||||
|
behind: ${{ steps.compare.outputs.behind }}
|
||||||
|
ahead: ${{ steps.compare.outputs.ahead }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: https://${{ secrets.READ_REPO_TOKEN }}@git.wxs.ro/xternal/checkout@v6.0.2
|
uses: https://${{ secrets.READ_REPO_TOKEN }}@git.wxs.ro/xternal/checkout@v6.0.2
|
||||||
@@ -164,3 +168,120 @@ jobs:
|
|||||||
"${{ env.GITHUB_API_URL }}/repos/${{ github.repository }}/issues"
|
"${{ env.GITHUB_API_URL }}/repos/${{ github.repository }}/issues"
|
||||||
echo "Created new upstream sync issue"
|
echo "Created new upstream sync issue"
|
||||||
fi
|
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: 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
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
BRANCH="chore/upstream-sync-$(date +%Y%m%d)"
|
||||||
|
git checkout -b "$BRANCH"
|
||||||
|
echo "branch=$BRANCH" >> "$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: 'Claude: review merge and create PR'
|
||||||
|
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.
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
## After merge is resolved and reviewed
|
||||||
|
1. Run tests: export GITHUB_SERVER_URL=https://github.com && bun test
|
||||||
|
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. Create a PR targeting main via the Forgejo API:
|
||||||
|
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:
|
||||||
|
- 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
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.CLAUDE_BOT_TOKEN }}
|
||||||
|
FORK_MODIFIED_FILES: ${{ steps.prepare.outputs.fork_files }}
|
||||||
|
AFFECTED_FORK_FILES: ${{ steps.prepare.outputs.affected_files }}
|
||||||
Reference in new issue
Block a user