0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GitLab Hands-On: Experience the Issue → MR → Release Flow

0
Last updated at Posted at 2026-07-10

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:

  1. Create a new group, and create a project under it
  2. Plan large units of work with Epics, and check the timeline view with Roadmap
  3. 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)
  4. Work on a branch linked to the issue
  5. Always submit changes via a merge request (MR)
  6. The merge to main is blocked unless the MR includes a reference to the target issue
  7. MR changes are automatically scanned by Secret Detection, and detected findings are triaged in the Vulnerability Report
  8. Direct pushes to the main branch are completely disabled
  9. 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

  1. That you have a GitLab account (https://gitlab.com, or your company/school's GitLab instance)
  2. 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)
  3. 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

  1. Select the "+" icon (Create new) in the top right of the screen, then select New group.
  2. Select Create group.
  3. Enter a clear name like hello-gitlab-group in Group name (the URL will be set automatically).
  4. Choose the Visibility Level as you prefer (Private recommended).
  5. Select the Create group button at the bottom of the screen.
  6. 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

  1. Select the "+" icon (Create new) in the top right of the screen, then select New project/repository.
  2. Select Create blank project.
  3. Enter a clear name like hello-gitlab in Project name.
  4. From the dropdown to the left of Project URL, choose the group you created in 3-1.
  5. Choose the Visibility Level as you prefer (Private recommended).
  6. Be sure to check the checkbox for Initialize repository with a README. If you forget this, the main branch won't exist, and later steps (like branch protection) won't be possible.
  7. 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.

  1. Use Search or go to at the top of the screen to open the group you created in 3-1.
  2. Open the group's Manage > Labels, and select New label.
  3. Enter type::bug in Title, pick a color, and select Create label.
  4. Repeat the same steps to create type::doc and type::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.

  1. Open the project's Plan > Issue boards.
  2. Select New list.
  3. Choose Assignee as the list type, select yourself from the dropdown, and select Add to board.
  4. A new list will be added between Open and Closed (the Open and Closed lists 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 "+" iconNew 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.

include is 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 full secret_detection job 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's Reports tab and in Secure > 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 pipeline and run it once with the SECRET_DETECTION_HISTORIC_SCAN variable set to true.

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 from every 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 notes field, so jq -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) as from.

6. Screen Configuration

6-1. Protect the main Branch

  1. Open the target project

  2. Select Settings > Repository in the left sidebar

  3. Expand the Branch rules section

  4. Select the main branch

  5. Configure as follows:

    Item Setting
    Allowed to merge Developers + Maintainers
    Allowed to push and merge No one
  6. 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

  1. Open Settings > Merge requests
  2. Enter the following into the Merge request title template field:
Resolve #%{issue_id} "%{issue_title}"
  1. 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.

  1. Open Settings > Merge requests

  2. Turn on the checkbox for Require the title to match a specified pattern — the Title pattern and Title example input fields will appear

  3. Enter the following regex in Title pattern:

    ^(Draft: )?.*#\d+.*
    
  4. Enter an example of the expected title in Title example (e.g., Resolve #123 "Added setup instructions to README")

  5. 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)

  1. Open the target project
  2. Select Settings > Repository in the left sidebar
  3. Expand the Protected tags section
  4. Select Add new
  5. Select Tag and enter * (a wildcard matching all tags) in the input field
  6. Select Create wildcard
  7. Choose Maintainers for Allowed to create
  8. 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

  1. Use Search or go to at the top of the screen to select the group you created in 3-1.
  2. Select Plan > Work items in the left sidebar.
  3. Select New item.
  4. Select Epic from the Type dropdown.
  5. Enter a clear name like README Documentation Maintenance in Title.
  6. Open Labels and select type::doc from 4-1 (since this is about README updates).
  7. Open Start date, select Fixed, and enter a date 3 days before today.
  8. Similarly for Due date, select Fixed and enter a date 7 days after today.
  9. Select Create epic.
  10. After creation, open Plan > Roadmap in the left sidebar.
  11. With the default display setting (the Weeks preset), confirm that the bar for the Epic you created spans across the red vertical line indicating "today."
  12. 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)

  1. From the Roadmap screen opened in Step 1, select the bar or title of the Epic you created to open its detail screen.
  2. Select Add in the Child items section.
  3. Choose Add a new issue.
  4. Enter Add setup instructions to README in Title.
  5. Select the project you created in 3-2 from the Project dropdown.
  6. Select Create issue. The created issue automatically becomes a child item of this Epic.
  7. Select the newly added issue from the Child items list to open its detail screen. Confirm that the issue template from 5-1 has been automatically applied.
  8. Select Edit under Labels in the right sidebar to open the label selection screen. Confirm that type::bug, type::doc, and type::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).
  9. Select type::doc and apply it (not required).

