Skip to content
Draft
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
32 changes: 12 additions & 20 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,12 @@ sync-schema:
# Install dependencies and build workspace packages.
[unix]
_setup-dev-deps:
pnpm install
cd sdk && pnpm build
./scripts/ensure-dev-deps.sh --force

# Repair only stale development dependencies and workspace package outputs.
[unix]
_ensure-dev-deps:
./scripts/ensure-dev-deps.sh

[unix]
_install-lefthook:
Expand Down Expand Up @@ -469,10 +473,11 @@ dev:
#!/usr/bin/env bash
set -euo pipefail

if [[ -n "${GOOSE_BIN:-}" ]]; then
just _setup-no-goose
else
GOOSE_BUILD_PROFILE=debug just setup
just _ensure-dev-deps

if [[ -z "${GOOSE_BIN:-}" ]]; then
LOCAL_GOOSE_BIN="$(GOOSE_DEV_MODE=required GOOSE_BUILD_PROFILE=debug ./scripts/ensure-local-goose.sh --print-bin)"
export GOOSE_BIN="$LOCAL_GOOSE_BIN"
fi

VITE_PORT="$(python3 -c "import hashlib,os; h=int(hashlib.sha256(os.getcwd().encode()).hexdigest(),16); print(10000 + h % 55000)")"
Expand Down Expand Up @@ -507,20 +512,7 @@ dev:
./scripts/prepare-bb-cli-resource.sh
fi

if [[ -n "${GOOSE_BIN:-}" ]]; then
echo "Using explicitly set GOOSE_BIN: ${GOOSE_BIN}"
else
LOCAL_GOOSE_BIN="$(GOOSE_BUILD_PROFILE=debug ./scripts/ensure-local-goose.sh --check-bin)" || {
rc=$?
if [[ $rc -eq 2 ]]; then
echo "❌ Local goose binary is not ready. Run 'just setup' first." >&2
exit 1
fi
exit $rc
}
export GOOSE_BIN="$LOCAL_GOOSE_BIN"
echo "Using local goose binary: ${GOOSE_BIN}"
fi
echo "Using Goose binary: ${GOOSE_BIN}"

DISTRO_DIR="$(pwd)/distro"
if [[ -z "${GOOSE_DISTRO_DIR:-}" && -d "$DISTRO_DIR" ]]; then
Expand Down
103 changes: 103 additions & 0 deletions scripts/ensure-dev-deps.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
set -euo pipefail

script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "$script_dir/.." && pwd)"
force=0

if [[ "${1:-}" == "--force" ]]; then
force=1
shift
fi

