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
42 changes: 0 additions & 42 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,6 @@ jobs:
needs: [lint, build, test, embeddings-integration]
runs-on: ubuntu-latest
permissions:
actions: write
checks: read
contents: write
issues: write
pull-requests: write
Expand All @@ -105,43 +103,3 @@ jobs:
with:
release-type: node
target-branch: main
- name: Run checks
id: checks
if: steps.prepare.outputs.prs_created == 'true'
env:
GH_TOKEN: ${{ github.token }}
RELEASE_PR: ${{ steps.prepare.outputs.pr }}
run: |
pr_number="$(jq -er '.number' <<< "${RELEASE_PR}")"
pr="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${pr_number}")"
pr_head_sha="$(jq -r '.head.sha' <<< "${pr}")"
pr_head_ref="$(jq -r '.head.ref' <<< "${pr}")"
version="$(gh api -H "Accept: application/vnd.github.raw+json" "repos/${GITHUB_REPOSITORY}/contents/package.json?ref=${pr_head_sha}" --jq '.version')"
semver='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*)|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.((0|[1-9][0-9]*)|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$'
if [[ ! "${version}" =~ ${semver} ]]; then
echo "::error::Release Please produced an invalid version: ${version}"
exit 1
fi
dispatch="$(
gh api --method POST \
-H "X-GitHub-Api-Version: 2026-03-10" \
"repos/${GITHUB_REPOSITORY}/actions/workflows/ci.yml/dispatches" \
-f "ref=${pr_head_ref}" \
-F return_run_details=true
)"
ci_run_id="$(jq -er '.workflow_run_id' <<< "${dispatch}")"
gh run watch "${ci_run_id}" --repo "${GITHUB_REPOSITORY}" --exit-status
echo "pr=${pr_number}" >> "${GITHUB_OUTPUT}"
echo "version=${version}" >> "${GITHUB_OUTPUT}"
- name: Request release approval
if: steps.prepare.outputs.prs_created == 'true'
env:
GH_TOKEN: ${{ github.token }}
RELEASE_PR: ${{ steps.checks.outputs.pr }}
VERSION: ${{ steps.checks.outputs.version }}
run: |
gh api --method POST "repos/${GITHUB_REPOSITORY}/dispatches" \
-f event_type=prepared-release \
-f "client_payload[sha]=${GITHUB_SHA}" \
-f "client_payload[pr]=${RELEASE_PR}" \
-f "client_payload[version]=${VERSION}"
132 changes: 83 additions & 49 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,30 +1,19 @@
name: Release
run-name: Release v${{ github.event.client_payload.version }}
run-name: Release latest from main

on:
repository_dispatch:
types: [prepared-release]
workflow_dispatch:

jobs:
approve:
name: approve v${{ github.event.client_payload.version }}
runs-on: ubuntu-latest
environment: release
concurrency:
group: release-approval
cancel-in-progress: true
permissions: {}
steps:
- name: Approval granted
env:
VERSION: ${{ github.event.client_payload.version }}
run: echo "Approved release v${VERSION}"
concurrency:
group: release
cancel-in-progress: false

