-
-
Notifications
You must be signed in to change notification settings - Fork 186
ci(review): gate the claude pairing and share the branch probe #7076
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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: '' | ||||||||||
|
Comment on lines
+26
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Standards (judgement call): two inputs on this new interface are worth a second look — Speculative Generality on one, a silent failure mode on the other.
No caller omits |
||||||||||
| 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") | ||||||||||
|
Comment on lines
+77
to
+78
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correctness: a transport failure here aborts the step instead of falling back. A composite action's Concrete path: DNS failure, connection refused, connect timeout or a TLS reset reaching It also makes the
Suggested change
|
||||||||||
|
|
||||||||||
| 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 | ||||||||||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Standards (judgement call): this new checkout keeps the token that the same commit removed from its sibling.
The commit message states the rule it should be following ("That checkout also kept the app token in its working tree, and the token reaches two repos"), and applies it to the
Suggested change
Same at |
||||||||
|
|
||||||||
| - 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' | ||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Comment on lines
+67
to
+73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Standards (judgement call): this checkout is unguarded while its only consumer is guarded, and the two workflows now diverge on it for no stated reason.
Copying the consumer's condition onto the checkout would restore the symmetry. (I'm not offering a suggestion block — the condition is long enough that it wants to be read against line 77 rather than pasted.) Minor, and not something this branch introduces: |
||
|
|
||
| - 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 | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Standards (judgement call) —
codebase-design: "Interface — everything a caller must know to use the module correctly: the type signature, but also invariants, ordering constraints, error modes […]".The invariant this PR exists to establish — pairing happens only for an admin author — is not in the action's interface. It lives in the caller's expression, and enforcing it takes two inputs blanked in concert.
head-ref: ''alone does not skip pairing: lines 60-65 re-derive the branch viagh pr viewwheneverpr-repositoryandpr-numberare set. The description here says "Pass an empty string to skip pairing and take the default" without saying thatpr-repositorymust go too.The call site at
claude-code-review.yml:209-211is correct today —pr-repositoryis gated, so the fallback never fires, and the ungatedpr-numberon line 211 is inert. But the gate can be half-applied and nothing notices: a future caller that reads this description and blanks onlyhead-refsilently resumes pairing through thegh pr viewpath, reopening the hole the commit message describes.head-refis also carrying two jobs — a branch name and an on/off flag — which is what forces theis_admin == 'true' && … || ''ternary at both gated inputs.Worth moving the decision behind the seam: an explicit
paired:/enabled:input that owns "not paired ⇒ default branch", with the PR coordinates passed unconditionally. Then the gate is one expression, and it cannot be applied to only part of the input set.