Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/github-intake-issue.yml
Original file line number Diff line number Diff line change
@@ -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
39 changes: 39 additions & 0 deletions .github/workflows/github-intake-pr.yml
Original file line number Diff line number Diff line change
@@ -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
81 changes: 81 additions & 0 deletions hack/github-intake/issue-intake.sh
Original file line number Diff line number Diff line change
@@ -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 <<EOF
<!-- ${MARKER} -->
# 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 <<EOF
issue_number=${ISSUE_NUMBER}
marker=${MARKER}
gitlab_issue_url=${GITLAB_ISSUE_URL}
backlink_mode=${GITHUB_BACKLINK_MODE:-none}
EOF
136 changes: 136 additions & 0 deletions hack/github-intake/lib/intake-common.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#!/usr/bin/env bash
set -euo pipefail

require_env() {
local name="$1"
if [[ -z "${!name:-}" ]]; then
echo "Missing required environment variable: ${name}" >&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}"
}
102 changes: 102 additions & 0 deletions hack/github-intake/pr-intake.sh
Original file line number Diff line number Diff line change
@@ -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 <<EOF
<!-- ${MARKER} -->
# 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 <<EOF
pr_number=${PR_NUMBER}
marker=${MARKER}
gitlab_issue_url=${GITLAB_ISSUE_URL}
backlink_mode=${GITHUB_BACKLINK_MODE:-none}
metadata_only=true
EOF
Loading