Files
Workflow config file is invalid. Please check your config file: go-yaml load error in reader at <unknown position>: control characters are not allowed (value: 1)
2025-12-08 17:46:35 +07:00

331 lines
13 KiB
YAML

# This is a reusable Gitea Actions workflow.
# It's designed to be called by other repositories to standardize the deployment process.
# It performs two main jobs:
# 1. Build: Builds a Docker image from the calling repository's Dockerfile and pushes it to a registry.
# 2. Deploy: Runs the ansible-gitea playbook to deploy the newly built image to a target server.
name: Reusable CI/CD
# This workflow is triggered by a call from another workflow.
on:
workflow_call:
# ------------------ INPUTS ------------------
# These are parameters that the calling workflow must provide.
inputs:
# The name of the application/service being deployed.
# Used to find project-specific variables in ansible.
app_name:
description: 'The application name'
required: true
type: string
# The deployment environment (e.g., staging, production).
deploy_env:
description: 'The deployment environment'
required: true
type: string
# The environment type (e.g., worker).
env_type:
description: 'The environment type'
required: true
type: string
# ------------------ SECRETS ------------------
# These are secrets that the calling workflow must pass.
# They should be configured in the Gitea organization or repository settings.
secrets:
# Address of the target host(s) for Ansible.
SERVER_HOST:
description: 'Target hosts for Ansible (e.g., "user@host")'
required: true
SERVER_PORT:
description: 'Target port for Ansible (e.g., "22")'
required: false
default: '22'
SERVER_USER:
description: 'Target user for Ansible (e.g., "cicd")'
required: false
default: 'cicd'
# SSH private key to connect to the Ansible target host.
SERVER_SSH_PRIVATE_KEY:
description: 'SSH Private Key for Ansible'
required: false
# Password for the target host.
SERVER_PASSWORD:
description: 'Password for Ansible'
required: false
# Password for the Ansible vault.
VAULT_PASSWORD:
description: 'Ansible Vault Password'
required: true
# The address of the HashiCorp Vault server.
VAULT_ADDR:
description: 'Vault server address (e.g., https://vault.example.com:8200)'
required: true
# The Vault role to use for authentication.
VAULT_ROLE:
description: 'Vault role for authentication'
required: true
# The URL of the Docker registry.
DOCKER_REGISTRY:
description: 'Docker Registry URL'
required: true
type: string
jobs:
# =============================================================================
# JOB 1: PREPARE - AUTO-DETECT FRAMEWORK AND SETTINGS
# =============================================================================
prepare:
runs-on: ubuntu-22.04
outputs:
node_version: ${{ steps.detect.outputs.NODE_VERSION }}
framework: ${{ steps.detect.outputs.FRAMEWORK }}
package_manager: ${{ steps.detect.outputs.PACKAGE_MANAGER }}
container_port: ${{ steps.detect.outputs.CONTAINER_PORT }}
build_command: ${{ steps.detect.outputs.BUILD_COMMAND }}
start_command: ${{ steps.detect.outputs.START_COMMAND }}
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Auto-detect settings
id: detect
run: |
echo "🔍 Starting auto-detection process..."
NODE_VERSION="18" # Default Node.js version
if [ -f ".nvmrc" ]; then
NODE_VERSION=$(cat .nvmrc | tr -d 'v' | tr -d '\n' | head -1)
elif [ -f "package.json" ] && grep -q '.engines.node' package.json; then
RAW_VERSION=$(grep '.engines.node' package.json | sed -E 's/.*"node": "([^"]+)".*/\1/')
NODE_VERSION=$(echo "$RAW_VERSION" | grep -oE '[0-9]+' | head -n 1)
fi
echo "✓ Detected Node.js version: ${NODE_VERSION}"
echo "NODE_VERSION=${NODE_VERSION}" >> $GITEA_OUTPUT
FRAMEWORK="nodejs"
CONTAINER_PORT="3000"
if [ -f "package.json" ]; then
if grep -q '"@nestjs/core"' package.json; then FRAMEWORK="nestjs";
elif grep -q '"next"' package.json; then FRAMEWORK="nextjs";
elif grep -q '"react-scripts"' package.json; then FRAMEWORK="reactjs";
elif grep -q '"vite"' package.json && grep -q '"react"' package.json; then FRAMEWORK="reactjs-vite";
elif grep -q '"express"' package.json; then FRAMEWORK="expressjs";
fi
fi
echo "✓ Detected framework: ${FRAMEWORK}"
echo "FRAMEWORK=${FRAMEWORK}" >> $GITEA_OUTPUT
echo "CONTAINER_PORT=${CONTAINER_PORT}" >> $GITEA_OUTPUT
PACKAGE_MANAGER="npm"
if [ -f "pnpm-lock.yaml" ]; then PACKAGE_MANAGER="pnpm";
elif [ -f "yarn.lock" ]; then PACKAGE_MANAGER="yarn";
fi
echo "✓ Detected package manager: ${PACKAGE_MANAGER}"
echo "PACKAGE_MANAGER=${PACKAGE_MANAGER}" >> $GITEA_OUTPUT
BUILD_CMD=""
START_CMD=""
if [ -f "package.json" ]; then
BUILD_CMD=$(grep -o '"build": "[^"]*"' package.json | sed 's/"build": "\(.*\)"//' || echo "")
START_CMD=$(grep -o '"start": "[^"]*"' package.json | sed 's/"start": "\(.*\)"//' || echo "")
fi
echo "✓ Detected build command: ${BUILD_CMD}"
echo "✓ Detected start command: ${START_CMD}"
echo "BUILD_COMMAND=${BUILD_CMD}" >> $GITEA_OUTPUT
echo "START_COMMAND=${START_CMD}" >> $GITEA_OUTPUT
# =============================================================================
# JOB 2: GENERATE DOCKERFILE
# =============================================================================
generate-dockerfile:
runs-on: ubuntu-22.04
needs: prepare
outputs:
dockerfile_path: steps.generate.outputs.dockerfile_path
steps:
- name: Checkout Ansible Repo for templates
uses: actions/checkout@v4
with:
repository: devops/ansible-gitea
path: .ansible
- name: Generate Dockerfile
id: generate
run: |
echo "🔧 Generating Dockerfile for ${{ needs.prepare.outputs.framework }} framework..."
# In a real scenario, you would have a script here to generate the Dockerfile
# from a template based on the framework.
# For this example, we'll just point to a placeholder.
# cp .ansible/templates/dockerfiles/${{ needs.prepare.outputs.framework }}.j2 Dockerfile
# For now, we assume a Dockerfile exists in the calling repo.
echo "dockerfile_path=./Dockerfile" >> $GITEA_OUTPUT
# =============================================================================
# JOB 3: BUILD AND PUSH DOCKER IMAGE
# =============================================================================
build:
runs-on: ubuntu-22.04
needs: [prepare, generate-dockerfile]
# Grant permissions for the workflow to request an OIDC token.
permissions:
id-token: write
contents: read
outputs:
image_tag: ${{ steps.meta.outputs.version }}
image_name: ${{ inputs.DOCKER_REGISTRY }}/${{ github.repository_owner }}/${{ inputs.app_name }}
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Import Secrets from HashiCorp Vault
id: vault
uses: hashicorp/vault-action@v2
with:
url: ${{ secrets.VAULT_ADDR }}
role: ${{ secrets.VAULT_ROLE }}
method: jwt
jwt: ${{ job.id_token }}
secrets: |
secret/data/${{ inputs.app_name }}/${{ inputs.deploy_env }} * | DOTENV_CONTENTS
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Gitea's Docker Registry
uses: docker/login-action@v3
with:
registry: ${{ inputs.DOCKER_REGISTRY }}
username: ${{ gitea.actor }}
password: ${{ secrets.token }}
- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ inputs.DOCKER_REGISTRY }}/${{ github.repository_owner }}/${{ inputs.app_name }}
tags: |
type=sha,prefix=,format=long
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ${{ needs.generate-dockerfile.outputs.dockerfile_path }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
secrets: |
env=${{ steps.vault.outputs.DOTENV_CONTENTS }}
# =============================================================================
# JOB 4: DEPLOY WITH ANSIBLE
# =============================================================================
deploy:
runs-on: ubuntu-22.04
needs: [prepare, build] # This job runs only after 'prepare' and 'build' jobs succeed.
permissions:
id-token: write
contents: read
steps:
- name: Checkout Application Code
uses: actions/checkout@v4
- name: Checkout Ansible Repo
uses: actions/checkout@v4
with:
# This should be the path to your ansible-gitea repository
repository: devops/ansible-gitea
path: .ansible
- name: Set up Ansible and Dependencies
run: |
sudo apt-get update && sudo apt-get install -y openssh-client sshpass python3 python3-pip
pip3 install ansible
ansible-galaxy collection install community.docker
- name: Setup SSH
if: ${{ secrets.SERVER_SSH_PRIVATE_KEY != '' }}
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SERVER_SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
# Disable strict host key checking for simplicity in CI
echo "StrictHostKeyChecking no" >> ~/.ssh/config
- name: Setup Ansible Vault
working-directory: .ansible
run: |
echo "${{ secrets.VAULT_PASSWORD }}" > .vault_pass
- name: Import App Secrets from HashiCorp Vault
id: vault
uses: hashicorp/vault-action@v2
with:
url: ${{ secrets.VAULT_ADDR }}
role: ${{ secrets.VAULT_ROLE }}
method: jwt
jwt: ${{ job.id_token }}
secrets: |
secret/data/${{ inputs.app_name }}/${{ inputs.deploy_env }} * | app_secrets
- name: Create Ansible secret file
run: |
ANSIBLE_PROJECT_DIR=".ansible/projects/${{ inputs.app_name }}"
mkdir -p "${ANSIBLE_PROJECT_DIR}/templates"
echo "${{ steps.vault.outputs.app_secrets }}" > "${ANSIBLE_PROJECT_DIR}/templates/.env.j2"
echo "✅ Created .env.j2 for Ansible"
- name: Run Ansible Playbook
working-directory: .ansible
env:
SERVER_HOST: ${{ secrets.SERVER_HOST }}
SERVER_PASSWORD: ${{ secrets.SERVER_PASSWORD }}
SERVER_SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_PRIVATE_KEY }}
run: |
auth_method_var=""
if [[ -n "$SERVER_SSH_PRIVATE_KEY" ]]; then
auth_method_var="auth_method=ssh_key ssh_key_path=/root/.ssh/id_rsa"
if [[ -n "$SERVER_PASSWORD" ]]; then
auth_method_var="$auth_method_var server_become_password=$SERVER_PASSWORD"
fi
elif [[ -n "$SERVER_PASSWORD" ]]; then
auth_method_var="auth_method=password server_password=$SERVER_PASSWORD"
auth_method_var="$auth_method_var server_become_password=$SERVER_PASSWORD"
fi
ansible-playbook playbook.yml \
-i "$SERVER_HOST," \
--extra-vars "app_name=${{ inputs.app_name }} \
deploy_env=${{ inputs.deploy_env }} \
server_user=${{ secrets.SERVER_USER }} \
server_port=${{ secrets.SERVER_PORT }} \
server_host=$SERVER_HOST \
git_repo=${{ github.server_url }}/${{ github.repository }} \
framework=${{ needs.prepare.outputs.framework }} \
docker_image_tag=${{ needs.build.outputs.image_tag }} \
docker_image_name=${{ needs.build.outputs.image_name }} \
docker_image_full=${{ needs.build.outputs.image_name }}:${{ needs.build.outputs.image_tag }} \
docker_registry=${{ secrets.DOCKER_REGISTRY }} \
$auth_method_var" \
--vault-password-file .vault_pass
cleanup:
name: Cleanup old images
runs-on: ubuntu-22.04
needs: deploy
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Clean up old Docker images and cache
run: |
echo "🧹 Cleaning up old Docker images and build cache..."
# Prune images older than 7 days (168 hours)
docker image prune -af --filter "until=168h" || echo "Docker image prune failed, continuing."
# Prune buildx cache
docker buildx prune -af --filter "until=168h" || echo "Docker buildx prune failed, continuing."
echo "✅ Cleanup finished."