Skip to content

Latest commit

 

History

43 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ux-github-workflows

Shared GitHub Actions workflows for UX team repositories.

Any repo the UX team owns can call these — there is nothing framework-specific here. Current consumers:

Repo Org
truenas/webui truenas
iXsystems/truenas-ui-components iXsystems
truenas-connect/ui truenas-connect

This repository must stay public

Consumers live in different GitHub organisations, and no single enterprise account spans them. Reusable workflows in a private repo cannot be called across orgs at all; internal requires a shared enterprise. Public is the only option that works everywhere, and public reusable workflows can be called from private repos, so private consumers such as truenas-connect/ui are covered.

Consequence: nothing secret goes in this repo. Secrets stay in each consumer repo and are passed in by name at the call site.

Workflows

check-ticket.yml

Fails a PR whose title does not reference a Jira ticket.

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

jobs:
  check-ticket:
    uses: iXsystems/ux-github-workflows/.github/workflows/check-ticket.yml@master
    with:
      ticket-prefixes: TNC   # optional; defaults to NAS
Input Default Notes
ticket-prefixes NAS Comma-separated Jira project keys. NAS,TNC accepts either.

The match is case-sensitive: bugclerk only links the Jira ticket for an uppercase key, so nas-12345 fails with a message saying so.

Callers own their on: trigger — a reusable workflow has no say in what triggers its caller.

This one is policy, not just plumbing — it makes a ticket mandatory. All four consumers have agreed to that, but a fifth repo should adopt it only once its team has. truenas/api-client-ts adopted it with ticket-prefixes: TNC knowing what it costs: only 9 of its previous 30 merged PRs carried a ticket, so this is a change in how that repo works, not a formalisation of what it already did.

Two repos require a ticket and a Conventional Commits title. That second check is pr-title.yml, below — it is a separate concern (semantic-release reads the title) and a separate workflow.

pr-title.yml

Requires a Conventional Commits PR title, optionally prefixed with <anything> / segments — NAS-141240 / 27.0.0-BETA.1 / feat(x): y and plain fix: y both pass. No inputs.

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

This is only worth running where a squash merge feeds the PR title to semantic-release as the commit subject — iXsystems/truenas-ui-components and truenas/api-client-ts today. It is a release gate wearing a style gate's clothes.

The caller's .releaserc.json has to agree with the pattern, or a title this accepts parses over there as a different type and the merge publishes nothing. That failure is a release that did not happen, which nobody notices. Keep parserOpts.headerPattern equal to:

^(?:[^:]+ / )?(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\(([^)]+)\))?!?: (.+)$

and breakingHeaderPattern to the same with !: for !?:.

The optional prefix is [^:]+, not .+, and that is the substantive difference between the two copies this replaced. A greedy .+ / swallows the real type: in fix: adjust a / b: c it matches fix: adjust a / and leaves b. truenas-ui-components had the greedy one in both its gate and its .releaserc.json; adopting this converged it on the strict pattern. Checked against the last 40 merged titles in both repos, nothing changes but that case.

check-member.yml

Reports whether the PR author has write access to the calling repo, as an is_member output:

jobs:
  check-member:
    if: github.event_name == 'pull_request'
    permissions:
      contents: read
    uses: iXsystems/ux-github-workflows/.github/workflows/check-member.yml@master

  test-ux-team:
    needs: [check-member]
    if: needs.check-member.outputs.is_member == 'true'
    runs-on: self-hosted
    # ...
Output Notes
is_member 'true' / 'false' — a string, not a boolean. Compare with == 'true'

main.yml in truenas/webui and truenas-connect/ui calls it to route tests to the self-hosted runner. Those were three separate copies of the same script before this existed — two workflow files plus one inlined directly in truenas-connect/ui's main.yaml.

Only meaningful on pull_request events: it reads context.payload.pull_request, and reports 'false' on any event that has no PR payload rather than failing. Guarding the job with if: github.event_name == 'pull_request' is still worth doing to skip a pointless runner — but then the downstream job needs always() (or !cancelled()) plus an explicit != 'true', so the skip does not cascade into it. See truenas/webui's main.yml for the worked example.

If the permission lookup fails it falls back to author_association, which is deliberately permissive. It decides where tests run; it must not be load-bearing for anything that gates a merge.

claude-review.yml

Automatic PR review, and the only one published here. It replaces the inline claude.yml each consumer grew its own copy of:

inline claude.yml (what consumers had) claude-review.yml
Output one sticky comment inline comments + one edited-in-place summary
Result advisory; the job passes either way fails at MEDIUM and above
Severities whatever the prompt asks for fixed enum, enforced by a JSON schema
Knows what it said last round no yes — prior threads and their resolved state
Marks its own comment stale no yes, while a new review is in flight
Submits a PR review no yes — request changes, needs-a-human comment, or approve
Posts as github-actions[bot] github-actions[bot], or a token the caller passes
Mode tag mode (track_progress) agent mode
Action version drifted to three different pins one, bumped here for everyone

There is deliberately one of these, not a choice of two. If it turns out to be wrong, change it here and every caller moves together; reverting is what git history is for, not a second workflow kept alive in case.

The call shape:

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  claude-review:
    uses: iXsystems/ux-github-workflows/.github/workflows/claude-review.yml@master
    permissions:
      contents: read
      issues: write
      pull-requests: write
    secrets:
      # Subscription auth, preferred when set. Map both: see below.
      claude-code-oauth-token: ${{ secrets.UX_CLAUDE_CODE_OAUTH_TOKEN }}
      anthropic-api-key: ${{ secrets.CLAUDE_API_KEY }}
      # Optional: post as a machine account instead of github-actions[bot].
      github-token: ${{ secrets.UX_REVIEW_BOT_TOKEN }}

No id-token: write: nothing here mints an OIDC token, because the workflow passes github_token explicitly and that skips the exchange the OIDC token was for — see below. The review job's own permissions: block does not ask for it either, so granting it in a caller has no effect on the token the job runs with.

Input Default Notes
model claude-opus-5 Passed through claude_args
prompt-file .claude/review-prompt.md The repo's own guidelines
require-write-access true Calls check-member.yml. Keep it on — it is what stops a drive-by PR spending tokens
skip-label skip-claude Skips the review and the gate. Does not clear a changes-requested review an earlier run posted; a person dismisses that in the PR
timeout-minutes 20
fetch-depth 10 Must cover the PR range
extra-allowed-tools '' Comma-separated permission rules appended to the reviewer's --allowedTools, e.g. Bash(go vet:*). Empty by default on purpose: anything that executes repo code runs PR-controlled code next to the job's write token, so each repo opts in as its own recorded decision
approve-when-clean true Submit an APPROVE review when nothing blocks and no human review is needed. Set false to get a COMMENT saying it would have approved instead, to watch the calls before they count
human-review-paths '' Newline-separated globs, gitignore rules: *, **, ?; a name with no slash (trailing one aside) matches at any depth, one with a slash is root-anchored, a directory match covers everything beneath it; no !, leading /, brackets or braces; # lines ignored. A PR touching a match always gets the needs-a-human COMMENT, whatever the reviewer decided
tooling-ref master Ref this repo's review/ assets come from; see below

The secret is named, not inherited, because the repos call it different things (CLAUDE_API_KEY vs CLAUDE_TOKEN). Auth is one of two secrets: an Anthropic API key, or a Claude Code OAuth token (from claude setup-token) for subscription billing. Map both. The token wins whenever it is non-empty, and the API key is blanked in that case rather than letting the CLI pick — the two bill different accounts, so an undocumented winner is not a choice the workflow will make silently. A job with neither fails before the checkout.

Mapping both is the point, not a fallback nobody expects to hit. UX_CLAUDE_CODE_OAUTH_TOKEN is an org secret, set on all three orgs the consumers live in — truenas, truenas-connect and iXsystems — but granted per repository within each. One that has not been granted to a given repo resolves to the empty string instead of failing, and reads identically to a caller that never mapped it. So a repo mapping both runs on the subscription where the grant exists and on API billing where it does not, and flips over on its own the moment someone adds the grant — no second pull request, and no window where review is broken because the secret and the workflow landed in the wrong order. When the API key is dropped for good, drop that line; until then the pair is the intended shape.

The pair is not protection against a token that is present but not working. The pick is made on emptiness, not on validity: a token that has expired, been revoked, or run into the subscription's usage limit is still a non-empty string, so it still wins, the API key is still blanked, and every repo holding the grant loses review at once. The API key covers a missing grant, never a bad or exhausted credential — nothing here fails over to API billing mid-run. Rotating the token before it expires, and watching the subscription's limit, are what cover those. The job log names which credential it chose — Authenticating with … — so a run that broke this way says so in its first step, and the gate reports the API error rather than a clean review, since submit-verdict.mjs reads the execution log when there is no structured output. The anthropics/claude-code-action version is hardcoded rather than an input: uses: does not evaluate expressions, and a configurable version is how the consumers ended up on v1.0.182, v1.0.154 and v1.0.134 in the first place. Bump it here and every caller moves.

github-token is optional and changes who posts. Left empty, comments and the review come from github-actions[bot] under the job's permissions: block. Set, they come from that token's identity. Use a machine account's fine-grained PAT (Contents read, Pull requests write, Issues write) scoped to the one repository, or a GitHub App installation token, and not a person's token: GitHub refuses to approve or request changes on the token owner's own PRs, so every PR that person opens would be un-gateable, and an approval posted as a person reads as that person's judgement rather than a check's. (The script dismisses only reviews carrying its own body marker, so a person's hand-written reviews are never touched.)

Nothing narrows a PAT. The job's permissions: block does not apply to it, and the reviewer's allowlist is a list of command prefixes, not a repository boundary: gh pr comment 42 --repo other/repo is inside Bash(gh pr comment:*). The reviewer processes PR-controlled content, so the PAT's repository list is exactly how far an injection can write. One token per repository keeps that to the repository under review. A machine account's approval counts towards required approvals like any user's, and needs no Actions setting; github-actions[bot]'s needs one (below).

A repo must not keep its own inline review running alongside this. Both post as the same identity by default, and this one's gh pr comment --edit-last edits the last comment that identity wrote — which, with an inline review also running, may be its sticky comment. The concurrency groups are distinct, so nothing cancels anything; the collision is over the comment, not the runner. Migrating means replacing claude.yml's contents, not adding a second workflow file.

What this needs from the repo

The review's structured output is scored by review/submit-verdict.mjs against review/schema.json: MEDIUM, HIGH and BLOCKER fail the job, LOW does not, and a review that produced no parseable output fails too — a reviewer that crashed must not read as a reviewer that found nothing. Findings are emitted as workflow annotations, so they land on the diff in the Files tab.

The same script submits one PR review from that score. The reviewer itself has no gh pr review, so the review on the PR is the script's decision alone and cannot disagree with the check:

Result Review submitted Job
Anything at MEDIUM or above REQUEST_CHANGES fails
Only LOW or none, a human must look COMMENT listing the reasons passes, no approval
Only LOW or none, approve-when-clean off COMMENT "would approve" passes
Only LOW or none, approve-when-clean on APPROVE passes
Only LOW or none, but GitHub refuses the APPROVE COMMENT quoting the refusal passes
No or unparseable output nothing fails

"A human must look" is the reviewer's own answer to the Does this need a human? section of review/rubric.md. In summary, it defaults to no and says yes for a line in the diff that removes or changes something external callers depend on, adds or major-bumps a runtime dependency, changes a workflow's permissions, secrets, what it publishes or what can merge, changes a visible default or removes a feature, deletes or loosens a test, migrates persisted data, or does what the repo's own review guidelines say a person decides. The PR description, deferred work, size, refactors, new tests and wording a user does not see are listed there as non-reasons; the rubric is the full list. human-review-paths is the floor under that judgement: it forces the answer for any PR touching a matching file regardless of what the reviewer said, and is where a repo puts the paths it always wants eyes on.

Each run adds a review; GitHub reviews are appended, not edited, so a PR with ten pushes carries ten of them, and the newest is the one that describes the head commit. REQUEST_CHANGES and APPROVE change the PR's state; the COMMENT outcomes do not.

Nothing is submitted when the reviewer crashed, on purpose: a changes-requested review from a run that reviewed nothing would need a person to dismiss it. The flip side: only a run that reaches the verdict clears a request-changes the workflow posted. A crashed run, or a PR labelled skip-label after a blocking round, leaves it standing until a person dismisses it in the PR. A COMMENT leaves the identity's earlier REQUEST_CHANGES or APPROVE in force, so after commenting the script dismisses its own of either kind — its own meaning posted by the same login and carrying this script's body marker, so another workflow's approval under the shared github-actions[bot] is left alone: a stale block would hold the merge, and a stale approval would let a change the verdict just said needs a person merge without one. That is best effort: on a protected branch, dismissing needs admin or a place on the review-dismissal list, and a refusal is a warning in the log, not a red check — the stale review then stays until a person dismisses it.

