Skip to content
GhostbranchdocsGitHub

The gate

What the gate checks before anything can be published, and what to do when it says no.

A green gate means two things: your CI passed, and nothing that shouldn’t go public is in the code that would. Both are checked on the exact files a publish would send, never on your branch as it stands.

ghostbranch gate <feature> runs these steps, and stops at the first one that fails:

  1. Rebase the feature onto public main, in a throwaway copy. Your branch isn’t touched. A stacked feature is rebased with the branches below it.
  2. Strip what never goes public: the overlay’s paths, such as .ghostbranch/, and any internal sections (see below). What’s left is the candidate.
  3. Scan the candidate for leaks.
  4. Push the candidate, plus the overlay branch’s files, to the gate branch ghostbranch-gate/<feature> in the shadow repo, and open a draft pull request there.
  5. Wait for every check on that pull request to finish, and record the result.

The scan runs before anything is pushed, so a leak never even reaches the shadow repo’s gate branch. And the gate never touches the public repo.

Before it pushes anything, the gate checks that the feature has something to publish. A feature with no commits of its own stops right there, before anything is pushed and before CI spends a minute:

Terminal
ghostbranch gate add-search
Replaying add-search onto public main
Gate for add-search: checks not_run, leak scan not_run.
0 commit(s), scanned as preserve
error: feature add-search has no commits of its own yet: commit on its branch, push it to the shadow repo, then gate [NOTHING_TO_PUBLISH]

The two progress lines about reading the repos are left out above. Here the commit was made, but not yet pushed to the shadow repo. NOTHING_TO_PUBLISH exits with code 1, and error.details.reason says why:

  • no_commits: the feature has no commits of its own yet. Commit, push to the shadow repo, then gate.
  • all_stripped: it has commits, but nothing is left once the overlay’s paths and internal sections are taken out. Nothing of it would go public.

Your own workflows run on the candidate, in the shadow repo: the same files a publish would send, with the same workflow files. So a green gate means your public CI will very likely pass too.

The candidate starts your workflows as any branch and pull request would. Private workflows that should run only in the shadow repo go on the overlay branch, as .github/workflows/ghostbranch-*.yml.

The gate fails with exit code 3 when CI fails (CHECKS_FAILED), when no check appears at all (NO_CI), or when CI runs too long (CI_TIMEOUT). The timeouts are in .ghostbranch.yml: gate.ci_appear_timeout (10 minutes by default) and gate.ci_timeout (60 minutes).

NO_CI says why in error.details.reason: no_workflow when the candidate has no workflow under .github/workflows at all, and actions_disabled_or_untriggered when Actions is off in the shadow repo or no workflow runs on pull_request. Common problems has the fix for each.

In a stack, a passing gate’s next looks at the levels around it: it says to gate the level below first if that one’s gate isn’t fresh, then any level above that has gone stale, and only then to promote the whole stack.

Many workflows have jobs that only run in the real repo: if: github.repository == 'you/project'. In the shadow repo those jobs would skip, and CI would test less than it should. So the gate rewrites such guards in the candidate only, and those jobs run in the shadow repo too. What gets published is never rewritten.

A guard written in a form the gate can’t rewrite safely, such as startsWith(github.repository, …), warns REPO_GUARD_UNREWRITABLE: that job skipped, and wasn’t tested. ghostbranch audit lists every guard it finds.

The scan reads everything a publish would send:

  • Secrets: API keys, tokens, private keys and the like, found by the built-in rules (leak_scan.secrets: true).
  • Your own patterns: regular expressions you list under leak_scan.block_patterns, for internal hostnames or project code names.
  • Internal markers that can’t be stripped safely (MARKER_INVALID).

It reads every file, including binary files through their readable text, and in the default preserve mode it reads every commit and its message, and also its author unless authors are published as your noreply identity. So a secret added in one commit and deleted in the next still blocks: the first commit would publish it. When you publish, promote scans the branch name and the pull request’s text too.

Here a feature added a (fake) GitHub token:

Terminal
ghostbranch gate add-search
Gate for add-search: checks not_run, leak scan found.
1 commit(s), scanned as preserve
leak f-34b03e644091: github-pat in drill/fake-token.txt:1 (commit(s) aa9a2bafc2ee)
error: 1 leak finding(s) in what would be published; nothing was pushed to the public repo [LEAK_FOUND]
Next:
ghostbranch gate add-search --repo you/project
amend the feature's last commit to remove it (a fix commit on top would leave it in a commit that preserve and curated promotes publish), push with `git push --force-with-lease shadow add-search`, then gate again
ghostbranch waivers add add-search f-34b03e644091 --repo you/project --reason <reason> (a person must run this)
only if a person judges these findings false positives: they review them at their console and give a reason

The finding names the rule, the file, the line and the commit, and never the value. With --json, each finding is in error.details.findings:

gate.json (part)
{
"id": "f-34b03e644091",
"rule": "github-pat",
"where": "tree",
"path": "drill/fake-token.txt",
"line": 1,
"secret": "[REDACTED]",
"waivable": true,
"commits": ["aa9a2bafc2eea9061a3853ed89dedf40ed791dba"]
}

Remove the content from the feature and gate again. As next says, a new commit that deletes it isn’t enough in preserve mode, because the older commit would still publish it. After a fix commit, the same gate still fails, and next gives the ways out:

