Skip to content
opencharlyPublic

About

OpenCharly org-wide community-health defaults (PR template, etc.) — single source inherited by every repo without its own.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

237 Commits

Folders and files

Repository files navigation

opencharly/.github — org-wide community-health defaults

This repository is GitHub's org-level source of default community-health files. Any repository in the opencharly org that does not ship its own copy inherits the files here — so a change lands once, not in every repo.

What lives here

  • .github/PULL_REQUEST_TEMPLATE.md — the OpenCharly PR template. It elicits the exact evidence the fresh pr-validator validator needs: the change-class R10 gate + pasted output, whether the changed code path ran live (which caps the attribution tier), and a full R0–R10 + pillars "state HOW / N/A" checklist.
  • scripts/org-ruleset.sh — the SINGLE organization-wide owner of BOTH the required validate / validate check AND main-branch protection. On GitHub Team it creates ONE org ruleset carrying the workflows rule (pointing at this repo's required workflow) and the ordinary branch rules (required status check, no force-push, no deletion, no creation), applied to every active non-fork main-default repo. It also enforces the two settings that cannot move to the org (allow_auto_merge and delete_branch_on_merge, per-repo settings with no org default). apply enables the org ruleset, deletes the now-redundant per-repo rulesets, and enforces both settings; verify asserts the whole end state. Its offline mock-gh test is scripts/org-ruleset_test.sh.
  • scripts/retire-per-repo-dispatchers.sh + .github/workflows/retire-per-repo-dispatchers.yml — the one-shot cutover step that DELETES each repo's redundant .github/workflows/pr-validator.yml dispatcher stub. It runs as a workflow because a delete on a protected main needs a ruleset-bypass commit author — the charly-auto-merge App (the workflow mints that token). Run it BEFORE scripts/org-ruleset.sh apply, which refuses while any dispatcher survives, so the org required workflow is never a second producer of the required check. Idempotent (absent files skipped); its offline mock-gh test is scripts/retire-per-repo-dispatchers_test.sh.
  • .github/workflows/pr-validator.yml — the org-wide charly/pr-validator gate. A reusable workflow (on: workflow_call) that also self-gates this .github repo (on: pull_request). It runs a fresh, independent charly review (the plugin-review plugin, welded into the charly release) as the PR validator, always posts a single PR comment, and sets the required charly/pr-validator check from a deterministic Verdict: PASS|BLOCK (PASS → exit 0 — only for a review that exited 0: a non-zero review exit may carry only a real BLOCK finding, so any other verdict it wrote is discarded (fail-closed) — BLOCK → exit 1, no trustworthy verdict → INCONCLUSIVE + exit 3 — the required check stays RED, so a provider that never answered can neither pass unreviewed code nor be mistaken for a code finding — mixed verdict → exit 2).
  • .github/workflows/org-wide-pr-validator-required.yml — the org-level REQUIRED WORKFLOW the org ruleset names. GitHub runs it on every PR in every targeted repo (definition read once from this repo at the pinned ref); it calls the reusable pr-validator.yml@main above, so the gate logic stays one source, and it carries the per-PR concurrency dedupe + actions: write (the #163 constraint — see the file header). This file replacing the per-repo dispatcher is why no repo carries validator config of its own.
  • .github/workflows/candy-validate.yml — the ONE org-level candy/box manifest gate (on: workflow_call). It clones opencharly/charly at the org variable vars.CHARLY_VERSION — the single knob — builds the binary, and runs charly box validate, skipping cleanly (green) when a repo has no charly.yml. Called from a job named candy with this workflow's job validate, its check context is candy / validate. It replaces the class of ~376 hand-rolled per-repo .github/workflows/deploy.yml candy gates (298 frozen at v2026.238.1242), whose stale pins failed every schema-versioning-removal PR's OWN repo CI. The pin is mandatory (fail-loud; no bundled fallback). Activation as a required gate is deliberately deferred until vars.CHARLY_VERSION names a release carrying the schema-versioning removal: the pre-removal pin still requires the retired stamp, so a required gate at the current pin would re-break the very PRs this replaces.
  • scripts/retire-per-repo-candy-gates.sh + .github/workflows/retire-per-repo-candy-gates.yml — the one-shot org cutover that DELETES those stale hand-rolled stubs (the retire-per-repo-dispatchers.sh precedent). It runs as a workflow because a delete on a protected main needs a ruleset-bypass commit author — the charly-auto-merge App (the workflow mints that token). Safety rule: a deploy.yml is deleted ONLY when its parsed top-level name: is exactly candy; name: marketplace, name: docs, and every other real gate are SKIPPED, never touched (the parsed name, not the path, is the gate). Idempotent (absent files skipped); its offline mock-gh test is scripts/retire-per-repo-candy-gates_test.sh.
  • scripts/strip-config-version.sh + .github/workflows/strip-config-version.yml — the one-shot org cutover that DELETES the schema-versioning-removal cutover's retired authored version: stamp from EVERY charly.yml in every repo, ROOT OR NESTED (the git TREES API enumerates them; the v1 tool fetched only the root manifest, leaving 206 nested charly.yml files across 140 repos stamped — measured 2026-09-28; over the org's 423 active main-default repos, 396 still carry a version: line; 394 roots would be stripped). It runs as a workflow because a commit to a protected main needs a ruleset-bypass commit author — the charly-auto-merge App (the workflow mints that token), the same authorization the candy-gate retirement uses; charly.yml is not a workflow file, so only the App's contents: write is needed. Because no released charly carrying the removal is available yet (the host leg of the removal is not landed) and charly migrate reformats the whole file (~100 cosmetic lines of churn, incl. trailing-whitespace changes INSIDE description: block scalars), the strip is a MINIMAL-DIFF, parser-safe line filter (scripts/_strip-filter.py) that touches ONLY the stamp lines. Safety rule: the predicate is STRUCTURAL — a version: line is deleted ONLY when it is the indent-0 DOCUMENT stamp or a DIRECT child of a candy:/box:/deploy: entity body (indent 8, and indent 4 in the two 2-space manifests). Every other version: is preserved, in particular a LIVE distro: -> version: field (#Distro.version, e.g. charly/charly.yml's embedded default build vocabulary, asserted by charly's own distro_cascade_test.go — an indent-8 version: under distro:, which a bare-indent match would wrongly delete); a version: line indented 16+ is a fenced YAML example inside a description: | block scalar and is PRESERVED (9 repos — deleting it would corrupt documentation, e.g. layer-charly-hermes's version: 2026.156.1921 # mandatory CalVer). The filter additionally refuses LOUDLY if a matched line ever sits inside a block scalar. Idempotent (nothing to strip → never written); DRY_RUN=1 reports what WOULD change (every file, root + nested) without committing; its offline mock-gh test is scripts/strip-config-version_test.sh (which locks the real layer-charly-hermes charly.yml as a committed fixture in scripts/fixtures/).
  • .github/workflows/workflow-checks.yml checks the action runtime pins and executes the pinned Node runtime and artifact upload against GitHub. It also runs the existing owner-script policy tests and governance reconciliation. Validate changes to the review/landing path by running pr-validator.yml on the actual PR branch and checking the live GitHub state and complete job log.
  • Body-only fix after a push — MANUAL (the REST re-run). A REQUIRED workflow acts ONLY on the default push-driven pull_request types and IGNORES on.types — MEASURED and confirmed by the GitHub docs ("Troubleshooting rules": ruleset workflows ignore branches/paths/ types; they run only on the default activity types opened, synchronize, reopened), so a body edit does NOT fire it, even with edited listed. A corrected PR body is therefore cleared by explicitly re-running the EXISTING failed charly/pr-validator run: gh api -X POST repos/<owner>/<repo>/actions/runs/<run-id>/rerun — find the run with gh run list --repo <owner>/<repo> --json databaseId,headSha,conclusion. Not gh run rerun: this workflow is defined HERE, and gh (measured on 2.100.0) resolves a run's workflow against the TARGET repo, gets HTTP 404 … /actions/workflows/<id> and exits 1 without re-running anything (opencharly/layer-charly-internals#71). The re-run is a new attempt on the SAME GITHUB_SHA with a NEW validate / validate check run; the merge gate reads the newest check run per name, so a passing re-run supersedes the failure without an empty re-freeze commit (measured on opencharly/docs#142). There is no automatic rerun-label channel any more: the rerun label + scheduled sweep was RETIRED because a label added for any reason — including a comment — re-ran the gate without a body change.

The candy-manifest gate (ONE reusable, no per-repo copy)

The candy/box validate gate used to be a hand-rolled per-repo .github/workflows/deploy.yml (name: candy, job build) that CI-time-cloned opencharly/charly at a HAND-PINNED tag. ~376 repos carried a copy, 298 frozen at v2026.238.1242, 16 distinct charly pins (374 at an inline tag — 15 distinct tags — plus plugin-herdr/pod-herdr at a submodule gitlink v2026.251.0841), nothing advancing them — so the schema-versioning-removal cutover left every version-strip PR failing its OWN repo CI. It is now ONE org-level source:

  • .github/workflows/candy-validate.yml — the reusable (on: workflow_call) that clones charly at the org variable vars.CHARLY_VERSION (the single knob) and runs charly box validate, skipping cleanly when a repo has no charly.yml. It is called from a job named candy; this workflow's job is validate, so the check context is candy / validate (the name a per-repo caller's job produces). Advancing the gate is one gh variable set CHARLY_VERSION --org opencharly …, never a 376-repo sweep.
  • scripts/retire-per-repo-candy-gates.sh + .github/workflows/retire-per-repo-candy-gates.yml — the one-shot cutover that deletes the stale stubs. A deploy.yml is deleted ONLY when its parsed top-level name: is candy AND its body carries the charly box validate signature; name: marketplace, name: docs, and every other real gate are SKIPPED. Needs the same charly-auto-merge App with contents: write and workflows: write.
$ gh workflow run retire-per-repo-candy-gates.yml   # delete the per-repo stubs

Activation is deliberately deferred. The reusable is NOT yet named by the org ruleset as a required workflow. vars.CHARLY_VERSION currently names a release whose box validate still requires the retired version: stamp (measured against the live pin: schema 2026.261.1747 is required (found ""). Run: charly migrate), so making it required now would re-break the very PRs this replacement exists to unblock. Once the pin names a release carrying the schema-versioning removal (the host leg of that removal, whose first attempt charly#716 was auto-closed and is being carried forward by its successor), the reusable may be named org-wide exactly as the validator is (a workflows ruleset rule pointing at a thin candy-validate-required.yml caller, or per-repo callers). Until then it is available for explicit opt-in and the retirement of the stale pins stands on its own (the deadlock is removed whether or not a new gate is required).

Future org-wide defaults (issue templates, CONTRIBUTING.md, SECURITY.md) belong here too — one source, inherited everywhere.

How the gate is installed in an org repo

It is not. The gate is a single source in this repo — the ONE org ruleset (scripts/org-ruleset.sh) names the required workflow and requires the validate / validate check for every repo, so no repo installs anything. The old per-repo .github/workflows/pr-validator.yml dispatcher stub existed only because org required-workflows need GitHub Team (the org was on the free plan when that pattern began); the org ruleset now replaces it, and .github/workflows/retire-per-repo-dispatchers.yml deletes the stub in every repo. The one-time cutover order is:

$ gh workflow run retire-per-repo-dispatchers.yml   # delete the per-repo stubs first
$ scripts/org-ruleset.sh apply                      # then enable the org ruleset
$ scripts/org-ruleset.sh verify                     # assert the whole end state

The gate runs on SAME-REPO PRs, whatever the repo is

The required workflow's validate job gates on github.event.pull_request.head.repo.full_name == github.repository — the head repo is a different repository than the base. It runs for every same-repo PR, including PRs in a first-party fork repo (the org vendors several upstreams as forks: dsh-github, crabbox, the pi-* family), and skips only a genuine external contribution (a head repo that is a different repo — whose fork secrets are unavailable and whose code is untrusted). A null head repo (a deleted fork) resolves to the empty string, which is != github.repository, so the job skips — the safe default.

This predicate is load-bearing. The retired head.repo.fork == false test asked "is the head repo itself a fork?", which is true for every PR in a fork repo, so it skipped the org's own PRs. Combined with the ruleset's REQUIRED validate / validate check, that made such a PR un-mergeable — the required check was never produced (measured 2026-10-07, opencharly/dsh-github#2: dsh-github main carries the ruleset while every pre-existing fork does not, and gh pr merge reports Required status check "validate / validate" is expected). The predicate is checked by the same_repo_gate row in scripts/governance-claims.tsv.

Consequence for a new fork repo. Forks are excluded from the ruleset by design (discover_excludes filters on isFork == true), so they carry no required check and their same-repo PRs merge without an org verdict. The hazard is a fork created after the last scripts/org-ruleset.sh apply: it is absent from the stale exclude list, so the ruleset's ~ALL-minus-exclude scope still lists it and its same-repo PRs get the REQUIRED check. With the retired predicate that check was never produced (deadlock); with this predicate it runs and the PR merges on PASS. The next apply re-derives the exclude set and drops the fork back out of the ruleset target.

New repos: bootstrap the initial main

The org ruleset carries the creation rule on refs/heads/main, so a brand-new repo cannot create its first main by any operator path — git push, the contents API, repo auto_init, and branch rename are all rejected (measured: GH013 Cannot create ref due to creations being restricted on a push; the contents API returns 409 Cannot create ref due to creations being restricted on a repo with no refs and 404 Branch main not found on a repo with other refs but no main; rename 422 repository rules do not permit renaming branch ... to 'main'). The ruleset's only bypass actor is the charly-auto-merge App — and without a main the required workflow cannot run, so the required check can never be produced (a deadlock).

Bootstrap a new repo's main with the App-token workflow:

$ gh workflow run bootstrap-repo-main.yml -f repos="my-new-repo"   # in opencharly/.github
$ gh api --method PATCH repos/opencharly/my-new-repo -f default_branch=main

scripts/bootstrap-repo-main.sh (run by .github/workflows/bootstrap-repo-main.yml) creates refs/heads/main from the repo's current default-branch HEAD via the git-data refs API — the API with no branch-must-exist precondition (the contents API requires the branch to already exist). It never moves an existing main and skips a repo with nothing to seed from, so it is safe and idempotent. The ref creation needs only the App's contents: write bypass; setting the repo's default branch is a repo-settings change the App cannot make (403 Resource not accessible by integration), so that final step uses the operator's admin token.

Setting up a new repository — what is automatic, what you add

The org ruleset targets every active, non-fork, main-default repo (discover_repos in scripts/lib-org.sh), so most of the gate is inherited with nothing copied into the repo:

Concern Automatic (the org / this repo) The repo must add
PR validation (validate / validate) The org ruleset's workflows rule runs org-wide-pr-validator-required.yml (definition here) → the reusable pr-validator.yml. No per-repo file. nothing
Branch protection (no force-push / delete / create; strict required check) The ONE org ruleset. nothing
Native auto-merge (squash) Armed by the validator on PASS; needs the per-repo setting allow_auto_merge=true (enforced by scripts/org-ruleset.sh apply). nothing (the script sets it)
Remote branch cleanup at merge delete_branch_on_merge=true (enforced by the same script). nothing
CalVer tag + CHANGELOG/<CalVer>.md The org reusable tag-on-merge.yml. .github/workflows/tag-on-merge.yml — the per-repo caller (fires on workflow_run of charly/pr-validator + push to main)
Candy/box manifest validation (candy / validate) The org reusable candy-validate.yml (clones charly at vars.CHARLY_VERSION; skips repos with no charly.yml). No per-repo file. nothing (an optional thin caller if a repo wants the check to run outside the org path)
The repo's own build/deploy CI (a real gate) — the repo's .github/workflows/ (e.g. deploy.yml where it is NOT the hand-rolled candy stub)

Checklist for a brand-new repo:

  1. Create opencharly/<name> (non-fork). Bootstrap main with bootstrap-repo-main.yml, then set the default branch (above).
  2. Add .github/workflows/tag-on-merge.yml (copy an existing repo's caller) — WITHOUT it the repo merges via the org validator but never gets a CalVer tag or a CHANGELOG. Add the repo's own CI (deploy.yml) too.
  3. Run scripts/org-ruleset.sh apply so allow_auto_merge and delete_branch_on_merge are enforced for the new repo (the org ruleset itself covers it automatically once it is active + non-fork + main-default).
  4. Land the first change via a PR; the org validator is the gate (never a direct push).
  5. Add it to the umbrella as a submodule and bump the pin via charly task sync + PR.

Excluded by design (no org workflow, no CalVer tags): forks (gst-wayland-display), archived repos (pi-review-action), and non-main defaults (pixelflux → av1, omarchy-eval-artifacts → runs). A repo in this set that should be tagged/validated must first become non-fork / active / main-default.

Measured gap. layer-nerdctl and plugin-nerdctl were created with no .github directory at all (so no tag-on-merge.yml caller): they validate via the org ruleset, but the tag-on-merge mechanism cannot have minted their two v2026.266.* tags (the caller it fires through is absent), so those tags are out-of-band and future merges will not tag. Adding the caller is the fix.

Required org-level configuration

Nothing is hardcoded and no credential is committed. The workflow reads provider/model/endpoint/key from the GitHub environment and passes them to the charly review step as the AI_REVIEW_* env (provider / model / base_url / api_key). Set these as org-level variables/secret (Settings → Secrets and variables → Actions → New repository secret / New variable, org level, visibility: all):

Name Kind Default Purpose
AI_REVIEW_PROVIDER variable ollama LLM provider name
AI_REVIEW_BASE_URL variable https://ollama.com/v1 Provider base URL override (empty = built-in)
AI_REVIEW_MODEL variable deepseek-v4.1-flash Exact model ID in the provider's catalog
AI_REVIEW_API_KEY secret — Provider API key (never committed)

The model id is passed verbatim to the provider's chat-completions endpoint (deepseek-v4.1-flash is an Ollama Cloud model id, served at https://ollama.com/v1 — the same catalog as the local ollama deepseek-v4.1-flash:cloud pointer); base_url selects the endpoint; no models.json is written and no model catalog is embedded. The gate is charly-native: plugin-review is welded into the charly release, driven by charly review --plan review-plan.yml (plan + prompt live in opencharly/action-review@main — the validator spec's single config source, updated without touching the workflow). The gate-mechanism version surface is the pinned charly release (vars.CHARLY_VERSION) — a mandatory org variable: the workflow has no bundled fallback. If the pin is unset or set with visibility narrower than all (invisible to this public repo), the validator fails loudly (exit 3, the INCONCLUSIVE class) with the recovery command rather than silently downgrading to an older engine. Set it with:

gh variable set CHARLY_VERSION --org opencharly --body <tag> --visibility all

Bump it deliberately per release.

Scope & evidence baseline (honest capability statement)

This gate is a static diff + thread review run by a fresh independent validator (charly review, the plugin-review plugin welded into the charly release). It runs read-only GitHub tools — get_pr_diff, get_pr_commits, get_pr_thread (the CURRENT live body is authoritative + prior comments), get_pr_meta — and no shell. For every claim it verifies it either (a) derives it from the diff/commits/thread, or (b) cross-checks the author's pasted evidence for internal consistency and states an explicit tool-limited disposition ("could not re-run from this environment") where independent re-execution would be required. It never fabricates a run and never lets a missing re-run pass on the author's word alone.

Consequence for runtime / Go / schema classes: the gate validates pasted bed/regen/lint output statically — it cannot independently re-run it. Deep independent re-execution of runtime-class evidence remains the full shell-enabled fresh-evaluator agent's job in the charly repo; this gate is the org-wide first line. Authors must paste complete, self-consistent, fraud-free evidence. No more is overclaimed.

Self-install note

On the PR that first installs this gate, the gate's end-to-end green check is not observable before the gate itself merges (a self-install). The validator guidance accordingly never treats that as a blocking finding: it verifies the mechanism statically, accepts an explicit operator sign-off, and passes unless a genuinely fixable, non-self-blocking defect remains. This is the only instance where the gate's own check is not independently green.

Bot-token rename push (org secrets)

Every PR must carry a CHANGELOG placeholder renamed to the merge-time CalVer. The validator performs that rename with a bot token, not GITHUB_TOKEN: GitHub requires approval for runs triggered by GITHUB_TOKEN pushes, and no event exists that can auto-approve them. A bot-token push re-triggers pull_request: synchronize normally.

Setup (already done org-wide):

  • GitHub App charly-auto-merge (id 4675576), installed on opencharly with All-repositories access. It is ALSO the ruleset bypass actor, so it additionally needs the workflows: write permission (Repository permissions → Workflows: Read and write) for the org cutover's dispatcher deletions (.github/workflows/*), on top of contents: write.
  • Org secrets CHARLY_AUTO_MERGE_APP_ID + CHARLY_AUTO_MERGE_PRIVATE_KEY. Fallback: CHARLY_BOT_TOKEN (fine-grained PAT, Contents: write + Workflows: write).

The rename step hard-fails when neither is configured, so a missing secret never silently degrades to the approval-required loop.

The org-wide required workflow (ONE config, no per-repo copy)

Branch protection AND the required validator used to be applied per repo (a branch ruleset + a copied .github/workflows/pr-validator.yml dispatcher). On GitHub Team they are now ONE organization ruleset (scripts/org-ruleset.sh), which carries both the workflows rule (naming org-wide-pr-validator-required.yml here) and the branch rules (strict required validate / validate, no force-push/deletion/create). One-time cutover order: gh workflow run retire-per-repo-dispatchers.yml, then scripts/org-ruleset.sh apply, then scripts/org-ruleset.sh verify.

About

OpenCharly org-wide community-health defaults (PR template, etc.) — single source inherited by every repo without its own.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages