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
30 changes: 27 additions & 3 deletions .github/workflows/prepare-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ name: "Prepare Release"
# is tagged or published here, and NOTHING is pushed directly to master: the bump
# reaches master only when a human merges the PR, so the org-wide "pull request
# required" ruleset stays satisfied without any bypass.
# A prepare run REFUSES to start while a previous release PR is still open, so
# at most one release is in flight at a time.
#
# The bump type decides the channel: `prerelease` produces a `-<preid>.N` suffix
# (channel = that preid, e.g. dev/rc); patch/minor/major produce a stable
Expand Down Expand Up @@ -42,10 +44,13 @@ defaults:
run:
shell: bash

# One prep run per bump type, and never cancel one mid-flight: a cancelled run can
# leave a pushed prep branch with no PR opened for it.
# One prep run at a time — a constant group, not per-bump-type: the open-PR
# guard below declares one release in flight, and a per-input group would let
# two DIFFERENT-bump dispatches run concurrently, both passing the guard before
# either PR exists. Never cancel one mid-flight: a cancelled run can leave a
# pushed prep branch with no PR opened for it.
concurrency:
group: prepare-release-${{ inputs.bump }}-${{ inputs.preid }}
group: prepare-release
cancel-in-progress: false

jobs:
Expand All @@ -66,6 +71,25 @@ jobs:
# Unshallowed so origin/<prep-branch> exists for --force-with-lease.
fetch-depth: 0

- name: Refuse when a release PR is already open
run: |
set -euo pipefail
# One release in flight at a time: an un-merged release/prep-v* PR
# means a previous prepare was never resolved — a second bump would
# race it (two competing version numbers for the next tag). Merge or
# close the open PR first; this run fails by design. Same-repo,
# bot-authored PRs only: prep PRs are always bot-created here, and an
# unfiltered match would let ANY fork PR named release/prep-v* block
# releases indefinitely (the repo is public) — a hand-created prep PR
# is deliberately invisible to this guard; do not "fix" that away.
open_release_prs="$(gh pr list --base master --state open --limit 100 \
--json number,headRefName,isCrossRepository,author \
--jq '[.[] | select(.isCrossRepository == false and .author.login == "app/github-actions" and (.headRefName | startswith("release/prep-v")))] | map("#\(.number) (\(.headRefName))") | join(", ")')"
if [[ -n "$open_release_prs" ]]; then
echo "::error::release PR(s) already open: ${open_release_prs} — merge or close before preparing a new release" >&2
exit 1
fi

- name: Setup pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0

Expand Down
70 changes: 69 additions & 1 deletion .github/workflows/tag-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,29 @@ name: "Tag Release"
# the version's channel, tags master, and publishes a GitHub release. Nothing is
# pushed to master here, so the org-wide "pull request required" ruleset needs no
# bypass.
# On stable releases a follow-up job mints a 1-hour wire-release-bot token and
# fires a repository_dispatch to Wire-Network/wire-tools-ts so its
# update-wireio-deps.yaml opens a PR updating every `@wireio/*` range.
#
# No inputs: the release version is whatever master currently carries. Its suffix
# IS the channel: no suffix => stable => npm dist-tag `latest`; a `-<preid>.N`
# suffix => that preid is the dist-tag (e.g. dev, rc). The two human gates
# (dispatch + Environment approval) are the safety, not a typed version.
# (the release-PR merge + the release Environment's required reviewers) are the
# safety, not a typed version.

on:
# Auto-triggered when a release/prep-v* PR merges to master — the human
# merge is gate 1; the `release` Environment's required reviewers are gate 2
# (an org-configuration action of this change — the environment ships with
# no protection rules). The merge must be performed by a HUMAN: an
# auto-merge or any GITHUB_TOKEN-driven merge is suppressed by GitHub and
# fires no run — use workflow_dispatch (the standing manual/recovery path)
# if that ever changes. `pull_request closed` fires for every PR; the
# job-level `if` below skips everything but merged, same-repo release-prep
# PRs (skipped runs on ordinary merges are cosmetic noise).
pull_request:
types: [closed]
branches: [master]
workflow_dispatch: {}

permissions:
Expand All @@ -31,12 +47,24 @@ concurrency:
jobs:
tag:
name: Tag master and publish
# Manual dispatch, or a MERGED, SAME-REPO release-prep PR — everything
# else (unmerged closes, non-release branches, fork PRs whose head happens
# to be named release/prep-v*) skips, and skipped `tag` also skips the
# dependent dispatch job via `needs`.
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event.pull_request.merged == true &&
github.event.pull_request.head.repo.full_name == github.repository &&
startsWith(github.event.pull_request.head.ref, 'release/prep-v'))
runs-on: ubuntu-latest
# Required reviewers on this Environment are gate 2 -- nothing below runs until
# a human approves the deployment.
environment: release
env:
GH_TOKEN: ${{ github.token }}
outputs:
dist_tag: ${{ steps.resolve.outputs.dist_tag }}
version: ${{ steps.resolve.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
Expand Down Expand Up @@ -143,3 +171,43 @@ jobs:
args+=(--prerelease)
fi
gh release create "$TAG" "${args[@]}"

dispatch-wire-tools-ts-update:
name: Dispatch the wire-tools-ts dependency update
needs: tag
# Stable releases only — prerelease channels must never drive downstream
# updates. A failure here cannot touch the published release (separate
# job, runs after publish/tag/release); the manual fallback is
# wire-tools-ts's own workflow_dispatch on update-wireio-deps.yaml.
if: needs.tag.outputs.dist_tag == 'latest'
runs-on: ubuntu-latest
timeout-minutes: 10
# Nothing here uses GITHUB_TOKEN — the cross-repo call rides the App token.
permissions: {}
steps:
# wire-release-bot: the org's release App (contents: write + metadata:
# read — exactly the repository_dispatch minimum; its installation must
# include wire-tools-ts, see the plan's org-configuration actions). A
# 1-hour installation token downscoped to wire-tools-ts replaces any
# long-lived PAT, and is used ONLY for the dispatch API call below —
# NEVER for git (the App is a ruleset bypass actor). This is the App's
# ONE sanctioned use, pinned by the never-use-wire-release-bot rule.
- name: Mint the dispatch token
id: dispatch-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.RELEASE_BOT_APP_ID }}
private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
owner: Wire-Network
repositories: wire-tools-ts
permission-contents: write

- name: Fire the repository_dispatch
env:
GH_TOKEN: ${{ steps.dispatch-token.outputs.token }}
RELEASE_VERSION: ${{ needs.tag.outputs.version }}
run: |
set -euo pipefail
gh api "repos/Wire-Network/wire-tools-ts/dispatches" \
-f event_type=wire-libraries-ts-release \
-f "client_payload[reason]=wire-libraries-ts release v${RELEASE_VERSION}"
Loading
Loading