GitLab Hands-On: Experience the Issue → MR → Release Flow
Assumed version: GitLab 19.0 or later / Required plan: Ultimate
This document is structured so that you actually create a new group, create a small project under it, and experience hands-on the full flow from issue creation through MR, merge, tagging, and Release. Please proceed in order from the top.
For enforcing issue references, we use the native "Merge request title validation" feature (Premium and above); for centrally managing Secret Detection findings, we use the Vulnerability Report (Ultimate only). This tutorial assumes the Ultimate plan.
1. Goals and Feature Overview
By completing this hands-on to the end, you will experience and confirm the following workflow:
- Create a new group, and create a project under it
- Plan large units of work with Epics, and check the timeline view with Roadmap
- Always create an issue before changing code (use Scoped Labels to classify issue types, and track progress using the assignee list in the issue board)
- Work on a branch linked to the issue
- Always submit changes via a merge request (MR)
- The merge to main is blocked unless the MR includes a reference to the target issue
- MR changes are automatically scanned by Secret Detection, and detected findings are triaged in the Vulnerability Report
- Direct pushes to the
mainbranch are completely disabled - After merging to
main, tagging is done manually, and GitLab Release creation (including automatic changelog generation) is automated via a CI job
Required permissions: Owner role at the group level, Maintainer or Owner role at the project level
Below is a quick-reference table matching each feature to its corresponding step, in the actual order of operations.
| Feature | Corresponding Step |
|---|---|
| Group creation | 3-1 |
Scoped Label (key::value format, for classifying issue type) |
4-1 |
| Issue board (Assignee list) | 4-2 |
| Epic / Roadmap | 7 (Steps 1–2) |
| Issue/MR templates | 5-1, 5-2 |
| Secret Detection (pipeline scanning) | 5-3 |
| GitLab Releases (CI job auto-creates on tag push) | 5-4, 7 |
| Changelog API (automatic release note generation) | 5-4, 9-1 |
| Protected branches (role-based protection) | 6-1 |
| Protected branches (user/group-specific) | 6-1 (optional) |
| MR title template | 6-2 |
| MR title validation (blocking via regex) | 6-3 |
| Protected tags (role-based) | 6-4, Appendix |
| Protected tags (user/group-specific) | Appendix |
| Managing Secret Detection results via Vulnerability Report | 7 |
| Push rules (commit message regex) | Mentioned in Appendix |
| Merge request approval rules (required approvals, Code Owners) | Mentioned in Appendix |
All features used in this tutorial are included in the Ultimate plan.
2. Things to Check First
- That you have a GitLab account (
https://gitlab.com, or your company/school's GitLab instance) - That you can create a new group (you'll create it in Section 3-1). After creation, confirm that you can apply the Ultimate plan to that group (a trial is fine)
- That you have a role that allows group creation (usually anyone can create one), or that you have the Owner role on an existing group
If you don't yet have an Ultimate contract, the free trial (30 days) is enough to try everything in this tutorial.
3. Create a Group and Project
Since this hands-on uses Epics, Roadmap, and group-level labels, we'll start by creating a new group.
3-1. Create a Group
- Select the "+" icon (Create new) in the top right of the screen, then select New group.
- Select Create group.
- Enter a clear name like
hello-gitlab-groupin Group name (the URL will be set automatically). - Choose the Visibility Level as you prefer (
Privaterecommended). - Select the Create group button at the bottom of the screen.
- Confirm that the Ultimate plan is applied to the group you created (if using a trial, you can start it from
Settings > Billing).
3-2. Create a Project Under the Group
- Select the "+" icon (Create new) in the top right of the screen, then select New project/repository.
- Select Create blank project.
- Enter a clear name like
hello-gitlabin Project name. - From the dropdown to the left of Project URL, choose the group you created in 3-1.
- Choose the Visibility Level as you prefer (
Privaterecommended). -
Be sure to check the checkbox for Initialize repository with a README. If you forget this, the
mainbranch won't exist, and later steps (like branch protection) won't be possible. - Select the Create project button at the bottom of the screen.
Now you have a small project containing only README.md. From here, we'll actually experience adding one feature to this project.
4. Create Labels and an Issue Board
4-1. Create a Scoped Label for Issue Type
A label in key::value format (a Scoped Label) means that labels within the same scope can't be applied simultaneously (e.g., applying type::bug automatically removes type::doc). Here, we won't use this for tracking progress — it's used purely to classify the type of issue. We won't set strict operating rules; it's fine to just apply whichever one fits at creation time.
- Use
Search or go toat the top of the screen to open the group you created in 3-1. - Open the group's
Manage > Labels, and selectNew label. - Enter
type::buginTitle, pick a color, and selectCreate label. - Repeat the same steps to create
type::docandtype::feature.
Note:
- There's a reason we're creating these labels on the group rather than the project.
- In Section 7, when you try to add a label to an issue in the project, the
type::*labels created here at the group level will appear as options automatically, without any configuration on the project side. This is because group labels are automatically inherited by all projects underneath.- We'll actually confirm this in Step 2 of Section 7.
4-2. Create an Assignee List on the Issue Board
For tracking progress, instead of labels, we'll use an Assignee list (a list showing issues assigned to a specific user). Simply dragging a card into this list assigns the issue to yourself, giving you an at-a-glance view of who's currently working on what.
- Open the project's
Plan > Issue boards. - Select
New list. - Choose
Assigneeas the list type, select yourself from the dropdown, and selectAdd to board. - A new list will be added between
OpenandClosed(theOpenandClosedlists on either end are always fixed).
Keep the default Open list as is. The flow will be: issues go into the Open list when created, and you drag them to your own assignee list when you declare that you're starting work on them.
5. Add the Initial Required Files
Before protecting the project, we'll add the necessary files first (since after protection, adding files itself would need to go through an MR, adding extra steps).
How to add a file: On the project's top page, near the file list, select the "+" icon → New file, enter the path and file content, and commit directly to main using Commit changes at the bottom.
5-1. Issue Template
Create .gitlab/issue_templates/Default.md.
## Summary
<!-- Describe what you want to do and why it's needed -->
## Definition of Done
- [ ]
- [ ]
## Additional Info
<!-- Reference links, screenshots, etc. -->
From now on, this template will be automatically applied whenever you create a New issue. We haven't included a /label quick action. Since the type::bug / type::doc / type::feature labels created in 4-1 are just for classification, it's enough to manually apply whichever fits after creating the issue (not required).
5-2. MR Description Template
Create .gitlab/merge_request_templates/Default.md.
## Related Issue
Closes #
## Changes
## How to Verify
Writing Closes #123 has the side effect that the target issue is automatically closed when the MR is merged.
5-3. .gitlab-ci.yml (for Secret Detection)
Add Pipeline secret detection, which detects API keys or tokens that have accidentally slipped into a commit.
variables:
AST_ENABLE_MR_PIPELINES: "true"
include:
- template: Jobs/Secret-Detection.gitlab-ci.yml
AST_ENABLE_MR_PIPELINES is a variable that ensures this job reliably runs as part of the MR pipeline we'll create later in Section 7. With just this, a secret_detection job will be added to your pipelines from now on, and will scan automatically on every push and MR creation.
includeis a feature that lets you pull in officially maintained GitLab template job definitions into your own.gitlab-ci.yml. The benefit is that you can reuse jobs without writing the contents yourself (image, script, etc.) — when it actually runs, this one line expands into the fullsecret_detectionjob definition (image specification, script, etc.), and the pipeline runs with that.
On the Ultimate plan, detection results aren't just downloadable as a pipeline artifact (
gl-secret-detection-report.json) — they're also displayed in the MR'sReportstab and inSecure > Vulnerability report, allowing centralized management (we'll confirm this in Section 7).The first time you enable this, existing commit history is not scanned (only the current tree state is). If you want to scan past commits as well, go to
Build > Pipelines > New pipelineand run it once with theSECRET_DETECTION_HISTORIC_SCANvariable set totrue.
Committing this file will trigger the first pipeline on the main branch. If you check the secret_detection job log under Build > Jobs, it should show no leaks found (we'll actually trigger a detection in Section 7).
5-4. .gitlab-ci.yml (for Automated Release Creation)
Add a job that, when a tag is pushed, automatically creates a GitLab Release accompanied by release notes generated via the Changelog API. Append the following to the file from 5-3.
stages:
- test
- release
create-release:
stage: release
image: registry.gitlab.com/gitlab-org/cli:latest
rules:
- if: '$CI_COMMIT_TAG'
before_script:
- apk add --no-cache curl jq git
- git fetch --tags
script:
- |
PREV_TAG=$(git tag --sort=-v:refname | sed -n '2p')
if [ -n "$PREV_TAG" ]; then
FROM_REF="$PREV_TAG"
else
FROM_REF=$(git rev-list --max-parents=0 HEAD | tail -n 1)
fi
echo "PREV_TAG=${PREV_TAG:-(none)}"
echo "FROM_REF=${FROM_REF}"
echo "--- commits in range ${FROM_REF}..HEAD ---"
git log --oneline "${FROM_REF}..HEAD"
RESPONSE=$(curl --silent --header "JOB-TOKEN: ${CI_JOB_TOKEN}" \
--get \
--data-urlencode "version=${CI_COMMIT_TAG}" \
--data-urlencode "from=${FROM_REF}" \
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/repository/changelog")
echo "--- raw API response ---"
echo "$RESPONSE"
NOTES=$(echo "$RESPONSE" | jq -r '.notes')
if [ -z "$NOTES" ] || [ "$NOTES" = "null" ]; then
echo "Could not retrieve content from the Changelog API (either the target commits have no trailer, or the API call failed)." > release_notes.md
else
echo "$NOTES" > release_notes.md
fi
release:
tag_name: '$CI_COMMIT_TAG'
name: 'Release $CI_COMMIT_TAG'
description: './release_notes.md'
We explicitly declare stages because the release stage is not included in GitLab's default stage list (the test stage used by the secret_detection job is included by default, so it works as-is).
This job calls GitLab's Changelog API with a GET request (a read-only call that doesn't modify the repository), retrieves the changes since the previous tag organized by category as text, and uses it directly as the Release description. Only commits in the format described in 9-1 are included.
Why explicitly specify
fromevery time:
- If you omit
from, the Changelog API automatically looks for the "previous version tag."- However, if there is no previous tag (i.e., this is the very first release) and you omit
from, the API returns an error.- This error response has no
notesfield, sojq -r '.notes'outputs the string"null", which then becomes the release description as-is.- To avoid this, when there's no previous tag, we explicitly pass the repository's first commit (
git rev-list --max-parents=0 HEAD) asfrom.
6. Screen Configuration
6-1. Protect the main Branch
-
Open the target project
-
Select
Settings > Repositoryin the left sidebar -
Expand the
Branch rulessection -
Select the
mainbranch -
Configure as follows:
Item Setting Allowed to merge Developers + Maintainers Allowed to push and merge No one -
Click
Save changes
This means that even users with the Developer or Maintainer role can no longer push directly to main — they must create and merge an MR.
Note: Unless you explicitly set "Allowed to push and merge" to "No one," direct pushes will not actually be restricted. Be careful not to leave this at its default.
6-2. Set an MR Title Template
- Open
Settings > Merge requests - Enter the following into the
Merge request title templatefield:
Resolve #%{issue_id} "%{issue_title}"
Save changes
When you select "Create merge request" from an issue's detail screen, the branch name and MR title are auto-generated. The title will be filled in a format like Resolve #123 "Added setup instructions to README".
If
%{issue_id}ends up blank:
- This variable is only populated when the branch name is in a format that includes the issue number (e.g.,
123-readme).- If you create a branch manually from scratch, this can end up blank.
- Always create it via the "Create merge request" button on the issue's detail screen (we'll actually do this in Step 4 of Section 7). Going through this button, right after the branch is created in a popup, you're taken directly to the MR creation screen that inherits that branch name.
This is merely a "default value" and has no enforcing power on its own. In 6-3 next, we'll build the mechanism that actually blocks merges.
6-3. Block Merges When an MR Lacks an Issue Reference
This is the heart of today's workflow. We'll use the native "MR title validation" feature, which checks the MR title against a regular expression, and physically disables the merge button if it doesn't match the pattern.
-
Open
Settings > Merge requests -
Turn on the checkbox for
Require the title to match a specified pattern— theTitle patternandTitle exampleinput fields will appear -
Enter the following regex in
Title pattern:^(Draft: )?.*#\d+.* -
Enter an example of the expected title in
Title example(e.g.,Resolve #123 "Added setup instructions to README") -
Save changes
Now, any MR whose title doesn't include an issue number will show a validation failure in the merge checks section, and the merge button will be blocked. Combined with the "MR title template" from 6-2, titles will already satisfy the pattern by default without users needing to do anything, so the operational overhead becomes practically zero.
The regex uses RE2 syntax. This differs somewhat from PCRE (the syntax used by many online regex testers) — lookaheads and backreferences aren't supported.
This title validation targets only the MR title, not the description. If your workflow only puts "Closes #123" in the description, this will conflict with the title validation, so be sure to use it together with the title template from 6-2.
6-4. Protect Tags (Recommended)
- Open the target project
- Select
Settings > Repositoryin the left sidebar - Expand the
Protected tagssection - Select
Add new - Select
Tagand enter*(a wildcard matching all tags) in the input field - Select
Create wildcard - Choose
MaintainersforAllowed to create - Select
Protect
This restricts tag creation to Maintainer and above, preventing unintended tags from being created by anyone.
7. Hands-On: Issue → MR → Release
With the setup complete, let's actually experience the full flow. We'll use the fictional change "add setup instructions to the README" as our example.
Step 1: Create an Epic
- Use
Search or go toat the top of the screen to select the group you created in 3-1. - Select
Plan > Work itemsin the left sidebar. - Select
New item. - Select
Epicfrom theTypedropdown. - Enter a clear name like
README Documentation MaintenanceinTitle. - Open
Labelsand selecttype::docfrom 4-1 (since this is about README updates). - Open
Start date, selectFixed, and enter a date 3 days before today. - Similarly for
Due date, selectFixedand enter a date 7 days after today. - Select
Create epic. - After creation, open
Plan > Roadmapin the left sidebar. - With the default display setting (the
Weekspreset), confirm that the bar for the Epic you created spans across the red vertical line indicating "today." - Confirm that this screen is in Gantt chart format, displaying periods as horizontal bars, giving it a WBS (Work Breakdown Structure)-like appearance.
Step 2: Create an Issue (from the Epic)
- From the Roadmap screen opened in Step 1, select the bar or title of the Epic you created to open its detail screen.
- Select
Addin theChild itemssection. - Choose
Add a new issue. - Enter
Add setup instructions to READMEinTitle. - Select the project you created in 3-2 from the
Projectdropdown. - Select
Create issue. The created issue automatically becomes a child item of this Epic. - Select the newly added issue from the
Child itemslist to open its detail screen. Confirm that the issue template from 5-1 has been automatically applied. - Select
EditunderLabelsin the right sidebar to open the label selection screen. Confirm thattype::bug,type::doc, andtype::feature— created at the group level in 4-1 — appear as options, even though nothing was configured on the project side (this is the experience of group labels being inherited by the project). - Select
type::docand apply it (not required).
Step 3: Declare You're Starting Work (Move the Card on the Board)
- Open
Plan > Issue boardsin the project. - The issue created in Step 2 is still shown in the
Openlist on the board (being a child item of the Epic doesn't affect how the board organizes lists). - Drag this card from the
Openlist to your own Assignee list created in 4-2. This assigns the issue to yourself, signaling to the team that work has started.
Step 4: Create a Branch
- From the issue's detail screen, select
Create merge request. - A popup appears where you can check/edit the name of the feature branch to be created (e.g.,
1-readme). - Select the
Create merge requestbutton inside the popup. The feature branch is only created at the moment you do this (it doesn't exist before then).
Step 5: Create an MR
- After the branch is created, you're taken to the still-empty MR creation screen, with the MR title auto-filled according to the template from 6-2. The description is pre-filled from the template in 5-2.
-
If the project has an MR description template configured (5-2), the issue number is not automatically inserted after
Closes #(without a template, it would be auto-inserted from the branch name, but having a template overrides that behavior). - Find the
Closes #line in the description and manually add the target issue number after the#(e.g.,Closes #1). - Select
Create merge requestat the bottom of the screen to create the MR.
Step 6: Edit a File
-
The branch selector in the top left has already automatically switched to the target feature branch (e.g.,
1-readme) once the MR was created. -
Select
Code→Open in Web IDEin the top right of the screen. -
Open and edit
README.md. Add an appropriate line (e.g., a## Setup Instructionsheading and a brief description). -
To actually confirm how Secret Detection behaves, also add the following line (remove the spaces around the hyphen so it resembles the appearance of a personal access token):
glpat - 12345678901234567890
Step 7: Commit
-
Select the
Source Controlicon in the left activity bar (or press Ctrl+Shift+G / Control+Shift+G). -
In the commit message field, enter a title on the first line, leave a blank line, then add
Changelog: added(explained in detail in 9-1):Add setup instructions to README Changelog: added -
Since the feature branch created in Step 4 is still selected, simply select
Commit(no need to create a new branch). Since the MR already exists, this commit is automatically added to the MR from Step 5.
Step 8: Check the Detection Results
- Open the
Reportstab on the MR's detail screen. - Select
Security scan, and confirm it shows something like "Secret detection detected 1 new potential vulnerability" (you can also see the full list of findings viaView all pipeline findings). - Open the
secret_detectionjob log fromBuild > Jobs, and confirm it showsleaks found: 1. - Downloading the artifact from
Job artifactsalso lets you see details like the type of secret detected (GitLab personal access token), the severity (Critical), and the line number within the file.
Step 9: Check Merge Checks
- Since the MR created from the issue starts in
Draftstate, first selectMark as readyat the top of the screen to remove theDraftstatus. - Confirm that the title validation result is shown in the merge checks section, and that it's green if everything is fine.
- Secret Detection findings don't block the merge itself (detection is only a warning — the merge decision is made by a person).
Step 10: Merge
- Select the
Mergebutton. - Confirm that the target issue is automatically closed, thanks to the
Closes #pattern in the MR description. - Because the fake secret from Step 6 has now been merged into
main, this "finding" becomes a "vulnerability."
Step 11: Check the Vulnerability
- Open
Secure > Vulnerability reportin the left sidebar. - Select the vulnerability detected in Step 8 (Secret type:
GitLab personal access token). - Check the details (secret type, remediation guidance, detection date/location) under
Description.
Step 12: Change the Status (Triage)
- In the vulnerability list, choose
Dismissedfrom the status options (Needs triage,Confirmed,Dismissed,Resolved). - Choose
Used in testsfrom theSelect dismissal reasondropdown. - Enter a reason like "Fake token for demo purposes" in the required
Add a commentfield. - Confirm that this vulnerability no longer appears on the Vulnerability Report's top screen. Triaging is just a record that "no action is needed for now" — the code itself doesn't change.
- If you want to clean things up, create one new issue (e.g.,
Remove fake token from README.md) and follow the same steps 2–10 (Create merge request → edit → commit → merge) to delete theglpat - ...line. If a real leak occurs, in addition to triaging or deleting it, you must immediately revoke the affected token. Since the pre-deletion commit remains in Git history, deletion alone does not fully eliminate the leak risk.
Step 13: Create a Tag
- Open
Code(orRepository) →Tagsin the left sidebar. - Select
New tag. - Enter a version number like
v0.1.0inTag name, and leaveCreate fromasmain. -
Leave
Release notesblank (since the CI job from 5-4 generates it automatically — otherwise a duplicate Release would be created separate from what's written here). - Select
Create tag.
Step 14: Check the Release
- The tag push triggers a pipeline run.
- Confirm the
create-releasejob succeeded underBuild > Jobs. - Open
Deploy > Releasesin the left sidebar. - Confirm that the content of the commit with the trailer from Step 7 has been automatically reflected as the release notes.
You've now experienced the full flow from issue creation through to Release.
9. Deciding on Version Numbers (Reference)
To avoid agonizing over this every time you create a tag, here's a rough guideline:
- patch (e.g., v1.2.0 → v1.2.1): bug fixes only
- minor (e.g., v1.2.1 → v1.3.0): backward-compatible new features
- major (e.g., v1.3.0 → v2.0.0): breaking changes
9-1. Add a Trailer to Commit Messages
To auto-generate release notes, add one dedicated Git trailer line at the end of the commit message.
Add setup instructions to README
Changelog: added
Why the string
Changelog: added:
- This isn't GitLab-specific notation — it builds on the standard "trailer" mechanism that has long been used in the Git world.
- It's a style of adding one
Key: Valueline at the end of a commit message — the same family asSigned-off-by: Name <email>orCo-authored-by: Name <email>(Git even has a dedicated command for handling trailers,git interpret-trailers).- The benefit is you don't need to change how you write the commit's first line (subject).
- The job from 5-4 that calls the Changelog API looks for this
Changelogkey name (as the default setting).- The vocabulary of values —
added/fixed/changed/removed/security— is also not a GitLab invention; it's essentially taken directly from the categories used in the widely adopted OSS convention Keep a Changelog (Added/Changed/Removed/Fixed/Security, etc.).
The main usable categories are added / fixed / changed / removed / security. Only commits with this trailer are targeted by the Changelog API called by the create-release job in 5-4 (commits without a trailer are ignored).
If you want to customize the output format, you can create a .gitlab/changelog_config.yml configuration file to define date formats and templates.
If you're publishing Docker images or build artifacts to a package registry, and the package version matches the release tag, GitLab will automatically include that package in the release evidence — no additional configuration needed.