Skip to content

Auto cherry-pick merged OSS PRs into Enterprise (AI-assisted conflict resolution) - #13873

Draft
skoryk-oleksandr wants to merge 1 commit into
projectcalico:masterfrom
skoryk-oleksandr:feat/ai-auto-cherry-pick
Draft

Auto cherry-pick merged OSS PRs into Enterprise (AI-assisted conflict resolution)#13873
skoryk-oleksandr wants to merge 1 commit into
projectcalico:masterfrom
skoryk-oleksandr:feat/ai-auto-cherry-pick

Conversation

@skoryk-oleksandr

Copy link
Copy Markdown
Contributor

Description

Adds an AI-assisted workflow that auto cherry-picks merged OSS master PRs
into Enterprise (tigera/calico-private master) and opens a PR there.

How it works

  • A generic script (.github/workflows/scripts/oss-pick.sh) does all git work:
    clone the target, cherry-pick -x the merged commit, build a bot-parseable
    PR body (with an Original Commit SHA block the merge-queue-bot can parse),
    carry over source labels, push the branch, open the PR.
  • The Claude action is invoked only when the pick conflicts, to resolve
    conflicts by editing files while keeping Enterprise-specific code. Every
    resulting PR is human-reviewed; conflict resolutions are written into the PR
    body and the PR is labelled auto-resolved-conflict.
  • The original author is DMed on Slack when the pick PR opens (soft-fail).

Security split (two stages)

  • auto-pick-oss-to-enterprise-trigger.yml: zero-permission trigger on the
    untrusted pull_request (closed) event. Holds no secrets.
  • auto-pick-oss-to-enterprise.yml: privileged worker, runs from the trusted
    default branch via workflow_run. Re-derives the merged PR from the
    trigger's head SHA, so no attacker-controlled PR payload reaches the
    privileged context.

The cherry-pick engine and the resolve/notify helpers are generic (source and
target are passed in), so future release-backport workflows (OSS master to
OSS release, Enterprise master to Enterprise release) can reuse them.

Removes trigger-merge-queue.yml, which dispatched the merge-queue-bot to
create the OSS-to-Enterprise pick on every master merge. This workflow now
does that directly.

Note for reviewers: backport gap

The merge-queue-bot's OSS_MERGE dispatch that trigger-merge-queue.yml fired
may also have driven cherry-pick-candidate release backports on OSS merge.
This PR replaces only the OSS master to Enterprise master pick. If the bot was
also creating release backports off that same trigger, those need a follow-up
backport workflow (reusing the generic script here) before this trigger is
removed in production. Please confirm the bot's OSS_MERGE behaviour before
merging.

Required secrets/vars (set on this repo before the workflow runs)

  • CLAUDE_TOKEN: Anthropic API key for the Claude action.
  • TIGERA_BOT_PAT: token with write access to tigera/calico-private.
  • SLACK_MERGE_CAT_TOKEN + SLACK_NOTIFY_MAP: Slack DM to the author.

Release Note

None

@marvin-tigera marvin-tigera added this to the Calico v3.34.0 milestone Sep 10, 2026
@marvin-tigera marvin-tigera added release-note-required Change has user-facing impact (no matter how small) docs-pr-required Change is not yet documented labels Sep 10, 2026
@skoryk-oleksandr
skoryk-oleksandr marked this pull request as ready for review September 10, 2026 21:45
@skoryk-oleksandr
skoryk-oleksandr requested a review from a team as a code owner September 10, 2026 21:45
Copilot AI lite review requested due to automatic review settings September 10, 2026 21:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved security, failure-handling, idempotency, PR-resolution, notification, and release-backport concerns remain.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds an AI-assisted, two-stage workflow to cherry-pick merged OSS PRs into Enterprise, resolve conflicts, create target PRs, and notify authors.

Changes:

  • Adds generic cherry-pick, PR-resolution, and Slack notification scripts.
  • Adds unprivileged trigger and privileged worker workflows.
  • Removes the legacy merge-queue dispatch workflow.
File summaries
File Description
.github/workflows/trigger-merge-queue.yml Removes the legacy merge-queue trigger.
.github/workflows/scripts/resolve-merged-pr.js Resolves merged source PR metadata.
.github/workflows/scripts/oss-pick.sh Performs cherry-picks and creates target PRs.
.github/workflows/scripts/notify-slack.js Notifies original authors through Slack.
.github/workflows/auto-pick-oss-to-enterprise.yml Runs the privileged pick and conflict-resolution flow.
.github/workflows/auto-pick-oss-to-enterprise-trigger.yml Starts the zero-permission trigger stage.
Review details

Suppressed comments (9)

.github/workflows/auto-pick-oss-to-enterprise.yml:92

  • This privileged workflow executes a mutable third-party action with CLAUDE_TOKEN and repository credentials. The comparable privileged workflow pins its action to a full commit SHA (.github/workflows/cherry_pick_candidate.yml:58); pin this action to a reviewed immutable SHA to prevent tag drift or a supply-chain change from gaining those permissions.
        uses: anthropics/claude-code-action@v1

.github/workflows/auto-pick-oss-to-enterprise.yml:46

  • The replacement worker only handles tigera/calico-private@master, while removing trigger-merge-queue.yml also removes the repository's OSS_MERGE dispatch. No release-branch backport workflow is added here; if merge-queue-bot used that dispatch for cherry-pick-candidate release backports, those backports will stop. Verify and replace that behavior before relying on this replacement.
      EXTRA_LABELS: merge-oss-cherry-pick

.github/workflows/auto-pick-oss-to-enterprise.yml:145

  • The escalation path aborts the cherry-pick but only says to “stop”; it does not require Claude to fail the step. With continue-on-error, Claude can therefore report success, the Open PR condition runs, and open-pr exits successfully on the now-clean base tree, recording a successful run while silently dropping the pick. Require a nonzero exit (or a checked sentinel) on this path.
            code/context that does not exist in Enterprise. In that case run
            `git cherry-pick --abort`, explain precisely what a human must decide
            in /tmp/resolution-report.md, and stop (do not push).

.github/workflows/auto-pick-oss-to-enterprise.yml:196

  • The notification step is described as soft-fail, but this gh api command runs before notify-slack.js and a GitHub Actions run shell exits on an unhandled command failure. A transient source-PR API error will therefore fail the workflow instead of skipping the DM; handle this lookup failure explicitly and exit 0 with a warning.
          J=$(gh api "repos/${SOURCE_REPO}/pulls/${SRC_PR}")
          export SRC_TITLE="$(printf '%s' "$J" | jq -r '.title')"
          export SRC_URL="$(printf '%s' "$J" | jq -r '.html_url')"
          node "$RUNNER_TEMP/notify-slack.js"

.github/workflows/scripts/notify-slack.js:63

  • This sends the Slack member ID directly as chat.postMessage's channel. The existing notifier in ci-notify.yml:356-383 documents that this returns channel_not_found and requires conversations.open to obtain a DM channel first, so the advertised author notification will fail with the token contract declared here. Open the IM and post to the returned channel ID, and update the required Slack scope accordingly.
      body: JSON.stringify({ channel: slackId, text, unfurl_links: false }),
      signal: AbortSignal.timeout(30000),

.github/workflows/scripts/oss-pick.sh:187

  • The resolver is instructed to run git cherry-pick --abort and stop for an irreconcilable conflict, but this condition still treats the resulting base tree as a successful no-op. The workflow then loses /tmp/resolution-report.md and completes without a PR or visible failure. Treat OUTCOME=conflict with no post-resolution commit as an escalation/failure, or require an explicit completed-resolution marker.
  if git diff --quiet "origin/${TARGET_BRANCH}" HEAD 2>/dev/null; then
    echo "::notice::resolution produced no net change over origin/${TARGET_BRANCH}; nothing to pick"
    exit 0

.github/workflows/scripts/oss-pick.sh:68

  • These credential-bearing URLs are persisted in .git/config by git clone and git remote add, and that workspace is then handed to the Claude action with Read/Bash access to untrusted OSS content. A prompt-injected file can read the URL and exfiltrate or use TIGERA_BOT_PAT to modify Enterprise; strip credentials from both remotes immediately after fetching (the later open-pr step already pushes with its explicit URL).
  git clone "$tgt_url" .
  git remote add source "$src_url"
  git fetch --no-tags source "$SOURCE_REF"

.github/workflows/scripts/oss-pick.sh:130

  • This fallback hides a failed reporting contract: if Claude resolves a conflict but forgets or fails to write /tmp/resolution-report.md, the resulting PR body says “No conflicts: the cherry-pick applied cleanly.” Fail closed for OUTCOME=conflict when the report is missing so reviewers are not misled about an AI-resolved change.
  local conflicts="No conflicts: the cherry-pick applied cleanly."
  if [ "${OUTCOME:-}" = "conflict" ] && [ -n "${RESOLUTION_REPORT:-}" ] && [ -f "$RESOLUTION_REPORT" ]; then
    conflicts="$(cat "$RESOLUTION_REPORT")"
  fi

.github/workflows/trigger-merge-queue.yml:1

  • Removing this workflow removes the only visible dispatch of the merge-queue-bot's OSS_MERGE event. The replacement workflow only creates the OSS-master-to-Enterprise-master PR and contains no release-backport dispatch, so any release backports driven by OSS_MERGE will stop when this deletion ships. Keep this trigger until that behavior is confirmed or add the replacement backport flow in the same change.
  • Files reviewed: 6/6 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.


permissions:
contents: read
pull-requests: write

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed. Reduced to pull-requests: read (dropped write). Everything github.token does here is a read: source clone, the gh api search/pulls lookups, and the Claude action's actor-permission check. Every target write (push, gh pr create, labels) uses TIGERA_BOT_PAT, not github.token. Verified end to end with the reduced scope: the Claude conflict-resolution step runs, Open PR creates the PR via the PAT, and the author DM is sent, all under pull-requests: read.

