Skip to content
Closed
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
229 changes: 229 additions & 0 deletions .github/workflows/mirror-pr-to-gitcode.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
# Copyright (c) 2026 Huawei Technologies Co., Ltd.
# This program is free software; you can redistribute it and/or modify it under the terms and conditions of
# CANN Open Software License Agreement Version 2.0 (the "License").
# Please refer to the License for details. You may not use this file except in compliance with the License.
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED,
# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
# See LICENSE in the root of the software repository for the full text of the License.

name: Mirror PR to GitCode

on:
push:
branches-ignore:
- main
workflow_dispatch:
inputs:
pr_number:
description: Upstream GitHub PR number to mirror
required: true
type: string

permissions:
contents: read

concurrency:
group: gitcode-mirror-${{ github.repository }}-${{ inputs.pr_number || github.ref_name }}
cancel-in-progress: true

env:
GH_TOKEN: ${{ github.token }}
GITHUB_UPSTREAM_REPOSITORY: hw-native-sys/PTOAS
GITHUB_UPSTREAM_BRANCH: ${{ vars.GITHUB_UPSTREAM_BRANCH || 'test' }}
GITCODE_API: https://gitcode.com/api/v5
GITCODE_UPSTREAM: cann/pto-as
GITCODE_TARGET_BRANCH: master
GITCODE_SOURCE_PREFIX: mirror/github-pr-
GITCODE_FORK_OWNER: ${{ vars.GITCODE_FORK_OWNER }}
GITCODE_FORK_REPOSITORY: ${{ vars.GITCODE_FORK_REPOSITORY || 'pto-as' }}
GITCODE_COMMIT_EMAIL: ${{ vars.GITCODE_COMMIT_EMAIL }}

jobs:
mirror:
runs-on: ubuntu-22.04
steps:
- name: Resolve upstream pull request
env:
INPUT_PR_NUMBER: ${{ inputs.pr_number }}
EVENT_BRANCH: ${{ github.ref_name }}
shell: bash
run: |
set -euo pipefail

if [[ -n "$INPUT_PR_NUMBER" ]]; then
pr_number="$INPUT_PR_NUMBER"
else
pr_number=""
fi
if [[ ! "$pr_number" =~ ^[0-9]+$ ]]; then
source_branch="$EVENT_BRANCH"
if [[ -z "$source_branch" || "$source_branch" =~ (^|/)(\.|\.\.)($|/) ||
"$source_branch" == -* || "$source_branch" == */ || "$source_branch" == *' ' * ||
! "$source_branch" =~ ^[A-Za-z0-9._/-]+$ ]]; then
echo "The GitHub source branch is invalid." >&2
exit 1
fi

encoded_head="${GITHUB_REPOSITORY_OWNER}:${source_branch}"
prs="$(gh api --method GET \
"repos/${GITHUB_UPSTREAM_REPOSITORY}/pulls" \
-f state=open -f base="$GITHUB_UPSTREAM_BRANCH" \
-f head="$encoded_head" -f per_page=100)"
count="$(jq 'length' <<< "$prs")"
if [[ "$count" != "1" ]]; then
echo "Expected exactly one open upstream PR for ${GITHUB_REPOSITORY}:${source_branch}; found ${count}." >&2
exit 1
fi
pr_number="$(jq -r '.[0].number' <<< "$prs")"
fi

metadata="$(gh api "repos/${GITHUB_UPSTREAM_REPOSITORY}/pulls/${pr_number}")"
base_ref="$(jq -r '.base.ref' <<< "$metadata")"
head_sha="$(jq -r '.head.sha' <<< "$metadata")"
source_repo="$(jq -r '.head.repo.full_name // empty' <<< "$metadata")"
source_branch="$(jq -r '.head.ref // empty' <<< "$metadata")"
pr_state="$(jq -r '.state' <<< "$metadata")"

