fix: let a colour through the CSS value guard #48
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" |