diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7d817d0..95f774d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,4 +1,4 @@ -name: Tests +name: Validate Demo on: push: @@ -7,7 +7,55 @@ on: jobs: validate: runs-on: macos-latest + env: + BASE_CACHE_DIR: ${{ github.workspace }}/.base-cache + BASE_SETUP_NOTIFY: "false" + PIP_DISABLE_PIP_VERSION_CHECK: "1" steps: - uses: actions/checkout@v4 + + - name: Install BATS + run: brew install bats-core + - name: Validate repository baseline run: ./tests/validate.sh + + - name: Run demo script tests + run: bats tests/demo_test.bats + + - name: Check out Base + run: git clone --depth 1 https://github.com/codeforester/base.git ../base + + - name: Detect Base demo support + id: base_demo + run: | + if ../base/bin/basectl demo --help >/dev/null 2>&1; then + echo "supported=true" >> "$GITHUB_OUTPUT" + else + echo "supported=false" >> "$GITHUB_OUTPUT" + echo "Base master does not support basectl demo yet; skipping full Base-backed validation." + fi + + - name: Set up Base + if: steps.base_demo.outputs.supported == 'true' + run: ../base/bin/basectl setup base --no-notify + + - name: Set up base-demo + if: steps.base_demo.outputs.supported == 'true' + run: ../base/bin/basectl setup base-demo --no-notify + + - name: Check base-demo + if: steps.base_demo.outputs.supported == 'true' + run: ../base/bin/basectl check base-demo + + - name: Doctor base-demo + if: steps.base_demo.outputs.supported == 'true' + run: ../base/bin/basectl doctor base-demo + + - name: Test base-demo + if: steps.base_demo.outputs.supported == 'true' + run: ../base/bin/basectl test base-demo + + - name: Run base-demo non-interactively + if: steps.base_demo.outputs.supported == 'true' + run: ../base/bin/basectl demo base-demo -- --non-interactive diff --git a/CHANGELOG.md b/CHANGELOG.md index d22e91e..208eb10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,3 +12,6 @@ and versions are tracked in the repo-root `VERSION` file. - Initialized the repository with the Base-managed repo baseline. - Added a Base manifest, Brewfile, activation source, example command, and baseline validation script. +- Added the interactive `basectl demo base-demo` walkthrough and BATS coverage. +- Expanded GitHub Actions validation for the demo script and Base-backed demo + flow. diff --git a/README.md b/README.md index c515caa..353e39b 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ basectl check base-demo basectl doctor base-demo basectl run base-demo hello basectl test base-demo +basectl demo base-demo ``` ## Repository Shape @@ -33,7 +34,11 @@ basectl test base-demo - `Brewfile` is the Homebrew-owned place for ordinary macOS tools. - `.base/activate.sh` demonstrates project activation state. - `src/hello.sh` is a tiny command target for `basectl run`. +- `demo/demo.sh` is the interactive walkthrough. - `tests/validate.sh` verifies that the repository baseline is intact. -The interactive `basectl demo base-demo` walkthrough lands in a follow-up change -after the Base demo command is available in a released Base version. +For CI or scripted validation, run the walkthrough without prompts: + +```bash +basectl demo base-demo -- --non-interactive +``` diff --git a/base_manifest.yaml b/base_manifest.yaml index 1f82f54..9600b21 100644 --- a/base_manifest.yaml +++ b/base_manifest.yaml @@ -15,4 +15,8 @@ commands: test: command: ./tests/validate.sh +demo: + script: ./demo/demo.sh + description: Interactive walkthrough of Base-managed project conventions + artifacts: [] diff --git a/demo/demo.sh b/demo/demo.sh new file mode 100755 index 0000000..abc945f --- /dev/null +++ b/demo/demo.sh @@ -0,0 +1,188 @@ +#!/usr/bin/env bash +set -euo pipefail + +BASE_DEMO_PROJECT="${BASE_PROJECT:-base-demo}" +BASE_DEMO_ROOT="${BASE_PROJECT_ROOT:-}" +BASE_DEMO_BASECTL="${BASE_DEMO_BASECTL:-basectl}" +BASE_DEMO_NON_INTERACTIVE=0 + +demo_script_dir() { + cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P +} + +demo_project_root() { + if [[ -n "$BASE_DEMO_ROOT" ]]; then + cd -- "$BASE_DEMO_ROOT" && pwd -P + return + fi + + cd -- "$(demo_script_dir)/.." && pwd -P +} + +BASE_DEMO_ROOT="$(demo_project_root)" + +usage() { + cat <<'EOF' +Usage: + demo/demo.sh [--non-interactive] [-h|--help] + +Run the base-demo interactive walkthrough. +EOF +} + +parse_args() { + while (($#)); do + case "$1" in + --non-interactive) + BASE_DEMO_NON_INTERACTIVE=1 + ;; + -h|--help|help) + usage + return 2 + ;; + *) + printf 'ERROR: Unknown demo option %q\n' "$1" >&2 + usage >&2 + return 1 + ;; + esac + shift + done +} + +pause() { + if [[ "$BASE_DEMO_NON_INTERACTIVE" == "1" || ! -t 0 ]]; then + return 0 + fi + + printf '\nPress Enter to continue...' + read -r _ + printf '\n' +} + +step() { + printf '\n== Step %s: %s ==\n\n' "$1" "$2" +} + +run_command() { + printf ' $' + printf ' %q' "$@" + printf '\n' + + if ! "$@"; then + printf '\nDemo step failed while running the command above.\n' >&2 + printf 'Run it manually from %s, fix the issue, and retry the demo.\n' "$BASE_DEMO_ROOT" >&2 + return 1 + fi +} + +capture_command() { + local output + + printf ' $' + printf ' %q' "$@" + printf '\n' + + if ! output="$("$@" 2>&1)"; then + printf '%s\n' "$output" >&2 + printf '\nDemo step failed while running the command above.\n' >&2 + return 1 + fi + + printf '%s\n' "$output" +} + +require_contains() { + local label="$1" + local output="$2" + local expected="$3" + + if [[ "$output" != *"$expected"* ]]; then + printf 'ERROR: Expected %s output to contain %q.\n' "$label" "$expected" >&2 + return 1 + fi +} + +intro() { + printf '\nbase-demo Walkthrough\n\n' + printf 'This demo shows the smallest useful Base-managed project shape.\n' + printf 'Each step runs a real command or validates a real file in this repo.\n' + pause +} + +project_shape_step() { + step 1 "Project Shape" + run_command test -f "$BASE_DEMO_ROOT/base_manifest.yaml" + run_command test -f "$BASE_DEMO_ROOT/Brewfile" + run_command test -x "$BASE_DEMO_ROOT/src/hello.sh" + run_command test -x "$BASE_DEMO_ROOT/tests/validate.sh" + pause +} + +manifest_step() { + step 2 "Manifest Contracts" + run_command grep -n "name: base-demo" "$BASE_DEMO_ROOT/base_manifest.yaml" + run_command grep -n "hello: ./src/hello.sh" "$BASE_DEMO_ROOT/base_manifest.yaml" + run_command grep -n "command: ./tests/validate.sh" "$BASE_DEMO_ROOT/base_manifest.yaml" + run_command grep -n "script: ./demo/demo.sh" "$BASE_DEMO_ROOT/base_manifest.yaml" + pause +} + +activation_step() { + step 3 "Project Activation Source" + # shellcheck source=/dev/null + source "$BASE_DEMO_ROOT/.base/activate.sh" + printf 'BASE_DEMO_ENV=%s\n' "${BASE_DEMO_ENV:-unset}" + require_contains "activation" "${BASE_DEMO_ENV:-}" "baseline" + pause +} + +run_step() { + local output + + step 4 "Declared Command" + output="$(capture_command "$BASE_DEMO_BASECTL" run "$BASE_DEMO_PROJECT" hello)" + printf '%s\n' "$output" + require_contains "run command" "$output" "hello from base-demo" + pause +} + +test_step() { + local output + + step 5 "Test Contract" + output="$(capture_command "$BASE_DEMO_BASECTL" test "$BASE_DEMO_PROJECT")" + printf '%s\n' "$output" + require_contains "test command" "$output" "Repository baseline is present." + pause +} + +demo_step() { + local output + + step 6 "Demo Contract" + output="$(capture_command "$BASE_DEMO_BASECTL" demo "$BASE_DEMO_PROJECT" --dry-run -- --non-interactive)" + printf '%s\n' "$output" + require_contains "demo command" "$output" "Would run demo" + pause +} + +main() { + parse_args "$@" || { + local status=$? + [[ "$status" -eq 2 ]] && return 0 + return "$status" + } + + cd -- "$BASE_DEMO_ROOT" + intro + project_shape_step + manifest_step + activation_step + run_step + test_step + demo_step + printf '\nbase-demo walkthrough complete.\n' +} + +main "$@" diff --git a/tests/demo_test.bats b/tests/demo_test.bats new file mode 100755 index 0000000..8cead60 --- /dev/null +++ b/tests/demo_test.bats @@ -0,0 +1,68 @@ +#!/usr/bin/env bats + +setup() { + TEST_ROOT="$(cd "$BATS_TEST_DIRNAME/.." && pwd -P)" + TEST_TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/base-demo-test.XXXXXX")" +} + +teardown() { + rm -rf "$TEST_TMPDIR" +} + +@test "demo script is declared and executable" { + grep -Fq "script: ./demo/demo.sh" "$TEST_ROOT/base_manifest.yaml" + [ -x "$TEST_ROOT/demo/demo.sh" ] +} + +@test "demo script prints help" { + run "$TEST_ROOT/demo/demo.sh" --help + + [ "$status" -eq 0 ] + [[ "$output" == *"Run the base-demo interactive walkthrough."* ]] +} + +@test "demo script runs in non-interactive mode" { + local fake_bin="$TEST_TMPDIR/bin" + local state_file="$TEST_TMPDIR/state" + + mkdir -p "$fake_bin" + cat > "$fake_bin/basectl" <<'EOF' +#!/usr/bin/env bash +printf 'basectl %s\n' "$*" >> "${BASE_DEMO_TEST_STATE:?}" +case "$*" in + run\ base-demo\ hello) + printf 'hello from base-demo\n' + printf 'BASE_PROJECT=base-demo\n' + printf 'BASE_DEMO_ENV=%s\n' "${BASE_DEMO_ENV:-unset}" + ;; + test\ base-demo) + printf 'Repository baseline is present.\n' + ;; + demo\ base-demo\ --dry-run\ --\ --non-interactive) + printf '[DRY-RUN] Would run demo for project base-demo.\n' + ;; + *) + printf 'unexpected basectl args: %s\n' "$*" >&2 + exit 1 + ;; +esac +EOF + chmod +x "$fake_bin/basectl" + + run env \ + BASE_PROJECT=base-demo \ + BASE_PROJECT_ROOT="$TEST_ROOT" \ + BASE_DEMO_BASECTL="$fake_bin/basectl" \ + BASE_DEMO_TEST_STATE="$state_file" \ + "$TEST_ROOT/demo/demo.sh" --non-interactive + + [ "$status" -eq 0 ] + [[ "$output" == *"base-demo Walkthrough"* ]] + [[ "$output" == *"BASE_DEMO_ENV=baseline"* ]] + [[ "$output" == *"hello from base-demo"* ]] + [[ "$output" == *"Repository baseline is present."* ]] + [[ "$output" == *"base-demo walkthrough complete."* ]] + grep -Fqx "basectl run base-demo hello" "$state_file" + grep -Fqx "basectl test base-demo" "$state_file" + grep -Fqx "basectl demo base-demo --dry-run -- --non-interactive" "$state_file" +} diff --git a/tests/validate.sh b/tests/validate.sh index 8514328..239b876 100755 --- a/tests/validate.sh +++ b/tests/validate.sh @@ -11,6 +11,8 @@ required_files=( Brewfile .base/activate.sh src/hello.sh + demo/demo.sh + tests/demo_test.bats .github/workflows/tests.yml ) @@ -21,7 +23,7 @@ for file in "${required_files[@]}"; do } done -for executable in tests/validate.sh .base/activate.sh src/hello.sh; do +for executable in tests/validate.sh .base/activate.sh src/hello.sh demo/demo.sh; do [[ -x "$executable" ]] || { printf 'Required file is not executable: %s\n' "$executable" >&2 exit 1 @@ -48,4 +50,9 @@ grep -Fq 'hello: ./src/hello.sh' base_manifest.yaml || { exit 1 } +grep -Fq 'script: ./demo/demo.sh' base_manifest.yaml || { + printf 'base_manifest.yaml does not declare the demo script.\n' >&2 + exit 1 +} + printf 'Repository baseline is present.\n'