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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ __pycache__/
*.db-shm
*.db-wal
data/documents/
data/cli-releases/
backups/
test-results/
.artifacts/
*.tsbuildinfo
Expand Down
16 changes: 12 additions & 4 deletions CONTINUE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## Current Snapshot

- Updated: 2026-06-21 20:34:43
- Updated: 2026-06-22 08:30:16
- Branch: `main`

## Recent Non-Continuity Commits
Expand All @@ -17,22 +17,30 @@

## Git Status

- M .gitignore
- M .githooks/pre-merge-commit
- M .githooks/pre-push
- M CONTINUE.md
- M CONTINUE_LOG.md
- M README.md
- M eslint.config.mjs
- M package.json
- M scripts/deploy.sh
- M eslint.config.mjs
- M scripts/check-text-conventions.mjs
- M specs/OVERVIEW.md
- M validate.ps1
- ?? Dockerfile.cli-builder
- ?? scripts/backup-postgres.sh
- ?? scripts/install-cli-releases.ps1
- ?? scripts/install-cli-releases.sh
- ?? scripts/restore-postgres.sh

## Active Specs

- No active spec folders detected.

## Next Recommended Actions

1. Commit and push CI fixes for PR #6 (`prettier`, `spec-overview`).
2. No unchecked tasks detected in the active specs.
1. Review and commit the imported operational tooling: Dockerized CLI release artifacts plus guarded PostgreSQL backup/restore scripts.
2. Commit and push CI fixes for PR #6 (`prettier`, `spec-overview`).
3. No unchecked tasks detected in the active specs.
9 changes: 9 additions & 0 deletions CONTINUE_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -1614,3 +1614,12 @@
- Ran Prettier on `CONTINUE.md`, `CONTINUE_LOG.md`, and `scripts/check-text-conventions.mjs`.
- Regenerated `specs/OVERVIEW.md` with `pnpm run specs:overview:update`.
- Focused verification passed: `pnpm run check:text` and Prettier check for the affected files.

## 2026-06-22 08:30:16

- Compared `C:\dev\resource-planning-codex` operational tooling with this template.
- Added Dockerized GoReleaser helper tooling for `starterctl` snapshots and release artifact installation scripts for POSIX and PowerShell.
- Added guarded PostgreSQL backup and restore scripts adapted to this repo's Compose defaults and Prisma PostgreSQL config.
- Updated `scripts/deploy.sh` to derive build metadata, take a pre-deploy PostgreSQL backup, optionally install CLI release artifacts, and restart app plus worker.
- Updated README and `.gitignore` for CLI release artifacts and PostgreSQL dump storage.
- Verification passed: `package.json` parse, PowerShell CLI release installer smoke test, and source-name scan for copied product strings. POSIX shell syntax checks could not run locally because this Windows host's `bash.exe` points at a broken WSL installation.
18 changes: 18 additions & 0 deletions Dockerfile.cli-builder
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
FROM golang:1.25-alpine

ARG GORELEASER_VERSION=2.16.0
ARG TARGETARCH

RUN apk add --no-cache ca-certificates curl git tar \
&& case "${TARGETARCH:-amd64}" in \
amd64) goreleaser_arch="x86_64" ;; \
arm64) goreleaser_arch="arm64" ;; \
*) echo "unsupported TARGETARCH=${TARGETARCH}" >&2; exit 1 ;; \
esac \
&& curl -fsSL "https://github.com/goreleaser/goreleaser/releases/download/v${GORELEASER_VERSION}/goreleaser_Linux_${goreleaser_arch}.tar.gz" \
| tar -xz -C /usr/local/bin goreleaser \
&& goreleaser --version

WORKDIR /workspace/cli

ENTRYPOINT ["goreleaser"]
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,20 @@ That guide covers:
- manual cross-platform builds
- GoReleaser snapshot packaging

Create local GoReleaser archives with:

```powershell
pnpm run cli:dist
```

Install downloaded or locally built CLI release archives into the deployment
artifact folder with:

```powershell
$env:CLI_RELEASES_SOURCE_DIR='.\cli\dist'
pnpm run cli:install-releases
```

## Docker Deployment