jobs:
release:
name: release v${{ github.event.client_payload.version }}
needs: approve
name: release latest from main
runs-on: ubuntu-latest
permissions:
actions: write
contents: write
issues: write
pull-requests: write
Expand All @@ -33,51 +22,96 @@ jobs:
tag_name: ${{ steps.finalize.outputs.tag_name }}
version: ${{ steps.finalize.outputs.version }}
steps:
- name: Verify approved release
- name: Require main
run: |
if [[ "${GITHUB_REF}" != "refs/heads/main" ]]; then
echo "::error::Run the Release workflow from main"
exit 1
fi
- name: Update release PR
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5
with:
release-type: node
target-branch: main
skip-github-release: true
- name: Find release candidate
id: candidate
env:
APPROVED_SHA: ${{ github.event.client_payload.sha }}
EXPECTED_VERSION: ${{ github.event.client_payload.version }}
GH_TOKEN: ${{ github.token }}
RELEASE_PR: ${{ github.event.client_payload.pr }}
run: |
if [[ ! "${RELEASE_PR}" =~ ^[0-9]+$ ]]; then
echo "::error::Invalid release PR: ${RELEASE_PR}"
candidates="$(
gh pr list --repo "${GITHUB_REPOSITORY}" --state open --base main \
--limit 100 \
--json number,headRefName \
--jq '[.[] | select(
.headRefName == "release-please--branches--main" or
(.headRefName | startswith("release-please--branches--main--components--"))
)]'
)"
candidate_count="$(jq 'length' <<< "${candidates}")"
if [[ "${candidate_count}" -ne 1 ]]; then
echo "::error::Expected exactly one open Release Please PR, found ${candidate_count}"
exit 1
fi
Comment thread
nicholls73 marked this conversation as resolved.
current_sha="$(gh api "repos/${GITHUB_REPOSITORY}/commits/main" --jq '.sha')"
if [[ "${current_sha}" != "${APPROVED_SHA}" ]]; then
echo "::error::main changed after approval; approve the latest Release run"
pr_number="$(jq -r '.[0].number' <<< "${candidates}")"
pr="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${pr_number}")"
if ! jq -e \
--arg repo "${GITHUB_REPOSITORY}" '
.state == "open" and
.base.ref == "main" and
.head.repo.full_name == $repo and
.user.login == "github-actions[bot]"
' <<< "${pr}" > /dev/null; then
echo "::error::Invalid Release Please PR"
exit 1
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.
pr="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${RELEASE_PR}")"
if ! jq -e --arg sha "${APPROVED_SHA}" --arg repo "${GITHUB_REPOSITORY}" '
.state == "open" and
.base.ref == "main" and
.base.sha == $sha and
.head.repo.full_name == $repo and
.user.login == "github-actions[bot]"
' <<< "${pr}" > /dev/null; then
echo "::error::Release PR does not match the approved main commit"
gh pr update-branch "${pr_number}" --repo "${GITHUB_REPOSITORY}" --rebase
pr="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${pr_number}")"
main_sha="$(gh api "repos/${GITHUB_REPOSITORY}/commits/main" --jq '.sha')"
head_sha="$(jq -r '.head.sha' <<< "${pr}")"
if [[ "$(gh api "repos/${GITHUB_REPOSITORY}/compare/${main_sha}...${head_sha}" --jq '.behind_by')" -ne 0 ]]; then
echo "::error::Release PR is not up to date with main"
exit 1
fi
pr_head_sha="$(jq -r '.head.sha' <<< "${pr}")"
pr_version="$(gh api -H "Accept: application/vnd.github.raw+json" "repos/${GITHUB_REPOSITORY}/contents/package.json?ref=${pr_head_sha}" --jq '.version')"
if [[ "${pr_version}" != "${EXPECTED_VERSION}" ]]; then
echo "::error::Release PR version ${pr_version} does not match approved version ${EXPECTED_VERSION}"
version="$(gh api -H "Accept: application/vnd.github.raw+json" "repos/${GITHUB_REPOSITORY}/contents/package.json?ref=${head_sha}" --jq '.version')"
semver='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*)|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.((0|[1-9][0-9]*)|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$'
if [[ ! "${version}" =~ ${semver} ]]; then
echo "::error::Release Please produced an invalid version: ${version}"
exit 1
fi
- name: Merge approved release PR
echo "main_sha=${main_sha}" >> "${GITHUB_OUTPUT}"
echo "pr=${pr_number}" >> "${GITHUB_OUTPUT}"
echo "head_sha=${head_sha}" >> "${GITHUB_OUTPUT}"
echo "head_ref=$(jq -r '.head.ref' <<< "${pr}")" >> "${GITHUB_OUTPUT}"
echo "version=${version}" >> "${GITHUB_OUTPUT}"
- name: Run checks
env:
GH_TOKEN: ${{ github.token }}
HEAD_REF: ${{ steps.candidate.outputs.head_ref }}
run: |
dispatch="$(
gh api --method POST \
-H "X-GitHub-Api-Version: 2026-03-10" \
"repos/${GITHUB_REPOSITORY}/actions/workflows/ci.yml/dispatches" \
-f "ref=${HEAD_REF}" \
-F return_run_details=true
)"
gh run watch "$(jq -er '.workflow_run_id' <<< "${dispatch}")" \
--repo "${GITHUB_REPOSITORY}" --exit-status
- name: Merge release PR
env:
APPROVED_SHA: ${{ github.event.client_payload.sha }}
EXPECTED_HEAD_SHA: ${{ steps.candidate.outputs.head_sha }}
EXPECTED_MAIN_SHA: ${{ steps.candidate.outputs.main_sha }}
GH_TOKEN: ${{ github.token }}
RELEASE_PR: ${{ github.event.client_payload.pr }}
RELEASE_PR: ${{ steps.candidate.outputs.pr }}
run: |
current_sha="$(gh api "repos/${GITHUB_REPOSITORY}/commits/main" --jq '.sha')"
if [[ "${current_sha}" != "${APPROVED_SHA}" ]]; then
echo "::error::main changed after approval; approve the latest Release run"
if [[ "${current_sha}" != "${EXPECTED_MAIN_SHA}" ]]; then
echo "::error::main changed before merge; run Release again"
exit 1
fi
gh pr merge "${RELEASE_PR}" --repo "${GITHUB_REPOSITORY}" --merge
gh pr merge "${RELEASE_PR}" --repo "${GITHUB_REPOSITORY}" --merge \
--match-head-commit "${EXPECTED_HEAD_SHA}"
- uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5
id: finalize
with:
Expand All @@ -87,7 +121,7 @@ jobs:
- name: Verify released version
if: steps.finalize.outputs.release_created == 'true'
env:
EXPECTED_VERSION: ${{ github.event.client_payload.version }}
EXPECTED_VERSION: ${{ steps.candidate.outputs.version }}
RELEASED_VERSION: ${{ steps.finalize.outputs.version }}
run: |
if [[ "${RELEASED_VERSION}" != "${EXPECTED_VERSION}" ]]; then
Expand All @@ -96,7 +130,7 @@ jobs:
fi

publish:
name: publish v${{ github.event.client_payload.version }}
name: publish v${{ needs.release.outputs.version }}
needs: release
if: needs.release.outputs.release_created == 'true'
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@ Release Please determines the next version from Conventional Commits merged sinc

Use Conventional Commit titles for commits and pull requests. A squash-merged pull request uses its title as the commit title. Changes such as `docs:`, `test:`, `ci:`, and `chore:` do not create a release by themselves.

Do not edit package versions or `CHANGELOG.md` manually. After successful CI on `main`, CI prepares the version and changelog from the commit history, then starts one versioned Release workflow that waits for approval before releasing and publishing it.
Do not edit package versions or `CHANGELOG.md` manually. After successful CI on `main`, CI prepares the version and changelog in a release pull request. To publish it, manually run the Release workflow from `main`; it updates the release pull request to latest `main`, runs fresh checks, then merges, tags, and publishes the release.
Loading