# 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 path to the secrets in Vault (e.g., "secret/data/my-app my_secret"). VAULT_SECRETS_PATH: description: 'Path to the secrets in Vault' 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": "\(.*\)"/\1/' || echo "") START_CMD=$(grep -o '"start": "[^"]*"' package.json | sed 's/"start": "\(.*\)"/\1/' || 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: | ${{ secrets.VAULT_SECRETS_PATH }} env_data | 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. 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 run: | echo "${{ secrets.VAULT_PASSWORD }}" > .ansible/.vault_pass - 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=${{ inputs.server_user }} \ server_port=${{ inputs.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=${{ inputs.DOCKER_REGISTRY }} \ $auth_method_var" \ --vault-password-file .vault_pass