A copy-paste GitHub Actions workflow for your own repository, with two jobs: a light one that checks the committed Flisk files, and a heavier one that boots a stack and waits for /health. It is adapted from this repository's own workflows (e2e-examples.yml, e2e-journeys.yml, ci.yml). Scope reminder: CI is a throwaway environment for Flisk, not a deployment path; see scope.
- Checked by running (Flisk 1.3.86 from a checkout of
main, macOS, noflisk up): the commands of the first job,flisk project --yes,flisk doctor --no-pingandflisk config verify, in a scratch git repository and in a fresh clone of it. Results are under what each command does. - Checked by running (CLI 1.3.186 built from the
feature/issue-1735-project-without-dockerbranch, macOS, Docker and Tilt removed fromPATH):flisk project --yesin a scratch git repository, andflisk upin the project it created. Results are under what the first job needs from the machine. - Not run anywhere: the workflow file itself on GitHub, and the second job. Booting a stack needs Docker networks, which could not be created on the machine this page was written on. The second job reuses the logic of
e2e-journeys.yml, which runs daily in this repository, but the exact script below was not executed. Run it once in a throwaway branch before you rely on it. - Linting: the YAML parses and every
run:block passesbash -n.actionlintwas not installed, so it was not run; this repository'sci.ymlruns it on.github/workflows/, not ondocs/. - GitLab CI and other systems: not tried. The commands are plain shell, but nothing here was tested outside GitHub Actions.
- Timings: none are claimed. The timeouts below are quoted from this repository's workflows or marked as a choice.
Save it as .github/workflows/flisk-ci.yml. The same file lives in the repository as docs/examples/ci/flisk-ci.yml.
# Copy to .github/workflows/flisk-ci.yml in your own repository.
# Two jobs: `verify` checks the committed Flisk files without starting any
# container; `boot` runs a real `flisk up` and waits for every service's /health.
# Adapted from flisk's e2e-examples.yml and e2e-journeys.yml.
name: flisk
on:
pull_request:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: flisk-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
# The flisk version CI uses. Replace it with the version your team runs; raise
# `minFliskVersion` in .flisk/project.json when you raise this.
FLISK_VERSION: "1.3.110"
# The stack `flisk up` starts in the `boot` job.
FLISK_STACK: shop
# What the `boot` job waits for, as space-separated <route>:<tilt resource>
# pairs. <route> is the path segment before /health in the URL that
# `flisk up <stack> --dry-run` prints; <tilt resource> is the service name.
HEALTH_SERVICES: "orders:orders-api"
jobs:
verify:
name: flisk config verify
runs-on: ubuntu-latest
timeout-minutes: 10 # a ceiling chosen for this recipe, not a measurement
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: 22
# `flisk doctor` looks for Tilt, a Docker daemon and the Compose plugin.
# `flisk project --yes` only writes files and does not need the runtime.
# The runner image has Docker; Tilt is not preinstalled.
- name: Install Tilt
run: curl -fsSL https://raw.githubusercontent.com/tilt-dev/tilt/master/scripts/install.sh | bash
- name: Install flisk
run: |
npm install --global "@flisk-sh/flisk@${FLISK_VERSION}"
flisk --version
# A fresh clone has neither the git-ignored .env nor .flisk/.flisk-out/, so
# `flisk config verify` alone reports them as missing.
- name: Generate the local project files
run: flisk project --yes
- name: flisk doctor
run: flisk doctor --no-ping
# Exits 1 when a generated file differs from what .flisk/project.json produces.
- name: flisk config verify
run: flisk config verify
boot:
name: flisk up and wait for /health
needs: verify
runs-on: ubuntu-latest
timeout-minutes: 30 # the value e2e-journeys.yml uses
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: 22
- name: Install Tilt
run: curl -fsSL https://raw.githubusercontent.com/tilt-dev/tilt/master/scripts/install.sh | bash
# From e2e-journeys.yml: the runner's preinstalled Postgres can hold
# port 5432. Remove it so nothing restarts it later in the job.
- name: Free port 5432 (runner's preinstalled Postgres)
run: |
sudo systemctl stop postgresql.socket 'postgresql@*.socket' 2>/dev/null || true
sudo systemctl stop postgresql.service 'postgresql@*.service' 2>/dev/null || true
sudo DEBIAN_FRONTEND=noninteractive apt-get remove --purge -y 'postgresql*' >/dev/null 2>&1 || true
sudo fuser -k 5432/tcp 2>/dev/null || true
- name: Install flisk
run: |
npm install --global "@flisk-sh/flisk@${FLISK_VERSION}"
flisk --version
- name: Generate the local project files
run: |
flisk project --yes
flisk doctor --no-ping
- name: flisk up and wait for /health
shell: bash
run: |
set -uo pipefail
mkdir -p "$RUNNER_TEMP/flisk-logs"
log="$RUNNER_TEMP/flisk-logs/up.log"
flisk up "$FLISK_STACK" > "$log" 2>&1 &
# `flisk up` prints one health URL per service. Wait for each to appear.
urls=""
for pair in $HEALTH_SERVICES; do
svc="${pair%%:*}"
url=""
for _ in $(seq 1 60); do
url="$(grep -oE "https?://[^ ]*/${svc}[^ ]*/health" "$log" | head -1 || true)"
[ -n "$url" ] && break
sleep 2
done
[ -n "$url" ] || { cat "$log"; echo "::error::flisk up never printed the $svc URL"; exit 1; }
urls="$urls $url"
done
echo "Waiting for:$urls"
failed_checks=0
pending=""
for n in $(seq 1 120); do # 120 polls of 10s: the 20 minutes e2e-journeys.yml allows
pending=""
for url in $urls; do
code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$url" || true)"
case "$code" in
2??) ;;
*) pending="$pending $url=$code" ;;
esac
done
if [ -z "$pending" ]; then
echo "All healthy after $((n * 10))s:$urls"
exit 0
fi
echo "poll $n ($((n * 10))s): waiting on$pending"
# Once a minute, ask Tilt whether a resource has failed. Tilt does not
# retry a failed image build, so that service will never turn healthy:
# give up after two checks in a row instead of waiting out the timeout.
if [ $((n % 6)) -eq 0 ]; then
bad="$(tilt get uiresources -o json 2>/dev/null | jq -r '[.items[] | select(.status.updateStatus == "error") | .metadata.name] | join(",")' || true)"
if [ -n "$bad" ]; then
failed_checks=$((failed_checks + 1))
echo "::warning::Tilt reports failed resource(s): $bad (check $failed_checks of 2)"
if [ "$failed_checks" -ge 2 ]; then
echo "::error::Giving up early: $bad failed to build and Tilt will not retry it"
exit 1
fi
else
failed_checks=0
fi
fi
sleep 10
done
tail -200 "$log"
echo "::error::not healthy within 20 minutes:$pending"
exit 1
# When the poll fails, "404 for 20 minutes" says nothing about why.
- name: Collect diagnostics
if: always()
shell: bash
run: |
out="$RUNNER_TEMP/flisk-logs"
mkdir -p "$out"
docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' > "$out/docker-ps.txt" 2>&1 || true
tilt get uiresources > "$out/tilt-resources.txt" 2>&1 || true
flisk doctor --no-ping > "$out/doctor.txt" 2>&1 || true
for c in $(docker ps -a --format '{{.Names}}'); do
docker logs --tail 200 "$c" > "$out/container-$c.log" 2>&1 || true
done
- name: Upload logs
if: always()
uses: actions/upload-artifact@v7
with:
name: flisk-logs-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/flisk-logs/
retention-days: 7
if-no-files-found: warn
# `--prune-networks` came with flisk PR #585; an older flisk rejects
# the flag, so fall back to a plain `flisk down`.
- name: flisk down
if: always()
run: flisk down --prune-networks || flisk down || trueChange three values at the top: FLISK_VERSION, FLISK_STACK and HEALTH_SERVICES. flisk up <stack> --dry-run shows the URLs a stack will print (http://api.localhost:8080/api/orders/health for the scaffolded orders-api in the shop stack, so the route is orders) without starting anything.
| Method | Used in the workflow | Notes |
|---|---|---|
npm install --global @flisk-sh/flisk@<version> |
Yes | Needs Node.js 22.12+ (README); actions/setup-node with node-version: 22 is what this repository's workflows use. Pinning the version is documented. |
curl -fsSL https://flisk.sh/install.sh | sh |
No | Prebuilt binary, no Node.js. Whether it can install an older version is not documented, so it cannot be pinned the same way. Its script was not audited (security). |
| Tilt | Yes, curl ... tilt-dev/tilt/master/scripts/install.sh | bash |
The command every workflow in this repository uses. It installs whatever the script's current default is; this repository does not pin a Tilt version. |
flisk upgrade is not used in CI: it goes to the latest release and has no version argument.
The version in the file (1.3.110) is the latest npm release at the time of writing. It was not run through this workflow. Pin the version your team develops with.
Add minFliskVersion to .flisk/project.json (upgrading). It is a floor: flisk doctor fails on an older CLI and says Run: flisk upgrade. flisk up checks it as well. The CHANGELOG lists both under Unreleased at the time of writing, so check flisk doctor on the version you pin before you depend on it.
Run for real in a scratch repository (a backend orders-api in stack shop, committed), Flisk 1.3.86, Docker and Tilt installed on that machine:
| Command | Where | Exit code |
|---|---|---|
flisk project --yes |
New repository | 0 |
flisk resource orders-api --type backend --stack shop (answering y) |
New repository | 0 |
flisk doctor --no-ping |
New repository | 0 |
flisk config verify |
New repository | 0 (All files are in sync!) |
flisk config verify |
Fresh git clone, before flisk project --yes |
1: five Missing file: .flisk/.flisk-out/... lines plus a diff |
flisk project --yes, then flisk config verify |
Fresh git clone |
0, 0 |
flisk doctor --no-ping |
Fresh clone after flisk project --yes |
0 |
flisk config verify after appending a line to the committed .tiltignore that Flisk generated |
Fresh clone | 1 |
flisk config verify after editing discovery.paths in .flisk/project.json |
Fresh clone | 1: Out of sync: .flisk/.flisk-out/spec.master |
flisk config verify --json for that edit |
Fresh clone | data.valid false, data.errors holds the same line |
Why the workflow runs flisk project --yes first: .env and .flisk/.flisk-out/ are git-ignored (gradual adoption), so a clone has neither. Verify reports the missing generated files as drift, and flisk doctor without .env fails with Missing required env variables: TILT_ENV, DB_PASSWORD. In the example project, flisk config regenerate also made verify pass without writing .env (and without touching tracked files), but doctor then failed for the missing variables, so the workflow uses flisk project --yes.
flisk project --yesexits 0 without a running Docker daemon, the Compose plugin and Tilt (observed on the branch build, with Docker and Tilt removed fromPATH: it wrote.flisk/project.jsonand the generated master files, then printed aRuntime readiness:block naming the missing Docker and Tilt instead of aCold start blockedfailure). The runtime gate now belongs toflisk up, which still exits 1 withCold start blockedand aFAIL docker / compose / tiltlist, andflisk doctorstill fails on the same three. A Flisk release that predates this change still exits 1 inflisk projectwithout the runtime, so pin a version that includes it before relying on the new behaviour. GitHub-hostedubuntu-latestrunners come with Docker, and the workflow installs Tilt, so no image is built and no container is started. Whether the preinstalled Docker on the runner satisfiesflisk doctor's Compose and healthcheck version checks was not verified on a runner.flisk config verifyon its own (afterflisk config regenerate) needed neither Docker nor Tilt onPATH: it exited 0 with both removed. Use that if you want a job without Docker, and skipflisk doctor.flisk project --yesmay change a tracked file. In the clone above it added the stackshoptoenabledStacksin.flisk/project.json, because the committed file did not list it. Commit the result; addgit diff --exit-code -- .flisk/project.jsonafter it if you want CI to fail when someone forgets.
From machine-readable CLI: for flisk config verify, drift or missing tracked master outputs exits 1 and includes a unified diff; a valid project exits 0; a missing project exits 1; invalid project JSON or an unexpected failure exits 2. In general 1 is a blocking finding or command failure and 2 is invalid arguments or an internal error. --json prints one envelope on stdout (schemaVersion, data, errors) and keeps the same exit codes. flisk doctor exits 1 on a blocking check; warnings alone do not fail it. A step with run: fails the job on any non-zero exit.
flisk project leaves an existing root .tiltignore alone (gradual adoption). flisk config verify now reports it as not generated by Flisk, left as is instead of drift, but the CHANGELOG lists that fix under Unreleased. On a flisk that predates it, flisk config verify fails with Out of sync: .tiltignore. If you hit it, move to a version that has the fix. A .tiltignore Flisk generated and someone edited is still reported (exit 1 above).
Mirrors e2e-journeys.yml, reduced to one stack:
- Install Tilt and free port 5432. The quickstart workflow explains why: the runner's preinstalled Postgres can take the port and it was restarted after being stopped, so the package is removed. This was needed there at the time; on the machine used for this page
flisk doctorchose Postgres port 15432 by default, so whether your repository needs the step was not checked. - Start
flisk up <stack>in the background, with its output in a log file, and wait for it to print one/healthURL for everyHEALTH_SERVICESentry. - Poll the URLs every 10 seconds until all answer with a 2xx status. At most 120 polls, which is the 20 minutes
e2e-journeys.ymlallows; the quickstart suite's 30-minute timeout is the value intests/e2e/journeys/quickstart/suite.json. - Once a minute, ask Tilt (
tilt get uiresources) for resources whose update status iserror. Tilt does not retry a failed image build, so after two checks in a row the job stops with the resource names instead of waiting out the timeout. The quickstart workflow also stops early when Tilt has no resource for a service (a service Flisk never discovered); that check is left out here. - Collect
docker ps -a,tilt get uiresources,flisk doctorand the last 200 lines of every container intoflisk-logs/, and upload it withactions/upload-artifact(kept 7 days, a choice). The step runs on success too, so a green run leaves the same record. flisk down --prune-networks.--prune-networkscame with PR #585 and is under Unreleased in the CHANGELOG; an older flisk rejects the flag (exit 1 for an unknown option, checked), so the step falls back toflisk down. Without the flag the project's Docker networks stay.flisk downneeds no confirmation, so--forceis unnecessary. On a throwaway runner the networks do not matter; on a self-hosted runner they do.
Not covered by the repository's own workflows, so not claimed here: caching Docker layers or the images Flisk builds locally. None of this repository's workflows does it. On a clean runner the first flisk up builds the project's golden layers and each service image from scratch; how long that takes for your stack was not measured.
flisk upstays in the background after its step ends, as ine2e-journeys.yml. If your runner image kills background processes between steps, the diagnostics andflisk downsteps will see nothing; fold them into one step.- Use a GitHub-hosted Linux runner. Windows has CLI smoke coverage only, per the comment in
e2e-journeys.yml. - The smoke block is an alternative to the
/healthpoll:flisk upruns it itself and fails with the URL and status.
The workflow uses major version tags (actions/checkout@v7, actions/setup-node@v7, actions/upload-artifact@v7), as this repository's workflows do; its zizmor.yml switches the unpinned-uses rule off on purpose, and a few actions there are pinned to a commit SHA. If your organisation requires SHA pins, replace the tags with SHAs. The workflow asks for permissions: contents: read only, and persist-credentials: false on checkout, because no step pushes. It reads no secrets.
- Adopting Flisk: step 4,
flisk config verifyin CI - Upgrading and version pinning
- Machine-readable CLI:
--jsonshapes and exit codes - Smoke check