-
Notifications
You must be signed in to change notification settings - Fork 20
[WIP] chore(ci): add script to mirror community PRs for safe CI execution #2032
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
bantonsson
wants to merge
1
commit into
main
Choose a base branch
from
ban/mirror-community-prs
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,200 @@ | ||
| #!/usr/bin/env bash | ||
|
|
||
| # Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| set -e | ||
|
|
||
| # | ||
| # Mirror an external (fork) pull request onto an internal branch so that CI | ||
| # runs with maintainer permissions rather than the restricted token available | ||
| # to fork PRs. | ||
| # | ||
| # The checked-out HEAD sha of the original PR is fetched directly from the | ||
| # fork over HTTPS (no persistent remote needed) and every commit is | ||
| # cherry-picked with GPG signing (-S) onto a fresh branch based on main. | ||
| # This avoids the "pwn request" problem: the workflow definitions and CI | ||
| # scripts that execute always come from the trusted base branch. | ||
| # | ||
| # Usage: mirror-community-pull-request.sh <pr-number> [<target-branch>] | ||
| # <pr-number>: GitHub PR number of the external/fork contribution. | ||
| # <target-branch>: Branch to base the mirror on (default: main). | ||
| # | ||
| # The mirror branch is named: external/community-pr-<N> | ||
| # | ||
| # Prerequisites: gh (GitHub CLI), jq, git with GPG signing configured. | ||
|
|
||
| REPO="DataDog/libdatadog" | ||
| PR_NUMBER=$1 | ||
| TARGET_BRANCH=${2:-main} | ||
| MIRROR_BRANCH="external/community-pr-${PR_NUMBER}" | ||
|
|
||
| # | ||
| # Check arguments. | ||
| # | ||
| if [ $# -eq 0 ]; then | ||
| echo "Usage: $0 <pr-number> [<target-branch>]" | ||
| echo " <pr-number>: PR number to mirror" | ||
| echo " <target-branch>: Base branch for the mirror (default: main)" | ||
| echo "" | ||
| echo "Creates branch 'external/community-pr-<N>' and a corresponding PR so CI" | ||
| echo "runs with maintainer permissions on the fork's changes." | ||
| exit 1 | ||
| fi | ||
| if [ -z "$PR_NUMBER" ]; then | ||
| echo "❌ PR number is not provided" | ||
| exit 1 | ||
| fi | ||
| if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then | ||
| echo "❌ PR number must be numeric" | ||
| exit 1 | ||
| fi | ||
|
|
||
| # | ||
| # Check requirements. | ||
| # | ||
| echo "- Checking requirements" | ||
| gh --version 1>/dev/null 2>&1 || { echo "❌ gh is not installed. Please install GitHub CLI."; exit 1; } | ||
| gh auth status 1>/dev/null 2>&1 || { echo "❌ Not logged into GitHub CLI. Please run \`gh auth login\`."; exit 1; } | ||
| jq --version 1>/dev/null 2>&1 || { echo "❌ jq is not installed. Please install jq."; exit 1; } | ||
| git diff --quiet --exit-code || { echo "❌ There are local changes. Please commit or stash them."; exit 1; } | ||
|
|
||
| # | ||
| # Fetch PR information. | ||
| # | ||
| echo "- Fetching PR #${PR_NUMBER} details" | ||
| PR_DATA=$(gh pr view "$PR_NUMBER" --repo "$REPO" \ | ||
| --json headRepository,headRepositoryOwner,headRefName,title,number,state,author \ | ||
| 2>/dev/null || echo "") | ||
| if [ -z "$PR_DATA" ]; then | ||
| echo "❌ PR #${PR_NUMBER} not found in ${REPO}" | ||
| exit 1 | ||
| fi | ||
|
|
||
| FORK_REPO=$(echo "$PR_DATA" | jq -r \ | ||
| '(.headRepository.nameWithOwner | select(. != "" and . != null)) | ||
| // (.headRepositoryOwner.login + "/" + .headRepository.name) | ||
| // empty') | ||
| FORK_BRANCH=$(echo "$PR_DATA" | jq -r '.headRefName // empty') | ||
| PR_TITLE=$(echo "$PR_DATA" | jq -r '.title // empty') | ||
| PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login // empty') | ||
| PR_LABELS=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json labels \ | ||
| --jq '[.labels[].name] | join(",")') | ||
|
|
||
| if [ -z "$FORK_REPO" ] || [ -z "$FORK_BRANCH" ]; then | ||
| echo "❌ Could not determine fork repository or branch for PR #${PR_NUMBER}" | ||
| exit 1 | ||
| fi | ||
|
|
||
| # | ||
| # Create mirror branch. | ||
| # | ||
| echo "- Mirroring PR #${PR_NUMBER} from ${FORK_REPO}:${FORK_BRANCH} → ${REPO}:${MIRROR_BRANCH}" | ||
|
|
||
| if git show-ref --verify --quiet "refs/heads/${MIRROR_BRANCH}" 2>/dev/null; then | ||
| echo -n "Branch ${MIRROR_BRANCH} already exists locally. Delete and recreate? (y/n) " | ||
| read -r ANSWER | ||
| [ "$ANSWER" = "y" ] || { echo "Aborting."; exit 1; } | ||
| git branch -D "$MIRROR_BRANCH" | ||
| fi | ||
|
|
||
| if git show-ref --verify --quiet "refs/remotes/origin/${MIRROR_BRANCH}" 2>/dev/null; then | ||
| echo -n "Branch ${MIRROR_BRANCH} already exists on remote. Force-push over it? (y/n) " | ||
| read -r ANSWER | ||
| [ "$ANSWER" = "y" ] || { echo "Aborting."; exit 1; } | ||
| fi | ||
|
|
||
| # Fetch fork branch directly (no persistent remote required) | ||
| echo "- Fetching fork branch" | ||
| git fetch --quiet "https://github.com/${FORK_REPO}.git" "$FORK_BRANCH" | ||
|
|
||
| # Collect commit SHAs from the PR | ||
| echo "- Getting commits from PR" | ||
| PR_COMMITS=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json commits \ | ||
| --jq '.commits[].oid') | ||
| if [ -z "$PR_COMMITS" ]; then | ||
| echo "❌ No commits found in PR #${PR_NUMBER}" | ||
| exit 1 | ||
| fi | ||
|
|
||
| CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) | ||
|
|
||
| echo "- Creating ${MIRROR_BRANCH} from origin/${TARGET_BRANCH}" | ||
| git fetch --quiet origin "$TARGET_BRANCH" | ||
| git checkout -b "$MIRROR_BRANCH" "origin/${TARGET_BRANCH}" | ||
|
|
||
| # | ||
| # Cherry-pick and sign commits. | ||
| # | ||
| echo "- Cherry-picking and signing commits" | ||
| for COMMIT in $PR_COMMITS; do | ||
| echo " - ${COMMIT}" | ||
| CHERRY_PICK_ARGS=("-S") | ||
| PARENT_COUNT=$(git rev-list --parents -n 1 "$COMMIT" 2>/dev/null | wc -w) | ||
| if [ "$PARENT_COUNT" -gt 2 ]; then | ||
| CHERRY_PICK_ARGS+=("-m" "1") | ||
| fi | ||
| if ! git cherry-pick "${CHERRY_PICK_ARGS[@]}" "$COMMIT"; then | ||
| if ! git diff --cached --quiet || ! git diff --quiet; then | ||
| echo "❌ Failed to cherry-pick ${COMMIT} — resolve conflicts then re-run." | ||
| git checkout "$CURRENT_BRANCH" | ||
| exit 1 | ||
| else | ||
| echo " (empty commit, skipping)" | ||
| git cherry-pick --skip | ||
| fi | ||
| fi | ||
| done | ||
|
|
||
| echo "- Pushing ${MIRROR_BRANCH} to origin" | ||
| git push -u origin "$MIRROR_BRANCH" --no-verify --force-with-lease | ||
|
|
||
| # | ||
| # Create mirror PR if one doesn't exist yet. | ||
| # | ||
| echo "- Checking for existing mirror PR" | ||
| EXISTING_PR=$(gh pr list --repo "$REPO" --head "$MIRROR_BRANCH" \ | ||
| --json number --jq '.[0].number // empty' 2>/dev/null) | ||
|
|
||
| if [ -n "$EXISTING_PR" ]; then | ||
| MIRROR_PR_URL="https://github.com/${REPO}/pull/${EXISTING_PR}" | ||
| echo "- Mirror PR already exists: #${EXISTING_PR}" | ||
| else | ||
| echo "- Creating mirror PR" | ||
| MIRROR_PR_BODY="This PR mirrors the changes from the external contribution below so that CI runs with maintainer permissions. | ||
|
|
||
| **Original PR:** https://github.com/${REPO}/pull/${PR_NUMBER} | ||
| **Original Author:** @${PR_AUTHOR} | ||
| **Original Branch:** ${FORK_REPO}:${FORK_BRANCH} | ||
|
|
||
| Closes #${PR_NUMBER} | ||
|
|
||
| --- | ||
| *Mirror created by \`scripts/mirror-community-pull-request.sh\`.*" | ||
|
|
||
| CREATE_ARGS=( | ||
| --repo "$REPO" | ||
| --base "$TARGET_BRANCH" | ||
| --head "$MIRROR_BRANCH" | ||
| --title "${PR_TITLE}" | ||
| --body "$MIRROR_PR_BODY" | ||
| ) | ||
| if [ -n "$PR_LABELS" ]; then | ||
| CREATE_ARGS+=(--label "$PR_LABELS") | ||
| fi | ||
|
|
||
| MIRROR_PR_URL=$(gh pr create "${CREATE_ARGS[@]}" 2>/dev/null || true) | ||
| if [ -z "$MIRROR_PR_URL" ]; then | ||
| MIRROR_PR_URL="https://github.com/${REPO}/compare/${TARGET_BRANCH}...${MIRROR_BRANCH}" | ||
| echo "- Could not create PR automatically; open one at: ${MIRROR_PR_URL}" | ||
| fi | ||
| fi | ||
|
|
||
| echo "" | ||
| echo "✅ Done" | ||
| echo " Original : https://github.com/${REPO}/pull/${PR_NUMBER} (@${PR_AUTHOR})" | ||
| echo " Mirror : ${MIRROR_PR_URL}" | ||
| echo " Branch : ${REPO}:${MIRROR_BRANCH}" | ||
|
|
||
| echo "- Restoring original branch" | ||
| git checkout "$CURRENT_BRANCH" | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When an external PR changes GitHub workflows, local actions, or scripts invoked by CI, this full cherry-pick puts those untrusted files onto a same-repository branch before opening the mirror PR. That defeats the stated safety property: this repo has
pull_requestworkflows that check out the PR head and use secrets (for example.github/workflows/test.ymllines 3, 27, and 265), so a fork author could include a workflow/CI-script change that runs with the maintainer permissions/secrets the mirror is meant to enable. The mirror needs to reset/filter CI-sensitive paths back to the trusted base or run trusted workflows against the diff instead of cherry-picking them verbatim.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That part needs to be verified by a human.