Files
bootstrap/tools/cicd/generate-docker-steps.sh
2026-02-22 17:52:23 +00:00

380 lines
11 KiB
Bash
Executable File

#!/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