CI:
sampleapp-cicd / ci (push) Failing after 19s
sampleapp-cicd / deploy (push) Skipped
sampleapp-cicd / notify (push) Successful in 2s

This commit is contained in:
2026-07-18 20:57:50 +02:00
parent 8716ff8ddc
commit 44d0d83366
+118 -1
View File
@@ -2,7 +2,7 @@ name: sampleapp-cicd
on:
push:
branches: ["**"]
branches: [main]
pull_request:
jobs:
@@ -35,6 +35,58 @@ jobs:
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: |
@@ -60,3 +112,68 @@ jobs:
--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" ]