name: sampleapp-cicd on: push: branches: [main] pull_request: jobs: ci: runs-on: ubuntu-latest env: REGISTRY: gitea.tests01.helity.org IMAGE: gitea.tests01.helity.org/lab/sampleapp steps: - name: Checkout uses: actions/checkout@v4 - name: Lint / validate content run: | set -e test -f index.html test -f Dockerfile grep -q "" index.html grep -q "FROM nginx" Dockerfile echo "lint OK" - name: Inject commit SHA into page run: | set -e sed -i "s/__GIT_SHA__/${GITHUB_SHA}/g" index.html grep -q "${GITHUB_SHA}" index.html && echo "sha injected: ${GITHUB_SHA}" - name: Build image run: | set -e docker build -t "${IMAGE}:${GITHUB_SHA}" -t "${IMAGE}:latest" . # --- Feature 4: Trivy vulnerability scan (gate) -------------------------- # Install the Trivy binary from the official script (the runner reaches the # internet; a pinned binary is more reproducible here than the # trivy-action, which resolves from github.com/master). The scan runs # against the locally-built image over the runner's docker.sock. - name: Install Trivy run: | set -e curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \ | sh -s -- -b /usr/local/bin v0.58.1 trivy --version # Gate: fail ci on any fixable CRITICAL/HIGH so a vulnerable image never # gets pushed or deployed. ignore-unfixed keeps the gate real but avoids # failing on distro CVEs with no available fix (nginx:alpine currently has # zero fixable CRITICAL/HIGH — the gate passes clean but is genuinely # enforcing). --scanners vuln keeps it to CVEs (no secret/misconfig noise). - name: Trivy scan (gate CRITICAL,HIGH) run: | set -e trivy image \ --exit-code 1 \ --severity CRITICAL,HIGH \ --ignore-unfixed \ --scanners vuln \ --no-progress \ "${IMAGE}:${GITHUB_SHA}" echo "trivy gate passed (no fixable CRITICAL/HIGH)" # --- Feature 4: SBOM (CycloneDX) generation + artifact upload ----------- - name: Generate SBOM (CycloneDX) if: always() run: | set -e trivy image \ --format cyclonedx \ --scanners vuln \ --no-progress \ --output sbom.cdx.json \ "${IMAGE}:${GITHUB_SHA}" echo "SBOM components: $(python3 -c 'import json; print(len(json.load(open("sbom.cdx.json")).get("components",[])))' 2>/dev/null || echo n/a)" ls -l sbom.cdx.json - name: Upload SBOM artifact if: always() uses: https://gitea.com/actions/upload-artifact@v4 with: name: sbom-cyclonedx-${{ github.sha }} path: sbom.cdx.json if-no-files-found: warn # --- Push (main only) --------------------------------------------------- - name: Push image (main only) if: github.ref == 'refs/heads/main' run: | set -e echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "${REGISTRY}" -u "${{ secrets.REGISTRY_USER }}" --password-stdin docker push "${IMAGE}:${GITHUB_SHA}" docker push "${IMAGE}:latest" docker logout "${REGISTRY}" deploy: runs-on: ubuntu-latest needs: ci if: github.ref == 'refs/heads/main' steps: - name: Trigger Rundeck deploy job run: | set -e code=$(curl -fsS -o /tmp/rd.json -w '%{http_code}' -X POST \ -H "X-Rundeck-Auth-Token: ${{ secrets.RUNDECK_TOKEN }}" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ "${{ secrets.RUNDECK_URL }}/api/40/job/${{ secrets.DEPLOY_JOB_UUID }}/run" \ --data "{\"options\":{\"image_tag\":\"${GITHUB_SHA}\"}}") echo "Rundeck run HTTP ${code}" python3 -c "import json;d=json.load(open('/tmp/rd.json'));print('execution id:',d.get('id'),'status:',d.get('status'),'permalink:',d.get('permalink'))" # --- Feature 2: notifications (Mattermost success+failure, GoAlert on failure) notify: runs-on: ubuntu-latest needs: [ci, deploy] if: always() env: MM_HOOK: ${{ secrets.MATTERMOST_WEBHOOK_URL }} GA_KEY: ${{ secrets.GOALERT_KEY_URL }} steps: - name: Compute pipeline outcome id: outcome run: | set -e CI="${{ needs.ci.result }}" DEPLOY="${{ needs.deploy.result }}" # deploy is skipped on non-main (PR) runs; treat skipped as non-failing. if [ "$CI" = "success" ] && { [ "$DEPLOY" = "success" ] || [ "$DEPLOY" = "skipped" ]; }; then echo "state=success" >> "$GITHUB_OUTPUT" echo "emoji=:white_check_mark:" >> "$GITHUB_OUTPUT" else echo "state=failure" >> "$GITHUB_OUTPUT" echo "emoji=:rotating_light:" >> "$GITHUB_OUTPUT" fi - name: Notify Mattermost (success or failure) env: STATE: ${{ steps.outcome.outputs.state }} EMOJI: ${{ steps.outcome.outputs.emoji }} CI_RESULT: ${{ needs.ci.result }} DEPLOY_RESULT: ${{ needs.deploy.result }} REPO: ${{ github.repository }} REF: ${{ github.ref_name }} SHA: ${{ github.sha }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_number }} run: | set -e # Build the text + JSON payload with python (env in, JSON out) so no # shell quoting of the message can break the webhook body. The webhook # is locked to the deployments channel, so no channel field is sent. # Single-line python keeps every line inside the YAML block scalar. payload="$(python3 -c 'import json,os; e=os.environ; t="%s **sampleapp-cicd %s** on `%s@%s`\nci=`%s` deploy=`%s`\ncommit `%s`\n%s" % (e["EMOJI"], e["STATE"], e["REPO"], e["REF"], e["CI_RESULT"], e["DEPLOY_RESULT"], e["SHA"], e["RUN_URL"]); print(json.dumps({"text": t}))')" code="$(curl -sS -o /dev/null -w '%{http_code}' -X POST \ -H "Content-Type: application/json" -d "$payload" "$MM_HOOK")" echo "mattermost webhook HTTP ${code}" [ "$code" = "200" ] - name: Notify GoAlert (failure only) if: steps.outcome.outputs.state == 'failure' env: CI_RESULT: ${{ needs.ci.result }} DEPLOY_RESULT: ${{ needs.deploy.result }} REPO: ${{ github.repository }} SHA: ${{ github.sha }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_number }} run: | set -e # GoAlert generic API: summary via query param, details via JSON body, # dedup collapses repeated failures onto one alert. Success = HTTP 204. url="$(python3 -c 'import os,urllib.parse; e=os.environ; base=e["GA_KEY"]; sep="&" if "?" in base else "?"; q=urllib.parse.urlencode({"summary":"CI/CD FAILED: %s (ci=%s deploy=%s)" % (e["REPO"], e["CI_RESULT"], e["DEPLOY_RESULT"]), "dedup":"sampleapp-cicd-failure"}); print(base+sep+q)')" body="$(python3 -c 'import json,os; print(json.dumps({"details":"commit %s - %s" % (os.environ["SHA"], os.environ["RUN_URL"]), "action":"trigger"}))')" code="$(curl -sS -o /dev/null -w '%{http_code}' -X POST \ -H "Content-Type: application/json" -d "$body" "$url")" echo "goalert generic HTTP ${code}" [ "$code" = "204" ] || [ "$code" = "200" ]