diff --git a/README.md b/README.md index 8c90540..27d8c9b 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Manual commands: Installer also links Claude-compatible paths back to Mosaic canonicals: - `~/.claude/agent-guides` -> `~/.mosaic/guides` -- `~/.claude/scripts/{git,codex,bootstrap}` -> `~/.mosaic/rails/...` +- `~/.claude/scripts/{git,codex,bootstrap,cicd}` -> `~/.mosaic/rails/...` - `~/.claude/templates` -> `~/.mosaic/templates/agent` - `~/.claude/presets/{domains,tech-stacks,workflows}` -> `~/.mosaic/profiles/...` - `~/.claude/presets/*.json` runtime overlays -> `~/.mosaic/runtime/claude/settings-overlays/` diff --git a/bin/mosaic-link-runtime-assets b/bin/mosaic-link-runtime-assets index 257183e..f0dfd9e 100755 --- a/bin/mosaic-link-runtime-assets +++ b/bin/mosaic-link-runtime-assets @@ -47,6 +47,7 @@ link_tree_files "$MOSAIC_HOME/guides" "$HOME/.claude/agent-guides" link_tree_files "$MOSAIC_HOME/rails/git" "$HOME/.claude/scripts/git" link_tree_files "$MOSAIC_HOME/rails/codex" "$HOME/.claude/scripts/codex" link_tree_files "$MOSAIC_HOME/rails/bootstrap" "$HOME/.claude/scripts/bootstrap" +link_tree_files "$MOSAIC_HOME/rails/cicd" "$HOME/.claude/scripts/cicd" link_tree_files "$MOSAIC_HOME/templates/agent" "$HOME/.claude/templates" link_tree_files "$MOSAIC_HOME/profiles/domains" "$HOME/.claude/presets/domains" link_tree_files "$MOSAIC_HOME/profiles/tech-stacks" "$HOME/.claude/presets/tech-stacks" diff --git a/rails/cicd/generate-docker-steps.sh b/rails/cicd/generate-docker-steps.sh new file mode 100755 index 0000000..f56740d --- /dev/null +++ b/rails/cicd/generate-docker-steps.sh @@ -0,0 +1,379 @@ +#!/bin/bash +# generate-docker-steps.sh - Generate Woodpecker CI pipeline steps for Docker build/push/link +# +# Outputs valid Woodpecker YAML for: +# - Kaniko Docker build & push steps (one per service) +# - Gitea package linking step +# - npm package publish step (optional) +# +# Usage: +# generate-docker-steps.sh \ +# --registry git.uscllc.com \ +# --org usc \ +# --repo uconnect \ +# --service backend-api:src/backend-api/Dockerfile \ +# --service web-portal:src/web-portal/Dockerfile \ +# --branches main,develop \ +# [--build-arg backend-api:NEXT_PUBLIC_API_URL=https://api.example.com] \ +# [--npm-package @uconnect/schemas:src/schemas] \ +# [--npm-registry https://git.uscllc.com/api/packages/usc/npm/] \ +# [--depends-on build] + +set -e + +# Defaults +REGISTRY="" +ORG="" +REPO="" +BRANCHES="main,develop" +DEPENDS_ON="build" +declare -a SERVICES=() +declare -a BUILD_ARGS=() +declare -a NPM_PACKAGES=() +NPM_REGISTRY="" + +show_help() { + cat <<'EOF' +Usage: generate-docker-steps.sh [OPTIONS] + +Generate Woodpecker CI YAML for Docker build/push/link via Kaniko. + +Required: + --registry Gitea hostname (e.g., git.uscllc.com) + --org Gitea organization (e.g., usc) + --repo Repository name (e.g., uconnect) + --service Service to build (repeatable) + +Optional: + --branches Comma-separated branches (default: main,develop) + --depends-on Step name Docker builds depend on (default: build) + --build-arg Build arg for a service (repeatable) + --npm-package npm package to publish (repeatable) + --npm-registry npm registry URL for publishing + --kaniko-setup-only Output just the kaniko_setup YAML anchor + -h, --help Show this help + +Examples: + # Mosaic Stack pattern + generate-docker-steps.sh \ + --registry git.mosaicstack.dev --org mosaic --repo stack \ + --service stack-api:apps/api/Dockerfile \ + --service stack-web:apps/web/Dockerfile \ + --build-arg stack-web:NEXT_PUBLIC_API_URL=https://api.mosaicstack.dev + + # U-Connect pattern + generate-docker-steps.sh \ + --registry git.uscllc.com --org usc --repo uconnect \ + --service uconnect-backend-api:src/backend-api/Dockerfile \ + --service uconnect-web-portal:src/web-portal/Dockerfile \ + --service uconnect-ingest-api:src/ingest-api/Dockerfile \ + --branches main,develop +EOF + exit 0 +} + +KANIKO_SETUP_ONLY=false + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --registry) REGISTRY="$2"; shift 2 ;; + --org) ORG="$2"; shift 2 ;; + --repo) REPO="$2"; shift 2 ;; + --service) SERVICES+=("$2"); shift 2 ;; + --branches) BRANCHES="$2"; shift 2 ;; + --depends-on) DEPENDS_ON="$2"; shift 2 ;; + --build-arg) BUILD_ARGS+=("$2"); shift 2 ;; + --npm-package) NPM_PACKAGES+=("$2"); shift 2 ;; + --npm-registry) NPM_REGISTRY="$2"; shift 2 ;; + --kaniko-setup-only) KANIKO_SETUP_ONLY=true; shift ;; + -h|--help) show_help ;; + *) echo "Unknown option: $1" >&2; exit 1 ;; + esac +done + +# Validate required args +if [[ -z "$REGISTRY" ]]; then echo "Error: --registry is required" >&2; exit 1; fi +if [[ -z "$ORG" ]]; then echo "Error: --org is required" >&2; exit 1; fi +if [[ -z "$REPO" ]]; then echo "Error: --repo is required" >&2; exit 1; fi +if [[ ${#SERVICES[@]} -eq 0 && "$KANIKO_SETUP_ONLY" != true ]]; then + echo "Error: at least one --service is required" >&2; exit 1 +fi + +# Parse branches into YAML list +IFS=',' read -ra BRANCH_LIST <<< "$BRANCHES" +BRANCH_YAML="[" +for i in "${!BRANCH_LIST[@]}"; do + if [[ $i -gt 0 ]]; then BRANCH_YAML="$BRANCH_YAML, "; fi + BRANCH_YAML="$BRANCH_YAML${BRANCH_LIST[$i]}" +done +BRANCH_YAML="$BRANCH_YAML]" + +# Helper: get build args for a specific service +get_build_args_for_service() { + local svc_name="$1" + local args=() + for ba in "${BUILD_ARGS[@]}"; do + local ba_svc="${ba%%:*}" + local ba_val="${ba#*:}" + if [[ "$ba_svc" == "$svc_name" ]]; then + args+=("$ba_val") + fi + done + echo "${args[@]}" +} + +# Helper: determine Dockerfile context from path +# e.g., apps/api/Dockerfile -> . (monorepo root) +# docker/postgres/Dockerfile -> docker/postgres +get_context() { + local dockerfile="$1" + local dir + dir=$(dirname "$dockerfile") + # If Dockerfile is at project root or in a top-level apps/src dir, use "." + if [[ "$dir" == "." || "$dir" == apps/* || "$dir" == src/* || "$dir" == packages/* ]]; then + echo "." + else + echo "$dir" + fi +} + +# ============================================================ +# Output: YAML anchor for kaniko setup +# ============================================================ +emit_kaniko_anchor() { + cat < /kaniko/.docker/config.json +EOF +} + +if [[ "$KANIKO_SETUP_ONLY" == true ]]; then + emit_kaniko_anchor + exit 0 +fi + +# ============================================================ +# Output: Header comment +# ============================================================ +cat < .npmrc +EOF + + # Detect scope from first package + FIRST_PKG="${NPM_PACKAGES[0]}" + PKG_NAME="${FIRST_PKG%%:*}" + SCOPE="${PKG_NAME%%/*}" + if [[ "$SCOPE" == @* ]]; then + echo " echo \"${SCOPE}:registry=${NPM_REGISTRY}\" >> .npmrc" + fi + + for pkg in "${NPM_PACKAGES[@]}"; do + PKG_NAME="${pkg%%:*}" + PKG_PATH="${pkg#*:}" + cat </dev/null || echo "0.0.0") + if [ "\$\$CURRENT" = "\$\$PUBLISHED" ]; then + echo "${PKG_NAME}@\$\$CURRENT already published, skipping" + else + echo "Publishing ${PKG_NAME}@\$\$CURRENT (was \$\$PUBLISHED)" + npm publish -w ${PKG_NAME} + fi +EOF + done + + cat <