The organization's defaults, in the one repository GitHub reads them from. It is public because GitHub only applies
default community health files and issue and pull-request templates from a public .github repository. Nothing private lives here.
| Path | What it does |
|---|---|
profile/README.md |
The organization's public profile page |
CONTRIBUTING.md, SECURITY.md, CODE_OF_CONDUCT.md, SUPPORT.md |
Defaults for every repository that has none of its own |
.github/ISSUE_TEMPLATE/, .github/pull_request_template.md |
The default issue forms (bug, idea, task; each sets the organization's issue type Bug, Feature or Task) and the pull-request template (its Fixes # line closes the issue on merge). A repository with its own ISSUE_TEMPLATE/ gets none of these |
.github/workflows/package-ci.yml |
The gate every workflow package calls from its ci.yml: zip matches tree, suite from the repository, suite from the extracted zip (ComfyUI Base's verify.sh), then the private cast guard |
.github/workflows/package-release.yml |
Called on a v<version> tag: checks the tag against PKG_VERSION, re-runs the gate with the cast guard required, and attaches <name>-runpod.zip to a GitHub release |
.github/workflows/package-gpu.yml |
By hand (workflow_dispatch in a package's gpu.yml): the suite's pod-only tiers (runpod, gpu) on the pod itself, which is a free ephemeral self-hosted runner labelled gpu-pod while it is up (ComfyUI Base podctl runner <pod>). It installs and restarts nothing |
.github/workflows/gitleaks.yml |
Secret scanning for every repository: gitleaks over the whole history, failing on a finding. package-ci.yml calls it for the packages; the others call it from their own gitleaks.yml |
.github/workflows/gitleaks-release.yml |
Each morning: a new gitleaks release becomes a pull request moving gitleaks.yml's VERSION and both SHA-256s (.github/scripts/gitleaks_release.py), which merges itself once gitleaks passes on it. Opened by the soulcraft-ci app |
workflow-templates/ |
"New workflow" templates for a new package repository |
The packages call the reusable workflows at @main: one maintainer, and a change here is a pull request whose own
check is the next package run. When a second consumer appears, tag v1 and move the callers to it, so a breaking
change ships as v2 one package at a time.
Every action these workflows use is pinned to a full commit SHA, with its version as a comment beside it
(GitHub's guidance: a SHA is the only reference that cannot be moved). Dependabot moves the pins in one grouped
pull request each weekday, which merges itself once gitleaks passes here.
Secrets the workflows read (organization secrets, set by workspace/org/apply.sh): CI_APP_ID and CI_APP_KEY, the
soulcraft-ci GitHub App that lets a package's CI read the private workspace's cast tool, and opens
gitleaks-release.yml's pull requests here (Contents, Pull requests and Workflows write on this repository); ROUTER_APP_KEY
(with the variable ROUTER_CLIENT_ID), the soulcraft-router app of the CI router below. The organization
variable COMFY_BASE_REPO says where ComfyUI Base lives (default SoulCraft-Org/ComfyUI-Base). Where the jobs run
depends on the repository, not on a variable (Paul, 2026-10-26): a private repository's jobs always run on the
organization's self-hosted runners (label soulcraft-hf, or soulcraft-free while CI_POOL is on; runner group
hf-free; SoulCraft-Org/workspace ci/hf-runner/) and never on GitHub-hosted minutes; a public repository uses
GitHub's ubuntu-24.04, which costs it nothing (the runner group refuses public repositories). The old switch
CI_RUNNER (hf) no longer decides this.
What runs when, so testing costs nothing (2026-10-16): tier 0 is ComfyUI Base's _build/quick.sh <pkg>, seconds,
run by whoever makes a change; tier 1 is package-ci.yml on each pull request (not again on the merge to main:
the pull request already ran it) and on each release tag, on the free self-hosted runner;
tier 2 is package-gpu.yml, by hand, on the pod.
GitHub has no fallback between runner labels (a job's runs-on labels must all match one runner), so a job aimed at
the free self-hosted pool waits whenever every slot is busy, even while free hosted minutes sit unused. With the router
on, package-ci.yml runs a route job before verify: it reads the organization's runners and sends verify to the
free pool (soulcraft-hf, or soulcraft-free while CI_POOL is on) when a slot is idle. Only when none is idle,
and overflow is on, it sends verify to the first overflow label that has used less than 90% of its free minutes this
month; with none left, verify waits for a slot as before. The required check stays verify / verify; route is an
extra check beside it. The run's summary and the route job's log say why it picked what it picked (route: ["blacksmith-2vcpu-ubuntu-2404-arm"]: 0 of 2 online soulcraft-hf slots idle (reserve 0); overflow to blacksmith-2vcpu-ubuntu-2404-arm (12 of its 4800 free minutes used in 2026-10)).
The minute count is GitHub's own job history, not a counter anyone writes: every job of this month's completed runs
that called package-ci.yml, summed per overflow label, each rounded up to a whole minute (how GitHub and Blacksmith
bill), re-runs included. Each run is read once and cached in the router process's tool folder, so a router reads only
the runs that finished since its last look; a freshly started router process reads up to 1,500 runs per route, 8 at a
time (October 2026's whole month took 94 seconds), and sends nothing to overflow until it has. Runs still in progress are not counted yet; the 10% headroom covers
them. The count only sees routed workflows: GitHub's minutes are shared with every GitHub-hosted job of the private
repositories (Dependabot's included), so in a month whose minutes are already spent elsewhere, lower that label's
number in CI_OVERFLOW_LABELS or leave it out (October 2026: the private repositories used 3,179 hosted minutes by the
24th, all of the Team plan's 3,000).
The job runs on its own light runner processes, label soulcraft-router (one per Hugging Face Space, beside the
runner processes; SoulCraft-Org/workspace ci/hf-runner/), never GitHub-hosted: a hosted job is billed a whole minute,
and the router runs on every pull request. While no soulcraft-router process is online, a routed run's route job
waits: turn CI_ROUTER off and re-run it.
The router never fails a run: no token (the app not set up yet, or a Dependabot run, which gets no organization secrets), an API error or unreadable data all route to the free pool.
| Variable | Value | Effect |
|---|---|---|
CI_ROUTER |
on |
the route job runs, in private repositories. Unset: today's behaviour, route is skipped and verify picks its runner as before |
CI_OVERFLOW_ENABLED |
true |
the kill switch: overflow allowed when no slot is idle. Anything else, or unset: never overflow (wait for a slot) |
CI_OVERFLOW_LABELS |
<runs-on label>=<free minutes a month> ... in order |
where overflow goes, first choice first. blacksmith-2vcpu-ubuntu-2404-arm=4800 (Blacksmith's app is installed); later Ubicloud at the end. Never list a GitHub-hosted label (ubuntu-latest, ubuntu-24.04): the private repositories draw no GitHub-hosted minutes (Paul, 2026-10-26). Never list a label no runner serves: a job waits 24 hours for it |
CI_ROUTER_RESERVE |
a number, default 0 |
overflow while this many slots or fewer are idle, keeping them for jobs the router does not route (gitleaks, release tags, other workflows). 0 = overflow only when every slot is busy |
On (the router, then overflow to Blacksmith's free minutes):
gh variable set CI_OVERFLOW_LABELS --org SoulCraft-Org --visibility private --body "blacksmith-2vcpu-ubuntu-2404-arm=4800"
gh variable set CI_OVERFLOW_ENABLED --org SoulCraft-Org --visibility private --body true
gh variable set CI_ROUTER --org SoulCraft-Org --visibility private --body on
Overflow off, the router still on (every job waits for a free slot, as before): gh variable set CI_OVERFLOW_ENABLED --org SoulCraft-Org --visibility private --body false. Everything off: gh variable delete CI_ROUTER --org SoulCraft-Org. A change applies to runs that start after it.
- The app. https://github.com/organizations/SoulCraft-Org/settings/apps/new : name
soulcraft-router, homepagehttps://github.com/SoulCraft-Org/.github, Webhook Active off, Permissions: Organization Self-hosted runners: Read-only, Repository Actions: Read-only (Metadata: Read-only comes with it), nothing else; "Only on this account"; Create. Then Install App, SoulCraft-Org, All repositories. - Its Client ID (on the app's page) as the organization variable
ROUTER_CLIENT_ID(visibility Private repositories; an id, not a secret). - Its key. Generate a private key and push it as the organization secret
ROUTER_APP_KEY(visibility Private repositories) with the API keys session (Soul-Craft/api-keys, local~/API-Keys). Nobody else handles it. - Check:
gh api orgs/SoulCraft-Org/installations --jq '.installations[] | select(.app_slug == "soulcraft-router") | .permissions'printsactions: read,metadata: readandorganization_self_hosted_runners: read. - Blacksmith (for its 4,800 free arm64 minutes, no card): install its GitHub App on SoulCraft-Org, then put its
label first in
CI_OVERFLOW_LABELS. The gate is proven on arm64 (Oracle); the first Blacksmith run is its test.
Why a third app, read-only: GITHUB_TOKEN has no organization permissions, so it cannot list the organization's
runners. soulcraft-ci (CI_APP_KEY) stays as it is. ROUTER_APP_KEY is an organization secret of the private
repositories, read only by route on the router's processes, and it can only read: the runner list and the
repositories' Actions history. No write anywhere, so no job can turn the switches above (which also steer what code CI
clones, such as COMFY_BASE_REPO).
Why no concurrency group to serialize the router: GitHub scopes those to one repository, so they cannot order routers
across the organization. Two routers deciding at the same moment can both pick the last idle slot; the cost is that one
verify waits for a slot, as without the router, never minutes.