Merge pull request 'fix: Vault-gated deployment promotion via Woodpecker environments (#755)' (#766) from fix/issue-755 into main

This commit is contained in:
johba 2026-03-26 18:28:57 +01:00
commit f32707ba65
3 changed files with 175 additions and 0 deletions

View file

@ -260,6 +260,20 @@ services:
networks: networks:
- disinto-net - disinto-net
# Staging deployment slot — activated by Woodpecker staging pipeline (#755).
# Profile-gated: only starts when explicitly targeted by deploy commands.
# Customize image/ports/volumes for your project after init.
staging:
image: alpine:3
profiles: ["staging"]
security_opt:
- apparmor=unconfined
environment:
DEPLOY_ENV: staging
networks:
- disinto-net
command: ["echo", "staging slot — replace with project image"]
volumes: volumes:
forgejo-data: forgejo-data:
woodpecker-data: woodpecker-data:
@ -307,6 +321,86 @@ generate_agent_docker() {
fi fi
} }
# Generate template .woodpecker/ deployment pipeline configs in a project repo.
# Creates staging.yml and production.yml alongside the project's existing CI config.
# These pipelines trigger on Woodpecker's deployment event with environment filters.
generate_deploy_pipelines() {
local repo_root="$1" project_name="$2"
local wp_dir="${repo_root}/.woodpecker"
mkdir -p "$wp_dir"
# Skip if deploy pipelines already exist
if [ -f "${wp_dir}/staging.yml" ] && [ -f "${wp_dir}/production.yml" ]; then
echo "Deploy: .woodpecker/{staging,production}.yml (already exist)"
return
fi
if [ ! -f "${wp_dir}/staging.yml" ]; then
cat > "${wp_dir}/staging.yml" <<'STAGINGEOF'
# .woodpecker/staging.yml — Staging deployment pipeline
# Triggered by vault-runner via Woodpecker promote API.
# Human approves promotion in vault → vault-runner calls promote → this runs.
when:
event: deployment
environment: staging
steps:
- name: deploy-staging
image: docker:27
commands:
- echo "Deploying to staging environment..."
- echo "Pipeline ${CI_PIPELINE_NUMBER} promoted from CI #${CI_PIPELINE_PARENT}"
# Pull the image built by CI and deploy to staging
# Customize these commands for your project:
# - docker compose -f docker-compose.yml --profile staging up -d
- echo "Staging deployment complete"
- name: verify-staging
image: alpine:3
commands:
- echo "Verifying staging deployment..."
# Add health checks, smoke tests, or integration tests here:
# - curl -sf http://staging:8080/health || exit 1
- echo "Staging verification complete"
STAGINGEOF
echo "Created: ${wp_dir}/staging.yml"
fi
if [ ! -f "${wp_dir}/production.yml" ]; then
cat > "${wp_dir}/production.yml" <<'PRODUCTIONEOF'
# .woodpecker/production.yml — Production deployment pipeline
# Triggered by vault-runner via Woodpecker promote API.
# Human approves promotion in vault → vault-runner calls promote → this runs.
when:
event: deployment
environment: production
steps:
- name: deploy-production
image: docker:27
commands:
- echo "Deploying to production environment..."
- echo "Pipeline ${CI_PIPELINE_NUMBER} promoted from staging"
# Pull the verified image and deploy to production
# Customize these commands for your project:
# - docker compose -f docker-compose.yml up -d
- echo "Production deployment complete"
- name: verify-production
image: alpine:3
commands:
- echo "Verifying production deployment..."
# Add production health checks here:
# - curl -sf http://production:8080/health || exit 1
- echo "Production verification complete"
PRODUCTIONEOF
echo "Created: ${wp_dir}/production.yml"
fi
}
# Check whether compose mode is active (docker-compose.yml exists). # Check whether compose mode is active (docker-compose.yml exists).
is_compose_mode() { is_compose_mode() {
[ -f "${FACTORY_ROOT}/docker-compose.yml" ] [ -f "${FACTORY_ROOT}/docker-compose.yml" ]
@ -1233,6 +1327,9 @@ p.write_text(text)
# Generate VISION.md template # Generate VISION.md template
generate_vision "$repo_root" "$project_name" generate_vision "$repo_root" "$project_name"
# Generate template deployment pipeline configs in project repo
generate_deploy_pipelines "$repo_root" "$project_name"
# Install cron jobs # Install cron jobs
install_cron "$project_name" "$toml_path" "$auto_yes" "$bare" install_cron "$project_name" "$toml_path" "$auto_yes" "$bare"

View file

@ -235,3 +235,35 @@ classify_pipeline_failure() {
echo "code" echo "code"
return 1 return 1
} }
# ci_promote <repo_id> <pipeline_number> <environment>
# Calls the Woodpecker promote API to trigger a deployment pipeline.
# The promote endpoint creates a new pipeline with event=deployment and
# deploy_to=<environment>, which fires pipelines filtered on that environment.
# Requires: WOODPECKER_TOKEN, WOODPECKER_SERVER (from env.sh)
# Returns 0 on success, 1 on failure. Prints the new pipeline number on success.
ci_promote() {
local repo_id="$1" pipeline_num="$2" environment="$3"
if [ -z "$repo_id" ] || [ -z "$pipeline_num" ] || [ -z "$environment" ]; then
echo "Usage: ci_promote <repo_id> <pipeline_number> <environment>" >&2
return 1
fi
local resp new_num
resp=$(woodpecker_api "/repos/${repo_id}/pipelines/${pipeline_num}" \
-X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "event=deployment&deploy_to=${environment}" 2>/dev/null) || {
echo "ERROR: promote API call failed" >&2
return 1
}
new_num=$(printf '%s' "$resp" | jq -r '.number // empty' 2>/dev/null)
if [ -z "$new_num" ]; then
echo "ERROR: promote returned no pipeline number" >&2
return 1
fi
echo "$new_num"
}

View file

@ -71,6 +71,52 @@ case "$ACTION_TYPE" in
fi fi
;; ;;
promote)
# Promote a Woodpecker pipeline to a deployment environment (staging/production).
# Payload: {"repo_id": N, "pipeline": N, "environment": "staging"|"production"}
PROMOTE_REPO_ID=$(echo "$PAYLOAD" | jq -r '.repo_id // ""')
PROMOTE_PIPELINE=$(echo "$PAYLOAD" | jq -r '.pipeline // ""')
PROMOTE_ENV=$(echo "$PAYLOAD" | jq -r '.environment // ""')
if [ -z "$PROMOTE_REPO_ID" ] || [ -z "$PROMOTE_PIPELINE" ] || [ -z "$PROMOTE_ENV" ]; then
log "ERROR: ${ACTION_ID} promote missing repo_id, pipeline, or environment"
FIRE_EXIT=1
else
# Validate environment is staging or production
case "$PROMOTE_ENV" in
staging|production) ;;
*)
log "ERROR: ${ACTION_ID} promote invalid environment '${PROMOTE_ENV}' (must be staging or production)"
FIRE_EXIT=1
;;
esac
if [ "$FIRE_EXIT" -eq 0 ]; then
WP_SERVER="${WOODPECKER_SERVER:-http://woodpecker:8000}"
WP_TOKEN="${WOODPECKER_TOKEN:-}"
if [ -z "$WP_TOKEN" ]; then
log "ERROR: ${ACTION_ID} promote requires WOODPECKER_TOKEN"
FIRE_EXIT=1
else
PROMOTE_RESP=$(curl -sf -X POST \
-H "Authorization: Bearer ${WP_TOKEN}" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "event=deployment&deploy_to=${PROMOTE_ENV}" \
"${WP_SERVER}/api/repos/${PROMOTE_REPO_ID}/pipelines/${PROMOTE_PIPELINE}" 2>/dev/null) || PROMOTE_RESP=""
NEW_PIPELINE=$(printf '%s' "$PROMOTE_RESP" | jq -r '.number // empty' 2>/dev/null)
if [ -n "$NEW_PIPELINE" ]; then
log "${ACTION_ID}: promoted pipeline ${PROMOTE_PIPELINE} to ${PROMOTE_ENV} -> new pipeline #${NEW_PIPELINE}"
else
log "ERROR: ${ACTION_ID} promote API failed (repo_id=${PROMOTE_REPO_ID} pipeline=${PROMOTE_PIPELINE} env=${PROMOTE_ENV})"
FIRE_EXIT=1
fi
fi
fi
fi
;;
blog-post|social-post|email-blast|pricing-change|dns-change|stripe-charge) blog-post|social-post|email-blast|pricing-change|dns-change|stripe-charge)
HANDLER="${VAULT_DIR}/handlers/${ACTION_TYPE}.sh" HANDLER="${VAULT_DIR}/handlers/${ACTION_TYPE}.sh"
if [ -x "$HANDLER" ]; then if [ -x "$HANDLER" ]; then