Files
sampleapp/.gitea/workflows/ci.yaml
T
gitea-admin 12fb361466
sampleapp-cicd / ci (push) Failing after 20s
sampleapp-cicd / deploy (push) Skipped
sampleapp-cicd / notify (push) Successful in 2s
CI:
2026-07-18 21:01:06 +02:00

182 lines
8.0 KiB
YAML

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 "<title>" 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) + SBOM ------------------
# Trivy runs from its official container over the runner's docker.sock, so
# there is no fragile binary install and it can scan the locally-built
# image. A shared cache + shared workdir are bind-mounted; the SBOM is
# written to the job workspace so the upload step can find it. A single
# `docker run` per action keeps the runner-in-runner path simple.
- name: Trivy scan (gate CRITICAL,HIGH)
run: |
set -e
# 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.
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v trivy-cache:/root/.cache/ \
aquasec/trivy:0.58.1 image \
--exit-code 1 \
--severity CRITICAL,HIGH \
--ignore-unfixed \
--scanners vuln \
--no-progress \
"${IMAGE}:${GITHUB_SHA}"
echo "trivy gate passed (no fixable CRITICAL/HIGH)"
- name: Generate SBOM (CycloneDX)
if: always()
run: |
set -e
# Write the SBOM to trivy's stdout and redirect it into the job
# workspace. A bind-mounted --output would land on the HOST path
# (docker.sock is the host daemon, so a sibling container's -v resolves
# host-side, not inside this job container); the stdout redirect writes
# straight into the job filesystem where the upload step can find it.
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v trivy-cache:/root/.cache/ \
aquasec/trivy:0.58.1 image \
--format cyclonedx \
--scanners vuln \
--no-progress \
"${IMAGE}:${GITHUB_SHA}" > sbom.cdx.json
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" ]