From a28a68258c40f73d68660dbfc25e0d369f4b0468 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Tue, 26 May 2026 04:35:05 +0000 Subject: [PATCH] add cli-sync workspace snapshot/load scripts (#9322) * feat(fixtures): add cli-sync workspace snapshot/load scripts * fix(fixtures): address review nits (env var password, mktemp, dead refs) * fix(fixtures): address CI review (SIGPIPE, JSON escaping, doc/code drift) --- .github/workflows/check-empty-fixture.yml | 19 ++++ fixtures/README.md | 68 +++++++++++++++ fixtures/check-empty.sh | 27 ++++++ fixtures/cli-sync/.gitkeep | 0 fixtures/cli-sync/wmill.yaml | 15 ++++ fixtures/load.sh | 102 ++++++++++++++++++++++ fixtures/snapshot.sh | 100 +++++++++++++++++++++ 7 files changed, 331 insertions(+) create mode 100644 .github/workflows/check-empty-fixture.yml create mode 100644 fixtures/README.md create mode 100755 fixtures/check-empty.sh create mode 100644 fixtures/cli-sync/.gitkeep create mode 100644 fixtures/cli-sync/wmill.yaml create mode 100755 fixtures/load.sh create mode 100755 fixtures/snapshot.sh diff --git a/.github/workflows/check-empty-fixture.yml b/.github/workflows/check-empty-fixture.yml new file mode 100644 index 0000000000..4842260645 --- /dev/null +++ b/.github/workflows/check-empty-fixture.yml @@ -0,0 +1,19 @@ +name: Check fixture is empty + +on: + push: + branches: [main] + paths: + - "fixtures/**" + pull_request: + paths: + - "fixtures/**" + +jobs: + check-empty-fixture: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Ensure fixtures/cli-sync/ has no committed snapshot + run: bash fixtures/check-empty.sh diff --git a/fixtures/README.md b/fixtures/README.md new file mode 100644 index 0000000000..693a813936 --- /dev/null +++ b/fixtures/README.md @@ -0,0 +1,68 @@ +# Fixtures + +Helpers for sharing a reproducible test workspace alongside a PR. + +The workflow is: + +1. While iterating on a PR, snapshot your local test workspace into + `fixtures/cli-sync/` so a teammate (or CI) can replay it. +2. Commit the snapshot on your branch so reviewers can load it locally. +3. **Before merging**, clear `fixtures/cli-sync/` again. CI fails on `main` / + PRs that try to merge a non-empty fixture (see `check-empty.sh`). + +## Scripts + +All scripts assume: + +- A local Windmill backend running at `http://localhost:8000` (see top-level + `AGENTS.md` for `cargo run` / `npm run dev`). +- Default super-admin credentials `admin@windmill.dev` / `changeme`. +- `bun` and `python3` are installed and on `PATH`. No `wmill` install + required — the scripts invoke `cli/src/main.ts` via `bun run` directly. + `python3` is used only to build correctly-escaped JSON request bodies. + +The login token is passed to the CLI via `--token`, which means it appears in +`/proc//cmdline` for the duration of the `bun run` call. This is fine +against a local dev instance; if you point the scripts at a real instance, +the credentials are no more exposed than running `wmill` directly with +explicit flags, but bear it in mind. + +### `load.sh` — load the fixture into a fresh workspace + +```bash +./fixtures/load.sh +``` + +Logs in as `admin@windmill.dev`, creates a new workspace with a random id +(`fixture-<8 hex chars>`), and pushes `fixtures/cli-sync/` into it via +`wmill sync push`. Prints the workspace id at the end so you can open it in +the UI. + +Flags: +- `--base-url ` (default `http://localhost:8000`) +- `--email ` (default `admin@windmill.dev`) +- `--password ` (default `changeme`) — prefer `WMILL_PASSWORD=` env + var when using a real password, since `--password` ends up in `ps` / + shell history. +- `--workspace ` (default `fixture-`) +- `--dir ` (default `fixtures/cli-sync`) + +### `snapshot.sh` — snapshot a workspace into the fixture folder + +```bash +./fixtures/snapshot.sh +``` + +Pulls the given workspace into `fixtures/cli-sync/` via `wmill sync pull`. +The target directory is cleared first (everything except `wmill.yaml` and +`.gitkeep`) so the snapshot reflects exactly what's in the workspace. + +Flags: same as `load.sh` minus `--workspace` (passed as positional arg). + +### `check-empty.sh` — fail if the fixture is non-empty + +Used by CI to guard `main`. Run it locally before opening a PR for merge: + +```bash +./fixtures/check-empty.sh +``` diff --git a/fixtures/check-empty.sh b/fixtures/check-empty.sh new file mode 100755 index 0000000000..9adfc25d3c --- /dev/null +++ b/fixtures/check-empty.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Fail if fixtures/cli-sync/ contains anything beyond the fixture scaffold +# (wmill.yaml, .gitkeep). Used by CI to guard `main` against accidentally +# merging PRs with a test workspace snapshot still committed. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +DIR="${1:-$SCRIPT_DIR/cli-sync}" + +ALLOWED_RE='^fixtures/cli-sync/(\.gitkeep|wmill\.yaml)$' + +# Use `git ls-files` so we only check tracked files. Untracked local snapshots +# are fine — devs may keep them locally between sessions. +EXTRA=$(cd "$REPO_ROOT" && git ls-files fixtures/cli-sync \ + | grep -vE "$ALLOWED_RE" || true) + +if [[ -n "$EXTRA" ]]; then + echo "✗ fixtures/cli-sync/ contains committed snapshot files:" >&2 + echo "$EXTRA" | sed 's/^/ /' >&2 + echo >&2 + echo " Run fixtures/snapshot.sh against an empty workspace or" >&2 + echo " remove the files before merging." >&2 + exit 1 +fi + +echo "✓ fixtures/cli-sync/ is clean" diff --git a/fixtures/cli-sync/.gitkeep b/fixtures/cli-sync/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/fixtures/cli-sync/wmill.yaml b/fixtures/cli-sync/wmill.yaml new file mode 100644 index 0000000000..82029f8951 --- /dev/null +++ b/fixtures/cli-sync/wmill.yaml @@ -0,0 +1,15 @@ +defaultTs: bun +includes: + - f/** +excludes: [] +skipVariables: false +skipResources: false +skipResourceTypes: false +skipSecrets: true +includeSchedules: true +includeTriggers: true +includeUsers: false +includeGroups: false +includeSettings: false +includeKey: false +syncBehavior: v1 diff --git a/fixtures/load.sh b/fixtures/load.sh new file mode 100755 index 0000000000..09d0719d3d --- /dev/null +++ b/fixtures/load.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# Load fixtures/cli-sync/ into a fresh workspace on a local Windmill instance. +# +# Requires: bun on PATH. No `wmill` install needed — we invoke +# cli/src/main.ts directly via `bun run`. +# +# Assumes a Windmill backend running at http://localhost:8000 with the +# default super-admin (admin@windmill.dev / changeme). Override via flags. +set -euo pipefail + +BASE_URL="http://localhost:8000" +EMAIL="admin@windmill.dev" +# Prefer WMILL_PASSWORD env var over --password flag — flags leak into +# /proc//cmdline and shell history. +PASSWORD="${WMILL_PASSWORD:-changeme}" +WORKSPACE="" +DIR="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --base-url) BASE_URL="$2"; shift 2 ;; + --email) EMAIL="$2"; shift 2 ;; + --password) PASSWORD="$2"; shift 2 ;; + --workspace) WORKSPACE="$2"; shift 2 ;; + --dir) DIR="$2"; shift 2 ;; + -h|--help) + sed -n '2,8p' "$0"; exit 0 ;; + *) echo "Unknown flag: $1" >&2; exit 2 ;; + esac +done + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +DIR="${DIR:-$SCRIPT_DIR/cli-sync}" +DIR="$(cd "$DIR" && pwd)" +CLI_ENTRY="$REPO_ROOT/cli/src/main.ts" + +if ! command -v bun >/dev/null 2>&1; then + echo "✗ bun is required but not found on PATH" >&2 + exit 1 +fi +if [[ ! -f "$CLI_ENTRY" ]]; then + echo "✗ Cannot find CLI entry at $CLI_ENTRY" >&2 + exit 1 +fi +if [[ ! -f "$DIR/wmill.yaml" ]]; then + echo "✗ No wmill.yaml in $DIR — is the fixture folder set up?" >&2 + exit 1 +fi + +if [[ -z "$WORKSPACE" ]]; then + # Use $RANDOM rather than piping /dev/urandom through head -c, which + # SIGPIPEs `tr` and aborts the script under `set -o pipefail`. + printf -v WORKSPACE 'fixture-%04x%04x' $RANDOM $RANDOM +fi + +# JSON body builder — interpolation via printf '%s' is unsafe for arbitrary +# emails / passwords / workspace ids. Python is universal enough for a dev +# script and produces correctly escaped JSON. +json_object() { + python3 -c ' +import json, sys +print(json.dumps(dict(zip(sys.argv[1::2], sys.argv[2::2])))) +' "$@" +} + +echo "→ Logging in as $EMAIL on $BASE_URL" +TOKEN="$(curl -sS -f -X POST "$BASE_URL/api/auth/login" \ + -H "Content-Type: application/json" \ + -d "$(json_object email "$EMAIL" password "$PASSWORD")")" +if [[ -z "$TOKEN" ]]; then + echo "✗ Login failed (empty token)" >&2 + exit 1 +fi + +echo "→ Creating workspace '$WORKSPACE'" +CREATE_OUT="$(mktemp)" +trap 'rm -f "$CREATE_OUT"' EXIT +HTTP_CODE="$(curl -sS -o "$CREATE_OUT" -w '%{http_code}' \ + -X POST "$BASE_URL/api/workspaces/create" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "$(json_object id "$WORKSPACE" name "$WORKSPACE")")" +if [[ "$HTTP_CODE" != "200" && "$HTTP_CODE" != "201" ]]; then + echo "✗ Workspace creation failed (HTTP $HTTP_CODE):" >&2 + cat "$CREATE_OUT" >&2 + echo >&2 + exit 1 +fi + +echo "→ Pushing $DIR to workspace '$WORKSPACE'" +( + cd "$DIR" + bun run "$CLI_ENTRY" sync push --yes \ + --base-url "$BASE_URL" \ + --workspace "$WORKSPACE" \ + --token "$TOKEN" +) + +echo +echo "✓ Fixture loaded into workspace '$WORKSPACE'" +echo " Open: ${BASE_URL%/}/?workspace=$WORKSPACE" diff --git a/fixtures/snapshot.sh b/fixtures/snapshot.sh new file mode 100755 index 0000000000..a984a47635 --- /dev/null +++ b/fixtures/snapshot.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# Snapshot a workspace into fixtures/cli-sync/ so it can be committed +# alongside a PR. +# +# Requires: bun on PATH. No `wmill` install needed. +# +# Assumes a Windmill backend running at http://localhost:8000 with the +# default super-admin (admin@windmill.dev / changeme). Override via flags. +set -euo pipefail + +BASE_URL="http://localhost:8000" +EMAIL="admin@windmill.dev" +# Prefer WMILL_PASSWORD env var over --password flag — flags leak into +# /proc//cmdline and shell history. +PASSWORD="${WMILL_PASSWORD:-changeme}" +DIR="" +WORKSPACE="" + +if [[ $# -lt 1 || "$1" == "-h" || "$1" == "--help" ]]; then + sed -n '2,8p' "$0" + echo + echo "Usage: $(basename "$0") [--base-url URL] [--email E] [--password P] [--dir PATH]" + exit 0 +fi + +WORKSPACE="$1"; shift + +while [[ $# -gt 0 ]]; do + case "$1" in + --base-url) BASE_URL="$2"; shift 2 ;; + --email) EMAIL="$2"; shift 2 ;; + --password) PASSWORD="$2"; shift 2 ;; + --dir) DIR="$2"; shift 2 ;; + *) echo "Unknown flag: $1" >&2; exit 2 ;; + esac +done + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +DIR="${DIR:-$SCRIPT_DIR/cli-sync}" +DIR="$(cd "$DIR" && pwd)" +CLI_ENTRY="$REPO_ROOT/cli/src/main.ts" + +if ! command -v bun >/dev/null 2>&1; then + echo "✗ bun is required but not found on PATH" >&2 + exit 1 +fi +if [[ ! -f "$CLI_ENTRY" ]]; then + echo "✗ Cannot find CLI entry at $CLI_ENTRY" >&2 + exit 1 +fi +if [[ ! -f "$DIR/wmill.yaml" ]]; then + echo "✗ No wmill.yaml in $DIR — is the fixture folder set up?" >&2 + exit 1 +fi + +# JSON body builder — interpolation via printf '%s' is unsafe for arbitrary +# emails / passwords. Python is universal enough for a dev script and +# produces correctly escaped JSON. +json_object() { + python3 -c ' +import json, sys +print(json.dumps(dict(zip(sys.argv[1::2], sys.argv[2::2])))) +' "$@" +} + +echo "→ Logging in as $EMAIL on $BASE_URL" +TOKEN="$(curl -sS -f -X POST "$BASE_URL/api/auth/login" \ + -H "Content-Type: application/json" \ + -d "$(json_object email "$EMAIL" password "$PASSWORD")")" +if [[ -z "$TOKEN" ]]; then + echo "✗ Login failed (empty token)" >&2 + exit 1 +fi + +# Clear previous snapshot content while preserving the fixture scaffold +# (wmill.yaml, .gitkeep). Anything else is removed so the snapshot reflects +# exactly what is in the workspace. +echo "→ Clearing previous snapshot in $DIR" +( + cd "$DIR" + find . -mindepth 1 -maxdepth 1 \ + ! -name 'wmill.yaml' \ + ! -name '.gitkeep' \ + -exec rm -rf {} + +) + +echo "→ Pulling workspace '$WORKSPACE' into $DIR" +( + cd "$DIR" + bun run "$CLI_ENTRY" sync pull --yes \ + --base-url "$BASE_URL" \ + --workspace "$WORKSPACE" \ + --token "$TOKEN" +) + +echo +echo "✓ Snapshot of '$WORKSPACE' written to $DIR" +echo " Commit the changes to share with reviewers. Run fixtures/check-empty.sh" +echo " to verify the dir is empty again before merging."