Skip to content

fix: let a colour through the CSS value guard #48

fix: let a colour through the CSS value guard

fix: let a colour through the CSS value guard #48

Workflow file for this run

name: Auto Release & Satis Rebuild
on:
push:
branches: [master]
permissions:
contents: write
jobs:
tests:
uses: ./.github/workflows/tests.yml
release:
needs: tests
runs-on: ubuntu-latest
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Ensure full tag history is available
# Belt and braces on top of fetch-depth: 0. A shallow or stale view of
# tags here would make every step below compute a wrong bump silently
# (under- or over-releasing) - too costly to leave to one safeguard.
run: git fetch --tags --force
- name: Get latest tag
# Single source of truth for "the last tag", reused by both the bump-range
# scan below and the version arithmetic. Previously this used
# `git tag --sort=-v:refname | head -1` (the highest semver string ANYWHERE
# in the tag namespace) while the bump step used `git describe` (the
# nearest ANCESTOR tag) - two different algorithms that silently diverge
# the moment a stray tag exists that isn't reachable from HEAD (this repo
# has had two such stray tags today). A version computed from one history
# and a bump computed from another is a release disconnected from reality.
# `tag` is empty when no ancestor tag exists at all (fresh repo); `base`
# is the same value with the documented v0.0.0 starting point as fallback,
# for the arithmetic below (which needs actual numbers to increment).
id: latest_tag
run: |
TAG=$(git describe --tags --abbrev=0 2>/dev/null || true)
if [ -z "$TAG" ]; then
echo "tag=" >> "$GITHUB_OUTPUT"
echo "base=v0.0.0" >> "$GITHUB_OUTPUT"
echo "No ancestor tag found on this branch"
else
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "base=$TAG" >> "$GITHUB_OUTPUT"
echo "Latest ancestor tag: $TAG"
fi
- name: Check if HEAD is already tagged
id: head_tag
run: |
TAG=$(git tag --points-at HEAD | sort -V | tail -1)
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
if [ -n "$TAG" ]; then
echo "HEAD is already tagged ($TAG) — a release for this commit already happened"
else
echo "HEAD is not tagged yet"
fi
- name: Determine version bump from commit message
# Scans every commit since the last ancestor tag, not just the tip, so a
# fix:/feat: whose release gets blocked by the gate can't be buried by a
# later docs:/ci:/chore: commit becoming the new tip. Bump = max severity
# found in the range (major > minor > patch > none).
#
# Subject and body are read and matched SEPARATELY, on purpose, after two
# incidents from blurring them: the type prefix and the !-bang form are
# Conventional Commits subject-line syntax and are matched only against
# the first physical line; BREAKING CHANGE:/BREAKING-CHANGE: is a footer
# and is matched only against everything after that line, anchored at
# line start with the colon required. No line in a commit body can
# trigger a bump except a genuine footer - a commit merely documenting
# the convention (which these commits do, routinely) must never be read
# as declaring one.
#
# %s/%b are NOT used for the split: git treats %s as the first
# PARAGRAPH, so a footer with no blank line before it (a malformed but
# real-world commit shape) folds into %s and %b comes back empty - the
# opposite failure direction from the first two incidents (a genuine
# breaking change silently downgraded to patch instead of a false
# major). Reading the raw message (%B) and splitting on the first
# physical line ourselves gives a true first-line subject regardless of
# blank lines.
#
# Two further edge cases found by review: a leading blank physical line
# (reachable via --cleanup=verbatim, commit-tree, or the GitHub web UI/
# Action commits) would make `head -n1` grab the blank line as the
# "subject" and hide a real feat:/fix: in the body, where the prefix
# regex never looks - so the subject is the first NON-empty physical
# line, not simply the first one. And leading whitespace on that line
# (git's default --cleanup=strip only trims trailing whitespace) would
# otherwise dodge every subject anchor - so the extracted subject is
# trimmed of leading whitespace before matching. The body is
# deliberately NOT trimmed: a BREAKING CHANGE: footer must sit at column
# zero per the spec, and an indented one must keep failing to match.
#
# This step reuses the single ancestor tag resolved in "Get latest tag"
# (steps.latest_tag.outputs.tag) instead of calling `git describe` again,
# so the range scanned here and the version incremented below can never
# disagree about what "the last tag" is.
#
# CRITICAL, cost a release on this exact repo: `--pretty=format:%H` does
# NOT terminate the final entry with a newline, and a `while read` loop
# does not run its body for a trailing line with no terminator - so the
# LAST commit of every range was silently dropped. On a multi-commit
# range some other commit usually still carried the max severity, which
# is why this stayed hidden; on a single-commit range (one fix:/feat:
# pushed alone) it always produced none. Using `tformat:%H` instead
# (which does terminate every entry, including the last) fixes the root
# cause; `read -r HASH || [ -n "$HASH" ]` is kept too, belt-and-braces,
# so a trailing unterminated line would still be processed even if some
# future edit reintroduces a non-terminating format.
id: bump
run: |
TAG="${{ steps.latest_tag.outputs.tag }}"
if [ -n "$TAG" ]; then
RANGE="$TAG..HEAD"
echo "Scanning commits since $TAG ($RANGE)"
else
RANGE="HEAD"
echo "No ancestor tag found; scanning full history"
fi
SEVERITY=0
while IFS= read -r HASH || [ -n "$HASH" ]; do
FULL=$(git log -1 --pretty=format:%B "$HASH")
SUBJECT_LINE=$(printf '%s\n' "$FULL" | awk '/[^[:space:]]/{print NR; exit}')
if [ -z "$SUBJECT_LINE" ]; then
SUBJECT_LINE=1
fi
SUBJECT=$(printf '%s\n' "$FULL" | sed -n "${SUBJECT_LINE}p" | sed 's/^[[:space:]]*//')
BODY=$(printf '%s\n' "$FULL" | tail -n +$((SUBJECT_LINE + 1)))
if echo "$SUBJECT" | grep -qE '^(feat|fix|refactor|perf|style|docs|test|chore|ci|build)(\(.+\))?!:' || echo "$BODY" | grep -qE '^BREAKING[ -]CHANGE:'; then
[ "$SEVERITY" -lt 3 ] && SEVERITY=3
elif echo "$SUBJECT" | grep -qE '^feat(\(.+\))?:'; then
[ "$SEVERITY" -lt 2 ] && SEVERITY=2
elif echo "$SUBJECT" | grep -qE '^(fix|perf|refactor)(\(.+\))?:'; then
[ "$SEVERITY" -lt 1 ] && SEVERITY=1
fi
done < <(git log $RANGE --pretty=tformat:%H 2>/dev/null)
case "$SEVERITY" in
3) echo "type=major" >> "$GITHUB_OUTPUT" ;;
2) echo "type=minor" >> "$GITHUB_OUTPUT" ;;
1) echo "type=patch" >> "$GITHUB_OUTPUT" ;;
# docs/test/chore/ci/build/style-only range, no shipped code -> no release
*) echo "type=none" >> "$GITHUB_OUTPUT" ;;
esac
- name: Calculate new version
id: version
# Tagging must be idempotent: only compute/create a tag when this commit
# doesn't have one yet. Re-runs and double-fires must never mint a second tag.
if: steps.bump.outputs.type != 'none' && steps.head_tag.outputs.tag == ''
run: |
TAG="${{ steps.latest_tag.outputs.base }}"
VERSION="${TAG#v}"
IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
BUMP="${{ steps.bump.outputs.type }}"
case "$BUMP" in
major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;;
minor) MINOR=$((MINOR + 1)); PATCH=0 ;;
patch) PATCH=$((PATCH + 1)) ;;
esac
NEW_TAG="v${MAJOR}.${MINOR}.${PATCH}"
echo "new_tag=$NEW_TAG" >> "$GITHUB_OUTPUT"
echo "New version: $NEW_TAG ($BUMP bump)"
- name: Create and push tag
if: steps.bump.outputs.type != 'none' && steps.head_tag.outputs.tag == ''
run: |
git tag ${{ steps.version.outputs.new_tag }}
git push origin ${{ steps.version.outputs.new_tag }}
- name: Trigger Satis rebuild
# Publishing must be retryable, unlike tagging: this runs whenever the
# commit is release-worthy, whether the tag was just created above or
# already existed from a prior run whose Satis trigger failed. Firing
# this twice is harmless; skipping it once leaves a tag with no build.
if: steps.bump.outputs.type != 'none'
run: |
TAG="${{ steps.head_tag.outputs.tag }}"
if [ -z "$TAG" ]; then
TAG="${{ steps.version.outputs.new_tag }}"
fi
curl -s -X POST https://packages.elju.it/_webhook \
-H "X-Satis-Api-Key: ${{ secrets.SATIS_API_KEY }}" \
-H "Content-Type: application/json" \
-d "{\"package\": \"${{ github.repository }}\", \"version\": \"$TAG\"}"
echo "Satis rebuild triggered for $TAG"