Making the review count is branch protection, per repo:

  • Require 1 approval and mark Automatic PR review required. The workflow's approval satisfies the first, which is the whole mechanism and the whole risk: a PR the reviewer misjudges as routine merges with no person involved. A repo that wants to see the calls first can set approve-when-clean: false, read the "would approve" comments for a while, and drop the line once they look right.
  • Dismiss stale pull request approvals when new commits are pushed. Without it an approval of one commit still counts while the next is being re-reviewed, and cancel-in-progress makes that window real.
  • With the job token only: enable Allow GitHub Actions to create and approve pull requests in the repository's (or organisation's) Actions settings, or APPROVE returns 422. A refused approval falls back to the "would approve" COMMENT, which quotes the refusal and clears the run's own earlier request-changes, so the PR is not left blocked by a round it has since passed. Any other review the token cannot post is a warning in the log, never a change to the check: the score decides the exit status, so a clean PR stays green and simply gets no review.
  • CODEOWNERS: if Require review from Code Owners is on, the approval only satisfies it when the posting identity is a code owner.

Why this passes github_token explicitly. Left unset, the action exchanges its OIDC token for an Anthropic GitHub App token, and that exchange refuses when the calling workflow differs from the version on the default branch:

Workflow validation failed. The workflow file must exist and have identical
content to the version on the repository's default branch.

It is a reasonable guard on Anthropic's own credentials — a PR should not mint an app token for a workflow nobody has merged — but it applies to the token exchange, not to reviewing. The effect was that the PR adopting this workflow could never be reviewed by it: the action skipped, the step went green in about four seconds, and the gate below fails closed on empty output, so every migration PR in every repo showed a red Automatic PR review.

Passing github_token: ${{ github.token }} makes setupGitHubToken return early, so the exchange never happens. GitHub has already scoped that token to the job's permissions: block, which is where the equivalent restriction belongs. The cost: comments come from github-actions[bot] rather than the Claude app. A pull_request from a fork is not a gap: it gets no secrets, so the auth check stops it before anything posts — unless the repo sends secrets to fork PRs, in which case GITHUB_TOKEN is read-only there and posting fails.

Whether a failed job blocks a merge is branch protection, set per repo. That is the reversible half of the decision, and adopting this workflow does not make it for you. There is deliberately no override label: bypassing a red check is something branch protection already gates on permission and records against a person. (skip-label is the exception, and it skips the whole review rather than a finding — restrict who can apply it.)

Worth knowing before marking Automatic PR review required: a skipped job satisfies a required status check. So every path that skips the review — the label, a non-member author, the write-access gate failing — reports green, and the check says "reviewed" about a PR nobody reviewed. The first two are the intended behaviour. The third was not, so a failed check-member now starts the review job and fails it in its first step instead of leaving it to skip; that costs a runner start and no API tokens. check-member answers 'false' on a permission lookup it cannot make, so reaching that step means the job itself died — runner or action infrastructure, and a re-run.

The severity rubric that assigns those levels is review/rubric.md, here rather than in each repo, because the gate and the schema are here: three copies of the rubric would drift from the thing scoring them. The workflow appends it to the caller's prompt-file, so a repo's own file should say what to look for in its code and leave grading alone. A repo migrating a prompt that already carries a rubric — truenas/api-client-ts does — should delete that half.

tooling-ref exists because the schema, rubric and scripts have to be checked out into the caller's workspace at run time, and a reusable workflow cannot see which ref it was itself called at. (github.job_workflow_sha is exactly that, but actionlint 1.7.7 does not know the property and fails the file, and this repo's CI runs actionlint.) It defaults to master, which matches every current caller. A caller pinning this workflow to a tag must pin tooling-ref to the same tag, or it gets master's tooling against a pinned workflow.

Everything the run generates, and the tooling checkout itself, goes in .claude-review/ in the workspace, added to .git/info/exclude so it stays out of git status and out of the review.

prompt-file is copied there too, and the prompt points the reviewer at the copy rather than at the caller's path. claude-code-action moves .claude/ aside to .claude-pr/.claude/ before the reviewer starts, so the default .claude/review-prompt.md — and anything else under .claude/ — is not there to be read by the time it matters. That failure is silent: the reviewer is the only thing that expands {{file:...}}, and a path resolving to nothing looks the same as a guidelines file with nothing to say, so the run costs a full review that reads as one with guidelines. .claude-review/ is outside the directory the action relocates. A caller can keep its file wherever it likes.

The relocation has a second half the copy does not fix: the tracked files under .claude/ are now missing from the worktree, so git diff and git status — which the reviewer runs to orient itself — report a deletion the pull request does not make. The prompt says so, since nothing in the repo the reviewer is looking at could tell it otherwise, and a finding raised on that phantom deletion at MEDIUM or above would fail the gate over work nobody did.