if [[ $# -ne 0 ]]; then
echo "Usage: scripts/ensure-dev-deps.sh [--force]" >&2
exit 2
fi

pnpm_bin="${PNPM_BIN:-pnpm}"
dependency_stamp="$repo_root/node_modules/.berd-dev-dependency-inputs.sha256"
installed_lock="$repo_root/node_modules/.pnpm/lock.yaml"
sdk_stamp="$repo_root/sdk/dist/.berd-dev-build-inputs.sha256"

hash_files() {
local path
for path in "$@"; do
if [[ -d "$path" ]]; then
find "$path" -type f -print
elif [[ -f "$path" ]]; then
printf '%s\n' "$path"
fi
done \
| LC_ALL=C sort -u \
| while IFS= read -r path; do
shasum -a 256 "$path"
done \
| shasum -a 256 \
| awk '{print $1}'
}

stamp_matches() {
local stamp="$1"
local expected="$2"
[[ -f "$stamp" ]] && [[ "$(<"$stamp")" == "$expected" ]]
}

write_stamp() {
local stamp="$1"
local value="$2"
local temp_stamp
mkdir -p "$(dirname "$stamp")"
temp_stamp="$(mktemp "${stamp}.XXXXXX")"
printf '%s\n' "$value" >"$temp_stamp"
mv "$temp_stamp" "$stamp"
}

dependency_inputs_hash() {
hash_files \
"$repo_root/package.json" \
"$repo_root/pnpm-lock.yaml" \
"$repo_root/pnpm-workspace.yaml" \
"$repo_root/sdk/package.json"
}

sdk_inputs_hash() {
hash_files \
"$repo_root/pnpm-lock.yaml" \
"$repo_root/pnpm-workspace.yaml" \
"$repo_root/sdk/package.json" \
"$repo_root/sdk/tsconfig.json" \
"$repo_root/sdk/generate-schema.ts" \
"$repo_root/sdk/schema" \
"$repo_root/sdk/src"
}

dependency_inputs="$(dependency_inputs_hash)"

if [[ "$force" == "1" ]] \
|| ! stamp_matches "$dependency_stamp" "$dependency_inputs" \
|| [[ ! -f "$installed_lock" ]] \
|| ! cmp -s "$repo_root/pnpm-lock.yaml" "$installed_lock"; then
echo "Preparing pnpm dependencies."
(cd "$repo_root" && "$pnpm_bin" install)
dependency_inputs="$(dependency_inputs_hash)"
write_stamp "$dependency_stamp" "$dependency_inputs"
else
echo "pnpm dependencies are current; skipping install."
fi

sdk_inputs="$(sdk_inputs_hash)"

if [[ "$force" == "1" ]] \
|| ! stamp_matches "$sdk_stamp" "$sdk_inputs" \
|| [[ ! -f "$repo_root/sdk/dist/index.js" ]] \
|| [[ ! -f "$repo_root/sdk/dist/index.d.ts" ]]; then
echo "Building @aaif/goose-sdk."
(cd "$repo_root/sdk" && "$pnpm_bin" build)
# Schema generation is part of the SDK build and may update derived source
# files, so record the inputs after the successful build.
sdk_inputs="$(sdk_inputs_hash)"
write_stamp "$sdk_stamp" "$sdk_inputs"
else
echo "@aaif/goose-sdk is current; skipping build."
fi
2 changes: 1 addition & 1 deletion scripts/release/tests/release-scripts.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ describe("managed Goose build profile", () => {
/_bundle-debug-unix:[\s\S]*if \[\[ -z "\$\{GOOSE_BIN:-\}" \]\]; then[\s\S]*GOOSE_BUILD_PROFILE=debug \.\/scripts\/ensure-local-goose\.sh/,
);
expect(justfile).toMatch(
/dev:[\s\S]*GOOSE_BUILD_PROFILE=debug just setup[\s\S]*GOOSE_BUILD_PROFILE=debug \.\/scripts\/ensure-local-goose\.sh --check-bin/,
/dev:[\s\S]*just _ensure-dev-deps[\s\S]*GOOSE_DEV_MODE=required GOOSE_BUILD_PROFILE=debug \.\/scripts\/ensure-local-goose\.sh --print-bin/,
);
expect(devE2e).toContain("GOOSE_BUILD_PROFILE=debug just setup");
expect(schema).toContain("GOOSE_BUILD_PROFILE=debug");
Expand Down
136 changes: 133 additions & 3 deletions scripts/release/tests/setup-tooling.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,65 @@ async function callsFor(path) {
return readFile(path, "utf8").catch(() => "");
}

async function devDepsFixture() {
const root = await mkdtemp(join(tmpdir(), "berd-dev-deps-"));
tempDirs.push(root);
const scriptDir = join(root, "scripts");
const sdkDir = join(root, "sdk");
const fakePnpm = join(root, "fake-pnpm");
const calls = join(root, "pnpm-calls");

await Promise.all([
mkdir(scriptDir),
mkdir(join(sdkDir, "schema"), { recursive: true }),
mkdir(join(sdkDir, "src"), { recursive: true }),
]);
await Promise.all([
copyFile(
join(repo, "scripts/ensure-dev-deps.sh"),
join(scriptDir, "ensure-dev-deps.sh"),
),
writeFile(join(root, "package.json"), '{"name":"fixture"}\n'),
writeFile(join(root, "pnpm-lock.yaml"), "lockfileVersion: '9.0'\n"),
writeFile(join(root, "pnpm-workspace.yaml"), "packages: ['.', 'sdk']\n"),
writeFile(join(sdkDir, "package.json"), '{"name":"sdk"}\n'),
writeFile(join(sdkDir, "tsconfig.json"), "{}\n"),
writeFile(join(sdkDir, "generate-schema.ts"), "export {};\n"),
writeFile(join(sdkDir, "schema/schema.json"), "{}\n"),
writeFile(join(sdkDir, "src/index.ts"), "export {};\n"),
writeFile(
fakePnpm,
`#!/bin/bash
set -euo pipefail
printf '%s:%s\\n' "$PWD" "$*" >> "${calls}"
if [[ "\${1:-}" == "install" ]]; then
if [[ "\${PNPM_REWRITE_LOCK:-0}" == "1" ]] && ! grep -q autoInstallPeers pnpm-lock.yaml; then
printf 'settings:\n autoInstallPeers: true\n' >> pnpm-lock.yaml
fi
mkdir -p node_modules/.pnpm
cp pnpm-lock.yaml node_modules/.pnpm/lock.yaml
elif [[ "\${1:-}" == "build" ]]; then
mkdir -p dist
touch dist/index.js dist/index.d.ts
fi
`,
),
]);
await Promise.all([
chmod(join(scriptDir, "ensure-dev-deps.sh"), 0o755),
chmod(fakePnpm, 0o755),
]);

const run = (args = [], env = {}) =>
spawnSync(join(scriptDir, "ensure-dev-deps.sh"), args, {
cwd: root,
encoding: "utf8",
env: { ...process.env, PNPM_BIN: fakePnpm, ...env },
});

return { root, calls, run };
}

afterEach(async () => {
await Promise.all(
tempDirs
Expand All @@ -85,21 +144,92 @@ afterEach(async () => {
});

describe("setup tooling regressions", () => {
it("uses the SDK's direct build commands through the pnpm setup recipe", async () => {
const [sdkPackage, justfile] = await Promise.all([
it("routes full setup and incremental dev preparation through the dependency guard", async () => {
const [sdkPackage, justfile, ensureDevDeps] = await Promise.all([
readFile(join(repo, "sdk/package.json"), "utf8"),
readFile(justfilePath, "utf8"),
readFile(join(repo, "scripts/ensure-dev-deps.sh"), "utf8"),
]);

expect(JSON.parse(sdkPackage).scripts.build).toBe(
"tsx generate-schema.ts && tsc",
);
expect(justfile).toMatch(
/_setup-dev-deps:\n {4}pnpm install\n {4}cd sdk && pnpm build\n/,
/_setup-dev-deps:\n {4}\.\/scripts\/ensure-dev-deps\.sh --force\n/,
);
expect(justfile).toMatch(
/_ensure-dev-deps:\n {4}\.\/scripts\/ensure-dev-deps\.sh\n/,
);
expect(justfile).toMatch(
/_install-lefthook:\n {4}\.\/scripts\/install-lefthook\.sh\n/,
);
expect(ensureDevDeps).toContain('"$pnpm_bin" install');
expect(ensureDevDeps).toContain('"$pnpm_bin" build');
});

it("records dependency inputs after pnpm updates the lockfile", async () => {
const fixture = await devDepsFixture();

const initial = fixture.run([], { PNPM_REWRITE_LOCK: "1" });
expect(initial.status, `${initial.stdout}\n${initial.stderr}`).toBe(0);

await writeFile(fixture.calls, "");
const warm = fixture.run();
expect(warm.status, `${warm.stdout}\n${warm.stderr}`).toBe(0);
expect(warm.stdout).toContain("skipping install");
expect(warm.stdout).toContain("skipping build");
expect(await callsFor(fixture.calls)).toBe("");
});

it("skips current dependencies and rebuilds only stale SDK inputs", async () => {
const fixture = await devDepsFixture();

const first = fixture.run();
expect(first.status, `${first.stdout}\n${first.stderr}`).toBe(0);
expect(await callsFor(fixture.calls)).toBe(
`${fixture.root}:install\n${join(fixture.root, "sdk")}:build\n`,
);

await writeFile(fixture.calls, "");
const warm = fixture.run();
expect(warm.status, `${warm.stdout}\n${warm.stderr}`).toBe(0);
expect(warm.stdout).toContain("skipping install");
expect(warm.stdout).toContain("skipping build");
expect(await callsFor(fixture.calls)).toBe("");

await writeFile(fixture.calls, "");
await writeFile(
join(fixture.root, "pnpm-lock.yaml"),
"lockfileVersion: '9.0'\nsettings:\n autoInstallPeers: true\n",
);
const dependencyChange = fixture.run();
expect(
dependencyChange.status,
`${dependencyChange.stdout}\n${dependencyChange.stderr}`,
).toBe(0);
expect(await callsFor(fixture.calls)).toBe(
`${fixture.root}:install\n${join(fixture.root, "sdk")}:build\n`,
);

await writeFile(fixture.calls, "");
await writeFile(
join(fixture.root, "sdk/src/index.ts"),
"export const changed = true;\n",
);
const sdkChange = fixture.run();
expect(sdkChange.status, `${sdkChange.stdout}\n${sdkChange.stderr}`).toBe(
0,
);
expect(await callsFor(fixture.calls)).toBe(
`${join(fixture.root, "sdk")}:build\n`,
);

await writeFile(fixture.calls, "");
const forced = fixture.run(["--force"]);
expect(forced.status, `${forced.stdout}\n${forced.stderr}`).toBe(0);
expect(await callsFor(fixture.calls)).toBe(
`${fixture.root}:install\n${join(fixture.root, "sdk")}:build\n`,
);
});

it("prefers the repository-local lefthook shim over PATH", async () => {
Expand Down
Loading