How to exclude a subdirectory from rules: changes:

How to exclude a subdirectory from rules: changes:

We have a project with a directory structure like:

/
  backend/
     ...
     config/
     ...
  frontend/
  ...

We want to run the build_backend job if changes are made anywhere under backend except for backend/config. If there is a change under backend/config then we want to run the build_config job, which looks like:

build_config:
  rules:
  - if: $CI_PIPELINE_SOURCE == "push"
    changes:
     - backend/config/**/*
    when: always

If build_backend looks like this:

build_backend:
  rules:
  - if: $CI_PIPELINE_SOURCE == "push" 
    changes:
     - backend/**/*
    when: always

  - if: $CI_PIPELINE_SOURCE == "push"
    changes:
     - backend/config/**/*
    when: never

Then a change in backend/config builds both backend and config because Gitlab stops processing when it sees when: always.

build_backend:
  rules:
  - if: $CI_PIPELINE_SOURCE == "push"
    changes:
    - backend/config/**/*
    when: never

  - if: $CI_PIPELINE_SOURCE == "push" 
    changes:
    - backend/**/*
    when: always

In this scenario if there is a change in both backend/src and backend/config then only config would be built because Gitlab stops when it sees the when: never.

What we want is for a change in both backend/src and backend/config to trigger both build_backend and build_config.

Is there a way to do what we want? Unfortunately there doesn’t appear to be a way to put a negative in the changes list currently. Ideally we could do something like this:

build_backend:
  rules:
  - if: $CI_PIPELINE_SOURCE == "push"
    changes:
    - backend/**/*
    - !backend/config/**/*
    when : always

or

backend_build:
  rules:
  - if: $CI_PIPELINE_SOURCE == "push"
    changes:
    - backend/**/*
    except_changes:
    - backend/config/**/*
    when: always

In the meantime there are two solutions that we could use.

  1. Move the backend/config directory up, so there would be a top level config directory as a sibling to frontend and backend. However this greatly complicates local development and the backend build. If this was just Linux we might be able to use a symlink (backend/config -> ../config) but the developers are mostly on Windows.

  2. Enumerate all the directories except config under backend, for example:

- if: $CI_PIPELINE_SOURCE == "push" && $CI_JOB_STAGE == "build"
  changes:
  - backend/*
  - backend/bin/**/*
  - backend/database/**/
  - backend/liquibase/**/*
  - backend/src/**/*
  - backend/svc/**/*
  - backend/tw/**/*
  - backend/ws/**/*
  - backend/xdocs/**/*
  when: always
- if: $CI_PIPELINE_SOURCE == "push" && $CI_JOB_STAGE == "deploy"
  changes:
  - backend/*
  - backend/bin/**/*
  - backend/database/**/
  - backend/liquibase/**/*
  - backend/src/**/*
  - backend/svc/**/*
  - backend/tw/**/*
  - backend/ws/**/*
  - backend/xdocs/**/*
  when: manual

The drawback to this is maintaining the list as the codebase evolves - especially since almost all developers don’t ever want to touch .gitlab-ci.yml. Also, there are actually two rules that use the same changes list (one always for build, another manual for deploy. I didn’t include that in the first examples for simplicity.