Working monorepos CICD Pipeline : Request for review and comment

Hello there !

I’ve set up a working pipeline for my gitlab repository, but i am not sure that i’ve followed best practices rules, and i think it could need some optimization too…

The background :

  • Let’s see my repo as a multi-packages repository. I have 3 first level directories and in each directory, i can have several packages sources.

[TAClient]
[package1]
[package-abc]
[this is also a package]
[TAServer]
[Yet another package]
[Assets]
[PackageX]
[PackageY]
So the top directories are fixed but not the sub dirs.

  • A “package” is a mix of powershell and javascript files.
  • For now i’ve only implemented code linting for both powershell and javascript files.

Now let’s review the gitlab-ci.yml file :

stages:
- narrow
- lint

.narrow_template:
  stage: narrow
  rules:
    - if : $CI_PIPELINE_SOURCE == "push"
      changes:
        - $DIRNAME/**/*
    - when: never

.lint_template:
  stage: lint
  rules:
    - if : $CI_PIPELINE_SOURCE == "push"
      changes:
        - $DIRNAME/**/*
    - when: never

ClientRemoteAction:narrow:
  extends: .narrow_template
  variables:
    DIRNAME: "taclient"
  image: node:17.3.0
  script:
    - CUSTOM_COMMIT_BEFORE_SHA=$(git rev-parse HEAD~1);
    - CUSTOM_COMMIT_SHA=$(git rev-parse HEAD~0);
    - MODIFIED_ITEMS=($(git diff --name-only $CUSTOM_COMMIT_BEFORE_SHA...$CUSTOM_COMMIT_SHA | grep ^$DIRNAME | cut -d "/" -f1-2 | uniq));
    - MODIFIED_PACKAGES=()
    - for i in "${MODIFIED_ITEMS[@]}"; do [[ -d $i ]] && MODIFIED_PACKAGES+=($i); done
    - if [ ${#MODIFIED_PACKAGES[*]} -eq 0 ]; then echo "Nothing to do !";exit 1;fi
    - COMMA_SEPARATED_PACKAGES=$(IFS=, ; echo "${MODIFIED_PACKAGES[*]}")
    - echo "MODIFIED_FOLDERS=$COMMA_SEPARATED_PACKAGES" >> variable.env
  artifacts:
    when: always
    reports:
       dotenv: variable.env

ClientRemoteAction:eslint:
  extends: .lint_template
  variables:
    DIRNAME: "taclient"
  image: node:17.3.0
  needs:
    - job: ClientRemoteAction:narrow
      artifacts: true
  before_script:
    - npm install eslint@7.32.0 eslint-config-standard eslint-plugin-import eslint-plugin-node eslint-plugin-promise
  script:
    - mod=${MODIFIED_FOLDERS//,/\/**/\*.js }/**/*.js
    - ./node_modules/.bin/eslint $mod --c "$DIRNAME/.eslintrc.conf" -f junit -o eslint.xml
  artifacts:
    reports:
      junit: eslint.xml

ClientRemoteAction:posh-lint:
  extends: .lint_template
  variables:
    DIRNAME: "taclient"
  image: mcr.microsoft.com/powershell:latest
  needs:
    - job: ClientRemoteAction:narrow
      artifacts: true
  before_script:
    - apt-get update -qq
    - apt-get install -qq git
    - pwsh -c '$global:progresspreference="silentlycontinue";Set-PSRepository -Name "PSGallery" -InstallationPolicy Trusted;Install-Module -Name "Pester", "PSScriptAnalyzer" -Scope CurrentUser'
  script:
    - pwsh "./$DIRNAME/lint.ps1"
  artifacts:
    reports:
      junit: posh-analysis.xml

ServerRemoteAction:narrow:
  extends: .narrow_template
  variables:
    DIRNAME: "taserver"
  script:
    - echo "ICI";

This setup allows me to use a variable called $DIRNAME which contains the first level directory name (taclient, taserver or assets)

The first stage, called narrow allows me to get a list of modified packages directories.
In this stage, i use git rev-parse to get the two SHAs needed by the git diff. If you wonder why i am doing this rather than using the built-in pipelines variables, it’s because when you create a new branch, you have the pipeline default COMMIT_BEFORE_SHA set to 000000000000000000000. Which is not the case when you get it with git rev-parse HEAD~1

When i have my list of modified package directories, i export it to a dotenv variable in a comma-separated string (since dotenv does’nt allow arrays)

Then i have two jobs, one for running eslint and the second for running PsScriptAnalyzer (for powershell).
I have not provided the lint.ps1 file, but if someone wants to see it no problem.

In the eslint job, i replace the commas of my dotenv variable by /**/*.js so if i have something like :
MODIFIED_FOLDERS=taclient/abc,taclient/def it becomes taclient/abc/**/*.js taclient/def/**.*.js
If there is another way of eslinting differents folders, i don’t know it :slight_smile:

Then i call eslint with its conf file. If you also wonder why i use an .eslintrc.conf instead of a json, js or any known eslint config file ext , this is because it’s the only workaround i’ve came up to make eslint work on my dev computer :frowning: (Windows + VsCode + eslint vscode extension) => If i use a regular config name, eslint doesn’t work, if i rename it to some unknown extension, it works…

Well this is it, thanks for your time !

Hello,

I think you could optimize it two steps:

  • Both narrow and lint template have the same rules, so specifiy them as workflow
  • Use narrow and lint as stages in your jobs instead of the extend
  • The variable DIRNAME is the same in almost all the options, so extract it
  • [optional] My personal preference is to specify image, stage, variables and then all else

It would look like this:

stages:
- narrow
- lint

workflow:
  rules:
    - if : $CI_PIPELINE_SOURCE == "push"
      changes:
        - $DIRNAME/**/*
    - when: never

variables:
  DIRNAME:
    value: "taclient"

ClientRemoteAction:narrow:
  image: node:17.3.0
  stage: narrow
  script:
    - CUSTOM_COMMIT_BEFORE_SHA=$(git rev-parse HEAD~1);
    - CUSTOM_COMMIT_SHA=$(git rev-parse HEAD~0);
    - MODIFIED_ITEMS=($(git diff --name-only $CUSTOM_COMMIT_BEFORE_SHA...$CUSTOM_COMMIT_SHA | grep ^$DIRNAME | cut -d "/" -f1-2 | uniq));
    - MODIFIED_PACKAGES=()
    - for i in "${MODIFIED_ITEMS[@]}"; do [[ -d $i ]] && MODIFIED_PACKAGES+=($i); done
    - if [ ${#MODIFIED_PACKAGES[*]} -eq 0 ]; then echo "Nothing to do !";exit 1;fi
    - COMMA_SEPARATED_PACKAGES=$(IFS=, ; echo "${MODIFIED_PACKAGES[*]}")
    - echo "MODIFIED_FOLDERS=$COMMA_SEPARATED_PACKAGES" >> variable.env
  artifacts:
    when: always
    reports:
       dotenv: variable.env

ClientRemoteAction:eslint:
  image: node:17.3.0
  stage: lint
  needs:
    - job: ClientRemoteAction:narrow
      artifacts: true
  before_script:
    - npm install eslint@7.32.0 eslint-config-standard eslint-plugin-import eslint-plugin-node eslint-plugin-promise
  script:
    - mod=${MODIFIED_FOLDERS//,/\/**/\*.js }/**/*.js
    - ./node_modules/.bin/eslint $mod --c "$DIRNAME/.eslintrc.conf" -f junit -o eslint.xml
  artifacts:
    reports:
      junit: eslint.xml

ClientRemoteAction:posh-lint:
  image: mcr.microsoft.com/powershell:latest
  stage: lint
  needs:
    - job: ClientRemoteAction:narrow
      artifacts: true
  before_script:
    - apt-get update -qq
    - apt-get install -qq git
    - pwsh -c '$global:progresspreference="silentlycontinue";Set-PSRepository -Name "PSGallery" -InstallationPolicy Trusted;Install-Module -Name "Pester", "PSScriptAnalyzer" -Scope CurrentUser'
  script:
    - pwsh "./$DIRNAME/lint.ps1"
  artifacts:
    reports:
      junit: posh-analysis.xml

ServerRemoteAction:narrow:
  stage: narrow
  variables:
    DIRNAME: "taserver"
  script:
    - echo "ICI";

Other than that I really like how to build an environment from a script in another job and use the artifact.

All the best!

Thank you very much for your anwser !

I will have a look at the workflow option. In the final version of this pipeline, more stages will be added :

  • a build stage after lint
    It will have the same rule as narrow and lint

  • two other stages : deploy-preprod and deploy-prod, these ones will have specific rules :
    for the preprod stage :
    - if : $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "preprod"
    for the production stage :
    - if : $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "prod"

I’ve also forgotten to add details about the branching model i’m using :

  • master is for dev, but i really want to forbid developers to work directly on master. They will have to create their own temp branch derived from master (feature_xxx, fix_xxx, whaterver_xxx)

  • preprod is for deploying on staging server, devs can’t push, they have to do a PR (they can validate their own PR)

  • prod will also be protected, but in this one, devs cannot validate their own PR.

In order to deliver something, a dev will have to :

  • merge its branch into master and push
  • do a PR on preprod and validate it. I think at this step. This should trigger an automatic creation of a PR on prod.

When all the tests are ok on staging server, someone else will validate the PR on prod in order to deploy.

Thank you for your advice, i will dig into the workflow option :slight_smile: