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
48 changes: 40 additions & 8 deletions .github/actions/backport-fix-imports/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,36 @@ rewrite and reports `unchanged`, so re-runs are safe.
The action never fails the job: every failure path ends with `conclusion=failed`,
a one-line `reason` and exit 0, and nothing is pushed.

## AI pass

Only with `anthropic-api-key` set, and only when `.go` files the backport
changed still have conflicts after the fix. Non-Go conflicts are never sent.

1. Each conflicted Go file goes, whole, to [`ai-step`](../ai-step/README.md) in
one call. The model answers per conflict block: replacement code, or "not
sure".
2. A file is changed only when every block in it has an answer with no conflict
markers.
3. Each touched package must pass `gofmt` and `go vet`, and have no other
conflicted file. Packages that pass are committed as
`fix(backport): resolve conflicts with ai`. The rest are reverted.
4. Before that commit is pushed, the backport PR gets the
`backport-ai-resolved` label, so auto-approve can keep it for a person. If
the PR cannot be labeled, the AI commit is not pushed.

`gofmt` and `go vet` run without the push token in their environment.

So a wrong or partial answer never reaches the branch. It stays a conflict for a
person, as without the AI pass. The pass is skipped when there are more than 20
conflict blocks or the input would exceed 300 KB.

## PR comment

When there is something to tell the assignee, the action upserts one sticky
comment on the backport PR: the fix commit, the AI commit to review closely,
and each file left for them with the reason. No comment when nothing was
changed and nothing is left.

## Usage

```yaml
Expand All @@ -66,19 +96,21 @@ a one-line `reason` and exit 0, and nothing is pushed.
```

The runner needs `python3` and `git`; `gofmt` is optional and only formats the
import blocks it resolved (it ships on GitHub-hosted Ubuntu runners). Pushing
import blocks it resolved (it ships on GitHub-hosted Ubuntu runners). The AI
pass also needs `go`, which GitHub-hosted runners have. Pushing
with a PAT, rather than `GITHUB_TOKEN`, is what makes the fix commit trigger
the backport PR's CI.

<!-- AUTO-DOC-INPUT:START - Do not remove or modify this section -->

| INPUT | TYPE | REQUIRED | DEFAULT | DESCRIPTION |
|-----------------|--------|----------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| backport-branch | string | true | | Head branch of the backport PR, <br>e.g. backport/release-4.12/pr-8606. Checked out, fixed and <br>pushed back. |
| github-token | string | true | | Token with contents write on the <br>repository, used to fetch and push <br>the backport branch. Supplied to git <br>through the environment, never argv or <br>.git/config. |
| push | string | false | `"true"` | false commits the fix locally without <br>pushing (dry run). |
| source-branch | string | false | `"main"` | Branch the original change merged into. <br>Its root go.mod gives the source <br>module paths. |
| target-branch | string | true | | Release branch the backport PR targets, <br>e.g. release-4.12. Its root go.mod gives <br>the target module paths. |
| INPUT | TYPE | REQUIRED | DEFAULT | DESCRIPTION |
|-------------------|--------|----------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| anthropic-api-key | string | false | | Anthropic API key. Set, it turns <br>on the AI pass for Go <br>conflicts left after the fix; empty, <br>the AI pass never runs. Needs <br>go and gofmt on the runner. |
| backport-branch | string | true | | Head branch of the backport PR, <br>e.g. backport/release-4.12/pr-8606. Checked out, fixed and <br>pushed back. |
| github-token | string | true | | Token with contents write on the <br>repository, used to fetch and push <br>the backport branch. Supplied to git <br>through the environment, never argv or <br>.git/config. |
| push | string | false | `"true"` | false commits the fix locally without <br>pushing (dry run). |
| source-branch | string | false | `"main"` | Branch the original change merged into. <br>Its root go.mod gives the source <br>module paths. |
| target-branch | string | true | | Release branch the backport PR targets, <br>e.g. release-4.12. Its root go.mod gives <br>the target module paths. |

<!-- AUTO-DOC-INPUT:END -->

Expand Down
92 changes: 89 additions & 3 deletions .github/actions/backport-fix-imports/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ description: |
an older module major than its source (for example a v5 main backported to a
v4 release branch). Rewrites the import paths of modules the repository owns,
moves files added under a source-only staging directory, resolves conflict
hunks made only of import lines, then commits and pushes one commit. Conflicts
in real code are left and counted. Advisory: every failure path warns and
exits 0, so it never fails the backport that calls it.
hunks made only of import lines, then commits and pushes one commit. With an
Anthropic key, Go conflicts that remain go to an AI pass whose answers are
committed only for packages that pass go vet. Anything else is left for
review. Advisory: every failure path warns and exits 0, so it never fails the
backport that calls it.
inputs:
target-branch:
description: 'Release branch the backport PR targets, e.g. release-4.12. Its root go.mod gives the target module paths.'
Expand All @@ -25,6 +27,10 @@ inputs:
description: 'false commits the fix locally without pushing (dry run).'
required: false
default: 'true'
anthropic-api-key:
description: 'Anthropic API key. Set, it turns on the AI pass for Go conflicts left after the fix; empty, the AI pass never runs. Needs go and gofmt on the runner.'
required: false
default: ''

outputs:
conclusion:
Expand Down Expand Up @@ -52,3 +58,83 @@ runs:
PUSH: ${{ inputs.push }}
GH_TOKEN: ${{ inputs.github-token }}
run: python3 "${{ github.action_path }}/src/fix.py"

- name: Collect Go conflicts for the AI pass
id: prepare
if: inputs.anthropic-api-key != '' && (steps.fix.outputs.conclusion == 'fixed' || steps.fix.outputs.conclusion == 'unchanged')
continue-on-error: true
shell: bash
env:
TARGET_BRANCH: ${{ inputs.target-branch }}
run: python3 "${{ github.action_path }}/src/ai.py" prepare

- name: Resolve Go conflicts
id: ai
if: steps.prepare.outputs.input-file != ''
continue-on-error: true
# Pinned by SHA like every internal composite (docs/CONVENTIONS.md).
uses: loft-sh/github-actions/.github/actions/ai-step@17f1806257f071e23c72c5fae7cfa7af78ab5bae # ai-step/v1
with:
provider: anthropic
# Few calls (only backports with Go conflicts left), and a wrong answer
# costs a reviewer more than a stronger model costs us.
effort: high
max-tokens: '16000'
anthropic-api-key: ${{ inputs.anthropic-api-key }}
input-file: ${{ steps.prepare.outputs.input-file }}
prompt: |
A change merged on ${{ inputs.source-branch }} was cherry-picked onto the older release branch ${{ inputs.target-branch }}, and some Go files have git conflicts.
The JSON below lists each conflicted file in full. In every conflict block, the part between "<<<<<<< HEAD" and "=======" is the release branch; the part between "=======" and ">>>>>>>" is the backported change. A "|||||||" section, when present, is the common ancestor. The id of a block is the file path, "#", and its 0-based position in that file.

For every block, return its id and either:
- resolvable true and code: the final lines that take the place of the whole block, from its "<<<<<<<" line through its ">>>>>>>" line, with the file's indentation; or
- resolvable false, when you are not sure.

Rules:
- Keep what the backported change does, adapted to the release branch. Use only packages, types, functions and fields that the release-branch side of the file shows exist. Import paths of this repository use release_module, never a newer major version.
- If the change depends on code the release branch does not have, or you are not certain the result compiles and behaves as intended, return resolvable false and say why in reason. A wrong answer is worse than none: a person reviews what you leave.
- code is the resolved source only: it never contains a "<<<<<<<", "|||||||", "=======" or ">>>>>>>" line.
- reason is one short sentence.
output-schema: |
{
"type": "object",
"required": ["resolutions"],
"properties": {
"resolutions": {"type": "array", "items": {"type": "object", "required": ["id", "resolvable", "code", "reason"], "properties": {"id": {"type": "string"}, "resolvable": {"type": "boolean"}, "code": {"type": "string"}, "reason": {"type": "string"}}}}
}
}

- name: Apply AI resolutions that pass go vet
if: steps.ai.outputs.conclusion == 'success'
continue-on-error: true
shell: bash
env:
TARGET_BRANCH: ${{ inputs.target-branch }}
BACKPORT_BRANCH: ${{ inputs.backport-branch }}
PUSH: ${{ inputs.push }}
GH_TOKEN: ${{ inputs.github-token }}
INPUT_FILE: ${{ steps.prepare.outputs.input-file }}
AI_RESULT: ${{ steps.ai.outputs.result }}
run: python3 "${{ github.action_path }}/src/ai.py" apply

- name: Write the backport PR comment
id: report
if: steps.fix.outputs.conclusion == 'fixed' || steps.fix.outputs.conclusion == 'unchanged'
continue-on-error: true
shell: bash
env:
BACKPORT_BRANCH: ${{ inputs.backport-branch }}
GH_TOKEN: ${{ inputs.github-token }}
run: python3 "${{ github.action_path }}/src/report.py"

- name: Comment on the backport PR
if: steps.report.outputs.body != ''
continue-on-error: true
# Pinned by SHA like every internal composite (docs/CONVENTIONS.md).
uses: loft-sh/github-actions/.github/actions/sticky-pr-comment@d462e847d4f8f4a139890264a35b68cc30472547 # sticky-pr-comment/v1
with:
marker: '<!-- backport-fix-imports -->'
body: ${{ steps.report.outputs.body }}
pr-number: ${{ steps.report.outputs.pr-number }}
github-token: ${{ inputs.github-token }}
expected-author: ${{ steps.report.outputs.author }}
Loading
Loading