Skip to content
Merged
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
19 changes: 15 additions & 4 deletions .github/workflows/comprehensive-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,33 @@ jobs:
- name: Run all unit suites (core, web, server, mcp-server)
run: npm run test:coverage && npm run test:unified:lib

# Genuinely-real checks: the installer exists, deployment compose parses
# Genuinely-real checks: the installer installs a ref that exists and can
# fetch its code, and the deployment compose files parse
installer-and-deploy-config:
name: Installer & Deploy Config
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22.18.0'
- name: Installer ref and image-tag contract
run: node --test tests/toolchain/installer-contract.mjs
- name: Installer end to end in a container (clone, update, setup-script URLs)
run: sh tests/integration/installer-container.sh
- name: Installation script present and executable
run: |
test -f scripts/install.sh || { echo "::error::scripts/install.sh missing"; exit 1; }
test -x scripts/install.sh || echo "::warning::scripts/install.sh is not marked executable"
echo "✅ installer present"
- name: Production docker compose config is valid
run: |
docker compose -f deployment/docker-compose.yml config > /dev/null \
&& echo "✅ deployment/docker-compose.yml is valid" \
|| { echo "::error::deployment/docker-compose.yml failed to parse"; exit 1; }
for file in deployment/docker-compose.yml deployment/docker-compose.registry.yml; do
docker compose -f "$file" config > /dev/null \
&& echo "✅ $file is valid" \
|| { echo "::error::$file failed to parse"; exit 1; }
done