CI checks that the {{file:.claude-review/...}} paths in the prompt agree with the steps that write them. Those files do not exist until a run creates them, so existence is not checkable here, but a reference and its producer are two unrelated string literals and renaming one alone is silent at run time — which is how the prompt-file reference stayed wrong for the whole life of the workflow with no run reporting it.

Actions

.github/actions/prepare

A composite action, not a reusable workflow: it runs as a step inside an existing job, so the caller keeps its own runs-on, permissions and checkout. Reusable workflows cannot do that — they bring their own job.

steps:
  - uses: actions/checkout@v4          # required first; this installs into the workspace
  - uses: iXsystems/ux-github-workflows/.github/actions/prepare@master
    with:
      cache-jest: 'true'               # optional
Input Default Notes
node-version 24.13.1 Pinned, not floating
cache-jest 'false' Caches .jest/cache; only useful where Jest runs
yarn-cache 'false' Caches Yarn's global cache folder

Inputs are strings — every composite-action input is. Compare with == 'true'.

Step order is load-bearing. actions/setup-node runs before corepack enable, because Corepack writes its shims into the active Node installation's bin directory: enable it first and then let setup-node swap in a different Node, and yarn goes missing. That is also why setup-node's own cache: 'yarn' is not used — it shells out to yarn before Corepack has run, and would either fail or silently cache Yarn 1's directory for a Yarn 4 repo. The yarn-cache input resolves the folder with yarn config get cacheFolder after Corepack instead.

This replaced identical local copies in truenas/webui and truenas-connect/ui and six inline repetitions in iXsystems/truenas-ui-components's ci-cd.yml, which had drifted to a floating '24' against the others' pinned 24.13.1.

.github/actions/truenas-appliance

A JavaScript action with a post step: it claims a freshly provisioned TrueNAS appliance on a lab host for the rest of the job and releases it when the job ends — on success, failure and cancellation. A composite action cannot do that, and a trailing if: always() step is skipped when the runner itself dies. The appliance's lease (lifetime, default 3h) covers the runner that never comes back: the next claim on that host prunes whatever has expired.

jobs:
  e2e:
    runs-on: [self-hosted, linux, truenas-lab]
    environment: e2e-lab
    steps:
      - uses: iXsystems/ux-github-workflows/.github/actions/truenas-appliance@master
        id: appliance
        with:
          host-user: ${{ vars.E2E_TN_GUEST_HOST_USER }}
          host-api-key: ${{ secrets.TN_GUEST_HOST_API_KEY }}
          template-password: ${{ secrets.TN_GUEST_TEMPLATE_PASSWORD }}
      # TN_HOST, TN_USERNAME, TN_PASSWORD … are now in the job environment,
      # and on steps.appliance.outputs as host, username, password, …
      - run: yarn e2e
      # No release step: the action's post step does it.
Input Default Notes
baseline fresh-install The only baseline that exists today
tn-guest, python lab paths tn_guest.py (iXsystems/api-ci-testbed) and a Python with truenas_api_client, on the runner
host, pool localhost, tank The lab host's API and the pool for VM datasets. localhost when the runner is the host, which is the lab's layout
host-user, host-api-key / host-password root, none An account with Full Admin on the host
iso empty Pin an ISO by host path. Empty resolves the newest v27 nightly, keeps it a week (iso-max-age-days), prunes to iso-keep; needs the runner to be the host
refresh-iso 'false' Fetch the newest nightly now, ignoring the week and the pin
template-password empty Set: claims clone a template for the ISO, built on first use, password rotated per claim. Unset: every claim installs from the ISO (~4 min instead of ~80 s)
template-prefix, template-keep e2e-template, 2 Templates are nicknamed <prefix>-<8 hex of ISO name + disk geometry>; beyond the newest template-keep others, they are collected once nothing is cloned from them
lifetime 3h The lease
memory-mb, vcpus 6144, 4 Per claim
os-disk-gb, data-disk-count, data-disk-gb 10, 10, 10 Part of the template identity; changing one builds a new template
export-env 'true' Also write TN_PROFILE, TN_HOST, TN_HOST_HTTP, TN_USERNAME, TN_PASSWORD, TN_DOMAIN, TN_BASELINE, TN_GUEST_ISO to the job environment

Outputs: profile, host, host-http, username, password (masked), domain, baseline, iso, iso-source (pinned, reused or downloaded).