- Local `pnpm run dev` uses SQLite via `DATABASE_URL=file:./dev.db`.
Expand All @@ -147,6 +161,9 @@ That guide covers:
- `pnpm docker build app migrate worker` builds the production app, migration, and worker images.
- `pnpm docker up -d worker` starts the Python background worker against the same Postgres database.
- Review [`docs/runtime-credentials.md`](./docs/runtime-credentials.md) before adding secrets to app, worker, or migration environments.
- `sh ./scripts/deploy.sh` now performs a pre-deploy PostgreSQL dump, applies migrations, and restarts the app and worker. Set `CLI_RELEASES_BUILD_DURING_DEPLOY=docker` to build `starterctl` release archives in a GoReleaser container, `CLI_RELEASES_BUILD_DURING_DEPLOY=true` to build with host tools, or `CLI_RELEASES_SOURCE_DIR=/path/to/artifacts` to install prebuilt archives.
- Manual PostgreSQL backup: `sh ./scripts/backup-postgres.sh`. Dumps are written to `./backups/postgres/business-app-starter-<UTC-timestamp>.dump` and validated with `pg_restore -l`.
- Guarded PostgreSQL restore: `RESTORE_CONFIRM=restore BACKUP_FILE=./backups/postgres/business-app-starter-<timestamp>.dump sh ./scripts/restore-postgres.sh`. The restore script validates the dump, takes a safety backup unless `SKIP_PRE_RESTORE_BACKUP=true`, restores with `pg_restore --clean --if-exists`, reruns Prisma migration/seed steps, and restarts runtime services by default.

## Mail Integration

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
"dev": "node scripts/run-next.mjs dev",
"docker": "node scripts/docker-compose.mjs",
"build": "next build",
"cli:dist": "cd cli && goreleaser release --snapshot --clean",
"cli:install-releases": "pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/install-cli-releases.ps1",
"start": "node scripts/run-next.mjs start",
"test": "vitest run",
"test:watch": "vitest",
Expand Down
64 changes: 64 additions & 0 deletions scripts/backup-postgres.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/env sh
set -eu

COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}"
COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-webapp-template}"
export COMPOSE_PROJECT_NAME
POSTGRES_SERVICE="${POSTGRES_SERVICE:-postgres}"
POSTGRES_DB="${POSTGRES_DB:-business_app_starter}"
POSTGRES_USER="${POSTGRES_USER:-starter}"
BACKUP_DIR="${BACKUP_DIR:-./backups/postgres}"
BACKUP_KEEP_COUNT="${BACKUP_KEEP_COUNT:-5}"
KEEP_DAYS="${KEEP_DAYS:-90}"
BACKUP_NAME_PREFIX="${BACKUP_NAME_PREFIX:-business-app-starter}"

timestamp="$(date -u +%Y-%m-%dT%H-%M-%SZ)"
backup_file="${BACKUP_DIR}/${BACKUP_NAME_PREFIX}-${timestamp}.dump"

step() {
printf '\n=== %s ===\n' "$1"
}

step "Prepare backup directory"
mkdir -p "$BACKUP_DIR"
printf 'Backup file: %s\n' "$backup_file"

step "Create PostgreSQL dump"
docker compose -f "$COMPOSE_FILE" exec -T "$POSTGRES_SERVICE" \
pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -F c > "$backup_file"

step "Verify backup archive"
if [ ! -s "$backup_file" ]; then
printf 'Backup archive is empty: %s\n' "$backup_file" >&2
exit 1
fi

archive_entries="$(docker compose -f "$COMPOSE_FILE" exec -T "$POSTGRES_SERVICE" \
pg_restore -l < "$backup_file" | wc -l | tr -d ' ')"
table_data_entries="$(docker compose -f "$COMPOSE_FILE" exec -T "$POSTGRES_SERVICE" \
pg_restore -l < "$backup_file" | grep -c 'TABLE DATA' || true)"

printf 'Archive entries: %s\n' "$archive_entries"
printf 'Table data entries: %s\n' "$table_data_entries"

if [ "${archive_entries:-0}" -eq 0 ] || [ "${table_data_entries:-0}" -eq 0 ]; then
printf 'Backup archive contains no restorable table data entries: %s\n' "$backup_file" >&2
exit 1
fi

step "Prune old dumps by age"
find "$BACKUP_DIR" -type f -name '*.dump' -mtime +"$KEEP_DAYS" -print -delete || true

step "Prune old dumps by count"
if [ "$BACKUP_KEEP_COUNT" -gt 0 ]; then
ls -1t "$BACKUP_DIR"/"$BACKUP_NAME_PREFIX"-*.dump 2>/dev/null \
| awk -v keep="$BACKUP_KEEP_COUNT" 'NR > keep { print }' \
| while IFS= read -r old_backup; do
[ -n "$old_backup" ] || continue
printf 'Removing old backup: %s\n' "$old_backup"
rm -f "$old_backup"
done
fi

step "Backup completed"
ls -lh "$backup_file"
65 changes: 63 additions & 2 deletions scripts/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,31 @@
set -eu

COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}"
COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-webapp-template}"
export COMPOSE_PROJECT_NAME

generate_app_version() {
head_iso="$(git show -s --format=%cI HEAD)"
head_date="$(TZ=UTC git show -s --date=format-local:%Y%m%d --format=%cd HEAD)"
day_start="$(TZ=UTC git show -s --date=format-local:%Y-%m-%dT00:00:00.000Z --format=%cd HEAD)"
sequence="$(git rev-list --count --first-parent --since="$day_start" --until="$head_iso" HEAD)"
printf '%s.%s' "$head_date" "$sequence"
}

APP_VERSION="${APP_VERSION:-$(generate_app_version)}"
export APP_VERSION

step() {
printf '\n=== %s ===\n' "$1"
}

step "Build metadata"
printf 'APP_VERSION=%s\n' "$APP_VERSION"
printf 'COMPOSE_PROJECT_NAME=%s\n' "$COMPOSE_PROJECT_NAME"

step "Compose data volumes"
docker compose -f "$COMPOSE_FILE" config --volumes

step "Build shared app image"
docker compose -f "$COMPOSE_FILE" build app

Expand All @@ -16,10 +36,51 @@ docker compose -f "$COMPOSE_FILE" up -d postgres
step "Prisma pre-deploy verification"
docker compose -f "$COMPOSE_FILE" run --rm --entrypoint sh migrate -lc "node scripts/prisma-predeploy-check.js"

step "Pre-deploy PostgreSQL backup"
BACKUP_KEEP_COUNT="${BACKUP_KEEP_COUNT:-5}" sh ./scripts/backup-postgres.sh

if [ "${CLI_RELEASES_BUILD_DURING_DEPLOY:-false}" = "docker" ]; then
step "Clean old CLI build cache volumes"
docker volume rm webapp-template-cli-go-mod webapp-template-cli-go-build >/dev/null 2>&1 || true

step "Build CLI release builder image"
docker build -f Dockerfile.cli-builder -t webapp-template-cli-builder:latest .

step "Build CLI release artifacts"
docker run --rm \
-e GOMODCACHE=/tmp/go/pkg/mod \
-e GOCACHE=/tmp/go-build \
-v "$(pwd):/workspace" \
-w /workspace/cli \
webapp-template-cli-builder:latest \
release --snapshot --clean --config .goreleaser.yaml

step "Install CLI release artifacts"
CLI_RELEASES_SOURCE_DIR="${CLI_RELEASES_SOURCE_DIR:-./cli/dist}" \
CLI_RELEASES_HOST_DIR="${CLI_RELEASES_HOST_DIR:-./data/cli-releases}" \
sh ./scripts/install-cli-releases.sh
elif [ "${CLI_RELEASES_BUILD_DURING_DEPLOY:-false}" = "true" ]; then
step "Build CLI release artifacts"
pnpm cli:dist

step "Install CLI release artifacts"
CLI_RELEASES_SOURCE_DIR="${CLI_RELEASES_SOURCE_DIR:-./cli/dist}" \
CLI_RELEASES_HOST_DIR="${CLI_RELEASES_HOST_DIR:-./data/cli-releases}" \
sh ./scripts/install-cli-releases.sh
elif [ -n "${CLI_RELEASES_SOURCE_DIR:-}" ]; then
step "Install CLI release artifacts"
CLI_RELEASES_SOURCE_DIR="$CLI_RELEASES_SOURCE_DIR" \
CLI_RELEASES_HOST_DIR="${CLI_RELEASES_HOST_DIR:-./data/cli-releases}" \
sh ./scripts/install-cli-releases.sh
else
step "CLI release artifacts"
printf 'CLI_RELEASES_BUILD_DURING_DEPLOY is not true/docker and CLI_RELEASES_SOURCE_DIR is not set; keeping existing artifacts in %s\n' "${CLI_RELEASES_HOST_DIR:-./data/cli-releases}"
fi

step "Prisma migrate deploy"
docker compose -f "$COMPOSE_FILE" run --rm --entrypoint sh migrate -lc "pnpm exec prisma migrate deploy --config prisma.config.postgres.ts"

step "Start app from shared image"
docker compose -f "$COMPOSE_FILE" up -d app
step "Rebuild and restart app + worker"
docker compose -f "$COMPOSE_FILE" up -d --build app worker

printf '\nDeploy completed successfully.\n'
54 changes: 54 additions & 0 deletions scripts/install-cli-releases.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
param(
[string]$SourceDir = $env:CLI_RELEASES_SOURCE_DIR,
[string]$TargetDir = $(if ($env:CLI_RELEASES_HOST_DIR) { $env:CLI_RELEASES_HOST_DIR } else { ".\data\cli-releases" })
)

$ErrorActionPreference = "Stop"

if (-not $SourceDir) {
throw "CLI_RELEASES_SOURCE_DIR or -SourceDir is required."
}
if (-not (Test-Path -LiteralPath $SourceDir -PathType Container)) {
throw "Source directory does not exist: $SourceDir"
}

$patterns = @(
"starterctl_*_windows_amd64.zip",
"starterctl_*_windows_arm64.zip",
"starterctl_*_linux_amd64.tar.gz",
"starterctl_*_linux_arm64.tar.gz",
"starterctl_*_darwin_amd64.tar.gz",
"starterctl_*_darwin_arm64.tar.gz",
"checksums.txt"
)

$targetFullPath = [System.IO.Path]::GetFullPath($TargetDir)
$tmpDir = "$targetFullPath.tmp"
if (Test-Path -LiteralPath $tmpDir) {
Remove-Item -LiteralPath $tmpDir -Recurse -Force
}
New-Item -ItemType Directory -Path $tmpDir -Force | Out-Null

$copied = 0
foreach ($pattern in $patterns) {
Get-ChildItem -LiteralPath $SourceDir -Filter $pattern -File | ForEach-Object {
Copy-Item -LiteralPath $_.FullName -Destination $tmpDir
$script:copied += 1
}
}

if ($copied -eq 0) {
Remove-Item -LiteralPath $tmpDir -Recurse -Force
throw "No CLI release artifacts found in $SourceDir"
}

if (Test-Path -LiteralPath $targetFullPath) {
Remove-Item -LiteralPath $targetFullPath -Recurse -Force
}
$parent = Split-Path -Parent $targetFullPath
if ($parent) {
New-Item -ItemType Directory -Path $parent -Force | Out-Null
}
Move-Item -LiteralPath $tmpDir -Destination $targetFullPath

Write-Host "Installed $copied CLI release artifact(s) into $targetFullPath"
42 changes: 42 additions & 0 deletions scripts/install-cli-releases.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env sh
set -eu

SOURCE_DIR="${CLI_RELEASES_SOURCE_DIR:-}"
TARGET_DIR="${CLI_RELEASES_HOST_DIR:-./data/cli-releases}"

fail() {
printf 'ERROR: %s\n' "$1" >&2
exit 1
}

[ -n "$SOURCE_DIR" ] || fail "CLI_RELEASES_SOURCE_DIR is required."
[ -d "$SOURCE_DIR" ] || fail "CLI_RELEASES_SOURCE_DIR does not exist: $SOURCE_DIR"

TMP_DIR="${TARGET_DIR}.tmp"
rm -rf "$TMP_DIR"
mkdir -p "$TMP_DIR"

found=0
for pattern in \
"starterctl_*_windows_amd64.zip" \
"starterctl_*_windows_arm64.zip" \
"starterctl_*_linux_amd64.tar.gz" \
"starterctl_*_linux_arm64.tar.gz" \
"starterctl_*_darwin_amd64.tar.gz" \
"starterctl_*_darwin_arm64.tar.gz" \
"checksums.txt"
do
for file in "$SOURCE_DIR"/$pattern; do
[ -f "$file" ] || continue
cp "$file" "$TMP_DIR"/
found=$((found + 1))
done
done

[ "$found" -gt 0 ] || fail "No CLI release artifacts found in $SOURCE_DIR"

rm -rf "$TARGET_DIR"
mkdir -p "$(dirname "$TARGET_DIR")"
mv "$TMP_DIR" "$TARGET_DIR"

printf 'Installed %s CLI release artifact(s) into %s\n' "$found" "$TARGET_DIR"
Loading