Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/workflows/script-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,13 @@ jobs:
- name: Install test dependencies
run: uv pip install --system jsonschema==4.23.0

- run: make script-test
- name: Verify bundled scripts are up to date
run: make check-bundle

- name: Script tests (source)
run: make script-test

- name: Script tests (bundled)
run: make script-test
env:
SCRIPT_TEST_TARGET: bundled
27 changes: 23 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
.DEFAULT_GOAL := help
.PHONY: help script-test test
.PHONY: help script-build check-bundle script-test test

BUNDLE_SRCS := scripts/post-code.src.sh scripts/post-fix.src.sh scripts/post-prioritize.src.sh
BUNDLE_OUTS := $(BUNDLE_SRCS:.src.sh=.sh)

help:
@echo "Available targets:"
@echo " help - Show this help message"
@echo " script-test - Run agent shell script unit tests"
@echo " test - Alias for script-test"
@echo " help - Show this help message"
@echo " script-build - Bundle .src.sh scripts into committed .sh artifacts"
@echo " check-bundle - Verify committed bundles match script-build output"
@echo " script-test - Run agent shell script unit tests"
@echo " test - Alias for script-test"

define run-timed
@start=$$(date +%s); \
Expand All @@ -15,7 +20,21 @@ define run-timed
exit $$rc
endef

script-build: $(BUNDLE_OUTS)

scripts/%.sh: scripts/%.src.sh scripts/bundle-sh.sh
scripts/bundle-sh.sh -o $@ $<

check-bundle: script-build
@git diff --exit-code -- $(BUNDLE_OUTS) || \
(echo 'Bundled scripts are stale; run make script-build' >&2; exit 1)

SCRIPT_TEST_TARGET ?= source
export SCRIPT_TEST_TARGET

