Add deploy command: ff the live checkout
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (mypy, mypy) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (pylint, pylint) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (rest, ) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (shellcheck, shellcheck) (push) Waiting to run
Python tests / test (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / lib-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run

testsAndMisc had no '- deploy:' line, so Ship reported 'pushed, not
deployed'. scripts/deploy.sh fast-forwards ~/src/testsAndMisc to
origin/main, keeps unrelated local edits, and health-checks HEAD =
origin/main plus the tracked hooks. bats: 7/7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-11-12 08:57:05 +01:00
parent 13db6e8116
commit f75fb60687
5 changed files with 172 additions and 37 deletions

View File

@ -172,6 +172,7 @@ number of error codes, so it is weaker than the `strict = true` in the config.
## Commands
- run: n/a: monorepo of independent scripts and apps, no single entry point (see Repository Layout)
- deploy: `scripts/deploy.sh` (fast-forwards the live `~/src/testsAndMisc` checkout to origin/main, keeping unrelated local edits; exits non-zero unless HEAD = origin/main and the tracked hooks are wired)
- test: `python3 -m pytest -q -o addopts="--strict-markers --strict-config"`
- test-changed: `scripts/test_changed.sh`
- lint: `pre-commit run --hook-stage pre-commit`

View File

@ -1,37 +0,0 @@
{
"intent": "The i3blocks shutdown countdown shows and enforces minute-precision shutdown times (e.g. 18:30) now that /etc/shutdown-schedule.conf carries *_MINUTES keys, while still reading pre-migration hour-only files.",
"scope": [
"linux_configuration/i3blocks/shutdown_countdown.sh",
"linux_configuration/tests/test_i3blocks_efficiency.sh",
"Non-goal: the writers (screen-locker helper, digital-wellbeing installer) live in their own repos"
],
"changes": [
"Parse MON_WED_MINUTES/THU_SUN_MINUTES/MORNING_END_MINUTES, prefer them, fall back to *_HOUR * 60; reject values outside 0-1440",
"Shutdown epoch and window thresholds computed from minutes; still builtins only (no forks)",
"Regression test: 18:30 minutes config shows '⏻ 18:30' at 18:15 and '⏻ SHUTDOWN' at 18:31 while the legacy HOUR copy says 18"
],
"verification": [
{
"command": "bash linux_configuration/tests/test_i3blocks_efficiency.sh",
"result": "pass",
"evidence": "All i3blocks efficiency regression tests passed (fork count <= 1 for the countdown)"
},
{
"command": "SHUTDOWN_CONFIG=<dual|legacy fixture> NOW_EPOCH=<Tue 17:00/18:29/18:31, Thu 22:00> bash shutdown_countdown.sh",
"result": "pass",
"evidence": "dual: 18:30, 18:30, SHUTDOWN, SHUTDOWN; legacy (19/23): 19:00, 19:00, 19:00, 23:00"
},
{
"command": "bash ~/.config/i3blocks/shutdown_countdown.sh (live, symlinked)",
"result": "pass",
"evidence": "⏻ 23:00 against the host's current hour-only /etc/shutdown-schedule.conf"
}
],
"risks": [
"A config with only *_MINUTES and malformed values shows INVALID TIME instead of a countdown (display only; enforcement is the check script)"
],
"rollback": [
"git revert the commit; the old script reads the *_HOUR keys, which every writer still emits",
"Re-run the live block and confirm it shows the hour-rounded time"
]
}

View File

@ -0,0 +1,30 @@
{
"intent": "testsAndMisc declares a deploy command, so Ship can deploy a pushed change instead of reporting 'pushed, not deployed'.",
"scope": [
"scripts/deploy.sh (new), CLAUDE.md Commands, linux_configuration/tests/test_deploy.bats (new)",
"Non-goal: refreshing copies the sudo installers put under /usr/local or /etc"
],
"changes": [
"scripts/deploy.sh fast-forwards ~/src/testsAndMisc to origin/main (pull --ff-only), keeps unrelated local edits, refuses on collision",
"Health check: on main, HEAD = origin/main, core.hooksPath = linux_configuration/.githooks; exits non-zero otherwise",
"CLAUDE.md Commands gains '- deploy: `scripts/deploy.sh`'"
],
"verification": [
{
"command": "bats linux_configuration/tests/test_deploy.bats",
"result": "pass",
"evidence": "red first (deploy.sh missing, exit 127); then 7/7 ok: ff, idempotent, keeps WIP, refuses collision, hooks unwired fails, off-main fails, CLAUDE.md line"
},
{
"command": "shellcheck scripts/deploy.sh",
"result": "pass",
"evidence": "no findings"
}
],
"risks": [
"Live checkout carries kuhy's uncommitted edits; an incoming commit touching them makes deploy fail rather than overwrite"
],
"rollback": [
"git revert this commit; the live checkout stays where the last deploy left it"
]
}

View File

@ -0,0 +1,85 @@
#!/usr/bin/env bats
# Tests for scripts/deploy.sh against a scratch origin + "live" clone, so the
# real ~/src/testsAndMisc is never touched.
#
# bats linux_configuration/tests/test_deploy.bats
bats_require_minimum_version 1.5.0
setup() {
ROOT="$(cd -- "$(dirname -- "$BATS_TEST_FILENAME")/../.." && pwd)"
SCRIPT="$ROOT/scripts/deploy.sh"
T="$BATS_TEST_TMPDIR"
export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@example.com
export GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@example.com
git init -q --bare -b main "$T/origin.git"
git clone -q "$T/origin.git" "$T/dev" 2>/dev/null
mkdir -p "$T/dev/linux_configuration/.githooks"
echo a >"$T/dev/file.txt"
touch "$T/dev/linux_configuration/.githooks/pre-commit"
git -C "$T/dev" add -A
git -C "$T/dev" commit -q -m base
git -C "$T/dev" push -q origin HEAD:main
git clone -q "$T/origin.git" "$T/live"
git -C "$T/live" config core.hooksPath linux_configuration/.githooks
export TESTSANDMISC_DIR="$T/live"
}
push_change() {
echo "$1" >"$T/dev/${2:-file.txt}"
git -C "$T/dev" commit -qam "$1"
git -C "$T/dev" push -q origin HEAD:main
}
@test "CLAUDE.md declares scripts/deploy.sh as the deploy command" {
local tick="\`" n=0 line
while IFS= read -r line; do
[[ "$line" == "- deploy: ${tick}scripts/deploy.sh${tick}"* ]] && n=$((n + 1))
done <"$ROOT/CLAUDE.md"
[ "$n" -eq 1 ]
[ -x "$SCRIPT" ]
}
@test "fast-forwards the live checkout to origin/main" {
push_change b
run "$SCRIPT"
[ "$status" -eq 0 ]
[ "$(git -C "$T/live" rev-parse HEAD)" = "$(git -C "$T/origin.git" rev-parse main)" ]
}
@test "is idempotent: a second run is a no-op that still passes" {
push_change b
"$SCRIPT"
run "$SCRIPT"
[ "$status" -eq 0 ]
}
@test "keeps unrelated uncommitted work and still deploys" {
echo wip >"$T/live/other.txt"
git -C "$T/live" add other.txt
push_change b
run "$SCRIPT"
[ "$status" -eq 0 ]
[ "$(cat "$T/live/other.txt")" = wip ]
}
@test "refuses when uncommitted work collides with the update" {
echo mine >"$T/live/file.txt"
push_change theirs
run "$SCRIPT"
[ "$status" -ne 0 ]
[ "$(cat "$T/live/file.txt")" = mine ]
}
@test "fails the health check when hooks are not wired" {
git -C "$T/live" config --unset core.hooksPath
run "$SCRIPT"
[ "$status" -ne 0 ]
[[ "$output" == *hooksPath* ]]
}
@test "fails when the live checkout is not on main" {
git -C "$T/live" switch -q -c feature
run "$SCRIPT"
[ "$status" -ne 0 ]
}

56
scripts/deploy.sh Executable file
View File

@ -0,0 +1,56 @@
#!/bin/bash
# ============================================================================
# Deploy testsAndMisc: fast-forward the live checkout (~/src/testsAndMisc) to
# origin/main. Everything this repo runs on the machine (i3 config, systemd
# units, symlinked scripts, git hooks) executes straight from that checkout,
# so updating it is the deploy.
#
# scripts/deploy.sh
#
# Idempotent. Uncommitted work in the live checkout is kept: git refuses the
# fast-forward (and this exits non-zero) only when an incoming commit touches
# a file with local edits. Copies installed under /usr/local or /etc by the
# sudo installers are not refreshed here; rerun that installer by hand.
#
# Exits non-zero unless the health check passes: the checkout is on main, its
# HEAD equals origin/main, and core.hooksPath points at the tracked hooks.
# Env: TESTSANDMISC_DIR (default ~/src/testsAndMisc).
# ============================================================================
set -euo pipefail
readonly REPO="${TESTSANDMISC_DIR:-${HOME}/src/testsAndMisc}"
readonly HOOKS_PATH=linux_configuration/.githooks
die() {
echo "deploy: $1" >&2
exit 1
}
update_checkout() {
[[ -d "${REPO}/.git" ]] || die "${REPO} is not a git checkout"
local branch
branch="$(git -C "$REPO" rev-parse --abbrev-ref HEAD)"
[[ "$branch" == main ]] || die "${REPO} is on '${branch}', not main"
git -C "$REPO" pull --ff-only --quiet origin main ||
die "git pull --ff-only failed in ${REPO} (diverged, or local edits collide)"
}
health_check() {
local head remote hooks
head="$(git -C "$REPO" rev-parse HEAD)"
remote="$(git -C "$REPO" rev-parse origin/main)"
[[ "$head" == "$remote" ]] || die "HEAD ${head:0:8} != origin/main ${remote:0:8}"
hooks="$(git -C "$REPO" config --get core.hooksPath || true)"
[[ "$hooks" == "$HOOKS_PATH" && -d "${REPO}/${HOOKS_PATH}" ]] ||
die "core.hooksPath is '${hooks}', want ${HOOKS_PATH} (run meta/scripts/install_hooks.sh)"
echo "deploy: ${REPO} at ${head:0:8} = origin/main, hooks wired"
}
main() {
update_checkout
health_check
}
main "$@"