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
50 changes: 48 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: CI

# This repository had no checks of its own. Both of the workflows it publishes
# are `on: workflow_call`, and a reusable workflow never triggers on its own
# This repository had no checks of its own. Every workflow it publishes is
# `on: workflow_call`, and a reusable workflow never triggers on its own
# pull requests — so nothing validated this repo before a merge.
#
# That matters more here than in an ordinary repo. Consumers reference @master,
Expand Down Expand Up @@ -56,6 +56,52 @@ jobs:
- name: Check every inputs.* reference is declared
run: node scripts/check-input-refs.js

# The review assets are published files that nothing in this repo executes:
# claude-review.yml checks them out into the *caller's* workspace and
# runs them there. So a syntax error, or a rename that leaves the workflow
# pointing at a path that no longer exists, would first be seen by a consumer
# — as a review job that dies partway, which is exactly the failure the gate
# is supposed to be immune to.
review-assets:
name: Check review assets
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '24.13.1'

- name: Parse the review scripts
run: |
for f in review/*.mjs; do
node --check "$f"
echo "ok $f"
done

# The workflow compacts the schema with jq and passes the result to
# --json-schema. Invalid JSON there fails the review step with a quoting
# error rather than anything that names the schema.
- name: Compact the schema the way the workflow does
run: jq -c . review/schema.json > /dev/null

- name: Check every tooling path the workflow references exists
run: |
missing=0
while read -r ref; do
file="${ref#.claude-review/tooling/}"
if [ ! -f "$file" ]; then
echo "::error::claude-review.yml references $ref, but $file is not in this repo"
missing=1
else
echo "ok $file"
fi
done < <(grep -oE '\.claude-review/tooling/[A-Za-z0-9_./-]+' \
.github/workflows/claude-review.yml | sort -u)
exit "$missing"

# Smoke test: this repo calls its own reusable workflow, so a change to
# check-member.yml is executed before it can be merged rather than after.
#
Expand Down
329 changes: 329 additions & 0 deletions .github/workflows/claude-review.yml

Large diffs are not rendered by default.

47 changes: 47 additions & 0 deletions .github/workflows/pr-ticket.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: PR ticket

# This repository publishes `check-ticket.yml`; this is the file that runs it
# here. Two things come out of that:
#
# - Pull requests to this repo carry a ticket, the same as the consumers' do.
# - `check-ticket.yml` is executed before a change to it can merge. `uses: ./`
# resolves against this repository, so a PR that breaks the gate fails its
# own check rather than surfacing in three consumers' next PR. That is the
# same reasoning as ci.yml's `self-test` job, which calls check-member.yml
# the same way.
#
# Separate from ci.yml because of the trigger. This needs `edited`: retitling
# the PR is how a red check here gets fixed, and without it the corrected title
# never re-runs, leaving the check red with nothing to click. ci.yml must not
# take `edited` in exchange — it would rebuild actionlint, input-refs,
# review-assets and the self-test every time someone edits a description.
#
# `synchronize` is here so the check reports against each new head SHA. Branch
# protection waits on a check that never ran for the commit it is looking at,
# and a title validated two pushes ago has not been validated for this one.
#
# NOTE if this is ever made a required status check: a PR opened with the
# default GITHUB_TOKEN fires no `pull_request` event, so this never runs and
# never reports — that PR is then unmergeable with no way to unblock it from
# the PR side. Automation opening PRs here must use a PAT or an app token.

on:
pull_request:
types: [opened, edited, reopened, synchronize]

permissions:
contents: read

# No `concurrency` here: check-ticket.yml declares its own group, keyed on the
# PR number, and a called workflow's concurrency applies. Adding a second group
# at this level would only cancel the caller around it.

jobs:
check-ticket:
# Local `./` on purpose — see above. Consumers call this same workflow as
# iXsystems/ux-github-workflows/.github/workflows/check-ticket.yml@master.
#
# `ticket-prefixes` is left at its default of NAS, which is what this repo
# files under. Pass `with: ticket-prefixes: NAS,TNC` if that stops being
# true.
uses: ./.github/workflows/check-ticket.yml
75 changes: 75 additions & 0 deletions .github/workflows/pr-title.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: PR Title (shared)

# Requires a Conventional Commits pull request title, optionally prefixed with
# "<anything> / " segments — e.g. "NAS-141240 / 27.0.0-BETA.1 / feat(x): y" or
# plain "fix: y".
#
# This matters only where a squash merge feeds the PR title to
# semantic-release as the commit subject, which is currently
# iXsystems/truenas-ui-components and truenas/api-client-ts. Both had their own
# copy; they had already drifted apart in the one place it counts (see below).
#
# **The caller's `.releaserc.json` has to agree with this pattern.** A title
# this gate accepts must parse, over there, to the type this gate thinks it
# saw. If it does not, the PR merges green and publishes nothing — the failure
# is a release that did not happen, which nobody is watching for. Callers must
# keep `parserOpts.headerPattern` and `breakingHeaderPattern` equal to:
#
# ^(?:[^:]+ / )?(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\(([^)]+)\))?!?: (.+)$
#
# and the breaking one with `!:` in place of `!?:`.
#
# Usage:
# on:
# pull_request_target:
# types: [opened, edited, synchronize]
#
# jobs:
# pr-title:
# permissions:
# pull-requests: read
# uses: iXsystems/ux-github-workflows/.github/workflows/pr-title.yml@master

on:
workflow_call:

permissions:
contents: read

concurrency:
group: pr-title-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
validate:
# API. A reusable call reports as "<caller job id> / <this name>", so consumers
# match this string in branch protection. Renaming it stops their required check
# reporting, silently, with no PR in their repo to explain it.
name: Validate Conventional Commit Title
runs-on: ubuntu-latest
steps:
- name: Check PR title
env:
# Passed via env, not interpolated into the script — a PR title is
# attacker-controlled text, and `${{ }}` in a `run:` block splices it
# into the shell source.
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
# The optional prefix is `[^:]+` rather than `.+` on purpose. A greedy
# `.+ / ` swallows the real type: in "fix: adjust a / b: c" it matches
# "fix: adjust a / " and leaves "b" as the type, so the gate passes a
# title that semantic-release then reads as a type with no release
# rule. Excluding colons from the prefix means the prefix cannot eat
# a `type:` that came before it.
#
# bash uses POSIX ERE, which has no (?:...) — the only difference from
# the .releaserc.json pattern is that these groups capture.
pattern='^([^:]+ / )?(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([^)]+\))?!?: .+$'
if [[ "$PR_TITLE" =~ $pattern ]]; then
echo "PR title OK: $PR_TITLE"
else
echo "::error::PR title must follow Conventional Commits, optionally prefixed with '<ticket> / <version> / '."
echo "::error::Examples: 'fix(auth): handle expired token' or 'NAS-141240 / 27.0.0-BETA.1 / feat(form-field): add tooltip'."
echo "::error::Got: $PR_TITLE"
exit 1
fi
Loading
Loading