Tagging nightly shipped a release
Goal
By changing the situation, you confirm how jobs get into and drop out of the list depending on the branch, the tag, the existence of a file, and the changes against the remote, and you engrave the deployment policy into the configuration with the order of the rules and the variables the rules set.
Why it matters
rules is not a condition asked when a job starts but a rule that decides the list when the pipeline is created. Only the first match applies, so the order of entries is itself the policy, and if you put them in the wrong order, the deployment disappears without an error or a release goes out on the wrong tag. The result of changes varies depending on what it is compared with, and if you keep the condition and the value (the deployment target) apart, a day will come when the two come out of line. Such differences are hard to see just by reading the configuration, so you have to change the situation and run it.
Steps
- Make
/root/glci-rulesa git repository and, in.gitignore, put.gitlab-ci-local/. In.gitlab-ci.yml, put stages[build, deploy], a jobunitwith no condition (build;echo "unit log=$LOG_LEVEL"), and, created only when$CI_COMMIT_BRANCH == "main",deploy-staging(deploy;echo staging), and commit on the main branch. The grader switches to thefeature/loginbranch in a copy and checks that deploy-staging disappears from the list. - Add a job
release-notes(deploy;echo notes) that is created only when$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/, and commit. gitlab-ci-local does not read git tags, so it imitates a tag pipeline with--variable CI_COMMIT_TAG=v1.2.0. With v1.2.0 it must be in the list, and withnightlyor no tag it must not. - Give the job
publish(deploy;echo publish) three entries in rules: if there is a tag,when: on_success; if it is the main branch,when: manualandallow_failure: false; and otherwisewhen: never. Commit. In the list, it must be manual (allowFailure false) on main, on_success if the tag variable is present, and absent on a feature branch. - Add a job
docker-build(build;echo docker) withrules: - exists: [Dockerfile]and commit. Do not create a Dockerfile in this repository yet. The grader checks that the job appears only when a Dockerfile is put into a copy. - Make a bare repository at
/root/glci-rules-origin.git, register it asorigin, push main, and then rungit remote set-head origin main. Then add a jobdocs-build(build;echo docs) withrules: - changes: ["docs/**/*"], commit, and push again. The grader makes a branch in which something under docs was changed and a branch in which only other files were changed, in copies, and checks that docs-build appears only in the former case. - Add a job
deploy(deploy;echo "target=$DEPLOY_TARGET"). Its rules arevariables: {DEPLOY_TARGET: production}for a tag andvariables: {DEPLOY_TARGET: staging}for main. Commit and push. When run on main,target=stagingmust be printed, and if you give the tag variable,target=productionmust be printed. - At the very top of
.gitlab-ci.yml, putworkflow: rules:. For main it isvariables: {LOG_LEVEL: warn}, and for everything else (when: always) it isvariables: {LOG_LEVEL: debug}. Commit and push. On main, the unit log must beunit log=warn, and on a feature branch it must beunit log=debug.
Notes
- This VM has no GitLab server or runner, and gitlab-ci-local 4.75.1 interprets .gitlab-ci.yml by the same rules as GitLab and runs jobs with the shell. If you write
image:, it tries to run with Docker, so do not use it. Protected variables, masking, CI_JOB_TOKEN, runner tags, and merge request pipeline creation are server features and are not reproduced here. - Run: at the repository root,
gitlab-ci-local --shell-isolation --no-artifacts-to-source(a separate working directory per job, artifacts not written back to the repository), job list:gitlab-ci-local --list-csv-all, interpreted configuration:gitlab-ci-local --preview. gitlab-ci-local passes only files tracked by git to jobs, sogit adda file after creating it. The grader copies the repository, commits all files, and runs it again with the same tool. - Differences in gitlab-ci-local (measured): it does not read a git tag as CI_COMMIT_TAG, so you imitate it with
--variable CI_COMMIT_TAG=.... rules:changes compares with origin's default branch and ignores compare_to. The behavior of not creating a pipeline through workflow:rules and the behavior of a manual job blocking later stages are not reproduced. - When you try switching branches, do it in a copy or a new branch, and come back to main and commit before grading.
- Specify when jobs run with rules · workflow · Predefined CI/CD variables · CI/CD YAML syntax reference
Create the staging deployment only on main
Make /root/glci-rules a git repository and, in .gitignore, put .gitlab-ci-local/. In .gitlab-ci.yml, put stages [build, deploy], a job unit with no condition (build; echo "unit log=$LOG_LEVEL"), and, created only when $CI_COMMIT_BRANCH == "main", deploy-staging (deploy; echo staging), and commit on the main branch. The grader switches to the feature/login branch in a copy and checks that deploy-staging disappears from the list.
rules is evaluated when the pipeline is created. If no condition matches, that job drops out of the list as 'never'. Change the branch and compare the when column with gitlab-ci-local --list-csv-all.
Look even at the shape of the tag name to create release notes
Add a job release-notes (deploy; echo notes) that is created only when $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/, and commit. gitlab-ci-local does not read git tags, so it imitates a tag pipeline with --variable CI_COMMIT_TAG=v1.2.0. With v1.2.0 it must be in the list, and with nightly or no tag it must not.
=~ is a regular expression comparison. The tag variable is filled only in a tag pipeline, so if you look only at its existence, a release goes out even for a temporary tag such as nightly. Wrap the regular expression in slashes.
Only the first match applies — the order is the policy
Give the job publish (deploy; echo publish) three entries in rules: if there is a tag, when: on_success; if it is the main branch, when: manual and allow_failure: false; and otherwise when: never. Commit. In the list, it must be manual (allowFailure false) on main, on_success if the tag variable is present, and absent on a feature branch.
It reads from the top and uses only the first entry that matches. If you put a when: never with no condition in the middle, the entries below it are never read, and no error appears. To use a manual job as a real approval gate, put allow_failure: false along with it.
Bake the image only in repositories that have a Dockerfile
Add a job docker-build (build; echo docker) with rules: - exists: [Dockerfile] and commit. Do not create a Dockerfile in this repository yet. The grader checks that the job appears only when a Dockerfile is put into a copy.
exists looks at whether a file at that path exists in the repository. When several repositories include the same template, you use it to turn on only the relevant jobs without editing the configuration of each repository.
Build the docs only on a branch where the docs changed
Make a bare repository at /root/glci-rules-origin.git, register it as origin, push main, and then run git remote set-head origin main. Then add a job docs-build (build; echo docs) with rules: - changes: ["docs/**/*"], commit, and push again. The grader makes a branch in which something under docs was changed and a branch in which only other files were changed, in copies, and checks that docs-build appears only in the former case.
For changes, the key is 'changed compared with what'. gitlab-ci-local compares with the remote default branch (origin/main), so a remote is needed. In GitLab, a branch pipeline compares with the previous push and a merge request pipeline compares with the target branch.
Which rule you matched decides the deployment target
Add a job deploy (deploy; echo "target=$DEPLOY_TARGET"). Its rules are variables: {DEPLOY_TARGET: production} for a tag and variables: {DEPLOY_TARGET: staging} for main. Commit and push. When run on main, target=staging must be printed, and if you give the tag variable, target=production must be printed.
The variables of a rules entry go into the job only when that entry matches. If you keep the condition and the value in one place, a mismatch such as "it was a tag but went out to staging" structurally disappears.
Values for the whole pipeline are decided in workflow
At the very top of .gitlab-ci.yml, put workflow: rules:. For main it is variables: {LOG_LEVEL: warn}, and for everything else (when: always) it is variables: {LOG_LEVEL: debug}. Commit and push. On main, the unit log must be unit log=warn, and on a feature branch it must be unit log=debug.
workflow:rules decides whether to create the pipeline and the variables that go into the whole pipeline. You do not have to repeat the same condition in every job. This is also the place to block merge request and branch pipelines from overlapping, but that behavior can be confirmed only on a GitLab server.