Skip to content

git-workflow

Recipe card from the charly-internals plugin (Development — contributor internals).

This card has additional detail pages:

git-workflow — branch-per-change, PR-only org-workflow-validated landing

Section titled “git-workflow — branch-per-change, PR-only org-workflow-validated landing”

Every change to an OpenCharly repo lands through ONE discipline: a pull request gated by the ORG-WIDE charly/pr-validator GitHub Actions workflow (opencharly/.github), which validates and, on a PASS verdict, enables GitHub’s OWN native auto-merge (squash) inline — there is no separate auto-merge workflow; the CalVer tag and the CHANGELOG are written afterwards by the independent tag-on-merge workflow, triggered by the merge. A direct push to main is FORBIDDEN and mechanically disabled — a per-repo branch RULESET blocks it (creation/deletion/non_fast_forward + a required validate / validate status check); the legacy branch-protection API is NOT used and branch-protection.sh actively removes it, because it has no bypass slot for the app that writes the CHANGELOG. The pre-push-gate adds a local backstop in every harness that wires it — including Claude Code, via .claude/settings.json’s PreToolUse hooks. The R10 pass authorizes OPENING the PR, never a self-merge: the two-step landing separates the author (who opens the PR) from the fresh validator (a check run on the same PR). This skill is the mechanics; the project rulebook “Post-Execution Policies” (AGENTS.md / CLAUDE.md) carries the mandate, /charly-internals:cutover-policy the one-phase rule, /charly-build:migrate the schema-version/tag coupling, and the marketplace’s internals/agents/pr-validator.md the validator’s own spec.

