name: CI on: push: branches: [main] pull_request: branches: [main] schedule: - cron: "0 6 * * 1" jobs: build-and-scan: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v7 with: fetch-depth: 0 persist-credentials: false - name: Scan for secrets run: | # docker cp, not a build COPY: a repo's own .dockerignore (e.g. one that # excludes .git for prod builds) would otherwise silently give an empty, # falsely-clean scan. CID=$(docker create zricethezav/gitleaks:v8.30.1 detect --source=/repo --no-banner -v) docker cp . "$CID:/repo" docker start -a "$CID" STATUS=$? docker rm "$CID" > /dev/null exit $STATUS - name: Lint Dockerfile with hadolint run: docker run --rm -i hadolint/hadolint:v2.15.1-alpine hadolint --failure-threshold error - < Dockerfile - name: Build Docker image run: | docker build -t lumeex:ci . 2>&1 | tee build.log if grep -q "Building wheel for" build.log; then echo "::warning::A dependency was built from source โ€” check Python/Alpine compatibility" fi - name: Smoke test (syntax check) run: | docker run --rm --entrypoint python lumeex:ci -c " import ast ok = True for f in ('build.py', 'gallery.py'): with open(f) as fh: source = fh.read() try: ast.parse(source) except SyntaxError as e: print(f'::error::Syntax error in {f}: {e}') ok = False if not ok: exit(1) print('OK: syntax is valid') " - name: Run unit tests run: | docker build --target test -t lumeex:test . docker run --rm lumeex:test pytest -v --cov=. --cov-report=term-missing --cov-fail-under=90 - name: Lint with ruff run: docker build --target lint -t lumeex:lint . - name: Check deprecation warnings run: | docker run --rm --entrypoint python lumeex:ci -W error::DeprecationWarning -c "import src.py.webui.webui" 2>&1 | tee deprecation.log || true if grep -qi "deprecat" deprecation.log; then echo "::warning::Deprecation warning detected, check logs" fi - name: Scan with Trivy (critical - blocking) id: trivy_critical continue-on-error: true run: | docker run --rm \ -e DOCKER_HOST=tcp://dockerhost:2375 \ --add-host=dockerhost:host-gateway \ aquasec/trivy:0.74.0 image --exit-code 1 --severity CRITICAL lumeex:ci - name: Handle CRITICAL findings if: steps.trivy_critical.outcome == 'failure' run: | if [ "${{ github.event_name }}" != "schedule" ]; then echo "::error::CRITICAL vulnerabilities found, failing the build." exit 1 fi echo "Scheduled scan found CRITICAL vulnerabilities โ€” attempting an automatic apk upgrade + rescan." sed -i '/^FROM .* AS base$/a RUN apk upgrade --no-cache' Dockerfile docker build -t lumeex:remediated . if docker run --rm \ -e DOCKER_HOST=tcp://dockerhost:2375 \ --add-host=dockerhost:host-gateway \ aquasec/trivy:0.74.0 image --exit-code 1 --severity CRITICAL lumeex:remediated; then echo "apk upgrade clears the CRITICAL finding(s) โ€” opening a PR for review." BRANCH="auto/cve-fix-$(date +%Y%m%d)-$(echo "${{ github.sha }}" | cut -c1-7)" git config user.name "lumeex-ci" git config user.email "ci@git.djeex.fr" git checkout -b "$BRANCH" git add Dockerfile git commit -m "Auto-remediate CRITICAL CVE via apk upgrade" git config --unset-all http.https://git.djeex.fr/.extraheader || true git push "https://Djeex:${{ secrets.CI_PUSH_TOKEN }}@git.djeex.fr/Djeex/lumeex.git" "HEAD:$BRANCH" PR_JSON=$(curl -s -X POST \ -H "Authorization: token ${{ secrets.CI_PUSH_TOKEN }}" \ -H "Content-Type: application/json" \ -d "$(jq -n --arg head "$BRANCH" '{title: "๐Ÿ”’ Auto: remediate CRITICAL CVE via apk upgrade", head: $head, base: "main", body: "Opened automatically by the scheduled CVE scan. An `apk upgrade --no-cache` cleared the CRITICAL Trivy finding(s) in a rebuild โ€” review the diff and merge to publish the fix."}')" \ "https://git.djeex.fr/api/v1/repos/Djeex/lumeex/pulls") echo "PR API response: $(echo "$PR_JSON" | jq -r '.html_url // .message // "unknown"')" else echo "::error::apk upgrade does not clear the CRITICAL finding(s) โ€” no automatic fix available, needs manual review." exit 1 fi - name: Scan with Trivy (high - informative) run: | docker run --rm \ -e DOCKER_HOST=tcp://dockerhost:2375 \ --add-host=dockerhost:host-gateway \ aquasec/trivy:0.74.0 image --exit-code 0 --severity HIGH lumeex:ci - name: Publish tagged image if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: | BEFORE="${{ github.event.before }}" if [ -n "$BEFORE" ] && [ "$BEFORE" != "0000000000000000000000000000000000000000" ] && git cat-file -e "$BEFORE" 2>/dev/null; then BASE_REF="$BEFORE" else BASE_REF="HEAD~1" fi CHANGED=$(git diff --name-only "$BASE_REF" "${{ github.sha }}") echo "Changed files:" echo "$CHANGED" if ! echo "$CHANGED" | grep -qE '^(Dockerfile|requirements\.txt|VERSION|build\.py|gallery\.py)$|^(src|config)/|^docker/\.sh/entrypoint\.sh$'; then echo "No container-relevant file changed, skipping publish." exit 0 fi if echo "$CHANGED" | grep -qE '^VERSION$'; then echo "VERSION was manually edited in this push, using it as-is." else echo "VERSION untouched but container files changed, auto-bumping the build number (Z)." OLD_VERSION=$(tr -d '[:space:]' < VERSION) IFS='.' read -r MAJOR MINOR PATCH <<< "$OLD_VERSION" NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))" echo "$NEW_VERSION" > VERSION git config user.name "lumeex-ci" git config user.email "ci@git.djeex.fr" git add VERSION git commit -m "Bump build version to $NEW_VERSION [skip ci]" # Belt and suspenders: actions/checkout can leave its own ephemeral # credential injected as an extraheader, which would silently override # the URL-embedded token below. persist-credentials:false on checkout # should already prevent this, but strip it here too just in case. git config --unset-all http.https://git.djeex.fr/.extraheader || true git push "https://Djeex:${{ secrets.CI_PUSH_TOKEN }}@git.djeex.fr/Djeex/lumeex.git" HEAD:main fi VERSION=$(tr -d '[:space:]' < VERSION) IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION" MINOR_TAG="${MAJOR}.${MINOR}" IMAGE=git.djeex.fr/djeex/lumeex echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.djeex.fr -u Djeex --password-stdin # Retag the already-built, already-scanned image โ€” never rebuild for publish, # so what ships is byte-for-byte what Trivy just scanned. docker tag lumeex:ci "$IMAGE:latest" docker tag lumeex:ci "$IMAGE:$MINOR_TAG" docker tag lumeex:ci "$IMAGE:$VERSION" docker push "$IMAGE:latest" docker push "$IMAGE:$MINOR_TAG" docker push "$IMAGE:$VERSION" TRIGGER_MSG=$(git log -1 --format=%s "${{ github.sha }}") PR_NUM=$(echo "$TRIGGER_MSG" | grep -oE '#[0-9]+' | head -1 | tr -d '#' || true) CATEGORY="๐Ÿ”ง Maintenance" CHANGE_TITLE="$TRIGGER_MSG" if [ -n "$PR_NUM" ]; then PR_JSON=$(curl -s -H "Authorization: token ${{ secrets.CI_PUSH_TOKEN }}" \ "https://git.djeex.fr/api/v1/repos/Djeex/lumeex/pulls/$PR_NUM") PR_TITLE=$(echo "$PR_JSON" | jq -r '.title // empty' 2>/dev/null || true) LABELS=$(echo "$PR_JSON" | jq -r '.labels[]?.name' 2>/dev/null || true) if [ -n "$PR_TITLE" ]; then CHANGE_TITLE="$PR_TITLE" fi if echo "$LABELS" | grep -qx 'bug'; then CATEGORY="โš ๏ธ Hotfix" elif echo "$LABELS" | grep -qx 'major'; then CATEGORY="๐Ÿ’ฅ Breaking change" elif echo "$LABELS" | grep -qx 'minor'; then CATEGORY="โœจ Update" fi fi REPO_URL="https://git.djeex.fr/Djeex/lumeex" COMMIT_LIST=$(git log --no-merges --format="- %s ([%h](${REPO_URL}/commit/%H))" "$BASE_REF".."${{ github.sha }}") SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7) SOURCE_LINE="[${SHORT_SHA}](${REPO_URL}/commit/${{ github.sha }})" if [ -n "$PR_NUM" ]; then SOURCE_LINE="[#${PR_NUM}](${REPO_URL}/pulls/${PR_NUM}) ยท ${SOURCE_LINE}" fi BODY=$(cat <