-
Notifications
You must be signed in to change notification settings - Fork 3
122 lines (107 loc) · 5.09 KB
/
Copy pathrelease_main.yaml
File metadata and controls
122 lines (107 loc) · 5.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
name: TinyNode Release on push to main
# Cuts a GitHub Release (and its tag) every time main advances. TinyNode has no
# development branch, so any push to main — a merged PR or a direct commit — is a
# promotion to production and gets a release.
#
# The version is DERIVED, not stored: this reads the highest existing v* release
# tag and bumps it. There is no version-bump commit pushed to any branch, so no
# bot needs write access to a protected branch.
#
# - First release ever (no v* tag yet): seed from package.json. With
# package.json at 1.0.0 this publishes v1.0.0.
# - Every release after that: bump the latest tag. Default is a patch bump
# (v1.0.0 -> v1.0.1). The push/merge commit can opt into a larger bump via
# trigger words in its message:
# "BREAKING CHANGE" / "type!:" / "[major]" -> major (v1.0.0 -> v2.0.0)
# "feat:" / "feat(scope):" / "[minor]" -> minor (v1.0.0 -> v1.1.0)
# anything else (default) -> patch (v1.0.0 -> v1.0.1)
#
# package.json's version is no longer the source of truth after the first
# release — the release tags are. It is kept only to seed that first release.
#
# This is independent of cd_prod.yaml (test + deploy) and tests.yaml; it does not
# gate on tests or the deploy. Idempotent: if the current commit already carries a
# v* tag, or the computed tag already exists, the run is a no-op. Safe to re-run
# via workflow_dispatch.
on:
push:
branches: main
workflow_dispatch:
permissions:
contents: write
# Serialize releases so two pushes landing close together can't both read the same
# latest tag and race to create it. cancel-in-progress: false queues the later run
# (every push is a distinct release to cut) rather than cancelling the in-flight one.
concurrency:
group: release-main
cancel-in-progress: false
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout (full history + tags)
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Derive next version and create the release
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Untrusted input — read from env, never inline into the script.
HEAD_COMMIT_MSG: ${{ github.event.head_commit.message }}
run: |
set -e
# Never release the same commit twice (idempotent re-runs / manual dispatch).
EXISTING_TAG="$(git tag -l 'v*' --points-at HEAD | head -n1)"
if [ -n "$EXISTING_TAG" ]; then
echo "Commit already released as ${EXISTING_TAG} — nothing to do."
exit 0
fi
# Resolve the bump level. Conventional Commits puts the <type>/<type>!: prefix on
# the SUBJECT (first line); BREAKING CHANGE is an uppercase footer on its own line.
# We match accordingly so prose can't force a bump — e.g. "docs: mention a breaking
# change", or a squash-merge body that concatenates every PR bullet, no longer trips
# a major. The [major]/[minor] tags remain deliberate escape hatches and may appear
# anywhere in the message. Default is patch.
SUBJECT="$(printf '%s' "$HEAD_COMMIT_MSG" | head -n1)"
LEVEL="patch"
if printf '%s' "$SUBJECT" | grep -qE '^[a-z]+(\([^)]*\))?!:' \
|| printf '%s' "$HEAD_COMMIT_MSG" | grep -qE '^BREAKING[ -]CHANGE:' \
|| printf '%s' "$HEAD_COMMIT_MSG" | grep -qiE '\[major\]'; then
LEVEL="major"
elif printf '%s' "$SUBJECT" | grep -qE '^feat(\([^)]*\))?:' \
|| printf '%s' "$HEAD_COMMIT_MSG" | grep -qiE '\[minor\]'; then
LEVEL="minor"
fi
echo "Bump level resolved from commit message: ${LEVEL}"
# Find the highest existing release tag (version-aware sort).
LATEST_TAG="$(git tag -l 'v*' --sort=-v:refname | head -n1)"
if [ -z "$LATEST_TAG" ]; then
# First release: seed from package.json (1.0.0 -> v1.0.0).
NEXT="$(node -p "require('./package.json').version")"
echo "No prior release tag — seeding first release from package.json: ${NEXT}"
else
# Derive the next version by bumping the latest tag with npm's semver engine.
BASE="${LATEST_TAG#v}"
TMP="$(mktemp -d)"
printf '{"name":"x","version":"%s"}\n' "$BASE" > "$TMP/package.json"
( cd "$TMP" && npm version "$LEVEL" --no-git-tag-version >/dev/null )
NEXT="$(node -p "require('${TMP}/package.json').version")"
rm -rf "$TMP"
echo "Latest release ${LATEST_TAG} -> next version ${NEXT}"
fi
TAG="v${NEXT}"
if gh release view "$TAG" >/dev/null 2>&1; then
echo "Release $TAG already exists — nothing to do."
exit 0
fi
gh release create "$TAG" \
--target "$GITHUB_SHA" \
--title "$TAG" \
--generate-notes \
--latest
echo "Published release $TAG"