## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## See the file COPYING for copying conditions.

## AI-Assisted

## Reusable workflow: Coverity Scan static analysis.
##
## Replaces the third-party 'vapier/coverity-scan-action' approach
## with an inline curl + tar + cov-build flow shared across the org's
## consumers. No third-party Action's permissions footprint, no
## Marketplace dependency, single auditable script set in
## developer-meta-files/ci/.
##
## ## Trust footprint
##
## scan.coverity.com (Black Duck / Synopsys) - the cov-analysis
## tarball download is gated on a valid project token, md5-verified
## against the same endpoint, and optionally sha256-pinned via a
## .coverity-tool-sha256.expected file in the consumer repo root.
## See ci/coverity-download.sh for the verification logic.
##
## ## Secret isolation
##
## Strict 'workflow_call.secrets' schema below requires the caller
## to forward exactly COVERITY_SCAN_TOKEN and COVERITY_SCAN_EMAIL.
## Callers must use an explicit 'secrets:' map - 'secrets: inherit'
## bypasses the schema (it forwards every caller-side secret
## regardless). Run consumer-secrets-audit.yml manually to verify
## the secrets surface.
##
## Secrets are scoped to the individual steps that need them
## (Sanity check secrets / Download Coverity Build Tool / Submit
## to Coverity Scan) - NOT to job-level env: - so the pre-gate
## steps and any non-canonical run never see the token values.
## See agents/github-actions-consumer-templates.md
## "Coverity canonical-repos gate placement".
##
## ## Per-repo configuration: .github/dm-consumer.yml
##
## Per-repo values are read at workflow runtime from
## .github/dm-consumer.yml in the consumer repo. Schema:
##
##   coverity:
##     project-name:    "<scan.coverity.com project>"  # required
##     canonical-repos: "<owner>/<repo>[,<owner>/<repo>...]"  # required
##     apt-packages:    "<space-separated apt deps>"  # optional, default empty
##     build-command:   "<bash invocation>"           # optional, default empty
##
## Missing required keys hard-fail at workflow start. Missing
## optional keys treat as empty (no apt-install step, Python-only
## build via ci/coverity-build-python.sh).
##
## ## Caller responsibilities
##
## - Define the trigger schedule. Coverity Scan rate-limits
##   submissions (daily build limit), so weekly cron is the
##   typical choice. Schedule cannot live in workflow_call per
##   G-A-002.
## - Hardcode the owner in 'jobs.<id>.uses:' (G-A-001).
## - Forward via explicit 'secrets:' map (see Caller example). Not
##   'inherit'.
## - Author .github/dm-consumer.yml in the consumer repo with the
##   required keys.
##
## ## Secrets
##
## Forwarded explicitly by the caller (see Caller example):
##
## - COVERITY_SCAN_TOKEN  (required)  Coverity project token.
##                                    MUST be set as a repository
##                                    secret per consumer - tokens
##                                    are scoped per project on
##                                    scan.coverity.com, and our
##                                    consumers (helper-scripts,
##                                    kloak, security-misc) are
##                                    distinct Coverity projects
##                                    with distinct tokens. Cannot
##                                    be set org-wide.
## - COVERITY_SCAN_EMAIL  (required)  Notification email registered
##                                    on the Coverity project. CAN
##                                    be set org-wide if every
##                                    project shares one notification
##                                    address.
##
## ## Caller example - byte-identical consumer wrapper
##
##   name: Coverity
##   on:
##     push:
##       tags:
##         - '*'
##     schedule:
##       - cron: '0 6 * * 0'
##     workflow_dispatch:
##   permissions:
##     contents: read
##   concurrency:
##     group: ${{ github.workflow }}
##     cancel-in-progress: false
##   jobs:
##     coverity:
##       uses: org-ai-assisted/developer-meta-files/.github/workflows/reusable-coverity.yml@master
##       secrets:
##         COVERITY_SCAN_TOKEN: ${{ secrets.COVERITY_SCAN_TOKEN }}
##         COVERITY_SCAN_EMAIL: ${{ secrets.COVERITY_SCAN_EMAIL }}
##       permissions:
##         contents: read
##
## Note: no 'with:' block on the reusable call. Per-repo values
## live in the consumer's .github/dm-consumer.yml, read at runtime.

name: Coverity Scan (reusable)

on:
  workflow_call:
    secrets:
      COVERITY_SCAN_TOKEN:
        required: true
      COVERITY_SCAN_EMAIL:
        required: true

permissions:
  contents: read

## No 'concurrency:' block - the caller's wrapper enforces the
## singleton (`group: ${{ github.workflow }}`, `cancel-in-progress:
## false`); declaring the same group here would cause Actions'
## deadlock detector to fire ("Canceling since a deadlock was
## detected for concurrency group: ..." between the top-level
## workflow and this reusable, because `github.workflow` inside a
## reusable resolves to the CALLER's workflow name). See
## agents/github-actions.md G-A-006 "Reusable-side concurrency".

