# Copyright (C) Project Tick # SPDX-License-Identifier: MIT # # ╔══════════════════════════════════════════════════════════════════╗ # ║ Project Tick — Monolithic CI Orchestrator ║ # ║ ║ # ║ Every push, pull request, merge queue entry, tag push, and ║ # ║ manual dispatch flows through this single gate. Nothing runs ║ # ║ unless this file says so. ║ # ╚══════════════════════════════════════════════════════════════════╝ name: CI on: push: branches: ["**"] tags: ["*"] pull_request: types: [opened, synchronize, reopened, ready_for_review] pull_request_target: types: [closed, labeled] merge_group: types: [checks_requested] workflow_dispatch: inputs: force-all: description: "Force run all project CI pipelines" type: boolean default: false build-type: description: "Build configuration for meshmc/forgewrapper" type: choice options: [Debug, Release] default: Debug permissions: contents: read concurrency: group: >- ci-${{ github.event_name }}-${{ github.event_name == 'merge_group' && github.event.merge_group.head_ref || (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && format('pr-{0}', github.event.pull_request.number) || github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} # ════════════════════════════════════════════════════════════════════ # Environment — shared across all jobs # ════════════════════════════════════════════════════════════════════ env: CI: true FORCE_ALL: ${{ github.event.inputs.force-all == 'true' || github.event_name == 'merge_group' }} jobs: # ╔════════════════════════════════════════════════════════════════╗ # ║ STAGE 0 — Gate & Triage ║ # ╚════════════════════════════════════════════════════════════════╝ gate: name: "Gate" runs-on: ubuntu-latest if: >- !(github.event_name == 'pull_request_target' && !github.event.pull_request.merged) && !(github.event_name == 'pull_request' && github.event.pull_request.draft) outputs: # ── Event classification ──────────────────────────────────── is_push: ${{ steps.classify.outputs.is_push }} is_pr: ${{ steps.classify.outputs.is_pr }} is_merge_queue: ${{ steps.classify.outputs.is_merge_queue }} is_tag: ${{ steps.classify.outputs.is_tag }} is_release_tag: ${{ steps.classify.outputs.is_release_tag }} is_backport: ${{ steps.classify.outputs.is_backport }} is_dependabot: ${{ steps.classify.outputs.is_dependabot }} is_master: ${{ steps.classify.outputs.is_master }} is_scheduled: ${{ steps.classify.outputs.is_scheduled }} run_level: ${{ steps.classify.outputs.run_level }} # ── Per-project change flags ──────────────────────────────── archived_changed: ${{ steps.changes.outputs.archived_changed }} cgit_changed: ${{ steps.changes.outputs.cgit_changed }} ci_changed: ${{ steps.changes.outputs.ci_changed }} cmark_changed: ${{ steps.changes.outputs.cmark_changed }} corebinutils_changed: ${{ steps.changes.outputs.corebinutils_changed }} forgewrapper_changed: ${{ steps.changes.outputs.forgewrapper_changed }} genqrcode_changed: ${{ steps.changes.outputs.genqrcode_changed }} hooks_changed: ${{ steps.changes.outputs.hooks_changed }} images4docker_changed: ${{ steps.changes.outputs.images4docker_changed }} json4cpp_changed: ${{ steps.changes.outputs.json4cpp_changed }} libnbtplusplus_changed: ${{ steps.changes.outputs.libnbtplusplus_changed }} meshmc_changed: ${{ steps.changes.outputs.meshmc_changed }} meta_changed: ${{ steps.changes.outputs.meta_changed }} mnv_changed: ${{ steps.changes.outputs.mnv_changed }} neozip_changed: ${{ steps.changes.outputs.neozip_changed }} tomlplusplus_changed: ${{ steps.changes.outputs.tomlplusplus_changed }} github_changed: ${{ steps.changes.outputs.github_changed }} root_changed: ${{ steps.changes.outputs.root_changed }} changed_projects: ${{ steps.changes.outputs.changed_projects }} changed_count: ${{ steps.changes.outputs.changed_count }} # ── Commit parsing ────────────────────────────────────────── commit_type: ${{ steps.changes.outputs.commit_type }} commit_scope: ${{ steps.changes.outputs.commit_scope }} commit_subject: ${{ steps.changes.outputs.commit_subject }} commit_breaking: ${{ steps.changes.outputs.commit_breaking }} commit_message: ${{ steps.changes.outputs.commit_message }} # ── Build config ──────────────────────────────────────────── build_type: ${{ steps.classify.outputs.build_type }} steps: - name: Harden runner uses: step-security/harden-runner@v2 with: egress-policy: audit - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Classify event id: classify env: GH_HEAD_REF: ${{ github.head_ref }} GH_BASE_REF: ${{ github.base_ref }} run: | # shellcheck disable=SC2129 set -euo pipefail REF="${GITHUB_REF:-}" EVENT="${{ github.event_name }}" ACTOR="${{ github.actor }}" HEAD_REF="${GH_HEAD_REF:-}" # ── Booleans ────────────────────────────────────────── { echo "is_push=$([[ "$EVENT" == "push" ]] && echo true || echo false)" echo "is_pr=$([[ "$EVENT" == "pull_request" || "$EVENT" == "pull_request_target" ]] && echo true || echo false)" echo "is_merge_queue=$([[ "$EVENT" == "merge_group" ]] && echo true || echo false)" echo "is_tag=$([[ "$REF" == refs/tags/* ]] && echo true || echo false)" echo "is_release_tag=$([[ "$REF" =~ ^refs/tags/(meshmc|neozip|mnv|cmark|forgewrapper)- ]] && echo true || echo false)" echo "is_backport=$([[ "$HEAD_REF" == backport-* || "$HEAD_REF" == backport/* ]] && echo true || echo false)" echo "is_dependabot=$([[ "$ACTOR" == "dependabot[bot]" ]] && echo true || echo false)" echo "is_master=$([[ "$REF" == "refs/heads/master" || "$REF" == "refs/heads/main" ]] && echo true || echo false)" echo "is_scheduled=false" } >> "$GITHUB_OUTPUT" # ── Run level ───────────────────────────────────────── # full = merge queue, tags, master push, manual force-all # standard = normal PR, branch push # minimal = dependabot, backport, draft if [[ "$EVENT" == "merge_group" ]] || \ [[ "$REF" == refs/tags/* ]] || \ [[ "$REF" == "refs/heads/master" ]] || \ [[ "${{ env.FORCE_ALL }}" == "true" ]]; then echo "run_level=full" >> "$GITHUB_OUTPUT" elif [[ "$ACTOR" == "dependabot[bot]" ]]; then echo "run_level=minimal" >> "$GITHUB_OUTPUT" elif [[ "$HEAD_REF" == backport-* ]]; then echo "run_level=standard" >> "$GITHUB_OUTPUT" else echo "run_level=standard" >> "$GITHUB_OUTPUT" fi # ── Build type ──────────────────────────────────────── if [[ "$REF" == refs/tags/* ]]; then echo "build_type=Release" >> "$GITHUB_OUTPUT" elif [[ -n "${{ github.event.inputs.build-type || '' }}" ]]; then echo "build_type=${{ github.event.inputs.build-type }}" >> "$GITHUB_OUTPUT" else echo "build_type=Debug" >> "$GITHUB_OUTPUT" fi - name: Detect changes id: changes if: github.event_name != 'pull_request_target' uses: ./.github/actions/change-analysis with: event-name: ${{ github.event_name }} base-sha: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || '' }} head-sha: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha || '' }} before-sha: ${{ github.event.before || '' }} current-sha: ${{ github.sha }} pr-title: ${{ github.event.pull_request.title || '' }} # ╔════════════════════════════════════════════════════════════════╗ # ║ STAGE 1 — Lint & Commit Checks (fast, blocks everything) ║ # ╚════════════════════════════════════════════════════════════════╝ lint: name: "Lint" needs: gate if: needs.gate.outputs.is_pr == 'true' uses: ./.github/workflows/ci-lint.yml with: run-level: ${{ needs.gate.outputs.run_level }} changed-projects: ${{ needs.gate.outputs.changed_projects }} secrets: inherit dependency-review: name: "Dependency Review" needs: gate if: needs.gate.outputs.is_pr == 'true' uses: ./.github/workflows/repo-dependency-review.yml secrets: inherit # ╔════════════════════════════════════════════════════════════════╗ # ║ STAGE 2 — Per-Project Build & Test Matrices ║ # ║ ║ # ║ Each project runs only when its directory changed, or when ║ # ║ force-all / merge-queue / master push triggers them all. ║ # ║ Inner reusable workflows handle their own matrices. ║ # ╚════════════════════════════════════════════════════════════════╝ # ── C / System Projects ───────────────────────────────────────── mnv: name: "MNV" needs: [gate, lint] if: >- always() && !cancelled() && (needs.lint.result == 'success' || needs.lint.result == 'skipped') && (needs.gate.outputs.mnv_changed == 'true' || needs.gate.outputs.run_level == 'full') uses: ./.github/workflows/mnv-ci.yml permissions: contents: read secrets: inherit cgit: name: "CGit" needs: [gate, lint] if: >- always() && !cancelled() && (needs.lint.result == 'success' || needs.lint.result == 'skipped') && (needs.gate.outputs.cgit_changed == 'true' || needs.gate.outputs.run_level == 'full') uses: ./.github/workflows/cgit-ci.yml permissions: contents: read secrets: inherit cmark: name: "CMark" needs: [gate, lint] if: >- always() && !cancelled() && (needs.lint.result == 'success' || needs.lint.result == 'skipped') && (needs.gate.outputs.cmark_changed == 'true' || needs.gate.outputs.run_level == 'full') uses: ./.github/workflows/cmark-ci.yml permissions: contents: read secrets: inherit corebinutils: name: "CoreBinutils" needs: [gate, lint] if: >- always() && !cancelled() && (needs.lint.result == 'success' || needs.lint.result == 'skipped') && (needs.gate.outputs.corebinutils_changed == 'true' || needs.gate.outputs.run_level == 'full') uses: ./.github/workflows/corebinutils-ci.yml permissions: contents: read secrets: inherit genqrcode: name: "GenQRCode" needs: [gate, lint] if: >- always() && !cancelled() && (needs.lint.result == 'success' || needs.lint.result == 'skipped') && (needs.gate.outputs.genqrcode_changed == 'true' || needs.gate.outputs.run_level == 'full') uses: ./.github/workflows/genqrcode-ci.yml permissions: contents: read secrets: inherit neozip: name: "NeoZip" needs: [gate, lint] if: >- always() && !cancelled() && (needs.lint.result == 'success' || needs.lint.result == 'skipped') && (needs.gate.outputs.neozip_changed == 'true' || needs.gate.outputs.run_level == 'full') uses: ./.github/workflows/neozip-ci.yml permissions: contents: read actions: read security-events: write secrets: inherit # ── C++ / Library Projects ────────────────────────────────────── json4cpp: name: "JSON4CPP" needs: [gate, lint] if: >- always() && !cancelled() && (needs.lint.result == 'success' || needs.lint.result == 'skipped') && (needs.gate.outputs.json4cpp_changed == 'true' || needs.gate.outputs.run_level == 'full') uses: ./.github/workflows/json4cpp-ci.yml permissions: contents: read security-events: write secrets: inherit libnbtplusplus: name: "libNBT++" needs: [gate, lint] if: >- always() && !cancelled() && (needs.lint.result == 'success' || needs.lint.result == 'skipped') && (needs.gate.outputs.libnbtplusplus_changed == 'true' || needs.gate.outputs.run_level == 'full') uses: ./.github/workflows/libnbtplusplus-ci.yml permissions: contents: read secrets: inherit tomlplusplus: name: "TOML++" needs: [gate, lint] if: >- always() && !cancelled() && (needs.lint.result == 'success' || needs.lint.result == 'skipped') && (needs.gate.outputs.tomlplusplus_changed == 'true' || needs.gate.outputs.run_level == 'full') uses: ./.github/workflows/tomlplusplus-ci.yml permissions: contents: read secrets: inherit # ── Java / Minecraft Projects ─────────────────────────────────── meshmc: name: "MeshMC" needs: [gate, lint] if: >- always() && !cancelled() && (needs.lint.result == 'success' || needs.lint.result == 'skipped') && (needs.gate.outputs.meshmc_changed == 'true' || needs.gate.outputs.run_level == 'full') uses: ./.github/workflows/meshmc-build.yml with: build-type: ${{ needs.gate.outputs.build_type }} permissions: contents: read id-token: write packages: write secrets: inherit forgewrapper: name: "ForgeWrapper" needs: [gate, lint] if: >- always() && !cancelled() && (needs.lint.result == 'success' || needs.lint.result == 'skipped') && (needs.gate.outputs.forgewrapper_changed == 'true' || needs.gate.outputs.run_level == 'full') uses: ./.github/workflows/forgewrapper-build.yml permissions: contents: read secrets: inherit # ── Container & Docker ────────────────────────────────────────── images4docker: name: "Docker Images" needs: [gate, lint] if: >- always() && !cancelled() && (needs.lint.result == 'success' || needs.lint.result == 'skipped') && (needs.gate.outputs.images4docker_changed == 'true' || needs.gate.outputs.run_level == 'full') uses: ./.github/workflows/images4docker-build.yml permissions: contents: read packages: write secrets: inherit # ╔════════════════════════════════════════════════════════════════╗ # ║ STAGE 3 — Extended Analysis (only on full runs) ║ # ║ ║ # ║ Fuzz testing, CodeQL, static analysis — expensive jobs that ║ # ║ run on merge queue, master, or manual dispatch only. ║ # ╚════════════════════════════════════════════════════════════════╝ cmark-fuzz: name: "CMark Fuzz" needs: [gate, cmark] if: >- always() && !cancelled() && needs.cmark.result == 'success' && needs.gate.outputs.run_level == 'full' && (needs.gate.outputs.cmark_changed == 'true' || needs.gate.outputs.run_level == 'full') uses: ./.github/workflows/cmark-fuzz.yml permissions: contents: read secrets: inherit json4cpp-fuzz: name: "JSON4CPP Fuzz" needs: [gate, json4cpp] if: >- always() && !cancelled() && needs.json4cpp.result == 'success' && needs.gate.outputs.run_level == 'full' && (needs.gate.outputs.json4cpp_changed == 'true' || needs.gate.outputs.run_level == 'full') uses: ./.github/workflows/json4cpp-fuzz.yml permissions: contents: read secrets: inherit json4cpp-amalgam: name: "JSON4CPP Amalgamation" needs: [gate, json4cpp] if: >- always() && !cancelled() && needs.json4cpp.result == 'success' && (needs.gate.outputs.json4cpp_changed == 'true' || needs.gate.outputs.run_level == 'full') uses: ./.github/workflows/json4cpp-amalgam.yml permissions: contents: read secrets: inherit tomlplusplus-fuzz: name: "TOML++ Fuzz" needs: [gate, tomlplusplus] if: >- always() && !cancelled() && needs.tomlplusplus.result == 'success' && needs.gate.outputs.run_level == 'full' && (needs.gate.outputs.tomlplusplus_changed == 'true' || needs.gate.outputs.run_level == 'full') uses: ./.github/workflows/tomlplusplus-fuzz.yml permissions: contents: read security-events: write secrets: inherit neozip-fuzz: name: "NeoZip Fuzz" needs: [gate, neozip] if: >- always() && !cancelled() && needs.neozip.result == 'success' && needs.gate.outputs.run_level == 'full' && (needs.gate.outputs.neozip_changed == 'true' || needs.gate.outputs.run_level == 'full') uses: ./.github/workflows/neozip-fuzz.yml permissions: contents: read secrets: inherit meshmc-codeql: name: "MeshMC CodeQL" needs: [gate, meshmc] if: >- always() && !cancelled() && needs.meshmc.result == 'success' && needs.gate.outputs.run_level == 'full' && (needs.gate.outputs.meshmc_changed == 'true' || needs.gate.outputs.run_level == 'full') uses: ./.github/workflows/meshmc-codeql.yml permissions: contents: read security-events: write secrets: inherit mnv-codeql: name: "MNV CodeQL" needs: [gate, mnv] if: >- always() && !cancelled() && needs.mnv.result == 'success' && needs.gate.outputs.run_level == 'full' && (needs.gate.outputs.mnv_changed == 'true' || needs.gate.outputs.run_level == 'full') uses: ./.github/workflows/mnv-codeql.yml permissions: contents: read security-events: write secrets: inherit neozip-codeql: name: "NeoZip CodeQL" needs: [gate, neozip] if: >- always() && !cancelled() && needs.neozip.result == 'success' && needs.gate.outputs.run_level == 'full' && (needs.gate.outputs.neozip_changed == 'true' || needs.gate.outputs.run_level == 'full') uses: ./.github/workflows/neozip-codeql.yml permissions: actions: read contents: read security-events: write secrets: inherit # ╔════════════════════════════════════════════════════════════════╗ # ║ STAGE 4 — Containers & Nix (only on full + meshmc changes) ║ # ╚════════════════════════════════════════════════════════════════╝ meshmc-container: name: "MeshMC Container" needs: [gate, meshmc] if: >- always() && !cancelled() && needs.meshmc.result == 'success' && needs.gate.outputs.run_level == 'full' && (needs.gate.outputs.meshmc_changed == 'true' || needs.gate.outputs.run_level == 'full') uses: ./.github/workflows/meshmc-container.yml permissions: contents: read packages: write secrets: inherit meshmc-nix: name: "MeshMC Nix" needs: [gate, meshmc] if: >- always() && !cancelled() && needs.meshmc.result == 'success' && needs.gate.outputs.run_level == 'full' && (needs.gate.outputs.meshmc_changed == 'true' || needs.gate.outputs.run_level == 'full') uses: ./.github/workflows/meshmc-nix.yml permissions: contents: read secrets: inherit # ╔════════════════════════════════════════════════════════════════╗ # ║ STAGE 5 — Documentation (master push only) ║ # ╚════════════════════════════════════════════════════════════════╝ json4cpp-docs: name: "JSON4CPP Docs" needs: [gate, json4cpp] if: >- always() && !cancelled() && needs.json4cpp.result == 'success' && needs.gate.outputs.is_master == 'true' && needs.gate.outputs.is_push == 'true' && needs.gate.outputs.json4cpp_changed == 'true' uses: ./.github/workflows/json4cpp-publish-docs.yml permissions: contents: write secrets: inherit tomlplusplus-docs: name: "TOML++ Docs" needs: [gate, tomlplusplus] if: >- always() && !cancelled() && needs.tomlplusplus.result == 'success' && needs.gate.outputs.is_master == 'true' && needs.gate.outputs.is_push == 'true' && needs.gate.outputs.tomlplusplus_changed == 'true' uses: ./.github/workflows/tomlplusplus-gh-pages.yml permissions: contents: write secrets: inherit # ╔════════════════════════════════════════════════════════════════╗ # ║ STAGE 6 — Backport Automation (PR merge events) ║ # ╚════════════════════════════════════════════════════════════════╝ backport: name: "Backport" needs: gate if: >- github.event_name == 'pull_request_target' && github.event.pull_request.merged == true && contains(toJSON(github.event.pull_request.labels.*.name), 'backport') uses: ./.github/workflows/meshmc-backport.yml permissions: contents: write pull-requests: write actions: write secrets: inherit merge-blocking: name: "Merge Blocking PR" needs: gate if: >- github.event_name == 'pull_request_target' && github.event.pull_request.merged == true && contains(toJSON(github.event.pull_request.labels.*.name), 'status: blocking') uses: ./.github/workflows/meshmc-merge-blocking-pr.yml secrets: inherit # ╔════════════════════════════════════════════════════════════════╗ # ║ STAGE 7 — Release (tag pushes only) ║ # ╚════════════════════════════════════════════════════════════════╝ neozip-release: name: "NeoZip Release" needs: gate if: >- needs.gate.outputs.is_tag == 'true' && startsWith(github.ref, 'refs/tags/neozip-') uses: ./.github/workflows/neozip-release.yml permissions: contents: write secrets: inherit # ╔════════════════════════════════════════════════════════════════╗ # ║ STAGE 8 — Final Verdicts ║ # ║ ║ # ║ Merge queue and branch protection rules check this job. ║ # ║ It collects results from all stages and reports pass/fail. ║ # ╚════════════════════════════════════════════════════════════════╝ verdict: name: "CI Verdict" if: always() needs: - gate - lint - mnv - cgit - cmark - corebinutils - genqrcode - neozip - json4cpp - libnbtplusplus - tomlplusplus - meshmc - forgewrapper - images4docker - cmark-fuzz - json4cpp-fuzz - json4cpp-amalgam - tomlplusplus-fuzz - neozip-fuzz - meshmc-codeql - mnv-codeql - neozip-codeql - meshmc-container - meshmc-nix runs-on: ubuntu-latest steps: - name: Evaluate results run: | # shellcheck disable=SC2129 set -euo pipefail { echo "## CI Verdict" echo "" echo "| Job | Result |" echo "|-----|--------|" } >> "$GITHUB_STEP_SUMMARY" FAILED=false check_job() { local name="$1" local result="$2" local icon="⬜" case "$result" in success) icon="✅" ;; failure) icon="❌"; FAILED=true ;; cancelled) icon="⏹️" ;; skipped) icon="⏭️" ;; esac echo "| $name | $icon $result |" >> "$GITHUB_STEP_SUMMARY" } check_job "Gate" "${{ needs.gate.result }}" check_job "Lint" "${{ needs.lint.result }}" check_job "MNV" "${{ needs.mnv.result }}" check_job "CGit" "${{ needs.cgit.result }}" check_job "CMark" "${{ needs.cmark.result }}" check_job "CoreBinutils" "${{ needs.corebinutils.result }}" check_job "GenQRCode" "${{ needs.genqrcode.result }}" check_job "NeoZip" "${{ needs.neozip.result }}" check_job "JSON4CPP" "${{ needs.json4cpp.result }}" check_job "libNBT++" "${{ needs.libnbtplusplus.result }}" check_job "TOML++" "${{ needs.tomlplusplus.result }}" check_job "MeshMC" "${{ needs.meshmc.result }}" check_job "ForgeWrapper" "${{ needs.forgewrapper.result }}" check_job "Docker Images" "${{ needs.images4docker.result }}" check_job "CMark Fuzz" "${{ needs.cmark-fuzz.result }}" check_job "JSON4CPP Fuzz" "${{ needs.json4cpp-fuzz.result }}" check_job "JSON4CPP Amalg" "${{ needs.json4cpp-amalgam.result }}" check_job "TOML++ Fuzz" "${{ needs.tomlplusplus-fuzz.result }}" check_job "NeoZip Fuzz" "${{ needs.neozip-fuzz.result }}" check_job "MeshMC CodeQL" "${{ needs.meshmc-codeql.result }}" check_job "MNV CodeQL" "${{ needs.mnv-codeql.result }}" check_job "NeoZip CodeQL" "${{ needs.neozip-codeql.result }}" check_job "MeshMC Docker" "${{ needs.meshmc-container.result }}" check_job "MeshMC Nix" "${{ needs.meshmc-nix.result }}" echo "" >> "$GITHUB_STEP_SUMMARY" if [[ "$FAILED" == "true" ]]; then echo "**Result: FAILED** — one or more required jobs failed." >> "$GITHUB_STEP_SUMMARY" exit 1 else echo "**Result: PASSED** — all executed jobs succeeded." >> "$GITHUB_STEP_SUMMARY" fi