The whole contract with the lab is appliance.sh beside the action: iso, claim, release, build-template, and the environment variables the inputs map onto. It runs by hand with the same variables, which is how it is debugged. main.js and post.js use only Node built-ins, so there is nothing to bundle and no node_modules to commit; they shell out to the script and turn its KEY=VALUE lines into outputs, environment and the post step's state. The password is registered with add-mask before it reaches an output.

What the runner has to provide, the timings, and the lessons behind every rule in the script are in truenas/webui's e2e/docs/05-ci.md, which is where this was built and first used.

Adoption status

Repo check-ticket pr-title check-member prepare review
truenas/webui adopted n/a — no semantic-release migrating (main.yml) migrating own claude.yml
iXsystems/truenas-ui-components adopted migrating n/a — no self-hosted runner migrating migrating (#175)
truenas-connect/ui adopted n/a — no semantic-release migrating (main.yaml) migrating migrating (#370)
truenas/api-client-ts migrating migrating via the review migrating migrating (#33)
iXsystems/ux-github-workflows (this repo) adopted (pr-ticket.yml) n/a — no semantic-release self-test in ci.yml n/a adopted (claude-review-self.yml)

This repo calls three of its own workflows, by relative path rather than @master, so a change to any of them is executed on the pull request that makes it instead of on a consumer's next one: pr-ticket.yml runs check-ticket.yml, ci.yml's self-test job runs check-member.yml, and claude-review-self.yml runs claude-review.yml. pr-ticket.yml is a separate file from ci.yml because the ticket check needs the edited trigger and the rest of CI does not want it.

claude-review-self.yml is this repo's own adoption of the review, and differs from a consumer's copy in two lines. The uses: is the local path, so a pull request changing the review workflow is reviewed by the version it proposes. tooling-ref is set to ${{ github.sha }} rather than left at master, so the rubric, schema and review/*.mjs come from that pull request too — otherwise a change to the rubric would run under the new workflow and be graded by the old rules. The guidelines it points the reviewer at are in .claude/review-prompt.md, the default path.

github.sha and not the head SHA, because it has to be the commit the workflow itself was loaded from. On pull_request that is the merge ref, where both the relative uses: and the review job's actions/checkout resolve. Pinning the tooling to the branch tip instead splits the two: the branch lacks whatever reached master after it was cut, so a claude-review.yml from the merge ref can call a review/ script that is not in the tree the tooling came from, and the step dies on MODULE_NOT_FOUND. The review-assets job does not catch it — it checks that the workflow and review/ agree inside a single tree, and two commits is the case it cannot see.

The cost of calling it locally is that a pull request which breaks the review workflow breaks its own review, and the failure looks like a review finding until you read the job log. That is the same trade self-test already makes, and it is the cheaper direction: the alternative is a consumer's CI finding out. On a fork's pull request the workflow comes from the merge ref, as on any other pull request — the fork's code merged into master. What a fork run does not get is secrets, so the review step fails for want of an API key rather than running fork-authored workflow code with one, and require-write-access stops a non-writer's pull request before that point.

api-client-ts is the repo the review came from. Each consumer migrates by replacing its own claude.yml with a call to this one — a small PR in that repo, reviewable on its own, rather than something this repo can do to them. webui has not been started.

Those PRs are reviewed by the workflow they install, which is only true because this one passes github_token — see above. Nothing is required in branch protection in these repos today, so a finding does not block a merge either.

truenas-ui-components has a local check-member.yml, which is where the one here came from. It has since drifted: the copy there is the version from before the missing-pull_request-payload guard, so it still throws on a non-PR event rather than answering false. Its migration (#175) deletes it — the shared review calls the shared gate, so the copy has nothing left to do.

Releasing

Callers reference @master, so anything landing on master is live in every consumer immediately — there is no per-repo review gate between a change here and three repos' CI running it.

That puts the whole burden on the PR into this repo:

  • Treat a change to a job name: as breaking. Consumers match "<caller job id> / <this name>" in branch protection, so a rename silently stops a required check reporting, with no PR in their repo to explain it.
  • Same for removing or renaming an input, or tightening a default.
  • Verify against one consumer's next real PR before assuming it is fine everywhere; the consumers differ in trigger, secret names and permissions.
  • review/ ships the same way. It is checked out at tooling-ref, which defaults to master, so an edit to the rubric changes how every gated review grades on its next run — including whether a finding fails a build.

If that becomes too sharp an edge, the alternative is tagging: cut v1, move callers to @v1, and release with git tag -f v1 && git push -f origin v1. That was the original intent, but with only three consumers and one team it was judged more ceremony than it buys.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages