Skip to content

Add backend preflight and generic agent skills #1486

Add backend preflight and generic agent skills

Add backend preflight and generic agent skills #1486

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
# Cancel in-progress runs for the same branch/PR
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-and-test:
name: Build & Test
runs-on: ubuntu-latest
# Pin to bash so `${PIPESTATUS[0]}` (a bash-only array) in the Build and
# Run tests steps is guaranteed — GitHub Actions defaults to bash on Linux
# in practice, but making it explicit avoids silent breakage if the
# default ever shifts.
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
# We deliberately do NOT cache [_opam/] here. setup-ocaml@v3 caches its
# own opam-root state at [~/.opam/] (compiler + repo metadata) keyed
# separately from anything we'd cache for [_opam/]. Caching the local
# switch alongside means the two can drift: a stale [~/.opam/] without
# the switch registered makes opam treat [_opam/] as a fresh switch and
# try to reinstall packages whose files still exist on disk — the
# classic "zarith ... META already exists" symptom we kept hitting
# whenever a cancel-in-progress run saved a half-consistent [_opam/].
# [dune-cache: true] handles the much larger speed win (avoiding
# recompilation), and the opam.ocaml.org archive mirror keeps the
# `opam install` step under ~90s by serving precompiled binaries.
- uses: ocaml/setup-ocaml@e32b06a3e831ff2fbc6f08cf35be2085e3918014 # v3.6.1
with:
ocaml-compiler: '5.4.0'
dune-cache: true
- name: Use opam.ocaml.org archive cache
# erratique.ch (upstream for cmdliner, mtime, fpath, logs, uutf, ...)
# is frequently flaky; the community cache mirrors opam-repository
# archives behind a CDN, so opam only falls back to upstream on a miss.
run: opam option 'archive-mirrors=["https://opam.ocaml.org/cache"]'
- name: Install dependencies
run: opam install . --deps-only --with-test -y
- name: Build
id: build
run: |
opam exec -- dune build 2>&1 | tee build.log
exit ${PIPESTATUS[0]}
- name: Annotate build errors
if: failure() && steps.build.outcome == 'failure'
run: |
# Parse OCaml compiler errors into GitHub annotations
python3 -c "
import re, sys
log = open('build.log').read()
# Match: File \"path\", line N, characters C-C:
for m in re.finditer(r'File \"([^\"]+)\", line (\d+), characters (\d+)-\d+:\n(.*?)(?=\nFile |\Z)', log, re.DOTALL):
f, line, col, msg = m.groups()
msg = msg.strip().replace('\n', ' ')
level = 'error' if 'Error' in msg else 'warning'
print(f'::{level} file={f},line={line},col={col}::{msg}')
"
- name: Run tests
# Single invocation — the runtest alias covers unit, expect, inline,
# and QCheck property tests, so a separate `--force` re-run would just
# repeat the entire suite. QCHECK_COUNT raises iteration depth for the
# property tests; non-property tests ignore it.
run: |
opam exec -- dune runtest 2>&1 | tee test.log
exit ${PIPESTATUS[0]}
env:
QCHECK_COUNT: "10000"
- name: Annotate test failures
if: failure() && hashFiles('test.log') != ''
run: |
python3 -c "
import re
log = open('test.log').read()
for m in re.finditer(r'File \"([^\"]+)\", line (\d+).*?:\s*(.*?FAILED.*)', log):
f, line, msg = m.groups()
print(f'::error file={f},line={line}::{msg.strip()}')
for m in re.finditer(r'FAIL\s+(.+?)\s+in\s+(\S+)\s+at\s+line\s+(\d+)', log):
name, f, line = m.groups()
print(f'::error file={f},line={line}::Test failed: {name}')
"
format:
name: Format Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Cache opam switch
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
with:
path: _opam
key: opam-${{ runner.os }}-ocaml-5.4.0-${{ hashFiles('*.opam') }}
- uses: ocaml/setup-ocaml@e32b06a3e831ff2fbc6f08cf35be2085e3918014 # v3.6.1
with:
ocaml-compiler: '5.4.0'
- name: Use opam.ocaml.org archive cache
run: opam option 'archive-mirrors=["https://opam.ocaml.org/cache"]'
- uses: ocaml/setup-ocaml/lint-fmt@e32b06a3e831ff2fbc6f08cf35be2085e3918014 # v3.6.1
shell-lint:
name: Shell Lint & Format
runs-on: ubuntu-latest
env:
SHFMT_VERSION: v3.13.1
# 2-space indent + case-item indent matches the existing scripts/ style.
SHFMT_FLAGS: "-i 2 -ci"
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Discover shell scripts
id: discover
# Match by extension *and* by shebang so files like git hooks or
# scripts without a `.sh` suffix still get checked. `git ls-files -z`
# respects .gitignore and avoids globbing surprises.
run: |
set -euo pipefail
# `if … fi` (not `&&`) so a non-matching grep doesn't fail the
# subshell under `set -e` and abort the whole pipeline.
{
git ls-files -z '*.sh' '*.bash'
git ls-files -z | while IFS= read -r -d '' f; do
[ -f "$f" ] || continue
case "$f" in *.sh|*.bash) continue ;; esac
# Only read the first line; skip binaries and large files cheaply.
if head -c 128 -- "$f" 2>/dev/null | head -n 1 \
| grep -Eq '^#![[:space:]]*((/usr/bin/env[[:space:]]+)?|/usr/bin/|/bin/)(bash|dash|ksh|sh)([[:space:]]|$)'; then
printf '%s\0' "$f"
fi
done
} | sort -zu | tee "$RUNNER_TEMP/shell-files.nul" >/dev/null
count=$(tr -cd '\0' < "$RUNNER_TEMP/shell-files.nul" | wc -c | tr -d ' ')
echo "count=$count" >> "$GITHUB_OUTPUT"
echo "Discovered $count shell script(s):"
tr '\0' '\n' < "$RUNNER_TEMP/shell-files.nul"
- name: Install shfmt
if: steps.discover.outputs.count != '0'
# Checksums pin the exact binary for SHFMT_VERSION; bumping the version
# requires regenerating both. Computed via:
# sha256sum shfmt_v3.13.1_linux_{amd64,arm64}
env:
SHFMT_SHA256_LINUX_AMD64: fb096c5d1ac6beabbdbaa2874d025badb03ee07929f0c9ff67563ce8c75398b1
SHFMT_SHA256_LINUX_ARM64: 32d92acaa5cd8abb29fc49dac123dc412442d5713967819d8af2c29f1b3857c7
run: |
set -euo pipefail
arch=$(uname -m)
case "$arch" in
x86_64) shfmt_arch="linux_amd64"; expected_sha="$SHFMT_SHA256_LINUX_AMD64" ;;
aarch64) shfmt_arch="linux_arm64"; expected_sha="$SHFMT_SHA256_LINUX_ARM64" ;;
*) echo "Unsupported architecture: $arch" >&2; exit 1 ;;
esac
curl -fsSL -o "$RUNNER_TEMP/shfmt" \
"https://github.com/mvdan/sh/releases/download/${SHFMT_VERSION}/shfmt_${SHFMT_VERSION}_${shfmt_arch}"
echo "${expected_sha} ${RUNNER_TEMP}/shfmt" | sha256sum -c -
chmod +x "$RUNNER_TEMP/shfmt"
sudo mv "$RUNNER_TEMP/shfmt" /usr/local/bin/shfmt
shfmt --version
- name: shellcheck
if: steps.discover.outputs.count != '0'
run: |
set -euo pipefail
shellcheck --version
xargs -0 shellcheck < "$RUNNER_TEMP/shell-files.nul"
- name: shfmt --diff
if: steps.discover.outputs.count != '0'
run: |
set -euo pipefail
# shellcheck disable=SC2086 # SHFMT_FLAGS intentionally word-splits
xargs -0 shfmt $SHFMT_FLAGS -d < "$RUNNER_TEMP/shell-files.nul"