# Honest PR comment derived from real job outcomes (no fabricated numbers)
report:
Expand Down
9 changes: 9 additions & 0 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,15 @@ GRAPHDONE_HOME=/opt/graphdone curl -fsSL https://graphdone.com/start.sh | sh
GRAPHDONE_NO_START=1 curl -fsSL https://graphdone.com/start.sh | sh
```

`scripts/install.sh` installs `main`, the branch it is served from. To install
a release tag or another branch, set `GRAPHDONE_REF` for the shell that runs it
(the clone, updates of `~/graphdone` and the setup-script downloads all follow
it). `sh install.sh sources` prints the repository, ref and URLs it will use:

```bash
curl -fsSL https://raw.githubusercontent.com/GraphDone/GraphDone-Core/main/scripts/install.sh | GRAPHDONE_REF=dev sh
```

## Updating GraphDone

Users can update their installation by running the same command:
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"dashboard:open": "node tests/lib/dashboard/server.mjs --open",
"dashboard:narrate": "node scripts/narrate-report.mjs",
"test:installation": "./scripts/test-installation-simple.sh",
"test:installer": "node --test tests/toolchain/installer-contract.mjs && sh tests/integration/installer-container.sh",
"test:https": "node tests/integration/ssl-certificate-analysis.js && node tests/integration/mobile-https-compatibility-test.js",
"test:report": "open test-artifacts/unified/report.html",
"lint": "turbo run lint",
Expand Down
100 changes: 89 additions & 11 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@
# Component 7: Success UI & Command Handler (Lines 3601-3700)
# ├─ Beautiful success message with URLs
# ├─ Management commands display
# └─ CLI argument processing (install/stop/remove)
# └─ CLI argument processing (install/stop/remove/sources)
#
# ⌨️ COMMAND REFERENCE
# ============================================================================
Expand All @@ -167,6 +167,12 @@
# Complete cleanup (removes containers, volumes, images):
# sh install.sh remove
#
# Show the repository, ref and setup-script URLs the installer uses:
# sh install.sh sources
#
# Install a branch or release tag other than main:
# GRAPHDONE_REF=dev sh install.sh
#
# 📊 EXIT CODES
# ============================================================================
# 0 - Success (GraphDone installed and running)
Expand Down Expand Up @@ -280,9 +286,44 @@ cleanup() {
trap 'cleanup; exit 130' INT TERM
trap 'cleanup' EXIT

# GitHub repository details
# GitHub repository details.
# GRAPHDONE_REF is the single source of truth for what gets installed: the
# fresh clone, the update of an installer-managed checkout and the fallback
# downloads of scripts/setup_*.sh all use it. It defaults to main because the
# one-line installer is served from main; set GRAPHDONE_REF to another branch
# or release tag to install that instead (tests point it at a branch).
GITHUB_REPO="GraphDone/GraphDone-Core"
GITHUB_BRANCH="fix/first-start"
GITHUB_REPO_URL="https://github.com/${GITHUB_REPO}.git"
GRAPHDONE_REF="${GRAPHDONE_REF:-main}"
# Prerequisite installers fetched from GitHub when not run from a checkout.
SETUP_SCRIPTS="setup_git.sh setup_nodejs.sh setup_docker.sh"

# Raw URL of a setup script at the configured ref
setup_script_url() {
printf 'https://raw.githubusercontent.com/%s/%s/scripts/%s\n' "$GITHUB_REPO" "$GRAPHDONE_REF" "$1"
}

# Print where this installer gets its code from (the `sources` command)
print_sources() {
printf 'repository %s\n' "$GITHUB_REPO_URL"
printf 'ref %s\n' "$GRAPHDONE_REF"
for script_name in $SETUP_SCRIPTS; do
printf 'setup-script %s\n' "$(setup_script_url "$script_name")"
done
}

# Bring an installer-managed checkout to the configured ref. Old installs may
# sit on a branch that no longer exists upstream, so fetch the ref explicitly
# instead of pulling the current branch's upstream.
update_checkout_to_ref() {
git fetch --quiet --tags origin "$GRAPHDONE_REF" || return 1
if git show-ref --verify --quiet "refs/tags/$GRAPHDONE_REF"; then
git checkout --quiet --detach FETCH_HEAD || return 1
else
git checkout --quiet "$GRAPHDONE_REF" || return 1
git merge --ff-only --quiet FETCH_HEAD || return 1
fi
}

# Helper function to run setup scripts (local or download from GitHub)
run_setup_script() {
Expand All @@ -296,7 +337,7 @@ run_setup_script() {
sh "scripts/$script_name" $script_args
else
# Download from GitHub and run
local github_url="https://raw.githubusercontent.com/${GITHUB_REPO}/${GITHUB_BRANCH}/scripts/${script_name}"
local github_url="$(setup_script_url "$script_name")"
local temp_script="/tmp/${script_name}.$$"

if command -v curl >/dev/null 2>&1; then
Expand Down Expand Up @@ -3272,8 +3313,17 @@ install_graphdone() {

cd "$INSTALL_DIR"

# Run git pull in background to show progress
git pull --quiet >/dev/null 2>&1 &
# Update in background to show progress. An installer-managed checkout
# follows GRAPHDONE_REF; a developer checkout the installer was run
# from keeps its current branch and pulls that branch's upstream.
update_log="$LOG_DIR/git-update-${INSTALL_TIMESTAMP}.log"
if [ "$FRESH_INSTALL" = true ]; then
update_target="$GRAPHDONE_REF"
update_checkout_to_ref >"$update_log" 2>&1 &
else
update_target="current branch"
git pull --quiet >"$update_log" 2>&1 &
fi
pull_pid=$!

# Add pink color for the circle
Expand Down Expand Up @@ -3338,7 +3388,18 @@ install_graphdone() {
# Smooth transition: show completion state briefly
printf " ${GREEN}●${NC}"
sleep 0.3
wait $pull_pid
pull_result=0
wait $pull_pid || pull_result=$?

if [ $pull_result -ne 0 ]; then
printf "\r\033[K"
printf " ${RED}✗${NC} ${BOLD}Failed to update GraphDone to ${update_target}${NC}\n"
if [ -f "$update_log" ]; then
printf "\n${BOLD}Last 20 lines from update log:${NC}\n"
tail -20 "$update_log"
fi
error "Git update failed in $INSTALL_DIR - commit or stash local changes, or remove the directory to reinstall"
fi

# Success line with exact 88-character content area
success_content="${GREEN}✓${NC} ${BOLD}Updated${NC} ${GREEN}to latest version${NC}"
Expand Down Expand Up @@ -3368,7 +3429,7 @@ install_graphdone() {

# Clone with progress - redirect to log file to capture any errors
local clone_log="$LOG_DIR/git-clone-${INSTALL_TIMESTAMP}.log"
git clone --progress --branch fix/first-start https://github.com/GraphDone/GraphDone-Core.git "$INSTALL_DIR" >"$clone_log" 2>&1 &
git clone --progress --branch "$GRAPHDONE_REF" "$GITHUB_REPO_URL" "$INSTALL_DIR" >"$clone_log" 2>&1 &
clone_pid=$!

# Single loop with timeout (no nested loops to avoid race conditions)
Expand Down Expand Up @@ -3400,8 +3461,8 @@ install_graphdone() {
elapsed=$((elapsed + 1))
done

wait $clone_pid
clone_result=$?
clone_result=0
wait $clone_pid || clone_result=$?

# Clear the line completely to prevent spinner artifacts
printf "\r\033[K"
Expand Down Expand Up @@ -3429,6 +3490,13 @@ install_graphdone() {

cd "$INSTALL_DIR"

# Test hook: stop once the code is in place. The installer's container
# test uses it; nothing is installed or started past this point.
if [ "${GRAPHDONE_INSTALL_STOP_AFTER:-}" = "clone" ]; then
printf " ${GREEN}✓${NC} GraphDone ${BOLD}%s${NC} is at %s; stopping (GRAPHDONE_INSTALL_STOP_AFTER=clone)\n" "$GRAPHDONE_REF" "$INSTALL_DIR"
exit 0
fi

# Project dependencies check and install (after code is downloaded)
# First show checking animation
PINK='\033[38;5;213m'
Expand Down Expand Up @@ -3999,7 +4067,17 @@ show_success() {
# Handle command line arguments
COMMAND="${1:-install}"

# GRAPHDONE_REF is passed to git; accept only plain branch or tag names.
case "$GRAPHDONE_REF" in
""|-*|*[!A-Za-z0-9._/-]*|*..*)
error "Invalid GRAPHDONE_REF '$GRAPHDONE_REF': use a branch or tag name such as main"
;;
esac

case "$COMMAND" in
sources)
print_sources
;;
stop)
stop_services
;;
Expand All @@ -4010,6 +4088,6 @@ case "$COMMAND" in
install_graphdone
;;
*)
error "Unknown command: $COMMAND. Use: install, stop, or remove"
error "Unknown command: $COMMAND. Use: install, stop, remove, or sources"
;;
esac
1 change: 1 addition & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ evidence format, and their limits.
| --- | --- |
| `npm test` | Package unit suites through Turbo; does not run browser acceptance. |
| `npm run test:unified:lib` | Report adapters, process outcomes, CLI safety, source provenance, and dashboard helpers. No browser or database required. |
| `npm run test:installer` | One-line installer: every ref it uses is `main` or the `GRAPHDONE_REF` override, image tags are `latest` or versions, and in a disposable container it clones, updates and reaches its setup-script URLs. Needs Docker. |
| `TEST_URL=http://localhost:3127 npm run test:smoke` | Focused user smoke against an already-running disposable stack. |
| `TEST_URL=http://localhost:3127 npm run test:unified:smoke` | Web unit tests, user smoke, and focused authentication acceptance, with one combined report. |
| `TEST_URL=http://localhost:3127 npm run test:pr` | Broader graph, mobile, and performance sequences. |
Expand Down
140 changes: 140 additions & 0 deletions tests/integration/installer-container.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#!/bin/sh
# End-to-end check of the one-line installer's code download (#243).
#
# Runs scripts/install.sh non-interactively in a disposable container and
# stops it right after the code step (GRAPHDONE_INSTALL_STOP_AFTER=clone), so
# nothing is installed or started. It proves that:
# 1. a fresh install clones GRAPHDONE_REF (default main) and lands on the
# commit the remote has for it;
# 2. re-running updates the installer-managed checkout in place;
# 3. an old install stranded on a branch deleted upstream (fix/first-start)
# is moved to GRAPHDONE_REF instead of failing on `git pull`;
# 4. every setup-script fallback URL the installer reports answers HTTP 200.
#
# Docker on the host is only used to run the container. Inside it, `docker`
# is a stub that reports a running daemon so the dependency check passes.
#
# Usage: sh tests/integration/installer-container.sh
# GRAPHDONE_REF=<branch|tag> ref to install (default main)
# INSTALLER_UNDER_TEST=<path> installer to run (default scripts/install.sh)
set -eu

IMAGE="node:22.18.0-trixie-slim"
MOUNT=/opt/graphdone-installer-test

if [ "${1:-}" != "--inside" ]; then
root="$(cd "$(dirname "$0")/../.." && pwd)"
installer="${INSTALLER_UNDER_TEST:-$root/scripts/install.sh}"
installer="$(cd "$(dirname "$installer")" && pwd)/$(basename "$installer")"
name="graphdone-installer-test-$$"
trap 'docker rm -f "$name" >/dev/null 2>&1 || true' EXIT INT TERM
docker run --rm --name "$name" \
-e GRAPHDONE_REF="${GRAPHDONE_REF:-main}" \
-e TERM=xterm \
-v "$installer:$MOUNT/install.sh:ro" \
-v "$root/tests/integration/installer-container.sh:$MOUNT/test.sh:ro" \
"$IMAGE" sh "$MOUNT/test.sh" --inside
exit $?
fi

# ── Inside the container ────────────────────────────────────────────────────
REF="$GRAPHDONE_REF"
INSTALL_DIR="$HOME/graphdone"
REPO_URL="https://github.com/GraphDone/GraphDone-Core.git"
failures=0

pass() { printf 'PASS %s\n' "$1"; }
fail() { printf 'FAIL %s\n' "$1"; failures=$((failures + 1)); }

