Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
121ee26
ci: automate v1 backports from the v2 label
baptmont Aug 13, 2026
123a257
fix(backport): read only the places that record a backport
baptmont Aug 13, 2026
7db1bbd
fix(backport): stop a failed API call from looking like an empty queue
baptmont Aug 13, 2026
c1712ef
fix(backport): do not lose the pull request to a missing label
baptmont Aug 13, 2026
cc8cb9a
ci: say so when the backport token is not usable yet
baptmont Aug 13, 2026
d00dbb4
ci: drive the queue off v1-needed instead of v2
baptmont Aug 13, 2026
8ac4cb5
ci: run the backport on GITHUB_TOKEN instead of a PAT
baptmont Aug 18, 2026
5f9413b
ci: one backport PR per pull request, not one per run
baptmont Aug 19, 2026
86e1e2c
ci: make the backport script fail loudly instead of quietly
baptmont Aug 20, 2026
2e71696
ci: lint the shell scripts
baptmont Aug 20, 2026
797f5ba
ci: fail closed when the v1 trailer search fails
baptmont Aug 20, 2026
c6eb2f1
fix(backport): print a push command that works from a detached worktree
baptmont Aug 24, 2026
f0aafae
Merge branch 'main' into baptmont/backport-tooling
baptmont Aug 24, 2026
2fbb51e
fix(backport): close the round-three findings
baptmont Aug 25, 2026
47d8ddd
fix(backport): make the merge-shape guard fail closed
baptmont Aug 25, 2026
fce6485
fix(backport): stop the recovery steps dropping files the patch adds
baptmont Aug 26, 2026
cc62798
docs(backport): record the real residual of the merge-shape guard
baptmont Aug 26, 2026
05aa688
Merge branch 'main' into baptmont/backport-tooling
baptmont Aug 26, 2026
9729a69
Merge branch 'main' into baptmont/backport-tooling
baptmont Sep 3, 2026
930daaa
Merge branch 'main' into baptmont/backport-tooling
baptmont Sep 3, 2026
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
116 changes: 116 additions & 0 deletions .github/workflows/backport.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Opens the v1 backport PR for merged `main` pull requests labelled `v1-needed`.
#
# `v1-needed` is the only label with behaviour attached to it: it says a change
# on main still owes a 1.x equivalent. `v1` and `v2` are informational, recording
# which branch a PR targets. This workflow acts on `v1-needed`, and is the
# automation around scripts/backport.sh.
#
# The script replays each merged PR's squash commit onto its own branch cut from
# `v1`, rewriting the module path (google.golang.org/adk/v2 ->
# google.golang.org/adk) so the patch matches the 1.x import blocks, and opens
# one pull request per backport. Conflicts are not guessed at: that PR is left
# alone and the script comments on the original asking for a manual backport.
# One conflict costs one backport, not the run.
#
# Triggering on push rather than pull_request_target is deliberate. This job
# holds a token that can push and open pull requests, and pull_request_target
# grants that to a workflow running in the context of a pull request; push only
# ever fires for something already merged. The work is driven off the label
# queue rather than the event payload, so nothing here depends on which event
# woke it up.
#
# The built-in GITHUB_TOKEN is enough. Since June 2026 a pull request opened by
# github-actions[bot] does trigger its `pull_request` workflows, in an
# approval-required state: the runs are created but held until someone with
# write access clicks "Approve workflows to run" on the backport PR.
# https://github.blog/changelog/2026-06-11-bot-created-pull-requests-can-run-workflows-if-approved/
# That is one extra click on a pull request a human has to review and merge
# anyway, and it costs the repository no long-lived credential: this token is
# scoped to this repository, expires with the job, and belongs to no account.
#
# It does need "Allow GitHub Actions to create and approve pull requests"
# (Settings -> Actions -> General, inherited from the organization). That is not
# readable from a workflow, so it is not checked up front; the script recognises
# the failure and says so.
name: Backport to v1

on:
push:
branches: [main]

# Catches a PR labelled after it merged, which no push will follow.
schedule:
- cron: '0 3 * * *'

workflow_dispatch:
inputs:
prs:
description: 'PR numbers to backport, space separated, or "all" to drain the queue'
required: true
default: all

# contents: to push the backport branches. pull-requests: to open them, label
# them, and comment on an original whose replay conflicted.
permissions:
contents: write
pull-requests: write

# Serialize: two pushes in quick succession must not race on the same v1 base.
concurrency:
group: backport-v1
cancel-in-progress: false

jobs:
backport:
# Only the upstream repository has a v1 branch and the labels this reads.
# A fork that enables Actions would otherwise go red on every push to its
# own main, on a job that cannot do anything useful there.
if: github.repository == 'google/adk-go'
runs-on: ubuntu-latest
# The concurrency group above is serialized, so a wedged run holds up every
# later backport, and six hours of that is the default. Not tighter than
# this, though: the job is killed mid-drain if it overruns, and a kill
# between the push and `gh pr create` leaves a branch with no pull request
# behind it, which in_flight then reads as a backport already under way. A
# full-history checkout plus a backlog of replays has to fit comfortably.
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# The full history: the replay needs the merge base of main and v1.
fetch-depth: 0
# Kept, rather than the usual false, because the script pushes the
# backport branches. No untrusted code is checked out here.
persist-credentials: true

# The bot, not the merging human: it is the bot that pushes these branches
# and opens the pull requests, and each replayed commit keeps its original
# author regardless.
- name: Configure git identity
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

- name: Backport
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Passed through the environment rather than interpolated into the
# script body, so the value stays data instead of becoming shell.
PRS: ${{ inputs.prs }}
run: |
set -euo pipefail
args=()
if [ -n "${PRS:-}" ] && [ "${PRS}" != "all" ]; then
# Validated rather than trusted: the split below turns whatever this
# holds into separate arguments, and an option or a glob among them
# would be read as one. Matched with [[ =~ ]] rather than grep,
# which is line-oriented -- $'1301\n*' satisfies a grep anchored to
# ^...$ and then globs against the repository root.
if [[ ! "${PRS}" =~ ^[0-9]+([[:blank:]]+[0-9]+)*$ ]]; then
echo "::error::prs must be PR numbers separated by spaces, or 'all'"
exit 1
fi
# shellcheck disable=SC2206 # Deliberate split into separate args.
args=(${PRS})
fi
scripts/backport.sh "${args[@]}" --pr
83 changes: 83 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ We'd love to accept your patches and contributions to this project.

- [How to contribute](#how-to-contribute)
- [Branches](#branches)
- [Backporting to `v1`](#backporting-to-v1)
- [Multi-Module Development](#multi-module-development)
- [Before you begin](#before-you-begin)
- [Sign our Contributor License Agreement](#sign-our-contributor-license-agreement)
Expand Down Expand Up @@ -45,6 +46,88 @@ To work on a 1.x fix, base your branch on `v1`:
git switch -c my-fix origin/v1
```

### Backporting to `v1`

Three labels are in play, and only one of them does anything:

| Label | Meaning |
| ----------- | -------------------------------------------------------------- |
| `v1-needed` | This change still owes a 1.x equivalent. **Drives the queue.** |
| `v2` | Informational: the PR targets `main`. |
| `v1` | Informational: the PR targets the `v1` branch. |

Add `v1-needed` while the context is fresh — at review time, not later. It is
the only signal the automation reads, so a fix that should reach 1.x without it
is a fix nobody backports.

Merging a `v1-needed` PR opens its backport by itself: the
[`Backport to v1`](.github/workflows/backport.yml) workflow replays the squash
commit onto a branch cut from `v1` and opens a pull request against it. **One
backport PR per original PR** — a change that conflicts costs its own backport
and nothing else. The workflow works off the label queue rather than the merge
event, so a PR labelled *after* it merged is picked up by the nightly run, and
re-running on one that is already backported is a no-op rather than an error.

The workflow runs `scripts/backport.sh`, which is also usable directly — to
check the queue, or to work through a conflict the automation could not:

```bash
scripts/backport.sh --list # what is pending?
scripts/backport.sh 1301 # replay one PR into a scratch worktree
scripts/backport.sh --pr # replay the whole queue and open the PRs
```

It replays each commit on a detached HEAD in a scratch worktree under `$TMPDIR`,
so your working tree and your branches are left alone — it does add and remove
its own worktree in your clone, and prunes stale worktree registrations while
doing so. Nothing leaves the machine without `--pr`.

The script rewrites the module path (`google.golang.org/adk/v2` →
`google.golang.org/adk`) in each patch before applying it. That difference is
otherwise the main source of cherry-pick conflicts, because it puts every Go
file's import block out of sync between the two branches.

A PR leaves the queue when its change reaches `v1` — matched on the
`(cherry picked from commit <sha>)` trailer the script writes, not on anything
parsed out of a title — or while a backport pull request for it is open. Close
that pull request and delete its `backport/v1/pr-<n>` branch and the PR is
queued again, which is how you regenerate a backport that went stale. A branch
left behind by a run that died does not suppress anything: the next run replays
it.

Backport PRs get the usual CI, because the `pull_request` triggers in `go.yml`
and `apidiff.yml` filter on the base branch and list `v1` — but **the runs start
held**. A pull request opened by `github-actions[bot]` gets its workflows in an
approval-required state, so open the backport PR and click **Approve workflows
to run** in the merge box; anyone with write access can.

That is what running on the built-in `GITHUB_TOKEN` costs, and it is worth
paying: no long-lived credential lives in the repository, and the click lands on
a pull request someone has to review anyway. The one repository setting it needs
is "Allow GitHub Actions to create and approve pull requests", under
Settings → Actions → General.

Authorship follows the same mechanism as today, with one visible change. Each
replayed commit keeps its original author, and squash-merging the backport PR
makes the PR owner the author of the commit that lands on `v1`, recording the
original author as a `Co-authored-by:` trailer. Because these pull requests are
opened by `github-actions[bot]`, that owner is now the bot: `git blame` on `v1`
will point at it, and the human author is the trailer.

Two things the tooling cannot do for you:

- **A clean apply is not a correct backport.** `v1` may lack a helper or a
refactor that `main` already had, so a patch can apply and still not
compile. Build and test the backport branch before merging it.
- **Dependency changes need judgement.** The 1.x dependency set differs from
main's, so `go.mod` hunks often reject. Re-run with `--skip-gomod` and
`go mod tidy` the module instead.

When a patch conflicts, a local run applies what it can and leaves `.rej` files
in the worktree to work from. The unattended run does not push a half-applied
tree: it comments on the original PR asking for a manual backport, once, and
leaves the PR queued so a later `v1` change can still let it land on its own.

## Multi-Module Development

**Policy**: New integrations with heavy or optional dependencies must be created as separate Go modules.
Expand Down
Loading
Loading