Skip to content

CI

CI #5

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
schedule:
# Weekly. These examples exist to work, and the way they stop working is a
# base image or a registry changing under them — which no PR would catch.
- cron: "17 6 * * 1"
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
discover:
name: Discover examples
runs-on: ubuntu-latest
outputs:
examples: ${{ steps.find.outputs.examples }}
steps:
- uses: actions/checkout@v4
- id: find
# Any top-level directory carrying a Dockerfile is an example.
run: |
examples=$(find . -maxdepth 2 -name Dockerfile -printf '%h\n' \
| sed 's|^\./||' | sort | jq -R . | jq -sc .)
echo "examples=$examples" >> "$GITHUB_OUTPUT"
echo "Found: $examples"
build:
name: Build ${{ matrix.example }}
needs: discover
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
example: ${{ fromJSON(needs.discover.outputs.examples) }}
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Build image
uses: docker/build-push-action@v6
with:
context: ./${{ matrix.example }}
push: false
load: true
tags: musher-examples/${{ matrix.example }}:ci
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Container starts and answers its readiness probe
run: |
set -euo pipefail
example="${{ matrix.example }}"
# An example may declare several Components; only the ones whose
# image ref is the ${IMAGE_REF} placeholder are built from this
# Dockerfile. Probe the first of those that declares a readiness
# path — the rest wrap public images CI has no business starting.
spec=""
for f in "$example"/musher/component*.yaml; do
ref=$(yq -r '.spec.workload.source.ref // ""' "$f")
case "$ref" in '$'*) ;; *) continue ;; esac
if yq -e '.spec.workload.health.readiness.path' "$f" >/dev/null 2>&1; then
spec="$f"; break
fi
done
probe=""
[ -n "$spec" ] && probe=$(yq -r '.spec.workload.health.readiness.path' "$spec")
if [ -z "$probe" ]; then
echo "no readiness probe declared — start-only check"
docker run --rm -d --name "ci-$example" "musher-examples/$example:ci"
sleep 3
docker ps --filter "name=ci-$example" --format '{{.Names}}' | grep -q "ci-$example"
docker rm -f "ci-$example"
exit 0
fi
port=$(yq -r '.spec.workload.endpoints.primary.containerPort' "$spec")
bp="$example/musher/blueprint.yaml"
net="ci-$example-net"
docker network create "$net" >/dev/null
# Stand in for the platform's wiring. Every Component that wraps a
# public image starts as a sidecar named after its Blueprint node,
# and each CONNECTION input on the probed Component is resolved the
# way Musher resolves it: take the producer's output template and
# substitute the producer's address for {{ self.privateAddress.* }}.
declare -a env_args=()
for node in $(yq -r '.spec.components | keys | .[]' "$bp"); do
slug=$(yq -r ".spec.components.\"$node\".componentId" "$bp" \
| sed -n 's/^\${\(.*\)_COMPONENT_ID}$/\1/p' | tr '[:upper:]_' '[:lower:]-')
f=$(grep -l "slug: $slug\$" "$example"/musher/component*.yaml || true)
[ -n "$f" ] || continue
ref=$(yq -r '.spec.workload.source.ref // ""' "$f")
case "$ref" in '$'*) continue ;; esac
docker run --rm -d --network "$net" --name "$node" "$ref" >/dev/null
echo "sidecar $node ← $ref"
done
for input in $(yq -r '.spec.contract.inputs // {} | keys | .[]' "$spec"); do
[ "$(yq -r ".spec.contract.inputs.\"$input\".suppliedBy // \"USER\"" "$spec")" = CONNECTION ] || continue
probed_node=$(yq -r ".spec.components | to_entries | map(select(.value.connections | has(\"$input\"))) | .[0].key" "$bp")
from_role=$(yq -r ".spec.components.\"$probed_node\".connections.\"$input\".fromRole" "$bp")
from_output=$(yq -r ".spec.components.\"$probed_node\".connections.\"$input\".fromOutput" "$bp")
key=$(yq -r ".spec.contract.inputs.\"$input\".target.envVarKey" "$spec")
prod_slug=$(yq -r ".spec.components.\"$from_role\".componentId" "$bp" \
| sed -n 's/^\${\(.*\)_COMPONENT_ID}$/\1/p' | tr '[:upper:]_' '[:lower:]-')
prod=$(grep -l "slug: $prod_slug\$" "$example"/musher/component*.yaml)
tmpl=$(yq -r ".spec.contract.outputs.\"$from_output\".value" "$prod")
ep=$(sed -n 's/.*{{ self\.privateAddress\.\([a-z0-9-]*\) }}.*/\1/p' <<<"$tmpl")
prod_port=$(yq -r ".spec.workload.endpoints.\"$ep\".containerPort" "$prod")
value=$(sed "s|{{ self\.privateAddress\.$ep }}|$from_role:$prod_port|" <<<"$tmpl")
env_args+=(--env "$key=$value")
echo "wired $key=$value"
done
docker run --rm -d --network "$net" --name "ci-$example" \
"${env_args[@]}" -p "18080:$port" "musher-examples/$example:ci"
for _ in $(seq 1 30); do
curl -fsS "http://localhost:18080$probe" >/dev/null 2>&1 && break
sleep 1
done
curl -fsS "http://localhost:18080$probe"
docker rm -f "ci-$example"
docker network rm "$net" >/dev/null
specs:
name: Validate specs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Every spec parses and carries the envelope
run: |
set -euo pipefail
status=0
for f in */musher/*.yaml; do
if ! yq -e '.specVersion == "v1"' "$f" >/dev/null 2>&1; then
echo "::error file=$f::missing or unsupported specVersion"; status=1; continue
fi
if ! yq -e '.kind == "COMPONENT" or .kind == "BLUEPRINT"' "$f" >/dev/null 2>&1; then
echo "::error file=$f::kind must be COMPONENT or BLUEPRINT"; status=1; continue
fi
yq -e 'has("metadata") and has("spec")' "$f" >/dev/null 2>&1 \
|| { echo "::error file=$f::missing metadata or spec"; status=1; }
echo "ok $f"
done
exit $status
- name: No floating image tags
# A floating tag builds fine and is rejected at publish. Catch it here,
# where the error names the file.
run: |
set -euo pipefail
status=0
for f in */musher/component*.yaml; do
ref=$(yq -r '.spec.workload.source.ref // ""' "$f")
case "$ref" in
# A ref starting with '$' is a placeholder the apply script
# substitutes at deploy time; there is no tag to judge yet.
'$'*|'') continue ;;
*:latest|*:main|*:master|*:stable|*:edge|*:nightly|*:dev|*:rolling)
echo "::error file=$f::floating tag '$ref' is rejected at publish"; status=1 ;;
*:*) ;;
*) echo "::error file=$f::image ref '$ref' has no tag"; status=1 ;;
esac
done
exit $status
- name: Every blueprint node resolves to a component file
run: |
set -euo pipefail
status=0
for bp in */musher/blueprint.yaml; do
dir=$(dirname "$bp")
slugs=$(yq -r '.metadata.slug' "$dir"/component*.yaml)
for node in $(yq -r '.spec.components | keys | .[]' "$bp"); do
ref=$(yq -r ".spec.components.\"$node\".componentId" "$bp")
# Placeholders are ${SLUG_COMPONENT_ID}; check the slug exists.
slug=$(echo "$ref" | sed -n 's/^\${\(.*\)_COMPONENT_ID}$/\1/p' \
| tr '[:upper:]_' '[:lower:]-')
if [ -n "$slug" ] && ! grep -qx "$slug" <<<"$slugs"; then
echo "::error file=$bp::node '$node' references unknown component '$slug'"
status=1
fi
done
done
exit $status
shell:
name: Shellcheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: shellcheck scripts/*.sh
required:
name: CI / required
if: always()
needs: [build, specs, shell]
runs-on: ubuntu-latest
steps:
- name: Fail if any job did not succeed
run: |
results='${{ join(needs.*.result, ' ') }}'
echo "job results: $results"
for r in $results; do
[ "$r" = "success" ] || { echo "a required job reported '$r'"; exit 1; }
done