jobs:
  coverity:
    name: Coverity Scan
    ## Pinned to ubuntu-24.04 so the apt-packaged yq (kislyuk's
    ## python-yq jq wrapper) remains stable across runner-image
    ## transitions; the config-load step's syntax depends on that
    ## implementation.
    runs-on: ubuntu-24.04
    timeout-minutes: 30

    steps:
      - name: Checkout consumer repository
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2
        with:
          persist-credentials: false

      - name: Checkout developer-meta-files (orchestration scripts)
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2
        with:
          repository: org-ai-assisted/developer-meta-files
          ref: master
          path: .github/dmf
          persist-credentials: false

      - name: Install yq
        ## Ubuntu 24.04 ships kislyuk's python-yq jq wrapper. The
        ## `// ""` defaults and `-r` raw output in the next step
        ## are written for that implementation.
        ##
        ## '--' after sudo terminates sudo's option parsing; '--'
        ## before the package name terminates apt-get's. Defense
        ## in depth; matches the install-deps pattern.
        run: |
          sudo --non-interactive -- apt-get update --error-on=any
          sudo --non-interactive -- apt-get install --yes --no-install-recommends -- yq

      - name: Load per-repo config from .github/dm-consumer.yml
        id: cfg
        ## See ci/dm-consumer-load.sh for the schema rules
        ## (required hard-fail, optional empty-default, newline
        ## rejection, hyphen->underscore output naming).
        run: .github/dmf/ci/dm-consumer-load.sh
          coverity
          project-name,canonical-repos
          apt-packages,build-command

      - name: Canonical-repos gate
        id: gate
        ## See ci/canonical-repos-gate.sh. Step-level gate replaces
        ## the old job-level if: (which referenced
        ## inputs.canonical-repos, now read from dm-consumer.yml).
        ## Strict by design: workflow_dispatch does NOT bypass.
        env:
          CANONICAL_REPOS: ${{ steps.cfg.outputs.canonical_repos }}
          THIS_REPO: ${{ github.repository }}
        run: .github/dmf/ci/canonical-repos-gate.sh

      - name: Sanity check secrets
        if: steps.gate.outputs.allowed == 'true'
        env:
          COVERITY_TOKEN: ${{ secrets.COVERITY_SCAN_TOKEN }}
          COVERITY_EMAIL: ${{ secrets.COVERITY_SCAN_EMAIL }}
          COVERITY_PROJECT: ${{ steps.cfg.outputs.project_name }}
        run: .github/dmf/ci/coverity-check-secrets.sh

      ## Cache key includes github.repository so different Coverity
      ## consumers (helper-scripts, kloak, ...) get distinct caches;
      ## key includes hashFiles() on the reusable workflow so a
      ## config change invalidates the cache.
      - name: Install build dependencies
        if: steps.gate.outputs.allowed == 'true' && steps.cfg.outputs.apt_packages != ''
        uses: org-ai-assisted/developer-meta-files/.github/actions/apt-install-with-cache@master
        with:
          packages: ${{ steps.cfg.outputs.apt_packages }}
          cache-key: ${{ runner.os }}-apt-coverity-${{ github.repository }}-${{ hashFiles('.github/workflows/reusable-coverity.yml') }}

      - name: Download Coverity Build Tool
        if: steps.gate.outputs.allowed == 'true'
        env:
          COVERITY_TOKEN: ${{ secrets.COVERITY_SCAN_TOKEN }}
          COVERITY_PROJECT: ${{ steps.cfg.outputs.project_name }}
        run: .github/dmf/ci/coverity-download.sh

      - name: Run Coverity build (Python default)
        if: steps.gate.outputs.allowed == 'true' && steps.cfg.outputs.build_command == ''
        run: .github/dmf/ci/coverity-build-python.sh

      - name: Run Coverity build (caller-supplied)
        if: steps.gate.outputs.allowed == 'true' && steps.cfg.outputs.build_command != ''
        env:
          BUILD_COMMAND: ${{ steps.cfg.outputs.build_command }}
        run: bash -c "${BUILD_COMMAND}"

      ## coverity-submit.sh always packs cov-int.tgz. DRY_RUN is
      ## hardcoded to false in this refactor: per the spec, dry-run
      ## is not a wrapper-overridable input. Manual dry-runs can
      ## be done in a development branch by editing the wrapper
      ## temporarily.
      - name: Submit to Coverity Scan
        if: steps.gate.outputs.allowed == 'true'
        env:
          COVERITY_TOKEN: ${{ secrets.COVERITY_SCAN_TOKEN }}
          COVERITY_EMAIL: ${{ secrets.COVERITY_SCAN_EMAIL }}
          COVERITY_PROJECT: ${{ steps.cfg.outputs.project_name }}
          DRY_RUN: 'false'
        run: .github/dmf/ci/coverity-submit.sh

      - name: Upload cov-int artifact (always)
        if: always()
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a  # v7.0.1
        with:
          name: cov-int
          path: cov-int.tgz
          if-no-files-found: ignore
          retention-days: 7

      - name: Emit step summary
        if: always()
        ## R-100 compliant (no control flow). 'canonical' is false on
        ## mirror runs where the gate skipped the heavy steps -
        ## useful signal in the PR Checks tab. The dashboard link is
        ## a navigation hint, not a deep link to the run;
        ## scan.coverity.com renders the slash in project_name as a
        ## dash for display but accepts either form for navigation.
        ##
        ## All template-substituted values (job.status,
        ## steps.cfg.outputs.project_name, steps.gate.outputs.allowed)
        ## flow through env: so they cannot break out of the run:
        ## block via shell metacharacters - same pattern as #80/#84/
        ## #86. project_name in particular is read from the consumer
        ## repo's dm-consumer.yml (PR-author-controllable), so env
        ## routing is the right defense.
        env:
          JOB_STATUS: ${{ job.status }}
          CANONICAL: ${{ steps.gate.outputs.allowed || 'false' }}
          PROJECT_NAME: ${{ steps.cfg.outputs.project_name || 'unknown' }}
        run: |
          .github/dmf/ci/step-summary-emit.sh \
            --tool 'Coverity Scan' \
            --column-header field \
            --row "outcome=${JOB_STATUS}" \
            --row "canonical=${CANONICAL}" \
            --row "project=${PROJECT_NAME}" \
            --details-url "https://scan.coverity.com/projects"
