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
122 changes: 122 additions & 0 deletions .github/workflows/release_main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
name: Release on push to main

# Cuts a GitHub Release (and its tag) every time main advances. RERUM 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.1.0 this publishes v1.1.0 (matching RERUM_API_VERSION).
# - Every release after that: bump the latest tag. Default is a patch bump
# (v1.1.0 -> v1.1.1). The push/merge commit can opt into a larger bump via
# trigger words in its message:
# "BREAKING CHANGE" / "type!:" / "[major]" -> major (v1.1.0 -> v2.0.0)
# "feat:" / "feat(scope):" / "[minor]" -> minor (v1.1.0 -> v1.2.0)
# anything else (default) -> patch (v1.1.0 -> v1.1.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); 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.1.0 -> v1.1.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"
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "rerum_server_nodejs",
"type": "module",
"version": "0.0.0",
"version": "1.1.0",
"private": true,
"description": "Rerum API server for database access.",
"keywords": [
Expand Down
Loading