script-test:
$(call run-timed,bash scripts/bundle-sh-test.sh)
$(call run-timed,bash scripts/post-failure-report-test.sh)
$(call run-timed,bash scripts/post-triage-test.sh)
$(call run-timed,bash scripts/post-prioritize-test.sh)
$(call run-timed,bash scripts/post-code-test.sh)
Expand Down
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,32 @@ Run all agent shell script test suites from the repo root:
make test
```

This is an alias for `make script-test`, which runs the seven `scripts/*-test.sh` suites. CI runs the same target via `.github/workflows/script-test.yml`.
This is an alias for `make script-test`, which runs the `scripts/*-test.sh` suites. CI also runs `make check-bundle` and executes `make script-test` twice (source and bundled modes) via `.github/workflows/script-test.yml`.

## Script bundling

Harness fetches each runner script as an isolated blob, so post-scripts cannot `source` files from `scripts/lib/` at runtime. Scripts that use shared libraries are maintained as source files and bundled before commit:

| Kind | Path | Edit? |
|------|------|-------|
| Library | `scripts/lib/*.lib.sh` | Yes — functions only, no side effects |
| Source | `scripts/*.src.sh` | Yes — editable script with `source` calls |
| Bundled | `scripts/*.sh` (from `.src.sh`) | No — generated; referenced by harness |

After editing a `.src.sh` or `.lib.sh` file:

```bash
make script-build # regenerate bundled .sh files
make check-bundle # verify committed bundles are current
```

Commit source and bundled files together. Test bundled scripts locally with:

```bash
make script-test SCRIPT_TEST_TARGET=bundled
```

Library tests source `.lib.sh` directly. Script tests honor `SCRIPT_TEST_TARGET` (`source` by default, `bundled` in CI's second pass).

## Versioning

Expand Down
101 changes: 101 additions & 0 deletions scripts/bundle-sh-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/usr/bin/env bash
# bundle-sh-test.sh — Tests for scripts/bundle-sh.sh

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BUNDLER="${SCRIPT_DIR}/bundle-sh.sh"
FIXTURES="${SCRIPT_DIR}/test-fixtures/bundle"
TMPDIR="$(mktemp -d)"
FAILURES=0

trap 'rm -rf "${TMPDIR}"' EXIT

assert_eq() {
local test_name="$1"
local expected="$2"
local actual="$3"
if [[ "${actual}" != "${expected}" ]]; then
echo "FAIL: ${test_name}"
echo " expected: ${expected}"
echo " actual: ${actual}"
FAILURES=$((FAILURES + 1))
else
echo "PASS: ${test_name}"
fi
}

assert_contains() {
local test_name="$1"
local needle="$2"
local haystack="$3"
if ! printf '%s' "${haystack}" | grep -qF "${needle}"; then
echo "FAIL: ${test_name}"
echo " expected to find: ${needle}"
FAILURES=$((FAILURES + 1))
else
echo "PASS: ${test_name}"
fi
}

assert_not_contains() {
local test_name="$1"
local needle="$2"
local haystack="$3"
if printf '%s' "${haystack}" | grep -qF "${needle}"; then
echo "FAIL: ${test_name}"
echo " expected NOT to find: ${needle}"
FAILURES=$((FAILURES + 1))
else
echo "PASS: ${test_name}"
fi
}

run_bundle_test() {
local test_name="$1"
local src="$2"
local out="${TMPDIR}/${test_name}.sh"

"${BUNDLER}" -o "${out}" "${src}"
bash "${out}"
echo "PASS: ${test_name}-executes"
}

# --- shebang and header ordering ---
OUT="${TMPDIR}/header.sh"
"${BUNDLER}" -o "${OUT}" "${FIXTURES}/simple.src.sh"
HEADER_LINE1="$(sed -n '1p' "${OUT}")"
HEADER_LINE2="$(sed -n '2p' "${OUT}")"
assert_eq "shebang-line-1" "#!/usr/bin/env bash" "${HEADER_LINE1}"
assert_contains "generated-banner-line-2" "GENERATED from simple.src.sh" "${HEADER_LINE2}"

# --- simple bundle executes ---
run_bundle_test "simple" "${FIXTURES}/simple.src.sh"

# --- nested bundle executes ---
run_bundle_test "nested" "${FIXTURES}/nested.src.sh"

# --- dedup: second source becomes comment ---
OUT="${TMPDIR}/dedup.sh"
"${BUNDLER}" -o "${OUT}" "${FIXTURES}/dedup.src.sh"
CONTENT="$(cat "${OUT}")"
assert_contains "dedup-includes-leaf-once" "leaf_fn()" "${CONTENT}"
assert_contains "dedup-already-bundled-comment" "# (already bundled: lib/leaf.lib.sh)" "${CONTENT}"
assert_eq "dedup-leaf-fn-count" "1" "$(printf '%s' "${CONTENT}" | grep -c '^leaf_fn()')"
bash "${OUT}"
echo "PASS: dedup-executes"

# --- missing library fails ---
if "${BUNDLER}" -o "${TMPDIR}/bad.sh" "${SCRIPT_DIR}/does-not-exist.src.sh" 2>/dev/null; then
echo "FAIL: missing-src-should-fail"
FAILURES=$((FAILURES + 1))
else
echo "PASS: missing-src-should-fail"
fi

echo ""
if [[ ${FAILURES} -gt 0 ]]; then
echo "${FAILURES} test(s) failed"
exit 1
fi
echo "All bundle-sh tests passed"
188 changes: 188 additions & 0 deletions scripts/bundle-sh.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
#!/usr/bin/env bash
# bundle-sh.sh — Inline scripts/lib/*.lib.sh sources into a .src.sh script.
#
# Usage:
# bundle-sh.sh [-o OUTPUT] SOURCE.src.sh
#
# Writes a self-contained .sh script with libraries inlined at each source line.
# Shebang remains line 1; a GENERATED banner is inserted on line 2.

set -euo pipefail

usage() {
cat <<'EOF' >&2
Usage: bundle-sh.sh [-o OUTPUT] SOURCE.src.sh

Bundle scripts/lib/*.lib.sh inclusions into a single executable script.
EOF
exit 1
}

output_path=""
src_path=""

while [[ $# -gt 0 ]]; do
case "$1" in
-o)
[[ $# -ge 2 ]] || usage
output_path="$2"
shift 2
;;
-h|--help)
usage
;;
*)
[[ -z "${src_path}" ]] || usage
src_path="$1"
shift
;;
esac
done

[[ -n "${src_path}" ]] || usage
[[ -f "${src_path}" ]] || { echo "bundle-sh: not found: ${src_path}" >&2; exit 1; }
[[ "${src_path}" == *.src.sh ]] || {
echo "bundle-sh: source must be a .src.sh file: ${src_path}" >&2
exit 1
}

src_abs="$(cd "$(dirname "${src_path}")" && pwd)/$(basename "${src_path}")"
src_dir="$(dirname "${src_abs}")"
src_base="$(basename "${src_abs}")"

declare -A BUNDLE_INCLUDED=()

bundle_resolve_path() {
local ref="$1"
local base_dir="$2"
local candidate=""

if [[ "${ref}" == /* ]]; then
candidate="${ref}"
elif [[ "${ref}" == ./* ]]; then
candidate="${base_dir}/${ref#./}"
elif [[ "${ref}" == ../* ]]; then
candidate="$(cd "${base_dir}" && cd "$(dirname "${ref}")" && pwd)/$(basename "${ref}")"
else
candidate="${base_dir}/${ref}"
fi

candidate="$(cd "$(dirname "${candidate}")" && pwd)/$(basename "${candidate}")"
printf '%s' "${candidate}"
}

bundle_is_lib() {
local path="$1"
[[ "${path}" == */scripts/lib/*.lib.sh ]] || [[ "${path}" == */lib/*.lib.sh && "${path}" == *".lib.sh" ]]
}

bundle_lib_body() {
local lib_path="$1"
local line
local first=true
local -a lines

mapfile -t lines < "${lib_path}"
for line in "${lines[@]}"; do
if [[ "${first}" == true && "${line}" =~ ^#! ]]; then
first=false
continue
fi
first=false

if [[ "${line}" =~ ^[[:space:]]*source[[:space:]]+(.+)[[:space:]]*$ ]]; then
bundle_expand_source "${BASH_REMATCH[1]}" "$(dirname "${lib_path}")"
continue
fi

printf '%s\n' "${line}"
done
}

bundle_expand_source() {
local raw_expr="$1"
local base_dir="$2"
local ref=""
local resolved=""
local rel_comment=""

ref="$(printf '%s' "${raw_expr}" | sed -E \
-e 's/^[[:space:]]*"//; s/"[[:space:]]*$//' \
-e 's/^[[:space:]]*'\''//; s/'\''[[:space:]]*$//' \
-e 's/^\$\{SCRIPT_DIR[^}]*\}\///' \
-e 's/^\$\{SCRIPT_DIR_POST[^}]*\}\///' \
-e 's/^\$\{SCRIPT_DIR[^}]*\}//')"

if [[ -z "${ref}" ]]; then
echo "bundle-sh: unsupported source expression: ${raw_expr}" >&2
exit 1
fi

if [[ "${ref}" == lib/* ]]; then
resolved="$(bundle_resolve_path "${ref}" "${src_dir}")"
else
resolved="$(bundle_resolve_path "${ref}" "${base_dir}")"
fi

if ! bundle_is_lib "${resolved}"; then
echo "bundle-sh: source outside scripts/lib/*.lib.sh: ${resolved}" >&2
exit 1
fi

if [[ ! -f "${resolved}" ]]; then
echo "bundle-sh: missing library: ${resolved}" >&2
exit 1
fi

rel_comment="${resolved#"${src_dir}/"}"

if [[ -n "${BUNDLE_INCLUDED[${resolved}]+x}" ]]; then
printf '# (already bundled: %s)\n' "${rel_comment}"
return 0
fi

BUNDLE_INCLUDED["${resolved}"]=1
printf '# BEGIN bundled: %s\n' "${rel_comment}"
bundle_lib_body "${resolved}"
printf '# END bundled: %s\n' "${rel_comment}"
}

bundle_src_file() {
local line=""
local first=true
local saw_shebang=false
local -a lines

mapfile -t lines < "${src_abs}"
for line in "${lines[@]}"; do
if [[ "${first}" == true && "${line}" =~ ^#! ]]; then
printf '%s\n' "${line}"
printf '# GENERATED from %s — DO NOT EDIT. Run: make script-build\n' "${src_base}"
saw_shebang=true
first=false
continue
fi
first=false

if [[ "${line}" =~ ^[[:space:]]*source[[:space:]]+(.+)[[:space:]]*$ ]]; then
bundle_expand_source "${BASH_REMATCH[1]}" "${src_dir}"
continue
fi

if [[ "${saw_shebang}" == false && "${line}" =~ ^#! ]]; then
printf '%s\n' "${line}"
printf '# GENERATED from %s — DO NOT EDIT. Run: make script-build\n' "${src_base}"
saw_shebang=true
continue
fi

printf '%s\n' "${line}"
done
}

if [[ -n "${output_path}" ]]; then
bundle_src_file > "${output_path}"
chmod +x "${output_path}"
else
bundle_src_file
fi
Loading
Loading