GitLab Hands-on: Safely Operating Changes to an Existing System
Prerequisite version: GitLab 19.0 or later / Required plan: Ultimate + GitLab Duo (Premium+ add-on, trial available)
When migrating the development and maintenance of an existing system to GitLab, what really needs to be verified isn't "can the source code be moved," but "is this a foundation on which day-to-day change work can be run safely and continuously." This hands-on is built to verify that question through hands-on operations.
A single change request against an existing system is filed as one Issue, and design, development, review, testing, and release are connected end-to-end using only GitLab's standard features. Controls that block merges without an Issue reference, Merge Request approval rules that require designated approvers, and a mechanism that visualizes test results as JUnit reports on the MR all serve as the foundation for keeping change work from becoming dependent on any one person, while still guaranteeing quality. In addition, you'll experience automatic detection and triage of secrets accidentally included in changes, and automatic release-note generation, giving you an end-to-end look at security and release management as well.
Further, for tasks like investigating design or understanding code, we'll also make ad hoc use of the GitLab Duo Agent Platform (DAP), so you can check out a level-up development experience that incorporates AI agents.
This isn't just about swapping out a version-control tool — it's a hands-on, in the smallest possible setup, for gathering the evidence you need to judge whether GitLab can function as a foundation for development, maintenance, and quality management with AI adoption in view.
0. Preparation: Check the Existing System (Bitbucket) You'll Import From
This hands-on proceeds under the scenario "migrate an existing system running on Bitbucket Cloud to GitLab." The existing system you'll migrate from is already prepared as the following public Bitbucket repository.
https://bitbucket.org/gl_tsukasa/hello-legacy-app.git
In section 3-2, you'll import this repository into GitLab as-is using New project > Import project.
A note on this document: The rest of the steps (the CI setup in Chapter 5, the implementation in Chapter 7, etc.) are written assuming this repository contains
README.md/package.json/src/discount.js(the discount-calculation logic) /test/discount.test.js(its test). If the actual imported result has different file names or function names, please read the affected sections against the actual content as you go.
0-1. Check the repository (optional)
- Visit
https://bitbucket.org/gl_tsukasa/hello-legacy-appin your browser. - Since it's a public repository, confirm you can browse the source code without a Bitbucket account.
- From the
Sourcetab, take a look at the overall file layout:README.md,package.json,src/discount.js,test/discount.test.js, and so on.
0-2. Clone locally and check the contents (optional)
If you'd like to inspect the contents more closely in your local environment, you can clone it as follows (no authentication is needed, since it's a public repository).
git clone https://bitbucket.org/gl_tsukasa/hello-legacy-app.git
cd hello-legacy-app
If Node.js (v20 or later) is installed, you can run the tests directly.
node --test
Confirm that all tests pass (pass).
About the command: When you explicitly pass a path to
node --test, that path is interpreted as a glob pattern. A directory name liketest/doesn't match as a glob pattern, so it gets misinterpreted as a path to a single file, which can lead to aCannot find moduleerror. With no argument,node --testfollows Node.js's standard discovery patterns (such as**/test/**/*.js), sotest/discount.test.jsis found automatically — use that form instead.
This clone is purely for verification. Bringing the code into GitLab won't be done by pushing this local copy — in section 3-2, you'll import it directly from Bitbucket using the
Import projectfeature. If you don't need a local clone, feel free to skip all of Chapter 0 and start from Chapter 3.
1. Goals and Plan Overview
By the time you finish this hands-on, you'll have experienced and confirmed the following workflow:
- Create a new group, and create a project underneath it (simulating the migration of an existing system)
- Manage the existing system's maintenance/change plan with an Epic, and check the timeline view in Roadmap
- Before changing code, always file a single change request as an Issue (classify the change type with Scoped Labels, and track progress with the assignee list on an Issue board)
- Before implementation, use GitLab Duo Chat ad hoc to check the design and behavior of the existing code
- Work in a branch tied to the issue
- Always submit changes via a Merge Request (MR)
- An MR that doesn't reference its issue is blocked from merging into main
- MR changes are automatically scanned by Secret Detection, and any findings are triaged in the Vulnerability Report
- MR test results are visualized on the MR as a JUnit report
- Merging an MR requires approval under a mandatory-approver (Merge request approval rule)
- Direct pushes to the
mainbranch are completely disallowed - After merging into
main, tagging is done manually, and GitLab Release creation (including automatic changelog generation) is automated by a CI job
Required permissions: Owner of the group, Maintainer or Owner of the project
Here's the overall flow as a diagram:
Here's a quick-reference table mapping each feature to the section where you'll use it, in the order you'll actually work through them.
| Feature | Corresponding step |
|---|---|
| Preparation: checking the import source (Bitbucket) | 0 |
| Creating the group | 3-1 |
| Import project (migrating the existing system from Bitbucket) | 3-2 |
Scoped Labels (key::value format, for classifying change type) |
4-1 |
| Issue board (assignee list) | 4-2 |
| Issue/MR templates | 5-1, 5-2 |
| Secret Detection (pipeline scanning) | 5-3 |
| Unit tests / JUnit report | 5-4, 7 (step 9) |
| GitLab Releases (CI job auto-creates on tag push) | 5-5, 7 |
| Changelog API (automatic release-note generation) | 5-5, 8-1 |
| Protected branches (role-based protection) | 6-1 |
| MR title template | 6-2 |
| MR title validation (blocking via regex) | 6-3 |
| Merge request approval rule (mandatory approver) | 6-4, 7 (step 11) |
| Protected tags (role-based) | 6-5 |
| Epic / Roadmap (maintenance plan for the existing system) | 7 (steps 1-2) |
| GitLab Duo Chat (ad hoc use for design/understanding code) | 0-1, 7 (step 4) |
| Managing Secret Detection findings via the Vulnerability Report | 7 |
| (Optional) Duo Agent Platform: Issue to MR Flow | 9 |
| (Optional) Reporting with Duo Agentic Chat | 10 |
| (Optional) Duo Code Review (MR review) | 11 |
All of the features used in this tutorial are included in the Ultimate plan (only GitLab Duo requires a separate add-on subscription).
2. Things to Check First
- You have a GitLab account (
https://gitlab.com, or your company's/school's GitLab instance) - You can create a new group (you'll do this in section 3-1). After creating it, you can apply the Ultimate plan to that group (a trial is fine)
- You have a role that allows you to create groups (usually anyone can), or the Owner role on an existing group
- GitLab Duo is available to you (you'll use Duo Chat in step 4. If it's not available, you can skip that step without affecting the rest of the flow)
If you don't yet have an Ultimate contract, a free 30-day trial is enough to try everything in this tutorial.
In a real migration project, you would bring an existing repository you already have (GitLab/GitHub/Bitbucket, or an internal Git server) into GitLab directly via
New project > Import project. In this hands-on, you'll experience that by actually importing, in section 3-2, the real sample repository on Bitbucket Cloud shown in Chapter 0.
3. ★ (The Real Starting Point) Create the Group and Project
Since we'll use Epics, Roadmap, and group-level labels throughout this hands-on, we start by creating a new group.
3-1. Create a group
- Select Create group or Create Sub group.
- Enter an easy-to-recognize Group name such as
hello-gitlab-group(the URL is decided automatically). - Set Visibility Level as you like (
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. Import the existing system from Bitbucket
Bring in hello-legacy-app (Bitbucket Cloud), which you checked in Chapter 0, as-is using GitLab's Import project feature. This is the operation equivalent to "migrating the existing system to GitLab."
-
Click New project in the top right of the screen.
-
Select Import project.
-
From the list of import sources, select Repository by URL (since Bitbucket's public repositories can be cloned via the standard Git protocol, you can bring it in this way without using a dedicated Bitbucket connector).
-
Enter the following in Git repository URL.
https://bitbucket.org/gl_tsukasa/hello-legacy-app.git -
Since it's a public repository, no authentication is required (leave it blank and proceed).
-
Check the Project name (it defaults to
hello-legacy-app; there's no need to change it). -
From the dropdown to the left of Project URL, select the group you created in 3-1.
-
Set Visibility Level as you like (
Privaterecommended). -
Select the Create project button at the bottom of the screen.
-
The import process begins. The import progress is shown at the top of the screen — wait until it says Finished.
-
Once complete, open the project's top page and confirm that
README.md,package.json,src/discount.js, andtest/discount.test.jsare all displayed. You can also confirm that the commit history from Bitbucket has been carried over, fromRepository > Commits.
At this point, the "migrate the existing system to GitLab" experience is complete. From here on, the remaining steps (adding labels, CI setup, etc.) are all performed against this project.
Branch protection (6-1) is set up later, in Chapter 6. Right now, before protection, you can push directly to
main— this is intentional (direct pushes are needed during the migration/initial setup of an existing system). Once protection is enabled, direct pushes become impossible, and everything must go through an MR.
If the repository is on a network GitLab can't reach, such as an internal Git server:
Repository by URLassumes GitLab's server can reach the source URL over HTTPS/SSH. If your network configuration doesn't allow that, clone locally first, then switch to the method ofgit remote add+git pushinto an empty GitLab project.
Above, we chose the Repository by URL button, but if you have a GitHub or Bitbucket account, we recommend using the respective dedicated button and authenticating, instead. Doing so also lets you import related information such as issues. For details, see the manuals: Migrate from Bitbucket Server, Migrate from Bitbucket Cloud, Migrate from GitHub
4. Create Labels and an Issue Board
4-1. Create Scoped Labels for the change type
Using labels in key::value format (Scoped Labels) means labels within the same scope can't be applied at the same time (e.g., applying type::bug automatically removes type::feature). Here, we won't use these for progress tracking — only to classify the type of change (bug fix / spec change / new feature, etc.). There's no need for a strict operating rule; it's fine to just apply whichever one fits at creation time.
- In the
Search or go tobox at the top of the screen, open the group you created in 3-1. - Open the group's
Manage > Labels, and selectNew label. - Enter
type::bugforTitle, pick a color, and selectCreate label. - Follow the same steps to create
type::featureandtype::changeas well.
Point:
- There's a reason we're creating these labels at the group level rather than the project level.
- In Chapter 7, when you try to add a label to an issue in the project, the
type::*labels you created here will already show up as options, without any configuration on the project side. That's because group labels automatically "flow down" to every project underneath.- You'll actually see this in action in step 2 of Chapter 7.
4-2. Create an assignee list on the Issue board
For progress tracking, we use an assignee list (a list that shows issues assigned to a particular user). Dragging a card onto this list is all it takes to assign that issue to yourself.
- Open
Plan > Issue boardsin the project. - Select
New list. - Choose
Assigneeas the list type, pick yourself from the dropdown, and selectAdd to board. - A new list is added between
OpenandClosed(both end lists,OpenandClosed, are always fixed).
Leave the default Open list as-is. The flow is: issues land in Open when created, and you drag them onto your own assignee list at the moment you declare you're starting work on them.
5. Add the Initial Necessary Files
Before protecting the project, let's add the necessary files first (adding files after protection would mean going through an MR every time, which adds friction).
How to add a file: On the project's top page, near the file listing, select the "+" icon → New file, enter the path and file content, then use Commit changes at the bottom to commit directly to main.
5-1. Issue template
Create .gitlab/issue_templates/Default.md.
## Change description
<!-- Describe what you want to change and why it's needed -->
## Completion criteria
- [ ]
- [ ]
## Related information
<!-- Reference links, screenshots, etc. -->
This automatically applies the template every time you create a New issue from now on. We haven't added a /label quick action here. The type::bug / type::feature / type::change labels created in 4-1 are just for classifying the change type, so it's fine to manually add one after creating the issue if it applies (it's not required).
5-2. MR description template
Create .gitlab/merge_request_templates/Default.md.
## Related issue
Closes #
## What changed
## How to verify
Writing Closes #123 has the side effect of automatically closing the referenced issue when the MR is merged.
5-3. .gitlab-ci.yml (for Secret Detection)
Add Pipeline secret detection, which catches API keys, tokens, and the like that accidentally end up in commits.
stages:
- test
- release
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 also reliably runs as part of the MR pipeline you'll create in Chapter 7. With just this, a secret_detection job gets added to every subsequent pipeline, and every push or MR creation is automatically scanned.
includeis GitLab's mechanism for pulling in an officially maintained template (a full set of job definitions) into your own.gitlab-ci.yml. The benefit is that you can reuse jobs without writing the contents (image, script, etc.) yourself — at actual run time, this one line expands into the fullsecret_detectionjob definition (image, script, and so on), and the pipeline runs with that.
On the Ultimate plan, findings aren't just downloadable as a pipeline artifact (
gl-secret-detection-report.json) — they're also shown in the MR'sReportstab and inSecure > Vulnerability report, giving you a single place to manage them (you'll check this in Chapter 7).The first time you enable this, the existing commit history is out of scope for this scan (only the current tree state is checked). If you want to scan past commits too, go to
Build > Pipelines > New pipelineand run it once with the variableSECRET_DETECTION_HISTORIC_SCANset totrue.
Once you commit this file, the first pipeline runs on the main branch. Open Build > Jobs and check the secret_detection job's log — it should say no leaks found (in Chapter 7, you'll actually get a leak detected, on purpose).
5-4. .gitlab-ci.yml (for test execution / JUnit report)
Add the following test job to the file from 5-3. It outputs test results as JUnit, and visualizes them automatically on the MR.
unit-test:
stage: test
image: node:22
rules:
- if: '$CI_COMMIT_TAG'
when: never
- when: on_success
script:
- node --test
--test-reporter=spec --test-reporter-destination=stdout
--test-reporter=junit --test-reporter-destination=report.xml
artifacts:
when: always
reports:
junit: report.xml
-
--test-reporter=spec --test-reporter-destination=stdoutoutputs a normal, human-readable log to the job screen, as usual. - At the same time,
--test-reporter=junit --test-reporter-destination=report.xmloutputs a JUnit-format XML report toreport.xml. - Pointing
artifacts.reports.junitat that path lets GitLab parse this XML and show the results in theTests(test summary) widget on the MR. -
artifacts.when: alwaysis specified so that even if the job fails because a test failed, the report file still gets uploaded, letting you check the failure details on the MR. -
We're deliberately not passing a path argument to
node --test. If you explicitly pass a directory name liketest/, Node.js tries to interpret it as a glob (glob) pattern, fails to match, and misinterprets it as a path to a single file — which can lead to aCannot find moduleerror. With no argument, Node.js's standard discovery patterns (such as**/test/**/*.js) automatically findtest/discount.test.js. -
We've explicitly specified
rules. If you write nothing forrules/only/except, GitLab CI implicitly appliesonly: [branches, tags], so this job would also run when you push a tag. By that point, this commit has already been merged intomainafter passing 6-4's approval rule and this job's own JUnit check, so there's no need to re-run the test on tag push. What's more, since thereleasestage runs after theteststage, if the test at tag-push time somehow fails, thecreate-releasejob won't run, and the Release creation gets blocked. To avoid that, when$CI_COMMIT_TAGis set (i.e., it's a tag push), we skip this job entirely withwhen: never, so it only runs on regular branch pushes and MRs.
The JUnit report's target file must have a
.xmlextension. Specifying anything else will cause an error on GitLab's side.
5-5. .gitlab-ci.yml (for automatic Release creation)
Add a job that, when a tag is pushed, automatically creates a GitLab Release with release notes generated by the Changelog API. Add the following to the file from 5-3/5-4.
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'
This job calls GitLab's Changelog API with a GET request (a read-only call that doesn't modify the repository), fetches the changes since the previous tag, grouped by category, as plain text, and uses that directly as the Release description. Only commits in the format described in 8-1 are eligible.
Why explicitly specify
fromevery time:
- If you omit
from, the Changelog API automatically looks for "the most recent version tag."- However, if
fromis omitted for the very first release (when no previous tag exists), the API returns an error.- That error response has no
notesfield, sojq -r '.notes'outputs the literal string"null", which then becomes the Release description as-is.- To avoid that, when there's no previous tag, we explicitly pass the repository's very first commit (
git rev-list --max-parents=0 HEAD) asfrom.
Putting together the pipeline you assembled across 5-3 through 5-5, it looks like this:
unit-test (5-4) explicitly sets when: never for the case where $CI_COMMIT_TAG is set (a tag push), so it is skipped on tag push. The create-release job in the release stage is restricted via rules to only run on tag push, so it doesn't run on regular pushes.
Whether secret_detection (5-3, from GitLab's official template) also runs on tag push depends on the template's own rules, and this document can't state that definitively. Please check for yourself, when you push a tag in step 16, whether this job runs in Build > Pipelines. If it's running unnecessarily and you'd rather it didn't, you can also override the job from the template and add your own rules.
6. Screen Configuration
6-1. Protect the main branch
-
Open the target project
-
Select
Settings > Repositoryfrom the left sidebar -
Expand the
Branch rulessection -
Select the
mainbranch -
Configure as follows
Item Value Allowed to merge Developers + Maintainers Allowed to push and merge No one -
Click
Save changes
This means that even Developer/Maintainer-role users can no longer push directly to main, and must always create an MR and merge it.
Point: Unless you explicitly set "Allowed to push and merge" to "No one," direct pushes themselves aren't restricted. Be careful not to leave this at the default.
6-2. Set an MR title template
- Open
Settings > Merge requests - Enter the following in the
Merge request title templatefield
Resolve #%{issue_id} "%{issue_title}"
Save changes
When you select "Create merge request" from an issue's detail page, the branch name and MR title are generated automatically. The title fills in a format like Resolve #123 "Implement additional discount for Gold members".
If
%{issue_id}ends up empty:
- This variable is only populated when the branch name is in a format that includes the issue number (e.g.,
123-gold-discount).- If you created the branch manually from scratch, it may end up blank.
- Always create the branch via the "Create merge request" button on the issue's detail page (you'll actually do this in step 5 of Chapter 7). Going through this button takes you straight to the MR creation screen carrying over the branch name, the moment after the branch is created via the popup.
This is nothing more than a "default value" — it carries no enforcement on its own. In the next section, 6-3, we set up a mechanism that actually blocks merges.
6-3. Block merges when an MR has no issue reference
This is the heart of today's workflow. We use the native "MR title validation" feature, which checks the MR's title against a regular expression, and physically prevents the merge button from being pressable if the pattern doesn't match.
-
Open
Settings > Merge requests -
Turning on the
Title must match a specified patterncheckbox reveals theTitle patternandTitle examplefields -
Enter this regular expression in
Title pattern^(Draft: )?.*#\d+.* -
Enter an example of the expected title in
Title example(e.g.,Resolve #123 "Implement additional discount for Gold members") -
Save changes
Now, an MR whose title doesn't include an issue number will show a validation failure in the merge checks area, and the merge button will be blocked. Combine this with 6-2's "MR title template," and titles satisfying the pattern get filled in automatically without users doing anything — making the operational overhead essentially zero.
The regular expression uses RE2 syntax. This differs in some respects from PCRE (the syntax used by many online regex testers), so lookaheads and backreferences aren't available.
This title validation only targets the MR's title, not the description. If your practice is to write "Closes #123" only in the description, that will conflict with the title validation — so always pair this with the 6-2 title template.
6-4. Set up the Merge request approval rule (mandatory approver)
Title validation enforces "a reference to the issue"; this enforces "review by a human." It's the core feature for keeping change work from becoming dependent on any one person.
-
Open
Settings > Merge requests -
In the
Merge request approvalssection, underApproval rules, selectAdd approval rule -
Configure the following in the right sidebar
Item Value Rule name e.g., Required reviewApprovals required 1(setting it to 0 makes the rule optional — always specify 1 or more)Add approvers The user(s) or group you want as reviewers (preferably someone other than yourself) -
Select
Save changes
Now the MR's merge checks will show as unmet, and the merge button will be blocked, until the required number of approvals (Approve) has been given by the designated approvers. You'll confirm this behavior in step 11 of Chapter 7.
If you're running this hands-on solo, with just your own account: GitLab has "Prevent approval by author" turned on by default (which prevents an MR's own author from approving it), and this is normally the reason self-approval isn't possible. If you want to complete this hands-on solo for verification purposes, you can temporarily lift this restriction as follows.
- Open
Settings > Merge requests- Scroll to
Approval settingsin theMerge request approvalssection- Uncheck
Prevent approval by author- Select
Save changesNow the MR's own author can press the
Approvebutton to approve their own MR, letting you complete step 11 (getting reviewer approval) solo, all the way through. This is purely a hands-on-only compromise. In real team operation, leave this setting on (checked) to preserve the effectiveness of review, and instead specify someone other than yourself as an approver in 6-4'sAdd approvers.Note also that if you don't specify anyone at all in
Add approvers, GitLab may treat a rule where "the only eligible approver is the MR's own author" as automatically approved (Auto approved). This is a mechanism to prevent an MR from becoming permanently unmergeable because no one can approve it — but since this hands-on wants you to actually experience pressing theApprovebutton, we recommend the setting change above (uncheckingPrevent approval by author) instead.
6-5. Protect tags (recommended)
- Open the target project
- Select
Settings > Repositoryfrom the left sidebar - Expand the
Protected tagssection - Select
Add new - Choose
Tag, and enter*(a wildcard matching all tags) in the input field - Select
Create wildcard - Select
MaintainersforAllowed to create - Select
Protect
Now tag creation is restricted to Maintainer and above, preventing unintended tags from being created by just anyone.
The three checks configured in 6-3, 6-4, and 5-4 must all pass together before a merge is possible, as shown below:
Note that Secret Detection (5-3) findings are not included in this merge check. Detection is purely a warning — the final decision on whether to merge is left to a human (you'll confirm this again in step 12 of Chapter 7).
7. Try It Yourself: Issue → Design → Development → Review → Test → Release
With the setup above complete, let's actually try the whole flow. We'll use, as our example, the change request "implement an additional discount for Gold members, on top of the existing discount logic."
Step 1: Create an Epic (the existing system's maintenance plan)
- In the
Search or go tobox at the top of the screen, select the group you created in 3-1. - Select
Plan > Work itemsfrom the left sidebar. - Select
New item. - Select
Epicfrom theTypedropdown. - Enter a name representing the overall maintenance activity in
Title, such asExisting system: periodic maintenance plan. - Add a sentence to
Description, such as "Continuously accept change requests against the existing system, and manage each one as an Issue under this Epic." - Open
Start date, chooseFixed, and enter today's date. - Similarly for
Due date, chooseFixedand enter a date 30 days from today (as one segment of the maintenance cycle). - Select
Create epic. - Once created, open
Plan > Roadmapfrom the left sidebar. - With the default display setting (the
Weekspreset), confirm that the bar for the Epic you created is displayed straddling the red vertical line marking "today." - This screen is a Gantt-chart format that displays periods as horizontal bars, so confirm that you can see the entire maintenance plan at a glance, like a WBS (work breakdown structure).
Step 2: Create an issue (from the Epic)
- From the Roadmap screen you opened in step 1, select the Epic's bar or title to open the Epic's detail page.
- Select
Addin theChild itemssection. - Choose
Add a new issue. - Enter
Implement additional 5% discount for Gold membersforTitle. - From the
Projectdropdown, select the project you created in 3-2. - Select
Create issue. The issue you create automatically becomes a child item of this Epic. - Select the issue that's been added to the
Child itemslist to open its detail page. Confirm that the 5-1 issue template has been automatically applied. - Select
EditfromLabelsin the right sidebar to open the label picker. Confirm thattype::bug/type::feature/type::change, which you created at the group level in 4-1, already appear as options even though you haven't configured anything at the project level (this is the experience of a group's labels "flowing down" to the project). - Select
type::featureand 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 you created in step 2 still shows up in the
Openlist on the board (being a child item of the Epic doesn't affect how the board splits it into lists). - Drag this card from the
Openlist onto the assignee list you created in 4-2. This assigns the issue to yourself, letting your team see that you've started work.
Step 4: Consider the design approach (using GitLab Duo Chat ad hoc)
Before diving into implementation, let's check the existing code's behavior. This is where we use GitLab Duo.
-
Select the Duo Chat icon at the top right of the project screen to open the chat panel on the right (in an IDE, a similar panel is available from VS Code as well).
-
With
src/discount.jsopen, or specifying its path, ask the following.Explain the current behavior of the calculateDiscount function in src/discount.js -
Confirm that Duo returns an explanation along the lines of "a function that returns a 10% discount on orders of 10,000 yen or more."
-
Next, discuss the implementation approach.
I want to add an extra 5% discount when the member rank is gold. Suggest how to change the calculateDiscount function to do this. -
Using Duo's suggestions (add an argument, add a conditional branch, etc.) as a reference, you'll actually change the code in step 5 and onward. Whether or not to adopt the suggestion as-is is always a human decision — the point here is the experience of using it ad hoc as a "sounding board for investigation and design."
Duo Chat really shines for changes that require understanding existing code (which is especially common right after migrating an existing system). There's no need to build it into a permanent workflow — it's enough to call on it whenever the situation calls for it.
Step 5: Create a branch
- On the issue's detail page, select
Create merge request. - A popup appears where you can confirm/edit the name of the feature branch that will be created (e.g.,
2-gold-discount). - Select the
Create merge requestbutton inside the popup. The moment you do this is the first time the feature branch actually gets created (it doesn't exist before this).
Step 6: Create an MR
- After the branch is created, you're taken to an MR creation screen whose contents are still empty, with the MR title already filled in per the 6-2 template. The description contains the 5-2 template.
-
If the project has an MR description template configured (5-2), the issue number is not automatically filled in 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's number after the#(e.g.,Closes #2). - Select
Create merge requestat the bottom of the screen to create the MR.
Step 7: Edit the files (implementation)
-
The branch selector in the top left is already switched to the target feature branch (e.g.,
2-gold-discount) automatically, since you're on the MR you just created. -
Select
Code→Open in Web IDEat the top right of the screen. -
Based on the design discussion in step 4, change
src/discount.jsas follows.function calculateDiscount(amount, memberRank = 'normal') { let discount = 0; if (amount >= 10000) { discount += amount * 0.1; } if (memberRank === 'gold') { discount += amount * 0.05; } return discount; } module.exports = { calculateDiscount }; -
Also add a Gold-member case to
test/discount.test.js.test('Gold members get an additional 5% discount added', () => { assert.strictEqual(calculateDiscount(10000, 'gold'), 1500); }); test('Normal members do not get an additional discount', () => { assert.strictEqual(calculateDiscount(10000, 'normal'), 1000); }); -
Additionally, to actually confirm Secret Detection in action, add the following line at the end of
src/discount.js(remove the spaces around the hyphen so it resembles the look of a personal access token).// Verification token (to be deleted): glpat - 12345678901234567890
Step 8: Commit
-
Select the
Source Controlicon in the left activity bar (or Ctrl+Shift+G / Control+Shift+G). -
In the commit message field, enter a title on the first line, leave a blank line, then enter
Changelog: added(explained in detail in 8-1).Implement additional 5% discount for Gold members Changelog: added -
The branch remains the feature branch created in step 5, so just select
Commitas-is (there's no need to create a new branch). Since the MR already exists, this commit is automatically added to the MR from step 6.
Step 9: Check the test results (JUnit report)
- After the commit, the pipeline runs automatically. Wait for it to finish in
Build > Pipelines. - Open the MR's detail page and check the
Testswidget (orTest summaryinside the pipeline). - Confirm that, as a result of the
unit-testjob added in 5-4, all the test cases, including the 2 you added, are showing as successful (Passed). - As an experiment, try deliberately changing one of the expected values in
test/discount.test.jsto something wrong, commit again, and see how a failed test shows up on the MR (which test names/counts failed). Once you've confirmed this, revert to the correct value.
Step 10: Check the findings (Secret Detection)
- Open the
Reportstab on the MR's detail page. - Select
Security scan, and confirm it shows something like "Secret detection detected 1 new potential vulnerability" (you can also see the full list of details viaView all pipeline findings). - Open the
secret_detectionjob's log fromBuild > Jobs, and confirm it showsleaks found: 1. - Downloading the artifact from
Job artifactsalso lets you check details such as the type of secret detected (GitLab personal access token), its severity (Critical), and the line number within the file.
Step 11: Get reviewer approval
- Since the MR created from the issue starts out as
Draft, first selectMark as readyat the top of the screen to clear theDraftstate. - Checking the merge checks area, you should see something like "Approval is required" due to the approval rule you set up in 6-4, and the merge button can't be pressed at this point.
-
If you have multiple members: open the MR with the approver account you added in 6-4, and select the
Approvebutton to approve it.
If you're verifying solo: as noted in 6-4, having uncheckedPrevent approval by author, select theApprovebutton on that same MR while still signed in as yourself (the author now also acts as the approver). - After approval, confirm the approval status in the merge checks area turns green (satisfied).
Step 12: Check the merge checks
- Confirm the merge checks area shows the title validation result, and that it's green if there's no issue.
- Together with the approval status from step 11, confirm that all three — title validation (issue reference), approval rule (human review), and JUnit tests (quality) — must be satisfied before the merge becomes possible.
- Secret Detection findings don't block the merge itself (detection is purely a warning — the decision to merge is left to a human, by design).
Step 13: Merge
- Select the
Mergebutton. - Confirm that, thanks to the
Closes #pattern in the MR description, the target issue is automatically closed as well. - With the fake secret from step 7 now merged into
main, this "finding" turns into a "vulnerability."
Step 14: Check the vulnerability
- Open
Secure > Vulnerability reportfrom the left sidebar. - Select the vulnerability detected in step 10 (Secret type:
GitLab personal access token). - Check the details (secret type, remediation, detection date/location) in
Description.
Step 15: Change the status (triage)
- From the vulnerability list, choose
Dismissedfrom the status options (Needs triage/Confirmed/Dismissed/Resolved). - Choose
Used in testsfrom theSelect a dismissal reasondropdown. - In
Add a comment (required), write a reason such as "this is a fake token for demonstration purposes." - Confirm that this vulnerability no longer appears on the Vulnerability Report's top screen. Triage is only a record that "no action is needed right now" — the code itself doesn't change.
- If you'd like to clean this up, create one more issue (e.g.,
Remove the fake token in discount.js), and use the same flow as steps 2-13 (Create merge request → edit → commit → merge) to delete that line. If a real leak occurred, in addition to triage or deletion, you'd need to revoke the token immediately. Since the pre-deletion commit remains in Git's history, deletion alone doesn't fully eliminate the leak risk.
Step 16: Create a tag
- Open
Code(orRepository) →Tagsfrom the left sidebar. - Select
New tag. - Enter a version number such as
v0.1.0forTag name, and leaveCreate fromasmain. -
Leave
Release notesblank (since 5-5's CI job auto-generates it — anything written here would create a duplicate Release separate from that). - Select
Create tag.
Step 17: Check the Release
- The tag push triggers a pipeline.
- Confirm the
create-releasejob succeeded inBuild > Jobs. - Open
Deploy > Releasesfrom the left sidebar. - Confirm that the content of the commit you attached a trailer to in step 8 is automatically reflected as release notes.
That completes a full run-through of the design (using Duo) → development → review (approval rule) → testing (JUnit) → release flow for a single change request against the existing system.
8. Deciding on Version Numbers (for reference)
To avoid agonizing over this every time you create a tag, let's set some guidelines.
- 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 added
- major (e.g., v1.3.0 → v2.0.0): backward-incompatible changes
8-1. Adding a trailer to the commit message
To auto-generate release notes, add a dedicated Git trailer line at the end of your commit message.
Implement additional 5% discount for Gold members
Changelog: added
Why the string
Changelog: added?
- This isn't a GitLab-specific notation — it's built on top of "trailers," a standard mechanism that's been used in the Git world for a long time.
- It's the practice of adding one
Key: Valueline at the end of a commit message, in the same family asSigned-off-by: Name <email>orCo-authored-by: Name <email>(Git even has a dedicated command for working with trailers,git interpret-trailers).- The advantage is that it doesn't require changing how you write the first line (subject) of a commit.
- The Changelog API called by 5-5's job looks for this
Changelogkey name (as its default configuration).- The vocabulary of values —
added/fixed/changed/removed/security— is likewise not a GitLab invention; it's essentially borrowed from the categories used by the widely-used OSS convention, Keep a Changelog (Added/Changed/Removed/Fixed/Security, etc.).
The main categories you can use are added / fixed / changed / removed / security. Only commits carrying this trailer are eligible for the Changelog API called by 5-5's create-release job (commits without the trailer are ignored).
If you want to customize the output format, you can create a default configuration file, .gitlab/changelog_config.yml, to define a date format and template.
If you publish Docker images or build artifacts to a package registry, as long as the package version matches the release tag, GitLab automatically includes that package in the release evidence. No additional configuration is needed.
8-2. When to Add the Trailer (operational guidance)
If you reflexively add Changelog: added to every single commit, even trivial changes end up in the release notes, which ends up making them harder to read. On the other hand, at the moment of committing, it's often hard to judge whether something is really significant enough to belong in the changelog (e.g., what you thought was just a README update turns out to actually affect users).
Basic policy: rather than deciding whether to add the trailer at commit time, it's better to decide right before merging the MR, once you can see the whole change.
-
Use
Modify commit messagesat merge time as your default approachThe MR's merge screen has a
Modify commit messageslink, which lets you edit, on the spot, the commit message that will actually be merged (if squashing is enabled, the message of the single commit it gets combined into). This lets you decide, the moment you merge, "this MR turned out bigger than expected, so let's keep a changelog entry for it" — no need to anticipate that at commit time.Display conditions: using this link requires at least the Maintainer role on the project (in this hands-on, since you're proceeding as an Owner/Maintainer of the group/project, this is usually not an issue). Also, if the project's merge method is set to "Fast-forward merge" only, this link may not appear, depending on your GitLab version. Since this hands-on doesn't change the merge method in 6-1 (it stays on the default "Merge commit" method), this restriction doesn't apply here.
Caution: For an MR with
Squash commitsenabled, by default the individual commits'Changelog:trailers are not automatically carried over into the single post-squash commit (they're only carried over if the project has configured its squash-commit-message template with the%{first_multiline_commit}variable). For projects using squashing, manually adding the trailer viaModify commit messagesat merge time effectively becomes your final checkpoint. -
If you're still on your feature branch, before merging (can be done entirely in the Web IDE)
If you realize you forgot to add it to your most recent commit, do the following in the Web IDE's
Source Controlpanel.- Select the
...(more actions) button to the right of the commit message field. - Select
Amend commit and Force push. - Enter the message with the trailer added, in the message field, and confirm.
This rewrites the message of the most recent commit and force-pushes it in one go. Since the feature branch isn't protected (per 6-1), force-pushing is fine. No terminal or Git commands needed.
This feature was added to the Web IDE relatively recently. Depending on your GitLab version, it may not appear in the
...menu. If so, rungit commit --amend→git push --force-with-leasein your local environment instead.If you want to edit a commit further back: as of this writing, the Web IDE only supports amending the single most recent commit — it doesn't support interactive rebase for editing commits further back. In that case, in your local environment, go back to the target commit with something like
git rebase -i HEAD~3, changepicktorewordto edit its message, then push withgit push --force-with-lease(this requires the CLI). - Select the
-
If you notice after merging
Since
mainis protected (per 6-1), you can't rewrite an already-merged commit. In this case, the practical approach is to edit the content ofrelease_notes.md— auto-generated by 5-5's job at the next Release creation — by hand from theDeploy > Releasesscreen, and add the entry there. Treat what the CI job generates as merely a first draft.
9. Optional: Automating the "Manufacturing Work Itself" with Duo Agent Platform
In Chapter 7 above, we used Duo Chat as an ad hoc "sounding board for design" (step 4). GitLab Duo Agent Platform also has a "Flow" feature that autonomously handles everything from design through implementation to writing unit tests, starting from just the content of an Issue. If you want to try applying AI not just to process management, review, and release governance, but to the "manufacturing work itself," try this optional chapter.
Prerequisites: This feature (Issue to Merge Request Flow) is included as an official feature as of the January 2026 GA (general availability) of the GitLab Duo Agent Platform as a whole. In addition to the Premium/Ultimate plan, you need one of the GitLab Duo Core/Pro/Enterprise add-ons. It also runs as a CI/CD pipeline, so you need a GitLab Runner available in the project.
9-1. Using the Issue to Merge Request Flow
The issue you created in step 2 of Chapter 7 (Implement additional 5% discount for Gold members) has already become Closed by the merge in step 13. So, for this optional chapter, we'll create one more new change request and use it to try the Flow.
-
Create a new issue, the same way as in Chapter 4 / Chapter 7 steps 1-2 (it's fine to create it as a child item under the Epic). Getting ahead of the "Tips for good results" below, keep the scope narrow, and write out a specific file path and completion criteria.
Title: Implement additional 8% discount for Platinum members ## Change description Add an additional discount for Platinum members to the calculateDiscount function in src/discount.js. Use the same style as the existing gold/silver member discount logic (the if (memberRank === 'gold') { ... } pattern). ## Completion criteria - [ ] When memberRank is 'platinum', add an additional 8% discount, regardless of amount - [ ] Add a Platinum-member test case to test/discount.test.js - [ ] Include the target issue number in the MR title (e.g., #4)Point: We've listed "implementation," "adding a test case," and "including the issue number in the MR title" as separate, explicit items in the completion criteria. Explicitly asking for the test to be created, and for the title's format, in the Issue is what gets the Flow to reflect them. Leaving it vague, along the lines of "just handle it well," tends to result in gaps — the test not getting created, or the MR title not satisfying 6-3's validation pattern.
-
Open the issue's detail page.
-
Select Generate MR with Duo below the issue header.
-
Open
Automate > Sessionsto check the execution status (session). Behind the scenes, the Flow runs as a CI/CD pipeline, automatically doing the following:- Analyze the issue's description and requirements
- Open a draft MR linked to the issue
- Draw up an implementation plan (development plan)
- Produce a code implementation (code structure/implementation content)
- Update the MR with the implementation
-
Once the pipeline completes, a link to the generated MR appears in the issue's activity log.
-
Open the generated MR, and review both the implementation and whether the test case you wrote into the completion criteria was actually added. If it wasn't added, or is insufficient, either rewrite the issue's completion criteria to be more specific and re-run, or add the test manually. Rather than merging what the AI generated as-is, always have a human review it before proceeding to step 9 (checking test results) and beyond.
-
Check the MR title. Having written "include the target issue number in the MR title" into the completion criteria means the title gets generated with the issue number included — for example,
feat: Implement additional 8% discount for Platinum members (#4). This form satisfies 6-3's title validation (^(Draft: )?.*#\d+.*), so the merge check passes without further work. Just to be safe, confirm the generated title actually includes#<number>, and add it manually if it doesn't.
In practice: running the Flow without writing anything in the completion criteria can leave the MR title without an issue number, which trips up 6-3's title validation. On the other hand, simply stating "include the issue number in the MR title" in the completion criteria is enough to get the Flow to generate the title accordingly. Just like with creating the test case, this is a good example of the effect of "not leaving things vague and instead spelling out, on the Issue side, exactly what deliverable you expect."
Tips for good results (GitLab's official best practices):
- Keep the issue's scope narrow (break large tasks down into small, specific units)
- Specify concrete, target file paths
- Write concrete acceptance criteria
- Include examples of existing code patterns to maintain consistency
- Also spell out, in the completion criteria, what you expect for non-code deliverables such as the MR title format
9-2. Relationship to This Hands-On's Steps
- The MR approval rule set up in 6-4 applies equally to MRs generated by the Flow. Even for changes made by AI, the governance that requires human review and approval before merging remains fully in place.
- The title validation from 6-3 also works the same way, mechanically, on MRs generated by the Flow. As confirmed in steps 1 and 7 of 9-1, stating in the completion criteria that "the MR title must include the issue number" gets the Flow to generate the title accordingly and satisfy the validation. If you don't specify anything, the title may end up without the issue number and fail validation — so the outcome depends on how you write the issue.
- The JUnit report from 5-4 and Secret Detection from 5-3 also run as-is on the pipeline for a Flow-generated MR, mechanically checking the quality and safety of AI-generated code.
- In short, the point to confirm in this optional chapter is that "even when you hand the manufacturing work (design, implementation, test creation) over to AI, the guardrails built in Chapters 5 and 6 for safely accepting it keep working as-is." That said, the guardrails "working" and the AI's output "satisfying those guardrails from the start" are two separate things — as with the title, there are still parts that need a human's touch to fix up.
10. Optional: Reporting with Duo Agentic Chat
Project progress management and visualizing development productivity don't have to mean building out a dedicated dashboard every time — you can just ask Duo Agentic Chat and get a report on the spot. Try experiencing a division of labor where GitLab's standard fixed-layout dashboards, like the Value Streams Dashboard or Project/Group Insights, serve a supplementary role, while ad hoc questions go to Agentic Chat.
10-1. Generating reports with Agentic Chat
Open Agentic Chat from the Duo Chat icon at the top right of a project or group screen. Agentic Chat routes your question internally to the appropriate specialized agent (Planner Agent, Data Analyst Agent, Security Analyst Agent, and so on) depending on the content. Try the following kinds of questions.
-
Progress check (Planner Agent):
List the Issues closed this week in this project, and the change requests that are still Open -
Review status summary (Planner Agent):
Summarize the list of MRs currently awaiting review, and their approval status -
Visualizing productivity (both the Planner Agent and Data Analyst Agent can handle this):
For MRs merged in the last month, tell me the average lead time from MR creation to merge -
Understanding quality status (Security Analyst Agent):
In the Vulnerability Report, aggregate the vulnerabilities currently needing triage, by severity
In practice (differences in how each agent behaves):
- The Planner Agent, for questions about listing/checking the status of Issues, fetches GitLab data directly using filters like
state='closed'/updated_after=..., and answers in table form. It also throws in observations from a PM's point of view, such as "the two Open items are missing an assignee and labels."- On the other hand, asking the Planner Agent about MR lead time or review status gets you an answer prefaced with "there's no tool to fetch the MR list directly, so I'll query MR data via Orbit (Knowledge Graph)." This route comes with a caveat every time about there being replication lag, along the lines of "for accurate auditing purposes, we recommend checking the MR list screen directly."
- Asking the Data Analyst Agent the same lead-time question gets a more concise answer, without the replication-lag caveat, since it uses GLQL (GitLab Query Language) directly instead of going through Orbit. The answer also comes with an explanation of the actual GLQL query used, and you can copy the query itself, or keep it as an embedded view, from the
⋮menu. This gives you a rule of thumb for which to use: for questions aimed at numerical analysis/aggregation, the Data Analyst Agent tends to give a more accurate, more reusable answer than the Planner Agent.- The Security Analyst Agent references the Vulnerability Report directly, and in addition to the severity breakdown, it also correctly recognized the secret-detection finding you triaged (dismissed) in step 15 of Chapter 7, returning something along the lines of "already dismissed by human judgment, so it's out of scope for re-triage."
- A bonus observation: when asked about review status, the Planner Agent pointed out that "neither MR has an assigned reviewer, and both are approved only by their own author. If you value a third-party review process, it may be worth putting reviewer-assignment rules in place." This is essentially calling out the very "uncheck Prevent approval by author and self-approve" setup we used in 6-4. Even knowing it's a hands-on-only compromise, it's notable that the AI picks up on the same concern on its own.
Since Agentic Chat answers by referencing your project's real-time data — issues, MRs, pipelines, security findings, and so on — you can keep asking different angles of question, which is the big difference from a fixed-format dashboard. That said, as noted above, the freshness and accuracy assumptions differ depending on which agent (or which internal route) is actually answering the same question, so for important decisions, always check the answer for a caveat about "replication lag" or similar.
10-2. Dividing Labor with the Standard Management Screens
In addition to ad hoc reporting via Agentic Chat, the following standard GitLab screens remain useful for ongoing, fixed-point observation.
| Screen | Main use |
|---|---|
Analyze > Value stream analytics |
Track the trend, over time, of how long each stage takes, from filing an issue to merge |
Analyze > Insights (requires configuration) |
Turn custom aggregations, like Issue/MR counts by label or assignee, into a dashboard |
Plan > Roadmap |
Get a bird's-eye view of Epic-level plans and progress, as a Gantt chart (already used in step 1 of Chapter 7) |
Secure > Vulnerability report |
List security findings and their triage status (already used in step 14 of Chapter 7) |
A rule of thumb for dividing labor: settle "the fixed graph you always look at in your weekly meeting" into a dashboard, and ask Agentic Chat for "the question you want answered right now, that changes each time" — that division tends to work well.
11. Optional: Deliberately Triggering a Finding with Duo Code Review (MR Review)
There's a feature where designating GitLab Duo as an MR's reviewer gets Duo to analyze the code changes and automatically post review comments (Code Review Flow). Here, to actually experience it, we'll set up a kind of "deliberate failure for demonstration purposes": deliberately implementing a new change request incorrectly, and letting Duo's review catch it.
Prerequisites: This feature (Code Review Flow) is included as an official feature as of the January 2026 GA (general availability) of the GitLab Duo Agent Platform as a whole. In addition to the Premium/Ultimate plan, you need one of the GitLab Duo Core/Pro/Enterprise add-ons. You also need the group setting
Turn on Allow foundational flows and Code Reviewenabled, and since Code Review Flow runs as a CI/CD job, you need either a Runner taggedgitlab--duo, or GitLab-hosted runners enabled for the project.
11-1. Preparing custom review instructions (mr-review-instructions.yaml)
Duo's review can be extended, beyond its standard review perspectives (security, maintainability, and so on), with project-specific perspectives via a custom instructions file.
Depending on the version and configuration of GitLab Duo, there's some chance that instructions written in one language aren't interpreted the way you intended. Just in case, we'll prepare both an English version and a Japanese version with the same content, so that if the English version doesn't produce the intended behavior (review comments coming back in English, and so on), we can switch to the Japanese version to narrow down the cause.
Since main is already protected as of 6-1, these files can't be committed directly via "commit directly to main before protection" either — we'll add them via the same Issue → branch → MR → merge flow as Chapter 7.
-
Open
Plan > Issues, and selectNew issue. Fill inAdd a custom instructions file for MR reviewforTitle, fill inDescriptionper the 5-1 template as below, and selectCreate issue.## Change description For Duo Code Review, add .gitlab/duo/mr-review-instructions.yaml (English version, active) and a backup Japanese-version file. ## Completion criteria - [ ] Add the English-version content to .gitlab/duo/mr-review-instructions.yaml - [ ] Add a backup Japanese version to .gitlab/duo/mr-review-instructions.ja.yaml.bak -
On the issue's detail page, select
Create merge requestto create the branch and MR (the same operation as steps 5-6 of Chapter 7). -
Open the created branch via
Code→Open in Web IDE. Since the.gitlab/duodirectory doesn't exist yet, it gets created automatically as you create a new file there. First, as the active file that Duo actually reads, create.gitlab/duo/mr-review-instructions.yamlwith the following English-version content.instructions: - name: Review language policy fileFilters: - "**/*" instructions: | 1. Write all review comments, summaries, and explanations of suggested code changes in English. 2. Even if code identifiers, existing comments, or documentation are in another language, your review text must be in English. 3. You may include technical terms as-is (e.g., function names, class names, framework names), but always explain their meaning or impact in English. 4. If there are any fixed or automatically generated phrases that must remain in their original language (for example, system headers), leave them as-is, but keep the main body of your feedback in English. 5. When you quote code or error messages, keep the code in its original form, but describe problems and recommendations in English. 6. Start each comment with a one-sentence summary of the issue (what is wrong and why it matters), before providing detailed explanation. 7. Clearly label the severity of each comment using one of: "Bug", "Suggestion", "Question", "Needs confirmation". 8. When suggesting a fix, always provide a concrete code example or a specific action to take, not just a general direction. 9. Keep comments concise — aim for 5 sentences or fewer per comment unless the issue is complex. - name: Boundary Value Review Standard fileFilters: - "**/*" instructions: | 1. For any code containing a numeric threshold check (>=, >, <=, <), verify that the comparison operator matches the acceptance criteria described in the linked Issue. Flag any mismatch. 2. For any change that adds a new conditional branch or logic path, verify that a corresponding test case has been added. -
Next, in the same branch, create a backup Japanese version under a filename Duo won't read directly, such as
.gitlab/duo/mr-review-instructions.ja.yaml.bak(using a.bakextension, or placing it outside.gitlab/duo, so that both files don't accidentally become active at once). The content is the Japanese version below.instructions: - name: レビュー言語ポリシー fileFilters: - "**/*" instructions: | 1. レビューコメント、要約、提案するコード変更の説明はすべて日本語で書くこと。 2. コードの識別子・既存のコメント・ドキュメントが英語であっても、レビュー本文は日本語で書くこと。 3. 技術用語(関数名・クラス名・フレームワーク名など)は英語のまま使ってよいが、その意味や影響は必ず日本語で説明すること。 4. システムヘッダーなど、英語のまま残す必要がある固定文言・自動生成文言がある場合はそのままにしてよいが、フィードバック本文は日本語で書くこと。 5. コードやエラーメッセージを引用する場合、コード自体は原文のまま保持し、問題点や推奨事項の説明は日本語で書くこと。 6. 各コメントは、詳細な説明の前に「何が問題で、なぜ重要か」を1文で要約することから始めること。 7. 各コメントの重要度は「バグ」「提案」「質問」「要確認」のいずれかで明示すること。 8. 修正を提案する場合は、一般的な方向性だけでなく、必ず具体的なコード例や取るべき対応を示すこと。 9. コメントは簡潔にすること(問題が複雑な場合を除き、1コメントにつき5文以内を目安とする)。 - name: 境界値レビュー標準 fileFilters: - "**/*" instructions: | 1. 数値の閾値判定(以上/より大きい/以下/未満など)を含むコードでは、比較演算子(>= / > / <= / <)が 対応するIssueの受け入れ条件と一致しているかを必ず確認すること。一致していない場合は指摘すること。 2. 新しい条件分岐やロジックを追加した変更では、対応するテストケースが追加されているかを確認すること。Point:
mr-review-instructions.yamlis where you write generic perspectives that apply to the whole project. Individual specifications and concrete numbers — like "Silver members: orders of 5,000 yen or more" — don't belong in this file; they go into the completion criteria of the Issue you'll create in 11-2 (or a comment in the relevant code). Keep the rule itself as a generic principle — "the comparison operator in a threshold check must match the linked Issue's acceptance criteria" — and let Duo apply it by cross-referencing the specific MR's context (the Issue's content). -
Add a trailer to the commit message, with the title on the first line, a blank line, then
Changelog: added. -
Merge once the normal merge checks pass (6-3's title validation, 6-4's approval, 5-4's JUnit tests). Since this is just adding config files, this chapter's theme — "review by Duo" — isn't required, but if you're curious, feel free to designate
GitLabDuoas aRevieweron this MR too, just like a normal MR. -
After merging, be sure to confirm that both files show up under
.gitlab/duo/onmain. Skipping this confirmation can lead to a confusing result later, in 11-2, where the "Boundary Value Review Standard" rule doesn't take effect and only the "Review language policy" (English output) does. From this point on, this custom instruction actually applies to the MR you'll create in 11-2.
Custom instructions add to Duo's standard review criteria — they don't replace them. They're also nothing more than "guidance for an AI reviewer," with no guarantee they'll be reliably applied in every case, so don't rely on this file alone for security controls or compliance requirements.
11-2. Deliberately Making an Incorrect Change, and Having Duo Review It
Create one new change request, and deliberately get a boundary condition wrong. The specific numeric spec ("5,000 yen or more") is deliberately kept out of the yaml in 11-1, and instead spelled out in the Issue's acceptance criteria and a comment in the code.
-
Create a new Issue.
-
In the
Search or go tobox at the top of the screen, open the project you imported in 3-2 (skip this if it's already open). -
Select
Plan > Issuesfrom the left sidebar. -
Select
New issue. -
Enter
Implement additional 3% discount for Silver membersforTitle. -
The 5-1 issue template has already been inserted into the
Descriptionfield, so fill it in as follows.## Change description Add an additional discount for Silver members to the calculateDiscount function in src/discount.js. ## Completion criteria - [ ] When memberRank is 'silver', add an additional 3% discount for orders of 5,000 yen or more - [ ] Add a Silver-member test case to test/discount.test.js ## Related information -
Select
type::featurefromLabelsin the right sidebar (the group labels created in 4-1 will show up as options here — not required). -
Select
Create issueat the bottom of the screen.
The point of writing the specific number "5,000 yen or more" as an acceptance criterion is that we're deliberately keeping it only in these completion criteria, not in the
mr-review-instructions.yamlfrom 11-1 (the same division of labor between generic rules and individual specifications explained in 11-1's "Point" section). -
-
Proceed the same way as steps 4-8 (design → create branch → create MR → implement → commit). During implementation, deliberately change
src/discount.jsas follows. Leave a comment so the acceptance criteria can also be referenced from the code.// Acceptance criteria (see Issue): applies to orders of 5,000 yen or more if (memberRank === 'silver' && amount > 5000) { discount += amount * 0.03; } -
Also, deliberately don't add a Silver-member test case to
test/discount.test.js, and commit. -
Once the MR is created, select
GitLabDuofromReviewersin the right sidebar. -
Check the review's progress from
Automate > Sessions. -
After a while, Duo's review comments get posted on the MR's timeline. Following 11-1's generic rule (Boundary Value Review Standard), confirm that Duo cross-checks "the change (
> 5000)" against "the Issue's acceptance criteria (5,000 yen or more) and the comment in the code," and produces findings such as:- A one-sentence summary at the top of the comment, with a
"Bug"-style label attached, pointing out the boundary condition (it's>, but the acceptance criteria says "or more") - A finding that no new test case was added for Silver members (this one should also come with a concrete suggested fix)
- The comment body being written entirely in English (confirming that the
Review language policyinstruction is taking effect)
- A one-sentence summary at the top of the comment, with a
-
Upon receiving the findings, actually fix them (change
>to>=, add the test case, and commit). Have Duo review it again, and confirm the findings have been resolved.
This optional chapter has two points to take away. One is getting a feel for the idea that "AI-driven review doesn't replace human review (the approval rule from 6-4) — it functions as a first pass, catching mechanically-detectable issues ahead of time." The other is getting a feel for the division of labor where the custom instructions file holds only generic principles, while individual specifications go into the Issue or code comments. The actual approval/merge decision is still always made by the human approver you configured in 6-4.
12. Appendix: Common Pitfalls
- Duo Chat doesn't show up: The GitLab Duo add-on may not be enabled. Check your contract status with your administrator. Even if it's unavailable, you can skip step 4 and proceed straight to step 5 and beyond without affecting the rest of the verification.
-
You set up an approval rule, but anyone can still merge: Check whether
Approvals requiredis still set to0.0means "optional" and doesn't enforce anything. -
You can't approve your own MR, when you're the only account: This is caused by
Prevent approval by author, underSettings > Merge requests > Merge request approvals > Approval settings, which is on by default. If you want to complete this hands-on solo, refer to the note in 6-4 and uncheck it. In real operation, leave it on, and specify someone other than yourself as an approver. -
The JUnit report doesn't show up on the MR: Check whether the path specified in
artifacts.reports.junitmatches the actual path of the output XML file. Also, specifying anything other than XML will cause an error. - Limitations of RE2 syntax: Since 6-3's regular expression uses RE2 syntax, lookaheads and backreferences aren't available. Be careful when testing it against an online regex tester that assumes PCRE.
- An MR generated by the Issue to MR Flow (Chapter 9) doesn't pass 6-3's title validation: Running the Flow without writing anything in the completion criteria can result in the MR title not including the issue number. As in 9-1's example, explicitly stating "include the target issue number in the MR title" in the completion criteria gets the Flow to generate the title accordingly and satisfy the validation. If you've already generated an MR that's failing validation, edit the title by hand to include the target issue number.
-
The Duo review in Chapter 11 doesn't produce an expected finding (e.g., the missing-test check from the Boundary Value Review Standard):
.gitlab/duo/mr-review-instructions.yamlmay not have been merged intomainas described in 11-1. Code Review Flow only takes effect once this file exists onmain, so if your review results seem to only reflect theReview language policyinstruction (output language), first check, inRepository > Files, whether this file actually exists on themainbranch and whether its content is correct.