Tasks: commands in CI¶
A task is any command you want to run in CI next to the tests and builds. For example: a mobile build with Expo's EAS, a smoke test against a staging API, a script that publishes docs, or a notification. Tasks are declared under tasks: in rendimiento.yaml, and each one runs as a step of the run, with its own pod, log and status.
services:
- name: api
path: api
tasks:
- name: mobile # a DNS label, unique among services, jobs and tasks
image: node:20-bookworm # any image with the tools the command needs
path: mobile # working directory; also what counts as a change
command: npx --yes eas-cli build --platform all --non-interactive --no-wait
secretEnv: { EXPO_TOKEN: expo/token } # one variable from a secret key
- name: smoke
image: curlimages/curl:8.10.1
command: curl -fsS https://api.shop.joserod.space/health
after: [api] # wait for the api's build (or another task)
when: always # also on branches and pull requests
optional: true # a failure doesn't fail the run
How a task runs¶
flowchart LR
push[push] --> plan[Plan]
plan --> t1[api:test] --> b1[api:build]
plan --> m[mobile:task]
b1 --> s[smoke:task]
b1 & m & s --> rel{all required<br/>steps succeeded?}
rel -- yes, default branch --> release[release]
- The commit is cloned into the pod, exactly as for tests and builds.
- The command runs with
sh -cinimage, frompath. - Its output streams into the run page like any other step.
- The pod and its temporary secret are deleted afterwards, whatever happened.
The task's environment:
| Variable | Value |
|---|---|
CI |
true |
GIT_SHA, GIT_BRANCH |
the commit and branch being run |
RENDIMIENTO_APP |
the app's name (also its namespace) |
your env: |
as written |
your secrets: and secretEnv: |
read from the app's secrets when the step starts |
When tasks run¶
| Situation | What happens |
|---|---|
| Push to the default branch | Runs, unless nothing under its path or watch: changed since the last release. |
| Push to another branch, or a pull request | Runs only with when: always. The default, when: deploy, keeps secrets away from unmerged code. |
Run button, or a change to rendimiento.yaml |
Runs (anything uncertain means "run everything"). |
A service it waits for (after:) failed |
Skipped. |
| A service it waits for was reused (unchanged) | Runs without waiting. |
A task at the repository root (path: ., the default) counts every change, so it runs on every push. Give a task its own path (and watch: for shared folders) so it only runs when something relevant changed. A mobile build that runs only when mobile/ changes saves both time and EAS build credits.
Success and releases¶
- A required task that fails fails the run, and nothing is released. That's what you want for checks such as a migration dry run, or a smoke test of something the release depends on.
- An optional task (
optional: true) shows as failed on the run page, but the run still succeeds and releases. - Tasks never produce images, so they don't change what a release contains.
Secrets¶
Set the values on the app's Settings → Secrets page. The secrets tasks name appear there alongside the services' secrets.
secrets: [name]loads every key of the secret that is a valid variable name, likeenvFrom.secretEnv: { VAR: secret/key }sets one variable. It wins over a whole secret.
When the step starts, the values are copied from the app's namespace into the step's own short-lived secret. The pod never gets access to the app's namespace, and the copy is deleted with the pod.
Values that appear in the log are replaced with ***. That's a safety net, not a guarantee: a value printed in pieces, encoded or transformed is not caught. Don't print secrets.
Resources and time¶
| Field | Default | Meaning |
|---|---|---|
size |
medium |
small, medium or large presets (medium: 100m CPU / 256Mi, up to 1 CPU / 512Mi). |
resources |
Override single values, as for services. | |
timeout |
the platform's STEP_TIMEOUT (45 min) |
Seconds; can only be shorter than the platform's. |
What a task can reach¶
Task pods run under the same guard rails as builds. They can reach the public internet (npm, Expo, GitHub, any public API), but not the cluster, other apps, databases, or your home network. So:
- Works: EAS builds (they run on Expo's servers), calls to public APIs and your public sites, publishing packages, notifications.
- Doesn't work yet: database migrations against the app's own Postgres, or calling a service only reachable inside the cluster. That needs a post-deploy task running in the app's namespace, which is on the roadmap.
Pre-deploy tasks¶
A task with stage: pre-deploy runs after the images are built but before the rollout. It's the place for database migrations: the schema changes before any new code serves traffic.
tasks:
- name: migrate
stage: pre-deploy
service: api # the api's NEW image, with the environment of the running api
command: python manage.py migrate --noinput
flowchart LR
ci[tests + builds pass] --> rec[release #12 recorded]
rec --> pre{pre-deploy tasks<br/>in the app's namespace}
pre -- all pass --> roll[roll out #12] --> post[post-deploy tasks<br/>+ verification]
pre -- one fails --> stop[#12 not deployed<br/>the app keeps #11]
- Where: a Job in the app's namespace, like post-deploy tasks. With
service:, it runs the service's new image with the environment of the Deployment currently running (itsDATABASE_URL, secrets,needs:). - When it fails: the release is recorded but not deployed. It shows Not deployed with the reason, the run fails, and you get an email ("release not deployed"). The app keeps running its current release, so nothing needs undoing. A blocked release can't be rolled back to, because its migration never succeeded.
- The first release: the app's environment (namespace, database, secrets) doesn't exist before it, so pre-deploy tasks are skipped with a note and run from the second release on. Make migrations safe to re-run (most migration tools are).
- Ordering:
after:refers to other pre-deploy tasks.optional: trueonly reports a failure, and the release deploys anyway.
Post-deploy tasks¶
A task with stage: post-deploy runs after the release is live, against the new version, as part of release verification:
tasks:
- name: migrate
stage: post-deploy
service: api # the api's new image and environment (DATABASE_URL from needs…)
command: python manage.py migrate --noinput
- name: smoke
stage: post-deploy
image: curlimages/curl:8.10.1
command: curl -fsS http://web/api/health && curl -fsS http://web/api/products | grep -q items
after: [migrate] # post-deploy tasks wait only for each other
- name: report
stage: post-deploy
image: curlimages/curl:8.10.1
command: ./notify-slack.sh
optional: true # a failure is reported, never rolled back
- Where: a Kubernetes Job in the app's namespace, so it reaches the app's services by name (
http://web) and its databases. It does not run in the isolated build namespace. - Which image:
image:runs that image.service:runs that service's live image with its environment: env, secrets,needs:addresses and secret files, copied from the Deployment the controller just applied. That's exactly what a migration needs. The service's data volume isn't mounted, since it belongs to the running pod. - When: once the release has rolled out healthy, alongside the verification window. Tasks without
after:start together; a task whoseafter:failed is skipped. - What a failure does: a required task that fails (non-zero exit, or
timeout, default 10 minutes) fails verification, and the release is rolled back to the last good one. The rollback email names the task and its last lines. Anoptional: truetask that fails is only reported. - What else they get:
RENDIMIENTO_APP,RENDIMIENTO_RELEASEandGIT_SHA, besides theirenv,secretsandsecretEnv(read directly from the app's namespace). - Where you see them: under each release in the Releases tab, with status, duration, the failure's reason, and a Log button. Jobs delete themselves after a day; the log stays with the release (its last 256 KiB).
Migrations belong in pre-deploy
A post-deploy migration runs after the new version's pods are already serving. Use stage: pre-deploy for migrations, and keep post-deploy for smoke tests and checks of the live release. Either way a rollback doesn't undo a migration, so write migrations that the previous version can also live with: add before you remove, the "expand and contract" pattern.
When verification is off (VERIFY_WINDOW=0 or verify.disabled), post-deploy tasks still run once the rollout is healthy, and their results are recorded, but nothing is rolled back.
Recipes¶
Expo / EAS build on every mobile change:
- Create an access token at expo.dev (Account settings → Access tokens).
- Add the task above to
rendimiento.yamland merge it. - On the app's Settings → Secrets, set secret
expowith keytoken.
--no-wait hands the build to Expo and finishes the step right away. Follow it on expo.dev. Leave --no-wait out to wait for the result, and raise timeout if needed.
Smoke test after each deploy build:
tasks:
- name: smoke
image: curlimages/curl:8.10.1
command: curl -fsS --retry 5 --retry-delay 3 https://shop.joserod.space/api/health
after: [api]
optional: true
This checks the currently deployed version: tasks run before the release of their own run. A check of the new version after it's live needs post-deploy tasks.
Reference¶
The field table is in the rendimiento.yaml reference. The code is spec.Task (fields, defaults, validation), pipeline.Plan (steps and after: dependencies), KubeExecutor.taskSecrets and maskSecrets (internal/pipeline/task.go), and skippedTasks in internal/platform/platform.go (when tasks are skipped).