ilia 35d17ed527
All checks were successful
CI / skip-ci-check (pull_request) Successful in 7s
CI / lint-and-test (pull_request) Successful in 12s
CI / ansible-validation (pull_request) Successful in 48s
CI / secret-scanning (pull_request) Successful in 7s
CI / dependency-scan (pull_request) Successful in 14s
CI / sast-scan (pull_request) Successful in 22s
CI / license-check (pull_request) Successful in 11s
CI / vault-check (pull_request) Successful in 10s
CI / playbook-test (pull_request) Successful in 24s
CI / container-scan (pull_request) Successful in 6s
CI / sonar-analysis (pull_request) Successful in 6s
CI / workflow-summary (pull_request) Successful in 6s
Fix CI sonar job: drop checkout (act mounts repo)
Latest sonar-scanner-cli is not Alpine; apk/nodejs bootstrap failed.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 22:06:21 -04:00

581 lines
21 KiB
YAML

---
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
runs-on: ubuntu-latest
if: needs.skip-ci-check.outputs.should-skip != '1' && (github.event_name == 'pull_request' || github.ref == 'refs/heads/master')
container:
image: node:20-bookworm
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
runs-on: ubuntu-latest
if: needs.skip-ci-check.outputs.should-skip != '1' && (github.event_name == 'pull_request' || github.ref == 'refs/heads/master')
env:
PIP_NO_CACHE_DIR: "1"
PIP_BREAK_SYSTEM_PACKAGES: "1"
container:
image: node:20-bookworm
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Bootstrap pip (PEP 668 / bookworm)
run: |
python3 --version
if ! python3 -m pip --version >/dev/null 2>&1; then
curl -fsSL https://bootstrap.pypa.io/get-pip.py -o /tmp/get-pip.py
python3 /tmp/get-pip.py --disable-pip-version-check --break-system-packages
fi
- name: Show disk space (runner may be full)
run: df -h / /tmp || true
- name: Configure CI Ansible (no vault, localhost inventory)
run: |
set -e
cat > /tmp/ci-inventory.ini <<'EOF'
[all]
localhost ansible_connection=local
EOF
cat > /tmp/ci-ansible.cfg <<EOF
[defaults]
inventory = /tmp/ci-inventory.ini
roles_path = ${GITHUB_WORKSPACE}/roles
host_key_checking = False
stdout_callback = default
callback_result_format = yaml
bin_ansible_callbacks = True
retry_files_enabled = False
interpreter_python = auto_silent
forks = 10
pipelining = True
EOF
echo "ANSIBLE_CONFIG=/tmp/ci-ansible.cfg" >> "$GITHUB_ENV"
echo "ANSIBLE_INVENTORY=/tmp/ci-inventory.ini" >> "$GITHUB_ENV"
- name: Install Ansible and linting tools
run: |
python3 -m pip install --no-cache-dir ansible-core ansible-lint yamllint pyyaml
ansible-galaxy collection install -r collections/requirements.yml
rm -rf /root/.cache/pip /tmp/pip-* 2>/dev/null || true
- name: Validate YAML syntax
run: |
echo "Checking YAML syntax..."
find . \( -name "*.yml" -o -name "*.yaml" \) \
! -path "./.git/*" \
! -path "./node_modules/*" \
! -path "./.venv/*" \
! -name "vault.yml" \
! -name "vault.yaml" \
! -name "vault_*.yml" \
! -name "vault_*.yaml" \
| while read -r file; do
if head -n 5 "$file" | grep -q '^\$ANSIBLE_VAULT'; then
echo "Skipping encrypted vault file: $file"
continue
fi
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: node:20-bookworm
steps:
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install and run Gitleaks
run: |
curl -sSfL https://github.com/gitleaks/gitleaks/releases/download/v8.21.2/gitleaks_8.21.2_linux_x64.tar.gz \
| tar -xz -C /usr/local/bin gitleaks
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
env:
PIP_NO_CACHE_DIR: "1"
PIP_BREAK_SYSTEM_PACKAGES: "1"
container:
image: node:20-bookworm
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Scan npm dependencies
run: |
if [ -f package-lock.json ]; then
npm ci
npm audit --audit-level=high
else
echo "No package-lock.json, skipping npm audit"
fi
continue-on-error: true
- name: Scan Python dependencies
run: |
if [ -f requirements.txt ]; then
if ! python3 -m pip --version >/dev/null 2>&1; then
curl -fsSL https://bootstrap.pypa.io/get-pip.py -o /tmp/get-pip.py
python3 /tmp/get-pip.py --disable-pip-version-check --break-system-packages
fi
python3 -m pip install --no-cache-dir pip-audit
python3 -m pip-audit -r requirements.txt
else
echo "No requirements.txt, skipping pip-audit"
fi
continue-on-error: true
sast-scan:
needs: skip-ci-check
if: needs.skip-ci-check.outputs.should-skip != '1'
runs-on: ubuntu-latest
env:
PIP_NO_CACHE_DIR: "1"
PIP_BREAK_SYSTEM_PACKAGES: "1"
container:
image: node:20-bookworm
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Bootstrap pip (PEP 668 / bookworm)
run: |
python3 --version
if ! python3 -m pip --version >/dev/null 2>&1; then
curl -fsSL https://bootstrap.pypa.io/get-pip.py -o /tmp/get-pip.py
python3 /tmp/get-pip.py --disable-pip-version-check --break-system-packages
fi
- name: Install Semgrep
run: python3 -m pip install --no-cache-dir 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-bookworm
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;Python-2.0;BlueOak-1.0.0;0BSD'
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
env:
PIP_NO_CACHE_DIR: "1"
PIP_BREAK_SYSTEM_PACKAGES: "1"
container:
image: node:20-bookworm
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Bootstrap pip (PEP 668 / bookworm)
run: |
if ! python3 -m pip --version >/dev/null 2>&1; then
curl -fsSL https://bootstrap.pypa.io/get-pip.py -o /tmp/get-pip.py
python3 /tmp/get-pip.py --disable-pip-version-check --break-system-packages
fi
- name: Install Ansible
run: python3 -m pip install --no-cache-dir ansible-core
- name: Validate vault files are encrypted
run: |
echo "Checking for Ansible Vault files..."
# Intentionally skip *.example files: they are plaintext templates.
# Only treat conventional vault files as "must be encrypted":
# - vault.yml / vault.yaml
# - vault_*.yml / vault_*.yaml
# Avoid false-positives like host_vars/vaultwardenVM.yml (host name contains "vault").
vault_files=$(find . \( -name "vault.yml" -o -name "vault.yaml" -o -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)
# Some vault files may start with '---' (YAML document start) on line 1.
if head -n 5 "$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 <file>"
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
env:
PIP_NO_CACHE_DIR: "1"
PIP_BREAK_SYSTEM_PACKAGES: "1"
container:
image: node:20-bookworm
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Bootstrap pip (PEP 668 / bookworm)
run: |
if ! python3 -m pip --version >/dev/null 2>&1; then
curl -fsSL https://bootstrap.pypa.io/get-pip.py -o /tmp/get-pip.py
python3 /tmp/get-pip.py --disable-pip-version-check --break-system-packages
fi
- name: Configure CI Ansible (no vault, localhost inventory)
run: |
set -e
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
[sites]
localhost ansible_connection=local
[comms]
localhost ansible_connection=local
[proxmox]
localhost ansible_connection=local
[caddy]
localhost ansible_connection=local
EOF
cat > /tmp/ci-ansible.cfg <<EOF
[defaults]
inventory = /tmp/ci-inventory.ini
roles_path = ${GITHUB_WORKSPACE}/roles
host_key_checking = False
stdout_callback = default
callback_result_format = yaml
bin_ansible_callbacks = True
retry_files_enabled = False
interpreter_python = auto_silent
forks = 10
pipelining = True
EOF
echo "ANSIBLE_CONFIG=/tmp/ci-ansible.cfg" >> "$GITHUB_ENV"
echo "ANSIBLE_INVENTORY=/tmp/ci-inventory.ini" >> "$GITHUB_ENV"
- name: Install Ansible
run: |
python3 -m pip install --no-cache-dir ansible-core
ansible-galaxy collection install -r collections/requirements.yml
rm -rf /root/.cache/pip /tmp/pip-* 2>/dev/null || true
- name: Validate playbooks (CI inventory, no vault)
run: |
set -e
echo "Validating playbooks against a CI-only localhost inventory (no vault required)..."
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: node:20-bookworm
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Install Trivy
continue-on-error: true
run: |
set -e
# 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 ! curl -fsSL "${TRIVY_URL}" -o /tmp/trivy.tar.gz; 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 <image:tag>"
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' && (github.event_name == 'pull_request' || github.ref == 'refs/heads/master')
runs-on: ubuntu-latest
continue-on-error: true
container:
image: sonarsource/sonar-scanner-cli:latest
env:
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
steps:
- name: Verify SonarQube connection
run: |
echo "Checking SonarQube connectivity..."
if [ -z "$SONAR_HOST_URL" ] || [ -z "$SONAR_TOKEN" ]; then
echo "⚠️ Skipping SonarQube analysis: SONAR_HOST_URL or SONAR_TOKEN secrets are not set."
exit 0
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 (continuing anyway)"
fi
- name: Run SonarScanner
run: |
echo "Starting SonarQube analysis..."
if [ -z "$SONAR_HOST_URL" ] || [ -z "$SONAR_TOKEN" ]; then
echo "Skipping SonarQube analysis: secrets not set."
exit 0
fi
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."
# Do not fail CI on Sonar auth/project setup issues.
exit 0
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