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
27 changes: 27 additions & 0 deletions .github/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Categorization for the auto-generated release notes — both the `--generate-notes` call in the
# GitHub Release job of workflows/publish.yml and the "Generate release notes" button.
#
# Categories match on labels, which workflows/pr-label.yml derives from the conventional-commit
# type in the pull request title. Order matters: the first matching category wins, so a dependency
# bump that also carries a hand-applied `bug` label is listed above Bug Fixes on purpose.
changelog:
categories:
- title: ⚠️ Breaking Changes
labels:
- breaking changes
- title: 📦 Dependencies
labels:
- dependencies
- title: 🚀 Features
labels:
- enhancement
- title: 🐛 Bug Fixes
labels:
- bug
- title: 📖 Documentation
labels:
- documentation
# chore, build, refactor, test — everything the labeler does not classify.
- title: 🧰 Other Changes
labels:
- '*'
31 changes: 0 additions & 31 deletions .github/workflows/actions/generate-changelog/action.yml

This file was deleted.

92 changes: 92 additions & 0 deletions .github/workflows/pr-label.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
name: Label PR

# pull_request_target so pull requests opened from a fork are labeled too: the pull_request token
# is read-only for them. Nothing is checked out and no pull-request-controlled code runs here —
# only the title and body are read, and they reach the script through the environment.
on:
pull_request_target:
types:
- opened
- edited
- reopened

permissions:
contents: read

jobs:
label:
name: Label
runs-on: ubuntu-latest
# Dependabot applies `dependencies` to its own pull requests.
# `edited` fires for body/base edits too, not just the title. `changes.title` only exists (and
# is truthy) when the title itself changed; it's simply absent — not an error — on `opened` and
# `reopened`, which have no `changes` object at all, so checking `action != 'edited'` first
# covers those without needing to read it.
if: >-
${{ github.repository_owner == 'koobiq' && github.actor != 'dependabot[bot]'
&& (github.event.action != 'edited' || github.event.changes.title) }}
permissions:
pull-requests: write # to add labels to the pull request
steps:
- name: Apply the labels matching the conventional-commit type
env:
GH_TOKEN: ${{ github.token }}
# Nothing is checked out here, and gh does not fall back to GITHUB_REPOSITORY.
GH_REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
# Already shaped by commitlint.yml, but still untrusted text: keep it out of the script.
PR_TITLE: ${{ github.event.pull_request.title }}
# Free-form text with no validation at all (commitlint only checks PR_TITLE) — kept out
# of the script the same way.
PR_BODY: ${{ github.event.pull_request.body }}
run: |
set -euo pipefail

# These feed the categories in .github/release.yml. The scope is what separates a
# dependency bump from an ordinary chore.
labels=()

case "$PR_TITLE" in
feat*) labels+=(enhancement) ;;
fix*) labels+=(bug) ;;
docs*) labels+=(documentation) ;;
esac

# A separate, independent check: `case` only fires its first match, and a dependency
# bump commonly carries a type prefix too (e.g. `fix(deps): bump x`). release.yml lists
# Dependencies above Bug Fixes specifically to handle a PR that carries both labels.
case "$PR_TITLE" in
*'(deps)'*|*'(deps-dev)'*) labels+=(dependencies) ;;
esac

# `feat(scope)!:` — the conventional-commit marker for a breaking change.
case "$PR_TITLE" in
*'!:'*) labels+=('breaking changes') ;;
esac

# The footer form, per the conventional-commits spec — checked separately since it lives
# in the description, not the title.
case "$PR_BODY" in
*'BREAKING CHANGE:'*|*'BREAKING-CHANGE:'*)
# Title and body can both flag it — guard against adding the label twice.
case "${labels[*]-}" in
*'breaking changes'*) ;;
*) labels+=('breaking changes') ;;
esac
;;
esac