Terminal
ghostbranch gate add-search
Gate for add-search: checks not_run, leak scan found.
2 commit(s), scanned as preserve
leak f-34b03e644091: github-pat in drill/fake-token.txt:1 (commit(s) aa9a2bafc2ee)
error: 1 leak finding(s) in what would be published; nothing was pushed to the public repo [LEAK_FOUND]
Next:
ghostbranch promote add-search --mode squash --plan --repo you/project --message <message>
if the problem is only in older commits (the final tree no longer has it), publish the feature as one commit, so they never ship
ghostbranch promote add-search --mode curated --plan --repo you/project --curate <curate> --title <title>
or curate the commits in a --curate file (see the gate result's commits): group each commit the finding names with a later one that removes the problem
ghostbranch gate add-search --repo you/project
or rewrite the commits in each finding's commits on the feature branch in the shadow repo (fix the files), push with `git push --force-with-lease shadow add-search`, then gate again
the waivers add line, as above

So you can rewrite the commit that added it and force-push the branch to the shadow repo, or publish in a history mode that never ships the older commit.

Sometimes a finding is a false positive, such as a fake key in a test fixture. Only a person can waive it, with waivers add and a one-time code at their own terminal. The code is there so a coding agent can’t waive its own findings.

Terminal
ghostbranch waivers add add-search f-34b03e644091 --reason "fake token in a test fixture" --plan
Dry run; nothing was changed. This would:
- waive leak finding f-34b03e644091 for you/project

Without --plan, it scans the feature again on your machine, shows each finding, and asks for the code. A waiver covers exactly that content in that file: if the content changes, it’s a new finding. Waivers live on the machine that made them. waivers list shows them and waivers revoke takes one back.

A finding in a commit message, a branch name or the pull request’s text can never be waived. Change the text instead.

Some things are worth a look before you publish, but aren’t leaks. The gate passes and warns:

  • BINARY_FILE: new or changed binary files would be published. They were scanned only through their readable text.
  • LARGE_FILE: files larger than leak_scan.large_file (5 MB by default).
  • WORKFLOW_CHANGED: workflow files under .github/workflows are new or changed.

You can keep notes in a file that go no further than the shadow repo. Lines between a BEGIN-INTERNAL marker line and an END-INTERNAL marker line are removed from every published file, and CI runs on the file without them. Each marker goes on a line of its own, in the file’s comment syntax: for example // BEGIN-INTERNAL in JavaScript, or an HTML comment holding END-INTERNAL in Markdown.

A line that looks like a marker but isn’t exactly one, such as a marker followed by more words, fails the gate with MARKER_INVALID rather than guess. Never delete only the marker lines: the lines between them would then be published.

Exit code 3 always means the gate said no: CI failed or didn’t run, the leak scan found something, or a marker was malformed. Nothing was published. Fix the feature, push it to the shadow repo, and gate again.

LEAK_FOUNDexit 3The leak scan found something in what would be published.
What to do

Remove the content in details.findings from the feature and gate again. A finding whose commits are listed may be only in older commits: publish with --mode squash or --mode curated so they never ship, or rewrite those commits on the feature branch, force-push it to the shadow repo, and gate again. If a person judges a finding in details.waivable a false positive, they run the waivers add command in next. Agents never waive findings or edit waivers.json.

MARKER_INVALIDexit 3A file has BEGIN-INTERNAL/END-INTERNAL markers that cannot be stripped safely.
Causes
  • unmatched_begin: a begin marker with no end
  • unmatched_end: an end marker with no begin
  • nested: a begin marker inside an open section
  • malformed: a line that looks like a marker but is not exactly one (more text, a doubled prefix, or a multi-line comment)
  • unsupported_encoding: markers in a UTF-16 or UTF-32 file, or in a file read as binary
What to do

Fix the marker lines at details.problems on the feature branch, push it to the shadow repo, and gate again. When details.commit names an older commit and the final tree no longer has the problem, publish with --mode squash or --mode curated so it never ships, or rewrite that commit on the feature branch and force-push it to the shadow repo. Never delete only the marker lines: the internal lines would then be published.

CHECKS_FAILEDexit 3CI failed on the gate candidate.
What to do

Fix the failing checks listed in details.failed, push to the feature branch, and gate again.

NO_CIexit 3No CI checks appeared on the gate candidate (error.details.reason).
Causes
  • no_workflow: the candidate has no workflow under .github/workflows, and no CI app posts checks
  • actions_disabled_or_untriggered: Actions is disabled in the shadow repo, or no workflow triggers on pull_request
  • no reason: the gate candidate couldn't be read, so which of the two holds is unknown
What to do

For no_workflow, add CI to the public repo, or, if CI is an external app, install it on the shadow repo. Otherwise enable Actions in the shadow repo or add a pull_request trigger. Then gate again.

CI_TIMEOUTexit 3CI did not finish in time.
What to do

Gate again, or raise gate.ci_timeout in .ghostbranch.yml.

REBASE_CONFLICTexit 4The feature does not rebase cleanly onto public main.
What to do

Rebase the feature branch onto public main yourself, resolve the files in details.files (the commit in details.commit stopped), push, and gate again.

BINARY_FILEwarningNew or changed binary files would be published.
What to do

Check that the files in details.paths are meant to go public; they were scanned only through their printable text.

LARGE_FILEwarningNew or changed files are larger than leak_scan.large_file.
What to do

Check that the files in details.paths are meant to go public.

WORKFLOW_CHANGEDwarningWorkflow files under .github/workflows are new or changed in what would be published.
What to do

A person checks the workflow changes in details.paths before approving the publish.

REPO_GUARD_UNREWRITABLEwarningA workflow guard mentions the public repo in a form the gate can't rewrite safely, so that job skips in the shadow repo and CI does not test it.
Causes
  • startsWith, contains, fromJSON or format around github.repository
  • a YAML form the rewrite can't re-emit safely, such as an anchor or a multi-line plain value
What to do

A person widens the guard in the public workflow to also allow the shadow repo, for example github.repository == 'org/project' || github.repository == 'org/project-shadow'. That writes the shadow repo's name into the public repo. Never widen the guard of a job that deploys or publishes: it would run in the shadow repo on unreleased code, and its secrets must never be added there; repos whose guarded jobs deploy should use ci.rewrite_repo_guards: warn. Guards in run: scripts, reusable workflows called by repo name and composite actions are never detected.

CHECKS_LIMITEDwarningThe token cannot read check runs, so only GitHub Actions workflows and commit statuses were considered.
What to do

Use a token that can read checks if you rely on CI apps other than Actions.