Skip to content

Commit 5b2bb58

Browse files
authored
Merge pull request #125 from CenterForDigitalHumanities/versioned-releases
Introduce release tagging
2 parents 1b9ce2b + cdc1689 commit 5b2bb58

2 files changed

Lines changed: 123 additions & 1 deletion

File tree

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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "tinynode",
33
"type": "module",
4-
"version": "0.1.1",
4+
"version": "1.0.0",
55
"private": true,
66
"keywords": [
77
"rerum",

0 commit comments

Comments
 (0)