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
29 changes: 27 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@ on:
release-notes:
description: "Release notes"
required: false
pull_request:
types: [closed]
# Releasing on push rather than on the pull_request closed event is deliberate. A pull request from a fork
# runs with a read-only GITHUB_TOKEN and no secrets even on merge, so it cannot create the release or reach
# the publishing credentials - which is how a merged, labeled fork contribution silently released nothing.
# A push to main always runs with a full-permission token, and the release action finds the merged pull
# request and its label from the commit.
push:
branches:
- main

# Creating the GitHub release needs a write-capable token.
permissions:
Expand All @@ -22,6 +28,7 @@ jobs:
outputs:
version: ${{ steps.release.outputs.version }}
publish: ${{ steps.release.outputs.should-publish }}
reason: ${{ steps.release.outputs.reason }}

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -103,3 +110,21 @@ jobs:
with:
version: ${{ needs.release.outputs.version }}
secrets: inherit

verify-published:
# A merge that publishes nothing is the failure mode that silently costs a release: the release job
# succeeds, every publish job is skipped for want of should-publish, and the whole run reports green. Fail
# instead, so a release that did not happen cannot be mistaken for one that did.
#
# Only for the reasons that mean something went wrong. Publishing nothing is correct and routine for the
# others - a commit pushed straight to main, a Dependabot merge, a re-run of a run that already released -
# and failing on those would make this job noise that everyone learns to ignore.
if: always() && needs.release.result == 'success' && contains(fromJSON('["no-label", "error"]'), needs.release.outputs.reason)
runs-on: ubuntu-latest
needs: [release]

steps:
- name: Report that nothing was published
run: |
echo "::error::Nothing was published and no release was cut (reason: ${{ needs.release.outputs.reason }}). For 'no-label', add exactly one of major, minor or patch to the merged pull request and re-run this workflow - see verify-semver-label, which is meant to catch this before the merge."
exit 1
51 changes: 51 additions & 0 deletions .github/workflows/verify-semver-label.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Verify Semver Label

# A merged pull request with no major/minor/patch label produces a Publish run that reports success while
# skipping every publish step, because the release action resolves should-publish to false. That reads as a
# release having happened when nothing was published. Requiring the label here turns a silent non-release into
# a visible failure before the merge, where it costs nothing to fix.
#
# It also closes a race the publish workflow cannot: a label added moments after the merge may land too late
# for the release to pick it up. Demanding the label before the merge means there is nothing to race.
#
# Triggered on labeled/unlabeled as well as the usual events, so adding the label re-runs the check rather than
# leaving a red cross behind that only a push would clear.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

on:
pull_request:
types: [opened, reopened, synchronize, labeled, unlabeled]
# Scoped to the same branch Publish releases from. A pull request stacked onto another one's branch cannot
# cut a release, so demanding a version label of it would be asking which version a merge that publishes
# nothing should carry.
branches:
- main

permissions:
contents: read

jobs:
verify:
runs-on: ubuntu-latest

steps:
- name: Require exactly one semantic version label
env:
LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }}
run: |
count=$(printf '%s' "$LABELS" | jq '[.[] | select(. == "major" or . == "minor" or . == "patch")] | length')

if [ "$count" -eq 1 ]; then
echo "Found one semantic version label."
exit 0
fi

if [ "$count" -eq 0 ]; then
echo "::error::This pull request has no semantic version label. Add exactly one of major, minor or patch. Without one, merging produces a Publish run that succeeds while skipping every publish step, so no release is cut and nothing is published."
else
echo "::error::This pull request carries $count semantic version labels. Exactly one of major, minor or patch is required, since the release version cannot be derived from more than one."
fi

exit 1
Loading