c2patool 0.28.4

Tool for displaying and creating C2PA manifests.
name: Promote release candidate

# Promotes a baked release-candidate branch (`0.N.0-rc`) onto the `stable`
# release line -- the deliberate, manual final step of a breaking train's bake
# (see docs/release-process.md).
#
# This workflow does NOT publish. It resets the candidate's `c2patool` version
# back to the last published release and force-pushes the baked train onto
# `stable`; the release-PR job in `release.yml` then opens the reviewed
# `0.N.0` version + changelog PR, and merging THAT is what publishes.
#
# Why reset the version rather than strip `-rc` to the final number: a push to
# `stable` whose version is already ahead of crates.io makes `release.yml` run
# `release-plz release` and publish immediately, which would skip the reviewed
# release PR and its generated changelog. Resetting to the last published
# version keeps `stable` level with the registry, so the release-PR job -- not
# this workflow -- computes the train's bump and writes the changelog.
#
# Note: this only resets c2patool's OWN version. The `c2pa` dependency version
# (tracking a c2pa-rs release, e.g. 0.91.0) is left untouched -- it is not the
# axis this repo publishes and is not reset by this workflow.
#
# Prerequisites:
#   * The `RELEASE_PLZ_ORG_TOKEN` identity must be allowed to force-push
#     `stable` and to create `v0.*` branches (branch-protection bypass).
#   * Release-candidate build tags must live OUTSIDE the `c2patool-v*` namespace
#     (see release-rc.yml), so release-plz baselines the changelog on the last
#     real release rather than on an `-rc.N` build tag.

permissions:
  contents: write

on:
  workflow_dispatch:

jobs:
  promote:
    name: Promote release candidate
    runs-on: ubuntu-latest
    steps:
      - name: Refuse to run outside a release-candidate branch
        run: |
          # An RC branch is named `<version>-rc` (no numeric suffix). Guard
          # against dispatching against main / stable / a v0.x line.
          case "${{ github.ref_name }}" in
            *-rc) echo "Promoting RC branch ${{ github.ref_name }}." ;;
            *)
              echo "::error::promote must be dispatched on a *-rc branch, not '${{ github.ref_name }}'."
              exit 1
              ;;
          esac

      - name: Checkout RC branch
        uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
        with:
          ref: ${{ github.ref_name }}
          fetch-depth: 0
          token: ${{ secrets.RELEASE_PLZ_ORG_TOKEN }}

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

      - name: Compute promotion versions
        id: ver
        run: |
          set -euo pipefail

          # The candidate version currently on the RC branch, e.g. 0.29.0-rc.3.
          # c2patool is a single-package (non-workspace) crate, so this is just
          # the one package's version -- no need to select by name.
          RC_C2PATOOL=$(cargo metadata --format-version=1 --no-deps | jq -r '.packages[0].version')

          # The outgoing `stable` version is what is on crates.io (stable tracks
          # the latest published release). It names the snapshot branch and is
          # the version we reset to so release-plz recomputes the train's bump.
          git fetch origin stable
          OLD_C2PATOOL=$(git show origin/stable:Cargo.toml | sed -n 's/^version = "\(.*\)"/\1/p' | head -1)
          if [ -z "$OLD_C2PATOOL" ]; then
            echo "::error::Could not read the outgoing c2patool version from origin/stable:Cargo.toml."
            exit 1
          fi

          # e.g. 0.28.3 -> v0.28.
          OLD_LINE="v$(echo "$OLD_C2PATOOL" | cut -d. -f1,2)"

          {
            echo "rc_c2patool=$RC_C2PATOOL"
            echo "old_c2patool=$OLD_C2PATOOL"
            echo "old_line=$OLD_LINE"
          } >> "$GITHUB_OUTPUT"
          echo "Promoting ${{ github.ref_name }} ($RC_C2PATOOL) onto stable; resetting to $OLD_C2PATOOL so the release-PR job computes the train bump."

      - name: Snapshot outgoing stable line
        run: |
          set -euo pipefail
          LINE="${{ steps.ver.outputs.old_line }}"
          if git ls-remote --exit-code --heads origin "$LINE" >/dev/null 2>&1; then
            echo "Snapshot branch $LINE already exists; leaving it as-is."
          else
            # Point the retiring line at the current tip of stable so critical
            # backports to the old release can still target it.
            git push origin "refs/remotes/origin/stable:refs/heads/$LINE"
            echo "Created snapshot branch $LINE from stable."
          fi

      - name: Reset candidate to the last published version
        run: |
          set -euo pipefail
          # Move c2patool's own version back to the last published number.
          # `stable` then sits level with crates.io, so the force-push below
          # does not trigger an immediate publish; the bump is left for the
          # release-PR job to compute and for a human to review.
          #
          # A plain string replace, not `cargo set-version` (from cargo-edit) --
          # that tool refuses to ever move a version backward, with no
          # override, so it cannot perform the reset this step exists to do.
          # Only the FIRST `version = "..."` line in Cargo.toml is touched (the
          # `[package]` version), so this can never collide with the `c2pa`
          # dependency's own `version = "..."` entries further down the file.
          RC_C2PATOOL="${{ steps.ver.outputs.rc_c2patool }}"
          OLD_C2PATOOL="${{ steps.ver.outputs.old_c2patool }}"
          if ! grep -q "^version = \"$RC_C2PATOOL\"" Cargo.toml; then
            echo "::error::Expected to find version = \"$RC_C2PATOOL\" as the package version in Cargo.toml."
            exit 1
          fi
          sed -i "0,/^version = \"$RC_C2PATOOL\"/s//version = \"$OLD_C2PATOOL\"/" Cargo.toml

      - name: Force-push baked train onto stable
        run: |
          set -euo pipefail
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"

          # The reset above is a no-op when the candidate branch's version was
          # never bumped away from the last published version in the first
          # place (e.g. release-rc.yml didn't run when the branch was cut), so
          # there is nothing to commit here. That's fine -- the candidate's
          # baked commits are still promoted by force-pushing HEAD below.
          if git diff --quiet; then
            echo "No version reset needed; promoting ${{ github.ref_name }}'s existing commits as-is."
          else
            git commit -am "chore: promote ${{ github.ref_name }} onto stable (reset to ${{ steps.ver.outputs.old_c2patool }} for release-plz)"
          fi

          # Force-push rather than merge so stable's history becomes exactly the
          # coherent set of changes baked on the RC branch (see
          # docs/release-process.md). The push triggers release.yml, whose
          # release-PR job opens the reviewed version + changelog PR. Pushed
          # with a PAT so that the downstream workflow actually runs -- a push
          # made with the default GITHUB_TOKEN does not cascade into other workflows.
          git push --force origin "HEAD:stable"

      - name: Summary
        run: |
          {
            echo "## Release candidate promoted"
            echo ""
            echo "- Branch \`${{ github.ref_name }}\` (candidate \`${{ steps.ver.outputs.rc_c2patool }}\`) force-pushed onto \`stable\`, reset to \`${{ steps.ver.outputs.old_c2patool }}\`."
            echo "- Outgoing \`stable\` snapshotted as \`${{ steps.ver.outputs.old_line }}\` for backports."
            echo ""
            echo "The release-PR job in \`release.yml\` will open the reviewed version + changelog PR on \`stable\`. **Merging that PR publishes to crates.io.**"
            echo ""
            echo "If release-plz under-bumps (the train is breaking by policy but \`cargo-semver-checks\` detected no break), force the final number on that PR with \`release-plz set-version\`."
          } >> "$GITHUB_STEP_SUMMARY"