Imported from keboola/ai-kit (
plugins/keboola-git/skills/keboola-git/SKILL.md). Install upstream withnpx skills add keboola/ai-kit --skill keboola-git. Copyright stays with the author.
Keboola-Managed Git (Forgejo) for Data Apps
Keboola python-js data apps can host their source in a Keboola-managed Git repo (Forgejo) instead of GitHub. This skill gives you the context to access those repos through the kbagent CLI: provision a repo, mint a push credential, push source in with raw git, and deploy. kbagent has no repo-copy helper — you drive git clone / git remote add / git push yourself.
What this does / when not
Use this for: reading or writing a managed Forgejo repo for a python-js data app; moving an app's source between GitHub and Keboola git; pushing source and deploying from the managed repo.
Not for: authoring the contents of keboola-config/ (nginx, supervisord, setup.sh) — that's the dataapp-developer:dataapp-development skill, which also covers Streamlit app development. This skill assumes the app source already exists somewhere; it handles the git plumbing to get it into Keboola and deployed.
Working Directory Context
All git and kbagent operations run from the user's project root or a scratch clone created under the user's CWD — never from this skill/plugin directory.
- Do clone/copy work in
./.keboola-git-work/<app>/under the user's CWD. Add.keboola-git-work/to the project.gitignoreso the scratch clone is never committed. - Before any in-place git operation on an existing repo, validate it's a worktree:
git rev-parse --is-inside-work-tree. - Never run git commands against the plugin install directory.
Prerequisites
- kbagent installed and on
PATH(kbagent --version). Install: see the kbagent docs /kbagent:kbagentskill. - Project registered with kbagent (one-time per project):
kbagent --json project add --project <alias> --stack <stack-url> --storage-token "$KBC_TOKEN" - Conversation id exported once per shell session:
export KBAGENT_CONVERSATION_ID=$(uuidgen) - Always pass
--jsonto tool calls so you can parseconfiguration_id,repo_url,git_clone_urlreliably.
1. Access the managed repo
Existing app — find its repo and config:
kbagent --json tool call get_data_apps --project <alias> --input '{}'
Note the configuration_id, data_app_id, and repo_url
(https://git.<stack>/keboola/app-<data_app_id>.git).
Create a prod app + repo — only when get_data_apps above returned no python-js app:
kbagent --json tool call modify_python_js_data_app --project <alias> \
--input '{"name":"<Display Name>","slug":"<slug>","description":"<desc>"}'
Returns configuration_id, data_app_id, repo_url.
An empty configuration_id creates, every time. It makes a new prod app and a new
managed repo, and the repo can never be attached to an existing app afterwards, so a stray
prod app is a stray repo. Look before you create, and when your context already names the app
(the page you were opened on, an id in the request) just use that id.
This skill's flow below targets the prod app's main. To stage changes on a branch behind a
preview instead, the draft flow (parent_configuration_id, deploy_data_app in dev mode)
is in plugins/dataapp-developer/skills/dataapp-development/references/python-js-prod-and-drafts.md.
2. Mint a push credential (one-time secret)
kbagent --json tool call create_python_js_data_app_git_credential --project <alias> \
--input '{"configuration_id":"<cfg>"}'
Returns git_clone_url = https://kai:<secret>@git.<stack>/keboola/app-<data_app_id>.git.
Always mint against the prod config. Drafts own no repo of their own.
- The secret is shown once. Mint a fresh one anytime — they're cheap and revocable-by-rotation.
- Keep it in a shell variable only. Never commit it, never
echoit into a file, never paste it into a log or message. Read it into a var and reference"$URL":URL='https://kai:<secret>@git.<stack>/keboola/app-<id>.git'
3. Clone / push pattern (raw git)
mkdir -p ./.keboola-git-work && cd ./.keboola-git-work
git clone --single-branch --branch <branch> <source-repo> app && cd app
git remote add keboola "$URL"
git push keboola HEAD:main
- The pre-receive hook declines branch deletes — pushing new commits to
mainor a draft branch advances normally, but you cannot delete a remote branch. - Never force-push a shared/managed branch.
4. The 15MB / HTTP 413 cap + build-at-deploy (CRITICAL)
Forgejo rejects pushes over ~15MB with HTTP 413. A single file over the limit cannot be split — you must not track it.
Push source only. Repos that commit their build (e.g. frontend/.next ~55MB, often including a 15.3MB macOS sharp binary that's also the wrong binary for the Linux runtime) must move the build into deploy time:
-
Size guard before pushing:
find . -size +15M -not -path '*/.git/*' -
If a build dir is tracked, stop tracking it and gitignore it:
git rm -r --cached frontend/.next printf '%s\n' 'frontend/.next/' 'node_modules/' >> .gitignore -
Move the build into
keboola-config/setup.shso it runs in the Linux container at deploy:# in keboola-config/setup.sh, run from the app's frontend dir: cd frontend && npm ci && npm run build # Next.js output:'standalone' does NOT copy static assets — copy them so the # standalone server.js can serve them. The destination MIRRORS the path from the # Next.js workspace root, so it differs by layout — detect where server.js landed: # find .next/standalone -maxdepth 3 -name server.js # Single-package repo (workspace root == frontend/, server.js at .next/standalone/server.js): cp -r .next/static .next/standalone/.next/static cp -r public .next/standalone/public # Monorepo (workspace root above frontend/, server.js at .next/standalone/frontend/server.js): # cp -r .next/static .next/standalone/frontend/.next/static # cp -r public .next/standalone/frontend/publicFor the exact setup.sh / nginx / supervisord wiring, cross-reference the
dataapp-developer:dataapp-developmentskill. -
Commit the source-only tree, then re-run the size guard (no tracked file >15MB).
-
Check git history, not just the working tree (CRITICAL).
git pushsends every object reachable from the pushed ref — so a build committed in an earlier commit still gets pushed and still 413s, even after step 2 removes it fromHEAD. Thefindguard only sees the working tree. Check reachable blobs:git rev-list --objects HEAD \ | git cat-file --batch-check='%(objecttype) %(objectsize) %(rest)' \ | awk '$1=="blob" && $2>15000000 {print $2, $3}'If anything prints, the over-cap blob is in history. Two remedies:
- Fresh managed repo (the common case): push a single clean commit so the old blobs
are never reachable —
git checkout --orphan clean-main && git add -A && git commit -m "Source-only (build at deploy)" git push keboola clean-main:main - History must be preserved: purge the blob with
git filter-repo(or BFG), then push.
- Fresh managed repo (the common case): push a single clean commit so the old blobs
are never reachable —
If a push still 413s, re-run both guards (working tree and history) and report the offending file — the user must decide how to externalize it (it can't be split).
5. Deploy + verify
kbagent --json tool call deploy_data_app --project <alias> \
--input '{"action":"deploy","configuration_id":"<cfg>"}'
Verify from the logs — they are the authoritative signal (build is ~90s+ for npm ci + next build; setup_sh can take ~2min total):
kbagent --allow-env-manage-token data-app logs --project <alias> --app-id <data_app_id> --lines 300
Look for, in order: ✓ Compiled successfully and Generating static pages (the next build
finished), Completed: setup_sh (the static-asset cp succeeded — under set -e a wrong
copy path would abort here), then success: node-frontend entered RUNNING state and
✓ Ready in …ms (the standalone server is up on :3000). The python service shows
success: python-api entered RUNNING state. If a deeper window is dominated by one service's
restart loop, raise --lines to see the other service's earlier startup.
Do NOT rely on an HTTP probe to confirm the frontend. An unauthenticated GET / returns
the Keboola platform login gate (HTTP 200, <title>Login</title>, no _next/static
references) — that's the platform auth proxy in front of the container, not your app. And
POST / returns 200 from the nginx location = / health rule regardless of app state. Real
proof that the built frontend serves is node-frontend entered RUNNING + Ready in in the
logs (optionally: authenticate with the app password, then check the HTML references
/_next/static/…).
Get the app password / set secrets + redeploy as needed:
kbagent --json --allow-env-manage-token data-app password --project <alias> --app-id <data_app_id>
kbagent --allow-env-manage-token data-app secrets-set --project <alias> --app-id <data_app_id> '#KEY=VAL'
Success = repo in Keboola git + app deploys + container builds + node-frontend RUNNING + backend process starts. Backend data errors — e.g. python-api crash-looping on a Storage 404 Not Found from its startup data load — mean the app is running and looking for tables that don't exist in an empty project. That's a data condition, not a git/deploy failure.
Gotchas
Full command catalog, the 413/build-at-deploy recipe, and the gotchas table live in
references/managed-git.md.