Skip to content
Snippets Groups Projects
Select Git revision
  • 4b02329ed258201aa103fa8cd08ad583900ec081
  • master default protected
  • 4
  • 4.13
  • 4.13.0
  • 4.12.3
  • 4.12.2
  • 4.12.1
  • 4.12
  • 4.12.0
  • 4.11
  • 4.11.1
  • 4.11.0
  • 4.10
  • 4.10.0
  • 4.9
  • 4.9.1
  • 4.9.2
  • 4.9.0
19 results

gitlab-ci-golang.yml

Blame
  • gitlab-ci-golang.yml 22.87 KiB
    # =========================================================================================
    # 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"
          if ! command -v gocover-cobertura  > /dev/null
          then
            log_info "Installing gocover-cobertura.."
            go install github.com/boumenot/gocover-cobertura@latest
          fi
          GOFLAGS="$GO_COBERTURA_FLAGS" 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
        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() {
        if ! command -v go-junit-report  > /dev/null
        then
          cd "$(mktemp -d)"
          go mod init go-junit-report
          log_info "Installing go-junit-report.."
          go install github.com/jstemmer/go-junit-report@latest
          cd -
        fi
      }
    
      function install_go_mod_outdated() {
        if ! command -v go-mod-outdated  > /dev/null
        then
          cd "$(mktemp -d)"
          go mod init go-mod-outdated
          log_info "Installing go-mod-outdated.."
          go install github.com/psampaz/go-mod-outdated@latest
          cd -
        fi  
      }
    
      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.10.0"]
      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}
        - export PATH=$GOBIN:$PATH
    
    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)
        - go-mod-outdated $GO_MOD_OUTDATED_ARGS < reports/go-list.native.json
        # text report (-ci fails)
        - 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
        - govulncheck ${GO_VULNCHECK_ARGS}
      rules:
        # exclude if GO_CI_LINT_DISABLED set
        - if: '$GO_VULNCHECK_DISABLED == "true"'
          when: never
        - !reference [.test-policy, rules]