services¶
A database beside each test step, its own and ready before the step starts.
Tests that need a database get one of their own.
flowchart LR
s0["lint"]
s1["api tests"]
s2["storage tests"]
The steps this script plans for a push about which nothing is known. An arrow is a needs.
# Tests that need a database get one of their own.
#
# A service is a container started beside a step and stopped with it. The step
# reaches it by its name, here `postgres`. It belongs to the step: the two
# test steps below run at the same time, each against its own empty database,
# so neither can see what the other wrote.
postgres = service(
"postgres",
image = "postgres:17",
env = {"POSTGRES_USER": "app", "POSTGRES_PASSWORD": "app", "POSTGRES_DB": "app"},
# The step starts once this succeeds inside the service's container. It
# asks over TCP: while the image is setting a database up it runs a server
# that answers only on its socket, and that one is not the one to wait for.
ready = "pg_isready -h localhost -U app",
# The image runs what it finds here as it sets the database up. The script
# is the service's: the test steps are not given db/.
files = {"db/roles.sql": "/docker-entrypoint-initdb.d/10-roles.sql"},
)
def tests(name, path):
return step(
name,
image = "python:3.13-slim",
run = "pip install -q -e '.[test]' && pytest -q " + path,
inputs = ["src", "pyproject.toml", path, "tests/conftest.py"],
env = {"DATABASE_URL": "postgresql://app:app@postgres:5432/app"},
services = [postgres],
)
def ci(ctx):
return [
step("lint", image = "python:3.13-slim", run = "pip install -q ruff && ruff check .", inputs = ["src", "tests", "pyproject.toml"]),
tests("api tests", "tests/api"),
tests("storage tests", "tests/storage"),
]
# Generated by `starci compile` from services.ci.star. Edit that, not this.
#
# Each job is one step of the plan: the same image, command, tools and
# packages, given only the step's inputs. A job skips its work when it has
# already passed with the same inputs.
#
# What does not carry over:
# - Passes are remembered in this repository's Actions cache, which GitHub
# scopes by branch and evicts when old. starci's own store has neither
# limit.
name: CI
on:
push:
branches: ['**']
tags: ['**']
pull_request:
workflow_dispatch:
jobs:
lint:
name: "lint"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
sparse-checkout-cone-mode: false
sparse-checkout: |
/pyproject.toml
/src
/tests
- name: Has this already passed with these inputs?
id: passed
uses: actions/cache@v4
with:
path: ${{ runner.temp }}/starci-passed
key: starci-passed-lint-d87a15139de0a6d7-${{ hashFiles('pyproject.toml', 'pyproject.toml/**', 'src', 'src/**', 'tests', 'tests/**') }}
- name: "lint"
if: steps.passed.outputs.cache-hit != 'true'
env:
STARCI_RUN: |-
pip install -q ruff && ruff check .
run: |
docker run --rm \
-v "$PWD:/workspace" \
-w /workspace \
python:3.13-slim sh -c "$STARCI_RUN"
- name: Remember this pass
if: steps.passed.outputs.cache-hit != 'true'
run: mkdir -p "$RUNNER_TEMP/starci-passed" && date > "$RUNNER_TEMP/starci-passed/at"
api-tests:
name: "api tests"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
sparse-checkout-cone-mode: false
sparse-checkout: |
/pyproject.toml
/src
/tests/api
/tests/conftest.py
/db/roles.sql
- name: Has this already passed with these inputs?
id: passed
uses: actions/cache@v4
with:
path: ${{ runner.temp }}/starci-passed
key: starci-passed-api-tests-1b86b1a2577c889f-${{ hashFiles('pyproject.toml', 'pyproject.toml/**', 'src', 'src/**', 'tests/api', 'tests/api/**', 'tests/conftest.py', 'tests/conftest.py/**', 'db/roles.sql', 'db/roles.sql/**') }}
- name: "Services: postgres"
if: steps.passed.outputs.cache-hit != 'true'
run: |
mkdir -p "$(dirname "$RUNNER_TEMP/starci-services/api-tests"'/postgres/db/roles.sql')"
cp -R 'db/roles.sql' "$RUNNER_TEMP/starci-services/api-tests"'/postgres/db/roles.sql'
git sparse-checkout set --no-cone '/pyproject.toml' '/src' '/tests/api' '/tests/conftest.py'
docker network create "starci-$GITHUB_RUN_ID-${GITHUB_RUN_ATTEMPT:-1}-api-tests"
docker run -d --name "starci-$GITHUB_RUN_ID-${GITHUB_RUN_ATTEMPT:-1}-api-tests-postgres" --network "starci-$GITHUB_RUN_ID-${GITHUB_RUN_ATTEMPT:-1}-api-tests" --network-alias postgres --hostname postgres -e 'POSTGRES_DB=app' -e 'POSTGRES_PASSWORD=app' -e 'POSTGRES_USER=app' -v "$RUNNER_TEMP/starci-services/api-tests"'/postgres/db/roles.sql:/docker-entrypoint-initdb.d/10-roles.sql':ro postgres:17
until=$(($(date +%s) + 120))
until timeout 10 docker exec "starci-$GITHUB_RUN_ID-${GITHUB_RUN_ATTEMPT:-1}-api-tests-postgres" sh -c 'pg_isready -h localhost -U app'; do
if [ "$(date +%s)" -ge "$until" ]; then
echo "service postgres was not ready after 120 seconds"
docker logs --tail 20 "starci-$GITHUB_RUN_ID-${GITHUB_RUN_ATTEMPT:-1}-api-tests-postgres"
exit 1
fi
sleep 0.5
done
- name: "api tests"
if: steps.passed.outputs.cache-hit != 'true'
env:
STARCI_RUN: |-
pip install -q -e '.[test]' && pytest -q tests/api
run: |
docker run --rm \
-v "$PWD:/workspace" \
-w /workspace \
--network "starci-$GITHUB_RUN_ID-${GITHUB_RUN_ATTEMPT:-1}-api-tests" \
-e 'DATABASE_URL=postgresql://app:app@postgres:5432/app' \
python:3.13-slim sh -c "$STARCI_RUN"
- name: Stop the services
if: always() && steps.passed.outputs.cache-hit != 'true'
run: |
docker rm -f -v "starci-$GITHUB_RUN_ID-${GITHUB_RUN_ATTEMPT:-1}-api-tests-postgres" || true
docker network rm "starci-$GITHUB_RUN_ID-${GITHUB_RUN_ATTEMPT:-1}-api-tests" || true
- name: Remember this pass
if: steps.passed.outputs.cache-hit != 'true'
run: mkdir -p "$RUNNER_TEMP/starci-passed" && date > "$RUNNER_TEMP/starci-passed/at"
storage-tests:
name: "storage tests"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
sparse-checkout-cone-mode: false
sparse-checkout: |
/pyproject.toml
/src
/tests/conftest.py
/tests/storage
/db/roles.sql
- name: Has this already passed with these inputs?
id: passed
uses: actions/cache@v4
with:
path: ${{ runner.temp }}/starci-passed
key: starci-passed-storage-tests-9dd714af153780db-${{ hashFiles('pyproject.toml', 'pyproject.toml/**', 'src', 'src/**', 'tests/conftest.py', 'tests/conftest.py/**', 'tests/storage', 'tests/storage/**', 'db/roles.sql', 'db/roles.sql/**') }}
- name: "Services: postgres"
if: steps.passed.outputs.cache-hit != 'true'
run: |
mkdir -p "$(dirname "$RUNNER_TEMP/starci-services/storage-tests"'/postgres/db/roles.sql')"
cp -R 'db/roles.sql' "$RUNNER_TEMP/starci-services/storage-tests"'/postgres/db/roles.sql'
git sparse-checkout set --no-cone '/pyproject.toml' '/src' '/tests/conftest.py' '/tests/storage'
docker network create "starci-$GITHUB_RUN_ID-${GITHUB_RUN_ATTEMPT:-1}-storage-tests"
docker run -d --name "starci-$GITHUB_RUN_ID-${GITHUB_RUN_ATTEMPT:-1}-storage-tests-postgres" --network "starci-$GITHUB_RUN_ID-${GITHUB_RUN_ATTEMPT:-1}-storage-tests" --network-alias postgres --hostname postgres -e 'POSTGRES_DB=app' -e 'POSTGRES_PASSWORD=app' -e 'POSTGRES_USER=app' -v "$RUNNER_TEMP/starci-services/storage-tests"'/postgres/db/roles.sql:/docker-entrypoint-initdb.d/10-roles.sql':ro postgres:17
until=$(($(date +%s) + 120))
until timeout 10 docker exec "starci-$GITHUB_RUN_ID-${GITHUB_RUN_ATTEMPT:-1}-storage-tests-postgres" sh -c 'pg_isready -h localhost -U app'; do
if [ "$(date +%s)" -ge "$until" ]; then
echo "service postgres was not ready after 120 seconds"
docker logs --tail 20 "starci-$GITHUB_RUN_ID-${GITHUB_RUN_ATTEMPT:-1}-storage-tests-postgres"
exit 1
fi
sleep 0.5
done
- name: "storage tests"
if: steps.passed.outputs.cache-hit != 'true'
env:
STARCI_RUN: |-
pip install -q -e '.[test]' && pytest -q tests/storage
run: |
docker run --rm \
-v "$PWD:/workspace" \
-w /workspace \
--network "starci-$GITHUB_RUN_ID-${GITHUB_RUN_ATTEMPT:-1}-storage-tests" \
-e 'DATABASE_URL=postgresql://app:app@postgres:5432/app' \
python:3.13-slim sh -c "$STARCI_RUN"
- name: Stop the services
if: always() && steps.passed.outputs.cache-hit != 'true'
run: |
docker rm -f -v "starci-$GITHUB_RUN_ID-${GITHUB_RUN_ATTEMPT:-1}-storage-tests-postgres" || true
docker network rm "starci-$GITHUB_RUN_ID-${GITHUB_RUN_ATTEMPT:-1}-storage-tests" || true
- name: Remember this pass
if: steps.passed.outputs.cache-hit != 'true'
run: mkdir -p "$RUNNER_TEMP/starci-passed" && date > "$RUNNER_TEMP/starci-passed/at"
# Generated by `starci compile --target circleci` from services.ci.star. Edit that, not this.
#
# Each job is one step of the plan: the same image, command, tools and
# packages, given only the step's inputs. A job skips its work when it has
# already passed with the same inputs.
#
# What does not carry over:
# - Passes are remembered in this project's CircleCI cache, which expires.
# starci's own store does not.
version: 2.1
jobs:
lint:
machine:
image: ubuntu-2404:current
resource_class: medium
steps:
- checkout
- run:
name: "Keep only this step's inputs"
command: |
git sparse-checkout set --no-cone '/pyproject.toml' '/src' '/tests'
- run:
name: "Identify the inputs"
command: |
git ls-files -s -- ':(top,literal)pyproject.toml' ':(top,literal)src' ':(top,literal)tests' > /tmp/starci-inputs
- restore_cache:
name: Has this already passed with these inputs?
keys:
- starci-passed-lint-d87a15139de0a6d7-{{ checksum "/tmp/starci-inputs" }}
- run:
name: "Stop here if it has"
command: |
if [ -e ~/starci-passed/at ]; then
echo "Passed before with the same inputs, on $(cat ~/starci-passed/at). Nothing to do."
circleci-agent step halt
fi
- run:
name: "lint"
environment:
STARCI_RUN: |-
pip install -q ruff && ruff check .
command: |
docker run --rm \
-v "$PWD:/workspace" \
-w /workspace \
python:3.13-slim sh -c "$STARCI_RUN"
- run:
name: "Remember this pass"
command: |
mkdir -p ~/starci-passed && date > ~/starci-passed/at
- save_cache:
key: starci-passed-lint-d87a15139de0a6d7-{{ checksum "/tmp/starci-inputs" }}
paths:
- ~/starci-passed
api-tests:
machine:
image: ubuntu-2404:current
resource_class: medium
steps:
- checkout
- run:
name: "Files for the services"
command: |
mkdir -p "$(dirname "$HOME/starci-services"'/postgres/db/roles.sql')"
cp -R 'db/roles.sql' "$HOME/starci-services"'/postgres/db/roles.sql'
- run:
name: "Keep only this step's inputs"
command: |
git sparse-checkout set --no-cone '/pyproject.toml' '/src' '/tests/api' '/tests/conftest.py'
- run:
name: "Identify the inputs"
command: |
git ls-files -s -- ':(top,literal)pyproject.toml' ':(top,literal)src' ':(top,literal)tests/api' ':(top,literal)tests/conftest.py' ':(top,literal)db/roles.sql' > /tmp/starci-inputs
- restore_cache:
name: Has this already passed with these inputs?
keys:
- starci-passed-api-tests-1b86b1a2577c889f-{{ checksum "/tmp/starci-inputs" }}
- run:
name: "Stop here if it has"
command: |
if [ -e ~/starci-passed/at ]; then
echo "Passed before with the same inputs, on $(cat ~/starci-passed/at). Nothing to do."
circleci-agent step halt
fi
- run:
name: "Services: postgres"
command: |
docker network create "starci-$CIRCLE_BUILD_NUM-api-tests"
docker run -d --name "starci-$CIRCLE_BUILD_NUM-api-tests-postgres" --network "starci-$CIRCLE_BUILD_NUM-api-tests" --network-alias postgres --hostname postgres -e 'POSTGRES_DB=app' -e 'POSTGRES_PASSWORD=app' -e 'POSTGRES_USER=app' -v "$HOME/starci-services"'/postgres/db/roles.sql:/docker-entrypoint-initdb.d/10-roles.sql':ro postgres:17
until=$(($(date +%s) + 120))
until timeout 10 docker exec "starci-$CIRCLE_BUILD_NUM-api-tests-postgres" sh -c 'pg_isready -h localhost -U app'; do
if [ "$(date +%s)" -ge "$until" ]; then
echo "service postgres was not ready after 120 seconds"
docker logs --tail 20 "starci-$CIRCLE_BUILD_NUM-api-tests-postgres"
exit 1
fi
sleep 0.5
done
- run:
name: "api tests"
environment:
STARCI_RUN: |-
pip install -q -e '.[test]' && pytest -q tests/api
command: |
docker run --rm \
-v "$PWD:/workspace" \
-w /workspace \
--network "starci-$CIRCLE_BUILD_NUM-api-tests" \
-e 'DATABASE_URL=postgresql://app:app@postgres:5432/app' \
python:3.13-slim sh -c "$STARCI_RUN"
- run:
name: Stop the services
when: always
command: |
docker rm -f -v "starci-$CIRCLE_BUILD_NUM-api-tests-postgres" || true
docker network rm "starci-$CIRCLE_BUILD_NUM-api-tests" || true
- run:
name: "Remember this pass"
command: |
mkdir -p ~/starci-passed && date > ~/starci-passed/at
- save_cache:
key: starci-passed-api-tests-1b86b1a2577c889f-{{ checksum "/tmp/starci-inputs" }}
paths:
- ~/starci-passed
storage-tests:
machine:
image: ubuntu-2404:current
resource_class: medium
steps:
- checkout
- run:
name: "Files for the services"
command: |
mkdir -p "$(dirname "$HOME/starci-services"'/postgres/db/roles.sql')"
cp -R 'db/roles.sql' "$HOME/starci-services"'/postgres/db/roles.sql'
- run:
name: "Keep only this step's inputs"
command: |
git sparse-checkout set --no-cone '/pyproject.toml' '/src' '/tests/conftest.py' '/tests/storage'
- run:
name: "Identify the inputs"
command: |
git ls-files -s -- ':(top,literal)pyproject.toml' ':(top,literal)src' ':(top,literal)tests/conftest.py' ':(top,literal)tests/storage' ':(top,literal)db/roles.sql' > /tmp/starci-inputs
- restore_cache:
name: Has this already passed with these inputs?
keys:
- starci-passed-storage-tests-9dd714af153780db-{{ checksum "/tmp/starci-inputs" }}
- run:
name: "Stop here if it has"
command: |
if [ -e ~/starci-passed/at ]; then
echo "Passed before with the same inputs, on $(cat ~/starci-passed/at). Nothing to do."
circleci-agent step halt
fi
- run:
name: "Services: postgres"
command: |
docker network create "starci-$CIRCLE_BUILD_NUM-storage-tests"
docker run -d --name "starci-$CIRCLE_BUILD_NUM-storage-tests-postgres" --network "starci-$CIRCLE_BUILD_NUM-storage-tests" --network-alias postgres --hostname postgres -e 'POSTGRES_DB=app' -e 'POSTGRES_PASSWORD=app' -e 'POSTGRES_USER=app' -v "$HOME/starci-services"'/postgres/db/roles.sql:/docker-entrypoint-initdb.d/10-roles.sql':ro postgres:17
until=$(($(date +%s) + 120))
until timeout 10 docker exec "starci-$CIRCLE_BUILD_NUM-storage-tests-postgres" sh -c 'pg_isready -h localhost -U app'; do
if [ "$(date +%s)" -ge "$until" ]; then
echo "service postgres was not ready after 120 seconds"
docker logs --tail 20 "starci-$CIRCLE_BUILD_NUM-storage-tests-postgres"
exit 1
fi
sleep 0.5
done
- run:
name: "storage tests"
environment:
STARCI_RUN: |-
pip install -q -e '.[test]' && pytest -q tests/storage
command: |
docker run --rm \
-v "$PWD:/workspace" \
-w /workspace \
--network "starci-$CIRCLE_BUILD_NUM-storage-tests" \
-e 'DATABASE_URL=postgresql://app:app@postgres:5432/app' \
python:3.13-slim sh -c "$STARCI_RUN"
- run:
name: Stop the services
when: always
command: |
docker rm -f -v "starci-$CIRCLE_BUILD_NUM-storage-tests-postgres" || true
docker network rm "starci-$CIRCLE_BUILD_NUM-storage-tests" || true
- run:
name: "Remember this pass"
command: |
mkdir -p ~/starci-passed && date > ~/starci-passed/at
- save_cache:
key: starci-passed-storage-tests-9dd714af153780db-{{ checksum "/tmp/starci-inputs" }}
paths:
- ~/starci-passed
workflows:
ci:
jobs:
- lint
- api-tests
- storage-tests
Download: services.ci.star, GitHub Actions, CircleCI