Rename the `rails/` directory to `tools/` for agent discoverability — agents frequently failed to locate helper scripts due to the non-intuitive directory name. Add backward-compat symlink `rails/ → tools/`. New tool suites: - Authentik: auth-token, user-list, user-create, group-list, app-list, flow-list, admin-status (8 scripts) - Coolify: team-list, project-list, service-list, service-status, deploy, env-set (7 scripts) - Woodpecker: pipeline-list, pipeline-status, pipeline-trigger (3 stubs) - GLPI: session-init, computer-list, ticket-list, ticket-create, user-list (6 scripts) - Health: stack-health.sh — stack-wide connectivity check Infrastructure: - Shared credential loader at tools/_lib/credentials.sh - install.sh creates symlink + chmod on tool scripts - All ~253 rails/ path references updated across 68+ files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
380 lines
11 KiB
Bash
Executable File
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
|