Build complete AI creator partnership prototype
This commit is contained in:
Executable
+189
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
if [[ "${SITES_ENV_READY:-}" != "1" ]]; then
|
||||
exec "${script_dir}/sites-env.sh" -- "$0" "$@"
|
||||
fi
|
||||
|
||||
command -v flock >/dev/null || {
|
||||
echo "install-ci.sh requires Linux flock." >&2
|
||||
exit 69
|
||||
}
|
||||
command -v timeout >/dev/null || {
|
||||
echo "install-ci.sh requires GNU timeout." >&2
|
||||
exit 69
|
||||
}
|
||||
command -v curl >/dev/null || {
|
||||
echo "install-ci.sh requires curl for the locked-tarball preflight." >&2
|
||||
exit 69
|
||||
}
|
||||
command -v sha256sum >/dev/null || {
|
||||
echo "install-ci.sh requires sha256sum for cache and install verification." >&2
|
||||
exit 69
|
||||
}
|
||||
|
||||
runtime_root="${SITES_PROJECT_ROOT}/.sites-runtime"
|
||||
expected_home="${runtime_root}/home"
|
||||
expected_cache="${runtime_root}/npm-cache"
|
||||
|
||||
echo "[sites] validating writable install environment"
|
||||
if [[ "${HOME}" != "${expected_home}" ]]; then
|
||||
echo "Expected HOME=${expected_home}, got HOME=${HOME}." >&2
|
||||
exit 78
|
||||
fi
|
||||
actual_cache="$(npm config get cache)"
|
||||
if [[ "${actual_cache}" != "${expected_cache}" ]]; then
|
||||
echo "Expected npm cache ${expected_cache}, got ${actual_cache}." >&2
|
||||
exit 78
|
||||
fi
|
||||
touch "${HOME}/.sites-write-test" "${expected_cache}/.sites-write-test"
|
||||
rm -f "${HOME}/.sites-write-test" "${expected_cache}/.sites-write-test"
|
||||
echo "[sites] environment passed: HOME=${HOME}, cache=${expected_cache}"
|
||||
|
||||
lock_file="${runtime_root}/install.lock"
|
||||
exec 9>"${lock_file}"
|
||||
if ! flock -n 9; then
|
||||
echo "Another dependency install is already running for ${SITES_PROJECT_ROOT}." >&2
|
||||
exit 75
|
||||
fi
|
||||
|
||||
# Catch an installer started outside this helper. Linux exposes both its command
|
||||
# line and working directory through /proc, so avoid broad process-name matches.
|
||||
for process in /proc/[0-9]*; do
|
||||
pid="${process##*/}"
|
||||
[[ "${pid}" != "$$" && "${pid}" != "${PPID}" ]] || continue
|
||||
process_cwd="$(readlink -f "${process}/cwd" 2>/dev/null || true)"
|
||||
[[ "${process_cwd}" == "${SITES_PROJECT_ROOT}" ]] || continue
|
||||
process_command="$(tr '\0' ' ' <"${process}/cmdline" 2>/dev/null || true)"
|
||||
if [[ "${process_command}" == *"npm ci"* ]]; then
|
||||
echo "Another npm ci is visible in ${SITES_PROJECT_ROOT}; refusing to overlap installs." >&2
|
||||
exit 75
|
||||
fi
|
||||
done
|
||||
|
||||
lockfile_sha256="$(sha256sum "${SITES_PROJECT_ROOT}/package-lock.json" | awk '{print $1}')"
|
||||
use_seeded_cache=0
|
||||
seed_cache="${SITES_NPM_CACHE_SEED:-}"
|
||||
if [[ -n "${seed_cache}" && -d "${seed_cache}" ]]; then
|
||||
seed_lockfile_sha256="$(cat "${seed_cache}/.sites-lockfile-sha256" 2>/dev/null || true)"
|
||||
if [[ "${seed_lockfile_sha256}" == "${lockfile_sha256}" ]]; then
|
||||
echo "[sites] restoring image-seeded npm cache"
|
||||
cp -a "${seed_cache}/." "${expected_cache}/"
|
||||
use_seeded_cache=1
|
||||
echo "[sites] image cache seed matched; registry fallback remains available"
|
||||
else
|
||||
echo "[sites] image cache seed does not match this lockfile; using the network path"
|
||||
fi
|
||||
fi
|
||||
|
||||
locked_vinext_output="$({ node --input-type=module - "${SITES_PROJECT_ROOT}/package-lock.json" <<'NODE'
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
const lock = JSON.parse(await readFile(process.argv[2], "utf8"));
|
||||
const vinext = lock.packages?.["node_modules/vinext"];
|
||||
if (!vinext?.resolved || !vinext?.integrity) {
|
||||
throw new Error("package-lock.json does not contain a resolved, integrity-pinned vinext tarball");
|
||||
}
|
||||
console.log(vinext.resolved);
|
||||
console.log(vinext.integrity);
|
||||
NODE
|
||||
} 2>/dev/null)" || {
|
||||
echo "Could not read the integrity-pinned vinext tarball from package-lock.json." >&2
|
||||
exit 65
|
||||
}
|
||||
mapfile -t locked_vinext <<<"${locked_vinext_output}"
|
||||
if [[ "${#locked_vinext[@]}" -ne 2 ]]; then
|
||||
echo "Expected exactly one Vinext URL and integrity value from package-lock.json." >&2
|
||||
exit 65
|
||||
fi
|
||||
|
||||
locked_tarball="${locked_vinext[0]}"
|
||||
locked_integrity="${locked_vinext[1]}"
|
||||
|
||||
if [[ "${use_seeded_cache}" == "0" ]]; then
|
||||
registry="$(npm config get registry)"
|
||||
preflight_url="$({ node --input-type=module - "${locked_tarball}" "${registry}" <<'NODE'
|
||||
const locked = new URL(process.argv[2]);
|
||||
const registry = new URL(process.argv[3]);
|
||||
if (locked.hostname === "registry.npmjs.org") {
|
||||
locked.protocol = registry.protocol;
|
||||
locked.host = registry.host;
|
||||
locked.pathname = `${registry.pathname.replace(/\/$/, "")}${locked.pathname}`;
|
||||
}
|
||||
process.stdout.write(locked.href);
|
||||
NODE
|
||||
} 2>/dev/null)" || {
|
||||
echo "Could not construct the locked-tarball preflight URL." >&2
|
||||
exit 65
|
||||
}
|
||||
|
||||
preflight_dir="${runtime_root}/preflight"
|
||||
preflight_tarball="${preflight_dir}/vinext.tgz"
|
||||
mkdir -p "${preflight_dir}"
|
||||
|
||||
echo "[sites] downloading the complete locked vinext tarball"
|
||||
curl \
|
||||
--fail \
|
||||
--location \
|
||||
--silent \
|
||||
--show-error \
|
||||
--retry 0 \
|
||||
--connect-timeout 15 \
|
||||
--max-time 120 \
|
||||
--output "${preflight_tarball}" \
|
||||
"${preflight_url}"
|
||||
|
||||
echo "[sites] verifying locked vinext tarball integrity"
|
||||
node --input-type=module - "${preflight_tarball}" "${locked_integrity}" <<'NODE'
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
const [algorithm, expected] = process.argv[3].split("-", 2);
|
||||
if (!algorithm || !expected) {
|
||||
throw new Error(`unsupported integrity value: ${process.argv[3]}`);
|
||||
}
|
||||
const actual = createHash(algorithm)
|
||||
.update(await readFile(process.argv[2]))
|
||||
.digest("base64");
|
||||
if (actual !== expected) {
|
||||
throw new Error(`vinext tarball integrity mismatch for ${algorithm}`);
|
||||
}
|
||||
NODE
|
||||
echo "[sites] network and integrity preflight passed"
|
||||
fi
|
||||
|
||||
echo "[sites] running exactly one bounded npm ci"
|
||||
export NPM_CONFIG_MAXSOCKETS=1
|
||||
export NPM_CONFIG_FETCH_RETRIES=0
|
||||
export NPM_CONFIG_FETCH_TIMEOUT=30000
|
||||
npm_ci_args=(ci --cache "${expected_cache}")
|
||||
if [[ "${use_seeded_cache}" == "1" ]]; then
|
||||
npm_ci_args+=(--prefer-offline)
|
||||
fi
|
||||
timeout \
|
||||
--signal=TERM \
|
||||
--kill-after="${SITES_INSTALL_KILL_AFTER:-15s}" \
|
||||
"${SITES_INSTALL_TIMEOUT:-8m}" \
|
||||
npm "${npm_ci_args[@]}"
|
||||
|
||||
vinext="${SITES_PROJECT_ROOT}/node_modules/.bin/vinext"
|
||||
if [[ ! -x "${vinext}" ]]; then
|
||||
echo "npm ci exited successfully but node_modules/.bin/vinext is unavailable." >&2
|
||||
exit 69
|
||||
fi
|
||||
|
||||
node --input-type=module - "${SITES_PROJECT_ROOT}/node_modules/.sites-install.json" "${lockfile_sha256}" <<'NODE'
|
||||
import { writeFile } from "node:fs/promises";
|
||||
|
||||
await writeFile(
|
||||
process.argv[2],
|
||||
`${JSON.stringify({
|
||||
lockfile_sha256: process.argv[3],
|
||||
node: process.version,
|
||||
platform: `${process.platform}-${process.arch}`,
|
||||
}, null, 2)}\n`,
|
||||
);
|
||||
NODE
|
||||
echo "[sites] npm ci passed and vinext is available"
|
||||
Reference in New Issue
Block a user