kbc -> kbagent CI/CD Migration
Verified against: kbagent v0.80.0, live-verified 2026-08-06 through
2026-08-10 (project 153, real repro, not a static code read). Every dated
claim below and in references/*.md is a point-in-time repro note, not a
version floor -- if kbagent has moved well past 0.80.x when you read this,
treat behavior claims as "true as of that version" and spot-check anything
load-bearing against current main rather than assuming it still holds.
Guides a customer through porting a kbc GitHub CI/CD pipeline (the
CLI-based-sync-demo shape:
per-project pull/push, multi-project promotion, branch-gated deploys) to the new
kbagent sync engine, emitting clean kbagent-native workflows.
Reality check — this is a one-time BREAKING migration, not a command swap
Do not tell the user they can just swap kbc for kbagent in place. Three
hard incompatibilities make this a deliberate cutover (verified against the code):
- The on-disk layout/format is different and incompatible.
kbc writes per config: config.json + meta.json + description.md (JSON).
kbagent writes per config: _config.yml (YAML, with name/description/
parameters hoisted + a _configuration_extra block) + extracted code files
(constants.py's CONFIG_FILENAME, sync/config_format.py).
- The first
kbagent sync pull therefore rewrites every configuration into a
new format. The old config.json/meta.json files are not read by kbagent
and become orphans that must be deleted. Expect a massive reformatting diff.
- kbagent sync is an ORCHESTRATOR, not cwd-per-folder.
kbc pull runs against
whatever directory you cd into. kbagent sync pull requires --project ALIAS
(resolved from a central config store) or --all-projects (commands/sync.py's
project-selection guard). In
CI we bridge this with env-injection: KBAGENT_PROJECT_FROM_ENV=1 synthesizes a
project under the reserved alias __env__, and every command passes
--project __env__ --directory <folder>.
- The two tools cannot co-own the same tree. Because the source-of-truth files
differ, you cannot have
kbc push and kbagent push both treating one directory
as canonical. You must cut over.
Consequence (this is what the user correctly anticipated)
- The migration is a one-time conversion commit on a dedicated branch, where
the JSON tree is replaced by the YAML tree. Reviewing that diff line-by-line is
impractical; you verify by behavior (
sync diff clean, dry-run push empty), not
by reading the reformat. Merging it to main is effectively a tooling version bump.
- Until cutover, keep the legacy
kbc workflows; afterwards delete them in the same PR.
Finding (project 153, see "Verified against" above) — use plain
sync init, never --adopt-existing, for the conversion:
--adopt-existing carries kbc's row paths (values/{name} for
keboola.variables, codes/{name} for keboola.shared-code) straight into the
kbagent manifest without translating them to kbagent's own rows/{name}
convention. kbagent's untracked-config scanner only recognizes a literal rows/
path segment, so it reports those inherited rows as brand-new top-level configs
— reproduced: sync status/sync diff stayed at 9 added / 1-2 deleted even
after the orphan-file cleanup, and pushing would create_config duplicate
siblings instead of updating the rows they actually are. Plain sync init
avoids this: it starts a genuinely empty manifest and lets sync pull populate
every path through kbagent's own naming logic from scratch — confirmed to reach
a fully clean sync status ("No local changes detected") and sync diff ("No
differences found") against the same project, 119 tracked configs.
One operational catch: kbc and kbagent write their manifest to the identical
path, .keboola/manifest.json, so plain sync init refuses to run
("Manifest already exists...") until that one file — not the
config.json/meta.json config tree next to it — is deleted first. Folded
into Step 3b below.
Never run sync push (without --dry-run) against an adopted-but-not-yet-
pulled kbc tree on any kbagent version — always sync pull and confirm a clean
sync diff first.
What still carries over unchanged
The orchestration shell is CLI-agnostic: manual/scheduled pull that commits state,
PR validation, GitHub-Environment-gated push, branch→env mapping, per-project loops.
Only three mechanics change: install (uv tool install not a binary download),
commands/flags (kbagent sync ..., see
references/command-mapping.md), and auth env vars
(KBAGENT_PROJECT_FROM_ENV=1 + KBC_TOKEN + KBC_STORAGE_API_URL).
The per-project directory layout is also untouched. Whatever top-level folder
name each project already uses in the repo — a numeric project id (9086/), a
promotion label (L0/, L1/), or a flat single-project repo (no per-project
folder at all) — kbagent keeps it exactly as-is: migrate_cicd.py discovers every
project by walking for .keboola/manifest.json and reuses that folder's existing
path verbatim in every generated step (--directory '{p.directory}'); it never
renames, moves, or re-derives the folder from the alias. Inside that folder the
branch subdirectory (main/, etc.) and the storage/-adjacent component-type
folders are unchanged too — only the file format one level below (config.json
→ _config.yml) changes. Do not use kbagent sync --all-projects to "adopt"
this layout — it enforces its own <directory>/<alias>/ convention (see the
guardrail below) and would rename/duplicate the tree; the generated CI never
uses it for exactly this reason.
Prerequisites — ask for these before Step 1
Four things this skill cannot infer; get them from the customer/operator first:
- The repo path. Where the
kbc-managed tree (with .keboola/manifest.json
and .github/workflows/) actually lives locally — Step 1's migrate_cicd.py
argument.
- A
kbagent binary or install. Either already on PATH, or install it now:
uv tool install keboola-cli==<ver> (see Step 2 for version pin) or a
downloaded standalone binary. No local kbc binary is required to run the
migration (kbagent is the only tool that touches the repo from Step 3b on) —
only to verify the "same data, new layout" claim by diffing a kbc pull
against a kbagent sync pull of the same project, which is optional.
- Auth for each project being converted — two different answers for CI vs. the
local conversion step. The generated CI workflows (Step 3/4) always need a
static per-project Storage API token secret (
KBC_TOKEN_<ALIAS>,
KBAGENT_PROJECT_FROM_ENV=1) — kbagent auth login is browser-based and
cannot run unattended on a GitHub Actions runner, so there is no login-based
alternative for CI. For the local, interactive one-time conversion in Step
3b, though, a raw token is not the only option: if the operator already has
(or runs) kbagent auth login + auth register-projects, they get a
registered alias with a session token and can run kbagent sync init/pull --project <alias> --directory <DIR> directly — no KBAGENT_PROJECT_FROM_ENV/
KBC_TOKEN env-injection needed, since that dance exists specifically to
bridge CI's no-persisted-config environment. Either way, never ask the
customer to paste a token into chat; read it via the clipboard-secret pattern
or point at wherever they already store it and reference it by path/alias.
How to run this — ask the customer, don't auto-pilot
This skill reads like a linear script, and it is tempting to run Steps 1-6
end-to-end without stopping. Don't. Every step below that touches the
customer's repo or their live Keboola project is a decision the customer
should make, not one you make for them — this is their production CI/CD and
their production data. Treat the numbered steps as a checklist of decisions to
surface, not a batch job to execute. Concretely, stop and ask before you:
- Pick the version pin (Step 2). Don't default to "latest" or to whatever
version happens to be installed on your machine — ask which lane (prod vs.
scratch) this repo is, and let the answer decide pinned vs. unpinned.
- Write anything (
--write in Step 3). Show the dry-run output first,
let the customer review the projects/legacy-files inventory, then ask
before generating files into their repo.
- Run
sync init / sync pull against a real project (Step 3b). This is
the breaking, one-time conversion — confirm which project to convert first
(always non-prod), and confirm the customer is fine with the change freeze
being in effect before you touch anything. Use plain sync init (no
--adopt-existing, see Step 3b) and require a genuinely empty sync status
before calling a project converted.
- Delete any file — orphaned
config.json/meta.json, or the now-empty
kbc-only type folders (app/, processor/, _shared/, see the reality
check above). Show what you're about to delete and why (they are no longer
read by kbagent) and let the customer say go, especially the first time
through — don't git rm on their behalf and mention it after the fact.
- Choose the branching model (Step 5). Single-branch vs. git-branching is
a workflow/process decision for their team, not a default you pick because
it's "usually better." Present the decision table and wait for their answer.
- Enable the push lane / run a real
sync push. Dry-run first, always;
a real push against a customer's project needs their explicit go-ahead
every time, not just once at the start of the migration.
If you're running this yourself against a live project to verify the skill's
claims (as opposed to guiding a customer), the same discipline still applies:
narrate what you're about to run and why before you run it, rather than
chaining the whole sequence unattended — that's how stale/incorrect claims go
unnoticed in a skill like this one, and how you end up cleaning up things you
didn't realize you'd need to.
Workflow
Step 1 — Analyze the existing repo (always start here)
Run the engine in dry-run mode to inventory projects and the legacy CI it replaces:
python <skill_dir>/scripts/migrate_cicd.py /path/to/repo
It prints every project found (one per .keboola/manifest.json), each project's
id / stack host / required token secret name, any ignoredComponents ("subset of
a project" — see Step 5), and the legacy kbc workflow/action files it supersedes.
Step 2 — Pick a version pin (decide before generating)
- Pinned (recommended for prod lanes):
--version X.Y.Z (PyPI) or
--git-ref vX.Y.Z (git tag). Reproducible CI. Check the
latest release for the current X.Y.Z.
- Unpinned (
keboola-cli, resolves to latest): only acceptable for a
non-prod/scratch lane. Warn the user: unpinned + the current auto-update behavior
means non-deterministic CI runs.
Step 3 — Generate the clean workflows
python <skill_dir>/scripts/migrate_cicd.py /path/to/repo --write \
--version X.Y.Z --main-branch main --schedule "0 * * * *"
Produces:
.github/workflows/kbagent-validate.yml — on PR: sync diff + sync push --dry-run per project (read-only drift + secret-encryption preflight).
.github/workflows/kbagent-pull.yml — manual + optional cron: sync pull --force per project, commits state back.
.github/workflows/kbagent-push.yml — manual, GitHub-Environment-gated: sync push per project, with an allow_delete input.
The legacy kbc files are left in place — review the new ones, then delete the
old workflows/actions in the same PR.
Step 3b — Perform the one-time config conversion (dedicated branch)
This is the breaking part. On a fresh migration branch, for each project, convert
the JSON tree to kbagent's YAML tree and remove the orphaned kbc files.
Recommended mechanic: plain sync init — do NOT use --adopt-existing (see
the reality-check note above for why: inherited row paths make sync status
permanently dirty and risk sync push creating duplicate configs). The exact
per-project command sequence — including the required rm .keboola/manifest.json
prep step, the acceptance criteria, and why it's an ordinary commit on an ordinary
branch (never a git-history rewrite, even though the reformat diff is huge) — is
maintained once in
references/migration-runbook.md ("PR 1 —
Conversion"); follow it verbatim rather than re-deriving the steps here.
Step 4 — Set up GitHub secrets, variables, environments
The engine prints exact gh commands. The model: one Storage API token secret
per project (KBC_TOKEN_<ALIAS>), and two Environments (prod, dev) so
prod pushes require approval. See references/secrets-setup.md
for the full mapping from the old secrets.KBC_SAPI_TOKEN_* / vars.KBC_* scheme.
Step 5 — Confirm scope ("subset of a project") and branching
- Subset: if a project should only sync part of its config tree, set
ignoredComponents (and/or allowedBranches) in that project's
.keboola/manifest.json — kbagent sync honors both, exactly like kbc.
- Branching: the old model used a fixed branch id per env. The new model maps
git branch → Keboola dev branch via
.keboola/branch-mapping.json +
kbagent sync branch-link. For PR-based promotion this is usually better:
a PR branch links to a Keboola dev branch, main pushes to production. This
is a per-project runtime choice, not something the generator needs to know
about — run kbagent sync init --git-branching and walk the user through
branch-link if they want per-PR isolated dev branches (see
references/branching-model.md). If they
want to keep the simple single-branch (production) model, leave
branch-mapping at the default (null = production) and skip this entirely.
Step 6 — Validate before merging
- Open the migration PR; the
kbagent-validate workflow runs sync diff — confirm
the diff is empty (no unintended drift) against each project.
- Manually run
kbagent-pull once against the already-converted tree and
confirm it's a no-op: git diff should be empty (or near-empty). This checks
that nothing drifted between the conversion commit and now — it is not the
same comparison as the one-time JSON→YAML conversion diff in Step 3b, which
is expected to touch every config file.
- Do a
kbagent-push dry-run (the validate workflow already does this) and read
the planned changes before the first real gated push.
Guardrails (state these to the user)
- Never add
--allow-plaintext-on-encrypt-failure to CI push — it silently
uploads #-secrets in cleartext if the Encryption API is down. The generated
push is fail-closed by design.
sync push --force deletes remote configs removed locally. It is wired to
the allow_delete workflow input (default off). Treat it like the old --force.
- Tokens live only in GitHub secrets and are injected as env vars per step; the
generated workflows never write a
config.json to disk.
- Never run
--all-projects in a directory that also holds a flat single-project
tree. --all-projects (sync pull --all-projects / sync push --all-projects)
is hard-coded to a <directory>/<alias>/ layout for every registered project
alias (_sync_bulk.py) — it is not "operate on whatever is in this directory."
Confirmed live: registering a persistent alias with kbagent project add --project 153 ... (needed for ad-hoc config update/config detail maintenance work
outside the CI flow) and then running sync pull --all-projects in a directory
that already had a flat manifest at ./.keboola/manifest.json silently
auto-created a second, separate tree at ./153/ — pull_all auto-inits any
registered alias with no manifest yet at its expected subpath, it does not detect
or reuse an existing flat-layout manifest for the same project. push_all is
slightly safer (skips instead of auto-creating) but still expects the same
subfolder convention. This is why the generated CI workflows never use
--all-projects — every step passes --project __env__ --directory '{directory}' explicitly (see migrate_cicd.py's _project_step). Carry the
same discipline into any manual/maintenance commands you run outside CI: always
--project ALIAS --directory DIR explicit, never --all-projects, in a
migration repo. If a customer (or you, helping them) already hit this, the
extra <alias>/ directory can simply be deleted — it holds a fresh, unrelated
pull, not anything derived from their real tree.
Reference material
- references/migration-runbook.md — the ordered PR sequence / cutover plan (pre-flight → conversion PR → start-over). Use this when the user asks "which PRs, what order, how do I cut over."
- references/branching-model.md — how to choose single-branch (Model A) vs git-branching (Model B), with a decision table.
- references/command-mapping.md — kbc ↔ kbagent commands, flags, env vars.
- references/secrets-setup.md — secrets/vars/environments migration table +
gh setup.
scripts/migrate_cicd.py — the analyzer + generator (stdlib only).
1---2name: kbagent-cicd-migration3description: Use when migrating an existing kbc (keboola-as-code) GitHub CI/CD pipeline to the new kbagent (keboola-cli) sync engine. Covers: converting per-project pull/push PR workflows, multi-project repos (e.g. L0/L1 dev->prod promotion), branch->environment mapping, GitHub secrets/variables/environments setup, the install step (uv tool install instead of downloading a Go binary), and the kbc->kbagent command/flag/env-var mapping. Triggers: migrate CI/CD, migrate pipeline, kbc to kbagent, port GitHub Actions, CLI-based-sync-demo, kbc pull push CI, project-as-code CI migration, replace kbc binary in CI, gitops migration, multi-project promotion, dev to prod Keboola, KBC_STORAGE_API_TOKEN to KBC_TOKEN, sync push CI, sync pull CI.4---56# kbc -> kbagent CI/CD Migration78> **Verified against:** kbagent v0.80.0, live-verified 2026-08-06 through9> 2026-08-10 (project 153, real repro, not a static code read). Every dated10> claim below and in `references/*.md` is a point-in-time repro note, not a11> version floor -- if kbagent has moved well past 0.80.x when you read this,12> treat behavior claims as "true as of that version" and spot-check anything13> load-bearing against current `main` rather than assuming it still holds.1415Guides a customer through porting a `kbc` GitHub CI/CD pipeline (the16[CLI-based-sync-demo](https://github.com/keboola/CLI-based-sync-demo) shape:17per-project pull/push, multi-project promotion, branch-gated deploys) to the new18`kbagent sync` engine, emitting **clean kbagent-native workflows**.1920## Reality check — this is a one-time BREAKING migration, not a command swap2122Do **not** tell the user they can just swap `kbc` for `kbagent` in place. Three23hard incompatibilities make this a deliberate cutover (verified against the code):24251. **The on-disk layout/format is different and incompatible.**26 - `kbc` writes per config: `config.json` + `meta.json` + `description.md` (JSON).27 - `kbagent` writes per config: **`_config.yml`** (YAML, with `name`/`description`/28 `parameters` hoisted + a `_configuration_extra` block) + extracted code files29 (`constants.py`'s `CONFIG_FILENAME`, `sync/config_format.py`).30 - The first `kbagent sync pull` therefore **rewrites every configuration** into a31 new format. The old `config.json`/`meta.json` files are **not read** by kbagent32 and become orphans that must be deleted. Expect a **massive reformatting diff**.332. **kbagent sync is an ORCHESTRATOR, not cwd-per-folder.** `kbc pull` runs against34 whatever directory you `cd` into. `kbagent sync pull` *requires* `--project ALIAS`35 (resolved from a central config store) or `--all-projects` (`commands/sync.py`'s36 project-selection guard). In37 CI we bridge this with env-injection: `KBAGENT_PROJECT_FROM_ENV=1` synthesizes a38 project under the reserved alias `__env__`, and every command passes39 `--project __env__ --directory <folder>`.403. **The two tools cannot co-own the same tree.** Because the source-of-truth files41 differ, you cannot have `kbc push` and `kbagent push` both treating one directory42 as canonical. You must cut over.4344### Consequence (this is what the user correctly anticipated)45- The migration is a **one-time conversion commit** on a **dedicated branch**, where46 the JSON tree is replaced by the YAML tree. Reviewing that diff line-by-line is47 impractical; you verify by **behavior** (`sync diff` clean, dry-run push empty), not48 by reading the reformat. Merging it to `main` is effectively a tooling version bump.49- Until cutover, keep the legacy `kbc` workflows; afterwards delete them in the same PR.5051> **Finding (project 153, see "Verified against" above) — use plain52> `sync init`, never `--adopt-existing`, for the conversion:**53> `--adopt-existing` carries kbc's row paths (`values/{name}` for54> `keboola.variables`, `codes/{name}` for `keboola.shared-code`) straight into the55> kbagent manifest without translating them to kbagent's own `rows/{name}`56> convention. kbagent's untracked-config scanner only recognizes a literal `rows/`57> path segment, so it reports those inherited rows as brand-new top-level configs58> — reproduced: `sync status`/`sync diff` stayed at 9 added / 1-2 deleted even59> after the orphan-file cleanup, and pushing would `create_config` duplicate60> siblings instead of updating the rows they actually are. Plain `sync init`61> avoids this: it starts a genuinely empty manifest and lets `sync pull` populate62> every path through kbagent's own naming logic from scratch — confirmed to reach63> a fully clean `sync status` ("No local changes detected") and `sync diff` ("No64> differences found") against the same project, 119 tracked configs.65>66> One operational catch: kbc and kbagent write their manifest to the identical67> path, `.keboola/manifest.json`, so plain `sync init` refuses to run68> ("Manifest already exists...") until that one file — not the69> `config.json`/`meta.json` config tree next to it — is deleted first. Folded70> into Step 3b below.71>72> Never run `sync push` (without `--dry-run`) against an adopted-but-not-yet-73> pulled kbc tree on any kbagent version — always `sync pull` and confirm a clean74> `sync diff` first.7576### What still carries over unchanged77The *orchestration shell* is CLI-agnostic: manual/scheduled pull that commits state,78PR validation, GitHub-Environment-gated push, branch→env mapping, per-project loops.79Only three mechanics change: **install** (`uv tool install` not a binary download),80**commands/flags** (`kbagent sync ...`, see81[references/command-mapping.md](references/command-mapping.md)), and **auth env vars**82(`KBAGENT_PROJECT_FROM_ENV=1` + `KBC_TOKEN` + `KBC_STORAGE_API_URL`).8384**The per-project directory layout is also untouched.** Whatever top-level folder85name each project already uses in the repo — a numeric project id (`9086/`), a86promotion label (`L0/`, `L1/`), or a flat single-project repo (no per-project87folder at all) — kbagent keeps it exactly as-is: `migrate_cicd.py` discovers every88project by walking for `.keboola/manifest.json` and reuses that folder's existing89path verbatim in every generated step (`--directory '{p.directory}'`); it never90renames, moves, or re-derives the folder from the alias. Inside that folder the91branch subdirectory (`main/`, etc.) and the `storage/`-adjacent component-type92folders are unchanged too — only the file format one level below (`config.json`93→ `_config.yml`) changes. **Do not** use `kbagent sync --all-projects` to "adopt"94this layout — it enforces its own `<directory>/<alias>/` convention (see the95guardrail below) and would rename/duplicate the tree; the generated CI never96uses it for exactly this reason.9798## Prerequisites — ask for these before Step 199100Four things this skill cannot infer; get them from the customer/operator first:101102- **The repo path.** Where the `kbc`-managed tree (with `.keboola/manifest.json`103 and `.github/workflows/`) actually lives locally — Step 1's `migrate_cicd.py`104 argument.105- **A `kbagent` binary or install.** Either already on `PATH`, or install it now:106 `uv tool install keboola-cli==<ver>` (see Step 2 for version pin) or a107 downloaded standalone binary. No local `kbc` binary is required to *run* the108 migration (kbagent is the only tool that touches the repo from Step 3b on) —109 only to *verify* the "same data, new layout" claim by diffing a `kbc pull`110 against a `kbagent sync pull` of the same project, which is optional.111- **Auth for each project being converted — two different answers for CI vs. the112 local conversion step.** The generated CI workflows (Step 3/4) always need a113 static per-project Storage API token secret (`KBC_TOKEN_<ALIAS>`,114 `KBAGENT_PROJECT_FROM_ENV=1`) — `kbagent auth login` is browser-based and115 cannot run unattended on a GitHub Actions runner, so there is no login-based116 alternative for CI. For the **local, interactive** one-time conversion in Step117 3b, though, a raw token is not the only option: if the operator already has118 (or runs) `kbagent auth login` + `auth register-projects`, they get a119 registered alias with a session token and can run `kbagent sync init/pull120 --project <alias> --directory <DIR>` directly — no `KBAGENT_PROJECT_FROM_ENV`/121 `KBC_TOKEN` env-injection needed, since that dance exists specifically to122 bridge CI's no-persisted-config environment. Either way, never ask the123 customer to paste a token into chat; read it via the clipboard-secret pattern124 or point at wherever they already store it and reference it by path/alias.125126## How to run this — ask the customer, don't auto-pilot127128This skill reads like a linear script, and it is tempting to run Steps 1-6129end-to-end without stopping. **Don't.** Every step below that touches the130customer's repo or their live Keboola project is a decision the customer131should make, not one you make for them — this is their production CI/CD and132their production data. Treat the numbered steps as a checklist of decisions to133surface, not a batch job to execute. Concretely, stop and ask before you:134135- **Pick the version pin (Step 2).** Don't default to "latest" or to whatever136 version happens to be installed on your machine — ask which lane (prod vs.137 scratch) this repo is, and let the answer decide pinned vs. unpinned.138- **Write anything (`--write` in Step 3).** Show the dry-run output first,139 let the customer review the projects/legacy-files inventory, then ask140 before generating files into their repo.141- **Run `sync init` / `sync pull` against a real project (Step 3b).** This is142 the breaking, one-time conversion — confirm which project to convert first143 (always non-prod), and confirm the customer is fine with the change freeze144 being in effect before you touch anything. Use plain `sync init` (no145 `--adopt-existing`, see Step 3b) and require a genuinely empty `sync status`146 before calling a project converted.147- **Delete any file** — orphaned `config.json`/`meta.json`, or the now-empty148 kbc-only type folders (`app/`, `processor/`, `_shared/`, see the reality149 check above). Show what you're about to delete and why (they are no longer150 read by kbagent) and let the customer say go, especially the first time151 through — don't `git rm` on their behalf and mention it after the fact.152- **Choose the branching model (Step 5).** Single-branch vs. git-branching is153 a workflow/process decision for their team, not a default you pick because154 it's "usually better." Present the decision table and wait for their answer.155- **Enable the push lane / run a real `sync push`.** Dry-run first, always;156 a real push against a customer's project needs their explicit go-ahead157 every time, not just once at the start of the migration.158159If you're running this yourself against a live project to verify the skill's160claims (as opposed to guiding a customer), the same discipline still applies:161narrate what you're about to run and why before you run it, rather than162chaining the whole sequence unattended — that's how stale/incorrect claims go163unnoticed in a skill like this one, and how you end up cleaning up things you164didn't realize you'd need to.165166## Workflow167168### Step 1 — Analyze the existing repo (always start here)169Run the engine in dry-run mode to inventory projects and the legacy CI it replaces:170171```bash172python <skill_dir>/scripts/migrate_cicd.py /path/to/repo173```174175It prints every project found (one per `.keboola/manifest.json`), each project's176id / stack host / required token secret name, any `ignoredComponents` ("subset of177a project" — see Step 5), and the legacy `kbc` workflow/action files it supersedes.178179### Step 2 — Pick a version pin (decide before generating)180- **Pinned (recommended for prod lanes):** `--version X.Y.Z` (PyPI) or181 `--git-ref vX.Y.Z` (git tag). Reproducible CI. Check the182 [latest release](https://pypi.org/project/keboola-cli/) for the current `X.Y.Z`.183- **Unpinned (`keboola-cli`, resolves to latest):** only acceptable for a184 non-prod/scratch lane. Warn the user: unpinned + the current auto-update behavior185 means non-deterministic CI runs.186187### Step 3 — Generate the clean workflows188```bash189python <skill_dir>/scripts/migrate_cicd.py /path/to/repo --write \190 --version X.Y.Z --main-branch main --schedule "0 * * * *"191```192Produces:193- `.github/workflows/kbagent-validate.yml` — on PR: `sync diff` + `sync push --dry-run` per project (read-only drift + secret-encryption preflight).194- `.github/workflows/kbagent-pull.yml` — manual + optional cron: `sync pull --force` per project, commits state back.195- `.github/workflows/kbagent-push.yml` — manual, **GitHub-Environment-gated**: `sync push` per project, with an `allow_delete` input.196197The legacy `kbc` files are **left in place** — review the new ones, then delete the198old workflows/actions in the same PR.199200### Step 3b — Perform the one-time config conversion (dedicated branch)201This is the breaking part. On a fresh migration branch, for each project, convert202the JSON tree to kbagent's YAML tree and remove the orphaned kbc files.203204**Recommended mechanic: plain `sync init` — do NOT use `--adopt-existing`** (see205the reality-check note above for why: inherited row paths make `sync status`206permanently dirty and risk `sync push` creating duplicate configs). The exact207per-project command sequence — including the required `rm .keboola/manifest.json`208prep step, the acceptance criteria, and why it's an ordinary commit on an ordinary209branch (never a git-history rewrite, even though the reformat diff is huge) — is210maintained once in211[references/migration-runbook.md](references/migration-runbook.md) ("PR 1 —212Conversion"); follow it verbatim rather than re-deriving the steps here.213214### Step 4 — Set up GitHub secrets, variables, environments215The engine prints exact `gh` commands. The model: **one Storage API token secret216per project** (`KBC_TOKEN_<ALIAS>`), and two **Environments** (`prod`, `dev`) so217prod pushes require approval. See [references/secrets-setup.md](references/secrets-setup.md)218for the full mapping from the old `secrets.KBC_SAPI_TOKEN_*` / `vars.KBC_*` scheme.219220### Step 5 — Confirm scope ("subset of a project") and branching221- **Subset:** if a project should only sync part of its config tree, set222 `ignoredComponents` (and/or `allowedBranches`) in that project's223 `.keboola/manifest.json` — `kbagent sync` honors both, exactly like `kbc`.224- **Branching:** the old model used a fixed branch id per env. The new model maps225 git branch → Keboola dev branch via `.keboola/branch-mapping.json` +226 `kbagent sync branch-link`. For PR-based promotion this is usually *better*:227 a PR branch links to a Keboola dev branch, `main` pushes to production. This228 is a per-project runtime choice, not something the generator needs to know229 about — run `kbagent sync init --git-branching` and walk the user through230 `branch-link` if they want per-PR isolated dev branches (see231 [references/branching-model.md](references/branching-model.md)). If they232 want to keep the simple single-branch (production) model, leave233 branch-mapping at the default (null = production) and skip this entirely.234235### Step 6 — Validate before merging236- Open the migration PR; the `kbagent-validate` workflow runs `sync diff` — confirm237 the diff is empty (no unintended drift) against each project.238- Manually run `kbagent-pull` once **against the already-converted tree** and239 confirm it's a no-op: `git diff` should be empty (or near-empty). This checks240 that nothing drifted between the conversion commit and now — it is not the241 same comparison as the one-time JSON→YAML conversion diff in Step 3b, which242 is expected to touch every config file.243- Do a `kbagent-push` dry-run (the validate workflow already does this) and read244 the planned changes before the first real gated push.245246## Guardrails (state these to the user)247- **Never** add `--allow-plaintext-on-encrypt-failure` to CI push — it silently248 uploads `#`-secrets in cleartext if the Encryption API is down. The generated249 push is fail-closed by design.250- `sync push --force` deletes remote configs removed locally. It is wired to251 the `allow_delete` workflow input (default off). Treat it like the old `--force`.252- Tokens live **only** in GitHub secrets and are injected as env vars per step; the253 generated workflows never write a `config.json` to disk.254- **Never run `--all-projects` in a directory that also holds a flat single-project255 tree.** `--all-projects` (`sync pull --all-projects` / `sync push --all-projects`)256 is hard-coded to a `<directory>/<alias>/` layout for *every* registered project257 alias (`_sync_bulk.py`) — it is not "operate on whatever is in this directory."258 Confirmed live: registering a persistent alias with `kbagent project add --project259 153 ...` (needed for ad-hoc `config update`/`config detail` maintenance work260 outside the CI flow) and then running `sync pull --all-projects` in a directory261 that already had a flat manifest at `./.keboola/manifest.json` silently262 **auto-created a second, separate tree at `./153/`** — `pull_all` auto-inits any263 registered alias with no manifest yet at its expected subpath, it does not detect264 or reuse an existing flat-layout manifest for the same project. `push_all` is265 slightly safer (skips instead of auto-creating) but still expects the same266 subfolder convention. This is why the generated CI workflows never use267 `--all-projects` — every step passes `--project __env__ --directory268 '{directory}'` explicitly (see `migrate_cicd.py`'s `_project_step`). Carry the269 same discipline into any manual/maintenance commands you run outside CI: always270 `--project ALIAS --directory DIR` explicit, never `--all-projects`, in a271 migration repo. If a customer (or you, helping them) already hit this, the272 extra `<alias>/` directory can simply be deleted — it holds a fresh, unrelated273 pull, not anything derived from their real tree.274275## Reference material276- [references/migration-runbook.md](references/migration-runbook.md) — **the ordered PR sequence / cutover plan** (pre-flight → conversion PR → start-over). Use this when the user asks "which PRs, what order, how do I cut over."277- [references/branching-model.md](references/branching-model.md) — **how to choose** single-branch (Model A) vs git-branching (Model B), with a decision table.278- [references/command-mapping.md](references/command-mapping.md) — kbc ↔ kbagent commands, flags, env vars.279- [references/secrets-setup.md](references/secrets-setup.md) — secrets/vars/environments migration table + `gh` setup.280- `scripts/migrate_cicd.py` — the analyzer + generator (stdlib only).