fix: Vault-gated deployment promotion via Woodpecker environments (#755)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
04696b35a9
commit
3372da594b
3 changed files with 175 additions and 0 deletions
97
bin/disinto
97
bin/disinto
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue