From bbb811c8e695add681387ad2fae5d047c78ccd7a Mon Sep 17 00:00:00 2001 From: Jamie Folsom Date: Fri, 13 Mar 2026 12:41:13 -0400 Subject: [PATCH 1/2] Add dev workflow script and fix build.content.mjs Fix build.content.mjs: - Support GITHUB_BRANCH env var for cloning content repos - Remove content/ before copying to prevent merge artifacts Add scripts/dev.sh - single-command local dev workflow: - Checks out the exact commit deployed on the Netlify site - Unlinks/relinks Netlify site to ensure correct env vars - Clears process env vars that override site settings - Backs up .env to let Netlify site vars take priority - Cleans generated content, .tina/, and i18n files - Sets USE_CONTENT_CACHE=true to avoid loader timeouts - Kills stale processes on ports 4321/9000 - Starts netlify dev Usage: npm run dev -- (sites defined in scripts/sites.json) --- .gitignore | 3 + package.json | 3 +- scripts/build.content.mjs | 9 +- scripts/dev.sh | 304 +++++++++++++++++++++++++++++++++++++ scripts/sites.example.json | 4 + 5 files changed, 321 insertions(+), 2 deletions(-) create mode 100755 scripts/dev.sh create mode 100644 scripts/sites.example.json diff --git a/.gitignore b/.gitignore index 66dad003..afbcf788 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,9 @@ tmp/ # Development config /public/config.dev*.json +# Local site registry (contains client-specific Netlify site IDs) +/scripts/sites.json + # Workspaces *.code-workspace diff --git a/package.json b/package.json index 43e7216c..f0fd204c 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "preview": "astro preview", "test-config": "vitest run config.test.ts", "vitest": "vitest run", - "playwright": "npx playwright test" + "playwright": "npx playwright test", + "dev": "bash scripts/dev.sh" }, "engines": { "node": ">=24.14.0 <25.0.0" diff --git a/scripts/build.content.mjs b/scripts/build.content.mjs index e5ea2b88..fa60cfaf 100644 --- a/scripts/build.content.mjs +++ b/scripts/build.content.mjs @@ -15,7 +15,14 @@ export const fetchContent = async () => { // Clone the content repo into the temporary directory const url = `https://github.com/${process.env.GITHUB_OWNER}/${process.env.GITHUB_REPO}.git`; - child_process.execSync(`git clone ${url} ${TEMP_DIR}`); + const branch = process.env.GITHUB_BRANCH; + const branchArg = branch ? `--branch ${branch} --single-branch` : ''; + child_process.execSync(`git clone ${branchArg} ${url} ${TEMP_DIR}`); + + // Remove existing content to prevent merge artifacts from previous projects + if (fs.existsSync('./content')) { + fs.rmSync('./content', { recursive: true }); + } // Copy the "content" folder to the current directory fs.cpSync(`${TEMP_DIR}/content`, './content', { recursive: true }); diff --git a/scripts/dev.sh b/scripts/dev.sh new file mode 100755 index 00000000..5d357e98 --- /dev/null +++ b/scripts/dev.sh @@ -0,0 +1,304 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SITES_FILE="$SCRIPT_DIR/sites.json" + +if [ ! -f "$SITES_FILE" ]; then + echo "Error: $SITES_FILE not found." >&2 + echo "Copy scripts/sites.example.json to scripts/sites.json and add your Netlify site IDs." >&2 + exit 1 +fi + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +info() { echo -e "${CYAN}[info]${NC} $*"; } +warn() { echo -e "${YELLOW}[warn]${NC} $*"; } +error() { echo -e "${RED}[error]${NC} $*" >&2; } +success() { echo -e "${GREEN}[ok]${NC} $*"; } + +usage() { + cat < [options] + +Switch to a CDP site and start the dev server. + +Options: + --no-match Skip checking out the site's deployed CDP version + --skip-clean Skip content/cache cleanup (faster restart for same site) + --list Show available site names + -h, --help Show this help + +Examples: + scripts/dev.sh atlas # Match deployed version, switch, and start + scripts/dev.sh uss --no-match # Skip version matching, just switch and start + scripts/dev.sh atlas --skip-clean # Restart Atlas without re-cloning content + npm run dev -- atlas # Same thing via npm +EOF +} + +list_sites() { + echo -e "${BOLD}Available sites:${NC}" + # Parse JSON keys without jq dependency + node -e " + const sites = JSON.parse(require('fs').readFileSync('$SITES_FILE', 'utf8')); + const maxLen = Math.max(...Object.keys(sites).map(k => k.length)); + for (const [name, id] of Object.entries(sites)) { + console.log(' ' + name.padEnd(maxLen + 2) + id); + } + " +} + +resolve_site_id() { + local name="$1" + node -e " + const sites = JSON.parse(require('fs').readFileSync('$SITES_FILE', 'utf8')); + if (sites['$name']) { + process.stdout.write(sites['$name']); + } else { + process.exit(1); + } + " 2>/dev/null +} + +kill_stale_ports() { + for port in 4321 9000; do + local pids + pids=$(lsof -ti:"$port" 2>/dev/null || true) + if [ -n "$pids" ]; then + warn "Killing stale processes on port $port" + echo "$pids" | xargs kill -9 2>/dev/null || true + fi + done +} + +check_node_version() { + local required_major + if [ -f .node-version ]; then + required_major=$(cat .node-version | head -1 | sed 's/^v//' | cut -d. -f1) + else + required_major=$(node -e " + const pkg = JSON.parse(require('fs').readFileSync('package.json', 'utf8')); + const engines = pkg.engines?.node || ''; + const match = engines.match(/(\d+)/); + if (match) process.stdout.write(match[1]); + " 2>/dev/null) + fi + + if [ -z "$required_major" ]; then + return 0 + fi + + local current_major + current_major=$(node -v 2>/dev/null | sed 's/^v//' | cut -d. -f1) + + if [ "$current_major" != "$required_major" ]; then + warn "Node $current_major active, but project requires Node $required_major" + if command -v nvm &>/dev/null || [ -s "$HOME/.nvm/nvm.sh" ]; then + info "Switching via nvm..." + export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" + # shellcheck source=/dev/null + [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" + nvm use "$required_major" 2>/dev/null || nvm install "$required_major" + success "Now using Node $(node -v)" + else + error "nvm not found. Install Node $required_major manually." + exit 1 + fi + else + success "Node $(node -v) matches project requirement" + fi +} + +match_deployed_version() { + local site_id="$1" + info "Fetching deployed version for site $site_id..." + + local deploy_info + deploy_info=$(npx netlify api getSite --data "{\"site_id\": \"$site_id\"}" 2>/dev/null \ + | node -e " + let data = ''; + process.stdin.on('data', d => data += d); + process.stdin.on('end', () => { + const site = JSON.parse(data); + const deploy = site.published_deploy || {}; + console.log(JSON.stringify({ + commit: deploy.commit_ref || '', + branch: deploy.branch || '', + created: deploy.created_at || '' + })); + }); + ") + + local commit branch created + commit=$(echo "$deploy_info" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).commit))") + branch=$(echo "$deploy_info" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).branch))") + created=$(echo "$deploy_info" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).created))") + + if [ -z "$commit" ]; then + warn "Could not determine deployed commit. Continuing on current branch." + return 0 + fi + + info "Deployed: commit ${commit:0:8} on branch $branch ($created)" + + # Check for uncommitted changes + if ! git diff --quiet HEAD 2>/dev/null || ! git diff --cached --quiet HEAD 2>/dev/null; then + error "Working tree has uncommitted changes. Commit or stash before using --match." + exit 1 + fi + + # Check out the exact deployed commit + local tag + tag=$(git tag --sort=-v:refname --merged "$commit" 2>/dev/null | head -1 || true) + if [ -n "$tag" ]; then + info "Nearest version tag: $tag" + fi + + info "Checking out deployed commit ${commit:0:8}..." + git checkout "$commit" + + success "Matched deployed version" +} + +clean_generated() { + info "Cleaning generated content..." + rm -rf content/ .tina/ + rm -f src/i18n/userDefinedFields.json src/i18n/search.json + rm -rf src/components/custom/project/ + success "Cleaned content/, .tina/, and generated files" +} + +# --- Main --- + +SITE="" +MATCH=true +SKIP_CLEAN=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --list) + list_sites + exit 0 + ;; + --no-match) + MATCH=false + shift + ;; + --skip-clean) + SKIP_CLEAN=true + shift + ;; + -h|--help) + usage + exit 0 + ;; + -*) + error "Unknown option: $1" + usage + exit 1 + ;; + *) + SITE="$1" + shift + ;; + esac +done + +# Interactive site selection if no site given +if [ -z "$SITE" ]; then + echo -e "${BOLD}Select a site:${NC}" + mapfile -t site_names < <(node -e " + const sites = JSON.parse(require('fs').readFileSync('$SITES_FILE', 'utf8')); + Object.keys(sites).forEach(k => console.log(k)); + ") + + select site_choice in "${site_names[@]}"; do + if [ -n "$site_choice" ]; then + SITE="$site_choice" + break + fi + done +fi + +# Resolve site name to ID +SITE_ID=$(resolve_site_id "$SITE" || true) +if [ -z "$SITE_ID" ]; then + error "Unknown site: $SITE" + echo "" + list_sites + exit 1 +fi + +echo "" +info "Starting dev workflow for ${BOLD}$SITE${NC} ($SITE_ID)" +echo "" + +# Step 1: Match deployed version (optional) +if [ "$MATCH" = true ]; then + match_deployed_version "$SITE_ID" + echo "" +fi + +# Step 2: Check Node version +check_node_version +echo "" + +# Step 3: Link Netlify site (unlink first to ensure switch works) +info "Linking to Netlify site..." +npx netlify unlink 2>/dev/null || true +npx netlify link --id "$SITE_ID" +success "Linked to $SITE" +echo "" + +# Step 4: Clean generated content +if [ "$SKIP_CLEAN" = false ]; then + clean_generated + echo "" +fi + +# Step 5: Kill stale processes +kill_stale_ports +echo "" + +# Step 6: Clear env vars that conflict with Netlify site settings +# These may be set from a previous netlify dev session, .env sourcing, etc. +# Unsetting them lets netlify dev inject the linked site's values. +SITE_MANAGED_VARS=( + CONFIG_URL CONFIG_FILE + GITHUB_OWNER GITHUB_REPO GITHUB_BRANCH GITHUB_PERSONAL_ACCESS_TOKEN + MONGODB_URI MONGODB_NAME MONGODB_COLLECTION_NAME + TINA_PUBLIC_IS_LOCAL +) +for var in "${SITE_MANAGED_VARS[@]}"; do + unset "$var" +done +info "Cleared site-managed env vars from process environment" + +# Step 7: Move .env aside so Netlify site env vars take priority +ENV_BACKED_UP=false +if [ -f .env ]; then + warn "Moving .env to .env.bak (Netlify site vars take priority)" + mv .env .env.bak + ENV_BACKED_UP=true +fi + +restore_env() { + if [ "$ENV_BACKED_UP" = true ] && [ -f .env.bak ]; then + mv .env.bak .env + info "Restored .env from .env.bak" + fi +} +trap restore_env EXIT + +# Step 8: Start dev server +export USE_CONTENT_CACHE=true +info "Starting dev server (USE_CONTENT_CACHE=true)..." +echo "" +npx netlify dev diff --git a/scripts/sites.example.json b/scripts/sites.example.json new file mode 100644 index 00000000..53cc326e --- /dev/null +++ b/scripts/sites.example.json @@ -0,0 +1,4 @@ +{ + "my-site": "netlify-site-id-here", + "my-other-site": "another-netlify-site-id" +} From cea89c9d7ce5d7be1fd66589ac4095b65f811a8e Mon Sep 17 00:00:00 2001 From: Jamie Folsom Date: Fri, 13 Mar 2026 12:46:29 -0400 Subject: [PATCH 2/2] Add --init flag to generate sites.json from Netlify Queries Netlify API for all CDP sites the user has access to and writes scripts/sites.json. Developers can then edit the file to use shorter names. --- scripts/dev.sh | 56 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/scripts/dev.sh b/scripts/dev.sh index 5d357e98..5a3dfe38 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -4,12 +4,6 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SITES_FILE="$SCRIPT_DIR/sites.json" -if [ ! -f "$SITES_FILE" ]; then - echo "Error: $SITES_FILE not found." >&2 - echo "Copy scripts/sites.example.json to scripts/sites.json and add your Netlify site IDs." >&2 - exit 1 -fi - # Colors RED='\033[0;31m' GREEN='\033[0;32m' @@ -33,9 +27,11 @@ Options: --no-match Skip checking out the site's deployed CDP version --skip-clean Skip content/cache cleanup (faster restart for same site) --list Show available site names + --init Generate scripts/sites.json from your Netlify account -h, --help Show this help Examples: + scripts/dev.sh --init # First-time setup: generate sites.json scripts/dev.sh atlas # Match deployed version, switch, and start scripts/dev.sh uss --no-match # Skip version matching, just switch and start scripts/dev.sh atlas --skip-clean # Restart Atlas without re-cloning content @@ -43,6 +39,43 @@ Examples: EOF } +init_sites() { + info "Querying Netlify for CDP sites..." + local json + json=$(npx netlify sites:list --json 2>/dev/null) + + if [ -z "$json" ]; then + error "Failed to fetch sites. Make sure you're logged in (netlify login)." + exit 1 + fi + + node -e " + const sites = JSON.parse(process.argv[1]); + const cdpSites = sites.filter(s => + s.build_settings?.repo_url === 'https://github.com/performant-software/core-data-places' + ); + + if (cdpSites.length === 0) { + console.error('No CDP sites found in your Netlify account.'); + process.exit(1); + } + + const registry = {}; + for (const s of cdpSites.sort((a, b) => a.name.localeCompare(b.name))) { + registry[s.name] = s.id; + } + + const fs = require('fs'); + fs.writeFileSync('$SITES_FILE', JSON.stringify(registry, null, 2) + '\n'); + console.log('Wrote $SITES_FILE with ' + cdpSites.length + ' sites:'); + for (const [name, id] of Object.entries(registry)) { + console.log(' ' + name + ' ' + id); + } + console.log(''); + console.log('Tip: edit the file to use shorter names (e.g. \"uss\" instead of \"universities-studying-slavery\").'); + " "$json" +} + list_sites() { echo -e "${BOLD}Available sites:${NC}" # Parse JSON keys without jq dependency @@ -183,6 +216,10 @@ SKIP_CLEAN=false while [[ $# -gt 0 ]]; do case "$1" in + --init) + init_sites + exit 0 + ;; --list) list_sites exit 0 @@ -227,6 +264,13 @@ if [ -z "$SITE" ]; then done fi +# Ensure sites.json exists +if [ ! -f "$SITES_FILE" ]; then + error "$SITES_FILE not found." + echo "Run 'scripts/dev.sh --init' to generate it from your Netlify account." >&2 + exit 1 +fi + # Resolve site name to ID SITE_ID=$(resolve_site_id "$SITE" || true) if [ -z "$SITE_ID" ]; then