Skip to content

Repository files navigation

.github

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.

Quick start

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: inherit

That 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.

What a caller controls

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.

Secrets the caller needs

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.

The workflows

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.

Release, then deploy

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.

Inputs

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

Secrets

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.

Composite actions

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.

What you stop maintaining

Adopting a workflow here removes a class of problem rather than a file.

  • Runner selection. Ask for light or heavy and 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-stack decides 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.

The badge that brought you here

A repo wired up to these workflows carries this badge in its README:

shared workflows v1

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.

Two things you can rely on

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 requests, fields are state

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.

Stacks

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.

Design lint

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=github

Warnings are advisory. --max-warnings N in the caller's own command is the ratchet -- tighten it there as the count comes down.

One job or three

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.xml

Either 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.

Docs-only changes, and the one context to require

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.

Caches

setup-stack resolves the mode from isolate, cache and the runner:

  • Verification jobs isolate. deps-verify pins caches to RUNNER_TEMP with 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-cache is 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 never restore-keys on 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

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: true

Runners

pick-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.

Versioning

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 v1

One 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.

Shared semantic-release config

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@v1

actions/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.

Renovate

{ 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.

Issue templates

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.

Why .github/.github/

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.

When nothing happens

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.

Contributing

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.

About

Org-wide GitHub defaults for dodi-smart: reusable workflows, composite actions, inherited issue templates, and the shared Renovate preset.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages