# =========================================================================================
# Copyright (C) 2021 Orange & contributors
#
# This program is free software; you can redistribute it and/or modify it under the terms
# of the GNU Lesser General Public License as published by the Free Software Foundation;
# either version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along with this
# program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth
# Floor, Boston, MA  02110-1301, USA.
# =========================================================================================
# default workflow rules: Merge Request pipelines
spec:
  inputs:
    image:
      description: The Docker image used to run Go (build+test or build only) - **set the version required by your project**
      default: registry.hub.docker.com/library/golang:bookworm
    project-dir:
      description: Go project root directory
      default: .
    goproxy:
      description: URL of Go module proxy (see [Go env](https://golang.org/cmd/go/#hdr-Environment_variables))
      default: ''
    test-image:
      description: Specific Docker image used to run Go tests (as a separate job)
      default: ''
    generate-modules:
      description: "Space separated list of Go code generator modules (ex: `stringer mockery`)"
      default: ''
    build-flags:
      description: Flags used by the [go build command](https://pkg.go.dev/cmd/go#hdr-Compile_packages_and_dependencies)
      default: -mod=readonly
    build-mode:
      description: The template build mode (accepted values are `application`, `modules` and `auto`)
      options:
      - auto
      - application
      - modules
      default: auto
    build-linker-flags:
      description: Linker flags used by the [go build command](https://pkg.go.dev/cmd/go#hdr-Compile_packages_and_dependencies) `-ldflags`
      default: -s -w
    build-packages:
      description: Packages to build with the [go build command](https://pkg.go.dev/cmd/go#hdr-Compile_packages_and_dependencies)
      default: ./...
    target-os:
      description: |-
        The `$GOOS` target [see available values](https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63)

        Fallbacks to default `$GOOS` from the Go Docker image
      default: ''
    target-arch:
      description: |-
        The `$GOARCH` target [see available values](https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63)

        Fallbacks to default `$GOARCH` from the Go Docker image
      default: ''
    test-flags:
      description: Flags used by the [go test command](https://pkg.go.dev/cmd/go#hdr-Test_packages)
      default: -mod=readonly -v -race
    test-packages:
      description: Packages to test with the [go test command](https://pkg.go.dev/cmd/go#hdr-Test_packages)
      default: ./...
    list-args:
      description: Arguments used by the list command
      default: list -u -m -mod=readonly -json all
    cobertura-flags:
      description: Build flags to add to use gocover-cobertura, leave blank if not needed
      default: ''
    ci-lint-disabled:
      description: Disable GolangCI-Lint
      type: boolean
      default: false
    ci-lint-image:
      description: The Docker image used to run `golangci-lint`
      default: registry.hub.docker.com/golangci/golangci-lint:latest-alpine
    ci-lint-args:
      description: '`golangci-lint` [command line arguments](https://github.com/golangci/golangci-lint#command-line-options)'
      default: -E gosec,goimports ./...
    mod-outdated-args:
      description: '`god-mod-outdated` [command line arguments](https://github.com/psampaz/go-mod-outdated#usage'
      default: -update -direct
    sbom-disabled:
      description: Disable Software Bill of Materials
      type: boolean
      default: false
    sbom-image:
      default: registry.hub.docker.com/cyclonedx/cyclonedx-gomod:latest
    sbom-opts:
      description: '[@cyclonedx/cyclonedx-gomod options](https://github.com/CycloneDX/cyclonedx-gomod#usage) used for SBOM analysis'
      default: -main .
    vulncheck-disabled:
      description: Disable Govulncheck
      type: boolean
      default: false
    vulncheck-args:
      description: '`govulncheck` [command line arguments](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck#hdr-Flags)'
      default: ./...
---
workflow:
  rules:
    # prevent MR pipeline originating from production or integration branch(es)
    - if: '$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ $PROD_REF || $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ $INTEG_REF'
      when: never
    # on non-prod, non-integration branches: prefer MR pipeline over branch pipeline
    - if: '$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS && $CI_COMMIT_REF_NAME !~ $PROD_REF && $CI_COMMIT_REF_NAME !~ $INTEG_REF'
      when: never
    - if: '$CI_COMMIT_MESSAGE =~ "/\[(ci skip|skip ci) on ([^],]*,)*tag(,[^],]*)*\]/" && $CI_COMMIT_TAG'
      when: never
    - if: '$CI_COMMIT_MESSAGE =~ "/\[(ci skip|skip ci) on ([^],]*,)*branch(,[^],]*)*\]/" && $CI_COMMIT_BRANCH'
      when: never
    - if: '$CI_COMMIT_MESSAGE =~ "/\[(ci skip|skip ci) on ([^],]*,)*mr(,[^],]*)*\]/" && $CI_MERGE_REQUEST_ID'
      when: never
    - if: '$CI_COMMIT_MESSAGE =~ "/\[(ci skip|skip ci) on ([^],]*,)*default(,[^],]*)*\]/" && $CI_COMMIT_REF_NAME =~ $CI_DEFAULT_BRANCH'
      when: never
    - if: '$CI_COMMIT_MESSAGE =~ "/\[(ci skip|skip ci) on ([^],]*,)*prod(,[^],]*)*\]/" && $CI_COMMIT_REF_NAME =~ $PROD_REF'
      when: never
    - if: '$CI_COMMIT_MESSAGE =~ "/\[(ci skip|skip ci) on ([^],]*,)*integ(,[^],]*)*\]/" && $CI_COMMIT_REF_NAME =~ $INTEG_REF'
      when: never
    - if: '$CI_COMMIT_MESSAGE =~ "/\[(ci skip|skip ci) on ([^],]*,)*dev(,[^],]*)*\]/" && $CI_COMMIT_REF_NAME !~ $PROD_REF && $CI_COMMIT_REF_NAME !~ $INTEG_REF'
      when: never
    - when: always

# test job prototype: implement adaptive pipeline rules
.test-policy:
  rules:
    # on tag: auto & failing
    - if: $CI_COMMIT_TAG
    # on ADAPTIVE_PIPELINE_DISABLED: auto & failing
    - if: '$ADAPTIVE_PIPELINE_DISABLED == "true"'
    # on production or integration branch(es): auto & failing
    - if: '$CI_COMMIT_REF_NAME =~ $PROD_REF || $CI_COMMIT_REF_NAME =~ $INTEG_REF'
    # early stage (dev branch, no MR): manual & non-failing
    - if: '$CI_MERGE_REQUEST_ID == null && $CI_OPEN_MERGE_REQUESTS == null'
      when: manual
      allow_failure: true
    # Draft MR: auto & non-failing
    - if: '$CI_MERGE_REQUEST_TITLE =~ /^Draft:.*/'
      allow_failure: true
    # else (Ready MR): auto & failing
    - when: on_success

variables:
  # variabilized tracking image
  TBC_TRACKING_IMAGE: registry.gitlab.com/to-be-continuous/tools/tracking:master

  # Default Go project root directory
  GO_PROJECT_DIR: $[[ inputs.project-dir ]]

  # Default Docker image (can be overridden)
  GO_IMAGE: $[[ inputs.image ]]

  GO_GENERATE_MODULES: $[[ inputs.generate-modules ]]

  # Default flags for 'build' command
  GO_BUILD_FLAGS: $[[ inputs.build-flags ]]

  # Default flags for go build linker
  GO_BUILD_LINKER_FLAGS: $[[ inputs.build-linker-flags ]]

  # Default packages for 'build' command
  GO_BUILD_PACKAGES: $[[ inputs.build-packages ]]

  # Default build mode (application/modules/auto)
  GO_BUILD_MODE: $[[ inputs.build-mode ]]

  # Default flags for 'test' command
  GO_TEST_FLAGS: $[[ inputs.test-flags ]]

  # Default packages for 'test' command
  GO_TEST_PACKAGES: $[[ inputs.test-packages ]]

  # Default arguments for 'list' command
  GO_LIST_ARGS: $[[ inputs.list-args ]]

  # Default arguments for go-mod-outdated command
  GO_MOD_OUTDATED_ARGS: $[[ inputs.mod-outdated-args ]]

  GO_VULNCHECK_ARGS: $[[ inputs.vulncheck-args ]]

  # Default golangci-lint Docker image (can be overridden)
  GO_CI_LINT_IMAGE: $[[ inputs.ci-lint-image ]]

  # Default arguments for golangci-lint command
  GO_CI_LINT_ARGS: $[[ inputs.ci-lint-args ]]

  GOPROXY: $[[ inputs.goproxy ]]
  GO_TEST_IMAGE: $[[ inputs.test-image ]]
  GO_TARGET_OS: $[[ inputs.target-os ]]
  GO_TARGET_ARCH: $[[ inputs.target-arch ]]
  GO_COBERTURA_FLAGS: $[[ inputs.cobertura-flags ]]
  GO_CI_LINT_DISABLED: $[[ inputs.ci-lint-disabled ]]
  GO_SBOM_DISABLED: $[[ inputs.sbom-disabled ]]
  GO_VULNCHECK_DISABLED: $[[ inputs.vulncheck-disabled ]]

  # Image of cyclonedx-gomod used for SBOM analysis
  GO_SBOM_IMAGE: $[[ inputs.sbom-image ]]
  # Options for cyclonedx-gomod used for SBOM analysis
  GO_SBOM_OPTS: $[[ inputs.sbom-opts ]]

  # default production ref name (pattern)
  PROD_REF: /^(master|main)$/
  # default integration ref name (pattern)
  INTEG_REF: /^develop$/

stages:
  - build
  - test
  - package-build
  - package-test
  - infra
  - deploy
  - acceptance
  - publish
  - infra-prod
  - production

.go-scripts: &go-scripts |
  # BEGSCRIPT
  set -e

  function log_info() {
    >&2 echo -e "[\\e[1;94mINFO\\e[0m] $*"
  }

  function log_warn() {
    >&2 echo -e "[\\e[1;93mWARN\\e[0m] $*"
  }

  function log_error() {
    >&2 echo -e "[\\e[1;91mERROR\\e[0m] $*"
  }

  function install_ca_certs() {
    certs=$1
    if [[ -z "$certs" ]]
    then
      return
    fi

    # import in system
    if echo "$certs" >> /etc/ssl/certs/ca-certificates.crt
    then
      log_info "CA certificates imported in \\e[33;1m/etc/ssl/certs/ca-certificates.crt\\e[0m"
    fi
    if echo "$certs" >> /etc/ssl/cert.pem
    then
      log_info "CA certificates imported in \\e[33;1m/etc/ssl/cert.pem\\e[0m"
    fi
  }

  function unscope_variables() {
    _scoped_vars=$(env | awk -F '=' "/^scoped__[a-zA-Z0-9_]+=/ {print \$1}" | sort)
    if [[ -z "$_scoped_vars" ]]; then return; fi
    log_info "Processing scoped variables..."
    for _scoped_var in $_scoped_vars
    do
      _fields=${_scoped_var//__/:}
      _condition=$(echo "$_fields" | cut -d: -f3)
      case "$_condition" in
      if) _not="";;
      ifnot) _not=1;;
      *)
        log_warn "... unrecognized condition \\e[1;91m$_condition\\e[0m in \\e[33;1m${_scoped_var}\\e[0m"
        continue
      ;;
      esac
      _target_var=$(echo "$_fields" | cut -d: -f2)
      _cond_var=$(echo "$_fields" | cut -d: -f4)
      _cond_val=$(eval echo "\$${_cond_var}")
      _test_op=$(echo "$_fields" | cut -d: -f5)
      case "$_test_op" in
      defined)
        if [[ -z "$_not" ]] && [[ -z "$_cond_val" ]]; then continue;
        elif [[ "$_not" ]] && [[ "$_cond_val" ]]; then continue;
        fi
        ;;
      equals|startswith|endswith|contains|in|equals_ic|startswith_ic|endswith_ic|contains_ic|in_ic)
        # comparison operator
        # sluggify actual value
        _cond_val=$(echo "$_cond_val" | tr '[:punct:]' '_')
        # retrieve comparison value
        _cmp_val_prefix="scoped__${_target_var}__${_condition}__${_cond_var}__${_test_op}__"
        _cmp_val=${_scoped_var#"$_cmp_val_prefix"}
        # manage 'ignore case'
        if [[ "$_test_op" == *_ic ]]
        then
          # lowercase everything
          _cond_val=$(echo "$_cond_val" | tr '[:upper:]' '[:lower:]')
          _cmp_val=$(echo "$_cmp_val" | tr '[:upper:]' '[:lower:]')
        fi
        case "$_test_op" in
        equals*)
          if [[ -z "$_not" ]] && [[ "$_cond_val" != "$_cmp_val" ]]; then continue;
          elif [[ "$_not" ]] && [[ "$_cond_val" == "$_cmp_val" ]]; then continue;
          fi
          ;;
        startswith*)
          if [[ -z "$_not" ]] && [[ "$_cond_val" != "$_cmp_val"* ]]; then continue;
          elif [[ "$_not" ]] && [[ "$_cond_val" == "$_cmp_val"* ]]; then continue;
          fi
          ;;
        endswith*)
          if [[ -z "$_not" ]] && [[ "$_cond_val" != *"$_cmp_val" ]]; then continue;
          elif [[ "$_not" ]] && [[ "$_cond_val" == *"$_cmp_val" ]]; then continue;
          fi
          ;;
        contains*)
          if [[ -z "$_not" ]] && [[ "$_cond_val" != *"$_cmp_val"* ]]; then continue;
          elif [[ "$_not" ]] && [[ "$_cond_val" == *"$_cmp_val"* ]]; then continue;
          fi
          ;;
        in*)
          if [[ -z "$_not" ]] && [[ "__${_cmp_val}__" != *"__${_cond_val}__"* ]]; then continue;
          elif [[ "$_not" ]] && [[ "__${_cmp_val}__" == *"__${_cond_val}__"* ]]; then continue;
          fi
          ;;
        esac
        ;;
      *)
        log_warn "... unrecognized test operator \\e[1;91m${_test_op}\\e[0m in \\e[33;1m${_scoped_var}\\e[0m"
        continue
        ;;
      esac
      # matches
      _val=$(eval echo "\$${_target_var}")
      log_info "... apply \\e[32m${_target_var}\\e[0m from \\e[32m\$${_scoped_var}\\e[0m${_val:+ (\\e[33;1moverwrite\\e[0m)}"
      _val=$(eval echo "\$${_scoped_var}")
      export "${_target_var}"="${_val}"
    done
    log_info "... done"
  }

  function output_coverage() {
    coverage_out=reports/go-coverage.native.out
    if [[ -f "$coverage_out" ]]
    then
      log_info "--- \\e[32mCoverage report(s) found\\e[0m (\\e[33;1m${coverage_out}\\e[0m): output"
      percent=$(go tool cover -func="$coverage_out" | tail -1 | awk -F" " '{print $NF}')
      echo "${percent} covered"

      go get github.com/boumenot/gocover-cobertura
      GOFLAGS="$GO_COBERTURA_FLAGS" go run github.com/boumenot/gocover-cobertura < "$coverage_out" > reports/go-coverage.cobertura.xml
    else
      log_info "--- \\e[32mCoverage report(s) not found\\e[0m: skip"
    fi
  }

  # evaluates Go build mode (manages 'auto' mode)
  function go_build_mode() {
    case "$GO_BUILD_MODE" in
      application|modules)
        echo "$GO_BUILD_MODE"
        ;;
      auto)
        go_main_src=$(find . -name "*.go" -exec grep -wl "^package main" {} \;)
        if [[ "$go_main_src" ]]
        then
          log_info "--- build mode auto-detected: \\e[96;1mapplication\\e[0m (main package found)"
          echo "application"
        else
          log_info "--- build mode auto-detected: \\e[96;1mmodules\\e[0m (no main package found)"
          echo "modules"
        fi
        ;;
      *)
        log_error "--- unsupported \\e[94;1m\$GO_BUILD_MODE\\e[0m value (expected values are \\e[96;1mapplication\\e[0m, \\e[96;1mmodules\\e[0m, \\e[96;1mauto\\e[0m)"
        exit 1
        ;;
    esac
  }

  function go_build() {
    case "$(go_build_mode)" in
      application)
        go_build_application
        ;;
      modules)
        go_build_modules
        ;;
    esac
  }

  function go_build_application() {
    log_info "building go application"
    GO_TARGET_OS="${GO_TARGET_OS:-$GOOS}"
    GO_TARGET_ARCH="${GO_TARGET_ARCH:-$GOARCH}"
    target_dir="$GOBIN/$GO_TARGET_OS/$GO_TARGET_ARCH"
    mkdir -p "$target_dir"
    # shellcheck disable=SC2086
    GOOS="$GO_TARGET_OS" GOARCH="$GO_TARGET_ARCH" go build -ldflags="$GO_BUILD_LINKER_FLAGS" $GO_BUILD_FLAGS -o "$target_dir" $GO_BUILD_PACKAGES
  }

  function go_build_modules() {
    log_info "building go modules"
    # shellcheck disable=SC2086
    go build -ldflags="$GO_BUILD_LINKER_FLAGS" $GO_BUILD_FLAGS $GO_BUILD_PACKAGES
  }

  function go_test() {
    mkdir -p -m 777 reports
    local go_text_report="reports/go-test.native.txt"

    set +e
    # shellcheck disable=SC2086
    go test $GO_TEST_FLAGS "-coverprofile=reports/go-coverage.native.out" $GO_TEST_PACKAGES > "$go_text_report"
    test_rc=$?
    set -e

    # dump text report in the console
    cat "$go_text_report" || (echo "Display of go test report file failed; Display of last 100 lines." && tail -n100 "$go_text_report") 

    # compute and dump code coverage in the console
    output_coverage

    # produce JUnit report (for GitLab)
    install_go_junit_report
    "$GOBIN/go-junit-report" < "$go_text_report" > reports/go-test.xunit.xml

    # produce JSON report (for SonarQube)
    go tool test2json < "$go_text_report" > reports/go-test.native.json

    # maybe fail
    if [[ "$test_rc" != "0" ]]; then exit "$test_rc"; fi
  }

  function install_go_junit_report() {
    cd "$(mktemp -d)"
    go mod init go-junit-report
    go install github.com/jstemmer/go-junit-report@latest
    cd -
  }

  function install_go_mod_outdated() {
    cd "$(mktemp -d)"
    go mod init go-mod-outdated
    go install github.com/psampaz/go-mod-outdated@latest
    cd -
  }

  function install_go_govulncheck() {
    if ! command -v govulncheck  > /dev/null
    then
      cd "$(mktemp -d)"
      go mod init govulncheck
      go install golang.org/x/vuln/cmd/govulncheck@latest
      cd -
    fi
  }

  unscope_variables

  # ENDSCRIPT

# job prototype
# defines default default docker image, tracking probe, cache policy and tags
.go-base:
  image: $GO_IMAGE
  services:
    - name: "$TBC_TRACKING_IMAGE"
      command: ["--service", "golang", "4.9.2"]
  variables:
    # The directory where 'go install' will install a command.
    GOBIN: "$CI_PROJECT_DIR/$GO_PROJECT_DIR/bin"
    # The directory where the go command will store cached information for reuse in future builds.
    GOCACHE: "$CI_PROJECT_DIR/$GO_PROJECT_DIR/.cache"
  cache:
    key: "$CI_COMMIT_REF_SLUG-golang"
    paths:
      - $GO_PROJECT_DIR/.cache/
  before_script:
    - !reference [.go-scripts]
    - |
      if command -v git > /dev/null
      then
        git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
      else
        log_warn "If you need to use private repository, you should provide an image with git executable"
      fi
    - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"
    - cd ${GO_PROJECT_DIR}

go-generate:
  extends: .go-base
  stage: .pre
  script:
    - go install $GO_GENERATE_MODULES
    - go generate
  rules:
    # only if $GO_GENERATE_MODULES is set
    - if: '$GO_GENERATE_MODULES != null && $GO_GENERATE_MODULES != ""'
  artifacts:
    name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
    expire_in: 1 day
    # default captured paths; otherwise has to be overwritten
    paths:
      - "${GO_PROJECT_DIR}/**/mock/"
      - "${GO_PROJECT_DIR}/**/mocks/"
      - "${GO_PROJECT_DIR}/**/*mock*.go"

go-build:
  extends: .go-base
  stage: build
  script:
    - go_build
  artifacts:
    name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
    expire_in: 1 day
    paths:
      - $GO_PROJECT_DIR/bin/
  rules:
    # if $GO_TEST_IMAGE set
    - if: '$GO_TEST_IMAGE != ""'

go-test:
  extends: .go-base
  image: $GO_TEST_IMAGE
  stage: build
  script:
    - go_test
  coverage: '/^(\d+.\d+\%) covered$/'
  artifacts:
    name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
    expire_in: 1 day
    when: always
    reports:
      junit:
        - "$GO_PROJECT_DIR/reports/go-test.xunit.xml"
      coverage_report:
        coverage_format: cobertura
        path: "$GO_PROJECT_DIR/reports/go-coverage.cobertura.xml"
    paths:
      - "$GO_PROJECT_DIR/reports/go-test.*"
      - "$GO_PROJECT_DIR/reports/go-coverage.*"
  rules:
    # if $GO_TEST_IMAGE set
    - if: '$GO_TEST_IMAGE == ""'
      when: never
    - !reference [.test-policy, rules]

go-build-test:
  extends: .go-base
  stage: build
  script:
    - go_build
    - go_test
  coverage: '/^(\d+.\d+\%) covered$/'
  artifacts:
    name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
    when: always
    expire_in: 1 day
    reports:
      junit:
        - "$GO_PROJECT_DIR/reports/go-test.xunit.xml"
      coverage_report:
        coverage_format: cobertura
        path: "$GO_PROJECT_DIR/reports/go-coverage.cobertura.xml"
    paths:
      - $GO_PROJECT_DIR/bin/
      - $GO_PROJECT_DIR/reports/
      - "$GO_PROJECT_DIR/reports/go-test.*"
      - "$GO_PROJECT_DIR/reports/go-coverage.*"
  rules:
    # if $GO_TEST_IMAGE not set
    - if: '$GO_TEST_IMAGE == ""'

go-ci-lint:
  extends: .go-base
  stage: build
  image: $GO_CI_LINT_IMAGE
  script:
    - mkdir -p -m 777 reports
    # produce all reports at once
    - golangci-lint run --out-format "colored-line-number:stdout,code-climate:reports/go-ci-lint.codeclimate.json,checkstyle:reports/go-ci-lint.checkstyle.xml" $GO_CI_LINT_ARGS
  artifacts:
    name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
    expire_in: 1 day
    when: always
    paths:
      - "$GO_PROJECT_DIR/reports/go-ci-lint.*"
    reports:
      codequality:
        - "$GO_PROJECT_DIR/reports/go-ci-lint.codeclimate.json"
  rules:
    # exclude if GO_CI_LINT_DISABLED set
    - if: '$GO_CI_LINT_DISABLED == "true"'
      when: never
    - !reference [.test-policy, rules]

go-mod-outdated:
  extends: .go-base
  stage: test
  dependencies: []
  script:
    - mkdir -p -m 777 reports
    # go list
    - go $GO_LIST_ARGS > reports/go-list.native.json
    - install_go_mod_outdated
    # console output (no fail)
    - $GOBIN/go-mod-outdated $GO_MOD_OUTDATED_ARGS < reports/go-list.native.json
    # text report (-ci fails)
    - $GOBIN/go-mod-outdated $GO_MOD_OUTDATED_ARGS -ci < reports/go-list.native.json > reports/go-mod-outdated.native.txt
  artifacts:
    name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
    expire_in: 1 day
    when: always
    paths:
      - "$GO_PROJECT_DIR/reports/go-list.native.json"
      - "$GO_PROJECT_DIR/reports/go-mod-outdated.native.txt"
  rules:
    # on schedule: auto
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      allow_failure: true
    # else manual & non-blocking
    - when: manual
      allow_failure: true

go-sbom:
  extends: .go-base
  stage: test
  image:
    name: $GO_SBOM_IMAGE
    entrypoint: [""]
  # manage separate GitLab cache to prevent permission denied error (this image being rootless, it can't rewrite Go cache - owned by root)
  # see: https://gitlab.com/gitlab-org/gitlab-runner/-/issues/29663
  cache:
    key: "$CI_COMMIT_REF_SLUG-golang-sbom"
    paths:
      - $GO_PROJECT_DIR/.cache/
  # force no dependency
  dependencies: []
  needs: []
  script:
    - mkdir -p -m 777 reports
    - go_mode=$(go_build_mode)
    - |
      cyclonedx-gomod "${go_mode:0:3}" -json -output reports/go-sbom.cyclonedx.json $GO_SBOM_OPTS
    - chmod a+r reports/go-sbom.cyclonedx.json
  artifacts:
    name: "SBOM for golang from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
    expire_in: 1 week
    when: always
    paths:
      - "$GO_PROJECT_DIR/reports/go-sbom.cyclonedx.json"
    reports:
      cyclonedx: 
        - "$GO_PROJECT_DIR/reports/go-sbom.cyclonedx.json"
  rules:
    # exclude if disabled
    - if: '$GO_SBOM_DISABLED == "true"'
      when: never
    - !reference [.test-policy, rules]

go-govulncheck:
  extends: .go-base
  stage: test
  dependencies: []
  script:
    - mkdir -p -m 777 reports  
    - install_go_govulncheck
    - $GOBIN/govulncheck ${GO_VULNCHECK_ARGS}
  rules:
    # exclude if GO_CI_LINT_DISABLED set
    - if: '$GO_VULNCHECK_DISABLED == "true"'
      when: never
    - !reference [.test-policy, rules]