# ========================================================================================= # 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]