Deploy / validate-dispatch (push) Successful in 43s
Deploy / scheduled-rebuild-dev (push) Skipped
Deploy / scheduled-rebuild-master (push) Skipped
Deploy / rollback-dev (push) Skipped
Deploy / rollback-master (push) Skipped
Deploy / changes (push) Successful in 13s
Deploy / secret-scan (push) Successful in 9s
Deploy / app-quality (push) Successful in 11m21s
Deploy / security-deps (push) Successful in 14s
Deploy / context (push) Failing after 1s
Deploy / app-image (push) Skipped
Deploy / deploy-dev (push) Skipped
Deploy / deploy-master (push) Skipped
628 lines
23 KiB
YAML
628 lines
23 KiB
YAML
name: Deploy
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: website-starter-deploy-${{ github.event_name == 'schedule' && 'scheduled' || github.event.inputs.rollback_target || github.ref_name }}
|
|
cancel-in-progress: false
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- dev
|
|
- master
|
|
workflow_dispatch:
|
|
inputs:
|
|
rollback_target:
|
|
description: "Rollback target: dev or master. Leave empty for a normal manual deploy."
|
|
required: false
|
|
app_image_tag:
|
|
description: "Next app image tag for rollback."
|
|
required: false
|
|
schedule:
|
|
- cron: "17 3 * * 1"
|
|
|
|
env:
|
|
DOCKER_BUILDKIT: 1
|
|
NODE_VERSION_FILE: .node-version
|
|
TRIVY_IMAGE: aquasec/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969
|
|
REGISTRY: ${{ vars.CONTAINER_REGISTRY }}
|
|
APP_IMAGE_REPOSITORY: ${{ vars.APP_IMAGE_REPOSITORY }}
|
|
APP_NAME: ${{ vars.APP_NAME }}
|
|
COMPANY_NAME: ${{ vars.COMPANY_NAME }}
|
|
CONTACT_EMAIL: ${{ vars.CONTACT_EMAIL }}
|
|
CONTACT_PHONE: ${{ vars.CONTACT_PHONE }}
|
|
CONTACT_ADDRESS: ${{ vars.CONTACT_ADDRESS }}
|
|
|
|
jobs:
|
|
validate-dispatch:
|
|
runs-on: ${{ vars.CI_RUNNER || 'ubuntu-latest' }}
|
|
timeout-minutes: 5
|
|
steps:
|
|
- name: Validate workflow dispatch inputs
|
|
env:
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
REF_NAME: ${{ github.ref_name }}
|
|
ROLLBACK_TARGET: ${{ github.event.inputs.rollback_target }}
|
|
APP_IMAGE_TAG: ${{ github.event.inputs.app_image_tag }}
|
|
run: |
|
|
set -eu
|
|
|
|
if [ "$EVENT_NAME" != "workflow_dispatch" ]; then
|
|
exit 0
|
|
fi
|
|
|
|
fail() {
|
|
echo "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
validate_tag() {
|
|
key="$1"
|
|
value="$2"
|
|
case "$value" in
|
|
[A-Za-z0-9_]* ) ;;
|
|
*) fail "Invalid Docker image tag for $key." ;;
|
|
esac
|
|
case "$value" in
|
|
*[!A-Za-z0-9_.-]* ) fail "Invalid Docker image tag for $key." ;;
|
|
esac
|
|
if [ "${#value}" -gt 128 ]; then
|
|
fail "Docker image tag for $key exceeds 128 characters."
|
|
fi
|
|
}
|
|
|
|
case "$REF_NAME" in
|
|
dev|master) ;;
|
|
*) fail "Manual deployments must be dispatched from dev or master." ;;
|
|
esac
|
|
|
|
if [ -z "$ROLLBACK_TARGET" ]; then
|
|
if [ -n "$APP_IMAGE_TAG" ]; then
|
|
fail "Image tags require rollback_target."
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
case "$ROLLBACK_TARGET" in
|
|
dev|master) ;;
|
|
*) fail "rollback_target must be dev or master." ;;
|
|
esac
|
|
if [ "$ROLLBACK_TARGET" != "$REF_NAME" ]; then
|
|
fail "rollback_target must match the dispatched branch."
|
|
fi
|
|
|
|
validate_tag APP_IMAGE_TAG "$APP_IMAGE_TAG"
|
|
|
|
changes:
|
|
if: github.event_name != 'schedule' && (github.event_name != 'workflow_dispatch' || github.event.inputs.rollback_target == '')
|
|
needs: [validate-dispatch]
|
|
runs-on: ${{ vars.CI_RUNNER || 'ubuntu-latest' }}
|
|
timeout-minutes: 5
|
|
outputs:
|
|
deploy: ${{ steps.detect.outputs.deploy }}
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
|
with:
|
|
lfs: true
|
|
fetch-depth: 0
|
|
|
|
- name: Detect changed areas
|
|
id: detect
|
|
env:
|
|
CHANGESET_BEFORE: ${{ github.event.before }}
|
|
run: sh scripts/detect-ci-changes.sh
|
|
|
|
app-quality:
|
|
if: github.event_name != 'schedule' && needs.changes.outputs.deploy == 'true'
|
|
needs: [changes]
|
|
runs-on: ${{ vars.CI_RUNNER || 'ubuntu-latest' }}
|
|
timeout-minutes: 30
|
|
env:
|
|
APP_URL: https://example.com
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
|
with:
|
|
lfs: true
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
|
with:
|
|
node-version-file: ${{ env.NODE_VERSION_FILE }}
|
|
cache: npm
|
|
cache-dependency-path: package-lock.json
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Restore and update image cache
|
|
env:
|
|
IMAGE_CACHE_NAMESPACE: ${{ github.ref_name }}
|
|
run: sh scripts/prepare-image-cache.sh
|
|
|
|
- name: Lint
|
|
run: npm run lint
|
|
|
|
- name: Test
|
|
run: npm run test
|
|
|
|
- name: Type check
|
|
run: npm run typecheck
|
|
|
|
- name: Build
|
|
env:
|
|
SKIP_IMAGE_OPTIMIZATION: true
|
|
run: npm run build
|
|
|
|
- name: E2E smoke test
|
|
env:
|
|
SKIP_IMAGE_OPTIMIZATION: true
|
|
run: npm run test:e2e
|
|
|
|
secret-scan:
|
|
if: github.event_name != 'schedule' && needs.changes.outputs.deploy == 'true'
|
|
needs: [changes]
|
|
runs-on: ${{ vars.CI_RUNNER || 'ubuntu-latest' }}
|
|
timeout-minutes: 15
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
|
with:
|
|
lfs: true
|
|
|
|
- name: Scan repository for committed secrets
|
|
run: sh scripts/trivy-scan.sh secret-fs .
|
|
|
|
security-deps:
|
|
if: github.event_name != 'schedule' && needs.changes.outputs.deploy == 'true'
|
|
needs: [changes]
|
|
runs-on: ${{ vars.CI_RUNNER || 'ubuntu-latest' }}
|
|
timeout-minutes: 30
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
|
with:
|
|
lfs: true
|
|
|
|
- name: Report app dependency vulnerabilities
|
|
run: sh scripts/trivy-scan.sh vuln-fs .
|
|
|
|
- name: Block high and critical app dependency vulnerabilities
|
|
env:
|
|
TRIVY_SEVERITY: HIGH,CRITICAL
|
|
TRIVY_EXIT_CODE: 1
|
|
run: sh scripts/trivy-scan.sh vuln-fs .
|
|
|
|
context:
|
|
if: >
|
|
needs.changes.outputs.deploy == 'true' &&
|
|
(
|
|
(github.event_name == 'push' &&
|
|
github.event.pull_request == null &&
|
|
(github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/master')) ||
|
|
(github.event_name == 'workflow_dispatch' &&
|
|
github.event.pull_request == null &&
|
|
(github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/master'))
|
|
)
|
|
needs: [changes]
|
|
runs-on: ${{ vars.CI_RUNNER || 'ubuntu-latest' }}
|
|
timeout-minutes: 5
|
|
outputs:
|
|
environment_name: ${{ steps.resolve.outputs.environment_name }}
|
|
primary_domain: ${{ steps.resolve.outputs.primary_domain }}
|
|
app_url: ${{ steps.resolve.outputs.app_url }}
|
|
cors_origin: ${{ steps.resolve.outputs.cors_origin }}
|
|
deploy_stack_name: ${{ steps.resolve.outputs.deploy_stack_name }}
|
|
traefik_certresolver: ${{ steps.resolve.outputs.traefik_certresolver }}
|
|
traefik_network_name: ${{ steps.resolve.outputs.traefik_network_name }}
|
|
steps:
|
|
- name: Resolve deployment target
|
|
id: resolve
|
|
env:
|
|
REF_NAME: ${{ github.ref_name }}
|
|
DEV_PRIMARY_DOMAIN: ${{ vars.DEV_PRIMARY_DOMAIN }}
|
|
DEV_APP_URL: ${{ vars.DEV_APP_URL }}
|
|
DEV_CORS_ORIGIN: ${{ vars.DEV_CORS_ORIGIN }}
|
|
DEV_DEPLOY_STACK_NAME: ${{ vars.DEV_DEPLOY_STACK_NAME }}
|
|
DEV_TRAEFIK_CERTRESOLVER: ${{ secrets.DEV_TRAEFIK_CERTRESOLVER }}
|
|
DEV_TRAEFIK_NETWORK_NAME: ${{ vars.DEV_TRAEFIK_NETWORK_NAME }}
|
|
PROD_PRIMARY_DOMAIN: ${{ vars.PROD_PRIMARY_DOMAIN }}
|
|
PROD_APP_URL: ${{ vars.PROD_APP_URL }}
|
|
PROD_CORS_ORIGIN: ${{ vars.PROD_CORS_ORIGIN }}
|
|
PROD_DEPLOY_STACK_NAME: ${{ vars.PROD_DEPLOY_STACK_NAME }}
|
|
PROD_TRAEFIK_CERTRESOLVER: ${{ secrets.PROD_TRAEFIK_CERTRESOLVER }}
|
|
PROD_TRAEFIK_NETWORK_NAME: ${{ vars.PROD_TRAEFIK_NETWORK_NAME }}
|
|
run: |
|
|
set -eu
|
|
|
|
set_output() {
|
|
printf '%s=%s\n' "$1" "$2" >> "$GITHUB_OUTPUT"
|
|
}
|
|
|
|
require_value() {
|
|
key="$1"
|
|
value="$2"
|
|
if [ -z "$value" ]; then
|
|
echo "Missing required workflow configuration: $key" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
case "$REF_NAME" in
|
|
dev)
|
|
config_prefix="DEV"
|
|
environment_name="dev"
|
|
primary_domain="$DEV_PRIMARY_DOMAIN"
|
|
app_url="${DEV_APP_URL:-}"
|
|
cors_origin="${DEV_CORS_ORIGIN:-}"
|
|
deploy_stack_name="${DEV_DEPLOY_STACK_NAME:-website-starter-dev}"
|
|
traefik_certresolver="$DEV_TRAEFIK_CERTRESOLVER"
|
|
traefik_network_name="${DEV_TRAEFIK_NETWORK_NAME:-traefik-external}"
|
|
;;
|
|
master)
|
|
config_prefix="PROD"
|
|
environment_name="production"
|
|
primary_domain="$PROD_PRIMARY_DOMAIN"
|
|
app_url="${PROD_APP_URL:-}"
|
|
cors_origin="${PROD_CORS_ORIGIN:-}"
|
|
deploy_stack_name="${PROD_DEPLOY_STACK_NAME:-website-starter-prod}"
|
|
traefik_certresolver="$PROD_TRAEFIK_CERTRESOLVER"
|
|
traefik_network_name="${PROD_TRAEFIK_NETWORK_NAME:-traefik}"
|
|
;;
|
|
*)
|
|
echo "Branch $REF_NAME is not configured for deployment." >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
require_value "${config_prefix}_PRIMARY_DOMAIN" "$primary_domain"
|
|
require_value "${config_prefix}_TRAEFIK_CERTRESOLVER" "$traefik_certresolver"
|
|
|
|
if [ -z "$app_url" ]; then
|
|
app_url="https://$primary_domain"
|
|
fi
|
|
if [ -z "$cors_origin" ]; then
|
|
cors_origin="$app_url"
|
|
fi
|
|
|
|
set_output "environment_name" "$environment_name"
|
|
set_output "primary_domain" "$primary_domain"
|
|
set_output "app_url" "$app_url"
|
|
set_output "cors_origin" "$cors_origin"
|
|
set_output "deploy_stack_name" "$deploy_stack_name"
|
|
set_output "traefik_certresolver" "$traefik_certresolver"
|
|
set_output "traefik_network_name" "$traefik_network_name"
|
|
|
|
app-image:
|
|
if: github.event_name != 'schedule' && needs.changes.outputs.deploy == 'true'
|
|
needs: [changes, context, app-quality, secret-scan, security-deps]
|
|
runs-on: ${{ vars.CI_RUNNER || 'ubuntu-latest' }}
|
|
timeout-minutes: 30
|
|
env:
|
|
APP_URL: ${{ needs.context.outputs.app_url }}
|
|
APP_IMAGE_NAME: ${{ env.APP_IMAGE_REPOSITORY }}:${{ github.ref_name }}-${{ github.sha }}-${{ github.run_id }}
|
|
APP_BRANCH_IMAGE_NAME: ${{ env.APP_IMAGE_REPOSITORY }}:${{ github.ref_name }}
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
|
with:
|
|
lfs: true
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
|
with:
|
|
node-version-file: ${{ env.NODE_VERSION_FILE }}
|
|
cache: npm
|
|
cache-dependency-path: package-lock.json
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Restore and update image cache
|
|
env:
|
|
IMAGE_CACHE_NAMESPACE: ${{ github.ref_name }}
|
|
run: sh scripts/prepare-image-cache.sh
|
|
|
|
- name: Build app runtime image
|
|
run: |
|
|
docker build --pull \
|
|
--build-arg BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
--build-arg VCS_REF="${{ github.sha }}" \
|
|
--build-arg APP_URL="$APP_URL" \
|
|
--build-arg APP_NAME="${APP_NAME:-Example Website}" \
|
|
--build-arg COMPANY_NAME="${COMPANY_NAME:-Example Company}" \
|
|
--build-arg CONTACT_EMAIL="${CONTACT_EMAIL:-info@example.com}" \
|
|
--build-arg CONTACT_PHONE="${CONTACT_PHONE:-+49 000 000000}" \
|
|
--build-arg CONTACT_ADDRESS="${CONTACT_ADDRESS:-Example Street 1, 12345 Example City}" \
|
|
--build-arg SOURCE_URL="${{ github.server_url }}/${{ github.repository }}" \
|
|
--build-arg SKIP_IMAGE_OPTIMIZATION=true \
|
|
--build-arg IMAGE_CACHE_NAMESPACE="${{ github.ref_name }}" \
|
|
-f Dockerfile \
|
|
-t "$APP_IMAGE_NAME" \
|
|
-t "$APP_BRANCH_IMAGE_NAME" \
|
|
.
|
|
|
|
- name: Report app runtime image vulnerabilities
|
|
run: sh scripts/trivy-scan.sh vuln-image "$APP_IMAGE_NAME"
|
|
|
|
- name: Block high and critical app runtime vulnerabilities
|
|
env:
|
|
TRIVY_SEVERITY: HIGH,CRITICAL
|
|
TRIVY_EXIT_CODE: 1
|
|
run: sh scripts/trivy-scan.sh vuln-image "$APP_IMAGE_NAME"
|
|
|
|
- name: Push app runtime image
|
|
env:
|
|
REGISTRY_USERNAME: ${{ secrets.PACKAGES_USERNAME }}
|
|
REGISTRY_TOKEN: ${{ secrets.PACKAGES_TOKEN }}
|
|
run: |
|
|
set -eu
|
|
if [ -z "${REGISTRY_USERNAME:-}" ] || [ -z "${REGISTRY_TOKEN:-}" ]; then
|
|
echo "Missing required secrets: PACKAGES_USERNAME and PACKAGES_TOKEN" >&2
|
|
exit 1
|
|
fi
|
|
docker_config="$(mktemp -d)"
|
|
cleanup() {
|
|
DOCKER_CONFIG="$docker_config" docker logout "$REGISTRY" >/dev/null 2>&1 || true
|
|
rm -rf "$docker_config"
|
|
}
|
|
trap cleanup EXIT
|
|
trap 'exit 1' HUP INT TERM
|
|
export DOCKER_CONFIG="$docker_config"
|
|
echo "$REGISTRY_TOKEN" | docker login "$REGISTRY" -u "$REGISTRY_USERNAME" --password-stdin
|
|
docker push "$APP_IMAGE_NAME"
|
|
docker push "$APP_BRANCH_IMAGE_NAME"
|
|
|
|
deploy-dev:
|
|
if: github.event_name != 'schedule' && needs.changes.outputs.deploy == 'true' && github.ref == 'refs/heads/dev'
|
|
needs: [changes, context, app-quality, app-image, secret-scan, security-deps]
|
|
runs-on: ${{ vars.CI_RUNNER || 'ubuntu-latest' }}
|
|
timeout-minutes: 30
|
|
env:
|
|
CALENDAR_URL: ${{ secrets.CALENDAR_URL }}
|
|
PRIMARY_DOMAIN: ${{ needs.context.outputs.primary_domain }}
|
|
APP_URL: ${{ needs.context.outputs.app_url }}
|
|
CORS_ORIGIN: ${{ needs.context.outputs.cors_origin }}
|
|
COMPOSE_PROJECT_NAME: ${{ needs.context.outputs.deploy_stack_name }}
|
|
TRAEFIK_STACK_NAME: ${{ needs.context.outputs.deploy_stack_name }}
|
|
TRAEFIK_CERTRESOLVER: ${{ needs.context.outputs.traefik_certresolver }}
|
|
TRAEFIK_NETWORK_NAME: ${{ needs.context.outputs.traefik_network_name }}
|
|
DEPLOY_TARGET: dev
|
|
APP_IMAGE_NAME: ${{ env.APP_IMAGE_REPOSITORY }}:dev-${{ github.sha }}-${{ github.run_id }}
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
|
with:
|
|
lfs: true
|
|
|
|
- name: Pull app runtime image
|
|
env:
|
|
REGISTRY_USERNAME: ${{ secrets.PACKAGES_USERNAME }}
|
|
REGISTRY_TOKEN: ${{ secrets.PACKAGES_TOKEN }}
|
|
run: |
|
|
set -eu
|
|
if [ -z "${REGISTRY_USERNAME:-}" ] || [ -z "${REGISTRY_TOKEN:-}" ]; then
|
|
echo "Missing required secrets: PACKAGES_USERNAME and PACKAGES_TOKEN" >&2
|
|
exit 1
|
|
fi
|
|
docker_config="$(mktemp -d)"
|
|
cleanup() {
|
|
DOCKER_CONFIG="$docker_config" docker logout "$REGISTRY" >/dev/null 2>&1 || true
|
|
rm -rf "$docker_config"
|
|
}
|
|
trap cleanup EXIT
|
|
trap 'exit 1' HUP INT TERM
|
|
export DOCKER_CONFIG="$docker_config"
|
|
echo "$REGISTRY_TOKEN" | docker login "$REGISTRY" -u "$REGISTRY_USERNAME" --password-stdin
|
|
docker pull "$APP_IMAGE_NAME"
|
|
|
|
- name: Validate runtime configuration
|
|
run: |
|
|
if [ -z "${CALENDAR_URL:-}" ]; then
|
|
echo "Missing required secret: CALENDAR_URL" >&2
|
|
exit 1
|
|
fi
|
|
|
|
- name: Print deployment target
|
|
run: |
|
|
echo "Environment: ${{ needs.context.outputs.environment_name }}"
|
|
echo "Site host: $PRIMARY_DOMAIN"
|
|
echo "Compose project: $COMPOSE_PROJECT_NAME"
|
|
|
|
- name: Deploy and verify app
|
|
run: sh scripts/deploy-and-verify.sh
|
|
|
|
deploy-master:
|
|
if: github.event_name != 'schedule' && needs.changes.outputs.deploy == 'true' && github.ref == 'refs/heads/master'
|
|
needs: [changes, context, app-quality, app-image, secret-scan, security-deps]
|
|
runs-on: ${{ vars.PROD_RUNNER || vars.CI_RUNNER || 'ubuntu-latest' }}
|
|
timeout-minutes: 30
|
|
env:
|
|
CALENDAR_URL: ${{ secrets.CALENDAR_URL }}
|
|
PRIMARY_DOMAIN: ${{ needs.context.outputs.primary_domain }}
|
|
APP_URL: ${{ needs.context.outputs.app_url }}
|
|
CORS_ORIGIN: ${{ needs.context.outputs.cors_origin }}
|
|
COMPOSE_PROJECT_NAME: ${{ needs.context.outputs.deploy_stack_name }}
|
|
TRAEFIK_STACK_NAME: ${{ needs.context.outputs.deploy_stack_name }}
|
|
TRAEFIK_CERTRESOLVER: ${{ needs.context.outputs.traefik_certresolver }}
|
|
TRAEFIK_NETWORK_NAME: ${{ needs.context.outputs.traefik_network_name }}
|
|
DEPLOY_TARGET: master
|
|
APP_IMAGE_NAME: ${{ env.APP_IMAGE_REPOSITORY }}:master-${{ github.sha }}-${{ github.run_id }}
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
|
with:
|
|
lfs: true
|
|
|
|
- name: Pull app runtime image
|
|
env:
|
|
REGISTRY_USERNAME: ${{ secrets.PACKAGES_USERNAME }}
|
|
REGISTRY_TOKEN: ${{ secrets.PACKAGES_TOKEN }}
|
|
run: |
|
|
set -eu
|
|
if [ -z "${REGISTRY_USERNAME:-}" ] || [ -z "${REGISTRY_TOKEN:-}" ]; then
|
|
echo "Missing required secrets: PACKAGES_USERNAME and PACKAGES_TOKEN" >&2
|
|
exit 1
|
|
fi
|
|
docker_config="$(mktemp -d)"
|
|
cleanup() {
|
|
DOCKER_CONFIG="$docker_config" docker logout "$REGISTRY" >/dev/null 2>&1 || true
|
|
rm -rf "$docker_config"
|
|
}
|
|
trap cleanup EXIT
|
|
trap 'exit 1' HUP INT TERM
|
|
export DOCKER_CONFIG="$docker_config"
|
|
echo "$REGISTRY_TOKEN" | docker login "$REGISTRY" -u "$REGISTRY_USERNAME" --password-stdin
|
|
docker pull "$APP_IMAGE_NAME"
|
|
|
|
- name: Validate runtime configuration
|
|
run: |
|
|
if [ -z "${CALENDAR_URL:-}" ]; then
|
|
echo "Missing required secret: CALENDAR_URL" >&2
|
|
exit 1
|
|
fi
|
|
|
|
- name: Print deployment target
|
|
run: |
|
|
echo "Environment: ${{ needs.context.outputs.environment_name }}"
|
|
echo "Site host: $PRIMARY_DOMAIN"
|
|
echo "Compose project: $COMPOSE_PROJECT_NAME"
|
|
|
|
- name: Deploy and verify app
|
|
run: sh scripts/deploy-and-verify.sh
|
|
|
|
scheduled-rebuild-dev:
|
|
if: github.event_name == 'schedule'
|
|
runs-on: ${{ vars.CI_RUNNER || 'ubuntu-latest' }}
|
|
timeout-minutes: 45
|
|
env:
|
|
DEPLOY_TARGET: dev
|
|
CALENDAR_URL: ${{ secrets.CALENDAR_URL }}
|
|
PRIMARY_DOMAIN: ${{ vars.DEV_PRIMARY_DOMAIN }}
|
|
APP_URL: ${{ vars.DEV_APP_URL }}
|
|
CORS_ORIGIN: ${{ vars.DEV_CORS_ORIGIN }}
|
|
COMPOSE_PROJECT_NAME: ${{ vars.DEV_DEPLOY_STACK_NAME }}
|
|
TRAEFIK_STACK_NAME: ${{ vars.DEV_DEPLOY_STACK_NAME }}
|
|
TRAEFIK_CERTRESOLVER: ${{ secrets.DEV_TRAEFIK_CERTRESOLVER }}
|
|
TRAEFIK_NETWORK_NAME: ${{ vars.DEV_TRAEFIK_NETWORK_NAME }}
|
|
steps:
|
|
- name: Checkout dev branch
|
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
|
with:
|
|
ref: dev
|
|
lfs: true
|
|
|
|
- name: Scan repository for committed secrets
|
|
run: sh scripts/trivy-scan.sh secret-fs .
|
|
|
|
- name: Report app dependency vulnerabilities
|
|
run: sh scripts/trivy-scan.sh vuln-fs .
|
|
|
|
- name: Block high and critical app dependency vulnerabilities
|
|
env:
|
|
TRIVY_SEVERITY: HIGH,CRITICAL
|
|
TRIVY_EXIT_CODE: 1
|
|
run: sh scripts/trivy-scan.sh vuln-fs .
|
|
|
|
- name: Rebuild latest base image and deploy dev
|
|
env:
|
|
REGISTRY_USERNAME: ${{ secrets.PACKAGES_USERNAME }}
|
|
REGISTRY_TOKEN: ${{ secrets.PACKAGES_TOKEN }}
|
|
run: sh scripts/scheduled-rebuild-deploy.sh
|
|
|
|
scheduled-rebuild-master:
|
|
if: github.event_name == 'schedule'
|
|
runs-on: ${{ vars.PROD_RUNNER || vars.CI_RUNNER || 'ubuntu-latest' }}
|
|
timeout-minutes: 45
|
|
env:
|
|
DEPLOY_TARGET: master
|
|
CALENDAR_URL: ${{ secrets.CALENDAR_URL }}
|
|
PRIMARY_DOMAIN: ${{ vars.PROD_PRIMARY_DOMAIN }}
|
|
APP_URL: ${{ vars.PROD_APP_URL }}
|
|
CORS_ORIGIN: ${{ vars.PROD_CORS_ORIGIN }}
|
|
COMPOSE_PROJECT_NAME: ${{ vars.PROD_DEPLOY_STACK_NAME }}
|
|
TRAEFIK_STACK_NAME: ${{ vars.PROD_DEPLOY_STACK_NAME }}
|
|
TRAEFIK_CERTRESOLVER: ${{ secrets.PROD_TRAEFIK_CERTRESOLVER }}
|
|
TRAEFIK_NETWORK_NAME: ${{ vars.PROD_TRAEFIK_NETWORK_NAME }}
|
|
steps:
|
|
- name: Checkout master branch
|
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
|
with:
|
|
ref: master
|
|
lfs: true
|
|
|
|
- name: Scan repository for committed secrets
|
|
run: sh scripts/trivy-scan.sh secret-fs .
|
|
|
|
- name: Report app dependency vulnerabilities
|
|
run: sh scripts/trivy-scan.sh vuln-fs .
|
|
|
|
- name: Block high and critical app dependency vulnerabilities
|
|
env:
|
|
TRIVY_SEVERITY: HIGH,CRITICAL
|
|
TRIVY_EXIT_CODE: 1
|
|
run: sh scripts/trivy-scan.sh vuln-fs .
|
|
|
|
- name: Rebuild latest base image and deploy master
|
|
env:
|
|
REGISTRY_USERNAME: ${{ secrets.PACKAGES_USERNAME }}
|
|
REGISTRY_TOKEN: ${{ secrets.PACKAGES_TOKEN }}
|
|
run: sh scripts/scheduled-rebuild-deploy.sh
|
|
|
|
rollback-dev:
|
|
if: github.event_name == 'workflow_dispatch' && github.event.inputs.rollback_target == 'dev' && github.ref == 'refs/heads/dev'
|
|
runs-on: ${{ vars.CI_RUNNER || 'ubuntu-latest' }}
|
|
timeout-minutes: 30
|
|
env:
|
|
DEPLOY_TARGET: dev
|
|
CALENDAR_URL: ${{ secrets.CALENDAR_URL }}
|
|
PRIMARY_DOMAIN: ${{ vars.DEV_PRIMARY_DOMAIN }}
|
|
APP_URL: ${{ vars.DEV_APP_URL }}
|
|
CORS_ORIGIN: ${{ vars.DEV_CORS_ORIGIN }}
|
|
COMPOSE_PROJECT_NAME: ${{ vars.DEV_DEPLOY_STACK_NAME }}
|
|
TRAEFIK_STACK_NAME: ${{ vars.DEV_DEPLOY_STACK_NAME }}
|
|
TRAEFIK_CERTRESOLVER: ${{ secrets.DEV_TRAEFIK_CERTRESOLVER }}
|
|
TRAEFIK_NETWORK_NAME: ${{ vars.DEV_TRAEFIK_NETWORK_NAME }}
|
|
APP_IMAGE_TAG: ${{ github.event.inputs.app_image_tag }}
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
|
with:
|
|
lfs: true
|
|
|
|
- name: Deploy rollback and verify app
|
|
env:
|
|
REGISTRY_USERNAME: ${{ secrets.PACKAGES_USERNAME }}
|
|
REGISTRY_TOKEN: ${{ secrets.PACKAGES_TOKEN }}
|
|
run: sh scripts/manual-rollback-deploy.sh
|
|
|
|
rollback-master:
|
|
if: github.event_name == 'workflow_dispatch' && github.event.inputs.rollback_target == 'master' && github.ref == 'refs/heads/master'
|
|
runs-on: ${{ vars.PROD_RUNNER || vars.CI_RUNNER || 'ubuntu-latest' }}
|
|
timeout-minutes: 30
|
|
env:
|
|
DEPLOY_TARGET: master
|
|
CALENDAR_URL: ${{ secrets.CALENDAR_URL }}
|
|
PRIMARY_DOMAIN: ${{ vars.PROD_PRIMARY_DOMAIN }}
|
|
APP_URL: ${{ vars.PROD_APP_URL }}
|
|
CORS_ORIGIN: ${{ vars.PROD_CORS_ORIGIN }}
|
|
COMPOSE_PROJECT_NAME: ${{ vars.PROD_DEPLOY_STACK_NAME }}
|
|
TRAEFIK_STACK_NAME: ${{ vars.PROD_DEPLOY_STACK_NAME }}
|
|
TRAEFIK_CERTRESOLVER: ${{ secrets.PROD_TRAEFIK_CERTRESOLVER }}
|
|
TRAEFIK_NETWORK_NAME: ${{ vars.PROD_TRAEFIK_NETWORK_NAME }}
|
|
APP_IMAGE_TAG: ${{ github.event.inputs.app_image_tag }}
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
|
with:
|
|
lfs: true
|
|
|
|
- name: Deploy rollback and verify app
|
|
env:
|
|
REGISTRY_USERNAME: ${{ secrets.PACKAGES_USERNAME }}
|
|
REGISTRY_TOKEN: ${{ secrets.PACKAGES_TOKEN }}
|
|
run: sh scripts/manual-rollback-deploy.sh
|