Skip to content
GhostbranchdocsGitHub

Setting up CI

Make the shadow repo's CI test what your public CI tests, without handing it secrets it shouldn't have.

The gate runs your own workflows in the shadow repo, on exactly what a publish would send. That’s only as good as the shadow repo’s CI: if a job skips there, or a secret it needs is missing, the gate tests less than your public CI does. ghostbranch audit tells you where the two differ.

Run it in a checkout of your public repo. It reads the workflows and changes nothing:

Terminal
ghostbranch audit
CI audit of you/project-shadow:
repo guards: none
secrets: none referenced
scheduled sync: off (run ghostbranch sync yourself)
managed .github/workflows/ghostbranch-sync.yml: current
warnings, on stderr:
warning: pushes to synced branches start these workflows in the shadow repo: .github/workflows/ci.yml; ghostbranch sync cancels them when the token has Actions write. To stop them for good, add this to their push-triggered jobs: if: github.event_name != 'push' || github.repository == 'you/project' (a job that already has an if: combines them: if: <existing> && (github.event_name != 'push' || github.repository == 'you/project'))
warning: you/project-shadow is private, so its Actions minutes are billed

Its progress lines are left out. This repo has one workflow, which runs on push and pull_request. The sections below go through each line and warning. init and upgrade run the audit too, and doctor sums it up on its ci line.

A job with if: github.repository == 'you/project' runs only in the public repo, so it would skip in the shadow repo. The gate rewrites such guards in its candidate, so those jobs run there too. The audit lists each guard it finds, as repo guard: <file> job <job>, and says whether the gate rewrites it.

  • REPO_GUARD_UNREWRITABLE: the guard is written in a form the gate can’t rewrite safely, such as startsWith(github.repository, …). That job skips in the shadow repo and isn’t tested.
  • REPO_GUARD: you set ci.rewrite_repo_guards: warn in .ghostbranch.yml, so the gate leaves guards alone, and the job skips.

The gate explains the rewrite.

The audit lists every secret your workflows refer to, as secrets.NAME, apart from GITHUB_TOKEN. Then it checks which ones the shadow repo has:

  • SECRETS_MISSING names the ones it lacks. The jobs that use them won’t run in the gate as they do in public CI.
  • SECRETS_UNLISTED means the token can’t list the shadow repo’s secret names, so check the list yourself. The token needs Secrets read on the shadow repo to list them.

Add only the secrets your tests need, in the shadow repo’s settings. Ghostbranch never reads or copies a secret’s value.

Two more kinds of job are listed only in audit --json, under data.secrets:

audit.json (data.secrets)
{
"referenced": [],
"listed": false,
"missing": [],
"inherit": [],
"environments": []
}
  • inherit: jobs that call a reusable workflow with secrets: inherit, as <file>:<job>. Such a job hands the called workflow every secret the shadow repo has, so check what that workflow does with them.
  • environments: jobs with an environment:. Their secrets may be environment secrets, which the audit can’t list, so check those jobs yourself.

The shadow repo is private, so GitHub bills its Actions minutes to its owner: ACTIONS_MINUTES_BILLED. Nothing needs doing about it, but it’s worth knowing what uses them:

  • Each gate runs your workflows once on its candidate. A feature with nothing to publish stops before that, with NOTHING_TO_PUBLISH, and costs no minutes.
  • Each sync pushes to the shadow repo, which starts any workflow that runs on push. Those runs re-test code your public CI already tested. Sync cancels them if its token has Actions write on the shadow repo. To stop them starting at all, add the guard from PUSH_TRIGGERS_ON_SYNC to each job that runs on push. Sync shows how.
  • Scheduled sync, if you turn it on: at least a minute a run.

The gate waits up to gate.ci_appear_timeout (10 minutes by default) for a first check on its pull request in the shadow repo. If none appears, it fails with NO_CI, and error.details.reason says which case it is:

  • no_workflow: the code the gate tested has no workflow under .github/workflows. That happens when your public repo has no CI yet. Add a workflow to the public repo. If your CI is an outside app that posts checks, not GitHub Actions, install that app on the shadow repo too.
  • actions_disabled_or_untriggered: there are workflows, but none started. Turn Actions on in the shadow repo’s settings, or add pull_request to a workflow’s triggers: the gate’s candidate arrives as a pull request.

Common problems shows the message.

Scheduled sync is a workflow in the shadow repo that keeps its branches up to date with the public repo on a timer, so you don’t have to run ghostbranch sync first. It’s off unless you turn it on. Sync has the steps: set sync.schedule in .ghostbranch.yml, then run ghostbranch upgrade --plan and ghostbranch upgrade.

The workflow only fast-forwards the branches listed in sync.branches (main by default). It never force-pushes. It doesn’t close merged features or cancel CI runs either: the next ghostbranch sync on your machine does that.

It pushes with the workflow’s own default token, which can’t push changes to workflow files. So when new public commits change a file under .github/workflows, the scheduled push is refused, and the run’s summary says so. Two ways out:

  • Run ghostbranch sync on your machine, which pushes with your token.
  • Add a GHOSTBRANCH_SYNC_TOKEN secret to the shadow repo, which the workflow then uses: a fine-grained token with Contents and Workflows write on the shadow repo only.

ghostbranch upgrade brings the files Ghostbranch manages on the overlay branch in line with your installed Ghostbranch and your .ghostbranch.yml. Today that’s only the scheduled sync workflow. The audit shows each managed file’s state:

  • current: as it should be. With scheduled sync off, that means there’s no workflow file.
  • missing, outdated or modified (edited by hand): commands warn OVERLAY_OUTDATED. Run ghostbranch upgrade --plan to see the change, then ghostbranch upgrade. It replaces a hand edit, and --plan warns you first.
  • newer: written by a newer Ghostbranch. This one leaves it alone and doesn’t warn: upgrade Ghostbranch, as in Working on two machines.

upgrade writes one commit to the shadow repo’s ghostbranch branch, then runs the audit. Updating the ghostbranch program itself is a different thing: see Upgrading Ghostbranch.

REPO_GUARDwarningA workflow job or step is guarded to the public repo's name, so it skips in the shadow repo (ci.rewrite_repo_guards is warn).
What to do

Set ci.rewrite_repo_guards: rewrite (the default) so the gate's CI runs it, or widen the guard in the public workflow (which writes the shadow repo's name into the public repo). Keep ci.rewrite_repo_guards: warn if a guarded job deploys or publishes: rewritten, it runs in the shadow repo on unreleased code, and its secrets must never be added there.

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.

SECRETS_MISSINGwarningWorkflows reference secrets the shadow repo doesn't have.
What to do

A person adds only the secrets the tests need. Never add deploy or publish credentials to the shadow repo: guarded jobs run there on unreleased code. Ghostbranch never reads or copies secret values.

This needs a person.

SECRETS_UNLISTEDwarningThe token can't list the shadow repo's secret names, so the audit could not tell which referenced secrets are missing.
What to do

Check that the names in data.secrets.referenced exist in the shadow repo, or give the token Secrets: read.

PUSH_TRIGGERS_ON_SYNCwarningThese workflows run on pushes to synced branches. ghostbranch sync pushes with your token, and so does a scheduled sync that uses GHOSTBRANCH_SYNC_TOKEN, so each sync starts them in the shadow repo, re-testing code public CI already tested, on private Actions minutes.
What to do

ghostbranch sync cancels the runs its pushes start when the token has Actions write on the shadow repo. To stop them for good, a person adds the guard in details.guard to those workflows' push-triggered jobs, for example if: github.event_name != 'push' || github.repository == 'org/project'. A job that already has an if: combines them: if: <existing> && (github.event_name != 'push' || github.repository == 'org/project'). Once every push-triggered job carries it, the audit stops reporting the file and sync stops looking for its runs. It changes nothing in the public repo's own CI. Or accept the minutes. A scheduled sync pushed with the workflow's default token starts no workflows.

ACTIONS_MINUTES_BILLEDwarningThe shadow repo is private, so its Actions minutes are billed to its owner.
What to do

Nothing is required. Scheduled sync bills at least one minute a run (every 6 hours is about 120 minutes a month); turn it off with sync.schedule: off and ghostbranch upgrade.

OVERLAY_OUTDATEDwarningA Ghostbranch-managed file on the overlay branch is missing, out of date, or edited by hand.
What to do

Run ghostbranch upgrade --plan to see the change, then ghostbranch upgrade.

AUDIT_INCOMPLETEwarninginit or upgrade finished, but the CI audit after it could not run.
What to do

Run ghostbranch audit.

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.

NOTHING_TO_PUBLISHexit 1Nothing would be published (error.details.reason).
Causes
  • no_commits: the feature has no commits of its own yet
  • all_stripped: every commit is empty, or the commits cancel out, once overlay paths and internal sections are stripped (error.details.dropped_empty)
  • an update of an open PR that already holds every change (no reason)
What to do

For no_commits, commit on the feature branch, push it to the shadow repo, then gate. Otherwise there is nothing to do: the feature has no public changes.