// The same four gates, for a Jenkins agent.
//
// This is a template. Copy it, do not wrap it.
//
// The structure mirrors the GitHub workflow deliberately, and the two share the
// three properties that matter:
//
//   * no version constraint is written here -- `odm-qa-pipeline pins` is asked;
//   * every gate records a result even when it fails, and the aggregate step
//     runs in `post { always }`, because reporting on the gates that did NOT
//     finish is the entire reason the aggregation exists;
//   * gate 2 takes ONE capture and judges it twice, and gate 4 names that same
//     file, so the attestation, the coverage and the certificate all describe a
//     single observation of the machine.
//
// That third property was true of the GitHub workflow for a release before it was
// true here. `tests/test_templates.py` now asserts it against both files, because
// a fix applied to one of two copies is a fix that is already drifting.
//
// `catchError` with `buildResult: 'SUCCESS'` on each gate looks alarming and is
// correct: a gate's own exit code must not decide the build. The verdict comes
// from `odm-qa-pipeline aggregate`, which is the only step that can see all four
// at once and notice the one that never reported.

pipeline {
    agent any

    parameters {
        string(name: 'TARGET',    defaultValue: '',            description: 'Redfish base URL')
        string(name: 'CONFIG',    defaultValue: 'config/',     description: 'entity-manager configuration')
        string(name: 'SCENARIOS', defaultValue: 'scenarios/',  description: 'injection scenarios')
        string(name: 'IDENTITY',  defaultValue: '',            description: 'identity JSON; empty skips gate 4')
        string(name: 'OPTIONAL',  defaultValue: '',            description: 'space-separated optional gates')
        string(name: 'PIPELINE_REQUIREMENT',
               defaultValue: 'odm-qa-pipeline',
               description: 'how to install the umbrella itself')
    }

    environment {
        BMC = credentials('bmc-credentials')   // BMC_USR / BMC_PSW
        VENV = "${WORKSPACE}/.qa-venv"
        PATH = "${WORKSPACE}/.qa-venv/bin:${PATH}"
    }

    stages {
        stage('Install the umbrella') {
            steps {
                sh '''
                    set -eu
                    python3 -m venv "${VENV}"
                    pip install --quiet --upgrade pip
                    pip install --quiet "${PIPELINE_REQUIREMENT}"
                    mkdir -p qa-results qa-artifacts
                    odm-qa-pipeline gates
                    odm-qa-pipeline pins
                '''
            }
        }

        stage('Gate 1 - DMTF conformance') {
            steps {
                catchError(buildResult: 'SUCCESS', stageResult: 'UNSTABLE') {
                    sh '''
                        set -u
                        odm-qa-pipeline pins --gate dmtf > requirements-dmtf.txt
                        pip install --quiet -r requirements-dmtf.txt
                        mkdir -p qa-artifacts/dmtf
                        set +e
                        rf_service_validator --ip "${TARGET}" -u "${BMC_USR}" -p "${BMC_PSW}" \
                            --logdir qa-artifacts/dmtf --nochkcert
                        service_rc=$?
                        rf_protocol_validator --rhost "${TARGET}" -u "${BMC_USR}" -p "${BMC_PSW}" \
                            --report-dir qa-artifacts/dmtf --no-cert-check
                        protocol_rc=$?
                        # Neither the exit code nor the presence of a file
                        # answers this on its own: a service the validator
                        # cannot reach exits as a failing machine does, having
                        # already written its debug log. dmtf-verdict reads what
                        # the validators wrote.
                        #
                        # Still inside `set +e`, deliberately. Jenkins runs sh
                        # with -xe, and this command exits 1 or 2 by design: an
                        # assignment from a substitution that returns non-zero
                        # would end the stage before the gate is recorded.
                        detail=$(odm-qa-pipeline dmtf-verdict --logdir qa-artifacts/dmtf \
                            --service-exit "${service_rc}" --protocol-exit "${protocol_rc}")
                        code=$?
                        set -e
                        echo "${detail}"
                        odm-qa-pipeline record --gate dmtf --exit-code "${code}" \
                            --detail "${detail}" --out qa-results/dmtf.json
                    '''
                }
            }
        }

        stage('Gate 2 - coverage and liveness') {
            steps {
                catchError(buildResult: 'SUCCESS', stageResult: 'UNSTABLE') {
                    sh '''
                        set -uo pipefail
                        odm-qa-pipeline pins --gate coverage > requirements-coverage.txt
                        pip install --quiet -r requirements-coverage.txt
                        mkdir -p qa-artifacts
                        set +e

                        # ONE capture, judged twice. This used to walk the machine
                        # twice -- `detect --target` and then `coverage --target` --
                        # so the attestation and the coverage handed to gate 4 came
                        # from two different observations taken at two different
                        # moments, and the certificate combined them without saying
                        # so. It also asked a BMC under test to serve its whole
                        # sensor tree twice for one gate.
                        bmc-sensor-audit capture --target "${TARGET}" \
                            --username "${BMC_USR}" --password "${BMC_PSW}" --insecure \
                            --out qa-artifacts/walk.json --print-digest \
                            | tee qa-artifacts/capture.log
                        # PIPESTATUS, not `$?`: after a pipe `$?` is `tee`'s, which
                        # succeeds whatever the capture did.
                        capture_rc=${PIPESTATUS[0]}

                        # Judge the FILE, not the run. `capture` exits 2 both when
                        # it could not reach the machine and when it reached it and
                        # one subtree answered with an error; the second is a walk
                        # the tool writes on purpose. This gate needs a whole one,
                        # so it asks for that explicitly.
                        bmc-sensor-audit validate-walk qa-artifacts/walk.json --require-complete
                        walk_rc=$?

                        bmc-sensor-audit detect --config "${CONFIG}" --walk qa-artifacts/walk.json \
                            --attest-out qa-artifacts/attestation.json \
                            --attest-target-label "unit-under-test"
                        detect_rc=$?
                        bmc-sensor-audit coverage --config "${CONFIG}" --walk qa-artifacts/walk.json \
                            --json > qa-artifacts/coverage.json
                        coverage_rc=$?
                        set -e
                        code=${capture_rc}
                        for rc in ${walk_rc} ${detect_rc} ${coverage_rc}; do
                          [ "${rc}" -gt "${code}" ] && code=${rc}
                        done
                        odm-qa-pipeline record --gate coverage --exit-code "${code}" \
                            --detail "capture ${capture_rc}, walk ${walk_rc}, detect ${detect_rc}, coverage ${coverage_rc}" \
                            --artifact qa-artifacts/attestation.json \
                            --out qa-results/coverage.json
                    '''
                }
            }
        }

        stage('Gate 3 - orchestrated injection') {
            steps {
                catchError(buildResult: 'SUCCESS', stageResult: 'UNSTABLE') {
                    sh '''
                        set -u
                        odm-qa-pipeline pins --gate injection > requirements-injection.txt
                        pip install --quiet -r requirements-injection.txt
                        set +e
                        qa-orchestrator check "${SCENARIOS}"; parse_rc=$?
                        qa-orchestrator run   "${SCENARIOS}"; run_rc=$?
                        set -e
                        if [ "${parse_rc}" -ne 0 ]; then
                            code=2; detail="a scenario would not parse; nothing was injected"
                        else
                            code=${run_rc}; detail="scenarios ran, exit ${run_rc}"
                        fi
                        odm-qa-pipeline record --gate injection --exit-code "${code}" \
                            --detail "${detail}" --out qa-results/injection.json
                    '''
                }
            }
        }

        stage('Gate 4 - certificate') {
            when { expression { return params.IDENTITY?.trim() } }
            steps {
                catchError(buildResult: 'SUCCESS', stageResult: 'UNSTABLE') {
                    sh '''
                        set -u
                        odm-qa-pipeline pins --gate certificate > requirements-certificate.txt
                        pip install --quiet -r requirements-certificate.txt
                        set +e
                        # `--walk` is what ties the document to the evidence.
                        # Gate 2 captured one walk and judged it twice; naming it
                        # here means the certificate records the handle of the
                        # exact file both verdicts came from, and a recipient can
                        # match it with `sha256sum`.
                        cert-generator render \
                            --attestation qa-artifacts/attestation.json \
                            --coverage qa-artifacts/coverage.json \
                            --walk qa-artifacts/walk.json \
                            --identity "${IDENTITY}" \
                            --out-json qa-artifacts/certificate.json \
                            --out-pdf qa-artifacts/certificate.pdf
                        code=$?
                        set -e
                        odm-qa-pipeline record --gate certificate --exit-code "${code}" \
                            --detail "rendered from the gate 2 attestation and its capture" \
                            --artifact qa-artifacts/certificate.pdf \
                            --out qa-results/certificate.json
                    '''
                }
            }
        }
    }

    post {
        always {
            // The only step that can see all four gates at once, and therefore
            // the only one that can report the gate which never ran.
            sh '''
                set -u
                args=""
                for gate in ${OPTIONAL}; do args="${args} --optional ${gate}"; done
                set +e
                odm-qa-pipeline aggregate --results qa-results \
                    --out qa-artifacts/summary.json ${args}
                verdict=$?
                set -e
                echo "aggregate exit ${verdict}"
                # 0 clean, 1 regressions, 2 could not complete. Both non-zero
                # codes fail the build; the summary says which, and why.
                exit "${verdict}"
            '''
            archiveArtifacts artifacts: 'qa-artifacts/**, qa-results/**',
                             allowEmptyArchive: true
        }
    }
}
