From a27531110721ce16bcf3121d1b798206f989a119 Mon Sep 17 00:00:00 2001 From: vivekr-splunk Date: Thu, 2 Apr 2026 03:39:25 +0000 Subject: [PATCH] migration: add GitHub intake workflows for GitLab handoff --- .github/workflows/github-intake-issue.yml | 37 ++++++ .github/workflows/github-intake-pr.yml | 39 +++++++ hack/github-intake/issue-intake.sh | 81 +++++++++++++ hack/github-intake/lib/intake-common.sh | 136 ++++++++++++++++++++++ hack/github-intake/pr-intake.sh | 102 ++++++++++++++++ 5 files changed, 395 insertions(+) create mode 100644 .github/workflows/github-intake-issue.yml create mode 100644 .github/workflows/github-intake-pr.yml create mode 100755 hack/github-intake/issue-intake.sh create mode 100755 hack/github-intake/lib/intake-common.sh create mode 100755 hack/github-intake/pr-intake.sh diff --git a/.github/workflows/github-intake-issue.yml b/.github/workflows/github-intake-issue.yml new file mode 100644 index 000000000..866503a3b --- /dev/null +++ b/.github/workflows/github-intake-issue.yml @@ -0,0 +1,37 @@ +name: GitHub Issue Intake + +on: + issues: + types: + - opened + - reopened + - edited + +permissions: + contents: read + issues: write + +concurrency: + group: github-intake-issue-${{ github.event.issue.number }} + cancel-in-progress: false + +jobs: + intake: + runs-on: ubuntu-latest + steps: + - name: Checkout trusted automation from default branch + uses: actions/checkout@v4 + with: + ref: ${{ github.event.repository.default_branch }} + persist-credentials: false + + - name: Sync issue metadata into GitLab + env: + GITHUB_BACKLINK_MODE: ${{ vars.INTAKE_BACKLINK_MODE || 'none' }} + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITLAB_BASE_URL: ${{ vars.GITLAB_BASE_URL }} + GITLAB_PROJECT_PATH: ${{ vars.GITLAB_PROJECT_PATH }} + GITLAB_API_TOKEN: ${{ secrets.GITLAB_INTAKE_TOKEN }} + GITLAB_INTAKE_LABEL: ${{ vars.GITLAB_INTAKE_LABEL || 'github-intake,github-intake::issue' }} + run: bash hack/github-intake/issue-intake.sh diff --git a/.github/workflows/github-intake-pr.yml b/.github/workflows/github-intake-pr.yml new file mode 100644 index 000000000..6cffaef63 --- /dev/null +++ b/.github/workflows/github-intake-pr.yml @@ -0,0 +1,39 @@ +name: GitHub PR Intake + +on: + pull_request_target: + types: + - opened + - reopened + - synchronize + - edited + +permissions: + contents: read + issues: write + pull-requests: write + +concurrency: + group: github-intake-pr-${{ github.event.pull_request.number }} + cancel-in-progress: false + +jobs: + intake: + runs-on: ubuntu-latest + steps: + - name: Checkout trusted base-repository automation only + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.sha }} + persist-credentials: false + + - name: Sync PR metadata into GitLab intake record + env: + GITHUB_BACKLINK_MODE: ${{ vars.INTAKE_BACKLINK_MODE || 'none' }} + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITLAB_BASE_URL: ${{ vars.GITLAB_BASE_URL }} + GITLAB_PROJECT_PATH: ${{ vars.GITLAB_PROJECT_PATH }} + GITLAB_API_TOKEN: ${{ secrets.GITLAB_INTAKE_TOKEN }} + GITLAB_INTAKE_LABEL: ${{ vars.GITLAB_INTAKE_LABEL || 'github-intake,github-intake::pr' }} + run: bash hack/github-intake/pr-intake.sh diff --git a/hack/github-intake/issue-intake.sh b/hack/github-intake/issue-intake.sh new file mode 100755 index 000000000..b19185fd7 --- /dev/null +++ b/hack/github-intake/issue-intake.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +. "${ROOT_DIR}/github-intake/lib/intake-common.sh" + +require_env GITHUB_EVENT_PATH +require_env GITHUB_REPOSITORY +require_env GITHUB_SERVER_URL +require_env GITHUB_API_URL +require_env GITLAB_BASE_URL +require_env GITLAB_PROJECT_PATH +require_env GITLAB_API_TOKEN + +EVENT_JSON="$(python3 - "${GITHUB_EVENT_PATH}" <<'PY' +import json +import sys + +event = json.load(open(sys.argv[1])) +issue = event["issue"] +payload = { + "number": issue["number"], + "title": issue["title"], + "body": issue.get("body") or "", + "html_url": issue["html_url"], + "author": issue["user"]["login"], + "state": issue["state"], +} +print(json.dumps(payload)) +PY +)" + +ISSUE_NUMBER="$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["number"])' "${EVENT_JSON}")" +ISSUE_TITLE="$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["title"])' "${EVENT_JSON}")" +ISSUE_BODY="$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["body"])' "${EVENT_JSON}")" +ISSUE_URL="$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["html_url"])' "${EVENT_JSON}")" +ISSUE_AUTHOR="$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["author"])' "${EVENT_JSON}")" +ISSUE_STATE="$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["state"])' "${EVENT_JSON}")" + +MARKER="github-intake:issue:${GITHUB_REPOSITORY}#${ISSUE_NUMBER}" +LABELS="${GITLAB_INTAKE_LABEL:-github-intake,github-intake::issue}" +EXISTING="$(gitlab_find_issue_by_marker "${MARKER}")" + +if [[ -n "${EXISTING}" ]]; then + GITLAB_ISSUE_URL="$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["web_url"])' "${EXISTING}")" + echo "GitLab issue already exists: ${GITLAB_ISSUE_URL}" +else + DESCRIPTION="$(cat < +# GitHub Issue Intake + +- Source issue: ${ISSUE_URL} +- Source repository: ${GITHUB_REPOSITORY} +- Source author: ${ISSUE_AUTHOR} +- Source state: ${ISSUE_STATE} +- Intake marker: \`${MARKER}\` + +## GitHub Body + +\`\`\` +${ISSUE_BODY} +\`\`\` +EOF +)" + + CREATED="$(gitlab_create_issue "[GitHub Issue #${ISSUE_NUMBER}] ${ISSUE_TITLE}" "${DESCRIPTION}" "${LABELS}")" + GITLAB_ISSUE_URL="$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["web_url"])' "${CREATED}")" + echo "Created GitLab issue: ${GITLAB_ISSUE_URL}" +fi + +maybe_post_backlink_comment \ + "${ISSUE_NUMBER}" \ + "${MARKER}" \ + "Tracked in GitLab: ${GITLAB_ISSUE_URL}\n\nMarker: \`${MARKER}\`" + +cat <&2 + exit 1 + fi +} + +urlencode() { + python3 - "$1" <<'PY' +import sys +import urllib.parse + +print(urllib.parse.quote(sys.argv[1], safe="")) +PY +} + +gitlab_project_api() { + local encoded + encoded="$(urlencode "${GITLAB_PROJECT_PATH}")" + printf '%s/api/v4/projects/%s' "${GITLAB_BASE_URL%/}" "${encoded}" +} + +gitlab_auth_header() { + printf 'PRIVATE-TOKEN: %s' "${GITLAB_API_TOKEN}" +} + +gitlab_find_issue_by_marker() { + local marker="$1" + local search_url + + search_url="$(gitlab_project_api)/issues?search=$(urlencode "${marker}")&per_page=100" + curl -fsSL \ + --header "$(gitlab_auth_header)" \ + "${search_url}" \ + | python3 - "${marker}" <<'PY' +import json +import sys + +marker = sys.argv[1] +issues = json.load(sys.stdin) +for issue in issues: + description = issue.get("description") or "" + if marker in description: + print(json.dumps({ + "iid": issue["iid"], + "web_url": issue["web_url"], + "title": issue["title"], + })) + raise SystemExit(0) +print("") +PY +} + +gitlab_create_issue() { + local title="$1" + local description="$2" + local labels="$3" + + curl -fsSL \ + --request POST \ + --header "$(gitlab_auth_header)" \ + --data-urlencode "title=${title}" \ + --data-urlencode "description=${description}" \ + --data-urlencode "labels=${labels}" \ + "$(gitlab_project_api)/issues" +} + +github_comments_api() { + local issue_number="$1" + printf '%s/repos/%s/issues/%s/comments' "${GITHUB_API_URL%/}" "${GITHUB_REPOSITORY}" "${issue_number}" +} + +github_comment_exists() { + local issue_number="$1" + local marker="$2" + + curl -fsSL \ + --header "Authorization: Bearer ${GITHUB_TOKEN}" \ + --header "Accept: application/vnd.github+json" \ + "$(github_comments_api "${issue_number}")" \ + | python3 - "${marker}" <<'PY' +import json +import sys + +marker = sys.argv[1] +comments = json.load(sys.stdin) +for comment in comments: + if marker in (comment.get("body") or ""): + raise SystemExit(0) +raise SystemExit(1) +PY +} + +github_post_comment() { + local issue_number="$1" + local body="$2" + + python3 - "${body}" <<'PY' >/tmp/github-intake-comment.json +import json +import sys + +print(json.dumps({"body": sys.argv[1]})) +PY + + curl -fsSL \ + --request POST \ + --header "Authorization: Bearer ${GITHUB_TOKEN}" \ + --header "Accept: application/vnd.github+json" \ + --header "Content-Type: application/json" \ + --data @/tmp/github-intake-comment.json \ + "$(github_comments_api "${issue_number}")" >/dev/null +} + +maybe_post_backlink_comment() { + local issue_number="$1" + local marker="$2" + local body="$3" + local mode="${GITHUB_BACKLINK_MODE:-none}" + + if [[ "${mode}" != "comment" ]]; then + echo "GitHub backlink comment skipped: mode=${mode}" + return 0 + fi + + if github_comment_exists "${issue_number}" "${marker}"; then + echo "GitHub backlink comment already present for ${marker}" + return 0 + fi + + github_post_comment "${issue_number}" "${body}" + echo "GitHub backlink comment posted for ${marker}" +} diff --git a/hack/github-intake/pr-intake.sh b/hack/github-intake/pr-intake.sh new file mode 100755 index 000000000..7b2d77904 --- /dev/null +++ b/hack/github-intake/pr-intake.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +. "${ROOT_DIR}/github-intake/lib/intake-common.sh" + +require_env GITHUB_EVENT_PATH +require_env GITHUB_REPOSITORY +require_env GITHUB_SERVER_URL +require_env GITHUB_API_URL +require_env GITLAB_BASE_URL +require_env GITLAB_PROJECT_PATH +require_env GITLAB_API_TOKEN + +EVENT_JSON="$(python3 - "${GITHUB_EVENT_PATH}" <<'PY' +import json +import sys + +event = json.load(open(sys.argv[1])) +pr = event["pull_request"] +payload = { + "number": pr["number"], + "title": pr["title"], + "body": pr.get("body") or "", + "html_url": pr["html_url"], + "author": pr["user"]["login"], + "state": pr["state"], + "draft": pr["draft"], + "head_ref": pr["head"]["ref"], + "head_sha": pr["head"]["sha"], + "head_repo": pr["head"]["repo"]["full_name"], + "base_ref": pr["base"]["ref"], + "base_sha": pr["base"]["sha"], +} +print(json.dumps(payload)) +PY +)" + +PR_NUMBER="$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["number"])' "${EVENT_JSON}")" +PR_TITLE="$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["title"])' "${EVENT_JSON}")" +PR_BODY="$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["body"])' "${EVENT_JSON}")" +PR_URL="$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["html_url"])' "${EVENT_JSON}")" +PR_AUTHOR="$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["author"])' "${EVENT_JSON}")" +PR_STATE="$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["state"])' "${EVENT_JSON}")" +PR_DRAFT="$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["draft"])' "${EVENT_JSON}")" +PR_HEAD_REF="$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["head_ref"])' "${EVENT_JSON}")" +PR_HEAD_SHA="$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["head_sha"])' "${EVENT_JSON}")" +PR_HEAD_REPO="$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["head_repo"])' "${EVENT_JSON}")" +PR_BASE_REF="$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["base_ref"])' "${EVENT_JSON}")" +PR_BASE_SHA="$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["base_sha"])' "${EVENT_JSON}")" + +MARKER="github-intake:pr:${GITHUB_REPOSITORY}#${PR_NUMBER}" +LABELS="${GITLAB_INTAKE_LABEL:-github-intake,github-intake::pr}" +EXISTING="$(gitlab_find_issue_by_marker "${MARKER}")" + +if [[ -n "${EXISTING}" ]]; then + GITLAB_ISSUE_URL="$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["web_url"])' "${EXISTING}")" + echo "GitLab intake record already exists: ${GITLAB_ISSUE_URL}" +else + DESCRIPTION="$(cat < +# GitHub PR Intake + +- Source PR: ${PR_URL} +- Source repository: ${GITHUB_REPOSITORY} +- Source author: ${PR_AUTHOR} +- Source state: ${PR_STATE} +- Draft: ${PR_DRAFT} +- Intake marker: \`${MARKER}\` +- Base branch: \`${PR_BASE_REF}\` +- Base SHA: \`${PR_BASE_SHA}\` +- Head branch: \`${PR_HEAD_REF}\` +- Head SHA: \`${PR_HEAD_SHA}\` +- Head repository: \`${PR_HEAD_REPO}\` + +This intake record is metadata-only. It is not an authoritative GitLab merge request. + +## GitHub Body + +\`\`\` +${PR_BODY} +\`\`\` +EOF +)" + + CREATED="$(gitlab_create_issue "[GitHub PR #${PR_NUMBER}] ${PR_TITLE}" "${DESCRIPTION}" "${LABELS}")" + GITLAB_ISSUE_URL="$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["web_url"])' "${CREATED}")" + echo "Created GitLab intake record: ${GITLAB_ISSUE_URL}" +fi + +maybe_post_backlink_comment \ + "${PR_NUMBER}" \ + "${MARKER}" \ + "Tracked in GitLab intake: ${GITLAB_ISSUE_URL}\n\nMarker: \`${MARKER}\`" + +cat <