Publishing providers to standalone mirrors
Last updated: 2026-06-11
kedge is a monorepo, but each provider under providers/ is also published to
its own standalone, read-only GitHub repository. This lets external
consumers depend on (and browse) a single provider without cloning the whole
monorepo — the same pattern Kubernetes uses with its
publishing-bot and Symfony/Laravel
use for their components.
Mirrors are source-only. Container images and Helm charts are built and published from the monorepo (
images.yamlandhelm-images.yaml), not from the mirrors. Every monorepo PR builds each provider image (single-arch, build-only) and packages each provider chart, so a broken Dockerfile or chart is caught in the PR rather than surfacing only after the split sync reaches a mirror. The mirrors exist purely so the provider modules arego get-able at their own paths and browsable in isolation.
Publishing is done with splitsh-lite, which produces a real, history-preserving subtree split (not a squashed snapshot). splitsh-lite is deterministic: the same source always splits to the same commit sha1s, so the mirror is append-only and pushes normally fast-forward.
What is published
| Provider directory | Mirror repository | Secret | Workflow |
|---|---|---|---|
providers/quickstart |
faroshq/provider-quickstart |
QUICKSTART_DEPLOY_KEY |
split-quickstart.yaml |
providers/code |
faroshq/provider-code |
CODE_DEPLOY_KEY |
split-code.yaml |
providers/infrastructure |
faroshq/provider-infrastructure |
INFRA_DEPLOY_KEY |
split-infrastructure.yaml |
providers/app-studio |
faroshq/provider-app-studio |
APP_STUDIO_DEPLOY_KEY |
split-app-studio.yaml |
providers/kuery |
faroshq/provider-kuery |
KUERY_DEPLOY_KEY |
split-kuery.yaml |
Each provider has its own workflow (identical except for the four env: values
and the secrets.* reference) and its own deploy key — a GitHub deploy key is
scoped to a single repo, so the keys cannot be shared across mirrors. See
Adding another provider for the generic pattern.
When it runs
The split workflow triggers on:
- push to
main— mirrors the branch (force-pushed, so the mirror always reflects the monorepo even if history is rewritten). - per-provider release tags —
providers/<name>/vX.Y.Z. The path prefix is stripped on the way out, so the mirror receives a plainvX.Y.Ztag (which records the released source on the mirror). Pushed non-force, so re-tagging fails loudly since tags are immutable. Note the mirror tag no longer builds anything — provider images and charts are published from the monorepo (a repo-widevX.Y.Ztag drives those builds viaimages.yaml/helm-images.yaml). - pull requests touching the provider’s subtree (or its workflow file) — these only validate (install splitsh-lite + compute the split); the deploy-key and push steps are gated to non-PR events, so a PR never writes to a mirror.
workflow_dispatch— manual run, used for the initial seed or a manual re-sync.
Runs are serialized per ref (concurrency group) and never cancelled
mid-push.
One-time setup per mirror
These steps are manual and must be done once per provider mirror. They
require admin on both the monorepo and the target repo. Substitute the
provider’s values from the What is published table for
<name> (e.g. quickstart), the mirror repo, and the secret name.
Each mirror needs its own deploy key. A GitHub deploy key (the public half) can be registered on only one repository — adding a key that is already a deploy key on another repo fails with “Key is already in use.” So you cannot reuse one key across
provider-quickstart,provider-code, andprovider-infrastructure; generate a fresh key per mirror.
1. Create the target repository
Create the mirror repo (e.g. faroshq/provider-code). An empty repo is
fine — the first run creates the main branch.
2. Generate an SSH deploy key
ssh-keygen -t ed25519 -C "kedge-split-<name>" -f /tmp/<name>_split -N ""
This produces a private key (/tmp/<name>_split) and a public key
(/tmp/<name>_split.pub).
3. Add the public key as a write deploy key on the mirror
In the mirror repo → Settings → Deploy keys → Add deploy key:
- Paste the contents of
/tmp/<name>_split.pub. - Check “Allow write access”.
A deploy key is scoped to that single repo, which is why we use it instead of a broad personal access token.
4. Add the private key as a secret on the monorepo
In faroshq/kedge → Settings → Secrets and variables → Actions → New repository secret:
- Name: the provider’s secret from the table (e.g.
CODE_DEPLOY_KEY). - Value: the full contents of
/tmp/<name>_split(the private key, including the-----BEGIN/END-----lines).
5. Seed the mirror
Run the workflow once manually: Actions → Split <name> provider → Run workflow. After the first successful run the mirror tracks the monorepo automatically.
6. Clean up
Delete the local key copies once they are stored in GitHub:
rm -f /tmp/<name>_split /tmp/<name>_split.pub
How the split works (internals)
Each split workflow (e.g. split-code.yaml):
- Checks out the monorepo with full history (
fetch-depth: 0) — required for splitsh-lite to compute the subtree. - Downloads the pinned splitsh-lite
v1.0.1prebuilt Linux binary (statically bundles libgit2;v2.0.0ships no Linux binary and needs cgo, so we pinv1.0.1). Thev1.0.1tarball stores the binary as./splitsh-lite(leading./), so the extraction names it explicitly. - On non-PR events, writes the provider’s deploy-key secret to
~/.sshand configuresgithub.comto use it. (On PRs this step is skipped.) - Runs
splitsh-lite --prefix=providers/<name> --origin=HEAD, which writes the split commits into the local object store and prints the tip sha. This runs on every event (PRs included) so it validates the install + split end-to-end without publishing.--origintakes a git ref, andHEADresolves cleanly for both branch pushes and PR merge commits. - On non-PR events, pushes that sha to the mirror — to
refs/heads/mainfor branch pushes (force), orrefs/tags/vX.Y.Zfor tag pushes (non-force, prefix stripped fromproviders/<name>/vX.Y.Z).
Adding another provider
All three current providers (quickstart, code, infrastructure) are wired
up. To publish a new one, copy any existing split workflow (they are identical
apart from four env: values, the trigger paths/tags, the concurrency group,
and the secrets.* reference) and change:
name: Split <name> provider
on:
push:
tags: ['providers/<name>/v*']
pull_request:
paths:
- 'providers/<name>/**'
- '.github/workflows/split-<name>.yaml'
concurrency:
group: split-<name>-$
env:
PREFIX: providers/<name> # the subtree to split
TARGET_REPO: git@github.com:faroshq/provider-<name>.git
TARGET_BRANCH: main
SPLITSH_VERSION: v1.0.1
# ...and reference a per-mirror secret, e.g. secrets.<NAME>_DEPLOY_KEY
Then repeat the one-time setup with a fresh key and secret name. Use a distinct deploy key + secret per mirror so a leaked key only affects one repo. (These can later be collapsed into a single matrix workflow if the list grows.)
Go module paths
A split mirror keeps the monorepo’s go.mod verbatim, so a provider’s module
path must match its mirror URL for the mirror to be go get-able at its own
path. Each published provider’s go.mod is therefore declared with the mirror
path rather than a monorepo-nested path:
| Provider | module declaration in go.mod |
|---|---|
providers/quickstart |
github.com/faroshq/provider-quickstart |
providers/code |
github.com/faroshq/provider-code |
providers/infrastructure |
github.com/faroshq/provider-infrastructure |
This is transparent to the monorepo because go.work references providers by
directory, not by module path, and no other monorepo module imports these
provider modules. When wiring up a new provider mirror, set its go.mod module
to the mirror URL (e.g. github.com/faroshq/provider-code) before the first
split, and fix up the provider’s own in-repo imports of that module path
accordingly (code and infrastructure each had ~17 self-imports to rewrite).