diff --git a/.buildkite/pipeline.sync_public_pr.yml b/.buildkite/pipeline.sync_public_pr.yml index ced769d08c..da630ed836 100644 --- a/.buildkite/pipeline.sync_public_pr.yml +++ b/.buildkite/pipeline.sync_public_pr.yml @@ -7,5 +7,16 @@ steps: text: "Pull Request Number" format: "^[0-9]+$" - - command: "bin/sync-public-pr" + - label: ":git: Sync Public PR to Private Repo" + command: "bin/sync-public-pr" depends_on: collect_pr_number + plugins: + - docker#v5.11.0: + image: "ruby:3.3-bookworm" + propagate-environment: true + mount-buildkite-agent: true + mount-ssh-agent: true + environment: + - GH_TOKEN + - GH_PRIVATE_REPO + - GH_PUBLIC_REPO diff --git a/bin/sync-public-pr b/bin/sync-public-pr index ba23d89e45..40366baaa5 100755 --- a/bin/sync-public-pr +++ b/bin/sync-public-pr @@ -1,24 +1,129 @@ -#!/usr/bin/env bash +#!/usr/bin/env ruby -set -euo pipefail +require "net/http" +require "open3" +require "json" -PR_NUMBER=$(buildkite-agent meta-data get "pull_request_number") -TARGET_BRANCH="docs-public-pr-${PR_NUMBER}" -SOURCE_REF="pull/${PR_NUMBER}/head" +# Job params +GH_TOKEN = ENV.fetch("GH_TOKEN") +PUBLIC_GH_REPO = ENV.fetch("PUBLIC_GH_REPO") +PRIVATE_GH_REPO = ENV.fetch("PRIVATE_GH_REPO") -echo "+++ Syncing PR #${PR_NUMBER} to local branch ${TARGET_BRANCH}" +PR_NUMBER = `buildkite-agent meta-data get "pull_request_number"`.strip.to_i +TARGET_BRANCH = "docs-public-pr-#{PR_NUMBER}" -set -x +class GithubClient + def initialize + @client = Net::HTTP.new("api.github.com", 443) + @client.use_ssl = true + end -git fetch "git@github.com:${PUBLIC_GH_REPO}.git" "${SOURCE_REF}:${TARGET_BRANCH}" + def public_pr(pr_number) + # https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#get-a-pull-request + response = @client.get("/repos/#{PUBLIC_GH_REPO}/pulls/#{pr_number}", headers) + puts response + JSON.parse(response.body) + end -git push --force origin "${TARGET_BRANCH}" + def private_pr_for_branch(branch_name) + # https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#list-pull-requests + # note this ignores closed/merged PRs + response = @client.get("/repos/#{PRIVATE_GH_REPO}/pulls?head=buildkite:#{branch_name}", headers) + puts response + JSON.parse(response.body).first + end -CREATE_PR_LINK="https://github.com/${PRIVATE_GH_REPO}/compare/${TARGET_BRANCH}?expand=1" + def open_pr(title:, body:, branch:) + # https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#create-a-pull-request + response = @client.post("/repos/#{PRIVATE_GH_REPO}/pulls", { + title: title, + body: body, + head: branch, + base: "main" + }.to_json, headers) + puts response + JSON.parse(response.body) + end -ANNOTATION_CONTENT="Synced PR ${PUBLIC_GH_REPO}#${PR_NUMBER} to private repo branch \`${TARGET_BRANCH}\`. + private -Open PR: ${CREATE_PR_LINK} -" + def headers + { + "Accept" => "application/vnd.github+json", + "Authorization" => "Bearer #{GH_TOKEN}", + "X-GitHub-Api-Version" =>"2022-11-28" + } + end +end -buildkite-agent annotate --context pr-link --style "info" "${ANNOTATION_CONTENT}" +def write_annotation(content, style: "info") + Open3.capture2("buildkite-agent", "annotate", "--style", style, stdin_data: content) +end + +client = GithubClient.new + +puts "+++ :git: Syncing #{PUBLIC_GH_REPO} PR ##{PR_NUMBER} to local branch #{TARGET_BRANCH}" + +# pull the magic PR ref from the public repo into local docs-public-pr- branch +source_ref = "pull/#{PR_NUMBER}/head" + +# https://git-scm.com/docs/git-fetch see docs on refspec format +refspec = "#{source_ref}:#{TARGET_BRANCH}" + +`git fetch --force "git@github.com:#{PUBLIC_GH_REPO}.git" "#{refspec}"` + + +puts "+++ :git: Pushing branch #{TARGET_BRANCH} to #{PRIVATE_GH_REPO}" + +`git push --force origin "#{TARGET_BRANCH}"` + +puts "+++ :github: Fetching original PR #{PUBLIC_GH_REPO} ##{PR_NUMBER}" + +public_pr = client.public_pr(PR_NUMBER) + +puts "--- :octocat: Checking for PR for #{TARGET_BRANCH} in #{PRIVATE_GH_REPO}" + +private_pr = client.private_pr_for_branch(TARGET_BRANCH) +puts private_pr + +if private_pr + annotation_content = <<~ANNOTATION + :open-pr: Re-synced PR #{PUBLIC_GH_REPO} ##{PR_NUMBER} to private repo branch `#{TARGET_BRANCH}`. + + Original PR: https://github.com/#{PUBLIC_GH_REPO}/pull/#{PR_NUMBER} + + Private PR: #{private_pr["html_url"]} :robot_face: + ANNOTATION + + write_annotation(annotation_content) + exit 0 +end + +puts "+++ :open-pr: Creating PR for #{TARGET_BRANCH} in #{PRIVATE_GH_REPO}" + +pr_description = <<-PR_DESCRIPTION + +:robot: Synced by #{ENV["BUILDKITE_BUILD_URL"]} :robot: + +_Note_: The original public PR will automatically close when this PR is merged. If there are additional changes to sync from the public PR, re-run the [Docs (Sync public PR)](https://buildkite.com/buildkite/docs-sync-public-pr) Pipeline. + +--- + +## [docs##{PR_NUMBER}](#{public_pr["html_url"]}): #{public_pr["title"]} + +Opened by @#{public_pr.dig("user", "login")} + +#{public_pr["body"]} +PR_DESCRIPTION + +private_pr = client.open_pr(title: public_pr["title"], body: pr_description, branch: TARGET_BRANCH) + +annotation_content = <<~ANNOTATION + :open-pr: Synced PR #{PUBLIC_GH_REPO} ##{PR_NUMBER} to private repo branch `#{TARGET_BRANCH}`. + + Original PR: #{public_pr["html_url"]} - #{public_pr["title"]} by @#{public_pr.dig("user", "login")} + + Private PR: #{private_pr["html_url"]} :robot_face: +ANNOTATION + +write_annotation(annotation_content) diff --git a/bin/utils.sh b/bin/utils.sh index 8584e37e4b..360ed9ee75 100644 --- a/bin/utils.sh +++ b/bin/utils.sh @@ -1,53 +1,75 @@ #!/bin/bash -# Find pull request number for a given branch. -# -# Why not `$BUILDKITE_PULL_REQUEST`? That env variable is only set -# for builds triggered via Github and it's possible previews are -# manually triggered via the API or Buildkite Dashboard. - -# ensure GH_REPO env var is present if [ -z "$GH_REPO" ]; then echo "GH_REPO env var is required" exit 1 fi +API_BASE_PATH="https://api.github.com/repos/${GH_REPO}" + +# Find pull request number for a given branch. +# +# Why not `$BUILDKITE_PULL_REQUEST`? That env variable is only set +# for builds triggered via Github and it's possible previews are +# manually triggered via the API or Buildkite Dashboard. function get_branch_pull_request_number() { + local branch=$1 + curl -L \ -H "Accept: application/vnd.github+json" \ -H "Authorization: Bearer $GH_TOKEN" \ -H "X-GitHub-Api-Version: 2022-11-28" \ - https://api.github.com/repos/${GH_REPO}/pulls\?head=buildkite\:$1 \ + ${API_BASE_PATH}/pulls\?head=buildkite\:${branch} \ | jq ".[0].number | select (.!=null)" } function find_github_comment() { + local pr_number="$1" + local msg="$2" + curl -L \ -H "Accept: application/vnd.github+json" \ -H "Authorization: Bearer $GH_TOKEN" \ -H "X-GitHub-Api-Version: 2022-11-28" \ - https://api.github.com/repos/${GH_REPO}/issues/$1/comments \ - | jq --arg msg "$2" '.[] | select(.body==$msg)' + ${API_BASE_PATH}/issues/${pr_number}/comments \ + | jq --arg msg "$msg" '.[] | select(.body==$msg)' } function post_github_comment() { + local pr_number=$1 + local msg=$2 + curl -L \ -X POST \ -H "Accept: application/vnd.github+json" \ -H "Authorization: Bearer $GH_TOKEN" \ -H "X-GitHub-Api-Version: 2022-11-28" \ - --data "{\"body\":\"$2\"}" \ - https://api.github.com/repos/${GH_REPO}/issues/$1/comments + --data "{\"body\":\"${msg}\"}" \ + ${API_BASE_PATH}/issues/${pr_number}/comments } function create_pull_request() { + local title="$1" + local body="$2" + local branch="$3" + + local request_body=$( + jq --null-input \ + --compact-output \ + --arg title "$title" \ + --arg body "$body" \ + --arg head "$branch" \ + --arg base "main" \ + '{title: $title, body: $body, head: $head, base: $base}' + ) + curl -L \ -X POST \ -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer $GH_TOKEN" \ + -H "Authorization Bearer $GH_TOKEN" \ -H "X-GitHub-Api-Version: 2022-11-28" \ - --data "{\"title\":\"$1\", \"body\":\"$2\", \"head\":\"$BRANCH\", \"base\":\"main\"}" \ - "https://api.github.com/repos/${GH_REPO}/pulls" + --json "$request_body" \ + "${API_BASE_PATH}/pulls" } function netlify_preview_id() {