# ========================================================================================= # 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 workflow: rules: # prevent branch pipeline when an MR is open (prefer MR pipeline) - if: '$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS' 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: "$CI_REGISTRY/to-be-continuous/tools/tracking:master" # Default ng workspace NG_WORKSPACE_DIR: . # Default Docker image for ANGULAR CLI (can be overridden) NG_CLI_IMAGE: trion/ng-cli-karma:latest # Angular lint NG_LINT_ARGS: "lint" # Angular test NG_TEST_ARGS: >- test --code-coverage --reporters progress,junit NG_E2E_ARGS: >- e2e # Angular Build NG_BUILD_ARGS: "build" NG_SBOM_OPTS: "--omit dev" # default production ref name (pattern) PROD_REF: '/^(master|main)$/' # default integration ref name (pattern) INTEG_REF: '/^develop$/' # ================================================== # Variables for publication # ================================================== # NG_PUBLISH_ENABLED # List of projects to publish, use space (" ") for separation # ex: NG_PUBLISH_PROJECTS: "Project1 Project2 myLib" # If no projects specified, all workspace projects are published. # Set some args of `npm publish` command # ex: NG_PUBLISH_ARGS: "--dry-run" NG_PUBLISH_ARGS: '--verbose' # ================================================== # Stages definition # ================================================== stages: - build - test - publish ############################################################################################### # Script definition # ############################################################################################### .ng-cli-scripts: &ng-cli-scripts | # BEGSCRIPT set -e function log_debug() { if [[ -n "$TRACE" ]] then echo -e "[\\e[1;36mTRACE\\e[0m] $*" fi } function log_info() { echo -e "[\\e[1;94mINFO\\e[0m] $*" } function log_warn() { echo -e "[\\e[1;93mWARN\\e[0m] $*" } function log_error() { echo -e "[\\e[1;91mERROR\\e[0m] $*" } function assert_defined() { if [[ -z "$1" ]] then log_error "$2" exit 1 fi } function sonar_lint_report() { if [[ "$SONAR_HOST_URL" ]] || [[ "$SONAR_URL" ]] then mkdir -p -m 777 reports # generate ts lint report in json for SONARqube # shellcheck disable=SC2086 ng $NG_LINT_ARGS --format=json --force > reports/ng-lint.tslint.json fi } 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 # configure for npm echo "$certs" > /tmp/custom-ca.pem export NODE_EXTRA_CA_CERTS=/tmp/custom-ca.pem # import in Java keystore (if keytool command found) if command -v keytool > /dev/null then # shellcheck disable=SC2046 javahome=${JAVA_HOME:-$(dirname $(readlink -f $(command -v java)))/..} # shellcheck disable=SC2086 keystore=${JAVA_KEYSTORE_PATH:-$(ls -1 $javahome/jre/lib/security/cacerts 2>/dev/null || ls -1 $javahome/lib/security/cacerts 2>/dev/null || echo "")} if [[ -f "$keystore" ]] then storepass=${JAVA_KEYSTORE_PASSWORD:-changeit} nb_certs=$(echo "$certs" | grep -c 'END CERTIFICATE') log_info "importing $nb_certs certificates in Java keystore \\e[33;1m$keystore\\e[0m..." for idx in $(seq 0 $((nb_certs - 1))) do # TODO: use keytool option -trustcacerts ? if echo "$certs" | awk "n==$idx { print }; /END CERTIFICATE/ { n++ }" | keytool -noprompt -import -alias "imported CA Cert $idx" -keystore "$keystore" -storepass "$storepass" then log_info "... CA certificate [$idx] successfully imported" else log_warn "... Failed importing CA certificate [$idx]: abort" return fi done else log_warn "Java keystore \\e[33;1m$keystore\\e[0m not found: could not import CA certificates" fi 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 run_ng_build_for_libraries() { # current directory should be ${NG_WORKSPACE_DIR} angular_json=./angular.json libs=$(node -pe "Object.entries(require('${angular_json}').projects).filter(entry => entry[1].projectType === 'library').map(entry => entry[0]).join(' ')") if [ -n "${libs}" ]; then for lib in ${libs}; do log_debug "building library ${lib}" # shellcheck disable=SC2086 ng $NG_BUILD_ARGS "${lib}" done fi } function run_ng_build_for_applications() { # current directory should be ${NG_WORKSPACE_DIR} angular_json=./angular.json apps=$(node -pe "Object.entries(require('${angular_json}').projects).filter(entry => entry[1].projectType === 'application').map(entry => entry[0]).join(' ')") if [ -n "${apps}" ]; then for app in ${apps}; do log_debug "building application ${app}" # shellcheck disable=SC2086 ng ${NG_BUILD_ARGS} --no-progress "${app}" done fi } function run_tests() { # shellcheck disable=SC2086 ng $NG_TEST_ARGS --watch=false --no-progress } function merge_coverage() { reports=$(find reports/ -type f -iname "ng-coverage*.cobertura.xml") reports_count=$(echo "$reports" | wc -l) if [[ ${reports_count} -gt 1 ]]; then log_info "merging ${reports_count} Cobertura reports into one..." final_report="reports/ng-coverage.cobertura.xml" reports_opts="" for report in ${reports}; do # in case of multiple projects, report name should be : 'ng-coverage-<projectName>.cobertura.xml' project=$(basename -- "$report" | sed "s:ng-coverage-::g" | sed "s:.cobertura.xml::g") reports_opts="${reports_opts} ${project}=${report}" done # shellcheck disable=SC2086 npx -y cobertura-merge -p -o "${final_report}" ${reports_opts} fi } function configure_gitlab_instance_level_npm_registry_auth() { npm config set "//${CI_SERVER_HOST}/api/v4/packages/npm/:_authToken" "${CI_JOB_TOKEN}" } function compute_gitlab_registry_url() { gitlab_registry_url="https://${CI_SERVER_HOST}/api/v4/projects/${CI_PROJECT_ID}/packages/npm" } function configure_npm_publish_registry_auth() { publish_registry=${NPM_PUBLISH_REGISTRY} if [[ "${publish_registry}" ]]; then log_info "configured publish registry: ${publish_registry}" if [[ "$NPM_PUBLISH_TOKEN" ]]; then shopt -s extglob npm_publish_registry_host_and_path=${publish_registry/#http?(s):/} shopt -u extglob npm config set "${npm_publish_registry_host_and_path}:_authToken" "${NPM_PUBLISH_TOKEN}" fi else compute_gitlab_registry_url log_info "No defined publication registry, falling back to GitLab packages ${gitlab_registry_url}" publish_registry=${gitlab_registry_url} log_info "be sure to have your projects name starting with @${CI_PROJECT_ROOT_NAMESPACE}/" npm config set "@${CI_PROJECT_ROOT_NAMESPACE}:registry" "https://${CI_SERVER_HOST}/api/v4/packages/npm/" npm config set "//${CI_SERVER_HOST}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken" "${CI_JOB_TOKEN}" fi } function npm_publish() { # current directory should be ${NG_WORKSPACE_DIR} angular_json=./angular.json package_json=./package.json projects_to_publish=${NG_PUBLISH_PROJECTS} if [ -z "${projects_to_publish}" ]; then log_info "No projects specified, publishing all projects" projects_to_publish=$(node -pe "Object.keys(require('${angular_json}').projects).join(' ')") fi log_info "Publishing the following projects: ${projects_to_publish}..." # first we check all specified projects exist for project in ${projects_to_publish}; do if ! projectRoot=$(node -pe "require('${angular_json}').projects['${project}'].root"); then log_error "unknown project: ${project}"; exit 1; fi done for project in ${projects_to_publish}; do projectTypeSelector="projects['${project}'].projectType" projectType=$(node -pe "require('${angular_json}').${projectTypeSelector}") case ${projectType} in library) # output dir for library is defined in ng-package.json projectRoot=$(node -pe "require('${angular_json}').projects['${project}'].root") projectRelativeOutputDir=$(node -pe "require('./${projectRoot}/ng-package.json').dest") projectOutputDir=${projectRoot}/${projectRelativeOutputDir} ;; *) # other output dir is defined in angular.json projectOutputDir=$(node -pe "require('${angular_json}').projects['${project}'].architect.build.options.outputPath") log_warn "It is not recommended to publish a project with '${projectType}' type as npm package." log_warn "Please consider using another packaging solution as this support might be limited." # WORKAROUND: # as application type projects don't generate a package.json which is required for publishing # we generate one with the global package.json version if [ ! -f "${projectOutputDir}/package.json" ]; then global_version=$(node -pe "require('${package_json}').version") cat > "${projectOutputDir}"/package.json <<- EOF { "name": "${project}", "version": "${global_version}" } EOF fi ;; esac projectOutputDir=$(realpath "${projectOutputDir}") if [ ! -d "${projectOutputDir}" ]; then log_error "'${project}' project output folder '${projectOutputDir}' not found" log_error "please check that module '${project}' has been built" exit 1 fi log_info "Publishing project '${project}' from ${projectOutputDir}" npm publish "${projectOutputDir}" "${NG_PUBLISH_ARGS}" done } unscope_variables # ENDSCRIPT ############################################################################################### # Generic job definition # ############################################################################################### .ng-cli-base: image: $NG_CLI_IMAGE services: - name: "$TBC_TRACKING_IMAGE" command: ["--service", "angular", "4.2.0"] # cache configuration cache: key: "$CI_COMMIT_REF_SLUG-angular" paths: - ${NG_WORKSPACE_DIR}/.npm/ before_script: - *ng-cli-scripts - cd ${NG_WORKSPACE_DIR} - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}" # NPM_CONFIG_REGISTRY is not supported by old npm versions: force with cli - if [[ "$NPM_CONFIG_REGISTRY" ]]; then npm config set registry $NPM_CONFIG_REGISTRY; fi - | if [[ "$NPM_CONFIG_SCOPED_REGISTRIES" ]]; then for scoped_registry in $NPM_CONFIG_SCOPED_REGISTRIES do npm config set "${scoped_registry%%:*}":registry "${scoped_registry#*:}"; done fi - configure_gitlab_instance_level_npm_registry_auth - npm ci --cache .npm --prefer-offline $NG_INSTALL_EXTRA_OPTS ############################################################################################### # check stage: # # - ng-lint # ############################################################################################### ng-lint: extends: .ng-cli-base stage: build script: # generate lint report for sonar - sonar_lint_report # display ng lint result for job console - ng $NG_LINT_ARGS artifacts: when: always # save artifact even if test failed name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG" paths: - $NG_WORKSPACE_DIR/reports expire_in: 1 day rules: - if: '$NG_LINT_DISABLED == "true"' when: never - !reference [.test-policy, rules] ############################################################################################### # build stage: # # - ng-build # ############################################################################################### ng-build: extends: .ng-cli-base stage: build script: - run_ng_build_for_libraries - run_ng_build_for_applications - run_tests - merge_coverage coverage: /^\s*Total line Coverage\s*:\s*(\d+(?:\.\d+)?)\%$/ artifacts: reports: coverage_report: coverage_format: cobertura path: "$NG_WORKSPACE_DIR/reports/ng-coverage.cobertura.xml" junit: - "$NG_WORKSPACE_DIR/reports/**/ng-test*.xunit.xml" when: always # save artifact even if test failed name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG" paths: - $NG_WORKSPACE_DIR/dist - "$NG_WORKSPACE_DIR/reports/**/ng-test*" - "$NG_WORKSPACE_DIR/reports/**/ng-coverage*" expire_in: 1 day ############################################################################################### # test stage: # # - ng-e2e # # - ng-sbom # ############################################################################################### ng-e2e: extends: .ng-cli-base stage: test script: - ng $NG_E2E_ARGS artifacts: reports: junit: $NG_WORKSPACE_DIR/reports/ng-e2e.xunit.xml when: always # save artifact even if test failed name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG" paths: - $NG_WORKSPACE_DIR/reports/ng-e2e.* expire_in: 1 day rules: # only run if feature is enabled - if: '$NG_E2E_ENABLED != "true"' when: never - !reference [.test-policy, rules] ng-sbom: extends: .ng-cli-base stage: test # force no dependency dependencies: [] script: - mkdir -p -m 777 reports - npx -y @cyclonedx/cyclonedx-npm${NG_SBOM_VERSION:+@$NG_SBOM_VERSION} --output-format JSON --output-file reports/ng-sbom.cyclonedx.json $NG_SBOM_OPTS - chmod a+r reports/ng-sbom.cyclonedx.json rules: # exclude if disabled - if: '$NG_SBOM_DISABLED == "true"' when: never - !reference [.test-policy, rules] artifacts: name: "SBOM for Angular from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG" when: always expire_in: 1 week paths: - $NG_WORKSPACE_DIR/reports/ng-sbom.cyclonedx.json ############################################################################################### # publish stage: # # - npm-publish # ############################################################################################### ng-publish: extends: .ng-cli-base stage: publish before_script: - *ng-cli-scripts - cd ${NG_WORKSPACE_DIR} - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}" - configure_gitlab_instance_level_npm_registry_auth - configure_npm_publish_registry_auth - npm ci --cache .npm --prefer-offline $NG_INSTALL_EXTRA_OPTS script: - npm_publish rules: # on production branche: manual - if: '$NG_PUBLISH_ENABLED == "true" && $CI_COMMIT_REF_NAME =~ $PROD_REF' when: manual