add cicd rail and link generate-docker-steps via mosaic
This commit is contained in:
@@ -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/`
|
||||
|
||||
@@ -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"
|
||||
|
||||
379
rails/cicd/generate-docker-steps.sh
Executable file
379
rails/cicd/generate-docker-steps.sh
Executable file
@@ -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 <host> Gitea hostname (e.g., git.uscllc.com)
|
||||
--org <name> Gitea organization (e.g., usc)
|
||||
--repo <name> Repository name (e.g., uconnect)
|
||||
--service <name:dockerfile> Service to build (repeatable)
|
||||
|
||||
Optional:
|
||||
--branches <list> Comma-separated branches (default: main,develop)
|
||||
--depends-on <step> Step name Docker builds depend on (default: build)
|
||||
--build-arg <service:KEY=VAL> Build arg for a service (repeatable)
|
||||
--npm-package <pkg:path> npm package to publish (repeatable)
|
||||
--npm-registry <url> 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 <<EOF
|
||||
# Kaniko base command setup
|
||||
- &kaniko_setup |
|
||||
mkdir -p /kaniko/.docker
|
||||
echo "{\\"auths\\":{\\"${REGISTRY}\\":{\\"username\\":\\"\$GITEA_USER\\",\\"password\\":\\"\$GITEA_TOKEN\\"}}}" > /kaniko/.docker/config.json
|
||||
EOF
|
||||
}
|
||||
|
||||
if [[ "$KANIKO_SETUP_ONLY" == true ]]; then
|
||||
emit_kaniko_anchor
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# Output: Header comment
|
||||
# ============================================================
|
||||
cat <<EOF
|
||||
|
||||
# ======================
|
||||
# Docker Build & Push (${BRANCHES} only)
|
||||
# ======================
|
||||
# Generated by: generate-docker-steps.sh
|
||||
# Registry: ${REGISTRY}/${ORG}
|
||||
# Requires secrets: gitea_username, gitea_token
|
||||
#
|
||||
# Tagging Strategy:
|
||||
# - Always: commit SHA (first 8 chars)
|
||||
EOF
|
||||
|
||||
for b in "${BRANCH_LIST[@]}"; do
|
||||
case "$b" in
|
||||
main) echo " # - main branch: 'latest'" ;;
|
||||
develop) echo " # - develop branch: 'dev'" ;;
|
||||
*) echo " # - ${b} branch: '${b}'" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo " # - git tags: version tag (e.g., v1.0.0)"
|
||||
echo ""
|
||||
|
||||
# ============================================================
|
||||
# Output: Kaniko build step for each service
|
||||
# ============================================================
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
SVC_NAME="${svc%%:*}"
|
||||
DOCKERFILE="${svc#*:}"
|
||||
|
||||
CONTEXT=$(get_context "$DOCKERFILE")
|
||||
SVC_BUILD_ARGS=$(get_build_args_for_service "$SVC_NAME")
|
||||
|
||||
# Build the kaniko command with build args
|
||||
KANIKO_EXTRA=""
|
||||
if [[ -n "$SVC_BUILD_ARGS" ]]; then
|
||||
for arg in $SVC_BUILD_ARGS; do
|
||||
KANIKO_EXTRA="$KANIKO_EXTRA --build-arg ${arg}"
|
||||
done
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
# Build and push ${SVC_NAME}
|
||||
docker-build-${SVC_NAME}:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_username
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
CI_COMMIT_BRANCH: \${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: \${CI_COMMIT_TAG}
|
||||
CI_COMMIT_SHA: \${CI_COMMIT_SHA}
|
||||
commands:
|
||||
- *kaniko_setup
|
||||
- |
|
||||
DESTINATIONS="--destination ${REGISTRY}/${ORG}/${SVC_NAME}:\${CI_COMMIT_SHA:0:8}"
|
||||
EOF
|
||||
|
||||
# Branch-specific tags
|
||||
for b in "${BRANCH_LIST[@]}"; do
|
||||
case "$b" in
|
||||
main)
|
||||
cat <<EOF
|
||||
if [ "\$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
DESTINATIONS="\$DESTINATIONS --destination ${REGISTRY}/${ORG}/${SVC_NAME}:latest"
|
||||
fi
|
||||
EOF
|
||||
;;
|
||||
develop)
|
||||
cat <<EOF
|
||||
if [ "\$CI_COMMIT_BRANCH" = "develop" ]; then
|
||||
DESTINATIONS="\$DESTINATIONS --destination ${REGISTRY}/${ORG}/${SVC_NAME}:dev"
|
||||
fi
|
||||
EOF
|
||||
;;
|
||||
*)
|
||||
cat <<EOF
|
||||
if [ "\$CI_COMMIT_BRANCH" = "${b}" ]; then
|
||||
DESTINATIONS="\$DESTINATIONS --destination ${REGISTRY}/${ORG}/${SVC_NAME}:${b}"
|
||||
fi
|
||||
EOF
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Version tag
|
||||
cat <<EOF
|
||||
if [ -n "\$CI_COMMIT_TAG" ]; then
|
||||
DESTINATIONS="\$DESTINATIONS --destination ${REGISTRY}/${ORG}/${SVC_NAME}:\$CI_COMMIT_TAG"
|
||||
fi
|
||||
/kaniko/executor --context ${CONTEXT} --dockerfile ${DOCKERFILE}${KANIKO_EXTRA} \$DESTINATIONS
|
||||
when:
|
||||
- branch: ${BRANCH_YAML}
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- ${DEPENDS_ON}
|
||||
|
||||
EOF
|
||||
done
|
||||
|
||||
# ============================================================
|
||||
# Output: Package linking step
|
||||
# ============================================================
|
||||
cat <<EOF
|
||||
# ======================
|
||||
# Link Packages to Repository
|
||||
# ======================
|
||||
link-packages:
|
||||
image: alpine:3
|
||||
environment:
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- echo "Waiting 10 seconds for packages to be indexed in registry..."
|
||||
- sleep 10
|
||||
- |
|
||||
set -e
|
||||
link_package() {
|
||||
PKG="\$\$1"
|
||||
echo "Linking \$\$PKG..."
|
||||
|
||||
for attempt in 1 2 3; do
|
||||
STATUS=\$\$(curl -s -o /tmp/link-response.txt -w "%{http_code}" -X POST \\
|
||||
-H "Authorization: token \$\$GITEA_TOKEN" \\
|
||||
"https://${REGISTRY}/api/v1/packages/${ORG}/container/\$\$PKG/-/link/${REPO}")
|
||||
|
||||
if [ "\$\$STATUS" = "201" ] || [ "\$\$STATUS" = "204" ]; then
|
||||
echo " Linked \$\$PKG"
|
||||
return 0
|
||||
elif [ "\$\$STATUS" = "400" ]; then
|
||||
echo " \$\$PKG already linked"
|
||||
return 0
|
||||
elif [ "\$\$STATUS" = "404" ] && [ \$\$attempt -lt 3 ]; then
|
||||
echo " \$\$PKG not found yet, waiting 5s (attempt \$\$attempt/3)..."
|
||||
sleep 5
|
||||
else
|
||||
echo " FAILED: \$\$PKG status \$\$STATUS"
|
||||
cat /tmp/link-response.txt
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
EOF
|
||||
|
||||
# List all services to link
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
SVC_NAME="${svc%%:*}"
|
||||
echo " link_package \"${SVC_NAME}\""
|
||||
done
|
||||
|
||||
# Close the link step
|
||||
cat <<EOF
|
||||
when:
|
||||
- branch: ${BRANCH_YAML}
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
EOF
|
||||
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
SVC_NAME="${svc%%:*}"
|
||||
echo " - docker-build-${SVC_NAME}"
|
||||
done
|
||||
|
||||
echo ""
|
||||
|
||||
# ============================================================
|
||||
# Output: npm publish step (if requested)
|
||||
# ============================================================
|
||||
if [[ ${#NPM_PACKAGES[@]} -gt 0 && -n "$NPM_REGISTRY" ]]; then
|
||||
cat <<EOF
|
||||
# ======================
|
||||
# Publish npm Packages
|
||||
# ======================
|
||||
publish-packages:
|
||||
image: node:20-alpine
|
||||
environment:
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
commands:
|
||||
- |
|
||||
echo "//${NPM_REGISTRY#https://}:_authToken=\$\$GITEA_TOKEN" > .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 <<EOF
|
||||
- |
|
||||
CURRENT=\$\$(node -p "require('./${PKG_PATH}/package.json').version")
|
||||
PUBLISHED=\$\$(npm view ${PKG_NAME} version 2>/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 <<EOF
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- ${DEPENDS_ON}
|
||||
EOF
|
||||
fi
|
||||
Reference in New Issue
Block a user