if [[ "$pr_state" != "open" || "$base_ref" != "$GITHUB_UPSTREAM_BRANCH" ||
! "$head_sha" =~ ^[0-9a-f]{40}$ || "$source_repo" != "$GITHUB_REPOSITORY" ||
-z "$source_branch" || ! "$source_branch" =~ ^[A-Za-z0-9._/-]+$ ]]; then
echo "The upstream PR does not belong to this GitHub fork or target branch." >&2
exit 1
fi

{
echo "PR_NUMBER=$pr_number"
echo "GITHUB_HEAD_SHA=$head_sha"
echo "GITHUB_SOURCE_BRANCH=$source_branch"
echo "MIRROR_BRANCH=${GITCODE_SOURCE_PREFIX}${pr_number}"
} >> "$GITHUB_ENV"

- name: Generate pull request patch
shell: bash
run: |
set -euo pipefail

github_dir="${RUNNER_TEMP}/github"
mkdir -p "$github_dir"
git -C "$github_dir" init -q
git -C "$github_dir" remote add upstream "https://github.com/${GITHUB_UPSTREAM_REPOSITORY}.git"
git -C "$github_dir" remote add fork "https://github.com/${GITHUB_REPOSITORY}.git"
git -C "$github_dir" -c http.extraHeader="Authorization: Bearer ${GH_TOKEN}" fetch \
--no-tags --filter=blob:none upstream \
"+refs/heads/${GITHUB_UPSTREAM_BRANCH}:refs/remotes/upstream/base"
git -C "$github_dir" -c http.extraHeader="Authorization: Bearer ${GH_TOKEN}" fetch \
--no-tags --filter=blob:none fork \
"+refs/heads/${GITHUB_SOURCE_BRANCH}:refs/remotes/fork/pr-head"

actual_head="$(git -C "$github_dir" rev-parse refs/remotes/fork/pr-head)"
if [[ "$actual_head" != "$GITHUB_HEAD_SHA" ]]; then
echo "The upstream PR changed while it was being read." >&2
exit 1
fi

merge_base="$(git -C "$github_dir" merge-base \
refs/remotes/upstream/base refs/remotes/fork/pr-head)"
git -C "$github_dir" -c diff.renames=false diff \
--no-ext-diff --no-textconv --binary --full-index \
"$merge_base" refs/remotes/fork/pr-head > "${RUNNER_TEMP}/github-pr.patch"

- name: Validate personal GitCode configuration
shell: bash
run: |
set -euo pipefail
if [[ ! "$GITCODE_FORK_OWNER" =~ ^[A-Za-z0-9_.-]+$ ||
! "$GITCODE_FORK_REPOSITORY" =~ ^[A-Za-z0-9_.-]+$ ||
! "$GITCODE_COMMIT_EMAIL" =~ ^[^[:space:]@]+@[^[:space:]@]+\.[^[:space:]@]+$ ]]; then
echo "Configure GITCODE_FORK_OWNER, GITCODE_FORK_REPOSITORY, and GITCODE_COMMIT_EMAIL in this fork." >&2
exit 1
fi
if [[ -z "${{ secrets.GITCODE_MIRROR_SSH_KEY }}" ||
-z "${{ secrets.GITCODE_KNOWN_HOSTS }}" ||
-z "${{ secrets.GITCODE_MIRROR_TOKEN }}" ]]; then
echo "Configure the GitCode mirror secrets in this fork." >&2
exit 1
fi
echo "GITCODE_FORK=${GITCODE_FORK_OWNER}/${GITCODE_FORK_REPOSITORY}" >> "$GITHUB_ENV"

- name: Configure GitCode SSH
env:
GITCODE_SSH_KEY: ${{ secrets.GITCODE_MIRROR_SSH_KEY }}
GITCODE_KNOWN_HOSTS: ${{ secrets.GITCODE_KNOWN_HOSTS }}
shell: bash
run: |
set -euo pipefail
install -d -m 700 "$HOME/.ssh"
printf '%s\n' "$GITCODE_SSH_KEY" > "$HOME/.ssh/gitcode_mirror"
printf '%s\n' "$GITCODE_KNOWN_HOSTS" > "$HOME/.ssh/known_hosts"
chmod 600 "$HOME/.ssh/gitcode_mirror" "$HOME/.ssh/known_hosts"
echo "GIT_SSH_COMMAND=ssh -i $HOME/.ssh/gitcode_mirror -o IdentitiesOnly=yes -o UserKnownHostsFile=$HOME/.ssh/known_hosts" >> "$GITHUB_ENV"

- name: Update personal GitCode fork branch
env:
GIT_AUTHOR_NAME: ${{ vars.GITCODE_FORK_OWNER }}
GIT_AUTHOR_EMAIL: ${{ vars.GITCODE_COMMIT_EMAIL }}
GIT_COMMITTER_NAME: ${{ vars.GITCODE_FORK_OWNER }}
GIT_COMMITTER_EMAIL: ${{ vars.GITCODE_COMMIT_EMAIL }}
shell: bash
run: |
set -euo pipefail

gitcode_dir="${RUNNER_TEMP}/gitcode"
git clone -q --single-branch --branch "$GITCODE_TARGET_BRANCH" \
"git@gitcode.com:${GITCODE_FORK}.git" "$gitcode_dir"
git -C "$gitcode_dir" checkout -q -B "$MIRROR_BRANCH" "origin/$GITCODE_TARGET_BRANCH"

old_remote="$(git -C "$gitcode_dir" ls-remote origin "refs/heads/${MIRROR_BRANCH}" | cut -f1)"
git -C "$gitcode_dir" apply --check --binary "${RUNNER_TEMP}/github-pr.patch"
git -C "$gitcode_dir" apply --index --binary "${RUNNER_TEMP}/github-pr.patch"

if ! git -C "$gitcode_dir" diff --cached --quiet; then
pr_title="$(gh api "repos/${GITHUB_UPSTREAM_REPOSITORY}/pulls/${PR_NUMBER}" --jq .title)"
git -C "$gitcode_dir" commit -q \
-m "$pr_title" \
-m "Mirrored from https://github.com/${GITHUB_UPSTREAM_REPOSITORY}/pull/${PR_NUMBER}" \
-m "GitHub-Head: ${GITHUB_HEAD_SHA}"
fi

git -C "$gitcode_dir" push \
--force-with-lease="refs/heads/${MIRROR_BRANCH}:${old_remote}" \
origin "HEAD:refs/heads/${MIRROR_BRANCH}"

- name: Create GitCode pull request if needed
env:
GITCODE_TOKEN: ${{ secrets.GITCODE_MIRROR_TOKEN }}
shell: bash
run: |
set -euo pipefail

pulls_file="${RUNNER_TEMP}/gitcode-pulls.json"
curl --fail-with-body --silent --show-error --retry 3 \
-H "Authorization: Bearer ${GITCODE_TOKEN}" \
-H 'Accept: application/json' \
"${GITCODE_API}/repos/${GITCODE_UPSTREAM}/pulls?state=all&per_page=100" > "$pulls_file"

existing_url="$(jq -r --arg branch "$MIRROR_BRANCH" --arg fork "$GITCODE_FORK" \
'.[] | select(.head.ref == $branch and .head.repo.full_name == $fork and .state == "open") | .html_url' \
"$pulls_file" | head -n 1)"
if [[ -n "$existing_url" ]]; then
echo "GitCode pull request already exists: $existing_url"
exit 0
fi

pr_title="$(gh api "repos/${GITHUB_UPSTREAM_REPOSITORY}/pulls/${PR_NUMBER}" --jq .title)"
body="Mirrored from: https://github.com/${GITHUB_UPSTREAM_REPOSITORY}/pull/${PR_NUMBER}\n\nGitHub head SHA: ${GITHUB_HEAD_SHA}\n\nThis branch is maintained from the GitHub PR and should not be edited directly."
payload="$(jq -n \
--arg title "[GitHub #${PR_NUMBER}] ${pr_title}" \
--arg body "$body" \
--arg head "${GITCODE_FORK_OWNER}:${MIRROR_BRANCH}" \
--arg base "$GITCODE_TARGET_BRANCH" \
'{title: $title, body: $body, head: $head, base: $base}')"
response="${RUNNER_TEMP}/gitcode-pr.json"
curl --fail-with-body --silent --show-error --retry 3 \
-X POST \
-H "Authorization: Bearer ${GITCODE_TOKEN}" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
"${GITCODE_API}/repos/${GITCODE_UPSTREAM}/pulls" \
--data "$payload" > "$response"
jq -r '"GitCode pull request: " + (.html_url // .web_url)' "$response"
78 changes: 78 additions & 0 deletions docs/gitcode-pr-mirror.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# GitCode PR mirror

This repository-specific workflow is designed for personal GitHub and GitCode
Forks. It mirrors pull requests from a personal GitHub Fork into the GitCode
upstream project `cann/pto-as`; GitHub `test` maps to GitCode `master` by
default while this workflow is being validated. Set the repository variable
`GITHUB_UPSTREAM_BRANCH=main` when the workflow is ready for production use.

## How it works

```text
Personal GitHub Fork branch push
Find the matching open PR in hw-native-sys/PTOAS
Compute the PR diff against the configured upstream branch
Push mirror/github-pr-<number> to the personal GitCode Fork
Create or reuse a PR to cann/pto-as:master
```

The workflow synchronizes the final diff instead of cherry-picking commits, so
the two repositories do not need identical Git histories. Later pushes to the
same GitHub branch update the same GitCode mirror branch and PR.

## Personal setup

Each contributor must first create both Forks:

```text
GitHub: <github-user>/PTOAS
GitCode: <gitcode-user>/pto-as
```

Copy this workflow into the personal GitHub Fork's default branch and enable
Actions. Configure these repository variables in that Fork:

- `GITCODE_FORK_OWNER`: the GitCode user or namespace that owns the Fork.
- `GITCODE_FORK_REPOSITORY`: the Fork repository name; normally `pto-as`.
- `GITCODE_COMMIT_EMAIL`: a verified email for the GitCode account.

Configure these repository secrets in the same GitHub Fork:

- `GITCODE_MIRROR_SSH_KEY`: a write-enabled SSH key for the personal GitCode
Fork; it must not be allowed to push `master`.
- `GITCODE_KNOWN_HOSTS`: the verified SSH host key for `gitcode.com`.
- `GITCODE_MIRROR_TOKEN`: a GitCode token for reading and creating PRs in
`cann/pto-as`.

The mirror commit author and committer are set to:

```text
<GITCODE_FORK_OWNER> <GITCODE_COMMIT_EMAIL>
```

## Daily usage

1. Create a GitHub PR from the personal Fork to the configured upstream branch
(`test` by default; set `GITHUB_UPSTREAM_BRANCH=main` for production).
2. Push to the PR branch in the personal GitHub Fork.
3. The `push` workflow finds the matching upstream PR and mirrors it.
4. Continue pushing to that GitHub branch; do not edit the GitCode mirror branch.

For an existing PR or a retry, open Actions in the personal GitHub Fork, run
`Mirror PR to GitCode`, and enter the upstream PR number.

The matching upstream PR must be open, target the configured upstream branch,
and have its head repo equal to the personal GitHub Fork. If no unique match is
found, the workflow stops without changing GitCode.

## Security requirements

This workflow does not check out or execute the pull request source code. It
only reads Git objects, computes a patch, and uses credentials from the
personal Fork's own Actions secrets. Keep all credentials in secrets, rotate
tokens that have been exposed, and never allow the mirror key to push the
GitCode protected branch.
Loading