Skip to content
Merged
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
78 changes: 78 additions & 0 deletions .github/actions/link-backport-prs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Link Backport PRs to Linear

A GitHub Action that links sorenlouv-created backport pull requests to the matching Linear sub-issue, so each backport closes the right per-release-line issue when it merges.

## What it does

When a merged source PR carries `backport-to-<branch>` labels, the [sorenlouv backport action](https://github.com/sorenlouv/backport-github-action) opens one backport PR per label. This action runs right after and, for each backport target:

1. Resolves the source PR's Linear issue (the parent) via Linear's `attachmentsForURL` reverse lookup, falling back to a `TEAM-123` identifier parsed from the branch name or body.
2. Finds the sub-issue whose title carries the release-line prefix for that target, e.g. `[0.34] Copy of ENGCP-906` for a backport to `v0.34` (a leading `v`, as in `[v0.34]`, is also accepted).
3. Appends `Fixes <sub-issue-id>` to that backport PR's body, unless it already references the issue.

The match is by title prefix, not milestone: the `[X.Y] Copy of ...` sub-issues created for a backport family do not reliably carry a patch milestone, so the title is the dependable key.

It is advisory and idempotent: it never fails the backport job (every error is a warning and it exits 0), it skips entirely when no `linear-token` is provided, and re-runs do not add duplicate `Fixes` lines.

## Usage

This action is wired into the shared [`backport.yaml`](../../workflows/backport.yaml) reusable workflow. A caller enables linking by passing a Linear token:

```yaml
jobs:
backport:
uses: loft-sh/github-actions/.github/workflows/backport.yaml@backport/v1
secrets:
gh-access-token: ${{ secrets.GH_ACCESS_TOKEN }}
linear-token: ${{ secrets.LINEAR_API_TOKEN }}
```

To run it directly:

```yaml
- uses: loft-sh/github-actions/.github/actions/link-backport-prs@link-backport-prs/v1
with:
source-pr: ${{ github.event.pull_request.number }}
repo-owner: ${{ github.repository_owner }}
repo-name: ${{ github.event.repository.name }}
github-token: ${{ secrets.GH_ACCESS_TOKEN }}
linear-token: ${{ secrets.LINEAR_API_TOKEN }}
```

The `github-token` must be the same PAT that created the backport PRs (a PAT, not the default `GITHUB_TOKEN`, so the backport PRs exist and are editable).

## Inputs

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

| INPUT | TYPE | REQUIRED | DEFAULT | DESCRIPTION |
|--------------|--------|----------|------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| dry-run | string | false | `"false"` | Log intended edits without applying them |
| github-token | string | true | | GitHub token with permission to read <br>and edit pull requests (must be the same PAT that created the backport PRs) |
| label-prefix | string | false | `"backport-to-"` | Prefix of the backport labels on <br>the source PR |
| linear-token | string | false | | Linear API token for resolving the <br>issue family. Optional by design: this <br>is an advisory step, so when <br>empty the action no-ops and exits <br>0 instead of failing, letting callers <br>adopt the shared backport workflow before <br>a Linear token is wired up. |
| repo-name | string | true | | The name of the repository |
| repo-owner | string | true | | The owner of the repository |
| source-pr | string | true | | The merged source pull request number <br>that was backported |

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

## Outputs

<!-- AUTO-DOC-OUTPUT:START - Do not remove or modify this section -->
No outputs.
<!-- AUTO-DOC-OUTPUT:END -->

## Development

### Testing

Run the unit tests:

```bash
./test.sh
# or
make test-link-backport-prs
```

The tests cover the pure matching logic: release-line extraction from a target branch, title-prefix matching (`[0.34]` and `[v0.34]`), sub-issue selection within an issue family, idempotency of the `Fixes` line, and identifier extraction fallback.
61 changes: 61 additions & 0 deletions .github/actions/link-backport-prs/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: 'Link Backport PRs to Linear'
description: 'Links sorenlouv-created backport PRs to the matching Linear sub-issue by adding "Fixes <id>" to the backport PR body'

inputs:
source-pr:
description: 'The merged source pull request number that was backported'
required: true
repo-owner:
description: 'The owner of the repository'
required: true
repo-name:
description: 'The name of the repository'
required: true
github-token:
description: 'GitHub token with permission to read and edit pull requests (must be the same PAT that created the backport PRs)'
required: true
linear-token:
description: 'Linear API token for resolving the issue family. Optional by design: this is an advisory step, so when empty the action no-ops and exits 0 instead of failing, letting callers adopt the shared backport workflow before a Linear token is wired up.'
required: false
Comment thread
Piotr1215 marked this conversation as resolved.
default: ''
label-prefix:
description: 'Prefix of the backport labels on the source PR'
required: false
default: 'backport-to-'
dry-run:
description: 'Log intended edits without applying them'
required: false
default: 'false'

runs:
using: 'composite'
steps:
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: ${{ github.action_path }}/src/go.mod

- name: Build and run Link Backport PRs
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.github-token }}
LINEAR_TOKEN: ${{ inputs.linear-token }}
ACTION_PATH: ${{ github.action_path }}
INPUT_SOURCE_PR: ${{ inputs.source-pr }}
INPUT_REPO_OWNER: ${{ inputs.repo-owner }}
INPUT_REPO_NAME: ${{ inputs.repo-name }}
INPUT_LABEL_PREFIX: ${{ inputs.label-prefix }}
INPUT_DRY_RUN: ${{ inputs.dry-run }}
run: |
cd "$ACTION_PATH/src"
go build -o link-backport-prs .
./link-backport-prs \
-source-pr="$INPUT_SOURCE_PR" \
-repo-owner="$INPUT_REPO_OWNER" \
-repo-name="$INPUT_REPO_NAME" \
-label-prefix="$INPUT_LABEL_PREFIX" \
-dry-run="$INPUT_DRY_RUN"

branding:
icon: 'link'
color: 'orange'
7 changes: 7 additions & 0 deletions .github/actions/link-backport-prs/src/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module github.com/loft-sh/github-actions/link-backport-prs

go 1.26.4

require github.com/google/go-github/v88 v88.0.0

require github.com/google/go-querystring v1.2.0 // indirect
7 changes: 7 additions & 0 deletions .github/actions/link-backport-prs/src/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v88 v88.0.0 h1:dZA9IKkPK1eXZj4ypngnpRj5FwdpTv4whix2PrQMP7M=
github.com/google/go-github/v88 v88.0.0/go.mod h1:rufTDgn2N45wjhukLTyxmvc9nilSp3mr3Rgtt6b1MPw=
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
Loading
Loading