--- name: CI "on": push: branches: [master] pull_request: types: [opened, synchronize, reopened] jobs: # Check if CI should be skipped based on branch name or commit message # Simple skip pattern: @skipci (case-insensitive) skip-ci-check: runs-on: ubuntu-latest outputs: should-skip: ${{ steps.check.outputs.skip }} steps: - name: Check out code (for commit message) uses: actions/checkout@v4 with: fetch-depth: 1 - name: Check if CI should be skipped id: check run: | # Simple skip pattern: @skipci (case-insensitive) # Works in branch names and commit messages SKIP_PATTERN="@skipci" # Get branch name (works for both push and PR) BRANCH_NAME="${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" # Get commit message (works for both push and PR) COMMIT_MSG="${GITHUB_EVENT_HEAD_COMMIT_MESSAGE:-}" if [ -z "$COMMIT_MSG" ]; then COMMIT_MSG="${GITHUB_EVENT_PULL_REQUEST_HEAD_COMMIT_MESSAGE:-}" fi if [ -z "$COMMIT_MSG" ]; then COMMIT_MSG=$(git log -1 --pretty=%B 2>/dev/null || echo "") fi SKIP=0 # Check branch name (case-insensitive) if echo "$BRANCH_NAME" | grep -qiF "$SKIP_PATTERN"; then echo "Skipping CI: branch name contains '$SKIP_PATTERN'" SKIP=1 fi # Check commit message (case-insensitive) if [ $SKIP -eq 0 ] && [ -n "$COMMIT_MSG" ]; then if echo "$COMMIT_MSG" | grep -qiF "$SKIP_PATTERN"; then echo "Skipping CI: commit message contains '$SKIP_PATTERN'" SKIP=1 fi fi echo "skip=$SKIP" >> $GITHUB_OUTPUT echo "Branch: $BRANCH_NAME" echo "Commit: ${COMMIT_MSG:0:50}..." echo "Skip CI: $SKIP" lint-and-test: needs: skip-ci-check if: needs.skip-ci-check.outputs.should-skip != '1' runs-on: ubuntu-latest # Skip push events for non-master branches (they'll be covered by PR events) if: github.event_name == 'pull_request' || github.ref == 'refs/heads/master' container: image: node:20-bullseye steps: - name: Check out code uses: actions/checkout@v4 - name: Install dependencies run: npm ci - name: Lint markdown run: npm run test:markdown - name: Check markdown links run: npm run test:links continue-on-error: true ansible-validation: needs: skip-ci-check if: needs.skip-ci-check.outputs.should-skip != '1' runs-on: ubuntu-latest # Skip push events for non-master branches (they'll be covered by PR events) if: github.event_name == 'pull_request' || github.ref == 'refs/heads/master' container: image: ubuntu:22.04 steps: - name: Install Node.js for checkout action run: | apt-get update && apt-get install -y curl git curl -fsSL https://deb.nodesource.com/setup_20.x | bash - apt-get install -y nodejs - name: Check out code uses: actions/checkout@v4 - name: Install Python and dependencies run: | apt-get update && apt-get install -y python3 python3-pip - name: Install Ansible and linting tools run: pip3 install --no-cache-dir ansible ansible-lint yamllint - name: Install Ansible collections run: | ansible-galaxy collection install -r collections/requirements.yml - name: Validate YAML syntax run: | echo "Checking YAML syntax..." find . -name "*.yml" -o -name "*.yaml" | grep -v ".git" | while read file; do python3 -c "import yaml; yaml.safe_load(open('$file'))" || exit 1 done - name: Run ansible-lint run: ansible-lint secret-scanning: needs: skip-ci-check if: needs.skip-ci-check.outputs.should-skip != '1' runs-on: ubuntu-latest container: image: zricethezav/gitleaks:latest steps: - name: Install Node.js for checkout action run: | apk add --no-cache nodejs npm curl - name: Check out code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Scan for secrets run: gitleaks detect --source . --no-banner --redact --exit-code 0 continue-on-error: true dependency-scan: needs: skip-ci-check if: needs.skip-ci-check.outputs.should-skip != '1' runs-on: ubuntu-latest container: image: aquasec/trivy:latest steps: - name: Install Node.js for checkout action run: | apk add --no-cache nodejs npm curl - name: Check out code uses: actions/checkout@v4 - name: Show dependency manifests (debug) run: | set -e echo "Repo root:" ls -la echo "" echo "Common dependency manifests:" ls -la package.json package-lock.json requirements.txt pyproject.toml poetry.lock Pipfile Pipfile.lock 2>/dev/null || true echo "" echo "Count of lock/manifests found:" find . -maxdepth 3 -type f \( \ -name "package-lock.json" -o \ -name "pnpm-lock.yaml" -o \ -name "yarn.lock" -o \ -name "requirements.txt" -o \ -name "pyproject.toml" -o \ -name "poetry.lock" -o \ -name "Pipfile.lock" \ \) | wc -l - name: Dependency vulnerability scan (Trivy) run: | trivy fs \ --scanners vuln \ --severity HIGH,CRITICAL \ --ignore-unfixed \ --timeout 10m \ --skip-dirs .git,node_modules \ --exit-code 0 \ . - name: Secret scan (Trivy) run: | trivy fs \ --scanners secret \ --timeout 10m \ --skip-dirs .git,node_modules \ --exit-code 0 \ . sast-scan: needs: skip-ci-check if: needs.skip-ci-check.outputs.should-skip != '1' runs-on: ubuntu-latest container: image: ubuntu:22.04 steps: - name: Install Node.js for checkout action run: | apt-get update && apt-get install -y curl curl -fsSL https://deb.nodesource.com/setup_20.x | bash - apt-get install -y nodejs - name: Check out code uses: actions/checkout@v4 - name: Install Semgrep run: | apt-get update && apt-get install -y python3 python3-pip pip3 install semgrep - name: Run Semgrep scan run: semgrep --config=auto --error continue-on-error: true license-check: needs: skip-ci-check if: needs.skip-ci-check.outputs.should-skip != '1' runs-on: ubuntu-latest container: image: node:20-bullseye steps: - name: Check out code uses: actions/checkout@v4 - name: Install license-checker run: npm install -g license-checker - name: Check npm licenses run: | if [ -f "package.json" ]; then npm ci # Exclude the repo itself (private=true packages are treated as UNLICENSED by license-checker). license-checker --excludePrivatePackages --onlyAllow 'MIT;Apache-2.0;BSD-3-Clause;ISC;BSD-2-Clause' else echo "No package.json found, skipping license check" fi vault-check: needs: skip-ci-check if: needs.skip-ci-check.outputs.should-skip != '1' runs-on: ubuntu-latest container: image: ubuntu:22.04 steps: - name: Install Node.js for checkout action run: | apt-get update && apt-get install -y curl curl -fsSL https://deb.nodesource.com/setup_20.x | bash - apt-get install -y nodejs - name: Check out code uses: actions/checkout@v4 - name: Install Python and dependencies run: | apt-get update && apt-get install -y python3 python3-pip - name: Install Ansible run: pip3 install --no-cache-dir ansible - name: Validate vault files are encrypted run: | echo "Checking for Ansible Vault files..." # Intentionally skip *.example files: they are plaintext templates. vault_files=$(find . -name "*vault*.yml" -o -name "*vault*.yaml" | grep -v ".git" | grep -v ".example" || true) if [ -z "$vault_files" ]; then echo "No vault files found" exit 0 fi failed=0 for vault_file in $vault_files; do echo "Checking $vault_file..." # Check if file starts with ANSIBLE_VAULT header (doesn't require password) if head -n 1 "$vault_file" | grep -q "^\$ANSIBLE_VAULT"; then echo "✓ $vault_file is properly encrypted (has vault header)" else echo "✗ ERROR: $vault_file does not have ANSIBLE_VAULT header - may be unencrypted!" failed=1 fi done if [ $failed -eq 1 ]; then echo "Some vault files are not encrypted. Please encrypt them with: ansible-vault encrypt " exit 1 fi echo "All vault files are properly encrypted!" playbook-test: needs: skip-ci-check if: needs.skip-ci-check.outputs.should-skip != '1' runs-on: ubuntu-latest container: image: ubuntu:22.04 steps: - name: Install Node.js for checkout action run: | apt-get update && apt-get install -y curl curl -fsSL https://deb.nodesource.com/setup_20.x | bash - apt-get install -y nodejs - name: Check out code uses: actions/checkout@v4 - name: Install Python and dependencies run: | apt-get update && apt-get install -y python3 python3-pip - name: Install Ansible run: pip3 install --no-cache-dir ansible - name: Install Ansible collections run: | ansible-galaxy collection install -r collections/requirements.yml - name: Validate playbooks (CI inventory, no vault) run: | set -e echo "Validating playbooks against a CI-only localhost inventory (no vault required)..." cat > /tmp/ci-inventory.ini <<'EOF' [dev] localhost ansible_connection=local [desktop] localhost ansible_connection=local [services] localhost ansible_connection=local [qa] localhost ansible_connection=local [ansible] localhost ansible_connection=local [tailscale] localhost ansible_connection=local [local] localhost ansible_connection=local EOF failed=0 for playbook in playbooks/*.yml site.yml configure_app.yml provision_vms.yml; do [ -f "$playbook" ] || continue echo "Testing $playbook..." if ansible-playbook -i /tmp/ci-inventory.ini "$playbook" --syntax-check --list-tasks; then echo "✓ $playbook validated (syntax-check + list-tasks)" else echo "✗ $playbook failed validation (syntax-check/list-tasks)" failed=1 fi done if [ $failed -eq 1 ]; then echo "❌ Some playbooks failed CI validation." echo "This should not require production inventory or vault secrets." exit 1 else echo "✅ All playbooks passed CI validation" fi container-scan: needs: skip-ci-check if: needs.skip-ci-check.outputs.should-skip != '1' runs-on: ubuntu-latest container: image: ubuntu:22.04 steps: - name: Install Node.js for checkout action run: | apt-get update && apt-get install -y curl curl -fsSL https://deb.nodesource.com/setup_20.x | bash - apt-get install -y nodejs - name: Check out code uses: actions/checkout@v4 - name: Install Trivy run: | set -e apt-get update && apt-get install -y wget curl tar # Use a fixed, known-good Trivy version to avoid URL/redirect issues TRIVY_VERSION="0.58.2" TRIVY_URL="https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz" echo "Installing Trivy version: ${TRIVY_VERSION}" echo "Downloading from: ${TRIVY_URL}" if ! wget --progress=bar:force "${TRIVY_URL}" -O /tmp/trivy.tar.gz 2>&1; then echo "❌ Failed to download Trivy archive" echo "Checking if file was partially downloaded:" ls -lh /tmp/trivy.tar.gz 2>/dev/null || echo "No file found" exit 1 fi if [ ! -f /tmp/trivy.tar.gz ] || [ ! -s /tmp/trivy.tar.gz ]; then echo "❌ Downloaded Trivy archive is missing or empty" exit 1 fi echo "Download complete. File size: $(du -h /tmp/trivy.tar.gz | cut -f1)" echo "Extracting Trivy..." if ! tar -xzf /tmp/trivy.tar.gz -C /tmp/ trivy; then echo "❌ Failed to extract Trivy binary from archive" tar -tzf /tmp/trivy.tar.gz 2>&1 | head -20 || true exit 1 fi if [ ! -f /tmp/trivy ]; then echo "❌ Trivy binary not found after extraction" ls -la /tmp/ | grep trivy || ls -la /tmp/ | head -20 exit 1 fi mv /tmp/trivy /usr/local/bin/trivy chmod +x /usr/local/bin/trivy /usr/local/bin/trivy --version trivy --version - name: Scan for Dockerfiles and container configs run: | if [ -f "Dockerfile" ] || [ -f "docker-compose.yml" ] || find . -name "Dockerfile*" -o -name "*.dockerfile" 2>/dev/null | grep -v ".git" | head -1 > /dev/null; then echo "Dockerfiles found. Scanning filesystem for container-related vulnerabilities..." echo "Note: This scans filesystem, not built images." echo "To scan actual images, build them first and use: trivy image " trivy fs --scanners vuln --severity HIGH,CRITICAL --format table . || true else echo "No Dockerfiles found, skipping container image scan" exit 0 fi continue-on-error: true sonar-analysis: needs: skip-ci-check if: needs.skip-ci-check.outputs.should-skip != '1' runs-on: ubuntu-latest container: image: ubuntu:22.04 env: SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} steps: - name: Check out code uses: actions/checkout@v4 - name: Install Java and SonarScanner run: | set -e apt-get update && apt-get install -y wget curl unzip openjdk-21-jre # Use a known working version to avoid download issues SONAR_SCANNER_VERSION="5.0.1.3006" SCANNER_URL="https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${SONAR_SCANNER_VERSION}-linux.zip" echo "Installing SonarScanner version: ${SONAR_SCANNER_VERSION}" echo "Downloading from: ${SCANNER_URL}" # Download with verbose error output if ! wget --progress=bar:force "${SCANNER_URL}" -O /tmp/sonar-scanner.zip 2>&1; then echo "❌ Failed to download SonarScanner" echo "Checking if file was partially downloaded:" ls -lh /tmp/sonar-scanner.zip 2>/dev/null || echo "No file found" exit 1 fi # Verify download if [ ! -f /tmp/sonar-scanner.zip ] || [ ! -s /tmp/sonar-scanner.zip ]; then echo "❌ Downloaded file is missing or empty" exit 1 fi echo "Download complete. File size: $(du -h /tmp/sonar-scanner.zip | cut -f1)" echo "Extracting SonarScanner..." if ! unzip -q /tmp/sonar-scanner.zip -d /tmp; then echo "❌ Failed to extract SonarScanner" echo "Archive info:" file /tmp/sonar-scanner.zip || true unzip -l /tmp/sonar-scanner.zip 2>&1 | head -20 || true exit 1 fi # Find the extracted directory (handle both naming conventions) EXTRACTED_DIR="" if [ -d "/tmp/sonar-scanner-${SONAR_SCANNER_VERSION}-linux" ]; then EXTRACTED_DIR="/tmp/sonar-scanner-${SONAR_SCANNER_VERSION}-linux" elif [ -d "/tmp/sonar-scanner-cli-${SONAR_SCANNER_VERSION}-linux" ]; then EXTRACTED_DIR="/tmp/sonar-scanner-cli-${SONAR_SCANNER_VERSION}-linux" else # Try to find any sonar-scanner directory EXTRACTED_DIR=$(find /tmp -maxdepth 1 -type d -name "*sonar-scanner*" | head -1) fi if [ -z "$EXTRACTED_DIR" ] || [ ! -d "$EXTRACTED_DIR" ]; then echo "❌ SonarScanner directory not found after extraction" echo "Contents of /tmp:" ls -la /tmp/ | grep -E "(sonar|zip)" || ls -la /tmp/ | head -20 exit 1 fi echo "Found extracted directory: ${EXTRACTED_DIR}" mv "${EXTRACTED_DIR}" /opt/sonar-scanner # Create symlink if [ -f /opt/sonar-scanner/bin/sonar-scanner ]; then ln -sf /opt/sonar-scanner/bin/sonar-scanner /usr/local/bin/sonar-scanner chmod +x /opt/sonar-scanner/bin/sonar-scanner chmod +x /usr/local/bin/sonar-scanner else echo "❌ sonar-scanner binary not found in /opt/sonar-scanner/bin/" echo "Contents of /opt/sonar-scanner/bin/:" ls -la /opt/sonar-scanner/bin/ || true exit 1 fi echo "Verifying installation..." if ! sonar-scanner --version; then echo "❌ SonarScanner verification failed" echo "PATH: $PATH" which sonar-scanner || echo "sonar-scanner not in PATH" exit 1 fi echo "✓ SonarScanner installed successfully" - name: Verify SonarQube connection run: | echo "Checking SonarQube connectivity..." if [ -z "$SONAR_HOST_URL" ] || [ -z "$SONAR_TOKEN" ]; then echo "❌ ERROR: SONAR_HOST_URL or SONAR_TOKEN secrets are not set!" echo "Please configure them in: Repository Settings → Actions → Secrets" exit 1 fi echo "✓ Secrets are configured" echo "SonarQube URL: ${SONAR_HOST_URL}" echo "Testing connectivity to SonarQube server..." if curl -f -s -o /dev/null -w "%{http_code}" "${SONAR_HOST_URL}/api/system/status" | grep -q "200"; then echo "✓ SonarQube server is reachable" else echo "⚠️ Warning: Could not verify SonarQube server connectivity" fi - name: Run SonarScanner run: | echo "Starting SonarQube analysis..." if ! sonar-scanner \ -Dsonar.projectKey=ansible \ -Dsonar.sources=. \ -Dsonar.host.url=${SONAR_HOST_URL} \ -Dsonar.token=${SONAR_TOKEN} \ -Dsonar.scm.disabled=true \ -Dsonar.python.version=3.10 \ -X; then echo "" echo "❌ SonarScanner analysis failed!" echo "" echo "Common issues:" echo " 1. Project 'ansible' doesn't exist in SonarQube" echo " → Create it manually in SonarQube UI" echo " 2. Token doesn't have permission to analyze/create project" echo " → Ensure token has 'Execute Analysis' permission" echo " 3. Token doesn't have 'Create Projects' permission (if project doesn't exist)" echo " → Grant this permission in SonarQube user settings" echo "" echo "Check SonarQube logs for more details." exit 1 fi continue-on-error: true workflow-summary: runs-on: ubuntu-latest needs: [lint-and-test, ansible-validation, secret-scanning, dependency-scan, sast-scan, license-check, vault-check, playbook-test, container-scan, sonar-analysis] if: always() steps: - name: Generate workflow summary run: | echo "## 🔍 CI Workflow Summary" >> $GITHUB_STEP_SUMMARY || true echo "" >> $GITHUB_STEP_SUMMARY || true echo "### Job Results" >> $GITHUB_STEP_SUMMARY || true echo "" >> $GITHUB_STEP_SUMMARY || true echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY || true echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY || true echo "| 📝 Markdown Linting | ${{ needs.lint-and-test.result }} |" >> $GITHUB_STEP_SUMMARY || true echo "| 🔧 Ansible Validation | ${{ needs.ansible-validation.result }} |" >> $GITHUB_STEP_SUMMARY || true echo "| 🔐 Secret Scanning | ${{ needs.secret-scanning.result }} |" >> $GITHUB_STEP_SUMMARY || true echo "| 📦 Dependency Scan | ${{ needs.dependency-scan.result }} |" >> $GITHUB_STEP_SUMMARY || true echo "| 🔍 SAST Scan | ${{ needs.sast-scan.result }} |" >> $GITHUB_STEP_SUMMARY || true echo "| 📄 License Check | ${{ needs.license-check.result }} |" >> $GITHUB_STEP_SUMMARY || true echo "| 🔒 Vault Check | ${{ needs.vault-check.result }} |" >> $GITHUB_STEP_SUMMARY || true echo "| 📋 Playbook Test | ${{ needs.playbook-test.result }} |" >> $GITHUB_STEP_SUMMARY || true echo "| 🐳 Container Scan | ${{ needs.container-scan.result }} |" >> $GITHUB_STEP_SUMMARY || true echo "| 🔍 SonarQube Analysis | ${{ needs.sonar-analysis.result }} |" >> $GITHUB_STEP_SUMMARY || true echo "" >> $GITHUB_STEP_SUMMARY || true echo "### 📊 Summary" >> $GITHUB_STEP_SUMMARY || true echo "" >> $GITHUB_STEP_SUMMARY || true echo "All security and validation checks have completed." >> $GITHUB_STEP_SUMMARY || true echo "" >> $GITHUB_STEP_SUMMARY || true echo "**Note:** Artifact uploads are not supported in Gitea Actions. Check individual job logs for detailed reports." >> $GITHUB_STEP_SUMMARY || true continue-on-error: true