diff --git a/.github/workflows/mirror-pr-to-gitcode.yml b/.github/workflows/mirror-pr-to-gitcode.yml new file mode 100644 index 0000000000..64d1c67d3d --- /dev/null +++ b/.github/workflows/mirror-pr-to-gitcode.yml @@ -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" diff --git a/docs/gitcode-pr-mirror.md b/docs/gitcode-pr-mirror.md new file mode 100644 index 0000000000..7da7b14d89 --- /dev/null +++ b/docs/gitcode-pr-mirror.md @@ -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- 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: /PTOAS +GitCode: /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 + +``` + +## 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.