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
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Trust policy for the backport workflow in DataDog/integrations-core
#
# This policy grants the tibdex/backport action permission to create
# backport pull requests when a merged PR has a backport label.
# This policy grants the backport-pr workflow (which runs `ddev release port-commit`)
# permission to push backport branches and open backport pull requests when a merged
# PR has a backport label.
#
# Naming convention:
# self: Only this repository (DataDog/integrations-core) can use this policy
Expand All @@ -13,7 +14,9 @@
# - Workflow file must be committed to master
#
# Permissions granted:
# - pull_requests: write - Create and update pull requests
# - contents: write - Push the backport branch
# - pull_requests: write - Create and update the backport pull request
# - workflows: write - Allow pushing commits that touch .github/workflows/
#
# Usage in workflows:
# - uses: DataDog/dd-octo-sts-action@acaa02eee7e3bb0839e4272dacb37b8f3b58ba80 # v1.0.3
Expand Down
87 changes: 73 additions & 14 deletions .github/workflows/backport-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,95 @@ on:
- labeled

jobs:
backport:
name: Backport PR
check:
name: Check for backport labels
runs-on: ubuntu-latest
permissions:
id-token: write # Required for OIDC token federation with dd-octo-sts
contents: read
# Cheap event filter: only merged PRs, and only the `closed` event or a freshly-added
# `backport/*` label. `should_run` below is the label-presence semaphore that gates the
# expensive backport job.
if: >
github.event.pull_request.merged
&& (
github.event.action == 'closed'
|| (
github.event.action == 'labeled'
&& contains(github.event.label.name, 'backport')
&& startsWith(github.event.label.name, 'backport/')
)
)
permissions:
contents: read
outputs:
should_run: ${{ steps.gate.outputs.should_run }}
steps:
- name: Detect backport labels
id: gate
env:
PR_LABELS_JSON: ${{ toJSON(github.event.pull_request.labels.*.name) }}
run: |
set -euo pipefail
if printf '%s' "${PR_LABELS_JSON}" | jq -e 'any(.[]; startswith("backport/"))' >/dev/null; then
echo "should_run=true" >> "${GITHUB_OUTPUT}"
else
echo "should_run=false" >> "${GITHUB_OUTPUT}"
fi

backport:
name: Backport PR
needs: check
if: needs.check.outputs.should_run == 'true'
runs-on: ubuntu-latest
permissions:
id-token: write # OIDC token federation with dd-octo-sts
# Writes use the scoped octo-sts token; ambient GITHUB_TOKEN stays read-only (pull_request_target).
contents: read
pull-requests: read
steps:
- name: Get GitHub token via dd-octo-sts
uses: DataDog/dd-octo-sts-action@acaa02eee7e3bb0839e4272dacb37b8f3b58ba80 # v1.0.3
id: octo-sts
with:
scope: DataDog/integrations-core
policy: self.backport.pull-request-target
- uses: tibdex/backport@9565281eda0731b1d20c4025c43339fb0a23812e # v2.0.4

- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
label_pattern: "^backport/(?<base>([^ ]+))$"
labels_template: "[\"backport\", \"bot\"]"
body_template: |
Backport <%= mergeCommitSha %> from #<%= number %>.
# Pin to master so the locally installed ddev always has `--from-pr`. Under
# `pull_request_target` the default ref is the PR's base branch, which for a PR merged into
# an older release branch would install a ddev lacking this command.
ref: master
# Full history reaches the merged commit and every release branch; octo-sts token authenticates the push.
fetch-depth: 0
token: ${{ steps.octo-sts.outputs.token }}
Comment thread
Kyle-Neale marked this conversation as resolved.

- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.13'

- name: Install ddev
uses: ./.github/actions/setup-ddev
with:
install-mode: local
cache-profile: local-ddev-base

- name: Configure ddev
run: |
ddev config set upgrade_check false
ddev config override

___
- name: Configure git identity
run: |
git config --global user.name "dd-agent-integrations-bot[bot]"
git config --global user.email "dd-agent-integrations-bot[bot]@users.noreply.github.com"

<%= body %>
github_token: ${{ steps.octo-sts.outputs.token }}
- name: Backport merged PR to labelled branches
env:
# ddev reads the token/user from these; the same token authenticates git push via checkout.
DD_GITHUB_TOKEN: ${{ steps.octo-sts.outputs.token }}
GH_TOKEN: ${{ steps.octo-sts.outputs.token }}
DD_GITHUB_USER: dd-agent-integrations-bot
run: |
ddev --no-interactive release port-commit --from-pr "${{ github.event.pull_request.number }}" \
--branch-prefix backport \
--pr-labels backport,bot
1 change: 1 addition & 0 deletions ddev/changelog.d/24470.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`release port-commit` gains a `--from-pr <number>` mode that backports a merged PR to every `backport/<base>` label on it (deriving the commit and target branches from the PR and skipping bases already backported), and now resets `.deps/` alongside `.in-toto` so regenerated dependency lockfiles are taken from the target branch.
86 changes: 51 additions & 35 deletions ddev/src/ddev/cli/release/port_commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@
@click.option('--draft', is_flag=True, default=False, help='Open the PR as a draft.')
@click.option('--verify', is_flag=True, default=False, help='Run commit hooks (skipped by default).')
@click.option('--dry-run', is_flag=True, default=False, help='Print every step instead of executing it.')
@click.option(
'--from-pr',
'from_pr',
type=int,
default=None,
metavar='PR_NUMBER',
help='Backport a merged PR to every `backport/<base>` label on it, deriving the commit and target '
'branches from the PR. Mutually exclusive with COMMIT_OR_PR.',
)
def port_commit(
app: Application,
commit_hash: str | None,
Expand All @@ -39,12 +48,13 @@ def port_commit(
draft: bool,
verify: bool,
dry_run: bool,
from_pr: int | None,
) -> None:
"""
Backport a commit onto a target branch.

Cherry-picks COMMIT_OR_PR onto `--target-branch` (default `master`) on a new branch named
`<github-user>/<prefix>-<sha[:10]>-<suffix>`, preserving `.in-toto` files from the target
`<github-user>/<prefix>-<sha[:10]>-<suffix>`, preserving `.in-toto` and `.deps/` files from the target
branch so package signatures stay intact. Pushes the branch and, unless `--no-pr` is set,
opens a pull request titled `[Backport] <subject>` and labeled with `--pr-labels`.

Expand All @@ -53,26 +63,30 @@ def port_commit(
first when a GitHub token is configured, and fall back to commit resolution on 404. If
omitted, the current HEAD commit is used after confirmation.

Pass `--from-pr <number>` instead of COMMIT_OR_PR to backport a merged PR to every
`backport/<base>` label on it, deriving the commit and target branches from the PR. A base
whose backport PR already exists (open, merged, or closed) is skipped, so re-runs are
idempotent. Give `--target-branch` alongside `--from-pr` to restrict the backport to that one
branch.

The GitHub user for the branch prefix is taken from `ddev config` (`github.user`) or the
`DD_GITHUB_USER` / `GITHUB_USER` / `GITHUB_ACTOR` environment variables.
"""
import logging

from ddev.cli.release.port_commit_workflow import (
PortStepError,
build_port_steps,
PortOptions,
display_completion_summary,
execute_port_plan,
resolve_port_plan,
run_backport_from_pr,
)

# httpx logs every request at INFO and clutters the workflow output. The PR-resolution and
# PR-creation steps already print their own status lines; the underlying HTTP traffic is noise.
logging.getLogger('httpx').setLevel(logging.WARNING)

plan = resolve_port_plan(
app,
commit_hash=commit_hash,
target_branch=target_branch,
options = PortOptions(
branch_prefix=branch_prefix,
branch_suffix=branch_suffix,
pr_labels=pr_labels,
Expand All @@ -81,36 +95,38 @@ def port_commit(
verify=verify,
dry_run=dry_run,
)
bundle = build_port_steps(app, plan)

success = False
error_msg: str | None = None
try:
for step in bundle.steps:
step.run()
success = True
except PortStepError as e:
error_msg = str(e)
finally:
# If the PR was created before the failure (e.g. labeling failed afterwards), the worktree
# holds no recoverable state — the work is pushed and the PR exists on GitHub. Suppress the
# warning in that case to avoid a misleading "inspect the worktree" message.
pr_already_created = bundle.pr_step is not None and bundle.pr_step.pr_url is not None
if not success and not plan.dry_run and not pr_already_created:
app.display_warning(f'Worktree left at `{plan.worktree_path}` for inspection.')

if error_msg is not None:
app.abort(error_msg)

try:
bundle.teardown.run()
except PortStepError as e:
app.display_warning(f'Could not remove worktree at `{plan.worktree_path}`: {e}')
app.display_warning(f'Run `git worktree remove --force {plan.worktree_path}` to clean it up manually.')

if from_pr is not None:
if commit_hash is not None:
app.abort('Pass either COMMIT_OR_PR or --from-pr, not both.')
from click.core import ParameterSource

ctx = click.get_current_context()
target_branch_explicit = ctx.get_parameter_source('target_branch') is not ParameterSource.DEFAULT
override_base = target_branch if target_branch_explicit else None
succeeded = run_backport_from_pr(
app,
pr_number=from_pr,
override_base=override_base,
options=options,
)
if not succeeded:
app.abort('One or more backports failed.')
return

plan = resolve_port_plan(
app,
commit_hash=commit_hash,
target_branch=target_branch,
options=options,
)

outcome = execute_port_plan(app, plan)
if outcome.error is not None:
app.abort(outcome.error)

if plan.dry_run:
app.display_success('Dry run complete.')
return

pr_url = bundle.pr_step.pr_url if bundle.pr_step is not None else None
display_completion_summary(app, plan, pr_url=pr_url)
display_completion_summary(app, plan, pr_url=outcome.pr_url)
Loading
Loading