Step 3: Declare You're Starting Work (Move the Card on the Board)

  1. Open Plan > Issue boards in the project.
  2. The issue created in Step 2 is still shown in the Open list on the board (being a child item of the Epic doesn't affect how the board organizes lists).
  3. Drag this card from the Open list 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

  1. From the issue's detail screen, select Create merge request.
  2. A popup appears where you can check/edit the name of the feature branch to be created (e.g., 1-readme).
  3. Select the Create merge request button 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

  1. 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.
  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).
  3. Find the Closes # line in the description and manually add the target issue number after the # (e.g., Closes #1).
  4. Select Create merge request at the bottom of the screen to create the MR.

Step 6: Edit a File

  1. 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.

  2. Select CodeOpen in Web IDE in the top right of the screen.

  3. Open and edit README.md. Add an appropriate line (e.g., a ## Setup Instructions heading and a brief description).

  4. 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

  1. Select the Source Control icon in the left activity bar (or press Ctrl+Shift+G / Control+Shift+G).

  2. 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
    
  3. 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

  1. Open the Reports tab on the MR's detail screen.
  2. 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 via View all pipeline findings).
  3. Open the secret_detection job log from Build > Jobs, and confirm it shows leaks found: 1.
  4. Downloading the artifact from Job artifacts also 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

  1. Since the MR created from the issue starts in Draft state, first select Mark as ready at the top of the screen to remove the Draft status.
  2. Confirm that the title validation result is shown in the merge checks section, and that it's green if everything is fine.
  3. 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

  1. Select the Merge button.
  2. Confirm that the target issue is automatically closed, thanks to the Closes # pattern in the MR description.
  3. Because the fake secret from Step 6 has now been merged into main, this "finding" becomes a "vulnerability."

Step 11: Check the Vulnerability

  1. Open Secure > Vulnerability report in the left sidebar.
  2. Select the vulnerability detected in Step 8 (Secret type: GitLab personal access token).
  3. Check the details (secret type, remediation guidance, detection date/location) under Description.

Step 12: Change the Status (Triage)

  1. In the vulnerability list, choose Dismissed from the status options (Needs triage, Confirmed, Dismissed, Resolved).
  2. Choose Used in tests from the Select dismissal reason dropdown.
  3. Enter a reason like "Fake token for demo purposes" in the required Add a comment field.
  4. 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.
  5. 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 the glpat - ... 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

  1. Open Code (or Repository) → Tags in the left sidebar.
  2. Select New tag.
  3. Enter a version number like v0.1.0 in Tag name, and leave Create from as main.
  4. Leave Release notes blank (since the CI job from 5-4 generates it automatically — otherwise a duplicate Release would be created separate from what's written here).
  5. Select Create tag.

Step 14: Check the Release

  1. The tag push triggers a pipeline run.
  2. Confirm the create-release job succeeded under Build > Jobs.
  3. Open Deploy > Releases in the left sidebar.
  4. 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: Value line at the end of a commit message — the same family as Signed-off-by: Name <email> or Co-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 Changelog key 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.

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?