wrapping-actions¶
setup-node, trivy, hadolint and docker build as functions that return steps.
Wrapping a GitHub action: a function that returns the step it stands for. Most actions are doing one of a few things, and each has a plainer form here.
flowchart LR
s0["web test"]
s1["api test"]
s2["trivy"]
s3["hadolint"]
s4["image"]
s0 --> s4
s1 --> s4
s2 --> s4
s3 --> s4
The steps this script plans for a push about which nothing is known. An arrow is a needs.
# Wrapping a GitHub action: a function that returns the step it stands for.
# Most actions are doing one of a few things, and each has a plainer form here.
#
# actions/checkout not needed: a step is given its inputs
# actions/setup-node, -python a different image, picked per step
# actions/cache caches = {name: path}
# actions/upload-artifact no equivalent: a step's output is its log
# aquasecurity/trivy-action an image that is the tool, and a command
# hadolint/hadolint-action the same
# docker/build-push-action a step in the docker image, see below
#
# Wrapping one means writing its `with:` inputs as the function's arguments,
# so that a repository's scripts read like the workflow they replace:
#
# setup_node(version = "22", run = "npm test") ~ uses: actions/setup-node
# with: node-version: 22
def setup_node(name, run, version = "22", cache_dir = "~/.npm", inputs = ["."], needs = []):
"""actions/setup-node with cache: npm."""
return step(
name,
image = "node:{}-bookworm-slim".format(version),
run = run,
inputs = inputs,
needs = needs,
caches = {"npm-" + version: cache_dir.replace("~", "/root")},
)
def setup_python(name, run, version = "3.13", needs = []):
"""actions/setup-python with cache: pip."""
return step(
name,
image = "python:{}-slim".format(version),
run = run,
needs = needs,
caches = {"pip-" + version.replace(".", "-"): "/root/.cache/pip"},
)
def trivy(name = "trivy", target = ".", severity = "HIGH,CRITICAL", scanners = "vuln,secret"):
"""aquasecurity/trivy-action. Its database changes daily, so a pass is not reused."""
return step(
name,
image = "aquasec/trivy:latest",
run = "trivy fs --scanners {} --severity {} --exit-code 1 {}".format(scanners, severity, target),
cache = False,
)
def hadolint(name = "hadolint", dockerfile = "Dockerfile", failure_threshold = "warning"):
"""hadolint/hadolint-action."""
return step(
name,
image = "hadolint/hadolint:latest-debian",
run = "hadolint --failure-threshold {} {}".format(failure_threshold, dockerfile),
inputs = [dockerfile],
)
def docker_build(name, tags, context = ".", dockerfile = "Dockerfile", build_args = {}, needs = []):
"""docker/build-push-action, without the push: build, and check the build works.
Pushing is something done once, from main, with credentials; see
branches.ci.star for a step that is never reused because it has effects.
"""
args = " ".join(["--build-arg {}={}".format(k, v) for k, v in sorted(build_args.items())])
return step(
name,
image = "docker:27-cli",
run = "docker build {} {} -f {} {}".format(" ".join(["-t " + t for t in tags]), args, dockerfile, context),
inputs = [context],
needs = needs,
)
def ci(ctx):
test = setup_node("web test", "npm ci && npm test", version = "22", inputs = ["web"])
checks = [
test,
setup_python("api test", "pip install -e . && pytest -q"),
trivy(severity = "CRITICAL"),
hadolint(),
]
image = docker_build("image", tags = ["app:ci"], build_args = {"NODE_VERSION": "22"}, needs = checks)
return checks + [image]
# Generated by `starci compile` from wrapping-actions.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:
web-test:
name: "web test"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
sparse-checkout-cone-mode: false
sparse-checkout: |
/web
- name: Has this already passed with these inputs?
id: passed
uses: actions/cache@v4
with:
path: ${{ runner.temp }}/starci-passed
key: starci-passed-web-test-7dd2f9c3a1ad596c-${{ hashFiles('web', 'web/**') }}
- name: "Cache: npm-22"
if: steps.passed.outputs.cache-hit != 'true'
uses: actions/cache@v4
with:
path: ${{ runner.temp }}/starci-cache/npm-22
key: starci-cache-npm-22-${{ github.run_id }}
restore-keys: starci-cache-npm-22-
- name: "web test"
if: steps.passed.outputs.cache-hit != 'true'
env:
STARCI_RUN: |-
npm ci && npm test
run: |
docker run --rm \
-v "$PWD:/workspace" \
-w /workspace \
-v "$RUNNER_TEMP/starci-cache/npm-22:/root/.npm" \
node:22-bookworm-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-test:
name: "api test"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Has this already passed with these inputs?
id: passed
uses: actions/cache@v4
with:
path: ${{ runner.temp }}/starci-passed
key: starci-passed-api-test-9155297462b3efdc-${{ hashFiles('**', '!.git/**') }}
- name: "Cache: pip-3-13"
if: steps.passed.outputs.cache-hit != 'true'
uses: actions/cache@v4
with:
path: ${{ runner.temp }}/starci-cache/pip-3-13
key: starci-cache-pip-3-13-${{ github.run_id }}
restore-keys: starci-cache-pip-3-13-
- name: "api test"
if: steps.passed.outputs.cache-hit != 'true'
env:
STARCI_RUN: |-
pip install -e . && pytest -q
run: |
docker run --rm \
-v "$PWD:/workspace" \
-w /workspace \
-v "$RUNNER_TEMP/starci-cache/pip-3-13:/root/.cache/pip" \
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"
trivy:
name: "trivy"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: "trivy"
env:
STARCI_RUN: |-
trivy fs --scanners vuln,secret --severity CRITICAL --exit-code 1 .
run: |
docker run --rm \
-v "$PWD:/workspace" \
-w /workspace \
aquasec/trivy:latest sh -c "$STARCI_RUN"
hadolint:
name: "hadolint"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
sparse-checkout-cone-mode: false
sparse-checkout: |
/Dockerfile
- name: Has this already passed with these inputs?
id: passed
uses: actions/cache@v4
with:
path: ${{ runner.temp }}/starci-passed
key: starci-passed-hadolint-b258bb38da604f59-${{ hashFiles('Dockerfile', 'Dockerfile/**') }}
- name: "hadolint"
if: steps.passed.outputs.cache-hit != 'true'
env:
STARCI_RUN: |-
hadolint --failure-threshold warning Dockerfile
run: |
docker run --rm \
-v "$PWD:/workspace" \
-w /workspace \
hadolint/hadolint:latest-debian 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"
image:
name: "image"
runs-on: ubuntu-latest
needs: [web-test, api-test, trivy, hadolint]
steps:
- uses: actions/checkout@v4
- name: Has this already passed with these inputs?
id: passed
uses: actions/cache@v4
with:
path: ${{ runner.temp }}/starci-passed
key: starci-passed-image-11ad3ee921a33f98-${{ hashFiles('**', '!.git/**') }}
- name: "image"
if: steps.passed.outputs.cache-hit != 'true'
env:
STARCI_RUN: |-
docker build -t app:ci --build-arg NODE_VERSION=22 -f Dockerfile .
run: |
docker run --rm \
-v "$PWD:/workspace" \
-w /workspace \
docker:27-cli 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"
# Generated by `starci compile --target circleci` from wrapping-actions.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:
web-test:
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 '/web'
- run:
name: "Identify the inputs"
command: |
git ls-files -s -- ':(top,literal)web' > /tmp/starci-inputs
- restore_cache:
name: Has this already passed with these inputs?
keys:
- starci-passed-web-test-7dd2f9c3a1ad596c-{{ 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
- restore_cache:
name: "Cache: npm-22"
keys:
- starci-cache-npm-22-
- run:
name: "web test"
environment:
STARCI_RUN: |-
npm ci && npm test
command: |
docker run --rm \
-v "$PWD:/workspace" \
-w /workspace \
-v "$HOME/starci-cache/npm-22:/root/.npm" \
node:22-bookworm-slim sh -c "$STARCI_RUN"
- save_cache:
name: "Save cache: npm-22"
key: starci-cache-npm-22-{{ .Revision }}
paths:
- ~/starci-cache/npm-22
- run:
name: "Remember this pass"
command: |
mkdir -p ~/starci-passed && date > ~/starci-passed/at
- save_cache:
key: starci-passed-web-test-7dd2f9c3a1ad596c-{{ checksum "/tmp/starci-inputs" }}
paths:
- ~/starci-passed
api-test:
machine:
image: ubuntu-2404:current
resource_class: medium
steps:
- checkout
- run:
name: "Identify the inputs"
command: |
git ls-files -s -- ':(top)' > /tmp/starci-inputs
- restore_cache:
name: Has this already passed with these inputs?
keys:
- starci-passed-api-test-9155297462b3efdc-{{ 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
- restore_cache:
name: "Cache: pip-3-13"
keys:
- starci-cache-pip-3-13-
- run:
name: "api test"
environment:
STARCI_RUN: |-
pip install -e . && pytest -q
command: |
docker run --rm \
-v "$PWD:/workspace" \
-w /workspace \
-v "$HOME/starci-cache/pip-3-13:/root/.cache/pip" \
python:3.13-slim sh -c "$STARCI_RUN"
- save_cache:
name: "Save cache: pip-3-13"
key: starci-cache-pip-3-13-{{ .Revision }}
paths:
- ~/starci-cache/pip-3-13
- run:
name: "Remember this pass"
command: |
mkdir -p ~/starci-passed && date > ~/starci-passed/at
- save_cache:
key: starci-passed-api-test-9155297462b3efdc-{{ checksum "/tmp/starci-inputs" }}
paths:
- ~/starci-passed
trivy:
machine:
image: ubuntu-2404:current
resource_class: medium
steps:
- checkout
- run:
name: "trivy"
environment:
STARCI_RUN: |-
trivy fs --scanners vuln,secret --severity CRITICAL --exit-code 1 .
command: |
docker run --rm \
-v "$PWD:/workspace" \
-w /workspace \
aquasec/trivy:latest sh -c "$STARCI_RUN"
hadolint:
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 '/Dockerfile'
- run:
name: "Identify the inputs"
command: |
git ls-files -s -- ':(top,literal)Dockerfile' > /tmp/starci-inputs
- restore_cache:
name: Has this already passed with these inputs?
keys:
- starci-passed-hadolint-b258bb38da604f59-{{ 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: "hadolint"
environment:
STARCI_RUN: |-
hadolint --failure-threshold warning Dockerfile
command: |
docker run --rm \
-v "$PWD:/workspace" \
-w /workspace \
hadolint/hadolint:latest-debian sh -c "$STARCI_RUN"
- run:
name: "Remember this pass"
command: |
mkdir -p ~/starci-passed && date > ~/starci-passed/at
- save_cache:
key: starci-passed-hadolint-b258bb38da604f59-{{ checksum "/tmp/starci-inputs" }}
paths:
- ~/starci-passed
image:
machine:
image: ubuntu-2404:current
resource_class: medium
steps:
- checkout
- run:
name: "Identify the inputs"
command: |
git ls-files -s -- ':(top)' > /tmp/starci-inputs
- restore_cache:
name: Has this already passed with these inputs?
keys:
- starci-passed-image-11ad3ee921a33f98-{{ 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: "image"
environment:
STARCI_RUN: |-
docker build -t app:ci --build-arg NODE_VERSION=22 -f Dockerfile .
command: |
docker run --rm \
-v "$PWD:/workspace" \
-w /workspace \
docker:27-cli sh -c "$STARCI_RUN"
- run:
name: "Remember this pass"
command: |
mkdir -p ~/starci-passed && date > ~/starci-passed/at
- save_cache:
key: starci-passed-image-11ad3ee921a33f98-{{ checksum "/tmp/starci-inputs" }}
paths:
- ~/starci-passed
workflows:
ci:
jobs:
- web-test
- api-test
- trivy
- hadolint
- image:
requires: [web-test, api-test, trivy, hadolint]
Download: wrapping-actions.ci.star, GitHub Actions, CircleCI