apt-get -qq -o Acquire::Retries=3 update >/dev/null
DEBIAN_FRONTEND=noninteractive apt-get -qq -o Acquire::Retries=3 install -y --no-install-recommends \
git curl ca-certificates sudo openssl ncurses-bin >/dev/null

cat > /usr/local/bin/docker <<'EOF'
#!/bin/sh
case "$1" in
--version) echo "Docker version 27.0.0, build test-stub" ;;
esac
exit 0
EOF
chmod +x /usr/local/bin/docker

# The commit the remote has for REF: a branch head or a (peeled) tag.
remote_sha=$(git ls-remote --heads --tags "$REPO_URL" | awk -v r="$REF" '
{ sha[$2] = $1 }
END {
if (("refs/heads/" r) in sha) print sha["refs/heads/" r]
else if (("refs/tags/" r "^{}") in sha) print sha["refs/tags/" r "^{}"]
else if (("refs/tags/" r) in sha) print sha["refs/tags/" r]
}')
if [ -n "$remote_sha" ]; then
pass "ref '$REF' exists on $REPO_URL at $remote_sha"
else
fail "ref '$REF' does not exist on $REPO_URL"
fi

run_installer() {
label="$1"
log="/tmp/installer-$label.log"
if (cd /tmp && GRAPHDONE_INSTALL_STOP_AFTER=clone sh "$MOUNT/install.sh" </dev/null >"$log" 2>&1); then
pass "installer exited 0 ($label)"
else
fail "installer exited non-zero ($label); last lines:"
tail -25 "$log" | sed 's/\x1b\[[0-9;?]*[A-Za-z]//g'
return 1
fi
}

check_checkout() {
label="$1"
head_sha=$(git -C "$INSTALL_DIR" rev-parse HEAD 2>/dev/null || echo none)
if [ "$head_sha" = "$remote_sha" ]; then
pass "$INSTALL_DIR is at $REF $remote_sha ($label)"
else
fail "$INSTALL_DIR HEAD $head_sha is not $REF $remote_sha ($label)"
fi
if git -C "$INSTALL_DIR" show-ref --verify --quiet "refs/heads/$REF"; then
branch=$(git -C "$INSTALL_DIR" rev-parse --abbrev-ref HEAD)
if [ "$branch" = "$REF" ]; then pass "checked out branch $REF ($label)"; else fail "checked out '$branch', expected $REF ($label)"; fi
fi
# The same marker the installer uses to recognise a GraphDone checkout.
if grep -q '"name": "graphdone"' "$INSTALL_DIR/package.json" 2>/dev/null; then
pass "clone is a GraphDone checkout ($label)"
else
fail "clone has no GraphDone package.json ($label)"
fi
}

if run_installer fresh; then
check_checkout fresh

if run_installer update; then
if grep -q "Update existing" /tmp/installer-update.log; then pass "second run took the update path"; else fail "second run did not take the update path"; fi
check_checkout update
fi

# An install made by the old installer: a local fix/first-start branch whose
# upstream was deleted, one commit behind.
git -C "$INSTALL_DIR" checkout -q -b fix/first-start HEAD~1
git -C "$INSTALL_DIR" config branch.fix/first-start.remote origin
git -C "$INSTALL_DIR" config branch.fix/first-start.merge refs/heads/fix/first-start
if run_installer stranded; then
check_checkout "stranded on fix/first-start"
fi
fi

# Setup-script fallbacks, exactly as the installer would fetch them.
sources=$(sh "$MOUNT/install.sh" sources) || { fail "installer 'sources' command failed"; sources=""; }
if echo "$sources" | grep -qx "ref $REF"; then pass "installer reports ref $REF"; else fail "installer does not report ref $REF"; fi
urls=$(echo "$sources" | awk '$1 == "setup-script" {print $2}')
[ -n "$urls" ] || fail "installer reported no setup-script URLs"
for url in $urls; do
code=$(curl -sS -o /dev/null -w '%{http_code}' --retry 3 --max-time 30 "$url" || echo 000)
if [ "$code" = 200 ]; then pass "HTTP 200 $url"; else fail "HTTP $code $url"; fi
done

if [ "$failures" -ne 0 ]; then
printf '\n%s installer check(s) failed\n' "$failures"
exit 1
fi
printf '\nAll installer checks passed for ref %s\n' "$REF"
Loading
Loading