Org-wide GitHub defaults for dodi-smart. Issue templates every repo inherits,
reusable workflows every repo calls, the composite actions those are built from,
and the shared Renovate preset.
A repo here calls a workflow instead of copying one. Its CI becomes a caller file of about twenty lines, and everything below that line is maintained once, in this repo, for every repo at the same time.
This repo is public because it has to be. A workflow run must be able to read the workflows it calls, and a public reusable workflow is the ordinary way to do that across an org. Nothing here is product-specific: no product names, no hostnames, no secret values.
Add one file to your repo. That is the whole integration.
# .github/workflows/pr-checks.yml
name: PR checks
on:
pull_request:
branches: [main, develop]
jobs:
checks:
uses: dodi-smart/.github/.github/workflows/pr-checks.yml@v1
with:
stack: bun
secrets: inheritThat gets you lint, typecheck, test and build on the right runner, with the package cache set up correctly for that runner, and it stays correct when the fleet changes.
| Part | What it is for |
|---|---|
uses: |
Which workflow, pinned to @v1. Always pin. |
with: |
Inputs. Stack, commands, runner weight, timeouts. |
secrets: inherit |
Passes the calling repo's secrets through. Nothing is stored here. |
on: and concurrency: |
Stay with you. Triggers and path filters depend on your branches and layout. |
| Secret | Needed for |
|---|---|
CLAUDE_CODE_OAUTH_TOKEN |
every agent workflow |
GH_APP_CLIENT_ID + GH_APP_PRIVATE_KEY |
validating the runner selector against the live fleet |
GH_APP_CLIENT_ID is the one name for that secret. Migrate a repo still carrying
the older GH_APP_ID. Without it the picker cannot read the org runner list, so
it skips validation, and an unvalidated selector that matches nothing looks
exactly like a busy fleet.
| Workflow | Fires on | Does |
|---|---|---|
pr-checks.yml |
pull request | Lint, typecheck, test, build, per stack |
deps-verify.yml |
Renovate/Dependabot PRs | Builds it, reads upstream changelogs, posts a verdict. Never merges. |
pr-review.yml |
ready_for_review, agent:review |
Second-opinion review, deeper on sensitive paths |
issue-triage.yml |
issue opened or reopened, agent:triage, @claude triage, manual dispatch with issue-number |
Classifies, sets fields, then plans or asks blocking questions |
issue-implement.yml |
agent:implement, @claude implement |
Branch, code, draft PR. Requires a plan. Never merges. |
claude-assist.yml |
@claude <anything else> |
The general assistant |
release.yml |
push to a release branch | semantic-release, single or multi-module |
supabase-deploy.yml |
called after release.yml |
Pushes a Supabase project's schema and functions for the tag a release just cut |
react-doctor.yml |
pull request, React repos | Static analysis of React/TS source. Advisory by default. |
zavet-check.yml |
pull request | Knowledge-layer checks, for repos that have one. Report-only on dependency bot PRs |
supabase-checks.yml |
pull request, Supabase repos | Deno edge-function check, generated-types check, pgTAP tests. Hosted only. |
pick-runner.yml |
called by the others | Chooses a runner and validates the choice |
release.yml reports what it did through workflow_call outputs, so a caller
can chain a deploy job on an actual release rather than a green job:
| Output | Meaning |
|---|---|
released |
'true' when at least one new version tag was pushed, 'false' otherwise |
version |
Newest released version, without the v prefix (e.g. 1.4.0) |
tag |
Newest released tag (e.g. v1.4.0) |
tags |
JSON array of ALL new tags this run, for multi-module repos |
They come from a tag diff taken around the release step, not from parsing
semantic-release's own output, so they work the same way whether the caller
uses modules or a custom release-command.
release.yml can also merge a release branch back into a prerelease branch:
| Input | Default | Meaning |
|---|---|---|
backmerge |
false |
Merge backmerge-from into backmerge-to after releasing |
backmerge-from |
"main" |
Branch the release was cut from |
backmerge-to |
"develop" |
Prerelease branch to merge into |
backmerge-resolve-paths |
"" |
Extra paths to auto-resolve toward backmerge-from on conflict, newline- or space-separated (e.g. a subdirectory manifest and its lockfile) |
A stable release after a prerelease always conflicts on the files both commits
rewrote, so the job auto-resolves package.json, package-lock.json,
bun.lock, pnpm-lock.yaml, yarn.lock and CHANGELOG.md toward
backmerge-from and fails on any other conflict. backmerge-resolve-paths
extends that list; it does not replace it.
supabase-deploy.yml is not triggered on its own. It is a job the caller
chains on release.yml with needs:, gated on release.yml's outputs, so it
deploys the tag a release actually cut rather than the push that started the
run:
jobs:
release:
uses: dodi-smart/.github/.github/workflows/release.yml@v1
secrets: inherit
deploy:
needs: release
# Production: only a cut, non-prerelease tag on main.
if: ${{ !cancelled() && needs.release.outputs.released == 'true' && github.ref == 'refs/heads/main' && !contains(needs.release.outputs.tag, '-') }}
uses: dodi-smart/.github/.github/workflows/supabase-deploy.yml@v1
with:
ref: ${{ needs.release.outputs.tag }}
cli-version: 2.117.0 # renovate: datasource=npm depName=supabase
environment: production
secrets:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_DB_PASSWORD }}
SUPABASE_PROJECT_ID: ${{ secrets.SUPABASE_PROJECT_ID }}A staging caller drops the released clause from the if: and deploys on
every push to its branch instead, since staging has nothing to gate on a
release output for.
Do not reach for on: workflow_run: { workflows: ["Release"] } instead. It
fires on completion, including a run that released nothing, so a no-op release
run still triggers a deploy of whatever HEAD happens to be at that moment. And
by default it checks out the SHA that started the original run, not the tag
@semantic-release/git pushes afterwards, so a workflow_run-triggered deploy
ships the commit before the release it is meant to deploy. Chaining with
needs: inside the same run is what makes the actual tag available as an
output at all.
cli-version is not defaulted by this workflow. A default here is a version
Renovate cannot see, so the CLI would upgrade itself with no diff for anyone to
review. Pin it in the caller with a Renovate regex-manager comment, as in the
example above, so a CLI release opens a normal pull request instead of taking
every project's next deploy down at once, the way an undetected supabase/cli
regression once did.
| Input | Default | Meaning |
|---|---|---|
ref |
(required) | Tag or sha to check out and deploy |
cli-version |
(required) | Supabase CLI version, pinned and Renovate-marked in the caller |
environment |
"" |
Name of a GitHub environment configured in the caller's repo. Empty means no environment: no protection rules, no environment secrets |
seed |
false |
Pass --include-seed to supabase db push |
functions |
true |
Deploy supabase/functions when it holds anything; false skips it even if it does |
health-url |
"" |
Base URL to probe after deploy. Empty skips the health check entirely |
health-routes |
"/" |
Space-separated routes appended to health-url |
health-attempts |
6 |
Retries before the health check fails the job |
timeout-minutes |
15 |
Job timeout |
| Secret | Required | Meaning |
|---|---|---|
SUPABASE_ACCESS_TOKEN |
yes | Supabase CLI auth |
SUPABASE_DB_PASSWORD |
yes | Non-interactive supabase link / db push |
SUPABASE_PROJECT_ID |
yes | The project ref to link and deploy against |
FUNCTION_SECRETS |
no | Multiline KEY=value, one per line, passed to supabase secrets set. Built by the caller from its own secrets (FUNCTION_SECRETS: | followed by NAME=${{ secrets.NAME }} lines) — nothing here is hard-coded with a function secret's name or value. Unset skips the step entirely |
The health check FAILS the job after health-attempts, deliberately: a deploy
that leaves every route erroring must not go green. A warning-only check is
strictly worse than no check, because it reads as a passing signal that
nobody then goes back to question.
| Action | Purpose |
|---|---|
actions/agent-gate |
Decides whether an agent may run. Evaluates agent:no-touch first, always. |
actions/run-agent |
Invokes the agent with the org's tool allowlist and reporting defaults |
actions/setup-stack |
Installs a toolchain, resolves cache isolation, supplies conventional commands |
actions/sticky-comment |
One keyed comment per pull request, rewritten in place on every later run |
actions/semantic-release-config |
Links the shared semantic-release config into a consumer's node_modules from a private prefix, never from a registry and never into the checkout it came with |
actions/pick-runner |
Resolves a runner weight to a selector, validated against the live fleet. What pick-runner.yml calls, and what a job that already runs hosted (like pr-checks.yml's pick) calls directly to pick more than once without a second hosted job. |
Adopting a workflow here removes a class of problem rather than a file.
- Runner selection. Ask for
lightorheavyand stop naming hardware. When the fleet changes, the mapping changes here, once, instead of in every caller in every repo. - Cache correctness. Where a package cache lives depends on the runner, the
job and the stack.
setup-stackdecides it. Getting this wrong costs a day. - Agent safety. The kill switch, the bot rules and the draft rules are one
implementation with tests, not one
if:per workflow that drifts. - Drift between copies. A hand-written CI file diverges the moment two repos edit it. This is the same file for all of them.
- Silent breakage. The picker checks its own selector against the live fleet and annotates the run when it matches nothing.
A repo wired up to these workflows carries this badge in its README:
It is a claim about that repo, so it is worth knowing what backs it. The badge
means the repo calls the workflows above at @v1, that those workflows are
active rather than sitting disabled, that it carries the shared label set and
an area map, and that its bot configs emit the label names the workflows expect.
An onboarding tool asserts all of it and fails the repo if the badge is
present while any of it is not. A badge nobody checks is decoration within one
drift, and worse than none, because it is read as a guarantee by everyone who
does not go and look.
Two org-level repository custom properties carry the same facts in queryable form:
| Property | Answers |
|---|---|
onboarded |
Which generation of these workflows the repo is on: v1, or none |
client |
Which body of work the repo belongs to |
onboarded holds a version rather than a yes/no, because the question actually
worth asking is which repos still need migrating, and a version answers it as a
single search instead of an audit. It is required with a default of none, so a
repo created next year answers it too rather than being quietly absent from
every count.
Property values are visible to org members only. The badge is the public half, and it deliberately names no repo but this one.
agent:no-touch stops everything. Checked before every other condition, in
every agent workflow, with no exemption. Not workflow_dispatch, not an explicit
command, not a maintainer. A kill switch that works on only some code paths is
not a kill switch, and position matters as much as existence: a check after an
early return silently stops covering that path. One implementation in
actions/agent-gate, with tests across every workflow shape.
No agent merges anything. Dependency verification posts a verdict and leaves merge policy to Renovate's own rules. The implement workflow opens a draft pull request and stops. Evidence is only useful if it is allowed to be wrong, and merging on a clean verdict forces conservative tuning, which produces noise, which gets the report ignored.
Labels are the only thing that fires a workflow, so they are how you ask. Fields are queryable across repos, so they are where the answer lives. Each fact sits on exactly one of them, because two sources for one fact disagree within weeks and then neither is trusted.
Every agent:* label is self-clearing. The workflow removes it when it finishes,
including when it refuses. If it is still there, the work is genuinely running.
agent:triage -> classify, then plan or ask
agent:implement -> branch + draft PR (only when Triage state = Plan ready)
agent:review -> review this PR
agent:no-touch -> stop everything, no exemptions
agent:implement does nothing unless the issue is planned. That is a field
comparison in the gate job, which also picks the runner once that comparison
passes, with no override. An agent asked to judge whether a plan is good
enough will sometimes accept a two-line issue body, and the cost is twenty
minutes of confident work on the wrong thing.
The stack is an input, not a separate template: bun, node, gradle,
android, flutter, rust, xcode, none.
with:
stack: gradle
install: "" # "" means this repo has no such step
build: ./gradlew assembleDebug ktlintCheck"@stack", the default, means that stack's conventional command. "" means the
step does not exist here. Those are different intentions, and a plain default
cannot express both, because Gradle and Cargo resolve on demand and genuinely
have no install step.
There used to be a caller template per stack. It failed the way templates fail: the Gradle one carried one product's task name, in a file every other Gradle repo was told to copy.
An optional step for a design-system verifier (oxlint hosting @shadcn/lint),
separate from lint because it has its own exit code and its own ratchet.
Default "" means the step does not exist -- every caller pinned at @v1 is
unaffected until it opts in. No @stack default: there is no conventional
command for this, on any stack.
with:
design-lint: bun run lint:design --format=githubWarnings are advisory. --max-warnings N in the caller's own command is the
ratchet -- tighten it there as the count comes down.
pr-checks.yml splits light work from heavy by default. single-job: true
collapses it onto the heavy runner.
Use it for Gradle. Three jobs mean three configuration phases and no shared
daemon. One job keeps one checkout and one warm GRADLE_USER_HOME. The split
stays the right default for a cheap, independent lint.
env (newline KEY=VALUE) reaches every command step, which is where build
tuning like GRADLE_OPTS belongs. build-env still applies to the build step
alone.
with:
stack: android
single-job: true
lint: ""
typecheck: ""
test: ""
build: ./gradlew --build-cache --parallel assembleDebug ktlintCheck --continue
env: |
GRADLE_OPTS=-Dorg.gradle.daemon=false -XX:MaxMetaspaceSize=512m
coverage: kover
coverage-path: app/build/reports/kover/reportDebug.xmlEither shape runs behind ONE hosted pick job, not two. It resolves both the
light and the heavy runner (skipping whichever single-job does not need), so
picking twice costs one hosted job instead of two.
pick also decides whether the change is docs-only: every file the PR touches
must match one of docs-only-paths (newline-separated globs, default **.md
and docs/**) against the PR base. When it is, checks, test, build and
all all skip -- but commitlint still runs, because it checks the commit
message, not the files. The default catches a Markdown-only change; widen it
per caller for e.g. docs/** design/**.
with:
stack: bun
docs-only-paths: |
**.md
design/**A skip is only ever a positive finding. On workflow_dispatch, or whenever the
diff against the PR base cannot be computed, docs_only is false and every
job runs as normal.
This is also why a caller should not add paths-ignore: ['**.md'] to its
own on: pull_request: trigger to get the same effect. paths-ignore skips
the entire workflow, which means the workflow never runs and creates no
status-check context at all -- and a branch ruleset that requires one then
waits on that pull request forever, because a check that was never created can
never turn green. docs-only-paths gets the same skip without losing the
context.
That context is pr-checks, a summary job that runs unconditionally
(if: always()) after everything else, whether or not anything was skipped.
It fails if checks, test, build, all or commitlint failed or was
cancelled, and passes -- printing "docs-only change, checks skipped" -- when
they were only skipped. It is the one job a branch ruleset should require:
<caller job id> / pr-checks exists on every push in both single-job and
split mode, where checks / checks and checks / all do not -- exactly one
of those two is always skipped depending on single-job, so neither can be
named in a ruleset that has to work for every caller.
setup-stack resolves the mode from isolate, cache and the runner:
- Verification jobs isolate.
deps-verifypins caches toRUNNER_TEMPwith GitHub cache off, because a verification job that can see yesterday's tree is not verifying. - Self-hosted uses home dirs, so the per-runner named volumes are actually read. GitHub cache stays off there.
~/.pub-cacheis job-scoped in every mode. A home dir is only worth using if a volume backs it, and the image mounts none for pub.- Hosted uses one mechanism per stack,
setup-gradle/rust-cache/flutter-action. Never a package store, and neverrestore-keyson one, which is how a partial tarball comes back on every retry.
Never cache a project build directory. Not build/, not */build, not
.gradle. */build/intermediates holds absolute paths and the workspace root is
not stable between runners, so AGP rejects its own inputs. A per-SHA key also
misses on every commit by construction, then falls through restore-keys to
whatever another branch left behind. It surfaces as a compile error in a file the
pull request never touched. Reuse of compiled output is the build tool's job, by
content hash, and setup-stack already wires it up.
supabase-checks.yml covers the Supabase-side checks a bun/node pr-checks.yml
run never touches: a Deno edge-function check, a generated-types check, and
pgTAP tests. It runs on ubuntu-latest only, with no runner picker — the
self-hosted fleet runs jobs inside containers on a shared daemon, so supabase db start publishes postgres's ports on the HOST while the CLI polls the
CONTAINER's own localhost. concurrency, paths: and the dispatch-aware draft
gate stay with you, same as pr-checks.yml.
| Input | Default | Meaning |
|---|---|---|
cli-version |
(required) | Supabase CLI version, pinned exact with a renovate: datasource=npm depName=supabase marker tracking the same supabase devDependency the repo installs from. Generated types must come from that same CLI or the diff below fails on formatting, not schema. |
types-path |
"" |
Path of the committed generated types. Empty disables the types job (reported skipped, not failed). |
migrations-paths |
supabase/migrations, supabase/seed.sql |
Newline-separated paths whose change triggers the types job's heavy steps on a pull request. workflow_dispatch always runs them. |
seed-check |
false |
Re-apply supabase/seed.sql after db start to prove it is re-runnable. |
deno-dir |
"" |
Directory of Deno-only source, e.g. supabase/functions. Empty disables the deno job. |
pgtap |
false |
Run supabase test db in its own job. |
deno-version |
v2.x |
Passed straight to denoland/setup-deno. |
timeout-minutes |
30 |
Per job. |
jobs:
supabase:
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.draft == false
uses: dodi-smart/.github/.github/workflows/supabase-checks.yml@v1
with:
# renovate: datasource=npm depName=supabase
cli-version: 2.116.0
types-path: src/lib/supabase/database.types.ts
seed-check: true
deno-dir: supabase/functions
pgtap: truepick-runner.yml takes a semantic weight and resolves it. It is a thin
wrapper around actions/pick-runner, the composite action that does the actual
selecting; call the action directly from inside a job that is already hosted
(as pr-checks.yml's pick job does, twice, and as every agent workflow's
gate job does, once, after agent-gate decides the run should proceed)
rather than paying for a second hosted job just to reuse the workflow.
weight |
Selector | Use |
|---|---|---|
light |
self-hosted,Linux,light |
lint, typecheck, checks, releases, reading a diff |
heavy |
self-hosted,Linux,large |
builds, Docker, full suites |
apple |
self-hosted,macOS,ARM64 |
Apple toolchain, signing |
hosted |
none | forces hosted-runner, ubuntu-latest by default |
Selectors name capability labels, never an architecture and never a machine name. A runner of any arch that joins a pool is picked up with no change here, and a machine name would not survive re-registration.
light and heavy are tiers of intent, so ask for the one that describes your
work. They stay apart even when one pool could serve both, because re-tiering is
then two lines here instead of an audit of every caller.
A selector that cannot be reached falls back to fallback, the light pool by
default, so an unreachable large pool costs capacity rather than hosted minutes.
Set fallback: ubuntu-latest for a job that must finish even with the whole
fleet offline, because a self-hosted fallback queues instead.
Public repos and fork pull requests always get hosted runners, with no way to
opt out. The runner group refuses public repos, and a fork PR would otherwise run
attacker-authored code on our own hardware against a cache the next job inherits.
That path does not read fallback.
Pin @v1. It is a moving tag, and it moves on its own: every release from
main force-advances it to the new version. A breaking input change cuts v2
rather than redefining v1.
So merging to main is a rollout. There is no staging step. The moment a
release is cut, every repo pinned to @v1 is running the new code, including the
picker and composite actions this repo's own workflows call internally. Verify in
the pull request, because after the merge it is already live everywhere.
Releases are cut by semantic-release from main, so the version comes from the
commit messages. feat: opens a minor, fix: a patch, and a breaking change
footer a major.
The major tag is derived from the version, so this works unchanged at v2 and
beyond. Releasing 2.0.0 creates v2 and stops touching v1, which freezes
at the last 1.x. Callers pinned to @v1 keep the old major until they choose
to move, which is the whole point of pinning a major.
One consequence to know before you cut a v2: main is the only release branch,
so once 2.0.0 ships there is no way to release a 1.x patch. That needs a
maintenance branch added to release.config.mjs, for example
branches: ["main", "1.x"], and it is easier to add before you need it than
during an incident.
chore(deps) also cuts a patch, which is specific to this repo. Renovate labels
every dependency update chore(deps), and elsewhere that deliberately releases
nothing. Here the dependencies are the action versions these workflows run on, so
a bump that never reached a release would leave every caller pinned to @v1 on
the old ones. A plain chore: with no deps scope still releases nothing.
Do not pin @main even so. @v1 still moves only when a release is cut, so a
commit that releases nothing, a docs: or a ci: change, never reaches a
caller. @main picks up every commit. @v1 is also a version you can name in a
rollback, and @main is not.
To roll back, point the tag at the previous release and force it:
git tag -f v1 v1.0.1 && git push -f origin v1One thing pinning does not buy you here. claude-code-action refuses to run when
the workflow file differs from the default-branch copy, which is a correct
control, since a pull request could otherwise edit the reviewer to exfiltrate its
token. It means a change to an agent workflow cannot be exercised on the pull
request that makes it, only after merging.
semantic-release/ is an npm workspace in this repo that holds the org's
shared semantic-release configuration, @dodi-smart/semantic-release-config.
It carries the release rules, the changelog sections and the plugin suite,
pinned to versions that agree with each other, so a consuming repo does not
have to work that out on its own.
The package is never published to any registry. It reaches a consumer through
actions/semantic-release-config, which copies this checkout's root
manifests, lockfile and semantic-release/ into a private prefix, installs
the plugin dependencies there, then links that prefix's semantic-release
directory into the consumer's node_modules by name. The checkout the
action was fetched with is left untouched, so nothing sharing it loses a
dependency. The shared release.yml runs that action automatically before
semantic-release, gated by its shared-config input (default true); a
caller whose release-command does not run semantic-release sets it false
to skip the install. A repo that hand-rolls its own release job adds one
step, after its own install and before semantic-release:
- uses: dodi-smart/.github/actions/semantic-release-config@v1actions/semantic-release-config/test.sh checks this the way Self test
runs it for the other actions: by resolving the linked package and its
plugins from a scratch consumer, not by reading the installer's output.
There are two ways to use the config once it is linked. Extend it whole, for
a single-package repo released from main with develop as a prerelease
channel:
// .releaserc.json
{ "extends": "@dodi-smart/semantic-release-config" }Or compose, when the repo needs its own plugin list, for example a version
file to rewrite. No extends line: import the helpers you want and list
them.
// release.config.mjs
import { branches, commitAnalyzer, releaseNotes, changelog, git, github } from "@dodi-smart/semantic-release-config";
export default {
branches,
plugins: [commitAnalyzer(), releaseNotes(), changelog, git({ assets: ["pubspec.yaml", "CHANGELOG.md"] }), github()],
};A consumer installs semantic-release and nothing else. Every plugin the
config names is a dependency of the package itself, and the package arrives
by the link, not by an install, so nothing is added to the consumer's
package.json or its lockfile.
Updates arrive the way workflow updates do: someone merges a change here, the
@v1 tag moves, and every consumer is on the new config the next time it
releases. There is no per-repo version to bump, and so no way for a consumer
to lag.
Watch for effect, not hidden. Changelog section entries used to hide a type
with a boolean hidden property; the preset that renders them now reads
effect: "bump" | "hidden" instead and does not warn on the old key, so a type
still carrying hidden: true renders anyway and leaks into the notes as an
untitled bullet. Compose your own types list with effect.
commitAnalyzer({ releaseRules }) replaces a shared rule of the same type and
scope rather than adding beside it, because the analyzer treats a
release: false match as undecided and lets a later matching rule win; a
shared rule could otherwise never be turned off. npm is always in the
default config, since that config is for a Node package; a repo that is not
one, even with an incidental package.json, composes and leaves npm out.
exec is a plugin path with no options of its own; pass your own *Cmd
entries as [exec, { successCmd: "..." }]. github() takes
{ releasedLabels }; pass github({ releasedLabels: false }) in a repo
without release:prod / release:staging labels.
Two plugins in semantic-release/package.json are pinned to exact beta
versions, because their stable releases cannot render this package's v10
changelog preset. Renovate in this repo offers the matching stable release
automatically once one exists, because the pin is exact rather than a caret
over a prerelease. When it lands, the pins here move to stable and every
consumer picks it up on its next release, the same way any other change here
reaches them.
Every plugin this package exports is an absolute path, resolved from inside
this package, not a bare package name. That is what makes the composed form
work with no extends. semantic-release resolves a plugin named in a config
from its own directory first, so a bare name would get whatever copy
semantic-release itself depends on; the extends redirect only fixes that
for plugins the extended config lists, and --extends <file> on the command
line, the reusable release workflow's modules path, replaces the config's
own extends entirely. A path sidesteps all three. Both usage modes are
covered by real dry runs in the package's end-to-end test, run through the
installer.
{ extends: ["github>dodi-smart/.github"] }default.json carries only what is true of every repo. Ecosystem rules stay in
the repo that has that ecosystem, because a rule matching nothing is worse than
no rule: it reads as coverage.
The filename matters. For a bare github>owner/repo, Renovate fetches
default.json and no other name, then falls back to renovate.json -- which
extends this preset, so resolution goes circular and every repo silently drops
to stock defaults. Renovate parses a .json preset as JSONC, so it keeps its
comments.
| Template | For |
|---|---|
| Bug | Something behaves incorrectly |
| Feature | A capability that does not exist yet |
| Customer request (unrefined) | Raw customer ask. Paste it verbatim and let triage work out the questions. |
| Chore | Maintenance with no user-visible change |
Blank issues stay enabled. The gh CLI and agents create bare issues, and
forcing them through a form would break every scripted path.
Not a typo, and not removable. A uses: value is {owner}/{repo}/{path}@{ref}.
This repo is named .github, and GitHub requires reusable workflows to live in
.github/workflows/ of their source repo, so both segments contain it. Composite
actions have no such rule, so they take a single segment:
dodi-smart/.github/actions/agent-gate@v1.
Check state before contents. A disabled_manually workflow produces no runs,
no logs and no failures. Every signal a person looks for is absent, which reads
exactly like "nothing needed doing".
gh api /repos/dodi-smart/<repo>/actions/workflows --jq '.workflows[]|"\(.state)\t\(.name)"'Otherwise: the pull request may change the workflow itself, see Versioning, or
agent:no-touch may be set, which is working as intended and is checked before
everything else, so nothing in the log will hint at it.
Self test runs on every pull request touching actions/, .github/workflows/
or the Renovate preset. It asserts the kill switch across every workflow shape,
checks the runner presets against the table above, parses every YAML file,
shellchecks the scripts, validates the Renovate preset, and runs actionlint.
Read AGENTS.md before changing anything. If you add a workflow, add its rule to
the table there with the one line that says why, and extend
actions/agent-gate/test.sh if it introduces a new gate shape.