بنقرة واحدة
write-nanci-pipeline
Write or update a Nanci CI pipeline (nanci_ci.py) for a repository
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Write or update a Nanci CI pipeline (nanci_ci.py) for a repository
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
| name | write-nanci-pipeline |
| description | Write or update a Nanci CI pipeline (nanci_ci.py) for a repository |
| allowed-tools | Read Glob Grep Bash Write Edit |
You are writing a CI pipeline for Nanci, a Python-based CI system. The pipeline lives in nanci_ci.py at the repository root.
from nanci import ci, job, autocache, have_files_changed, sh, no_shell, bash
from nanci import Artifact, AsArtifact
from nanci import trigger, PushTrigger, PullRequestTrigger
import asyncio
Every CI unit of work is an async function decorated with @job. Jobs run inside Docker containers.
@job
async def build():
"make build" # string literals are shell commands (sugar for ci.exec)
f"echo {variable}" # f-strings work too
await ci.exec("make test") # explicit form — returns stdout as str
print("Done") # regular Python; stdout is captured and shown in UI
asyncio.run(build())
asyncio.gather.@job
async def parent():
result = await child_job("arg1")
asyncio.run(parent())
String literals inside a @job body are syntactic sugar and run in a stateful shell, meaning state persists across commands in the same job:
@job
async def the_job():
"VAR=123" # set variable
"export EXPORTED=abc" # export variable — visible to later exec calls
"cd /tmp" # change directory — persists for next commands
assert (await ci.exec("pwd")) == "/tmp\n"
'''
function greet() {
echo "Hello $1"
}
''' # define a shell function — callable later
"greet World"
By default jobs use bash. To use a different shell, pass shell= to @job:
from nanci import sh, no_shell, bash
@job(shell=sh) # use /bin/sh
async def posix_job():
...
@job(shell=no_shell) # run commands without a shell (no variable expansion, no builtins)
async def raw_job():
...
By default Nanci uses its own default image. To specify a different image:
@job(image="rust:latest")
async def build_rust():
"cargo build --release"
@job(image="postgres") # tag defaults to :latest when omitted
async def db_job():
...
The image is pulled automatically if not already present locally.
Transfer files between the host (where nanci_ci.py runs) and the container:
@job
async def the_job():
# Upload: host → container
await ci.upload("src/", "/app/src") # upload directory
await ci.upload("config.toml", "/etc/cfg") # upload file
# Download: container → host
await ci.download("/app/dist", "./dist") # download directory
await ci.download("/tmp/report.txt", ".") # download to containing dir
.nanciignore are excluded from uploads.Use AsArtifact() to download into a temporary artifact instead of the host filesystem, then pass the artifact to another job:
from nanci import Artifact, AsArtifact
@job
async def build():
"cargo build --release"
artifact = await ci.download("/app/target/release/mybin", AsArtifact())
return artifact # Artifact object
@job
async def deploy(artifact: Artifact):
await ci.upload(artifact, "/deploy/mybin")
"systemctl restart myservice"
@job
async def pipeline():
bin_artifact = await build()
await deploy(bin_artifact)
asyncio.run(pipeline())
Decorate a job with @autocache (below @job) to skip it automatically when nothing relevant has changed. The cache key is derived from:
ci.upload)@job
@autocache
async def install_deps():
await ci.upload("requirements.txt", "/app/requirements.txt")
"pip install -r /app/requirements.txt"
Caveat: If the job returns a value and the job is skipped on a subsequent run, the return value cannot be used — accessing it raises an error. Design cached jobs to be side-effect-only (or ensure downstream jobs are also cached).
The cache state is stored in .nanci_autocache at the project root.
have_files_changed)For finer-grained control, use have_files_changed to check whether specific files changed since the last run:
from nanci import have_files_changed
@job
async def test():
if have_files_changed(["src/", "tests/"]):
"pytest"
else:
print("No changes, skipping tests")
Cache state is stored in .nanci_manualcache.
Read the trigger that launched the pipeline (push, pull request, or none):
from nanci import trigger, PushTrigger, PullRequestTrigger
if isinstance(trigger, PushTrigger):
print(f"Push to branch: {trigger.branch}")
elif isinstance(trigger, PullRequestTrigger):
print(f"Pull request #{trigger.pr_number}")
else:
print("No trigger (local run)")
Use this to run different jobs depending on the event (e.g., deploy only on push to main).
from nanci import ci, job, autocache, trigger, PushTrigger
import asyncio
@job(image="node:20")
@autocache
async def install():
await ci.upload("package.json", "/app/package.json")
await ci.upload("package-lock.json", "/app/package-lock.json")
"cd /app && npm ci"
@job(image="node:20")
@autocache
async def build():
await install()
await ci.upload("src/", "/app/src")
"cd /app && npm run build"
await ci.download("/app/dist", "./dist")
@job(image="node:20")
@autocache
async def test():
await install()
await ci.upload("src/", "/app/src")
await ci.upload("tests/", "/app/tests")
"cd /app && npm test"
@job
async def deploy():
# only deploy on push to main
if isinstance(trigger, PushTrigger) and trigger.branch == "main":
await ci.upload("dist/", "/deploy/app")
"systemctl restart myapp"
@job
async def pipeline():
await asyncio.gather(build(), test())
await deploy()
asyncio.run(pipeline())
If the project is initialised as a uv project (i.e. pyproject.toml + uv.lock exist at the root), you can do a trial run in headless mode to check for errors:
NANCI_HEADLESS=true uv run python nanci_ci.py
You must ask the user for permission before running this command. The pipeline actually executes — it spins up containers and runs all jobs.
In headless mode stdout emits one JSON object per line. Each object has an "at" (ISO-8601 timestamp) and "type" field. The full set of event types:
| type | extra fields | meaning |
|---|---|---|
MainThreadStartMessage | — | pipeline script started |
MainThreadTerminateMessage | — | pipeline script finished normally |
JobStartMessage | job_id, name | job began |
JobEndMessage | job_id, exception (null or string) | job finished; non-null exception means failure |
JobImagePullStartMessage | job_id, image | pulling Docker image |
JobImagePullEndMessage | job_id | image pull done |
JobExecStartMessage | job_id, cmd | shell command started |
JobExecOutputMessage | job_id, chunk (base64 bytes) | stdout/stderr output from command |
JobExecEndMessage | job_id | shell command finished |
JobStartMoveMessage | job_id, direction ("upload"/"download"), src, dst | file transfer started |
JobEndMoveMessage | job_id | file transfer finished |
JobAutocacheHitMessage | job_id | job was skipped due to autocache hit |
StdStreamMessage | text | output from print() inside a job |
UncaughtExceptionMessage | exception (string) | unhandled exception in the pipeline script |
A pipeline is successful when all JobEndMessage events have exception: null and there is no UncaughtExceptionMessage. Use this output to diagnose failures and fix the pipeline before presenting it to the user.
$ARGUMENTS
Explore the repository first to understand its structure, language, build system, and test setup. Then write an appropriate nanci_ci.py that covers building, testing, and (if applicable) deploying the project. Place the file at the repository root.