diff --git a/.github/actions/paired-branch/action.yml b/.github/actions/paired-branch/action.yml new file mode 100644 index 00000000000..78ff1136e70 --- /dev/null +++ b/.github/actions/paired-branch/action.yml @@ -0,0 +1,91 @@ +name: Paired branch +description: >- + Resolve the branch of another repository that pairs with this pull request — one named the same + as the PR head branch — falling back to a default when there is none. + +inputs: + repository: + description: Repository to look the branch up in, as owner/name. + required: true + head-ref: + description: >- + The PR head branch name. Pass an empty string to skip pairing and take the default; a caller + that gates pairing on authorization does that. + required: false + default: '' + pr-repository: + description: >- + Repository holding the pull request, as owner/name. Used to look the head branch up when + head-ref is empty because the event payload carries no head — an issue_comment event. + required: false + default: '' + pr-number: + description: Pull request number, read together with pr-repository. + required: false + default: '' + default-branch: + description: Branch to use when the paired branch does not exist or cannot be probed. + required: false + default: master + token: + description: Token authorising the branch probe. Required when the repository is private. + required: false + default: '' + gh-token: + description: Token for the gh CLI, used only when looking up the head branch name. + required: false + default: '' + +outputs: + ref: + description: The paired branch, or the default branch. + value: ${{ steps.resolve.outputs.ref }} + +runs: + using: composite + steps: + - id: resolve + shell: bash + env: + REPOSITORY: ${{ inputs.repository }} + HEAD_REF: ${{ inputs.head-ref }} + PR_REPOSITORY: ${{ inputs.pr-repository }} + PR_NUMBER: ${{ inputs.pr-number }} + DEFAULT_BRANCH: ${{ inputs.default-branch }} + TOKEN: ${{ inputs.token }} + GH_TOKEN: ${{ inputs.gh-token }} + run: | + BRANCH="$HEAD_REF" + + if [[ -z "$BRANCH" && -n "$PR_REPOSITORY" && -n "$PR_NUMBER" ]]; then + BRANCH=$(gh pr view "$PR_NUMBER" --repo "$PR_REPOSITORY" --json headRefName --jq '.headRefName') || { + echo "::warning::Could not read the head branch of $PR_REPOSITORY#$PR_NUMBER; using $DEFAULT_BRANCH" + BRANCH="" + } + fi + + if [[ -z "$BRANCH" ]]; then + echo "ref=$DEFAULT_BRANCH" >> "$GITHUB_OUTPUT" + exit 0 + fi + + AUTH=() + if [[ -n "$TOKEN" ]]; then + AUTH=(-H "Authorization: Bearer $TOKEN") + fi + + STATUS=$(curl -s -o /dev/null -w '%{http_code}' "${AUTH[@]}" \ + "https://api.github.com/repos/$REPOSITORY/branches/$BRANCH") + + case "$STATUS" in + 200) + echo "ref=$BRANCH" >> "$GITHUB_OUTPUT" + ;; + 404) + echo "ref=$DEFAULT_BRANCH" >> "$GITHUB_OUTPUT" + ;; + *) + echo "::warning::Probing $REPOSITORY for branch $BRANCH returned HTTP $STATUS; using $DEFAULT_BRANCH" + echo "ref=$DEFAULT_BRANCH" >> "$GITHUB_OUTPUT" + ;; + esac diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index f8bf97d5733..260e1517800 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -147,31 +147,26 @@ jobs: gh api "repos/$REPO/issues/comments/$id" -X DELETE > /dev/null || true done + - name: Checkout trusted actions + if: steps.gate.outputs.proceed == 'true' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: master + sparse-checkout: .github/actions + sparse-checkout-cone-mode: false + path: .actions + - name: Determine cloud branch id: cloud-branch if: steps.gate.outputs.proceed == 'true' - env: - HEAD_REF: ${{ github.head_ref || github.event.pull_request.head.ref }} - PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} - APP_TOKEN: ${{ steps.app-token.outputs.token }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - run: | - if [[ -n "$HEAD_REF" ]]; then - BRANCH="$HEAD_REF" - else - BRANCH=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json headRefName --jq '.headRefName') || { - echo "::warning::Failed to resolve PR head ref name, falling back to master" - true - } - BRANCH="${BRANCH:-master}" - fi - if curl -sf -H "Authorization: Bearer $APP_TOKEN" \ - "https://api.github.com/repos/shellhub-io/cloud/branches/$BRANCH" > /dev/null 2>&1; then - echo "ref=$BRANCH" >> "$GITHUB_OUTPUT" - else - echo "ref=master" >> "$GITHUB_OUTPUT" - fi + uses: ./.actions/.github/actions/paired-branch + with: + repository: shellhub-io/cloud + head-ref: ${{ github.head_ref || github.event.pull_request.head.ref }} + pr-repository: ${{ github.repository }} + pr-number: ${{ github.event.pull_request.number || github.event.issue.number }} + token: ${{ steps.app-token.outputs.token }} + gh-token: ${{ secrets.GITHUB_TOKEN }} - name: Checkout cloud (context) if: steps.gate.outputs.proceed == 'true' @@ -183,47 +178,6 @@ jobs: fetch-depth: 1 path: cloud - - name: Determine claude branch - id: claude-branch - if: steps.gate.outputs.proceed == 'true' - env: - HEAD_REF: ${{ github.head_ref || github.event.pull_request.head.ref }} - PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} - APP_TOKEN: ${{ steps.app-token.outputs.token }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - run: | - if [[ -n "$HEAD_REF" ]]; then - BRANCH="$HEAD_REF" - else - BRANCH=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json headRefName --jq '.headRefName') || { - echo "::warning::Failed to resolve PR head ref name, falling back to master" - true - } - BRANCH="${BRANCH:-master}" - fi - if curl -sf -H "Authorization: Bearer $APP_TOKEN" \ - "https://api.github.com/repos/shellhub-io/claude/branches/$BRANCH" > /dev/null 2>&1; then - echo "ref=$BRANCH" >> "$GITHUB_OUTPUT" - else - echo "ref=master" >> "$GITHUB_OUTPUT" - fi - - - name: Checkout claude config - if: steps.gate.outputs.proceed == 'true' - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - with: - repository: shellhub-io/claude - token: ${{ steps.app-token.outputs.token }} - ref: ${{ steps.claude-branch.outputs.ref }} - fetch-depth: 1 - path: claude - - - name: Setup workspace context - if: steps.gate.outputs.proceed == 'true' - run: | - "$GITHUB_WORKSPACE/claude/workspace.sh" sync -w "$GITHUB_WORKSPACE" --project shellhub - - name: Check PR author team membership id: author-check if: steps.gate.outputs.proceed == 'true' @@ -246,6 +200,34 @@ jobs: echo "is_admin=false" >> "$GITHUB_OUTPUT" fi + - name: Determine claude branch + id: claude-branch + if: steps.gate.outputs.proceed == 'true' + uses: ./.actions/.github/actions/paired-branch + with: + repository: shellhub-io/claude + head-ref: ${{ steps.author-check.outputs.is_admin == 'true' && (github.head_ref || github.event.pull_request.head.ref) || '' }} + pr-repository: ${{ steps.author-check.outputs.is_admin == 'true' && github.repository || '' }} + pr-number: ${{ github.event.pull_request.number || github.event.issue.number }} + token: ${{ steps.app-token.outputs.token }} + gh-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Checkout claude config + if: steps.gate.outputs.proceed == 'true' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + repository: shellhub-io/claude + token: ${{ steps.app-token.outputs.token }} + persist-credentials: false + ref: ${{ steps.claude-branch.outputs.ref }} + fetch-depth: 1 + path: claude + + - name: Setup workspace context + if: steps.gate.outputs.proceed == 'true' + run: | + "$GITHUB_WORKSPACE/claude/workspace.sh" sync -w "$GITHUB_WORKSPACE" --project shellhub + - name: Load review procedure id: review-procedure if: steps.gate.outputs.proceed == 'true' diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 22a9b2ac78a..4db669b981f 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -64,31 +64,25 @@ jobs: with: ref: ${{ steps.pr-ref.outputs.sha || '' }} + - name: Checkout trusted actions + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: master + sparse-checkout: .github/actions + sparse-checkout-cone-mode: false + path: .actions + - name: Determine cloud branch id: cloud-branch if: github.event_name == 'pull_request_review_comment' || (github.event_name == 'issue_comment' && github.event.issue.pull_request) - env: - HEAD_REF: ${{ github.head_ref || github.event.pull_request.head.ref }} - PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} - APP_TOKEN: ${{ steps.app-token.outputs.token }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - run: | - if [[ -n "$HEAD_REF" ]]; then - BRANCH="$HEAD_REF" - else - BRANCH=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json headRefName --jq '.headRefName') || { - echo "::warning::Failed to resolve PR head ref name, falling back to master" - true - } - BRANCH="${BRANCH:-master}" - fi - if curl -sf -H "Authorization: Bearer $APP_TOKEN" \ - "https://api.github.com/repos/shellhub-io/cloud/branches/$BRANCH" > /dev/null 2>&1; then - echo "ref=$BRANCH" >> "$GITHUB_OUTPUT" - else - echo "ref=master" >> "$GITHUB_OUTPUT" - fi + uses: ./.actions/.github/actions/paired-branch + with: + repository: shellhub-io/cloud + head-ref: ${{ github.head_ref || github.event.pull_request.head.ref }} + pr-repository: ${{ github.repository }} + pr-number: ${{ github.event.pull_request.number || github.event.issue.number }} + token: ${{ steps.app-token.outputs.token }} + gh-token: ${{ secrets.GITHUB_TOKEN }} - name: Checkout cloud (context) uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7