#!/usr/bin/env bash # Exercise the pipeline scripts end to end against a stub DealSage, without # Gitea. Builds a throwaway app repo, then runs three scenarios: # 1. auto policy: checks pass (one advisory fails), build, upload, promote → live # 2. a required check fails: nothing is allocated # 3. manual policy: built and kept on the branch channel, not promoted # Usage: templates/ci-actions/test/run-local.sh set -euo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" SCRIPTS="$HERE/../pipeline/scripts" PORT="${PORT:-8765}" WORK="$(mktemp -d)" trap 'kill ${STUB_PID:-} 2>/dev/null || true; rm -rf "$WORK"' EXIT pass=0; fail=0 check() { if eval "$2"; then echo " ok $1"; pass=$((pass+1)); else echo " FAIL $1"; fail=$((fail+1)); fi; } make_app() { # dir required_check_cmd local d="$1" reqcmd="$2" mkdir -p "$d/.dealsage" "$d/.gitea/workflows" "$d/src/lib" cat > "$d/package.json" <ok' > dist/index.html && echo 'console.log(1)' > dist/assets/app.js", "typecheck": "$reqcmd" } } JSON cat > "$d/.dealsage/app.json" < "$d/.gitea/workflows/publish.yml" : > "$d/src/lib/dealsage.ts"; : > "$d/src/Root.tsx"; : > "$d/src/main.tsx" printf '# Agent guide\n\n\n' > "$d/AGENTS.md" git -C "$d" init -q -b feature/x && git -C "$d" -c user.name=t -c user.email=t@t add -A \ && git -C "$d" -c user.name=t -c user.email=t@t commit -qm "Add fixture app" } run_pipeline() { # dir -> runs manifest, checks, build, publish; echoes the failing stage or "ok" local d="$1" ghenv ghout ghsum ghenv="$(mktemp)"; ghout="$(mktemp)"; ghsum="$(mktemp)" export GITHUB_ENV="$ghenv" GITHUB_OUTPUT="$ghout" GITHUB_STEP_SUMMARY="$ghsum" export GITHUB_SHA="$(git -C "$d" rev-parse HEAD)" GITHUB_REF_NAME="feature/x" GITHUB_ACTOR="tester" export DEALSAGE_FUNCTIONS_URL="http://127.0.0.1:$PORT" PUBLISH_TOKEN="test-publish-token" export SUPABASE_ANON_KEY="anon" SUPABASE_URL="https://example.supabase.co" CUSTOM_APPS_WORKER_URL="http://127.0.0.1:$PORT" export MANIFEST_PATH=".dealsage/app.json" unset APP_ID MANIFEST_JSON CHECKS_JSON CHECKS_RESULT_JSON local stage for stage in manifest run-checks build publish; do ( cd "$d" && bash "$SCRIPTS/$stage.sh" ) > "$WORK/$stage.log" 2>&1 || { echo "$stage"; cat "$ghenv" > "$WORK/last.env"; return 0; } # composite steps share state through GITHUB_ENV (heredoc form); load it. eval "$(python3 - "$ghenv" <<'PY' import re,sys,shlex txt=open(sys.argv[1]).read() for m in re.finditer(r'^(\w+)<<__DS_EOF__\n(.*?)\n__DS_EOF__$', txt, re.S|re.M): print(f"export {m.group(1)}={shlex.quote(m.group(2))}") PY )" done cp "$ghout" "$WORK/last.out"; cp "$ghsum" "$WORK/last.summary" echo ok } start_stub() { # policy : > "$WORK/requests.jsonl" STUB_LOG="$WORK/requests.jsonl" STUB_POLICY="$1" python3 "$HERE/stub_server.py" "$PORT" & STUB_PID=$! for _ in $(seq 1 30); do curl -s -o /dev/null "http://127.0.0.1:$PORT/" && break; sleep 0.1; done } stop_stub() { kill "$STUB_PID" 2>/dev/null || true; wait "$STUB_PID" 2>/dev/null || true; } req() { jq -c "$1" "$WORK/requests.jsonl"; } echo "Scenario 1: auto policy" make_app "$WORK/app1" "true"; start_stub auto r="$(run_pipeline "$WORK/app1")" check "pipeline completed" "[ '$r' = ok ]" check "four checks recorded (typecheck, lint, conventions, build)" "[ \"\$(req 'select(.path|endswith(\"/record-app-check\"))' | wc -l)\" = 4 ]" check "advisory lint failure recorded as failed" "req 'select(.body.checkId==\"lint\") | .body.status' | grep -q '\"failed\"'" check "conventions passed" "req 'select(.body.checkId==\"conventions\") | .body.status' | grep -q '\"passed\"'" check "check payload carries commit, branch and log tail" "req 'select(.body.checkId==\"typecheck\") | .body | (.commitSha|length)==40 and .branch==\"feature/x\" and (.logTail|length)>0' | grep -q true" check "allocate sent branch, message, author, manifest and check results" "req 'select(.body.action==\"allocate\") | .body | .branch==\"feature/x\" and .commitMessage==\"Add fixture app\" and .author==\"t\" and (.manifest.dependencies.objectTypes|length)==1 and (.checks|length)==3' | grep -q true" check "two files uploaded through the Worker with the upload token" "[ \"\$(req 'select(.method==\"PUT\" and .auth==\"Bearer upl-token\")' | wc -l)\" = 2 ]" check "promote requested after upload" "req 'select(.body.action==\"promote\") | .body.version' | grep -q 7" check "outputs: version=7 promoted=true" "grep -q '^version=7$' '$WORK/last.out' && grep -q '^promoted=true$' '$WORK/last.out'" stop_stub echo "Scenario 2: required check fails" make_app "$WORK/app2" "echo 'type error' && exit 1"; start_stub auto r="$(run_pipeline "$WORK/app2")" check "stopped at run-checks" "[ '$r' = run-checks ]" check "failure still recorded" "req 'select(.body.checkId==\"typecheck\") | .body.status' | grep -q '\"failed\"'" check "nothing allocated" "! req 'select(.body.action==\"allocate\")' | grep -q ." check "advisory and platform checks still ran" "[ \"\$(req 'select(.path|endswith(\"/record-app-check\"))' | wc -l)\" = 3 ]" stop_stub echo "Scenario 3: manual policy" make_app "$WORK/app3" "true"; start_stub manual r="$(run_pipeline "$WORK/app3")" check "pipeline completed" "[ '$r' = ok ]" check "outputs: promoted=false, channel=branch/feature/x" "grep -q '^promoted=false$' '$WORK/last.out' && grep -q '^channel=branch/feature/x$' '$WORK/last.out'" check "summary mentions the channel" "grep -q 'channel' '$WORK/last.summary'" stop_stub echo; echo "$pass passed, $fail failed" [ "$fail" = 0 ]