jekyll-theme-zer0 0.22.20 → 0.22.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +74 -4
- data/README.md +325 -40
- data/_data/README.md +1 -0
- data/_data/roadmap.yml +215 -0
- data/scripts/bin/install +717 -0
- data/scripts/bin/test +45 -2
- data/scripts/generate-roadmap.rb +200 -0
- data/scripts/generate-roadmap.sh +21 -0
- data/scripts/lib/install/README.md +63 -0
- data/scripts/lib/install/agents.sh +166 -0
- data/scripts/lib/install/ai/diagnose.sh +199 -0
- data/scripts/lib/install/ai/openai.sh +233 -0
- data/scripts/lib/install/ai/suggest.sh +182 -0
- data/scripts/lib/install/ai/wizard.sh +160 -0
- data/scripts/lib/install/config.sh +56 -0
- data/scripts/lib/install/deploy/README.md +52 -0
- data/scripts/lib/install/deploy/azure-swa.sh +50 -0
- data/scripts/lib/install/deploy/docker-prod.sh +71 -0
- data/scripts/lib/install/deploy/github-pages.sh +44 -0
- data/scripts/lib/install/deploy/registry.sh +190 -0
- data/scripts/lib/install/doctor.sh +301 -0
- data/scripts/lib/install/fs.sh +52 -0
- data/scripts/lib/install/logging.sh +33 -0
- data/scripts/lib/install/pages.sh +255 -0
- data/scripts/lib/install/platform.sh +71 -0
- data/scripts/lib/install/profile.sh +113 -0
- data/scripts/lib/install/template.sh +137 -0
- data/scripts/lib/install/upgrade.sh +184 -0
- data/scripts/lib/install/wizard_interactive.sh +189 -0
- metadata +27 -2
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/lib/install/ai/wizard.sh
|
|
3
|
+
#
|
|
4
|
+
# `install wizard --ai` — opt-in OpenAI-powered config generation.
|
|
5
|
+
#
|
|
6
|
+
# Flow:
|
|
7
|
+
# 1. ai_enabled() check (ZER0_NO_AI kill-switch)
|
|
8
|
+
# 2. ai_require_key() check (OPENAI_API_KEY)
|
|
9
|
+
# 3. Prompt user for: site description, target audience, deploy preference
|
|
10
|
+
# 4. Read system prompt from templates/ai/prompts/wizard-system.md
|
|
11
|
+
# 5. ai_estimate_cost() → user sees price estimate
|
|
12
|
+
# 6. ai_call_chat() → returns JSON: {title, description, tagline, navigation,
|
|
13
|
+
# welcome_post_outline, suggested_deploy_target}
|
|
14
|
+
# 7. ai_show_diff_confirm() per generated file (_config.yml, navigation,
|
|
15
|
+
# welcome post)
|
|
16
|
+
# 8. On any failure → fall back to non-AI wizard (delegated to caller).
|
|
17
|
+
#
|
|
18
|
+
# Public API:
|
|
19
|
+
# wizard_ai_run <target_dir> <repo_root> [--auto-accept]
|
|
20
|
+
#
|
|
21
|
+
# Returns 0 on success, 1 on failure (caller should fall back).
|
|
22
|
+
|
|
23
|
+
# shellcheck disable=SC2034
|
|
24
|
+
AI_WIZARD_LIB_VERSION="1.0.0"
|
|
25
|
+
|
|
26
|
+
wizard_ai_run() {
|
|
27
|
+
local target_dir="$1" repo_root="$2"
|
|
28
|
+
shift 2 || true
|
|
29
|
+
|
|
30
|
+
local auto_accept=0
|
|
31
|
+
while [[ $# -gt 0 ]]; do
|
|
32
|
+
case "$1" in
|
|
33
|
+
--auto-accept) auto_accept=1 ;;
|
|
34
|
+
*) log_warning "wizard_ai_run: ignoring unknown flag: $1" ;;
|
|
35
|
+
esac
|
|
36
|
+
shift
|
|
37
|
+
done
|
|
38
|
+
|
|
39
|
+
if ! ai_enabled; then
|
|
40
|
+
log_warning "AI is disabled (ZER0_NO_AI=1) — cannot run --ai wizard."
|
|
41
|
+
return 1
|
|
42
|
+
fi
|
|
43
|
+
if ! ai_require_key; then
|
|
44
|
+
return 1
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# 1. Gather user inputs
|
|
48
|
+
log_info "AI Wizard — describe your site in a sentence or two."
|
|
49
|
+
log_info "Examples: 'Personal blog about Rust performance' or 'Docs site for an open-source CLI'."
|
|
50
|
+
printf "Site description: "
|
|
51
|
+
local site_desc audience deploy_pref
|
|
52
|
+
read -r site_desc
|
|
53
|
+
if [[ -z "$site_desc" ]]; then
|
|
54
|
+
log_error "Empty description — aborting AI wizard."
|
|
55
|
+
return 1
|
|
56
|
+
fi
|
|
57
|
+
printf "Target audience (e.g., 'developers', 'data scientists', 'general'): "
|
|
58
|
+
read -r audience
|
|
59
|
+
[[ -z "$audience" ]] && audience="developers"
|
|
60
|
+
printf "Deploy preference (github-pages | azure-swa | docker-prod | unsure): "
|
|
61
|
+
read -r deploy_pref
|
|
62
|
+
[[ -z "$deploy_pref" ]] && deploy_pref="unsure"
|
|
63
|
+
|
|
64
|
+
# 2. Load system prompt
|
|
65
|
+
local sys_prompt_file="$repo_root/templates/ai/prompts/wizard-system.md"
|
|
66
|
+
if [[ ! -f "$sys_prompt_file" ]]; then
|
|
67
|
+
log_error "System prompt missing: $sys_prompt_file"
|
|
68
|
+
return 1
|
|
69
|
+
fi
|
|
70
|
+
local system_prompt
|
|
71
|
+
system_prompt="$(cat "$sys_prompt_file")"
|
|
72
|
+
|
|
73
|
+
# 3. Build user prompt
|
|
74
|
+
local user_prompt
|
|
75
|
+
user_prompt="Site description: ${site_desc}
|
|
76
|
+
Target audience: ${audience}
|
|
77
|
+
Deploy preference: ${deploy_pref}
|
|
78
|
+
|
|
79
|
+
Return ONLY a JSON object with keys: title, description, tagline, suggested_deploy_target, navigation (array of {label, url}), welcome_post_outline (string of 3-5 bullet points)."
|
|
80
|
+
|
|
81
|
+
# Sanitize (paranoid — user input could contain pasted secrets)
|
|
82
|
+
user_prompt="$(printf '%s' "$user_prompt" | ai_sanitize_text)"
|
|
83
|
+
|
|
84
|
+
# 4. Cost estimate + confirm
|
|
85
|
+
local model
|
|
86
|
+
model="$(ai_default_model wizard)"
|
|
87
|
+
local in_chars=$(( ${#system_prompt} + ${#user_prompt} ))
|
|
88
|
+
log_info "About to call OpenAI:"
|
|
89
|
+
ai_estimate_cost "$model" "$in_chars" 800
|
|
90
|
+
if [[ "$auto_accept" != "1" ]]; then
|
|
91
|
+
printf "Proceed with API call? [y/N] "
|
|
92
|
+
local go
|
|
93
|
+
read -r go
|
|
94
|
+
if [[ ! "$go" =~ ^[Yy]$ ]]; then
|
|
95
|
+
log_warning "Aborted by user."
|
|
96
|
+
return 1
|
|
97
|
+
fi
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# 5. Call API
|
|
101
|
+
log_info "Calling $model ..."
|
|
102
|
+
local raw
|
|
103
|
+
if ! raw="$(ai_call_chat "$model" "$system_prompt" "$user_prompt" 1024 0.4)"; then
|
|
104
|
+
log_error "OpenAI call failed."
|
|
105
|
+
return 1
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# 6. Parse JSON (strip code-fence if present)
|
|
109
|
+
local json
|
|
110
|
+
json="$(printf '%s' "$raw" | sed -E '/^```(json)?$/d')"
|
|
111
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
112
|
+
log_error "python3 required to parse wizard response."
|
|
113
|
+
return 1
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# Extract fields safely
|
|
117
|
+
local title description tagline suggested_deploy
|
|
118
|
+
title="$(printf '%s' "$json" | python3 -c 'import json,sys;d=json.load(sys.stdin);print(d.get("title",""))' 2>/dev/null || echo "")"
|
|
119
|
+
description="$(printf '%s' "$json" | python3 -c 'import json,sys;d=json.load(sys.stdin);print(d.get("description",""))' 2>/dev/null || echo "")"
|
|
120
|
+
tagline="$(printf '%s' "$json" | python3 -c 'import json,sys;d=json.load(sys.stdin);print(d.get("tagline",""))' 2>/dev/null || echo "")"
|
|
121
|
+
suggested_deploy="$(printf '%s' "$json" | python3 -c 'import json,sys;d=json.load(sys.stdin);print(d.get("suggested_deploy_target",""))' 2>/dev/null || echo "")"
|
|
122
|
+
|
|
123
|
+
if [[ -z "$title" ]]; then
|
|
124
|
+
log_error "Failed to parse AI response (missing 'title' field)."
|
|
125
|
+
log_info "Raw response: $raw"
|
|
126
|
+
return 1
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
# 7. Build proposed _config.yml fragment + diff
|
|
130
|
+
local cfg_file="$target_dir/_config.yml"
|
|
131
|
+
local proposed
|
|
132
|
+
proposed="$(cat <<EOF
|
|
133
|
+
# Generated by install wizard --ai
|
|
134
|
+
title: "$title"
|
|
135
|
+
description: "$description"
|
|
136
|
+
tagline: "$tagline"
|
|
137
|
+
EOF
|
|
138
|
+
)"
|
|
139
|
+
log_info "AI suggests deploy target: ${suggested_deploy:-(none)}"
|
|
140
|
+
if [[ -f "$cfg_file" ]]; then
|
|
141
|
+
log_warning "_config.yml already exists. The AI suggestions above won't be merged automatically."
|
|
142
|
+
log_info "Recommended: cp the values you like into _config.yml manually."
|
|
143
|
+
else
|
|
144
|
+
local accepted_path
|
|
145
|
+
if accepted_path="$(ai_show_diff_confirm "$cfg_file" "$proposed" "Create _config.yml" "$auto_accept")"; then
|
|
146
|
+
mv "$accepted_path" "$cfg_file"
|
|
147
|
+
log_success "Wrote $cfg_file"
|
|
148
|
+
fi
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
# 8. Print full JSON for user reference (so welcome post outline + nav
|
|
152
|
+
# aren't lost). User can manually convert to files.
|
|
153
|
+
echo
|
|
154
|
+
log_info "Full AI response (use these to flesh out navigation + a welcome post):"
|
|
155
|
+
echo "─────────────────────────────────────────────────────────────"
|
|
156
|
+
printf '%s\n' "$json"
|
|
157
|
+
echo "─────────────────────────────────────────────────────────────"
|
|
158
|
+
|
|
159
|
+
return 0
|
|
160
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =========================================================================
|
|
3
|
+
# scripts/lib/install/config.sh
|
|
4
|
+
# =========================================================================
|
|
5
|
+
# Configuration loader for install.sh.
|
|
6
|
+
#
|
|
7
|
+
# load_install_config <SCRIPT_DIR> [<SOURCE_DIR>]
|
|
8
|
+
# Searches for templates/config/install.conf in either of the supplied
|
|
9
|
+
# directories. On success: sources it, exports TEMPLATES_DIR, returns 0.
|
|
10
|
+
# On failure: applies hard-coded fallback defaults and returns 1.
|
|
11
|
+
#
|
|
12
|
+
# This function is identical in behaviour to the previous private
|
|
13
|
+
# `_load_install_config` inside install.sh; the only change is location +
|
|
14
|
+
# accepting the search roots as parameters (so the function is testable
|
|
15
|
+
# without globals).
|
|
16
|
+
# =========================================================================
|
|
17
|
+
|
|
18
|
+
load_install_config() {
|
|
19
|
+
local script_dir="${1:-${SCRIPT_DIR:-$(pwd)}}"
|
|
20
|
+
local source_dir="${2:-${SOURCE_DIR:-$script_dir}}"
|
|
21
|
+
|
|
22
|
+
local config_paths=(
|
|
23
|
+
"$script_dir/templates/config/install.conf"
|
|
24
|
+
"$source_dir/templates/config/install.conf"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
local config_path
|
|
28
|
+
for config_path in "${config_paths[@]}"; do
|
|
29
|
+
if [[ -f "$config_path" ]]; then
|
|
30
|
+
# shellcheck source=/dev/null
|
|
31
|
+
source "$config_path"
|
|
32
|
+
export TEMPLATES_DIR="$(dirname "$(dirname "$config_path")")"
|
|
33
|
+
return 0
|
|
34
|
+
fi
|
|
35
|
+
done
|
|
36
|
+
|
|
37
|
+
# Fallback defaults when templates not available (remote install
|
|
38
|
+
# without bundled templates/, or stripped distribution).
|
|
39
|
+
export THEME_NAME="${THEME_NAME:-zer0-mistakes}"
|
|
40
|
+
export THEME_GEM_NAME="${THEME_GEM_NAME:-jekyll-theme-zer0}"
|
|
41
|
+
export THEME_DISPLAY_NAME="${THEME_DISPLAY_NAME:-Zer0-Mistakes Jekyll Theme}"
|
|
42
|
+
export GITHUB_USER="${GITHUB_USER:-bamr87}"
|
|
43
|
+
export GITHUB_REPO="${GITHUB_REPO:-bamr87/zer0-mistakes}"
|
|
44
|
+
export GITHUB_URL="${GITHUB_URL:-https://github.com/bamr87/zer0-mistakes}"
|
|
45
|
+
export GITHUB_RAW_URL="${GITHUB_RAW_URL:-https://raw.githubusercontent.com/bamr87/zer0-mistakes/main}"
|
|
46
|
+
export DEFAULT_PORT="${DEFAULT_PORT:-4000}"
|
|
47
|
+
export DEFAULT_URL="${DEFAULT_URL:-http://localhost:4000}"
|
|
48
|
+
export JEKYLL_VERSION="${JEKYLL_VERSION:-~> 4.3}"
|
|
49
|
+
export FFI_VERSION="${FFI_VERSION:-~> 1.17.0}"
|
|
50
|
+
export WEBRICK_VERSION="${WEBRICK_VERSION:-~> 1.7}"
|
|
51
|
+
export COMMONMARKER_VERSION="${COMMONMARKER_VERSION:-0.23.10}"
|
|
52
|
+
export GITHUB_PAGES_MAX_VERSION="${GITHUB_PAGES_MAX_VERSION:-232}"
|
|
53
|
+
export COMMONMARKER_MACOS_VERSION="${COMMONMARKER_MACOS_VERSION:-~> 0.23}"
|
|
54
|
+
export RUBY_MIN_VERSION_MACOS="${RUBY_MIN_VERSION_MACOS:-2.6.0}"
|
|
55
|
+
return 1
|
|
56
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# scripts/lib/install/deploy/
|
|
2
|
+
|
|
3
|
+
Pluggable deploy-target modules consumed by `scripts/bin/install deploy`
|
|
4
|
+
(Phase 4 of the installer refactor). Each module configures one target;
|
|
5
|
+
the registry coordinates discovery, dispatch, and verification.
|
|
6
|
+
|
|
7
|
+
## Files
|
|
8
|
+
|
|
9
|
+
| File | Role |
|
|
10
|
+
| ----------------- | ------------------------------------------------------------------- |
|
|
11
|
+
| `registry.sh` | Module discovery, dispatch, shared `deploy_render` / `deploy_copy`. |
|
|
12
|
+
| `github-pages.sh` | Actions workflow that publishes `_site/` to `gh-pages`. |
|
|
13
|
+
| `azure-swa.sh` | Azure SWA workflow + `staticwebapp.config.json`. |
|
|
14
|
+
| `docker-prod.sh` | Multi-stage Docker build + production compose + nginx config. |
|
|
15
|
+
|
|
16
|
+
## Module contract
|
|
17
|
+
|
|
18
|
+
Every module must define:
|
|
19
|
+
|
|
20
|
+
| Symbol | Purpose |
|
|
21
|
+
| --------------------------------------- | -------------------------------------------------------- |
|
|
22
|
+
| `DEPLOY_<SLUG_UPPER>_TITLE` | One-line display name shown by `install list-targets`. |
|
|
23
|
+
| `DEPLOY_<SLUG_UPPER>_SUMMARY` | One-line description shown by `install list-targets`. |
|
|
24
|
+
| `deploy_<slug>_check_prereqs <dir>` | Print warnings; return non-zero only on hard blockers. |
|
|
25
|
+
| `deploy_<slug>_install <dir>` | Idempotent file install (uses `deploy_render_if_absent`).|
|
|
26
|
+
| `deploy_<slug>_verify <dir>` | Confirm expected files exist + look correct. |
|
|
27
|
+
| `deploy_<slug>_doc_url` | Print the canonical upstream documentation URL. |
|
|
28
|
+
|
|
29
|
+
Modules use the lightweight `deploy_render` placeholder set
|
|
30
|
+
(`{{RUBY_VERSION}}`, `{{DEFAULT_BRANCH}}`, `{{GITHUB_USER}}`,
|
|
31
|
+
`{{SITE_NAME}}`) so they can run without the full install.sh global
|
|
32
|
+
environment.
|
|
33
|
+
|
|
34
|
+
## Adding a target
|
|
35
|
+
|
|
36
|
+
1. Add `templates/deploy/<slug>/` with the assets (workflow YAML,
|
|
37
|
+
Dockerfile, README, etc.). Use `*.template` for files that need
|
|
38
|
+
variable substitution.
|
|
39
|
+
2. Create `scripts/lib/install/deploy/<slug>.sh` exporting the four
|
|
40
|
+
hooks above.
|
|
41
|
+
3. Add `<slug>` to `DEPLOY_TARGETS_LIST` in `registry.sh` (alphabetical).
|
|
42
|
+
4. Optionally reference `<slug>` under `deploy_targets:` in
|
|
43
|
+
`templates/profiles/*.yml` so the profile can suggest it.
|
|
44
|
+
5. Update `templates/deploy/README.md` with the new target row.
|
|
45
|
+
|
|
46
|
+
## CLI integration
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
./scripts/bin/install list-targets
|
|
50
|
+
./scripts/bin/install deploy github-pages /path/to/site
|
|
51
|
+
./scripts/bin/install deploy azure-swa,docker-prod /path/to/site
|
|
52
|
+
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/lib/install/deploy/azure-swa.sh
|
|
3
|
+
#
|
|
4
|
+
# Deploy module: Azure Static Web Apps. Replaces the legacy
|
|
5
|
+
# install.sh::create_azure_static_web_apps_workflow heredoc and adds
|
|
6
|
+
# a sensible staticwebapp.config.json.
|
|
7
|
+
|
|
8
|
+
DEPLOY_AZURE_SWA_TITLE="Azure Static Web Apps"
|
|
9
|
+
DEPLOY_AZURE_SWA_SUMMARY="Workflow + config for Azure SWA. Requires AZURE_STATIC_WEB_APPS_API_TOKEN secret."
|
|
10
|
+
|
|
11
|
+
deploy_azure_swa_check_prereqs() {
|
|
12
|
+
local target_dir="$1"
|
|
13
|
+
if [ ! -f "$target_dir/Gemfile" ]; then
|
|
14
|
+
log_warning "Gemfile not found in $target_dir — Azure build step will fail until one exists."
|
|
15
|
+
fi
|
|
16
|
+
return 0
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
deploy_azure_swa_install() {
|
|
20
|
+
local target_dir="$1"
|
|
21
|
+
local repo_root="${REPO_ROOT:-$(deploy_repo_root)}"
|
|
22
|
+
local src_dir="$repo_root/templates/deploy/azure-swa"
|
|
23
|
+
|
|
24
|
+
deploy_render_if_absent \
|
|
25
|
+
"$src_dir/azure-static-web-apps.yml.template" \
|
|
26
|
+
"$target_dir/.github/workflows/azure-static-web-apps.yml"
|
|
27
|
+
|
|
28
|
+
deploy_copy \
|
|
29
|
+
"$src_dir/staticwebapp.config.json" \
|
|
30
|
+
"$target_dir/staticwebapp.config.json"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
deploy_azure_swa_verify() {
|
|
34
|
+
local target_dir="$1"
|
|
35
|
+
local wf="$target_dir/.github/workflows/azure-static-web-apps.yml"
|
|
36
|
+
local cfg="$target_dir/staticwebapp.config.json"
|
|
37
|
+
local ok=0
|
|
38
|
+
[ -f "$wf" ] || { log_error "Missing $wf"; ok=1; }
|
|
39
|
+
[ -f "$cfg" ] || { log_error "Missing $cfg"; ok=1; }
|
|
40
|
+
[ "$ok" = "0" ] || return 1
|
|
41
|
+
grep -q 'Azure/static-web-apps-deploy' "$wf" || {
|
|
42
|
+
log_warning "Workflow does not reference Azure/static-web-apps-deploy"
|
|
43
|
+
return 1
|
|
44
|
+
}
|
|
45
|
+
return 0
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
deploy_azure_swa_doc_url() {
|
|
49
|
+
echo "https://learn.microsoft.com/azure/static-web-apps/"
|
|
50
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/lib/install/deploy/docker-prod.sh
|
|
3
|
+
#
|
|
4
|
+
# Deploy module: self-hosted production Docker (multi-stage Ruby + Nginx).
|
|
5
|
+
# Installs docker/Dockerfile.prod, docker-compose.prod.yml, docker/nginx.conf,
|
|
6
|
+
# and (if absent) a .dockerignore tuned for the build context.
|
|
7
|
+
|
|
8
|
+
DEPLOY_DOCKER_PROD_TITLE="Self-hosted production Docker"
|
|
9
|
+
DEPLOY_DOCKER_PROD_SUMMARY="Two-stage build (Ruby builder + nginx:alpine runtime) with healthcheck + compose."
|
|
10
|
+
|
|
11
|
+
deploy_docker_prod_check_prereqs() {
|
|
12
|
+
local target_dir="$1"
|
|
13
|
+
if ! command -v docker >/dev/null 2>&1; then
|
|
14
|
+
log_warning "docker CLI not found in PATH — files will be installed but you cannot build the image locally."
|
|
15
|
+
fi
|
|
16
|
+
if [ ! -f "$target_dir/Gemfile" ]; then
|
|
17
|
+
log_warning "Gemfile not found in $target_dir — Docker build will fail until one exists."
|
|
18
|
+
fi
|
|
19
|
+
return 0
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
deploy_docker_prod_install() {
|
|
23
|
+
local target_dir="$1"
|
|
24
|
+
local repo_root="${REPO_ROOT:-$(deploy_repo_root)}"
|
|
25
|
+
local src_dir="$repo_root/templates/deploy/docker-prod"
|
|
26
|
+
|
|
27
|
+
DEPLOY_SITE_NAME="${DEPLOY_SITE_NAME:-$(basename "$target_dir")}"
|
|
28
|
+
|
|
29
|
+
deploy_render_if_absent \
|
|
30
|
+
"$src_dir/Dockerfile.prod.template" \
|
|
31
|
+
"$target_dir/docker/Dockerfile.prod"
|
|
32
|
+
|
|
33
|
+
deploy_render_if_absent \
|
|
34
|
+
"$src_dir/docker-compose.prod.yml.template" \
|
|
35
|
+
"$target_dir/docker-compose.prod.yml"
|
|
36
|
+
|
|
37
|
+
deploy_copy \
|
|
38
|
+
"$src_dir/nginx.conf" \
|
|
39
|
+
"$target_dir/docker/nginx.conf"
|
|
40
|
+
|
|
41
|
+
# .dockerignore: only install when missing so we never clobber user rules.
|
|
42
|
+
if [ ! -f "$target_dir/.dockerignore" ]; then
|
|
43
|
+
deploy_copy "$src_dir/.dockerignore" "$target_dir/.dockerignore"
|
|
44
|
+
else
|
|
45
|
+
log_warning ".dockerignore already exists, leaving untouched."
|
|
46
|
+
fi
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
deploy_docker_prod_verify() {
|
|
50
|
+
local target_dir="$1"
|
|
51
|
+
local ok=0
|
|
52
|
+
for f in \
|
|
53
|
+
"$target_dir/docker/Dockerfile.prod" \
|
|
54
|
+
"$target_dir/docker-compose.prod.yml" \
|
|
55
|
+
"$target_dir/docker/nginx.conf"; do
|
|
56
|
+
if [ ! -f "$f" ]; then
|
|
57
|
+
log_error "Missing $f"
|
|
58
|
+
ok=1
|
|
59
|
+
fi
|
|
60
|
+
done
|
|
61
|
+
[ "$ok" = "0" ] || return 1
|
|
62
|
+
grep -q 'nginx:alpine' "$target_dir/docker/Dockerfile.prod" || {
|
|
63
|
+
log_warning "Dockerfile.prod is missing the nginx:alpine runtime stage"
|
|
64
|
+
return 1
|
|
65
|
+
}
|
|
66
|
+
return 0
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
deploy_docker_prod_doc_url() {
|
|
70
|
+
echo "https://docs.docker.com/compose/production/"
|
|
71
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/lib/install/deploy/github-pages.sh
|
|
3
|
+
#
|
|
4
|
+
# Deploy module: GitHub Pages (Actions-based, peaceiris/actions-gh-pages).
|
|
5
|
+
# Generates .github/workflows/jekyll-gh-pages.yml.
|
|
6
|
+
|
|
7
|
+
DEPLOY_GITHUB_PAGES_TITLE="GitHub Pages (Actions)"
|
|
8
|
+
DEPLOY_GITHUB_PAGES_SUMMARY="Builds with Bundler and publishes _site/ to the gh-pages branch."
|
|
9
|
+
|
|
10
|
+
deploy_github_pages_check_prereqs() {
|
|
11
|
+
local target_dir="$1"
|
|
12
|
+
if [ ! -f "$target_dir/_config.yml" ]; then
|
|
13
|
+
log_warning "_config.yml not found in $target_dir — workflow will still install but may not build."
|
|
14
|
+
fi
|
|
15
|
+
return 0
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
deploy_github_pages_install() {
|
|
19
|
+
local target_dir="$1"
|
|
20
|
+
local repo_root="${REPO_ROOT:-$(deploy_repo_root)}"
|
|
21
|
+
local src="$repo_root/templates/deploy/github-pages/jekyll-gh-pages.yml.template"
|
|
22
|
+
local dest="$target_dir/.github/workflows/jekyll-gh-pages.yml"
|
|
23
|
+
|
|
24
|
+
DEPLOY_SITE_NAME="${DEPLOY_SITE_NAME:-$(basename "$target_dir")}"
|
|
25
|
+
deploy_render_if_absent "$src" "$dest"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
deploy_github_pages_verify() {
|
|
29
|
+
local target_dir="$1"
|
|
30
|
+
local f="$target_dir/.github/workflows/jekyll-gh-pages.yml"
|
|
31
|
+
if [ ! -f "$f" ]; then
|
|
32
|
+
log_error "Expected $f not present"
|
|
33
|
+
return 1
|
|
34
|
+
fi
|
|
35
|
+
grep -q 'peaceiris/actions-gh-pages' "$f" || {
|
|
36
|
+
log_warning "Workflow does not reference peaceiris/actions-gh-pages"
|
|
37
|
+
return 1
|
|
38
|
+
}
|
|
39
|
+
return 0
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
deploy_github_pages_doc_url() {
|
|
43
|
+
echo "https://docs.github.com/pages"
|
|
44
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/lib/install/deploy/registry.sh
|
|
3
|
+
#
|
|
4
|
+
# Discovery + dispatch helpers for deploy target modules.
|
|
5
|
+
#
|
|
6
|
+
# Each module under scripts/lib/install/deploy/<slug>.sh must define the
|
|
7
|
+
# four hooks below (the registry verifies presence after sourcing):
|
|
8
|
+
#
|
|
9
|
+
# deploy_<slug>_check_prereqs <target_dir>
|
|
10
|
+
# Print warnings / errors. Return 0 if safe to proceed.
|
|
11
|
+
#
|
|
12
|
+
# deploy_<slug>_install <target_dir>
|
|
13
|
+
# Render templates / copy files into <target_dir>. Idempotent.
|
|
14
|
+
#
|
|
15
|
+
# deploy_<slug>_verify <target_dir>
|
|
16
|
+
# Confirm the install produced the expected files. Return 0 on OK.
|
|
17
|
+
#
|
|
18
|
+
# deploy_<slug>_doc_url
|
|
19
|
+
# Print a single URL pointing at upstream documentation.
|
|
20
|
+
#
|
|
21
|
+
# A target's display name + one-line description live next to the module
|
|
22
|
+
# in scripts/lib/install/deploy/<slug>.sh as `DEPLOY_<SLUG_UPPER>_TITLE`
|
|
23
|
+
# and `DEPLOY_<SLUG_UPPER>_SUMMARY` (sourced via `eval`).
|
|
24
|
+
#
|
|
25
|
+
# Bash 3.2 compatible. No associative arrays, no mapfile.
|
|
26
|
+
|
|
27
|
+
# Canonical list of supported targets (alphabetical).
|
|
28
|
+
DEPLOY_TARGETS_LIST="azure-swa docker-prod github-pages"
|
|
29
|
+
|
|
30
|
+
# Resolve REPO_ROOT lazily so callers can override via $1.
|
|
31
|
+
deploy_repo_root() {
|
|
32
|
+
if [ -n "${REPO_ROOT:-}" ]; then
|
|
33
|
+
echo "$REPO_ROOT"
|
|
34
|
+
return 0
|
|
35
|
+
fi
|
|
36
|
+
local here
|
|
37
|
+
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
38
|
+
( cd "$here/../../.." && pwd )
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
deploy_targets_dir() {
|
|
42
|
+
local repo_root="${1:-$(deploy_repo_root)}"
|
|
43
|
+
echo "$repo_root/templates/deploy"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
deploy_modules_dir() {
|
|
47
|
+
local repo_root="${1:-$(deploy_repo_root)}"
|
|
48
|
+
echo "$repo_root/scripts/lib/install/deploy"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
deploy_target_known() {
|
|
52
|
+
local slug="$1" t
|
|
53
|
+
for t in $DEPLOY_TARGETS_LIST; do
|
|
54
|
+
[ "$t" = "$slug" ] && return 0
|
|
55
|
+
done
|
|
56
|
+
return 1
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Convert kebab-case to function-name fragment: github-pages -> github_pages
|
|
60
|
+
deploy_slug_fn() {
|
|
61
|
+
echo "$1" | tr '-' '_'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Convert kebab-case to upper var fragment: github-pages -> GITHUB_PAGES
|
|
65
|
+
deploy_slug_var() {
|
|
66
|
+
echo "$1" | tr '[:lower:]-' '[:upper:]_'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Source a target module (idempotent). Sets DEPLOY_LAST_LOADED on success.
|
|
70
|
+
deploy_load_module() {
|
|
71
|
+
local slug="$1"
|
|
72
|
+
local repo_root="${2:-$(deploy_repo_root)}"
|
|
73
|
+
local module="$(deploy_modules_dir "$repo_root")/${slug}.sh"
|
|
74
|
+
if [ ! -f "$module" ]; then
|
|
75
|
+
log_error "Deploy module not found: $module"
|
|
76
|
+
return 1
|
|
77
|
+
fi
|
|
78
|
+
# shellcheck disable=SC1090
|
|
79
|
+
. "$module"
|
|
80
|
+
DEPLOY_LAST_LOADED="$slug"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Print one-line summary for `install list-targets`.
|
|
84
|
+
deploy_print_summary() {
|
|
85
|
+
local slug="$1"
|
|
86
|
+
local repo_root="${2:-$(deploy_repo_root)}"
|
|
87
|
+
deploy_load_module "$slug" "$repo_root" >/dev/null 2>&1 || return 0
|
|
88
|
+
local var_frag title summary
|
|
89
|
+
var_frag="$(deploy_slug_var "$slug")"
|
|
90
|
+
eval "title=\${DEPLOY_${var_frag}_TITLE:-$slug}"
|
|
91
|
+
eval "summary=\${DEPLOY_${var_frag}_SUMMARY:-(no summary)}"
|
|
92
|
+
printf ' %-13s %s\n' "$slug" "$title"
|
|
93
|
+
printf ' %s\n' "$summary"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# Run the four hooks for a single target.
|
|
97
|
+
deploy_run_target() {
|
|
98
|
+
local slug="$1" target_dir="$2"
|
|
99
|
+
local repo_root="${3:-$(deploy_repo_root)}"
|
|
100
|
+
local fn
|
|
101
|
+
|
|
102
|
+
if ! deploy_target_known "$slug"; then
|
|
103
|
+
log_error "Unknown deploy target: $slug"
|
|
104
|
+
log_info "Available targets: $DEPLOY_TARGETS_LIST"
|
|
105
|
+
return 1
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
if [ ! -d "$target_dir" ]; then
|
|
109
|
+
log_error "Target directory does not exist: $target_dir"
|
|
110
|
+
return 1
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
deploy_load_module "$slug" "$repo_root" || return 1
|
|
114
|
+
fn="$(deploy_slug_fn "$slug")"
|
|
115
|
+
|
|
116
|
+
log_info "▶ Configuring deploy target: $slug"
|
|
117
|
+
|
|
118
|
+
if ! "deploy_${fn}_check_prereqs" "$target_dir"; then
|
|
119
|
+
log_error "Prerequisite check failed for $slug"
|
|
120
|
+
return 1
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
if ! "deploy_${fn}_install" "$target_dir"; then
|
|
124
|
+
log_error "Install step failed for $slug"
|
|
125
|
+
return 1
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
if ! "deploy_${fn}_verify" "$target_dir"; then
|
|
129
|
+
log_warning "Verification reported issues for $slug (manual review recommended)"
|
|
130
|
+
else
|
|
131
|
+
log_success "Deploy target $slug installed successfully"
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
local url
|
|
135
|
+
url="$("deploy_${fn}_doc_url" 2>/dev/null || true)"
|
|
136
|
+
[ -n "$url" ] && log_info "Documentation: $url"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Lightweight renderer used by all deploy modules. Operates on a small,
|
|
140
|
+
# explicit allow-list of placeholders so modules don't need to set up
|
|
141
|
+
# install.sh's full global environment.
|
|
142
|
+
#
|
|
143
|
+
# Usage: deploy_render <template_file> <output_file>
|
|
144
|
+
# Variables consulted (with defaults):
|
|
145
|
+
# DEPLOY_RUBY_VERSION (default 3.3)
|
|
146
|
+
# DEPLOY_DEFAULT_BRANCH (default main)
|
|
147
|
+
# DEPLOY_GITHUB_USER (default $GITHUB_USER, then $USER, then "me")
|
|
148
|
+
# DEPLOY_SITE_NAME (default basename of target dir, then "site")
|
|
149
|
+
deploy_render() {
|
|
150
|
+
local src="$1" dest="$2"
|
|
151
|
+
[ -f "$src" ] || { log_error "Template not found: $src"; return 1; }
|
|
152
|
+
|
|
153
|
+
local ruby_v branch user site
|
|
154
|
+
ruby_v="${DEPLOY_RUBY_VERSION:-3.3}"
|
|
155
|
+
branch="${DEPLOY_DEFAULT_BRANCH:-main}"
|
|
156
|
+
user="${DEPLOY_GITHUB_USER:-${GITHUB_USER:-${USER:-me}}}"
|
|
157
|
+
site="${DEPLOY_SITE_NAME:-site}"
|
|
158
|
+
|
|
159
|
+
mkdir -p "$(dirname "$dest")"
|
|
160
|
+
sed \
|
|
161
|
+
-e "s|{{RUBY_VERSION}}|${ruby_v}|g" \
|
|
162
|
+
-e "s|{{DEFAULT_BRANCH}}|${branch}|g" \
|
|
163
|
+
-e "s|{{GITHUB_USER}}|${user}|g" \
|
|
164
|
+
-e "s|{{SITE_NAME}}|${site}|g" \
|
|
165
|
+
"$src" > "$dest"
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# Copy a file verbatim (no rendering). Skips when destination exists
|
|
169
|
+
# unless DEPLOY_FORCE=1.
|
|
170
|
+
deploy_copy() {
|
|
171
|
+
local src="$1" dest="$2"
|
|
172
|
+
[ -f "$src" ] || { log_error "Source not found: $src"; return 1; }
|
|
173
|
+
if [ -f "$dest" ] && [ "${DEPLOY_FORCE:-0}" != "1" ]; then
|
|
174
|
+
log_warning "Exists, skipping: ${dest}"
|
|
175
|
+
return 0
|
|
176
|
+
fi
|
|
177
|
+
mkdir -p "$(dirname "$dest")"
|
|
178
|
+
cp "$src" "$dest"
|
|
179
|
+
log_info "Wrote: ${dest}"
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# Same as deploy_copy but for rendered templates (logs accordingly).
|
|
183
|
+
deploy_render_if_absent() {
|
|
184
|
+
local src="$1" dest="$2"
|
|
185
|
+
if [ -f "$dest" ] && [ "${DEPLOY_FORCE:-0}" != "1" ]; then
|
|
186
|
+
log_warning "Exists, skipping: ${dest}"
|
|
187
|
+
return 0
|
|
188
|
+
fi
|
|
189
|
+
deploy_render "$src" "$dest" && log_info "Rendered: ${dest}"
|
|
190
|
+
}
|