ilia c3e6caf9e8
All checks were successful
CI / skip-ci-check (push) Successful in 1m18s
CI / lint-and-test (push) Successful in 1m23s
CI / ansible-validation (push) Successful in 3m2s
CI / secret-scanning (push) Successful in 1m19s
CI / dependency-scan (push) Successful in 1m24s
CI / sast-scan (push) Successful in 2m32s
CI / license-check (push) Successful in 1m23s
CI / vault-check (push) Successful in 2m22s
CI / playbook-test (push) Successful in 2m25s
CI / container-scan (push) Successful in 1m51s
CI / sonar-analysis (push) Successful in 2m32s
CI / workflow-summary (push) Successful in 1m17s
refactor-servers-workstations-shell-monitoring (#4)
### Summary

This PR refactors the playbook layout to reduce duplication and make host intent clearer (servers vs workstations), splits monitoring by host type, and restores full Zsh setup for developers while keeping servers aliases-only.

### Key changes

- **New playbooks**
  - `playbooks/servers.yml`: baseline for server-class hosts (no desktop apps)
  - `playbooks/workstations.yml`: baseline for dev/desktop/local + **desktop apps only on `desktop` group**

- **Monitoring split**
  - `roles/monitoring_server`: server monitoring + intrusion prevention (includes `fail2ban`, sysstat)
  - `roles/monitoring_desktop`: desktop-oriented monitoring tooling
  - Updated playbooks to use the correct monitoring role per host type

- **Shell role: server-safe + developer-friendly**
  - `roles/shell` now supports two modes:
    - `shell_mode: minimal` (default): aliases-only, does not overwrite `.zshrc`
    - `shell_mode: full`: installs Oh My Zsh + Powerlevel10k + plugins and deploys a managed `.zshrc`
  - `playbooks/development.yml` and `playbooks/workstations.yml` use `shell_mode: full`
  - `playbooks/servers.yml` remains **aliases-only**

- **Applications**
  - Applications role runs only on `desktop` group (via `workstations.yml`)
  - Removed Brave installs/repo management
  - Added **CopyQ** to desktop apps (`applications_desktop_packages`)

- **Docs + architecture**
  - Added canonical doc tree under `project-docs/` (overview/architecture/standards/workflow/decisions)
  - Consolidated architecture docs: `docs/reference/architecture.md` is now a pointer to `project-docs/architecture.md`
  - Fixed broken doc links by adding the missing referenced pages under `docs/`

### Behavior changes (important)

- Desktop GUI apps install **only** on the `desktop` inventory group (not on servers, not on dev VMs unless they are in `desktop`).
- Dev/workstation Zsh is now provisioned in **full mode** (managed `.zshrc` + p10k).

### How to test (local CI parity)

```bash
make test
npm test
```

Optional dry runs (interactive sudo may be required):

```bash
make check
make check-local
```

### Rollout guidance

- Apply to a single host first:
  - Workstations: `make workstations HOST=<devhost>`
  - Servers: `make servers HOST=<serverhost>`
- Then expand to group runs.

Reviewed-on: #4
2026-01-01 22:11:24 -05:00

662 lines
24 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-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
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: 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: 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 = /workspace/ilia/ansible/roles
host_key_checking = False
stdout_callback = 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 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;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
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.
# 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
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: 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
EOF
cat > /tmp/ci-ansible.cfg <<'EOF'
[defaults]
inventory = /tmp/ci-inventory.ini
roles_path = /workspace/ilia/ansible/roles
host_key_checking = False
stdout_callback = 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 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)..."
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 <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
container:
image: ubuntu:22.04
env:
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
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 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 "⚠️ 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