From 39f08a78059c6c1d5020ac8138e1ce89b3e6c771 Mon Sep 17 00:00:00 2001 From: Labault Date: Wed, 17 Jun 2026 19:31:30 +0200 Subject: [PATCH 01/10] =?UTF-8?q?=F0=9F=90=9B=20fix(setup):=20make=20setup?= =?UTF-8?q?=20exit=20status=20reliable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two exit-code defects in the setup runner: - Post-install validation used `command -v brew >/dev/null && success ...`. Under `set -e`, a missing tool made that line return non-zero and aborted an otherwise successful install, skipping the remaining checks and the success message. Replace it with a check_tool helper that warns instead of failing. - Output went through `exec > >(tee ...)`, a process substitution whose completion (and thus the final log lines) races with shell exit. Run the work in a function piped to tee and propagate the real status via PIPESTATUS, so the captured transcript is complete and the exit code is the setup work's, not tee's. --- scripts/setup.sh | 61 ++++++++++++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/scripts/setup.sh b/scripts/setup.sh index 997a5d7..9d45793 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -95,30 +95,41 @@ LOG_DIR="${MAC_DEV_SETUP_LOG_DIR:-$HOME/Library/Logs/mac-dev-setup}" mkdir -p "$LOG_DIR" LOG_FILE="$LOG_DIR/setup.log" -exec > >(tee -a "$LOG_FILE") 2>&1 -info "Logging to $LOG_FILE" - -# ---------------------------- -# EXECUTION -# ---------------------------- -PROFILE="$PROFILE" bash "$SCRIPT_DIR/brew.sh" -bash "$SCRIPT_DIR/git.sh" -bash "$SCRIPT_DIR/zsh.sh" - -# ---------------------------- -# POST VALIDATION -# ---------------------------- -info "Post install validation" - -command -v brew >/dev/null && success "brew ok" -command -v git >/dev/null && success "git ok" -command -v zsh >/dev/null && success "zsh ok" +check_tool() { + # Report a tool without letting a missing one abort setup under "set -e". + if command -v "$1" >/dev/null; then + success "$1 ok" + else + warn "$1 not found after setup" + fi +} -# ---------------------------- -# CI MODE -# ---------------------------- -if [ "$CI" = "true" ]; then - info "Running in CI mode" -fi +run_setup() { + info "Logging to $LOG_FILE" + + # ---------------------------- + # EXECUTION + # ---------------------------- + PROFILE="$PROFILE" bash "$SCRIPT_DIR/brew.sh" + bash "$SCRIPT_DIR/git.sh" + bash "$SCRIPT_DIR/zsh.sh" + + # ---------------------------- + # POST VALIDATION + # ---------------------------- + info "Post install validation" + check_tool brew + check_tool git + check_tool zsh + + if [ "$CI" = "true" ]; then + info "Running in CI mode" + fi + + success "Mac Dev Setup completed successfully" +} -success "Mac Dev Setup completed successfully" +# Pipe through tee so the full transcript is captured reliably, and propagate +# the real exit status of the setup work (not tee's) via PIPESTATUS. +run_setup 2>&1 | tee -a "$LOG_FILE" +exit "${PIPESTATUS[0]}" From fd54053593454b4b7397539568a9196f223d8fb5 Mon Sep 17 00:00:00 2001 From: Labault Date: Wed, 17 Jun 2026 19:32:38 +0200 Subject: [PATCH 02/10] =?UTF-8?q?=F0=9F=91=B7=20ci:=20gate=20shell=20scrip?= =?UTF-8?q?ts=20with=20shellcheck?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The project is shell-heavy but shellcheck (though installed) ran in no gate. Add the pinned shellcheck-py pre-commit hook with `-x` so sourced libraries are followed (enabled via .shellcheckrc external-sources), and scope it to scripts/ and install.sh. The hook ships its own binary, so it runs in CI through the existing `pre-commit run --all-files` step with no extra setup. All current scripts pass cleanly. --- .pre-commit-config.yaml | 7 +++++++ .shellcheckrc | 2 ++ 2 files changed, 9 insertions(+) create mode 100644 .shellcheckrc diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aedad36..d285fc2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,6 +14,13 @@ repos: hooks: - id: gitleaks + - repo: https://github.com/shellcheck-py/shellcheck-py + rev: v0.10.0.1 + hooks: + - id: shellcheck + args: [-x] + files: ^(scripts/.*\.sh|install\.sh)$ + - repo: local hooks: - id: markdownlint-cli2 diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 0000000..d597c6a --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,2 @@ +# Allow `# shellcheck source=...` directives to resolve sourced libraries. +external-sources=true From 02e7d516efa9d0dcc713acd85e2990f5165dc5cd Mon Sep 17 00:00:00 2001 From: Labault Date: Wed, 17 Jun 2026 19:41:15 +0200 Subject: [PATCH 03/10] =?UTF-8?q?=E2=9C=85=20test:=20add=20a=20bats=20suit?= =?UTF-8?q?e=20for=20libs,=20CLI,=20update=20and=20install?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the single smoke script with a real bats-core suite (run via "npm test"): - profiles: validation, discovery, safe-charset/traversal rejection - path_manager: PATH block install idempotency, removal, content safety - command_registry: discovery, metadata/@name/@description parsing - cli: help, --help/-h, unknown-command suggestion, usage - setup: dry-run safety and absolute log path - doctor: non-zero exit on a missing tool - uninstall: identical-only --remove-config behavior - completion: committed file matches the generator - update: fast-forward, dry-run, dirty-tree refusal against a local remote - install: symlink/PATH creation, idempotency, uninstall (macOS only) The git-using tests clear inherited GIT_DIR/GIT_INDEX_FILE so they are robust when the suite runs inside a git hook. bats is a dev dependency so it is available wherever npm ci runs. scripts/test-cli.sh is removed; its checks are covered above. --- package-lock.json | 11 ++++ package.json | 3 +- scripts/test-cli.sh | 101 ------------------------------------ tests/cli.bats | 48 +++++++++++++++++ tests/command_registry.bats | 61 ++++++++++++++++++++++ tests/completion.bats | 11 ++++ tests/doctor.bats | 30 +++++++++++ tests/install.bats | 58 +++++++++++++++++++++ tests/path_manager.bats | 51 ++++++++++++++++++ tests/profiles.bats | 49 +++++++++++++++++ tests/setup.bats | 31 +++++++++++ tests/uninstall.bats | 25 +++++++++ tests/update.bats | 79 ++++++++++++++++++++++++++++ 13 files changed, 456 insertions(+), 102 deletions(-) delete mode 100755 scripts/test-cli.sh create mode 100644 tests/cli.bats create mode 100644 tests/command_registry.bats create mode 100644 tests/completion.bats create mode 100644 tests/doctor.bats create mode 100644 tests/install.bats create mode 100644 tests/path_manager.bats create mode 100644 tests/profiles.bats create mode 100644 tests/setup.bats create mode 100644 tests/uninstall.bats create mode 100644 tests/update.bats diff --git a/package-lock.json b/package-lock.json index 046fbd9..ecbd23c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@commitlint/cli": "^21.0.2", "@commitlint/config-conventional": "^21.0.2", "@gitmoji/gitmoji-regex": "^1.0.0", + "bats": "^1.13.0", "commitlint-config-gitmoji": "^2.3.1", "husky": "^9.1.7" } @@ -443,6 +444,16 @@ "dev": true, "license": "MIT" }, + "node_modules/bats": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/bats/-/bats-1.13.0.tgz", + "integrity": "sha512-giSYKGTOcPZyJDbfbTtzAedLcNWdjCLbXYU3/MwPnjyvDXzu6Dgw8d2M+8jHhZXSmsCMSQqCp+YBsJ603UO4vQ==", + "dev": true, + "license": "MIT", + "bin": { + "bats": "bin/bats" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", diff --git a/package.json b/package.json index 53a958e..17dfc2f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "doc": "docs" }, "scripts": { - "test": "bash scripts/test-cli.sh", + "test": "bats tests", "prepare": "husky" }, "repository": { @@ -26,6 +26,7 @@ "@commitlint/cli": "^21.0.2", "@commitlint/config-conventional": "^21.0.2", "@gitmoji/gitmoji-regex": "^1.0.0", + "bats": "^1.13.0", "commitlint-config-gitmoji": "^2.3.1", "husky": "^9.1.7" } diff --git a/scripts/test-cli.sh b/scripts/test-cli.sh deleted file mode 100755 index dddb8b4..0000000 --- a/scripts/test-cli.sh +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" - -assert_contains() { - haystack="$1" - needle="$2" - - if ! printf '%s\n' "$haystack" | grep -F "$needle" >/dev/null; then - printf 'Expected output to contain: %s\n' "$needle" >&2 - exit 1 - fi -} - -help_output="$(bash "$REPO_DIR/scripts/cli.sh" help)" -assert_contains "$help_output" "doctor" -assert_contains "$help_output" "setup" -assert_contains "$help_output" "uninstall" -assert_contains "$help_output" "update" - -setup_help_output="$(bash "$REPO_DIR/scripts/setup.sh" --help)" -assert_contains "$setup_help_output" "Usage: scripts/setup.sh" - -mac_setup_help_output="$(bash "$REPO_DIR/scripts/cli.sh" setup --help)" -assert_contains "$mac_setup_help_output" "Usage: mac setup" - -mac_doctor_help_output="$(bash "$REPO_DIR/scripts/cli.sh" doctor --help)" -assert_contains "$mac_doctor_help_output" "Usage: mac doctor" - -setup_dry_run_output="$(bash "$REPO_DIR/scripts/cli.sh" setup --profile minimal --dry-run)" -assert_contains "$setup_dry_run_output" "Dry run mode activated" - -if bash "$REPO_DIR/scripts/cli.sh" updte >/tmp/mac-dev-setup-cli-test.out 2>&1; then - printf 'Expected an unknown command to fail.\n' >&2 - exit 1 -fi -assert_contains "$(cat /tmp/mac-dev-setup-cli-test.out)" "mac update" -rm -f /tmp/mac-dev-setup-cli-test.out - -log_test_dir="$(mktemp -d)" -( - cd "$log_test_dir" - bash "$REPO_DIR/scripts/setup.sh" --profile minimal --dry-run >/dev/null -) -if [ -e "$log_test_dir/logs" ]; then - printf 'setup.sh must not create a logs directory in the current directory.\n' >&2 - rm -rf "$log_test_dir" - exit 1 -fi -rm -rf "$log_test_dir" - -if grep -Eq 'mkdir -p logs|"logs/setup\.log"' "$REPO_DIR/scripts/setup.sh"; then - printf 'setup.sh must use an absolute log path, not a relative logs/ directory.\n' >&2 - exit 1 -fi - -( - # shellcheck source=scripts/lib/profiles.sh - source "$REPO_DIR/scripts/lib/profiles.sh" - if ! profile_name_is_valid full; then exit 1; fi - if profile_name_is_valid "../etc"; then exit 1; fi - if ! profile_validate "$REPO_DIR" minimal; then exit 1; fi - if profile_validate "$REPO_DIR" no-such-profile; then exit 1; fi -) || { - printf 'Profile validation contract failed.\n' >&2 - exit 1 -} - -uninstall_home="$(mktemp -d)" -cp "$REPO_DIR/configs/zsh/.zprofile" "$uninstall_home/.zprofile" -printf 'unmanaged content\n' >"$uninstall_home/.zshrc" -uninstall_config_output="$(HOME="$uninstall_home" bash "$REPO_DIR/scripts/commands/uninstall.sh" --remove-config --dry-run 2>&1)" -assert_contains "$uninstall_config_output" "Would remove .zprofile" -assert_contains "$uninstall_config_output" ".zshrc differs" -rm -rf "$uninstall_home" - -doctor_bin="$(mktemp -d)" -for bootstrap_tool in bash dirname; do - ln -s "$(command -v "$bootstrap_tool")" "$doctor_bin/$bootstrap_tool" -done -for stub in sw_vers uname brew git zsh; do - printf '#!/bin/sh\nexit 0\n' >"$doctor_bin/$stub" - chmod +x "$doctor_bin/$stub" -done -# mac is intentionally absent so the diagnostic must report a failure. -if PATH="$doctor_bin" bash "$REPO_DIR/scripts/commands/doctor.sh" >/tmp/mac-dev-setup-doctor.out 2>&1; then - printf 'Expected doctor to exit non-zero when a required tool is missing.\n' >&2 - rm -rf "$doctor_bin" /tmp/mac-dev-setup-doctor.out - exit 1 -fi -assert_contains "$(cat /tmp/mac-dev-setup-doctor.out)" "mac CLI missing" -assert_contains "$(cat /tmp/mac-dev-setup-doctor.out)" "Doctor found problems" -rm -rf "$doctor_bin" /tmp/mac-dev-setup-doctor.out - -bash "$REPO_DIR/scripts/generate-zsh-completion.sh" >/dev/null -git -C "$REPO_DIR" diff --exit-code -- configs/zsh/completions/_mac >/dev/null - -printf 'CLI smoke tests passed.\n' diff --git a/tests/cli.bats b/tests/cli.bats new file mode 100644 index 0000000..ecb4cea --- /dev/null +++ b/tests/cli.bats @@ -0,0 +1,48 @@ +#!/usr/bin/env bats + +setup() { + REPO_DIR="$(cd "$BATS_TEST_DIRNAME/.." && pwd)" + CLI="$REPO_DIR/scripts/cli.sh" +} + +@test "mac help lists every command" { + run bash "$CLI" help + [ "$status" -eq 0 ] + [[ "$output" == *setup* ]] + [[ "$output" == *doctor* ]] + [[ "$output" == *update* ]] + [[ "$output" == *uninstall* ]] +} + +@test "mac --help and -h behave like help" { + run bash "$CLI" --help + [ "$status" -eq 0 ] + [[ "$output" == *setup* ]] + run bash "$CLI" -h + [ "$status" -eq 0 ] + [[ "$output" == *setup* ]] +} + +@test "unknown command fails and suggests the nearest match" { + run bash "$CLI" updte + [ "$status" -ne 0 ] + [[ "$output" == *"mac update"* ]] +} + +@test "no arguments prints usage and fails" { + run bash "$CLI" + [ "$status" -ne 0 ] + [[ "$output" == *"mac help"* ]] +} + +@test "mac setup --help prints usage without running" { + run bash "$CLI" setup --help + [ "$status" -eq 0 ] + [[ "$output" == *"Usage: mac setup"* ]] +} + +@test "mac doctor --help prints usage without diagnostics" { + run bash "$CLI" doctor --help + [ "$status" -eq 0 ] + [[ "$output" == *"Usage: mac doctor"* ]] +} diff --git a/tests/command_registry.bats b/tests/command_registry.bats new file mode 100644 index 0000000..99f0e02 --- /dev/null +++ b/tests/command_registry.bats @@ -0,0 +1,61 @@ +#!/usr/bin/env bats + +setup() { + REPO_DIR="$(cd "$BATS_TEST_DIRNAME/.." && pwd)" + source "$REPO_DIR/scripts/lib/command_registry.sh" + COMMANDS_DIR="$REPO_DIR/scripts/commands" +} + +@test "registry build lists the known commands" { + run command_registry_build "$COMMANDS_DIR" + [ "$status" -eq 0 ] + [[ "$output" == *setup* ]] + [[ "$output" == *doctor* ]] + [[ "$output" == *update* ]] + [[ "$output" == *uninstall* ]] +} + +@test "registry captures the Description header" { + run command_registry_build "$COMMANDS_DIR" + [[ "$output" == *diagnostics* ]] +} + +@test "command_exists distinguishes known and unknown commands" { + run command_registry_command_exists "$COMMANDS_DIR" setup + [ "$status" -eq 0 ] + run command_registry_command_exists "$COMMANDS_DIR" nope + [ "$status" -ne 0 ] +} + +@test "command_script_path resolves to the command file" { + run command_registry_command_path "$COMMANDS_DIR" doctor + [ "$status" -eq 0 ] + [[ "$output" == *"/commands/doctor.sh" ]] +} + +@test "@name and @description metadata headers override defaults" { + tmp_dir="$(mktemp -d)" + cat >"$tmp_dir/whatever.sh" <<'CMD' +#!/bin/bash +# @name greet +# @description Say hello to the user. +echo hi +CMD + chmod +x "$tmp_dir/whatever.sh" + + run command_registry_build "$tmp_dir" + rm -rf "$tmp_dir" + [ "$status" -eq 0 ] + # The name column is overridden to "greet" (the filename would be "whatever"). + [[ "$output" == greet$'\t'* ]] + [[ "$output" == *"Say hello to the user."* ]] +} + +@test "non-executable files are ignored" { + tmp_dir="$(mktemp -d)" + printf '#!/bin/bash\n# Description: ignored\n' >"$tmp_dir/skip.sh" + run command_registry_build "$tmp_dir" + rm -rf "$tmp_dir" + [ "$status" -eq 0 ] + [ -z "$output" ] +} diff --git a/tests/completion.bats b/tests/completion.bats new file mode 100644 index 0000000..5ecd85b --- /dev/null +++ b/tests/completion.bats @@ -0,0 +1,11 @@ +#!/usr/bin/env bats + +setup() { + REPO_DIR="$(cd "$BATS_TEST_DIRNAME/.." && pwd)" +} + +@test "the committed zsh completion matches the generator output" { + bash "$REPO_DIR/scripts/generate-zsh-completion.sh" >/dev/null + run git -C "$REPO_DIR" diff --exit-code -- configs/zsh/completions/_mac + [ "$status" -eq 0 ] +} diff --git a/tests/doctor.bats b/tests/doctor.bats new file mode 100644 index 0000000..1853dec --- /dev/null +++ b/tests/doctor.bats @@ -0,0 +1,30 @@ +#!/usr/bin/env bats + +setup() { + REPO_DIR="$(cd "$BATS_TEST_DIRNAME/.." && pwd)" +} + +@test "doctor exits non-zero and reports the missing tool" { + bin="$(mktemp -d)" + for tool in bash dirname; do + ln -s "$(command -v "$tool")" "$bin/$tool" + done + # Everything resolves except mac, so the diagnostic must fail. + for stub in sw_vers uname brew git zsh; do + printf '#!/bin/sh\nexit 0\n' >"$bin/$stub" + chmod +x "$bin/$stub" + done + + run env PATH="$bin" bash "$REPO_DIR/scripts/commands/doctor.sh" + rm -rf "$bin" + + [ "$status" -ne 0 ] + [[ "$output" == *"mac CLI missing"* ]] + [[ "$output" == *"Doctor found problems"* ]] +} + +@test "doctor --help is read-only and exits cleanly" { + run bash "$REPO_DIR/scripts/commands/doctor.sh" --help + [ "$status" -eq 0 ] + [[ "$output" == *"Usage: mac doctor"* ]] +} diff --git a/tests/install.bats b/tests/install.bats new file mode 100644 index 0000000..23960a5 --- /dev/null +++ b/tests/install.bats @@ -0,0 +1,58 @@ +#!/usr/bin/env bats + +# install.sh targets macOS and clones MAC_DEV_SETUP_REPO_URL. Point the URL at +# the local repository so the test runs offline, and use a unique CLI name so it +# never collides with a real "mac" already on PATH. + +setup() { + REPO_DIR="$(cd "$BATS_TEST_DIRNAME/.." && pwd)" + if [ "$(uname -s)" != "Darwin" ]; then + skip "install.sh targets macOS only" + fi + # Clear inherited git env (e.g. when run inside a husky hook) so the local + # clone in install.sh is not hijacked by the parent repo's GIT_DIR. + unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_PREFIX \ + GIT_OBJECT_DIRECTORY GIT_COMMON_DIR 2>/dev/null || true + WORK="$(mktemp -d)" + CLI_NAME="mds-test-cli" + export MAC_DEV_SETUP_REPO_URL="$REPO_DIR" + export MAC_DEV_SETUP_INSTALL_DIR="$WORK/install" + export MAC_DEV_SETUP_BIN_DIR="$WORK/bin" + export MAC_DEV_SETUP_SHELL_CONFIG="$WORK/profile" + export MAC_DEV_SETUP_CLI_NAME="$CLI_NAME" + INSTALL="$REPO_DIR/install.sh" +} + +teardown() { + rm -rf "$WORK" +} + +@test "install creates the CLI symlink and a managed PATH block" { + run bash "$INSTALL" + [ "$status" -eq 0 ] + [ -L "$WORK/bin/$CLI_NAME" ] + run grep -F "mac-dev-setup PATH" "$WORK/profile" + [ "$status" -eq 0 ] +} + +@test "install is idempotent" { + bash "$INSTALL" + run bash "$INSTALL" + [ "$status" -eq 0 ] + [[ "$output" == *"already installed"* ]] +} + +@test "uninstall removes the symlink and the PATH block" { + bash "$INSTALL" + run bash "$INSTALL" --uninstall + [ "$status" -eq 0 ] + [ ! -e "$WORK/bin/$CLI_NAME" ] + run grep -F "mac-dev-setup PATH" "$WORK/profile" + [ "$status" -ne 0 ] +} + +@test "install --help prints usage" { + run bash "$INSTALL" --help + [ "$status" -eq 0 ] + [[ "$output" == *"Usage: install.sh"* ]] +} diff --git a/tests/path_manager.bats b/tests/path_manager.bats new file mode 100644 index 0000000..0728ef3 --- /dev/null +++ b/tests/path_manager.bats @@ -0,0 +1,51 @@ +#!/usr/bin/env bats + +setup() { + REPO_DIR="$(cd "$BATS_TEST_DIRNAME/.." && pwd)" + source "$REPO_DIR/scripts/lib/path_manager.sh" + # path_manager.sh enables `set -euo pipefail`; relax it so the test driver + # behaves normally (the functions do not rely on it). + set +e +u +o pipefail + PROFILE_FILE="$(mktemp)" + BIN_DIR="$HOME/.local/bin" +} + +teardown() { + rm -f "$PROFILE_FILE" +} + +@test "install writes a managed PATH block" { + path_manager_install "$BIN_DIR" "$PROFILE_FILE" + run grep -F "$PATH_MANAGER_BEGIN_MARKER" "$PROFILE_FILE" + [ "$status" -eq 0 ] + run grep -F "$PATH_MANAGER_END_MARKER" "$PROFILE_FILE" + [ "$status" -eq 0 ] +} + +@test "install is idempotent" { + path_manager_install "$BIN_DIR" "$PROFILE_FILE" + path_manager_install "$BIN_DIR" "$PROFILE_FILE" + run grep -cF "$PATH_MANAGER_BEGIN_MARKER" "$PROFILE_FILE" + [ "$output" -eq 1 ] +} + +@test "uninstall removes the managed block" { + path_manager_install "$BIN_DIR" "$PROFILE_FILE" + path_manager_uninstall "$BIN_DIR" "$PROFILE_FILE" + run grep -F "$PATH_MANAGER_BEGIN_MARKER" "$PROFILE_FILE" + [ "$status" -ne 0 ] +} + +@test "uninstall preserves unrelated content" { + printf 'export EXISTING=1\n' >"$PROFILE_FILE" + path_manager_install "$BIN_DIR" "$PROFILE_FILE" + path_manager_uninstall "$BIN_DIR" "$PROFILE_FILE" + run grep -F "export EXISTING=1" "$PROFILE_FILE" + [ "$status" -eq 0 ] +} + +@test "uninstall on a clean file is a no-op" { + printf 'export EXISTING=1\n' >"$PROFILE_FILE" + run path_manager_uninstall "$BIN_DIR" "$PROFILE_FILE" + [ "$status" -eq 0 ] +} diff --git a/tests/profiles.bats b/tests/profiles.bats new file mode 100644 index 0000000..46bfeb2 --- /dev/null +++ b/tests/profiles.bats @@ -0,0 +1,49 @@ +#!/usr/bin/env bats + +setup() { + REPO_DIR="$(cd "$BATS_TEST_DIRNAME/.." && pwd)" + source "$REPO_DIR/scripts/lib/profiles.sh" +} + +@test "profile_default is full" { + [ "$(profile_default)" = "full" ] +} + +@test "profile_list returns discovered profiles, sorted" { + run profile_list "$REPO_DIR" + [ "$status" -eq 0 ] + [ "$output" = "full minimal" ] +} + +@test "profile_brewfile builds the expected path" { + [ "$(profile_brewfile "$REPO_DIR" minimal)" = "$REPO_DIR/profiles/minimal/Brewfile" ] +} + +@test "profile_validate accepts existing profiles" { + run profile_validate "$REPO_DIR" full + [ "$status" -eq 0 ] + run profile_validate "$REPO_DIR" minimal + [ "$status" -eq 0 ] +} + +@test "profile_validate rejects empty, unknown and traversal names" { + run profile_validate "$REPO_DIR" "" + [ "$status" -ne 0 ] + run profile_validate "$REPO_DIR" nope + [ "$status" -ne 0 ] + run profile_validate "$REPO_DIR" "../etc" + [ "$status" -ne 0 ] + run profile_validate "$REPO_DIR" "a/b" + [ "$status" -ne 0 ] +} + +@test "profile_name_is_valid enforces a safe charset" { + run profile_name_is_valid full + [ "$status" -eq 0 ] + run profile_name_is_valid my-profile_2 + [ "$status" -eq 0 ] + run profile_name_is_valid "with space" + [ "$status" -ne 0 ] + run profile_name_is_valid "../x" + [ "$status" -ne 0 ] +} diff --git a/tests/setup.bats b/tests/setup.bats new file mode 100644 index 0000000..668ac7c --- /dev/null +++ b/tests/setup.bats @@ -0,0 +1,31 @@ +#!/usr/bin/env bats + +setup() { + REPO_DIR="$(cd "$BATS_TEST_DIRNAME/.." && pwd)" +} + +@test "setup dry run reports the dry-run mode and exits cleanly" { + run bash "$REPO_DIR/scripts/cli.sh" setup --profile minimal --dry-run + [ "$status" -eq 0 ] + [[ "$output" == *"Dry run mode activated"* ]] +} + +@test "setup dry run creates no logs directory in the working directory" { + tmp="$(mktemp -d)" + ( cd "$tmp" && bash "$REPO_DIR/scripts/setup.sh" --profile minimal --dry-run >/dev/null ) + result=1 + [ -e "$tmp/logs" ] || result=0 + rm -rf "$tmp" + [ "$result" -eq 0 ] +} + +@test "setup.sh uses an absolute log path, not a relative logs/ directory" { + run grep -Eq 'mkdir -p logs|"logs/setup\.log"' "$REPO_DIR/scripts/setup.sh" + [ "$status" -ne 0 ] +} + +@test "setup rejects an unsafe profile name" { + run bash "$REPO_DIR/scripts/cli.sh" setup --profile "no/such" + [ "$status" -ne 0 ] + [[ "$output" == *"Invalid profile"* ]] +} diff --git a/tests/uninstall.bats b/tests/uninstall.bats new file mode 100644 index 0000000..ce095fe --- /dev/null +++ b/tests/uninstall.bats @@ -0,0 +1,25 @@ +#!/usr/bin/env bats + +setup() { + REPO_DIR="$(cd "$BATS_TEST_DIRNAME/.." && pwd)" +} + +@test "uninstall --remove-config removes managed files only when identical" { + home="$(mktemp -d)" + cp "$REPO_DIR/configs/zsh/.zprofile" "$home/.zprofile" + printf 'unmanaged content\n' >"$home/.zshrc" + + run env HOME="$home" bash "$REPO_DIR/scripts/commands/uninstall.sh" \ + --remove-config --dry-run + rm -rf "$home" + + [ "$status" -eq 0 ] + [[ "$output" == *"Would remove .zprofile"* ]] + [[ "$output" == *".zshrc differs"* ]] +} + +@test "uninstall --help prints usage and exits cleanly" { + run bash "$REPO_DIR/scripts/commands/uninstall.sh" --help + [ "$status" -eq 0 ] + [[ "$output" == *"Usage: mac uninstall"* ]] +} diff --git a/tests/update.bats b/tests/update.bats new file mode 100644 index 0000000..113c714 --- /dev/null +++ b/tests/update.bats @@ -0,0 +1,79 @@ +#!/usr/bin/env bats + +# Exercises scripts/commands/update.sh against a throwaway local remote, so no +# network access is required. update.sh resolves its REPO_DIR from its own +# location, so running the copy inside the clone operates on the clone. + +setup() { + REPO_DIR="$(cd "$BATS_TEST_DIRNAME/.." && pwd)" + # When the suite runs inside a git hook (husky), git exports GIT_DIR/ + # GIT_INDEX_FILE etc.; clear them so `git -C ` is not hijacked. + unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_PREFIX \ + GIT_OBJECT_DIRECTORY GIT_COMMON_DIR 2>/dev/null || true + WORK="$(mktemp -d)" + REMOTE="$WORK/remote.git" + SEED="$WORK/seed" + CLONE="$WORK/clone" + + git init -q --bare "$REMOTE" + + mkdir -p "$SEED" + cp -R "$REPO_DIR/scripts" "$SEED/scripts" + git -C "$SEED" init -q -b main + git -C "$SEED" config user.email "test@example.com" + git -C "$SEED" config user.name "Test" + git -C "$SEED" add . + git -C "$SEED" commit -qm "init" + git -C "$SEED" remote add origin "$REMOTE" + git -C "$SEED" push -q -u origin main + + git clone -q "$REMOTE" "$CLONE" + UPDATE="$CLONE/scripts/commands/update.sh" +} + +teardown() { + rm -rf "$WORK" +} + +seed_new_commit() { + echo "change $1" >>"$SEED/scripts/marker.txt" + git -C "$SEED" add . + git -C "$SEED" commit -qm "update $1" + git -C "$SEED" push -q origin main +} + +@test "update reports an up-to-date checkout" { + run bash "$UPDATE" + [ "$status" -eq 0 ] + [[ "$output" == *"already up to date"* ]] +} + +@test "dry run does not move HEAD" { + seed_new_commit a + before="$(git -C "$CLONE" rev-parse HEAD)" + run bash "$UPDATE" --dry-run + [ "$status" -eq 0 ] + [[ "$output" == *"Dry run OK"* ]] + [ "$(git -C "$CLONE" rev-parse HEAD)" = "$before" ] +} + +@test "update fast-forwards to the remote tip" { + seed_new_commit b + target="$(git -C "$SEED" rev-parse HEAD)" + run bash "$UPDATE" + [ "$status" -eq 0 ] + [ "$(git -C "$CLONE" rev-parse HEAD)" = "$target" ] +} + +@test "update refuses when tracked files are dirty" { + printf '\n# local edit\n' >>"$CLONE/scripts/cli.sh" + run bash "$UPDATE" + [ "$status" -ne 0 ] + [[ "$output" == *"local changes"* ]] +} + +@test "update --help prints usage" { + run bash "$UPDATE" --help + [ "$status" -eq 0 ] + [[ "$output" == *"Usage: mac update"* ]] +} From a12058969865c0de804f5a0fdc03b2837021bfb2 Mon Sep 17 00:00:00 2001 From: Labault Date: Wed, 17 Jun 2026 19:42:07 +0200 Subject: [PATCH 04/10] =?UTF-8?q?=F0=9F=91=B7=20ci:=20run=20the=20bats=20t?= =?UTF-8?q?est=20suite=20in=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run `npm test` (the bats suite) in both gates: the Ubuntu quality job (libs, CLI, update lifecycle — install tests skip off macOS) and the macOS job (adds real install.sh coverage via Node + npm ci). Drop the standalone "Verify generated Zsh completion" step, now covered by completion.bats. --- .github/workflows/ci-macos.yml | 13 +++++++++++++ .github/workflows/ci.yml | 8 +++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml index e091102..f0c62e4 100644 --- a/.github/workflows/ci-macos.yml +++ b/.github/workflows/ci-macos.yml @@ -14,6 +14,19 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Install Node dependencies + env: + HUSKY: "0" + run: npm ci + + - name: Run test suite + run: npm test + - name: Cache Homebrew uses: actions/cache@v4 with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a6b183d..13c3206 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,6 +80,9 @@ jobs: HUSKY: "0" run: npm ci + - name: Run test suite + run: npm test + - name: Lint commit messages env: PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} @@ -113,8 +116,3 @@ jobs: - name: Run repository quality checks run: pre-commit run --all-files --show-diff-on-failure - - - name: Verify generated Zsh completion - run: | - bash scripts/generate-zsh-completion.sh - git diff --exit-code -- configs/zsh/completions/_mac From 59188ab434678d8fceffde7b2a55e7b6bb5ab39b Mon Sep 17 00:00:00 2001 From: Labault Date: Wed, 17 Jun 2026 19:43:45 +0200 Subject: [PATCH 05/10] =?UTF-8?q?=F0=9F=91=B7=20ci:=20drop=20release-pleas?= =?UTF-8?q?e=20in=20favor=20of=20the=20documented=20manual=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit release-please was configured but unreliable: the project's gitmoji-first commits (e.g. "🐛 fix:") are not parsed as conventional types by release-please, so it would rarely detect releasable changes — a false sense of automation that also contradicted the fully manual flow already described in release-process.md. Remove the workflow, make the manual-release choice explicit in release-process.md, and update the 1.0.0 criterion accordingly. --- .github/workflows/release-please.yml | 21 --------------------- docs/releases/release-process.md | 5 +++++ docs/releases/v1.0.0.md | 2 +- 3 files changed, 6 insertions(+), 22 deletions(-) delete mode 100644 .github/workflows/release-please.yml diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml deleted file mode 100644 index 7391613..0000000 --- a/.github/workflows/release-please.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Release Please - -on: - push: - branches: - - main - -permissions: - contents: write - pull-requests: write - -jobs: - release: - runs-on: ubuntu-latest - - steps: - - name: Run Release Please - uses: googleapis/release-please-action@v4 - with: - release-type: simple - package-name: mac-dev-setup diff --git a/docs/releases/release-process.md b/docs/releases/release-process.md index 2b07c1d..112e84e 100644 --- a/docs/releases/release-process.md +++ b/docs/releases/release-process.md @@ -4,6 +4,11 @@ This document describes the release workflow for Mac Dev Setup. The project uses semantic versioning where applicable and maintains notable changes in the root `CHANGELOG.md` file. +Releases are performed **manually** by following the steps below. There is no +automated release bot: the repository's gitmoji-first commit convention does not +map cleanly onto conventional-commit release automation, so the changelog, +version bump, tag, and GitHub release are prepared by a maintainer. + ## Release preparation Before starting a release: diff --git a/docs/releases/v1.0.0.md b/docs/releases/v1.0.0.md index ef24cbd..253f2b8 100644 --- a/docs/releases/v1.0.0.md +++ b/docs/releases/v1.0.0.md @@ -52,7 +52,7 @@ uninstall flows, and quality checks suitable for ongoing maintenance. - [x] Repository quality validation is configured in GitHub Actions. - [x] Homebrew validation is configured in GitHub Actions. - [x] macOS validation is configured in GitHub Actions. -- [x] Release Please automation is configured for future releases. +- [x] The release process is documented and performed manually (see release-process.md). - [x] Dependabot monitors GitHub Actions dependencies. ## Documentation From 557803bc669703181140664595716f26651bda76 Mon Sep 17 00:00:00 2001 From: Labault Date: Wed, 17 Jun 2026 19:47:26 +0200 Subject: [PATCH 06/10] =?UTF-8?q?=E2=9C=A8=20feat(cli):=20expose=20default?= =?UTF-8?q?s,=20keyboard=20and=20vscode=20commands?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit apply-macos-defaults.sh, install-keyboard-layout.sh and install-vscode-extensions.sh existed but were not reachable from the CLI. Add discoverable `mac defaults`, `mac keyboard` and `mac vscode` (passing through --with-optional) wrappers with --help, regenerate the zsh completion, cover them in the bats suite, and document the commands in the README and the relevant tool docs. They remain opt-in and are not run by `mac setup`. --- README.md | 11 +++++++++++ configs/zsh/completions/_mac | 3 +++ docs/keyboard/french-oss.md | 8 +++++++- docs/macos/macos-defaults.md | 8 +++++++- docs/vscode/vscode.md | 16 ++++++++++++++-- scripts/commands/defaults.sh | 35 +++++++++++++++++++++++++++++++++++ scripts/commands/keyboard.sh | 36 ++++++++++++++++++++++++++++++++++++ scripts/commands/vscode.sh | 30 ++++++++++++++++++++++++++++++ tests/cli.bats | 11 +++++++++++ 9 files changed, 154 insertions(+), 4 deletions(-) create mode 100755 scripts/commands/defaults.sh create mode 100755 scripts/commands/keyboard.sh create mode 100755 scripts/commands/vscode.sh diff --git a/README.md b/README.md index 9331f8b..de902ca 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,17 @@ Remove the installed checkout when it matches `~/.mac-dev-setup`: mac uninstall --remove-install-dir ``` +### Optional commands + +These commands apply opt-in changes and are not run by `mac setup`: + +```bash +mac defaults # apply curated macOS Finder/Dock/keyboard defaults +mac keyboard # install the Francais OSS Mac keyboard layout +mac vscode # install the curated VS Code extensions +mac vscode --with-optional +``` + ## Managed Files MacDevSetup may manage these user-level files: diff --git a/configs/zsh/completions/_mac b/configs/zsh/completions/_mac index ea5ef5d..346e1e9 100644 --- a/configs/zsh/completions/_mac +++ b/configs/zsh/completions/_mac @@ -7,10 +7,13 @@ _mac() { commands=( 'help:List available commands.' + 'defaults:Apply the curated macOS system defaults.' 'doctor:Run system diagnostics for the macOS development setup.' + 'keyboard:Install the French OSS keyboard layout.' 'setup:Install and configure the macOS development setup.' 'uninstall:Remove the mac CLI symlink and managed shell PATH entry.' 'update:Update the mac CLI from its git repository.' + 'vscode:Install the curated VS Code extensions.' ) if (( CURRENT == 2 )); then diff --git a/docs/keyboard/french-oss.md b/docs/keyboard/french-oss.md index 8adc7ca..5eed79f 100644 --- a/docs/keyboard/french-oss.md +++ b/docs/keyboard/french-oss.md @@ -52,7 +52,13 @@ The layout contains adaptations for the internal Apple keyboard, including corre ## Installation -Install the layout with: +Install the layout with the CLI: + +~~~bash +mac keyboard +~~~ + +Or run the script directly: ~~~bash ./scripts/install-keyboard-layout.sh diff --git a/docs/macos/macos-defaults.md b/docs/macos/macos-defaults.md index 6cc1992..d4f7850 100644 --- a/docs/macos/macos-defaults.md +++ b/docs/macos/macos-defaults.md @@ -73,7 +73,13 @@ Validate it with ShellCheck: shellcheck scripts/apply-macos-defaults.sh ~~~ -Apply the configuration: +Apply the configuration with the CLI: + +~~~bash +mac defaults +~~~ + +Or run the script directly: ~~~bash ./scripts/apply-macos-defaults.sh diff --git a/docs/vscode/vscode.md b/docs/vscode/vscode.md index 23699ce..bdfb884 100644 --- a/docs/vscode/vscode.md +++ b/docs/vscode/vscode.md @@ -54,13 +54,19 @@ They cover the main PHP and Symfony development workflow: - DotEnv syntax support - YAML validation and completion -Install them with: +Install them with the CLI: + +~~~bash +mac vscode +~~~ + +Or run the script directly: ~~~bash ./scripts/install-vscode-extensions.sh ~~~ -The script is idempotent and skips extensions that are already installed. +The command is idempotent and skips extensions that are already installed. ## Optional extensions @@ -74,6 +80,12 @@ They include personal, visual, AI-assisted, and specialized workflow extensions. Install both recommended and optional extensions with: +~~~bash +mac vscode --with-optional +~~~ + +Or run the script directly: + ~~~bash ./scripts/install-vscode-extensions.sh --with-optional ~~~ diff --git a/scripts/commands/defaults.sh b/scripts/commands/defaults.sh new file mode 100755 index 0000000..285be29 --- /dev/null +++ b/scripts/commands/defaults.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# Description: Apply the curated macOS system defaults. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" + +# shellcheck source=scripts/lib/logging.sh +source "$REPO_DIR/scripts/lib/logging.sh" + +print_usage() { + log_line "Usage: mac defaults [--help]" + log_line "" + log_line "Apply the curated Finder, Dock, screenshot, and keyboard defaults." +} + +main() { + case "${1:-}" in + "") ;; + --help | -h) + print_usage + exit 0 + ;; + *) + error "Unknown option: $1" + print_usage >&2 + exit 1 + ;; + esac + + exec bash "$REPO_DIR/scripts/apply-macos-defaults.sh" +} + +main "$@" diff --git a/scripts/commands/keyboard.sh b/scripts/commands/keyboard.sh new file mode 100755 index 0000000..bdb212d --- /dev/null +++ b/scripts/commands/keyboard.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# Description: Install the French OSS keyboard layout. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" + +# shellcheck source=scripts/lib/logging.sh +source "$REPO_DIR/scripts/lib/logging.sh" + +print_usage() { + log_line "Usage: mac keyboard [--help]" + log_line "" + log_line "Install the bundled Francais OSS Mac keyboard layout. Log out and" + log_line "back in, then enable it in macOS Input Sources." +} + +main() { + case "${1:-}" in + "") ;; + --help | -h) + print_usage + exit 0 + ;; + *) + error "Unknown option: $1" + print_usage >&2 + exit 1 + ;; + esac + + exec bash "$REPO_DIR/scripts/install-keyboard-layout.sh" +} + +main "$@" diff --git a/scripts/commands/vscode.sh b/scripts/commands/vscode.sh new file mode 100755 index 0000000..e2ac76f --- /dev/null +++ b/scripts/commands/vscode.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Description: Install the curated VS Code extensions. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" + +# shellcheck source=scripts/lib/logging.sh +source "$REPO_DIR/scripts/lib/logging.sh" + +print_usage() { + log_line "Usage: mac vscode [--with-optional] [--help]" + log_line "" + log_line "Install the recommended VS Code extensions, optionally including" + log_line "the optional set with --with-optional." +} + +main() { + case "${1:-}" in + --help | -h) + print_usage + exit 0 + ;; + esac + + exec bash "$REPO_DIR/scripts/install-vscode-extensions.sh" "$@" +} + +main "$@" diff --git a/tests/cli.bats b/tests/cli.bats index ecb4cea..d61edd4 100644 --- a/tests/cli.bats +++ b/tests/cli.bats @@ -12,6 +12,17 @@ setup() { [[ "$output" == *doctor* ]] [[ "$output" == *update* ]] [[ "$output" == *uninstall* ]] + [[ "$output" == *defaults* ]] + [[ "$output" == *keyboard* ]] + [[ "$output" == *vscode* ]] +} + +@test "the extra commands expose --help" { + for cmd in defaults keyboard vscode; do + run bash "$CLI" "$cmd" --help + [ "$status" -eq 0 ] + [[ "$output" == *"Usage: mac $cmd"* ]] + done } @test "mac --help and -h behave like help" { From 39f07653e41f3b1f4de77bbcbeed87bafc009dd6 Mon Sep 17 00:00:00 2001 From: Labault Date: Wed, 17 Jun 2026 19:48:03 +0200 Subject: [PATCH 07/10] =?UTF-8?q?=F0=9F=93=9D=20docs(changelog):=20record?= =?UTF-8?q?=20the=20v1-readiness=20work?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Note the new opt-in commands, the bats suite, shellcheck gating, the manual release process, and the setup exit-status fix in the Unreleased section. --- CHANGELOG.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4918c82..39e7e4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,10 @@ The format is inspired by Keep a Changelog, and the project follows semantic ver - Generated Zsh completion for `mac`, built from the command registry and verified in CI. - Structured logging helpers and unified error handling across the CLI. -- CLI smoke tests (`npm test`) and commit linting through commitlint. +- Opt-in `mac defaults`, `mac keyboard` and `mac vscode` commands wrapping the + macOS defaults, keyboard layout and VS Code extension scripts. +- A bats test suite covering the libraries, CLI, update and install flows, run + via `npm test` in CI. ### Changed @@ -35,16 +38,20 @@ The format is inspired by Keep a Changelog, and the project follows semantic ver writes to the global config. - macOS CI consolidated into a single gate, and commits are linted with commitlint across the pull request range. +- Shell scripts are linted with shellcheck in CI. - `mac setup` writes its log to a user-level location instead of the current working directory. +- Releases are performed manually following the documented process; the + unreliable release-please workflow was removed. ### Fixed - `mac setup --help` and `mac doctor --help` print usage instead of failing or silently running diagnostics. - `mac doctor` exits non-zero when a required tool is missing. -- Setup no longer regenerates the versioned completion file and no longer fails - when completion is not yet active in the current shell. +- Setup no longer regenerates the versioned completion file, no longer fails + when completion is not yet active in the current shell, and reports a reliable + exit status. ## 0.4.1 - 2026-06-17 From 30c49a58c364ee38acb1f3d1c56031b3efddcd37 Mon Sep 17 00:00:00 2001 From: Labault Date: Wed, 17 Jun 2026 19:50:13 +0200 Subject: [PATCH 08/10] =?UTF-8?q?=E2=9C=85=20test:=20pin=20the=20update=20?= =?UTF-8?q?test=20remote=20to=20the=20main=20branch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The update lifecycle test created the throwaway bare remote with the runner's default branch (master on CI) while pushing main, leaving the clone with a dangling HEAD and no files. Initialise the bare remote with `-b main` so the test is deterministic regardless of init.defaultBranch. --- tests/update.bats | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/update.bats b/tests/update.bats index 113c714..2897488 100644 --- a/tests/update.bats +++ b/tests/update.bats @@ -15,7 +15,9 @@ setup() { SEED="$WORK/seed" CLONE="$WORK/clone" - git init -q --bare "$REMOTE" + # Pin the bare repo's default branch so it matches the pushed branch + # regardless of the runner's init.defaultBranch. + git init -q --bare -b main "$REMOTE" mkdir -p "$SEED" cp -R "$REPO_DIR/scripts" "$SEED/scripts" From d4743f82b2f6735e75a50e417efdc92de4164d0a Mon Sep 17 00:00:00 2001 From: Labault Date: Wed, 17 Jun 2026 19:53:38 +0200 Subject: [PATCH 09/10] =?UTF-8?q?=E2=9C=85=20test:=20clone=20install=20tes?= =?UTF-8?q?t=20from=20a=20dedicated=20seed=20repo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The install idempotency test cloned the working checkout, whose HEAD can be detached on CI (a PR merge ref), breaking the second run's `git pull --ff-only`. Build a seed repo on a real main branch and install from it, making the test independent of how the runner checked the repo out. --- tests/install.bats | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/install.bats b/tests/install.bats index 23960a5..0d1a562 100644 --- a/tests/install.bats +++ b/tests/install.bats @@ -15,7 +15,20 @@ setup() { GIT_OBJECT_DIRECTORY GIT_COMMON_DIR 2>/dev/null || true WORK="$(mktemp -d)" CLI_NAME="mds-test-cli" - export MAC_DEV_SETUP_REPO_URL="$REPO_DIR" + + # Clone from a dedicated seed repo on a real "main" branch rather than the + # working checkout, which may be detached (e.g. a PR merge ref) on CI and + # would break the idempotent `git pull --ff-only` path. + SEED="$WORK/seed" + mkdir -p "$SEED" + cp -R "$REPO_DIR/scripts" "$SEED/scripts" + git -C "$SEED" init -q -b main + git -C "$SEED" config user.email "test@example.com" + git -C "$SEED" config user.name "Test" + git -C "$SEED" add . + git -C "$SEED" commit -qm "seed" + + export MAC_DEV_SETUP_REPO_URL="$SEED" export MAC_DEV_SETUP_INSTALL_DIR="$WORK/install" export MAC_DEV_SETUP_BIN_DIR="$WORK/bin" export MAC_DEV_SETUP_SHELL_CONFIG="$WORK/profile" From f03ebe6cb523e7ed941c2f4588b10b1cac60d3f3 Mon Sep 17 00:00:00 2001 From: Labault Date: Wed, 17 Jun 2026 20:02:34 +0200 Subject: [PATCH 10/10] =?UTF-8?q?=F0=9F=90=9B=20fix(brew):=20stop=20declar?= =?UTF-8?q?ing=20VS=20Code=20extensions=20in=20the=20Brewfile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The full Brewfile duplicated configs/vscode/extensions(.optional).txt as `vscode` entries, so `brew bundle` pulled them from the VS Code marketplace during macOS CI — a flaky dependency that failed on anthropic.claude-code. Remove the vscode entries; extensions are now managed solely through `mac vscode` (and `--with-optional`), and the macOS gate no longer depends on the marketplace. --- CHANGELOG.md | 3 +++ README.md | 6 +++--- profiles/full/Brewfile | 18 ------------------ 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39e7e4f..3f2afbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,9 @@ The format is inspired by Keep a Changelog, and the project follows semantic ver - macOS CI consolidated into a single gate, and commits are linted with commitlint across the pull request range. - Shell scripts are linted with shellcheck in CI. +- VS Code extensions are no longer declared in the Brewfile (which made the + macOS CI depend on the flaky extension marketplace); install them with + `mac vscode`, the single source of truth being the configs/vscode lists. - `mac setup` writes its log to a user-level location instead of the current working directory. - Releases are performed manually following the documented process; the diff --git a/README.md b/README.md index de902ca..333aad8 100644 --- a/README.md +++ b/README.md @@ -65,9 +65,9 @@ Setup: maintenance. `full` installs the complete curated developer environment, including language -tooling, quality tools, GUI applications, VS Code extensions, and -container/database utilities. The root `Brewfile` is a compatibility link to -`profiles/full/Brewfile`. +tooling, quality tools, GUI applications, and container/database utilities. The +root `Brewfile` is a compatibility link to `profiles/full/Brewfile`. VS Code +extensions are managed separately and installed with `mac vscode`. ## CLI Usage diff --git a/profiles/full/Brewfile b/profiles/full/Brewfile index bd376c3..d08995d 100644 --- a/profiles/full/Brewfile +++ b/profiles/full/Brewfile @@ -97,24 +97,6 @@ cask "visual-studio-code" # Rust-based terminal cask "warp" -# Editor extensions - -vscode "anthropic.claude-code" -vscode "avaly.restore-git-branch-tabs-improved" -vscode "bmewburn.vscode-intelephense-client" -vscode "christian-kohler.path-intellisense" -vscode "eamodio.gitlens" -vscode "github.vscode-github-actions" -vscode "hogashi.crontab-syntax-highlight" -vscode "mechatroner.rainbow-csv" -vscode "mehedidracula.php-namespace-resolver" -vscode "mikestead.dotenv" -vscode "monokai.theme-monokai-pro-vscode" -vscode "redhat.vscode-yaml" -vscode "sanderronde.phpstan-vscode" -vscode "whatwedo.twig" -vscode "xdebug.php-debug" - # Runtime-managed tools uv "claude-monitor"