feat: kbc→kbagent CI/CD migration skill + sync CLI behavior tests - #402
feat: kbc→kbagent CI/CD migration skill + sync CLI behavior tests#402Matovidlo wants to merge 17 commits into
Conversation
|
@claude review |
There was a problem hiding this comment.
Pull request overview
This PR adds a new kbagent CI/CD migration skill (docs + a stdlib-only workflow generator) intended to help customers migrate legacy kbc GitHub Actions pipelines to kbagent sync, and it adds behavioral CLI tests to lock down key sync safety guards (explicit project selection and dry-run propagation).
Changes:
- Added
kbagent-cicd-migrationskill documentation + reference materials underplugins/kbagent/skills/. - Added
scripts/migrate_cicd.pygenerator to detect legacykbcworkflows and emitkbagent-native validate/pull/push workflows. - Added
tests/test_sync_cli_behavior.pyto pin CLI guard behavior for project selection andpush --dry-run.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
tests/test_sync_cli_behavior.py |
Adds CLI behavior regression tests for project selection guards and dry-run propagation. |
plugins/kbagent/skills/kbagent-cicd-migration/SKILL.md |
Migration skill runbook explaining the breaking JSON→YAML conversion and CI/CD cutover steps. |
plugins/kbagent/skills/kbagent-cicd-migration/scripts/migrate_cicd.py |
Analyzer + generator that discovers projects and emits GitHub Actions workflows + secrets checklist. |
plugins/kbagent/skills/kbagent-cicd-migration/references/secrets-setup.md |
Documents GitHub secrets/environments setup for the generated workflows. |
plugins/kbagent/skills/kbagent-cicd-migration/references/migration-runbook.md |
Ordered PR sequence / operational runbook for a safe migration. |
plugins/kbagent/skills/kbagent-cicd-migration/references/command-mapping.md |
Reference mapping between kbc and kbagent commands/flags/env vars. |
plugins/kbagent/skills/kbagent-cicd-migration/references/branching-model.md |
Decision guide for single-branch vs git-branching workflow models. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ry-run safety Pin the behaviors that make sync safe and that distinguish kbagent's orchestrator model from kbc's cwd-per-folder model: sync pull/diff/push require --project or --all-projects (and the two are mutually exclusive), --branch is per-project, and push --dry-run propagates the dry-run flag without writing. 6 tests via CliRunner with a mocked SyncService. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the kbagent-cicd-migration skill: a guided, evidence-based runbook plus a stdlib-only generator (scripts/migrate_cicd.py) that discovers projects from .keboola/manifest.json, detects the legacy kbc CI it replaces, and emits clean kbagent-native validate/pull/push GitHub workflows using uv tool install + kbagent sync with the KBAGENT_PROJECT_FROM_ENV auth model. References cover the kbc<->kbagent command/flag/env mapping, GitHub secrets/environments setup, the one-time breaking-conversion runbook (verified against a live project: the adopt-existing 136-delete footgun and the orphaned config.json cleanup), and the single-branch vs git-branching decision guide. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ran the recommended migration path end-to-end against project 153 (fresh kbc init pull + kbagent v0.80.0) to verify the skill's own claims: - migrate_cicd.py generated a `push` workflow using a nonexistent `--allow-delete` flag; kbagent's actual flag is `--force`. Fixed the generator and the command-mapping table. - Plain `sync init` (the recommended no-adopt-existing mechanic) cannot run verbatim against a directory straight out of `kbc pull` -- kbc and kbagent share the same manifest path (`.keboola/manifest.json`), so it fails fast with "Manifest already exists" until that one file (not the config.json/meta.json tree) is deleted first. Folded the missing `rm` step into SKILL.md and migration-runbook.md. - The "delete now-empty kbc-only type folders" cleanup step was a no-op: app/processor/_shared still hold description.md + code bodies kbagent never reads, so `find -empty -delete` matches nothing. Replaced with a full subtree removal, confirmed to leave `sync diff`/`sync status` unaffected. - Re-confirmed the `--adopt-existing` phantom-rows bug reproduces exactly as documented (9 added / 1 deleted) on a side-by-side copy of the same tree, validating the plain-init recommendation. - Added a Prerequisites section (repo path, kbagent install, per-project storage host+token, which project first) that the skill previously assumed rather than stated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ponytail pass over the skill: SKILL.md had accumulated three chronological layers of investigation history (2026-06, then two 2026-08 rounds) that each superseded the last without removing it, plus a full duplicate of migration-runbook.md's per-project conversion procedure and its git-history-rewrite warning. Collapsed to a single current-state finding and pointed Step 3b at the runbook instead of re-deriving it -- same information, 383 -> 262 lines. Also clarified the Prerequisites auth bullet: the generated CI workflows always need a static per-project Storage API token (`kbagent auth login` is browser-based and can't run unattended in CI), but the local/interactive Step 3b conversion can use an already browser-authenticated + registered project alias instead of a raw token, skipping the KBAGENT_PROJECT_FROM_ENV env-injection entirely. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
88a84da to
ee02e27
Compare
… layout - Drop the `sync init --adopt-existing ... || true` step from every generated CI run: the manifest is already committed post-conversion and checked out by actions/checkout, so init was both redundant and silently swallowing real failures (wrong token/project mismatch) via `|| true`. - Fix `--json` placement: it's a global option, not a per-subcommand flag (`kbagent --json sync diff ...`, not `sync diff --json`). - Fix wrong pull flag names in command-mapping.md (--no-storage/--no-jobs/ --with-samples, not --skip-storage/--skip-jobs/--with-table-samples). - Add the missing required --project to the sync init mapping row. - Remove the unused --git-branching flag from the generator (it never affected generation; git-branching is a per-project runtime choice made via `sync init --git-branching` + `branch-link`, documented in branching-model.md). - Fix the Step 6 "tiny diff" claim, which contradicted the reality-check's "expect a massive reformatting diff" -- clarify it's checking for drift since the conversion commit, not comparing against the original kbc tree. - De-duplicate the branching-model decision table (was in both migration-runbook.md and branching-model.md). - Explicitly document that per-project directory layout (project-id-named, L0/L1-labeled, or flat single-project) is preserved verbatim -- the generator never renames or reorganizes it, and --all-projects must not be used for exactly this reason.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Suppressed comments (6)
plugins/kbagent/skills/kbagent-cicd-migration/scripts/migrate_cicd.py:321
_guess_repo_slugfails to parse repo names containing dots because the regex excludes.in the repo segment, which can produce an incorrect--repo <owner>/<repo>in the generatedghcommands. Capture the full slug and strip an optional.gitsuffix instead.
if config.exists():
m = re.search(r"github\.com[:/]([^/]+/[^/\s.]+)", config.read_text(errors="ignore"))
if m:
return m.group(1)
plugins/kbagent/skills/kbagent-cicd-migration/scripts/migrate_cicd.py:104
discover_projectsaccepts manifests with missingproject.id/project.apiHost, which later yields invalid workflow env (e.g.KBC_STORAGE_API_URL: https://) and misleading secrets output. Treat these fields as required and skip or error on invalid manifests.
proj = data.get("project", {})
projects.append(
Project(
alias=_alias_from_dir(rel),
directory=rel,
plugins/kbagent/skills/kbagent-cicd-migration/SKILL.md:33
- The code reference
constants.py:425is incorrect (it currently points at unrelated OTLP constants), which undermines the “verified against the code” claim. Prefer stable file/symbol references (or just file paths) instead of brittle line numbers.
- `kbagent` writes per config: **`_config.yml`** (YAML, with `name`/`description`/
`parameters` hoisted + a `_configuration_extra` block) + extracted code files
(`constants.py:425`, `sync/config_format.py`).
- The first `kbagent sync pull` therefore **rewrites every configuration** into a
plugins/kbagent/skills/kbagent-cicd-migration/references/command-mapping.md:34
- The file/line references here are stale:
constants.py:163andconfig_store.py:193don’t correspond to the env-injection mechanism. Use stable symbol/file references (e.g.,constants.pyENV_PROJECT_FROM_ENVandConfigStore._inject_env_project) rather than line numbers.
| *(implicit)* | `KBAGENT_PROJECT_FROM_ENV=1` | **Required** opt-in so kbagent synthesizes an ephemeral project from the env in CI (no `config.json` on disk). See `constants.py:163`, `config_store.py:193` |
plugins/kbagent/skills/kbagent-cicd-migration/references/secrets-setup.md:50
- The
.gitignorereference is incorrect:config_store.py:359is unrelated to gitignore creation. Point to the actual helper (or drop the line number) so readers can verify the claim reliably.
- Do **not** commit `.kbagent/config.json` with tokens (the new CLI auto-writes a
`.gitignore` for its config dir — `config_store.py:359`).
- Do **not** pass `--allow-plaintext-on-encrypt-failure` in CI.
plugins/kbagent/skills/kbagent-cicd-migration/SKILL.md:40
- The parenthetical reference
sync.py:67,495appears to be stale and doesn’t point to the sync guardrails described here. Removing the line numbers (or pointing at the correct module) will avoid misleading users as the code moves.
2. **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` (`sync.py:67,495`). 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
…pilot pass 2 - Replace brittle file:line citations (constants.py:425, constants.py:163, config_store.py:193, config_store.py:359) with stable symbol references -- all four had drifted from the code they were pointing at. - discover_projects now skips (with a warning) any manifest missing project.id or project.apiHost instead of silently emitting an invalid workflow (e.g. KBC_STORAGE_API_URL: https://). - _guess_repo_slug now handles repo names containing dots (e.g. "my.repo") by anchoring on end-of-line and stripping an optional .git suffix, instead of excluding "." from the repo-name character class.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Suppressed comments (4)
plugins/kbagent/skills/kbagent-cicd-migration/scripts/migrate_cicd.py:149
- The workflow generator pins
uv tool installto the legacy distribution namekeboola-agent-cli. In this repo, end-user installation and self-update are based on the currentkeboola-clidistribution name, so generated CI can end up installing the wrong package (or fail if the legacy name is not published for the requested version).
elif version:
spec = f"keboola-agent-cli=={version}"
else:
# Unpinned: only acceptable for non-production lanes. The skill warns.
spec = "keboola-agent-cli"
plugins/kbagent/skills/kbagent-cicd-migration/references/migration-runbook.md:21
- The version pin guidance references
keboola-agent-cli==X.Y.Z, but the repo’s current distribution name used for installs/updates iskeboola-cli. Aligning this avoids copy/paste installs that target the legacy package name.
- [ ] **Pick a kbagent version** and pin it (`keboola-agent-cli==X.Y.Z` or
`git+...@vX.Y.Z`). Never unpinned on a prod lane.
plugins/kbagent/skills/kbagent-cicd-migration/references/command-mapping.md:10
- The install mapping row uses
keboola-agent-cli==<ver>, but this repo’s documented install/update distribution name iskeboola-cli. Using the legacy name here can break reproducible installs or pin the wrong artifact.
| Download Go binary zip from `keboola/keboola-as-code` GitHub release, unzip to `/usr/local/bin/kbc` | `uv tool install keboola-agent-cli==<ver>` (PyPI) or `uv tool install 'git+https://github.com/keboola/cli@<tag>'` |
plugins/kbagent/skills/kbagent-cicd-migration/SKILL.md:108
- This install example uses
keboola-agent-cli==<ver>, but the repo’s installation docs and current distribution name arekeboola-cli. Using the legacy name here can lead to users pinning/installing the wrong package.
`uv tool install keboola-agent-cli==<ver>` (see Step 2 for version pin) or a
The current PyPI distribution is keboola-cli (pyproject.toml, APP_NAME_CANDIDATES in constants.py) -- keboola-agent-cli is the legacy pre-0.63 fallback name only. The generated CI, the runbook, and the command-mapping table were all pinning `uv tool install` to the legacy name, which risks installing the wrong package or failing outright once/if the legacy distribution stops being published.
…af dir _alias_from_dir took only Path(directory).name, so nested multi-project layouts (env/prod, other/prod) collided on the same KBC_TOKEN_<ALIAS> secret name and generated CI would push two different projects with one token. Sanitize the whole relative path instead.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Suppressed comments (5)
plugins/kbagent/skills/kbagent-cicd-migration/SKILL.md:192
- This example command also hardcodes
--version 0.58.0. To keep the runbook evergreen, useX.Y.Z(or reference “current stable version”) instead of a specific historical version.
python <skill_dir>/scripts/migrate_cicd.py /path/to/repo --write \
--version 0.58.0 --main-branch main --schedule "0 * * * *"
**tests/test_sync_cli_behavior.py:83**
* Similarly, `sync push` has both `push` and `push_all` code paths. Adding an assertion that `push_all` wasn’t called makes this guard test fully cover the “must not reach the service” intent.
def test_push_without_project_is_usage_error(self, tmp_path: Path) -> None:
code, mock = _invoke(["sync", "push", "--directory", str(tmp_path)], tmp_path)
assert code == 2
mock.push.assert_not_called()
**plugins/kbagent/skills/kbagent-cicd-migration/SKILL.md:183**
* The migration guide hardcodes version `0.58.0` and states “PyPI, once published”, but this repo is already at 0.80.0. Using a stale example version can mislead users into pinning an unnecessarily old release (or thinking PyPI isn’t available). Prefer a placeholder (X.Y.Z) and optionally mention the current release as an example.
This issue also appears on line 190 of the same file.
- Pinned (recommended for prod lanes):
--version 0.58.0(PyPI, once published)
or--git-ref v0.58.0(git tag, until PyPI exists). Reproducible CI.
**plugins/kbagent/skills/kbagent-cicd-migration/scripts/migrate_cicd.py:35**
* The script docstring also hardcodes `0.58.0` / `v0.58.0` and mentions “no PyPI release yet”, which is likely to go stale. Use placeholders (`X.Y.Z` / `vX.Y.Z`) so copy/paste guidance stays accurate across releases.
python migrate_cicd.py <repo_dir> [--write] \\
[--version 0.58.0 | --git-ref vX.Y.Z] \\
[--main-branch main] [--schedule "0 * * * *"]
Examples:
# Inspect what would change (no writes):
python migrate_cicd.py ../CLI-based-sync-demo
# Generate workflows pinned to a published PyPI version:
python migrate_cicd.py ../CLI-based-sync-demo --write --version 0.58.0
# Pin to a git tag instead (no PyPI release yet):
python migrate_cicd.py ../CLI-based-sync-demo --write --git-ref v0.58.0
**tests/test_sync_cli_behavior.py:78**
* This test asserts the service method `diff` wasn’t called, but `sync diff` also has an `--all-projects` path that calls `diff_all`. Since the docstring says usage errors must not reach the service, it’s worth asserting neither `diff` nor `diff_all` was invoked so a regression can’t slip through by accidentally defaulting to the bulk path.
This issue also appears on line 79 of the same file.
def test_diff_without_project_is_usage_error(self, tmp_path: Path) -> None:
code, mock = _invoke(["sync", "diff", "--directory", str(tmp_path)], tmp_path)
assert code == 2
mock.diff.assert_not_called()
</details>
…ge-error tests - Replace the hardcoded --version 0.58.0 / v0.58.0 examples (and the "PyPI, once published" / "no PyPI yet" framing) across SKILL.md and migrate_cicd.py with an X.Y.Z placeholder -- keboola-cli has been on PyPI since well before this repo reached 0.80.0, so the old examples both pinned a stale version and implied PyPI wasn't available yet. - test_diff_without_project_is_usage_error / test_push_without_project_is_usage_error now also assert diff_all/push_all weren't called, matching the existing pull/pull_all guard test -- closes the gap where a regression routing a bad --project/--all-projects combo into the --all-projects code path could slip through undetected.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Suppressed comments (5)
tests/test_sync_cli_behavior.py:142
- This invocation also benefits from an explicit
--config-dirso the CLI cannot resolve a real on-disk config via CWD traversal. (--config-diris a global option, so it needs to be placed before thesyncsubcommand.)
result = runner.invoke(
app,
[
"--json",
"sync",
"push",
"--project",
"prod",
"--dry-run",
"--directory",
str(tmp_path),
],
)
plugins/kbagent/skills/kbagent-cicd-migration/scripts/migrate_cicd.py:221
main_branchis unused ingen_pullas well. Rename it to_main_branch(or remove it and adjust call sites) so the interface doesn’t imply the output depends on this parameter.
def gen_pull(projects: list[Project], main_branch: str, schedule: str | None) -> str:
tests/test_sync_cli_behavior.py:62
- To keep these CLI tests hermetic and avoid accidentally picking up a developer’s real local/global config (via resolve_config_dir walking up from CWD), pass an explicit
--config-dirpointing at the tmp config directory in the invocation. This reduces flakiness and makes the test intent clearer (everything should run against the temp store).
This issue also appears on line 130 of the same file.
def _invoke(args: list[str], tmp_path: Path) -> tuple[int, MagicMock]:
"""Invoke the CLI with a mocked SyncService; return (exit_code, mock)."""
store = _store(tmp_path / "config")
mock_sync = MagicMock()
with (
patch("keboola_agent_cli.cli.ConfigStore") as MockStore,
patch("keboola_agent_cli.cli.ProjectService") as MockProj,
patch("keboola_agent_cli.cli.SyncService") as MockSync,
):
MockStore.return_value = store
MockProj.return_value = ProjectService(config_store=store)
MockSync.return_value = mock_sync
result = runner.invoke(app, args)
return result.exit_code, mock_sync
plugins/kbagent/skills/kbagent-cicd-migration/SKILL.md:40
- The inline code reference
sync.py:67,495is brittle and already out of date as line numbers drift; it makes future verification harder because readers may not find the referenced logic. Prefer a stable file reference (optionally with the symbol/command names) instead of line numbers.
2. **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` (`sync.py:67,495`). 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
plugins/kbagent/skills/kbagent-cicd-migration/scripts/migrate_cicd.py:192
main_branchis unused ingen_validate, which makes the signature misleading and may trigger unused-argument lint in stricter configurations. If it’s intentionally unused, rename it to_main_branchto document that and silence unused-arg checks.
This issue also appears on line 221 of the same file.
def gen_validate(projects: list[Project], main_branch: str) -> str:
- gen_validate/gen_pull no longer take an unused main_branch parameter -- only gen_push's environment-gating expression actually needs it. - Fix another stale line-number reference (sync.py:67,495) in SKILL.md, pointing at the project-selection guard by name instead. - Tests now pass an explicit --config-dir so the CLI invocation can't resolve a real on-disk config by walking up from CWD, keeping the suite hermetic regardless of the host environment it runs on.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Suppressed comments (1)
plugins/kbagent/skills/kbagent-cicd-migration/references/command-mapping.md:24
- The command mapping table documents
sync pull/push/diffwithout--project/--all-projects, but the CLI enforces that one of those selectors must be provided (seesrc/keboola_agent_cli/commands/sync.py, which exits with usage error when neither is set). As written, these rows will fail if copy/pasted and also contradict the skill’s own “orchestrator” guardrail narrative.
| `kbc pull -d DIR --force` | `kbagent sync pull --directory DIR --force` | `--force` overrides local-vs-remote conflicts (3-way diff) |
| `kbc push -d DIR` | `kbagent sync push --directory DIR` | Encrypts `#`-secrets fail-closed before write |
| `kbc push -d DIR --force` | `kbagent sync push --directory DIR --force` | Push's `--force` removes remote configs deleted locally (there is no `--allow-delete` flag — same flag name as pull's `--force`, but a different meaning per command) |
| `kbc push --dry-run` / push-dry action | `kbagent sync push --dry-run --directory DIR` | Shows planned changes without writing |
| `kbc diff -d DIR` | `kbagent [--json] sync diff --directory DIR` | `--json` is a **global** option (before `sync`, not after `diff`); gives structured drift for CI gating |
…diff rows Those rows showed kbagent sync pull/push/diff with only --directory DIR, which the CLI rejects (--project ALIAS or --all-projects is required) -- copy-pasting them verbatim would fail with a usage error and contradicted the skill's own "orchestrator, not cwd-per-folder" guardrail.
Depends on #561 (kbagent auth pat-create/pat-revoke, v0.81.0). Updates the migration skill to recommend minting a scoped Personal Access Token (kbagent auth pat-create --project-id <id>) for each project's KBC_TOKEN_<ALIAS> secret instead of pasting a raw Storage token from the Keboola UI -- a PAT is scoped to one project, has a controllable expiry (--ttl-days), and revokes independently of the account (kbagent auth pat-revoke) without touching anything else that account can do. No change to the generated GitHub Actions YAML itself: a kbc_pat_... value is a drop-in for KBC_TOKEN under KBAGENT_PROJECT_FROM_ENV=1 (kbagent detects the prefix and sends it as Authorization: Bearer automatically), so this is purely a change in how the operator obtains the secret's value. The raw Storage token path remains documented as the fallback for stacks/accounts that can't complete auth login + TOTP step-up.
…secrets" This reverts commit 18eb782.
…docstring, tuple) Independent code-quality review found no blockers, just three minor/nit items: delete a zero-logic _install_steps_placeholder() wrapper and reference the module constant directly; narrow a test module docstring that overclaimed --branch/--all-projects guard coverage across pull/diff/push when only pull is actually exercised (the guard is duplicated identically in all three, per commands/sync.py); replace a bare 2-tuple test-helper return with a NamedTuple per this repo's "no new bare-tuple returns" convention. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
@claude review |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Suppressed comments (2)
plugins/kbagent/skills/kbagent-cicd-migration/scripts/migrate_cicd.py:362
allowedBranchesis parsed intoProject.allowed_branchesbut never surfaced in the analyzer output or used in generation, even though the module docstring claims it is read to surface scoping. Either print it in the discovery report (to make the scoping explicit) or remove the unused field/parse to avoid dead data.
for p in projects:
subset = ""
if p.ignored_components:
subset = f" [subset: {len(p.ignored_components)} ignored component(s)]"
print(
plugins/kbagent/skills/kbagent-cicd-migration/scripts/migrate_cicd.py:189
- The generated workflow uses single quotes around the discovered directory (
--directory '{p.directory}'). If a repo folder name contains a single quote (or other shell-sensitive characters), this produces an invalid shell token and the generated CI step will fail. Use shell-safe quoting (e.g.,shlex.quote) when rendering the directory argument.
f" KBC_STORAGE_API_URL: {p.stack_url}\n"
" run: |\n"
f" {prefix}sync {command} --project __env__ --directory '{p.directory}'\n"
padak
left a comment
There was a problem hiding this comment.
Review of #402 — feat: kbc→kbagent CI/CD migration skill + sync CLI behavior tests
Generated by
kbagent-pr-reviewersubagent. Verdict and findings below
are advisory; the human author retains every veto. CI-coverable issues
(lint, format, tests) are confirmed viamake check/pytest, not duplicated here.
Summary
This PR adds a new, self-contained Claude Code skill (plugins/kbagent/skills/kbagent-cicd-migration/) that guides a customer through migrating a kbc-based GitHub CI/CD pipeline to kbagent sync, plus a stdlib-only generator script (migrate_cicd.py) that emits kbagent-native GitHub Actions workflows, and 6 new CLI-behavior tests pinning sync pull/diff/push project-selection guards. It touches no src/ code and adds no CLI commands, so most of the Plugin synchronization map (CLI-surface rows) does not apply. The reference content is unusually well fact-checked against current main (v0.80.0) — every spot-checked claim (never_fetched diff semantics, COMPONENT_TYPE_MAP, sync/manifest.py field names, sync push --force semantics, "Manifest already exists" error text, ENV_PROJECT_FROM_ENV) verified correct against the live source. However, reproducing the generator against a synthetic fixture repo surfaced a real shell-injection vulnerability in the generated CI YAML (directory names are interpolated unescaped into a single-quoted run: string) and a secrets/environment-scoping inconsistency between secrets-setup.md's guidance and the generated validate workflow. Verdict: REQUEST CHANGES on the injection finding; everything else is solid.
Verdict
- Verdict: REQUEST CHANGES
- Blocking findings: 1
- Non-blocking findings: 3
- Nits: 1
Blocking findings
[B-1] plugins/kbagent/skills/kbagent-cicd-migration/scripts/migrate_cicd.py:167-189 (_project_step) — unescaped shell interpolation of p.directory in generated CI
_project_step() builds each generated workflow's run: line as
f" {prefix}sync {command} --project __env__ --directory '{p.directory}'\n".
p.directory comes straight from a real on-disk folder name discovered via
repo.glob("**/.keboola/manifest.json") (discover_projects, line 92) with no
sanitization. Reproduced live: a project directory named
foo'; touch pwned; echo ' renders as
kbagent sync pull --force --project __env__ --directory 'foo'; touch pwned; echo ''
— a syntactically valid bash command that breaks out of the quoted argument and
executes arbitrary shell commands in a step that has KBC_TOKEN /
KBC_STORAGE_API_URL secrets in its env: block, i.e. secret exfiltration is a
one-line addition to the injected payload. Any contributor who can add a folder +
.keboola/manifest.json to the customer's repo before the operator runs the
generator (a normal PR in a multi-contributor repo) controls this string. The
PR's own "security review" pass (per description) reports "no material issues" —
this class of bug is exactly what that pass should have caught. Fix: sanitize
Project.directory at discovery time (reject/escape non-[A-Za-z0-9._/-]
characters) or render with shlex.quote() in _project_step before embedding.
Non-blocking findings
[NB-1] plugins/kbagent/skills/kbagent-cicd-migration/references/secrets-setup.md:34 vs scripts/migrate_cicd.py gen_validate — environment-scoped secrets break the generated validate workflow
secrets-setup.md step 1 instructs: "Scope KBC_TOKEN_* for production
projects to the prod environment." But gen_validate() (migrate_cicd.py,
~line 192) emits a validate job with no environment: key, and it runs
sync diff + sync push --dry-run for every project including prod ones on
every pull_request. GitHub Actions only exposes environment-scoped secrets to
jobs that declare that environment: — reproduced by generating the fixture
workflow and confirming kbagent-validate.yml's validate job has no
environment: field while kbagent-push.yml's does. Following the documented
secrets setup verbatim means KBC_TOKEN_<prod-alias> resolves to an empty
string inside validate, and every PR-time diff/dry-run against a prod project
fails auth. Either add a matching environment: to the validate steps for
prod-scoped projects, or amend secrets-setup.md to say environment scoping
applies to push only (not diff/dry-run).
[NB-2] plugins/kbagent/skills/kbagent-cicd-migration/scripts/migrate_cicd.py — no unit tests for the generator itself
The PR's new test file (tests/test_sync_cli_behavior.py) only exercises
kbagent's existing sync CLI guards; it does not test migrate_cicd.py's
own logic (discover_projects, _alias_from_dir, _project_step,
detect_legacy_ci). A basic test asserting the rendered run: line is safe for
a directory name containing a single quote would have caught [B-1] before
merge. Not one of CONTRIBUTING.md's mandatory CLI-command test tiers (this is a
skill script, not a commands//services/ change), so non-blocking, but worth
adding given the script is meant to run unattended against customer repos.
[NB-3] plugins/kbagent/skills/kbagent-cicd-migration/scripts/migrate_cicd.py:159,161 — generated GitHub Action versions are one major behind this repo's own CI
The generator pins actions/checkout@v4 and astral-sh/setup-uv@v5, while
this repo's own .github/workflows/{ci,e2e,release*}.yml all use
actions/checkout@v5 and astral-sh/setup-uv@v7. Functionally harmless (older
majors still work), but since the skill is meant to hand customers "clean,
current" CI, bumping the pinned action versions to match what kbagent itself
uses would avoid handing out stale-on-day-one workflows.
Nits
[NIT-1]plugins/kbagent/skills/kbagent-cicd-migration/SKILL.md/references/*.md— the live-verification dates (2026-08-06, "kbagent v0.80.0") are baked into prose sentences rather than a single "verified against" header; harmless today (main is in fact v0.80.0) but will read as a stale citation once the CLI moves past 0.80.x, with no(since vX.Y.Z)-style tag to signal it's a point-in-time repro note rather than a version floor.
Verification log
gh pr view 402 --json title,body,files,...→ 7 files added (+1211/-0), titlefeat: kbc→kbagent CI/CD migration skill + sync CLI behavior tests, conventional prefixfeat:matches (new skill + tests) ✓gh pr diff 402/gh api repos/keboola/cli/contents/<path>?ref=f98d237a...→ fetched all 7 PR files at the actual PR head SHA (working tree stayed onclaude/code-review-pr-402-26ec37/main throughout; no checkout performed) ✓gh pr view 402 --json commits→ last commitf98d237aon 2026-08-10 (today), not 2026-06-09 — the PR was rebased/actively maintained through today despite the original open date; live-verification claims inside the docs (v0.80.0, 2026-08-06/08-10) line up with currentmain's actualpyproject.tomlversion (0.80.0) ✓ — staleness concern from the task focus does not materialize for this PR's content- Cross-checked doc claims against current
mainsource:sync/manifest.py:120-121(allowedBranches/ignoredComponentsfields) ✓,sync/config_format.pyCOMPONENT_TYPE_MAPexists ✓,services/sync_service.py"Manifest already exists" error text ✓,never_fetcheddiff/push semantics ✓,constants.pyENV_PROJECT_FROM_ENV = "KBAGENT_PROJECT_FROM_ENV"✓,config_store.py_inject_env_project/_ensure_gitignore✓,commands/sync.pypull --force/--theirsandpush --force(no--allow-delete) flag semantics matchcommand-mapping.mdword-for-word ✓ uv run pytest <fetched tests_test_sync_cli_behavior.py>(run against current main, package rebuilt viauv run) → 6 passed ✓- Ran the fetched
migrate_cicd.pyagainst a synthetic 2-project fixture repo (--write --version 0.80.0 --main-branch main --schedule "0 * * * *") → generated 3 workflow files;python3 -c "import yaml; yaml.safe_load(...)"on all 3 → valid YAML ✓ - Reproduced the shell-injection finding
[B-1]: importedmigrate_cicd.pydirectly and called_project_step()with a directory name containing'; touch pwned; echo '→ renderedrun:line breaks out of the quoted string (see finding) ✗ - Confirmed
[NB-1]by inspecting the generatedkbagent-validate.yml'svalidatejob — noenvironment:key, whilekbagent-push.yml'spushjob has one ✓ (bug reproduced, not just read from the diff) plugins/kbagent/.claude-plugin/plugin.json/.claude-plugin/marketplace.json— no per-skill registration list; Claude Code auto-discoversskills/*/SKILL.md, so no silent-drift surface applies to adding a second skill directory ✓scripts/generate_skill.pySKILL_MDconstant is hardcoded toplugins/kbagent/skills/kbagent/SKILL.md→make skill-check/make skill-gendo not touch the new skill'sSKILL.md, no CI drift risk ✓scripts/check_file_size.pyPKG_ROOT.rglobscans onlysrc/keboola_agent_cli→ the 422-linemigrate_cicd.pyunderplugins/kbagent/skills/.../scripts/is exempt fromloc-check✓- No new CLI commands added (confirmed via
git diff main...HEAD -- src/keboola_agent_cliproducing no hits ingh pr diff) →OPERATION_REGISTRY,AGENT_CONTEXT,CLAUDE.md"All CLI Commands",commands-reference.md, server routers, andkeboola-expert.md§2 matrix rows are all N/A for this PR ✓ - Grepped fetched files for realistic-token patterns and bare
except:→ none found;print()usage inmigrate_cicd.pyis acceptable (stdlib-only standalone CLI script outsidesrc/, not subject to convention #10's "no print()" rule for the package) ✓ - Did not run full
make checkagainst the PR's exact tree: doing so would require checking outf98d237a...or copying PR files into the working tree, both of which are writes to the repo working tree that this reviewer's read-only mandate forbids. Verified the new test file in isolation instead (see pytest run above); PR description claims a fullmake checkrun (5411 passed, 11 skipped) which is consistent with the repo's current scale but was not independently re-run here.
Open questions for the author
- Was the "security review" mentioned in the PR description (independent agent pass, "no material issues") run against the actual rendered YAML output, or only against the generator source read statically? A static read of
_project_stepalone is easy to mis-judge as safe because the f-string looks like a normal quoted shell arg; only rendering it with an adversarial directory name surfaces the injection.
…ection gap Reviewer padak reproduced a real vulnerability: a project directory name discovered via .keboola/manifest.json globbing was interpolated unescaped into a single-quoted shell `run:` line, so a directory like `foo'; touch pwned; echo '` breaks out of the quoted argument and executes arbitrary shell in a step that has KBC_TOKEN/KBC_STORAGE_API_URL secrets in its env: block -- a one-line addition away from secret exfiltration. Anyone who can land a folder + manifest.json in the repo before the operator runs the generator controls this string. Fixed both ends: discover_projects() now rejects a directory name outside [A-Za-z0-9._-] at discovery time (skip + warn, same pattern as the existing missing-project.id handling), and _project_step() renders --directory via shlex.quote() as defense-in-depth rather than naive single-quote wrapping. Also addresses the review's non-blocking findings: - secrets-setup.md now explains that environment-scoping KBC_TOKEN_* to `prod` makes it invisible to the (un-scoped) validate job, which needs it for its read-only diff/dry-run against prod projects -- previously the doc recommended scoping with no mention of this interaction. - Bumped actions/checkout@v4 -> v5 and astral-sh/setup-uv@v5 -> v7 in generated workflows to match what this repo's own CI pins. - Added tests/test_migrate_cicd.py (13 cases): discover_projects, _alias_from_dir, detect_legacy_ci, and a shell-injection regression test using padak's exact repro string. - Consolidated the scattered live-verification dates into one "Verified against" header per doc file instead of re-stating the date in every sentence. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Addressing the review — fixed in b48f257 (all findings; [B-1] Shell injection via unescaped [NB-1] Environment-scoping breaks [NB-2] No unit tests for the generator — Added [NB-3] Stale action versions — Bumped [NIT-1] Scattered live-verification dates — Consolidated into one "Verified against" header per doc file ( Open question — Honestly, no: the earlier "security review" pass was a static read of the generator source by an independent review agent, not a rendering with adversarial input. That's exactly the gap that let B-1 through, as you pointed out — a static read of |
padak
left a comment
There was a problem hiding this comment.
Delta review of #402 — kbc → kbagent CI/CD migration skill (fix commit b48f257)
Generated by
kbagent-pr-reviewersubagent. This is a delta review against
my prior review posted 2026-08-10 (targetf98d237a), re-verified against the
new headb48f2578after the author's fix commit. Verdict and findings below
are advisory; the human author retains every veto.
Summary
The fix commit (b48f257) genuinely closes the shell-injection hole: discover_projects()
now rejects unsafe directory names at discovery time and _project_step() independently
shlex.quote()s the value as defense-in-depth — I reproduced the original exploit string
against both layers and confirmed no injection is possible. The claimed 13-case
tests/test_migrate_cicd.py file exists, imports cleanly, and all 13 pass; the 6-case
tests/test_sync_cli_behavior.py also passes against current main. The NB-1 doc fix
is coherent. However, two of the "fully addressed" claims are only partially true:
the action-version bump (NB-3) missed one reference-doc mention that still shows the old
version, and the "consolidated into one header" claim (NIT-1) left several scattered
inline dates untouched. Neither is a regression with functional impact — verdict is
COMMENT, no blocking findings.
Priority hypothesis: nested-directory regex regression — CLEARED
I built a fixture repo with a nested project at projects/12345/.keboola/manifest.json
(the multi-project layout the PR explicitly promises to preserve) and ran the generator.
The nested project was discovered and included in all three generated workflows,
alias PROJECTS_12345, --directory projects/12345 rendered correctly. The regex is
_SAFE_DIRECTORY_RE = re.compile(r"^[A-Za-z0-9._][A-Za-z0-9._/-]*$") (migrate_cicd.py:90)
— the rest-of-string character class explicitly includes /, so nested paths pass. The
hypothesis that the B-1 fix would silently drop nested projects to a stderr warning is
not what happens. Confirmed correct behavior, no correctness regression.
Verdict
- Verdict: COMMENT
- Blocking findings: 0
- Non-blocking findings: 2
- Nits: 2
Per-prior-finding disposition
| ID | Prior severity | Status | Evidence |
|---|---|---|---|
B-1 shell injection via unescaped p.directory |
BLOCKING | RESOLVED | Exact repro string foo'; touch pwned; echo ' rejected at discover_projects() (skip + stderr warning, no workflow emitted); independently confirmed _project_step() alone (bypassing discovery) renders it via shlex.quote() into a properly '\''-escaped literal — no shell breakout at either layer. |
NB-1 secrets env-scoping breaks validate |
NON-BLOCKING | RESOLVED | secrets-setup.md now explicitly explains the scoping/visibility interaction and gives the operator two concrete unambiguous options (a)/(b). No broken default is left implicit. |
| NB-2 no unit tests for the generator | NON-BLOCKING | RESOLVED | tests/test_migrate_cicd.py — 13 cases as claimed, all pass (see Verification log). Both named regression tests exist and genuinely pin the two layers (TestDiscoverProjects::test_rejects_unsafe_directory_name, TestProjectStepShellSafety::test_directory_with_single_quote_is_shell_safe). |
| NB-3 stale pinned action versions | NON-BLOCKING | PARTIALLY RESOLVED — see [NB-1-new] below |
Generator itself (_install_steps, gen_validate/gen_pull/gen_push) correctly bumped to checkout@v5 / setup-uv@v7, matching this repo's own CI. But references/command-mapping.md — a file the fix commit did touch (to add the "Verified against" header) — still recommends the old setup-uv@v5 two lines below that header. |
| NIT-1 scattered live-verification dates | NIT | PARTIALLY RESOLVED | A consolidating "Verified against" header was added to SKILL.md, migration-runbook.md, command-mapping.md as claimed, but the commit message's framing ("consolidated ... instead of re-stating the date in every sentence") overstates it — several original per-sentence 2026-08-06 / "live-verified" mentions remain inline in SKILL.md and migration-runbook.md. |
Non-blocking findings
[NB-1-new] plugins/kbagent/skills/kbagent-cicd-migration/references/command-mapping.md:16 — install-mapping row still recommends setup-uv@v5
The fix commit bumped the generator's action pins to checkout@v5/setup-uv@v7
(migrate_cicd.py:180, verified against this repo's own .github/workflows/*.yml, which
are uniformly on v5/v7). But command-mapping.md's "kbc (old) → kbagent (new)" table
still has | Custom installcomposite action |astral-sh/setup-uv@v5+ oneuv tool
install line | — a human reading this reference doc as the authoritative mapping (the
file's own opening line calls it "Authoritative") gets a stale recommendation that
contradicts what the generator actually emits. Low impact (the generated workflow itself
is correct, and any human copying from the doc would just be one version behind), but it's
exactly the kind of doc/generator drift this PR's tooling is supposed to prevent for
end-users migrating off kbc.
[NB-2-new] plugins/kbagent/skills/kbagent/SKILL.md — hub skill's description/workflow table doesn't reference the new CI/CD-migration topic
Correction to my prior review's conclusion that "no silent-drift surface applies" — that
was too lenient. CONTRIBUTING.md's Plugin synchronization map has a row for
plugins/kbagent/skills/kbagent/SKILL.md — description / rules / workflow links: "New
topic area in description triggers; new workflow file added to bottom table | CI catches
drift: NO". This PR introduces the second skill directory ever under
plugins/kbagent/skills/ (only kbagent existed before) and a genuinely new topic area
(kbc→kbagent CI/CD migration). I re-verified at head b48f257: plugins/kbagent/skills/kbagent/SKILL.md
is untouched by this PR (git show b48f257 --stat for that path is empty) and still has
zero references to cicd/ci-cd/kbc/migration (grep hits only unrelated lines:
KBC.configuration.folderName, "KBC UI merge URL", sql-migration-workflow). This is
mitigated by the new skill carrying its own strong, keyword-rich description: frontmatter
(kbagent-cicd-migration/SKILL.md:2-13), which Claude Code's skill discovery matches
independently per-skill-directory — so the new skill is not literally undiscoverable, just
not cross-linked from the primary hub a user/agent is most likely to already have loaded.
Non-blocking given that mitigation, but worth a follow-up per the mandatory checklist in
CONTRIBUTING.md > "Documentation changes (mandatory!)".
Nits
[NIT-1]plugins/kbagent/skills/kbagent-cicd-migration/scripts/migrate_cicd.py:113— the rejection stderr message says directory names "must match[A-Za-z0-9._-]only", but the actual_SAFE_DIRECTORY_RE(line 90) permits/in every position after the first char (by design — nested project paths need it, confirmed above). The message is misleading for an operator debugging a skip; consider"(must start with a letter/digit/./_ and contain only those plus / -)". Same character-class note: because.is allowed everywhere after the first char, a (currently unreachable, sincediscover_projects()only ever feeds it real glob-derived descendant paths) string like"../etc"would technically pass the regex — not exploitable today, but worth a..-segment guard if this validator is ever reused for a different, less-trusted directory source.[NIT-2]plugins/kbagent/skills/kbagent-cicd-migration/references/migration-runbook.md:58,97andSKILL.md:61— per-sentence2026-08-06/ "live-verified" / "confirmed live" mentions remain despite the commit message describing the date consolidation as complete; harmless but slightly overclaims the fix's scope.
Verification log
gh pr view 402 --json headRefOid→b48f25782787fbd6b3477292f72d521dafa7bbf7, matches the SHA given in the task ✓gh pr diff 402→ 1437-line diff, 8 files touched (skill tree + 2 test files), nosrc/changes ✓- Built a fixture repo with
projects/12345/.keboola/manifest.json(nested layout) + a root-level manifest, ranmigrate_cicd.py(fetched atb48f257) → both projects discovered,projects/12345correctly rendered with--directory projects/12345,alias=PROJECTS_12345— hypothesis CLEARED ✓ - Built a fixture repo with directory name
foo'; touch pwned; echo ', ran generator dry-run and--write→ project skipped at discovery (! skipping ... unsafe directory name), zero workflow files reference it, nopwnedfile created anywhere ✓ - Called
_project_step()directly with aProjectwhosedirectoryis the malicious string (bypassingdiscover_projects()entirely) → rendered as--directory 'foo'"'"'; touch pwned; echo '"'"'', a correctlyshlex.quote()-escaped single argument; nopwnedfile created — confirms defense-in-depth holds even if the discovery-layer guard were ever bypassed ✓ - Copied fetched
tests/test_migrate_cicd.py+migrate_cicd.pyinto an isolated scratch tree mirroring the real relative path, ran with this repo's own.venv/bin/pytest→ 13 passed (matches the claimed 13 cases and file's own test count viagrep -c 'def test_') ✓ - Copied fetched
tests/test_sync_cli_behavior.pyintotests/on the current (main) checkout, ran with.venv/bin/pytest→ 6 passed, temp file removed afterward, working tree confirmed clean (git status --shortempty) ✓ .venv/bin/ruff check+ruff format --checkon both fetched files → clean, already formatted ✓- Generated actual workflow YAML via the fetched generator (
--write --version 0.80.0 --schedule "0 6 * * *"), parsed all three.github/workflows/*.ymlwith PyYAML → parses without error (theon:→ boolean-True-key artifact is a standard, harmless PyYAML/YAML-1.1 quirk that GitHub's own parser handles specially; every workflow in this repo's own.github/workflows/uses the same bareon:) ✓ grep -rn "actions/checkout@\|setup-uv@" .github/workflows/*.ymlon currentmain→ uniformlycheckout@v5/setup-uv@v7(one unrelated pre-existingcheckout@v4inmcp-parity-canary.yml, not touched by this PR) — confirms the generator's version bump now matches repo convention, and surfaces the stalecommand-mapping.mdmention (NB-1-new) ✓git diff f98d237..b48f257 -- <doc files>→ isolated exactly what the fix commit changed per file, used to confirm which claims are literally true vs. partially true ✓pyproject.tomlversion onmain=0.80.0;gh release list→v0.80.0isLatest(2026-08-03); PR touches no version/changelog/plugin.json files → correctly no version bump needed for this PR (informational note in prior context, not a blocker) ✓- Could not run the full
make check(5424-test claim) in this delta review — nosrc/files changed by this PR, and the two new/changed test files were independently verified in isolation (13 + 6 = 19 tests, all passing); the arithmetic5411 (prior claim) + 13 (new file) = 5424 (new claim)is internally consistent with the author's numbers.
Open questions for the author
(none)
padak
left a comment
There was a problem hiding this comment.
Approving — the blocking finding is genuinely closed and CI is green (4/4).
Verified at b48f2578: the shell-injection fix holds at both layers. _SAFE_DIRECTORY_RE = ^[A-Za-z0-9._][A-Za-z0-9._/-]*$ splits the first character from the tail, which is the right shape — excluding - and / from the leading position stops a directory name from masquerading as a flag or an absolute path, something shlex.quote() alone would not prevent, while the tail still admits / so nested multi-project layouts survive. That last point was my main regression worry after reading the fix summary (which abbreviated the charset as [A-Za-z0-9._-]); a fixture repo with projects/12345/.keboola/manifest.json discovers and renders correctly, so the concern does not apply.
Two non-blocking follow-ups from the delta review, both one-liners — fine either in this PR or a follow-up, not worth holding the merge:
references/command-mapping.md:16still recommendsastral-sh/setup-uv@v5while the generator now emitsv7. The file opens by calling itself authoritative, so it is the one place a human is most likely to copy the stale pin from.plugins/kbagent/skills/kbagent/SKILL.mdhas no reference to the new CI/CD-migration topic area. Per CONTRIBUTING.md's Plugin synchronization map this is the "new topic area indescriptiontriggers" row, which CI does not catch. Mitigated by the new skill's own description being rich enough for discovery — flagging it so it is a deliberate choice rather than an oversight. (My first review called this N/A; that was too lenient.)
One release-time note, not a change request: this PR correctly carries no version bump, but v0.80.0 is already published and plugin.json/marketplace.json stay at it, so the skill reaches nobody until 0.81.0 ships. It also joins the pool of no-bump PRs that need backfilling into changelog.py at release — make changelog-check audits releases against the changelog, not PRs, so it will not flag the omission.
Nice work on the generator test file, and on the straight answer about the earlier security pass being a static read.
- command-mapping.md's install-mapping row still recommended astral-sh/setup-uv@v5; bumped to v7 to match what the generator itself now emits (and this repo's own CI). - Actually trim the inline 2026-08-06/"live-verified"/"confirmed live" date mentions in SKILL.md and migration-runbook.md that survived the prior commit's "Verified against" header -- that commit added the header but left several per-sentence dates untouched, overclaiming the fix's scope. - Cross-link the new kbagent-cicd-migration skill from the hub plugins/kbagent/skills/kbagent/SKILL.md (description trigger + workflow links table row) -- CONTRIBUTING.md's sync map flags a new skill topic area as a silent-drift risk CI does not catch. - Fixed the discover_projects() rejection message, which claimed directory names "must match [A-Za-z0-9._-] only" when the actual regex permits `/` in every position after the first char (by design, for nested project paths) -- now describes the real character class. Also added an explicit `..`-segment guard as defense-in-depth, since the regex alone would technically accept a literal ".." path segment. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Thanks for the delta review and the approval — and for confirming the nested-project charset shape was intentional (the Both non-blocking follow-ups from your delta review are now in
Also, since I was in there: actually trimmed the remaining inline
|
…4-char limit The pointer sentence to the sibling kbagent-cicd-migration skill pushed the frontmatter description to 1142 chars against a 1024 hard limit (issue #447), failing test_description_within_claude_desktop_limit in CI. The original description had only 23 chars of headroom, leaving no room for a full sentence. Discoverability doesn't regress: the decision-table row added earlier in this PR ("Migrating a kbc (keboola-as-code) GitHub CI/CD pipeline to kbagent sync") and the sibling skill's own description/triggers (kbc, migrate CI/CD, port GitHub Actions) already cover it.
Summary
kbagent-cicd-migrationskill (plugins/kbagent/skills/kbagent-cicd-migration/): a guided, evidence-based runbook plus a stdlib-only generator (scripts/migrate_cicd.py) that migrates akbc(keboola-as-code) project-as-code GitHub CI/CD pipeline to the newkbagent syncengine..keboola/manifest.json, detects the legacykbcworkflows/actions it supersedes, and emits clean kbagent-nativevalidate/pull/pushGitHub workflows usinguv tool install+kbagent syncwith theKBAGENT_PROJECT_FROM_ENV=1auth model (per-project token secrets, GitHub-Environment approval gating, no committed tokens).--directory '<discovered-dir>'), so existing multi-project folder conventions (e.g.<project_id>folders) are preserved rather than re-derived or renamed -- confirmed in code, not just documented.tests/test_sync_cli_behavior.py): pin the project-selection guards that define kbagent's orchestrator model (--project/--all-projectsrequired and mutually exclusive,--branchis per-project) and thatpush --dry-runpropagates the dry-run flag without writing.Why
Customers run
kbc-based GitHub CI/CD (per-project pull/push PRs, multi-project promotion). The new CLI'ssyncis a manifest-compatible successor but not a drop-in: the on-disk format differs (config.json/meta.json→_config.yml) andsyncis an orchestrator over registered project aliases, not cwd-per-folder. This skill gives a safe, repeatable cutover, and the tests lock the guard behaviors that prevent wrong-target or whole-tree operations.Validated live against a real project: native pull round-trips to zero drift; the
adopt-existing → diff"136 to delete" footgun and the orphaned-config.jsoncleanup are documented in the runbook.Review pass (code quality + OWASP/security, independent agents)
Security review found no material issues: secret-handling guidance is consistently correct throughout (GitHub Environment-scoped secrets, never plaintext, explicit warning against
--allow-plaintext-on-encrypt-failurein CI, generator only ever prints secret names never values), and the test file uses an obviously-fake token with everything else mocked -- no real credentials, noeval/shell=True.Quality review found no blockers -- verdict was "ready for human review as-is" -- but three minor/nit items, all fixed in a follow-up commit:
_install_steps_placeholder()wrapper that just returned a module constant -- deleted, call sites reference the constant directly (ponytail: one less indirection layer for no benefit).--project/--all-projectsmutual-exclusion and--branch+--all-projectsconflict guards were pinned forpull/diff/pushalike, but onlypullis actually exercised (the identical guard is duplicated in all three percommands/sync.py) -- docstring narrowed to say so accurately instead of overclaiming coverage.NamedTupleper this repo's "no new bare-tuple returns" convention.Change type
Feature — migration tooling + tests. No source/CLI-command changes, no version bump.
Impact analysis
plugins/kbagent/skills/and one test file undertests/.src/, no new CLI commands, no public API or behavior change.Test plan
tests/test_sync_cli_behavior.py— 6 tests, green.make check(ruff, format, ty, skill-check, version-check, command-sync-check, changelog-check, check-error-codes, check-sentinel-guards, loc-check, full pytest suite): 5411 passed, 11 skipped, 0 failed.CLI-based-sync-demorepo (2 projects discovered, 3 legacy actions flagged, 3 valid workflows generated).Deployment
Merge & automatic deploy. No migration.
Rollback plan
Revert of this PR.
🤖 Generated with Claude Code