Validator re-trigger + PR-body evidence mechanics (FIXED at the workflow level, #38 wave)

Section titled “Validator re-trigger + PR-body evidence mechanics (FIXED at the workflow level, #38 wave)”
  • A body-only fix needs a NEW commit, and an empty commit IS such a commit. The org validator reviews the diff at the PR’s head SHA and keys its verdict to that head, so editing only the body leaves the head unchanged and the review stale. Append a commit — git commit --allow-empty -m "docs: re-freeze the PR body against the final head" is the standard append-only move (never force) — and the push fires a fresh pull_request (synchronize) run on the NEW head: measured on opencharly/plugin-pipeline#28 and opencharly/plugin-vm#35, an empty commit produced a fresh charly/pr-validator ev=pull_request run keyed to the new SHA. gh workflow run pr-validator.yml -f pr-number=<N> --ref <branch> is the alternative, but it re-dispatches on the SAME head: GitHub’s rollup keeps BOTH check runs for that name, and a stale earlier failure of the same name can keep the PR BLOCKED even after the dispatched run is green — a fresh SHA from an empty commit is the reliable path. (The “empty commit does nothing” belief is WRONG: the dispatcher on: pull_request: types: [opened, synchronize, …] has no paths:/diff guard, so a no-content push still fires it.)
  • A workflow_dispatch without --ref runs on the DEFAULT branch and its check registers there — the reusable workflow now re-dispatches itself on the PR head ref (self-heal), so the green check always lands on the branch head.
  • The workflow now dedupes + self-heals (opencharly/.github pr-validator): a per-PR concurrency group (cancel-in-progress: true) cancels the in-flight run on re-dispatch/push, so duplicate same-name check runs can no longer poison mergeability; a workflow_dispatch re-dispatches itself on the PR head ref so the green check lands on the head.
  • The PR body must match a FROZEN head. Commit SHAs and diff-stats in the body are mutable until the branch stops moving; a rebase/amend after writing the body guarantees a Rule-4 body-truthfulness BLOCK. Freeze the branch, write the body against the final head (real git log --oneline origin/main...HEAD + git diff --stat output), push ONCE, and never touch the branch again until merge. Any required fix = ONE atomic batch: commit → compute the new head → rewrite the body → push.
  • gh_pr_status mode:'watch' may act as a one-shot in some environments — use a bounded check poll loop as the fallback (terminal verdicts only, never loop on a BLOCK).
  • No direct push to main (the project rulebook’s PR-only landing mandate — see “Post-Execution Policies”). Enforced by a per-repo branch RULESET on refs/heads/maincreation + deletion + non_fast_forward + a strict required status check named exactly validate / validate, with the charly-auto-merge GitHub App as the only bypass actor (its scoped bypass is what lets tag-on-merge’s CHANGELOG commit land on a protected main). The LEGACY branch-protection API is deliberately NOT used: it has no bypass slot for that app, so branch-protection.sh deletes it wherever it survives, and enforce_admins plays no part. The pre-push-gate adds a local backstop in every harness that wires it — Claude Code included, via .claude/settings.json’s PreToolUse hooks. Organization-wide apply/verify is owned only by opencharly/.github/scripts/branch-protection.sh.

  • Never force-push, on any branch, ever (mandate, same rulebook section). main only fast-forwards via native auto-merge’s squash; a feat/ branch, once pushed, advances only by ADDING commits (the author’s change plus any review-round fix commits), and the squash-merge collapses them. A stale feat/ catches up with gh pr update-branch (a merge, NOT a rebase-force); tags are add-only. Amending a feat/ branch is a normal authoring action — legal until the first push (amending a pushed branch would require a force-push, which is forbidden).

  • R10-gated; the merge requires the charly/pr-validator gate’s green check run. R10 PASS authorizes opening the PR (with pasted evidence); a rule violation or R10 FAIL means a red charly/pr-validator check and no merge — fix in the same tree, re-run R10, re-push; the check resets and the validator re-runs.

  • Zero warnings is part of R10 (project rulebook R1). A version-mismatch warning clears with charly box reconcile; any other warning gets /charly-internals:root-cause-analyzer then a real fix — “warning” is never an accepted end state.

  • Atomic on main, never on feat/. The org-wide charly/pr-validator workflow’s PASS enables GitHub native auto-merge (squash), which folds the author’s change and any review-round fix commits into one commit on main; the feat/ branch may freely accumulate fix commits across review rounds. The merge-time CalVer tag and the CHANGELOG/<CalVer>.md entry (written from the PR body — the PR body IS the changelog) are created after merge by the org-wide tag-on-merge workflow (see “CalVer” in references/validator-and-calver.md). Two separate cutovers must never share one PR.

  • Update the PR; never close-and-recreate (except for work that will not land at all — a disproven premise, an abandoned approach). When a review demands changes, append a commit and push it fast-forward — the check resets and the validator re-runs. This is what makes the no-force-push rule livable: because main gets a squash, a branch carrying five fix commits still lands as one.

  • Validator re-review is per-HEAD; body-only edits do not re-trigger it. The org validator reviews the diff at the PR’s head SHA and keys its verdict comment to that head. Editing only the PR body leaves the head unchanged, so a fix that is purely evidentiary (pasting real output, correcting an accounting line) needs a NEW commit to re-validate — git commit --allow-empty with a docs message is the standard append-only re-trigger (never force; see the “Validator re-trigger” bullet above for why an empty commit works and a dispatch does not). Corollary: a PR whose diff is EMPTY because the base already contains the change (you branched from a stale snapshot) is a no-op — close it rather than re-pushing (the validator flags it as body-truthfulness violation: body describes files the diff does not carry). Always git fetch origin main + diff against CURRENT main before opening or finalizing a PR.

  • A dispatched workflow defaults to the DEFAULT BRANCH. gh workflow run <wf> --ref <branch> is required to dispatch CI on a PR head — a dispatch that lands on main produces no check-run counts toward that PR’s required checks (and workflow_dispatch with no --ref silently uses the default branch). For run-on-head diagnostics: target the PR’s branch explicitly.

  • Tree-safety before destructive actions (R6). Check git status + git stash list before any destructive working-tree action — git stash discards in-progress work; rm on a tracked file is destructive. When the sandbox blocks an action, find a non-destructive alternative rather than working around it. The stash/pop cycle can itself silently un-stage a git rm: a stash taken while a deletion is staged restores the deletion as unstaged on pop, so a git status right after the cycle that shows the deleted file back as a plain unstaged change (rather than the staged deletion you left) has quietly lost the staging — re-stage it (git rm <path> again, or git add -u) before committing. A stash/pop round-trip is never a no-op on a mixed add+rm working tree.

  • Right worktree — pin one absolute path for the whole edit→commit→push sequence. Before branching, staging, or committing, confirm the worktree you are driving is the same one your edits landed in: git -C <path> rev-parse --show-toplevel must equal the path you edited, and git -C <path> status --short must list those edits. Under symlinked or near-twin sibling worktrees — a parent dir that is itself a symlink (~/projects~/Sync/projects), or look-alike names such as …/charly vs …/<other-worktree>cd-ing to the wrong sibling makes git switch -c + git commit run against a clean tree and report “nothing to commit”, silently landing nothing (or landing in the wrong repo). Never change the path spelling mid-sequence. An unexpected “nothing to commit” right after editing a file is the signature of this mistake — stop and re-verify --show-toplevel before retrying (blind retry is an R1 violation).

  • The universal PR-gate — audit before any PR action, unconditionally. Before opening, updating, or merging any pull request, run the aggregate audit: gh pr list across every touched repo + git worktree list + the live teammate/agent roster. This is a standing preflight, run first every time — never reached for only once something already looks off — because skipping it risks a duplicate PR for scope already covered in flight, a branch update from a stale worktree, or a merge over a still-running validator’s verdict. Full operational detail: /charly-internals:agents “The universal PR-gate”.

  • Post-commit staging verification. After every commit, re-run git status --short (expect it empty, or only unrelated untracked paths) and git show --stat (confirm every intended file is actually listed) — a multi-path git add naming several paths where one is mistyped or stale can commit only the files that did resolve while git commit still succeeds, producing a commit that would not even compile. git show --stat catches that AFTER the fact, and it is the only post-commit check ON THE COMMITTING SIDE that sees a commit whose message describes an intent its diff does not carry — no gate that reads the tree can, because the mismatch is between the message and the tree, and the tree does not hold the message. It is NOT the only thing that can catch the class: any reader holding message and diff together — a review, a cross-repo reference sweep — catches it too, and catches the cases --stat cannot, such as a mismatch that spans two repositories.

  • Chain the edit to the commit so the failure cannot reach one. A script that edits and the git add / git commit that follow are SEPARATE statements: a guard that aborts the edit does not stop the commit, which then lands under a message describing an edit it does not contain. Joining them — edit && git add … && git commit … — makes a failed edit unable to produce a commit at all. The defect occurred three times in one session; the chain was exercised ONCE, and that occurrence left no artifact by construction — so three is the evidence for the PROBLEM and one is the evidence for the REMEDY. Do not report them under a single “measured”. The chain is necessary, not sufficient: && short-circuits on a non-zero exit, so an edit that fails LOUDLY cannot reach the commit, but a SILENT no-op edit — a pattern that matches nothing — exits 0 and lands the commit under a describing message exactly as before. That case still needs the post-commit read above; the chain closes only the aborting one. A rule held in a head is exactly as effective as a rule not held, unless something in the command line enforces it.

  • git add is all-or-nothing: never name a path that no longer exists. A single git add whose pathspec includes a vanished path fails the WHOLE add — fatal: pathspec '<old>' did not match any files, exit 128, nothing staged, including the paths that did resolve. Two shapes hit this, and both are silent because a PRIOR command already staged something, so the failed add leaves git status looking exactly as intended:

    • a deletion, where the removed file’s path is still named in the add (the deletion was already staged) — lands a partial commit carrying only the deletion;
    • a rename plus a content edit, where git mv old new is followed by git add old new (the mv already staged the rename) — lands the rename with the content edit dropped.

    Stage a rename+edit as git add <new> alone, and a deletion by DIRECTORY or with git add -u; never name the vanished path. This shape defeats the post-commit check above, which is why it needs stating separately: git show --stat DOES list the file, as old.md => new.md | 0, so “every intended file is listed” passes. Presence is not the signal — the tells are the magnitude (| 0 insertions on a commit meant to change content) and a leftover M <new> in git status afterwards. Read the numbers and the post-commit status, not just the filenames, and confirm content directly with git show HEAD:<new-path> | head -1 whenever the edit is the point of the commit.

  • Check-coverage is part of R10. The change must ship the test coverage that proves its functionality (check: checks for new/changed layers & images, Go tests for charly code) AND the live run must have exercised it. A change whose new functionality has no test that would fail without it is not landable.

  • Every repo is tagged at merge. The superproject, every box/<distro>, plugins, and docs all mint v<YYYY.DDD.HHMM> on their own merged HEAD — the tag marks the MERGE, decoupled from any charly.yml version: schema field, so a repo needs no charly.yml to be tagged. The sole exception is the sdk contract repo, which tags under its own Go-module scheme v0.<YYYYDDD>.<HHMM with all leading zeros stripped> (B2 step 0) — not an exemption but a hard Go-module requirement: v<YYYY.DDD.HHMM> is not a valid Go module version (semver forbids a leading-zero segment — 0733733 — and a major ≥ 2 would force a /vN module-path suffix that breaks every import github.com/opencharly/sdk), so the stripped v0.<…> form is mandatory, not a choice. A skipped tag is a defect, not an exemption: the orchestrator verifies the tag landed after each merge (git ls-remote --tags origin v<VER> non-empty) and, if tag-on-merge skipped it, backfills it add-only on the merged HEAD (git tag -a v<VER> -m "<subject>" <merged-HEAD> + git push origin refs/tags/v<VER>) — tags are immutable, so a backfill only adds one, never moves an existing tag.

Topic File
B1 (the two-step branch-per-change loop, concurrent landings, the cross-repo WIP landing sequence) and B4 (sync to upstream + prune) references/branch-and-pr-loop.md
B2 (multi-repo/multi-worktree coordination, per-module verification), B3 (agent teams in per-teammate worktrees), B6 (cross-repo @github landing), and B7 (multi-worktree landing + refresh, the canonical end-to-end) references/multi-repo-coordination.md
B5 (the fresh evaluator + fork/PR path, the two-gate autonomous-landing model), CalVer generation, post-landing cleanliness + report format, and the validation-FAILS recovery sequence references/validator-and-calver.md
Evidence discipline — provenance vs plausibility of a pasted gate, the three freshness surfaces (head / body / pasted output), positive-vs-negative claim decay, sweeping for claims a fix invalidated, the merged-tree gate for a BEHIND PR, source-and-regeneration as one cross-repo cutover, submodule pointers reverted by a non-conflicting merge, and why status-absence on a known head proves nothing references/evidence-and-freshness.md
  • the project rulebook “Post-Execution Policies” — the mandate this skill operationalizes.
  • marketplace/internals/agents/pr-validator.md — the fresh evaluator’s full spec.
  • opencharly/.github/scripts/branch-protection.sh — the sole organization-wide branch-protection apply/verify owner.
  • /charly-internals:cutover-policy — one-phase, atomic-commit, R10-at-the-end.
  • /charly-build:migrateversion: ↔ tag coupling, per-merge tags, push order.
  • /charly-build:reconcile — cross-repo @github pin alignment used by B6.
  • /charly-check:check — the check-coverage gate (R10) every change must satisfy.
  • /charly-internals:root-cause-analyzer — run on any FAIL before re-trying.

Invoke before any git / gh action that commits, branches, pushes, opens a PR, or drives the pr-validator merge/tag — and whenever syncing to upstream, applying branch protection, or pruning branches/worktrees across the main repo and its submodules.