From 2574f8e3adc3f0839ed4ee397479555197fa2fb4 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Fri, 16 Oct 2026 23:20:36 +0200 Subject: [PATCH 1/2] Validate release refs before platform builds --- .github/workflows/platforms.yml | 57 ++++++++++++++++++++++++++++++--- .github/workflows/release.yml | 2 +- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/.github/workflows/platforms.yml b/.github/workflows/platforms.yml index f8f19cfd..a827a98b 100644 --- a/.github/workflows/platforms.yml +++ b/.github/workflows/platforms.yml @@ -14,7 +14,7 @@ on: workflow_call: inputs: ref: - description: "The commit, branch or tag to build. Empty for the one that triggered the caller." + description: "Release tag on main to build (vX.Y.Z). Empty for the commit that triggered the caller." type: string default: '' version: @@ -25,13 +25,62 @@ on: description: "How long the natives artifacts are kept." type: number default: 1 + outputs: + sha: + description: "The validated commit built and tested by this workflow." + value: ${{ jobs.resolve-ref.outputs.sha }} env: # The default webrtc.install.dir of webrtc-jni/pom.xml, below the home directory. WEBRTC_INSTALL_FOLDER: webrtc/build jobs: + # Resolve once, before any repository code runs or caches are accessed. A + # release may build the tag just pushed by prepare-release, but only if its + # commit is already in main. All consumers use the resulting immutable SHA. + resolve-ref: + name: Resolve build commit + runs-on: ubuntu-22.04 + permissions: + contents: read + outputs: + sha: ${{ steps.resolve.outputs.sha }} + steps: + - name: Fetch release history + if: inputs.ref != '' + uses: actions/checkout@v7 + with: + ref: main + fetch-depth: 0 + persist-credentials: false + + - name: Validate and resolve the build commit + id: resolve + env: + REQUESTED_REF: ${{ inputs.ref }} + TRIGGER_SHA: ${{ github.sha }} + shell: bash + run: | + if [ -z "$REQUESTED_REF" ]; then + echo "sha=$TRIGGER_SHA" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if ! [[ "$REQUESTED_REF" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::ref must be a release tag of the form vX.Y.Z" + exit 1 + fi + + release_sha=$(git rev-parse --verify "refs/tags/$REQUESTED_REF^{commit}") + if ! git merge-base --is-ancestor "$release_sha" refs/remotes/origin/main; then + echo "::error::The release tag must point to a commit on main" + exit 1 + fi + + echo "sha=$release_sha" >> "$GITHUB_OUTPUT" + build: + needs: resolve-ref name: build (${{ matrix.platform.name }}) strategy: fail-fast: false @@ -66,7 +115,7 @@ jobs: - name: Checkout code uses: actions/checkout@v7 with: - ref: ${{ inputs.ref }} + ref: ${{ needs.resolve-ref.outputs.sha }} # The media module builds FFmpeg from its third-party submodule. submodules: true @@ -102,7 +151,7 @@ jobs: # be loaded on the runner that built them. test-natives: name: test-natives (${{ matrix.platform.name }}) - needs: build + needs: [resolve-ref, build] timeout-minutes: 30 strategy: fail-fast: false @@ -150,7 +199,7 @@ jobs: - name: Checkout code uses: actions/checkout@v7 with: - ref: ${{ inputs.ref }} + ref: ${{ needs.resolve-ref.outputs.sha }} - name: Test natives uses: ./.github/actions/test-natives diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bf1f1a84..d994a849 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -166,7 +166,7 @@ jobs: - name: Checkout code uses: actions/checkout@v7 with: - ref: ${{ !inputs.dryRun && needs.prepare-release.outputs.release_tag || '' }} + ref: ${{ needs.platforms.outputs.sha }} - name: Set up Maven cache uses: actions/cache@v6 From 63241f59968e567a46aea0d07f419e1d1a6148ab Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Fri, 16 Oct 2026 23:29:17 +0200 Subject: [PATCH 2/2] Split release preparation from tag-scoped publication --- .github/workflows/platforms.yml | 63 +----------- .github/workflows/release-publish.yml | 138 ++++++++++++++++++++++++++ .github/workflows/release.yml | 108 +++++++------------- docs/guide/build.md | 27 ++++- 4 files changed, 205 insertions(+), 131 deletions(-) create mode 100644 .github/workflows/release-publish.yml diff --git a/.github/workflows/platforms.yml b/.github/workflows/platforms.yml index a827a98b..a4f39bb8 100644 --- a/.github/workflows/platforms.yml +++ b/.github/workflows/platforms.yml @@ -7,16 +7,12 @@ name: Platforms # from all platforms together, because Maven Central publishes a version as one # bundle. # -# Called by build.yml for every push and PR, and by release.yml for the tag it -# releases, so that both build exactly the same way. +# Called by build.yml for every push and PR, and by release-publish.yml for the tag it +# releases (and release.yml for dry runs), so that both build exactly the same way. on: workflow_call: inputs: - ref: - description: "Release tag on main to build (vX.Y.Z). Empty for the commit that triggered the caller." - type: string - default: '' version: description: "Optional local project version override for a release dry run." type: string @@ -25,62 +21,13 @@ on: description: "How long the natives artifacts are kept." type: number default: 1 - outputs: - sha: - description: "The validated commit built and tested by this workflow." - value: ${{ jobs.resolve-ref.outputs.sha }} env: # The default webrtc.install.dir of webrtc-jni/pom.xml, below the home directory. WEBRTC_INSTALL_FOLDER: webrtc/build jobs: - # Resolve once, before any repository code runs or caches are accessed. A - # release may build the tag just pushed by prepare-release, but only if its - # commit is already in main. All consumers use the resulting immutable SHA. - resolve-ref: - name: Resolve build commit - runs-on: ubuntu-22.04 - permissions: - contents: read - outputs: - sha: ${{ steps.resolve.outputs.sha }} - steps: - - name: Fetch release history - if: inputs.ref != '' - uses: actions/checkout@v7 - with: - ref: main - fetch-depth: 0 - persist-credentials: false - - - name: Validate and resolve the build commit - id: resolve - env: - REQUESTED_REF: ${{ inputs.ref }} - TRIGGER_SHA: ${{ github.sha }} - shell: bash - run: | - if [ -z "$REQUESTED_REF" ]; then - echo "sha=$TRIGGER_SHA" >> "$GITHUB_OUTPUT" - exit 0 - fi - - if ! [[ "$REQUESTED_REF" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "::error::ref must be a release tag of the form vX.Y.Z" - exit 1 - fi - - release_sha=$(git rev-parse --verify "refs/tags/$REQUESTED_REF^{commit}") - if ! git merge-base --is-ancestor "$release_sha" refs/remotes/origin/main; then - echo "::error::The release tag must point to a commit on main" - exit 1 - fi - - echo "sha=$release_sha" >> "$GITHUB_OUTPUT" - build: - needs: resolve-ref name: build (${{ matrix.platform.name }}) strategy: fail-fast: false @@ -115,7 +62,7 @@ jobs: - name: Checkout code uses: actions/checkout@v7 with: - ref: ${{ needs.resolve-ref.outputs.sha }} + ref: ${{ github.sha }} # The media module builds FFmpeg from its third-party submodule. submodules: true @@ -151,7 +98,7 @@ jobs: # be loaded on the runner that built them. test-natives: name: test-natives (${{ matrix.platform.name }}) - needs: [resolve-ref, build] + needs: build timeout-minutes: 30 strategy: fail-fast: false @@ -199,7 +146,7 @@ jobs: - name: Checkout code uses: actions/checkout@v7 with: - ref: ${{ needs.resolve-ref.outputs.sha }} + ref: ${{ github.sha }} - name: Test natives uses: ./.github/actions/test-natives diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml new file mode 100644 index 00000000..6b0668d7 --- /dev/null +++ b/.github/workflows/release-publish.yml @@ -0,0 +1,138 @@ +name: Publish release + +# GITHUB_TOKEN pushes do not trigger push workflows. release.yml explicitly +# dispatches this workflow on the tag after its atomic push. A maintainer can +# also dispatch it on that tag to retry, or push a release tag directly. +on: + push: + tags: + - 'v*' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: release-publish-${{ github.ref }} + cancel-in-progress: false + +jobs: + release-version: + name: Validate release tag + runs-on: ubuntu-22.04 + outputs: + version: ${{ steps.version.outputs.version }} + steps: + - name: Checkout release commit + uses: actions/checkout@v7 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + persist-credentials: false + + # No repository actions, build tools or caches are used until the tag is + # checked against both the triggering SHA and the trusted main history. + - name: Validate the tag and project version + id: version + env: + RELEASE_REF: ${{ github.ref }} + RELEASE_SHA: ${{ github.sha }} + shell: bash + run: | + if ! [[ "$RELEASE_REF" =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Publish release must run on a vX.Y.Z tag" + exit 1 + fi + if [ "$(git rev-parse --verify "$RELEASE_REF^{commit}")" != "$RELEASE_SHA" ]; then + echo "::error::The release tag no longer points to the triggering commit" + exit 1 + fi + if ! git merge-base --is-ancestor "$RELEASE_SHA" refs/remotes/origin/main; then + echo "::error::The release commit must be on main" + exit 1 + fi + + version="${RELEASE_REF#refs/tags/v}" + project_version=$(sed -n '0,//s|.*\([^<]*\).*|\1|p' pom.xml) + if [ "$project_version" != "$version" ]; then + echo "::error::The release tag does not match the project version" + exit 1 + fi + echo "version=$version" >> "$GITHUB_OUTPUT" + + platforms: + needs: release-version + uses: ./.github/workflows/platforms.yml + with: + retention-days: 7 + + publish-central: + name: Publish to Maven Central + needs: [release-version, platforms] + runs-on: ubuntu-22.04 + + steps: + - name: Checkout code + uses: actions/checkout@v7 + with: + ref: ${{ github.sha }} + + - name: Set up Maven cache + uses: actions/cache@v6 + with: + path: | + ~/.m2/repository + !~/.m2/repository/dev/onvoid/webrtc + key: maven-publish-${{ hashFiles('**/pom.xml') }} + restore-keys: maven-publish- + + - name: Set up JDK 17 + uses: actions/setup-java@v6 + with: + java-version: '17' + distribution: 'temurin' + server-id: central + server-username: MAVEN_USERNAME + server-password: MAVEN_TOKEN + gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} + gpg-passphrase: MAVEN_GPG_PASSPHRASE + + - name: Deploy + uses: ./.github/actions/publish + env: + MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} + MAVEN_TOKEN: ${{ secrets.MAVEN_TOKEN }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} + with: + version: ${{ needs.release-version.outputs.version }} + profiles: release + + publish-release: + name: Publish GitHub release + needs: publish-central + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout release commit + uses: actions/checkout@v7 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + persist-credentials: false + + - name: Generate the release notes + uses: orhun/git-cliff-action@v4 + with: + config: .github/changelog/cliff-release.toml + args: -v --current --strip footer --use-branch-tags + env: + OUTPUT: ${{ runner.temp }}/release-notes.md + + - name: Publish + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_TAG: ${{ github.ref_name }} + run: | + gh release create "$RELEASE_TAG" --draft --verify-tag \ + -t "Release $RELEASE_TAG" -F "$RUNNER_TEMP/release-notes.md" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d994a849..a11047be 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: Release +name: Prepare release on: workflow_dispatch: @@ -13,13 +13,13 @@ on: type: boolean default: false -# Only the jobs that push commits, tags or the GitHub release get write access. +# Preparation pushes commits/tags and dispatches publication; dry-run jobs stay read-only. permissions: contents: read # Two releases at once would race on the pushed commits and tags. concurrency: - group: release + group: release-prepare cancel-in-progress: false jobs: @@ -29,23 +29,25 @@ jobs: # way leaves main untouched, so the release can simply be dispatched again. prepare-release: name: Prepare release + if: github.ref == 'refs/heads/main' runs-on: ubuntu-22.04 permissions: contents: write + actions: write outputs: - release_tag: ${{ steps.release-version.outputs.tag }} release_version: ${{ steps.release-version.outputs.version }} - release_body: ${{ steps.git-cliff.outputs.content }} steps: # The full history, for git-cliff. - name: Checkout code uses: actions/checkout@v7 with: - ref: main + ref: ${{ github.sha }} fetch-depth: 0 + # Maven release:prepare needs a named branch, pinned to the dispatched SHA. - name: Git config run: | + git switch -C main git config --global user.name "${{ github.actor }}" git config --global user.email "${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com" @@ -59,6 +61,10 @@ jobs: run: | version=$(sed -n '0,//s|.*\([^<]*\).*|\1|p' pom.xml) version="${version%-SNAPSHOT}" + if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::The release version must look like 1.2.3" + exit 1 + fi echo "Release version: $version, next development version: $DEVELOPMENT_VERSION" if ! [[ "$DEVELOPMENT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+-SNAPSHOT$ ]]; then @@ -77,16 +83,6 @@ jobs: echo "version=$version" >> "$GITHUB_OUTPUT" echo "tag=v$version" >> "$GITHUB_OUTPUT" - # Written outside the checkout, which release:prepare requires to be clean. - - name: Generate the release notes - uses: orhun/git-cliff-action@v4 - id: git-cliff - with: - config: .github/changelog/cliff-release.toml - args: -v -u --strip footer --tag ${{ steps.release-version.outputs.tag }} --use-branch-tags - env: - OUTPUT: ${{ runner.temp }}/release-notes.md - - name: Update CHANGELOG.md uses: orhun/git-cliff-action@v4 with: @@ -147,18 +143,26 @@ jobs: RELEASE_TAG: ${{ steps.release-version.outputs.tag }} run: git push --atomic origin HEAD:main "refs/tags/$RELEASE_TAG" - # The tag prepare-release pushed. A dry run pushes none and builds the - # dispatched commit instead. + # Explicit dispatch is supported with GITHUB_TOKEN, unlike push events. + # --ref makes the new run's github.sha the release commit itself. + - name: Start publication on the release tag + if: ${{ !inputs.dryRun }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_TAG: ${{ steps.release-version.outputs.tag }} + run: gh workflow run release-publish.yml --ref "$RELEASE_TAG" + platforms: needs: prepare-release + if: inputs.dryRun uses: ./.github/workflows/platforms.yml with: - ref: ${{ !inputs.dryRun && needs.prepare-release.outputs.release_tag || '' }} retention-days: 7 - version: ${{ inputs.dryRun && needs.prepare-release.outputs.release_version || '' }} + version: ${{ needs.prepare-release.outputs.release_version }} publish-central: - name: Publish to Maven Central + name: Assemble dry-run deployment bundle + if: inputs.dryRun needs: [prepare-release, platforms] runs-on: ubuntu-22.04 @@ -166,7 +170,7 @@ jobs: - name: Checkout code uses: actions/checkout@v7 with: - ref: ${{ needs.platforms.outputs.sha }} + ref: ${{ github.sha }} - name: Set up Maven cache uses: actions/cache@v6 @@ -188,47 +192,33 @@ jobs: gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} gpg-passphrase: MAVEN_GPG_PASSPHRASE - - name: Determine the version to publish - id: release-version + - name: Set the release dry-run version env: - RELEASE_TAG: ${{ needs.prepare-release.outputs.release_tag }} - run: | - version="${RELEASE_TAG#v}" - if [ "${{ inputs.dryRun }}" == "true" ]; then - # Exercise release bundling rather than the snapshot deployment path. - mvn -B versions:set -DnewVersion="$version" -DgenerateBackupPoms=false - fi + RELEASE_VERSION: ${{ needs.prepare-release.outputs.release_version }} + run: mvn -B versions:set -DnewVersion="$RELEASE_VERSION" -DgenerateBackupPoms=false - echo "Version to publish: $version" - echo "version=$version" >> "$GITHUB_OUTPUT" - - # skipUpload stages and bundles the release, then stops before uploading. - # As a second line of defence the Central credentials are replaced by - # a placeholder during a dry run: should the upload ever happen regardless, it - # fails on authentication instead of publishing a release. Signing does run, so - # the GPG passphrase stays intact. + # Sign and assemble the real release bundle, but never upload it. The + # placeholder credentials also prevent publication if skipUpload regresses. - name: Deploy uses: ./.github/actions/publish env: - MAVEN_USERNAME: ${{ inputs.dryRun && 'dry-run-must-not-upload' || secrets.MAVEN_USERNAME }} - MAVEN_TOKEN: ${{ inputs.dryRun && 'dry-run-must-not-upload' || secrets.MAVEN_TOKEN }} + MAVEN_USERNAME: dry-run-must-not-upload + MAVEN_TOKEN: dry-run-must-not-upload MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} with: - version: ${{ steps.release-version.outputs.version }} + version: ${{ needs.prepare-release.outputs.release_version }} profiles: release - skip-upload: ${{ inputs.dryRun }} + skip-upload: true - name: Verify the dry-run deployment bundle exists - if: ${{ inputs.dryRun }} run: test -s target/central-publishing/central-bundle.zip - name: Summarize what would have been published - if: ${{ inputs.dryRun }} run: | { echo "## Dry run — nothing was published" echo - echo "Version: \`${{ steps.release-version.outputs.version }}\`" + echo "Version: \`${{ needs.prepare-release.outputs.release_version }}\`" echo echo "Artifacts destined for the deployment bundle:" echo '```' @@ -239,7 +229,6 @@ jobs: } >> "$GITHUB_STEP_SUMMARY" - name: Upload the deployment bundle for inspection - if: ${{ inputs.dryRun }} uses: actions/upload-artifact@v7 with: name: deployment-bundle-dry-run @@ -254,28 +243,3 @@ jobs: natives/*.asc if-no-files-found: error retention-days: 7 - - publish-release: - name: Publish GitHub release - needs: [prepare-release, publish-central] - if: ${{ !inputs.dryRun }} - runs-on: ubuntu-latest - permissions: - contents: write - - steps: - - name: Checkout code - uses: actions/checkout@v7 - - # The notes are commit subjects, so they go through the environment and a file - # rather than into the script, where a quote or a backtick would be run. - # --verify-tag fails instead of creating a tag that release:prepare did not. - - name: Publish - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_VERSION: ${{ needs.prepare-release.outputs.release_tag }} - RELEASE_BODY: ${{ needs.prepare-release.outputs.release_body }} - run: | - printf '%s\n' "$RELEASE_BODY" > release-notes.md - gh release create "$RELEASE_VERSION" --draft --verify-tag \ - -t "Release $RELEASE_VERSION" -F release-notes.md \ No newline at end of file diff --git a/docs/guide/build.md b/docs/guide/build.md index 681e89b3..29a6b514 100644 --- a/docs/guide/build.md +++ b/docs/guide/build.md @@ -28,4 +28,29 @@ On the first run, the WebRTC source tree will be loaded into the `//w [build-linux-ubuntu]: https://chromium.googlesource.com/chromium/src/+/master/docs/linux/build_instructions.md#system-requirements [build-linux-other]: https://chromium.googlesource.com/chromium/src/+/master/docs/linux/build_instructions.md#Notes-for-other-distros -[build-windows]: https://chromium.googlesource.com/chromium/src/+/master/docs/windows_build_instructions.md#visual-studio \ No newline at end of file +[build-windows]: https://chromium.googlesource.com/chromium/src/+/master/docs/windows_build_instructions.md#visual-studio + +## Release workflow + +Maintainers start **Prepare release** (`release.yml`) from `main`, providing the +next development version, such as `0.16.0-SNAPSHOT`. Preparation commits the +changelog and version changes and atomically pushes `main` and the release tag. +It then dispatches **Publish release** (`release-publish.yml`) on that tag using +`GITHUB_TOKEN` with `actions: write`; no additional token is needed. A push made +with `GITHUB_TOKEN` does not trigger a tag push workflow on its own. + +Publication requires a `vX.Y.Z` tag whose commit belongs to `main` and whose +version matches the root pom. All platform builds, native tests and publication +jobs check out the triggering commit SHA. After testing every platform, the +workflow publishes one Maven Central bundle and creates a draft GitHub release. + +Selecting **dryRun** in Prepare release builds and tests the dispatched commit +with the release version, signs and assembles a deployment bundle, and uploads +it as `deployment-bundle-dry-run`. It pushes no commits or tags, dispatches no +publication workflow, and uses placeholder Central credentials with uploads +disabled. + +If preparation pushed the tag but dispatch failed, start Publish release manually +and select that tag. Before retrying a failed publication, check whether Maven +Central already accepted the version; published releases cannot be overwritten. +Maintainer-pushed `v*` tags also start Publish release automatically.