From 6b1ebc33dcd6bd70e66db7db61e194089c5a2a5e Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 1 Apr 2026 15:23:37 -0500 Subject: [PATCH] Add a "release-gate" step to the release workflow (#18804) The way this works is 1. `release-gate` is an environment which requires approval from another person in the organization 2. Once approved, the release can continue 3. GitHub then requires approval for every subsequent job, which we use the `release` environment for 4. We do not require team members to approve on the `release` environment because we run _many_ child jobs during releases 5. The `release` environment uses a deployment protection rule which queries a GitHub App we manage 6. The GitHub App checks if the `release-gate` job was successful in the same workflow and approves or denies accordingly The GitHub App's source is at https://github.com/open-security-tools/ost-environment-gate and includes another explanation of what's going on in this process. We don't make the release-gate block everything, so builds can start at least while we wait for the release-gate to be approved. --- .github/workflows/publish-crates.yml | 1 - .github/workflows/publish-mirror.yml | 1 - .github/workflows/publish-pypi.yml | 2 -- .github/workflows/release.yml | 18 ++++++++++++++++-- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish-crates.yml b/.github/workflows/publish-crates.yml index 11a6e2fb7..e6f3c4ad0 100644 --- a/.github/workflows/publish-crates.yml +++ b/.github/workflows/publish-crates.yml @@ -17,7 +17,6 @@ jobs: runs-on: ubuntu-latest environment: name: release - deployment: false permissions: contents: read id-token: write diff --git a/.github/workflows/publish-mirror.yml b/.github/workflows/publish-mirror.yml index 6cec5edad..324fd0fd3 100644 --- a/.github/workflows/publish-mirror.yml +++ b/.github/workflows/publish-mirror.yml @@ -17,7 +17,6 @@ jobs: runs-on: ubuntu-latest environment: name: release - deployment: false env: VERSION: ${{ fromJson(inputs.plan).announcement_tag }} steps: diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index ecd033228..30b0f2db8 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -17,7 +17,6 @@ jobs: runs-on: ubuntu-latest environment: name: release - deployment: false permissions: id-token: write # For PyPI's trusted publishing steps: @@ -36,7 +35,6 @@ jobs: runs-on: ubuntu-latest environment: name: release - deployment: false permissions: id-token: write # For PyPI's trusted publishing steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eeff69753..7ae8b3e15 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -52,6 +52,16 @@ env: CARGO_DIST_CHECKSUM: "cd355dab0b4c02fb59038fef87655550021d07f45f1d82f947a34ef98560abb8" jobs: + release-gate: + name: release-gate + if: ${{ inputs.tag != 'dry-run' }} + runs-on: ubuntu-latest + environment: + name: release-gate + deployment: false + steps: + - run: echo "Release approved" + # Run 'dist plan' (or host) to determine what tasks we need to do plan: runs-on: "depot-ubuntu-latest-4" @@ -108,7 +118,8 @@ jobs: custom-build-docker: needs: - plan - if: ${{ needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload' || inputs.tag == 'dry-run' }} + - release-gate + if: ${{ always() && needs.plan.result == 'success' && (needs.release-gate.result == 'success' || needs.release-gate.result == 'skipped') && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload' || inputs.tag == 'dry-run') }} uses: ./.github/workflows/build-docker.yml with: plan: ${{ needs.plan.outputs.val }} @@ -256,6 +267,7 @@ jobs: needs: - plan - host + - release-gate if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }} uses: ./.github/workflows/publish-pypi.yml with: @@ -270,6 +282,7 @@ jobs: needs: - plan - host + - release-gate - custom-publish-pypi # DIRTY: see #16989 if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }} uses: ./.github/workflows/publish-crates.yml @@ -286,12 +299,13 @@ jobs: needs: - plan - host + - release-gate - custom-publish-pypi - custom-publish-crates # use "always() && ..." to allow us to wait for all publish jobs while # still allowing individual publish jobs to skip themselves (for prereleases). # "host" however must run to completion, no skipping allowed! - if: ${{ always() && needs.host.result == 'success' && (needs.custom-publish-pypi.result == 'skipped' || needs.custom-publish-pypi.result == 'success') && (needs.custom-publish-crates.result == 'skipped' || needs.custom-publish-crates.result == 'success') }} + if: ${{ always() && needs.host.result == 'success' && needs.release-gate.result == 'success' && (needs.custom-publish-pypi.result == 'skipped' || needs.custom-publish-pypi.result == 'success') && (needs.custom-publish-crates.result == 'skipped' || needs.custom-publish-crates.result == 'success') }} runs-on: "depot-ubuntu-latest-4" environment: name: release