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
73 changes: 73 additions & 0 deletions .github/workflows/backport.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: Backport

# Cherry-picks a merged `main` PR onto a release-line branch (upstream-first:
# everything lands on `main` first, then is brought forward). Add a label of
# the form `backport-<branch>` to a PR; when it merges, this opens a backport
# PR against that branch:
# * backport-stable -> stable (the currently-active additive line)
# * backport-v0.28 -> v0.28 (a specific long-lived line)
#
# The opened backport PR is gated by Tier 1A (see ci.yml). To run the full
# beta-preflight suite on a risky backport, add the `check-release` label to
# the backport PR.
#
# Engine: korthout/backport-action (self-contained GitHub Action; no external
# service). Mergify is a documented alternative if richer conflict handling or
# merge queues are ever needed -- see docs/release-process.md.

on:
pull_request_target:
types: [closed, labeled]

permissions:
contents: write
pull-requests: write

jobs:
backport:
name: Backport merged PR
if: >-
github.event.pull_request.merged == true &&
github.event.pull_request.base.ref == 'main' &&
contains(join(github.event.pull_request.labels.*.name, ','), 'backport-')
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
token: ${{ secrets.RELEASE_PLZ_ORG_TOKEN }}

- name: Create backport PRs
id: backport
uses: korthout/backport-action@2e830a1d0b8269505846ddd407a70876913ad1f8 # v4.6.0
with:
github_token: ${{ secrets.RELEASE_PLZ_ORG_TOKEN }}
comment_style: summary
# Labels look like `backport-<branch>`; the captured group is the
# target branch name.
label_pattern: '^backport-(?<target>.+)$'
pull_title: '${pull_title} (backport #${pull_number})'
pull_description: |
Automated backport of #${pull_number} to `${target_branch}`.

Upstream-first: this change already merged to `main`. If the
cherry-pick did not apply cleanly, adapt it so it compiles and
passes tests on this branch.

# Arm GitHub's native auto-merge on each backport PR that was opened. The
# actual merge only fires once the target branch's protection rules are
# satisfied -- i.e. CI is green and at least one approving review is in --
# so those requirements must be configured as branch protection on the
# release-line branches (CI passing + 1 required approval). Requires
# "Allow auto-merge" to be enabled in the repository settings.
- name: Enable auto-merge on backport PRs
if: steps.backport.outputs.created_pull_numbers != ''
env:
GH_TOKEN: ${{ secrets.RELEASE_PLZ_ORG_TOKEN }}
CREATED_PRS: ${{ steps.backport.outputs.created_pull_numbers }}
run: |
for pr in $CREATED_PRS; do
echo "Enabling auto-merge (squash) on backport PR #$pr"
gh pr merge "$pr" --auto --squash --repo "$GITHUB_REPOSITORY"
done
52 changes: 52 additions & 0 deletions .github/workflows/check-no-patch-deps.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Check no patch/git deps

# Reusable guard: fails if a `[patch]` section or a git dependency is present.
# `main`'s own `c2pa` dependency IS a git dependency by design -- it tracks
# c2pa-rs's `main` branch (see docs/release-process.md, "Tracking c2pa-rs
# main") so integration breaks surface immediately. That's fine on `main`,
# but it must never reach a release-line branch: crates.io requires every
# dependency to resolve to a published version. This is the mechanical
# enforcement of that split (note the triggers below deliberately exclude
# `main`).

on:
workflow_call:
pull_request:
branches:
- stable
- 'v0.*'
- '*-rc*'

jobs:
check:
name: No patch/git dependencies

# Fork-PR trust is enforced by GitHub's "Fork pull request workflows from
# outside collaborators" Actions setting (see ci.yml), not in this
# workflow; `workflow_call` invocations run unconditionally.
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master@2026-06-30
with:
toolchain: stable

- name: Fail if a [patch] section is present
run: |
if grep -rEn '^\[patch' --include=Cargo.toml .; then
echo "::error::A [patch] section is present in Cargo.toml. Settle it to a published version before releasing."
exit 1
fi
echo "No [patch] sections found."

- name: Fail if a git dependency is present
run: |
if cargo metadata --format-version=1 --no-deps \
| jq -e '.. | objects | select(.source? != null and (.source | tostring | test("git\\+")))' >/dev/null; then
echo "::error::A git dependency is present. Settle it to a published version before releasing."
exit 1
fi
echo "No git dependencies found."
132 changes: 132 additions & 0 deletions .github/workflows/reconciliation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
name: Release-branch reconciliation

# Reactive backstop for the upstream-first rule. The proactive guard is
# upstream-first-check.yml, which blocks a PR to a release branch that isn't
# already on `main`; this job catches anything that still slips through (e.g. a
# direct push that bypassed a PR). Everything is supposed to land on `main`
# first and only be cherry-picked onto release-line branches. This job detects
# the opposite -- a commit that exists on a release-line branch but not on
# `main` -- and opens an issue so the change can be forward-ported and isn't
# lost across a future major bump.
#
# It deliberately does NOT merge a release-line branch back into `main`:
# merging into the actively-refactored `main` is the conflict-prone direction
# and risks resurrecting code a refactor removed.
#
# Release-ceremony commits are excluded. A release line carries version/
# changelog bookkeeping commits that are native to the release process and
# must NOT be forward-ported -- RC version bumps (release-rc.yml), the final
# release-prep commit, etc. `main` owns its own version (release-plz plus the
# dev-cycle bump in release-train-cut.yml), so replaying these onto `main`
# would only cause conflicts. They are identified structurally -- by a diff
# confined entirely to bookkeeping paths (Cargo.toml, Cargo.lock, CHANGELOG.md)
# -- rather than by author or commit message, both of which vary (some bumps
# are made by github-actions[bot], some by a maintainer by hand). Any commit
# that touches a non-bookkeeping file is real work and is still reported.

on:
schedule:
- cron: '0 13 * * 1-5' # weekday mornings (UTC)
workflow_dispatch:

permissions:
contents: read
issues: write

jobs:
reconcile:
name: Detect drift from main
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Extend with active v0.* lines as they are created.
branch: [stable]
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0

- name: Find commits on release branch missing from main
id: drift
run: |
if ! git ls-remote --exit-code --heads origin "${{ matrix.branch }}" >/dev/null 2>&1; then
echo "Branch ${{ matrix.branch }} does not exist yet; nothing to reconcile."
echo "missing=" >> "$GITHUB_OUTPUT"
exit 0
fi
git fetch origin main "${{ matrix.branch }}"
# `git cherry` marks with `+` the commits on the release branch whose
# change is NOT present (as an equivalent patch) on main. Cherry-picks
# already on main are patch-equivalent and are NOT marked, so the `+`
# set is exactly the commits native to the release line.
CANDIDATES=$(git cherry origin/main "origin/${{ matrix.branch }}" | sed -n 's/^+ //p')

# Drop release-ceremony commits: those whose entire diff is confined
# to version/changelog bookkeeping files (see header comment). A
# commit touching any other path is real work and is reported.
MISSING=""
for sha in $CANDIDATES; do
files=$(git diff-tree --no-commit-id --name-only -r "$sha")
# -vE prints paths that are NOT bookkeeping; `|| true` keeps the
# step alive under `set -e` when grep matches nothing (all files
# are bookkeeping, i.e. a ceremony commit).
non_bookkeeping=$(printf '%s\n' "$files" \
| grep -vE '(^|/)Cargo\.toml$|^Cargo\.lock$|(^|/)CHANGELOG\.md$' || true)
if [ -n "$files" ] && [ -z "$non_bookkeeping" ]; then
echo "Skipping release-ceremony commit $sha (version/changelog bookkeeping only)."
else
MISSING="${MISSING}+ ${sha}"$'\n'
fi
done
MISSING=$(printf '%s' "$MISSING" | sed '/^$/d')

{
echo "missing<<EOF"
echo "$MISSING"
echo "EOF"
} >> "$GITHUB_OUTPUT"

- name: Open or update reconciliation issue
if: steps.drift.outputs.missing != ''
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
script: |
const branch = '${{ matrix.branch }}';
const missing = `${{ steps.drift.outputs.missing }}`;
const title = `Reconciliation: \`${branch}\` has commits missing from \`main\``;
const body = [
`The following commits are on \`${branch}\` but not on \`main\` (an upstream-first violation):`,
'',
'```',
missing,
'```',
'',
'Please forward-port these to `main`. This branch is intentionally **not** auto-merged back into `main`.',
].join('\n');

// Avoid opening a duplicate every run: update an existing open issue.
const existing = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'reconciliation',
});
const match = existing.data.find(i => i.title === title);
if (match) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: match.number,
body,
});
} else {
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title,
labels: ['reconciliation'],
body,
});
}
60 changes: 60 additions & 0 deletions .github/workflows/release-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Release-plz PR

# Opens/updates the version-bump + changelog "release PR" on a release-line
# branch (stable, or a retired v0.x line). Merging that PR is what triggers a
# publish (release.yml). For a breaking train, this prepares the 0.(x+1).0 bump
# once the release-candidate branch has been promoted (merged) into stable.
#
# Release-candidate branches themselves do not run release-plz; they only bake
# under the Tier CI suites. This runs release-plz `release-pr` ONLY -- it never
# publishes. Publishing is handled separately by release.yml.
# See docs/release-process.md.

permissions:
pull-requests: write
contents: write

on:
push:
branches:
- stable
- 'v0.*'
workflow_dispatch:

jobs:
release-pr:
name: Release-plz PR
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
token: ${{ secrets.RELEASE_PLZ_ORG_TOKEN }}

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master@2026-06-30
with:
toolchain: stable

- name: Run release-plz (release-pr)
uses: MarcoIeni/release-plz-action@064f4d1e36c843611ddf013be726beaa4ad804db # v0.5.129
with:
command: release-pr
verbose: true
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_ORG_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_ORG_TOKEN }}

# release-plz sometimes opens a new release branch + PR instead of
# updating the existing one, leaving the old branch behind. Delete stale
# release-plz branches to reduce noise, keeping the most recent.
- name: Clean up stale release-plz branches
run: |
git --no-pager branch --remote |\
grep 'origin/release-plz-' |\
sort -r |\
tail -n +2 |\
sed 's/origin\///' |\
xargs -r -I {} git push origin --delete {}
Loading