if [ ${#labels[@]} -eq 0 ]; then
echo "No label maps to \"$PR_TITLE\"."
exit 0
fi

# Re-adding a label the pull request already carries is a no-op, so the `edited` and
# `reopened` re-runs are free. Labels are only ever added: a title corrected from `feat:`
# to `fix:` keeps the stale `enhancement` until someone removes it by hand.
add_label_flags=()
for label in "${labels[@]}"; do
add_label_flags+=(--add-label "$label")
done

gh pr edit "$PR_NUMBER" "${add_label_flags[@]}"
106 changes: 105 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ on:
- '*.*.*'
# - '[0-9]+.[0-9]+.[0-9]+'
# - '*@*.*.*' for NX integration
# Entry point for a tag npm already has: a backfill for tags released before the release job
# existed, a retry after a failed run, and the way an 18.x tag gets its release, since that
# branch keeps its own copy of this file.
workflow_dispatch:
inputs:
tag:
description: Existing tag to create the GitHub release for, for example 19.8.4
required: true

# Read-only default; the write scopes this workflow needs are granted on the job itself, so a
# future job added here does not silently inherit them.
Expand All @@ -19,7 +27,8 @@ jobs:
# `check-npm-resolution` gates this job on the npm registry answering; a stall must not hold the
# release runner for the six-hour default.
timeout-minutes: 60
if: ${{ github.repository_owner == 'koobiq' }}
# A manual run only creates the missing GitHub release; republishing to npm is never a button.
if: ${{ github.repository_owner == 'koobiq' && github.event_name == 'push' }}
permissions:
contents: read # for actions/checkout to read the repository
packages: write
Expand Down Expand Up @@ -49,3 +58,98 @@ jobs:
# send NPM_TOKEN_KOOBIQ over an unverified connection. This flag reaches only the
# notification request in packages/cli/src/release/notify-release.ts.
MATTERMOST_ALLOW_UNTRUSTED_TLS: 'true'

github-release:
name: GitHub Release
runs-on: ubuntu-latest
needs: publish
# Two near-simultaneous tag pushes (e.g. 20.5.0 and 19.9.0) would otherwise both read the same
# "current latest release" (below) before either finishes creating its own, and the Latest
# badge could land on whichever finishes last — even the older release train. One queued run
# at a time closes that window. The group is a fixed string, never templated with the ref/tag:
# the whole point is serializing across DIFFERENT tags, not the same one.
concurrency:
group: github-release-latest-badge
cancel-in-progress: false # queue, never cancel a release that's already being created
# `!cancelled() && !failure()` lets the manual path run while `publish` is skipped, but not if
# `publish` failed or the run was cancelled. Kept a separate job so the write scope below never
# shares a process with NPM_PUBLISH_TOKEN, and nothing is checked out here, so no repository
# code runs with it either.
if: ${{ !cancelled() && !failure() && github.repository_owner == 'koobiq' }}
Comment thread
lskramarov marked this conversation as resolved.
permissions:
contents: write # for `gh release create` to publish the release
steps:
- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
# Nothing is checked out here, and gh does not fall back to GITHUB_REPOSITORY.
GH_REPO: ${{ github.repository }}
# Read through the environment, never interpolated into the script: on a manual run this
# is free text, and a git tag may legally carry shell metacharacters.
TAG: ${{ inputs.tag || github.ref_name }}
run: |
set -euo pipefail

# The push filter is looser than the grammar parse-version.ts accepts, and the dispatch
# input is not validated at all.
if ! printf '%s' "$TAG" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta|rc)\.[0-9]+)?$'; then
echo "::error::\"$TAG\" is not a release tag."
exit 1
fi

# A re-run, or a dispatch for a tag already handled, must neither fail nor duplicate.
# `gh release view` prints exactly "release not found" (exit 1) when the tag has no
# release yet — verified against this repo with `gh` 2.97.0. Any other failure (auth,
# rate limit, a transient GitHub outage) also exits 1 but with different text, and must
# not be silently treated as "go ahead and create it".
if release_error=$(gh release view "$TAG" 2>&1 1>/dev/null); then
echo "Release $TAG already exists — nothing to do."
exit 0
elif [ "$release_error" != "release not found" ]; then
echo "::error::gh release view \"$TAG\" failed: $release_error"
exit 1
fi

# `make_latest` defaults to true on the REST API and gh sends nothing when --latest is
# absent, so left implicit a 19.x patch tagged after 20.x would take the badge off 20.x.
# Decide it here: the badge moves only when this tag outranks the release holding it.
prerelease=()
latest=false

case "$TAG" in
*-*)
prerelease=(--prerelease) # a prerelease can never be latest
;;
*)
# Same distinction as above, and the same message: a repository with no releases at
# all answers "release not found" too — verified against github/gitignore with `gh`
# 2.97.0 — and there the first release does take the badge. Any other failure would
# otherwise read as "nothing holds the badge" and move it here, so it fails instead.
# The two streams stay separate: stdout carries the tag name, stderr the message.
if ! current=$(gh release view --json tagName --jq .tagName 2> "$RUNNER_TEMP/latest-release.err"); then
latest_error=$(cat "$RUNNER_TEMP/latest-release.err")

if [ "$latest_error" != "release not found" ]; then
echo "::error::gh release view --json tagName failed: $latest_error"
exit 1
fi

current=''
fi

if [ -z "$current" ] || [ "$(printf '%s\n%s\n' "$TAG" "$current" | sort -V | tail -1)" = "$TAG" ]; then
latest=true
fi
;;
esac

# --verify-tag: without it a typo in the dispatch input makes gh create a brand new tag on
# the default branch. --generate-notes reproduces the body every release has carried so
# far; GitHub resolves the previous tag from the tag's own ancestry, so the range stays
# right across the parallel 19.x / 20.x trains and for a late backfill.
gh release create "$TAG" \
--verify-tag \
--generate-notes \
--title "$TAG" \
--latest="$latest" \
"${prerelease[@]}"
Loading