Comment thread .github/workflows/scripts/oss-pick.sh Outdated
Comment on lines +60 to +63
# Idempotency: if the pick branch already exists on the target, stop.
if git ls-remote --exit-code --heads "$tgt_url" "$BRANCH_NAME" >/dev/null 2>&1; then
echo "Branch $BRANCH_NAME already exists on ${TARGET_REPO}; already picked."
emit "outcome=already"; return 0

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed. The idempotency check now keys on the target PR, not the branch: it queries gh pr list --head --state all, and only returns "already" when a PR exists. A branch with no PR is treated as a stranded push from a prior run whose PR creation failed, so it is deleted and re-picked cleanly (self-heal) instead of being skipped forever.

return gh([
'api',
`search/issues?q=sha:${HEAD_SHA}+repo:${SOURCE_REPO}+is:pr`,
'--jq', '.items[0].number // empty',

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed. resolve now fetches every PR the sha: search returns and selects the one whose head.sha equals HEAD_SHA (the PR that was actually merged), instead of trusting items[0]. If none match it skips with the candidate list. Unit-tested across single-match, multi-match with a decoy first, not-merged, no-head-match, and no-candidate cases.

@skoryk-oleksandr
skoryk-oleksandr marked this pull request as draft September 10, 2026 22:06
@skoryk-oleksandr

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Addressed the following findings (validated end to end in a test repo before updating here):

  • Escalation silently dropped the pick (worker prompt + open-pr no-net-change): fixed. On an irreconcilable conflict the agent now writes /tmp/pick-escalated; a "Detect escalation" step gates the Open PR step off, and a "Fail on escalation" step prints the resolution report to the job summary and exits non-zero. An escalation is now a loud, visible failure with no PR, instead of a green run that looked identical to a legitimate no-op.

  • Credentials persisted in .git/config, exposed to the agent (oss-pick.sh clone/fetch): fixed. The tokened remote URLs are stripped (git remote set-url ... https://github.com/...) immediately after fetch, before the conflict-resolution agent (Bash/Read over untrusted OSS content) sees the workspace. The later open-pr step pushes with an explicit tokened URL, so nothing downstream relies on the stored credential.

  • Branch stranded without a PR (idempotency): fixed. See the inline reply. The check now keys on the target PR (gh pr list --head --state all); a branch with no PR is deleted and re-picked.

  • Notify step's gh api was not soft-fail: fixed. The source-PR lookup is now guarded (if ! J=$(gh api ... 2>/dev/null); then warn; exit 0; fi), so a transient error cannot fail the run after the pick PR already exists.

  • PR body could claim "applied cleanly" for a conflict pick with a missing report: fixed. build_pr_text now prints a review warning for OUTCOME=conflict when the resolution report is missing or empty, and never the "applied cleanly" text.

Two comments left as-is, on purpose:

  • Slack DM via chat.postMessage with channel=<member-id>: this is verified working with the token this workflow uses (the same bot the merge-queue-bot posts DMs with, chat:write only). conversations.open is not required for it; the DM was received in the end-to-end test. No change.

  • Pinning anthropics/claude-code-action@v1 to a SHA: a legitimate supply-chain point, but @v1 is a deliberate maintainability choice here. Noting it for the human reviewer rather than changing it.

Also, the earlier revision deleted trigger-merge-queue.yml; that deletion has been reverted, since that workflow also drives the merge-queue-bot's release backports, which this change does not yet replace.

@skoryk-oleksandr
skoryk-oleksandr marked this pull request as ready for review September 10, 2026 22:10
@skoryk-oleksandr skoryk-oleksandr added docs-not-required Docs not required for this change release-note-not-required Change has no user-facing impact and removed release-note-required Change has user-facing impact (no matter how small) docs-pr-required Change is not yet documented labels Sep 10, 2026
@skoryk-oleksandr
skoryk-oleksandr marked this pull request as draft September 10, 2026 22:23
@tomastigera tomastigera self-assigned this Sep 11, 2026
@skoryk-oleksandr
skoryk-oleksandr marked this pull request as ready for review September 11, 2026 14:48
Adds a GitHub Actions workflow that, when a PR merges to master,
cherry-picks the change onto tigera/calico-private master and opens a
PR there. A generic script does the git work (clone, cherry-pick -x,
build the bot-parseable PR body, carry labels, push, open PR); the
Claude action is invoked only to resolve merge conflicts by editing
files, keeping Enterprise-specific code. The original author is DMed on
Slack when the pick PR is opened.

The workflow is split into two stages for security: a zero-permission
trigger on the untrusted PR event, and a privileged worker that runs
from the trusted default branch via workflow_run. The cherry-pick
script and the resolve/notify helpers are generic and reused by future
release-backport workflows.

Required secrets/vars on the repo:
  - CLAUDE_TOKEN: Anthropic API key for the Claude action.
  - TIGERA_BOT_PAT: token with write access to tigera/calico-private.
  - SLACK_MERGE_CAT_TOKEN + SLACK_NOTIFY_MAP: Slack DM to the author.
@skoryk-oleksandr
skoryk-oleksandr marked this pull request as draft September 11, 2026 22:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs-not-required Docs not required for this change release-note-not-required Change has no user-facing impact

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants