Skip to content

Lab 12: Pull request, CI, and Copilot code review

Time
About 40 minutes
Level
advanced
Platforms
Windows 11, macOS
Copilot surfaces
github.com, Copilot code review, VS Code
You'll use
code-review, prompt-file, custom-agent, instructions
Starts from
lab09-complete: Adds AI metadata: Q&A on, default labels, and the fabric/ai-prep drafts.
Ends at
None needed
Last verified
Not yet run end to end on Windows 11

Works on macOS

Nobody has run this lab end to end on Windows 11 yet. The steps follow the current docs for each tool, but menu labels may differ. The verify command at the end is the source of truth, and feedback is welcome.

Model-as-code work should end in a pull request, not in a local folder. CI proves the model parses, report bindings resolve, scripts pass, and the lab site still builds. Copilot code review adds another reviewer that reads the diff and the repo instructions. You also compare it with the local /review-model-pr prompt so you can see what each surface catches.

  • Pull request: the review boundary for semantic model, report, and AI metadata changes. See Branching and pull requests.
  • CI: this repo’s tenant-free GitHub Actions workflow. It runs on pull requests without Fabric, Power BI, or secrets. See CI workflows.
  • Copilot code review: a paid GitHub Copilot feature that you request from the PR sidebar. It leaves comments like a reviewer, usually as a comment review.
  • Local model review: the /review-model-pr prompt with the model-reviewer agent. It can run local validation and focus on Power BI model conventions.
  • Ruleset review: optional repository settings can request Copilot review automatically on pull requests.
  • You have your own GitHub copy of the repo from Lab 01.
  • You are on the feature branch that contains Labs 07 to 09, usually feature/ai-ready-names.
  • gh auth status succeeds, or you know how to open a PR in the GitHub web UI.
  • You have a paid Copilot plan or org access for Copilot code review. Needs a paid Copilot plan

Step 1. Read what CI and review will check

Section titled “Step 1. Read what CI and review will check”

Open the repo review files before you push.

  1. Open Quick Open: Ctrl+P (macOS: Cmd+P), or Go > Go to File.
  2. Open .github/workflows/ci.yml.
  3. Open .github/pull_request_template.md.
  4. Open .github/prompts/review-model-pr.prompt.md and .github/agents/model-reviewer.agent.md.
  5. Open .github/instructions/tmdl.instructions.md and .github/instructions/pbir.instructions.md.

The workflow’s real job names are in the file:

.github/workflows/ci.yml (lines 17-114)
jobs:
validate:
name: Validate (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 20
defaults:
run:
shell: pwsh
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
# The same bootstrap a learner runs. It installs the hashed requirements and runs every
# validation step (data, expected, questions, runs, tmdl, bindings, customizations, hygiene).
- name: Initialize and validate
run: ./scripts/Initialize-DevEnvironment.ps1
- name: pytest
run: |
$python = $IsWindows ? '.venv/Scripts/python.exe' : '.venv/bin/python'
& $python -m pytest tools/python/tests
if ($LASTEXITCODE) { exit $LASTEXITCODE }
- name: ruff
if: runner.os == 'Linux'
run: |
.venv/bin/python -m ruff check tools/python
if ($LASTEXITCODE) { exit $LASTEXITCODE }
.venv/bin/python -m ruff format --check tools/python
if ($LASTEXITCODE) { exit $LASTEXITCODE }
- name: Pester
run: |
$pester = Get-Module -ListAvailable Pester | Sort-Object Version -Descending | Select-Object -First 1
if (-not $pester -or $pester.Version -lt [version]'5.5.0') {
Install-Module Pester -MinimumVersion 5.5.0 -Scope CurrentUser -Force -SkipPublisherCheck
}
Import-Module Pester -MinimumVersion 5.5.0
$config = New-PesterConfiguration
$config.Run.Path = 'scripts/tests'
$config.Run.Exit = $true
$config.Output.Verbosity = 'Detailed'
Invoke-Pester -Configuration $config
- name: PSScriptAnalyzer
run: |
if (-not (Get-Module -ListAvailable PSScriptAnalyzer)) {
Install-Module PSScriptAnalyzer -Scope CurrentUser -Force
}
$findings = Invoke-ScriptAnalyzer -Path scripts -Recurse -Settings ./PSScriptAnalyzerSettings.psd1
$findings | Format-Table -AutoSize
if ($findings) { exit 1 }
# Warns instead of failing until the maintainer has pushed the checkpoint tags.
- name: Checkpoint tags
if: runner.os == 'Linux'
run: ./scripts/lab/Test-CheckpointTags.ps1 -AllowMissing
site:
name: Lab site build
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: site
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
cache: npm
cache-dependency-path: site/package-lock.json
- run: npm ci --no-audit --no-fund
- run: npm run check
- run: npm run build
dependency-review:
name: Dependency review
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
with:
fail-on-severity: moderate

The model reviewer is stricter about report bindings and lineage than a generic code review:

.github/agents/model-reviewer.agent.md (lines 26-40)
## What to look for
**Blocking**
- Validation or lint failures.
- A renamed table or column that the report or a measure still uses by its old name.
- A changed `sourceColumn`, `lineageTag`, or Power Query partition that wasn't part of the request.
- Bidirectional or many-to-many relationships without a written reason.
- Edits under `data/generated/`, `evaluation/expected/`, `*.SemanticModel/Copilot/`, or `.pbi/`.
- Anything that looks like real patient, customer, or tenant data.
**Suggestions**
- Descriptions that repeat the name instead of saying what the object means.
- Measures without a format string or display folder.
- DAX that works but is hard to read, for example nested `CALCULATE` where a variable is clearer.
- AI instructions that are vague, contradict a measure description, or are longer than they need to be.

You should see: CI jobs named Validate (ubuntu-latest), Validate (windows-latest), Lab site build, and Dependency review on pull requests.

Step 2. Run local validation before you push

Section titled “Step 2. Run local validation before you push”
Terminal window
pwsh ./scripts/Invoke-Validation.ps1
pwsh ./scripts/Test-ReportBindings.ps1

You should see: validation passes and report bindings pass. If either fails, fix it before opening a PR. A PR is for review, not for discovering basic parse failures.

Step 3. Push your feature branch to your copy

Section titled “Step 3. Push your feature branch to your copy”

Push to origin, which should be your own repo, not upstream.

Terminal window
git remote -v
git branch --show-current
git push -u origin feature/ai-ready-names

If your branch has a different name, use the name from git branch --show-current.

You should see: origin points at https://github.com/<your-github-user>/<your-repo>.git, and Git prints a URL for opening a pull request.

Use GitHub CLI or the web UI.

Terminal window
gh pr create --fill

For the web UI:

  1. Open your repo on GitHub.com.
  2. Select Pull requests > New pull request.
  3. Set base to main and compare to feature/ai-ready-names.
  4. Select Create pull request.
  5. Fill the template. Paste the Lab 10 comparison output in Business question results if you have it.

The template tells reviewers what evidence to look for:

.github/pull_request_template.md
## What changed
<!-- One or two sentences. What does this PR change and why? -->
## Type of change
- [ ] Semantic model (TMDL) or report (PBIR)
- [ ] Synthetic data, questions, or expected results
- [ ] Copilot customization (instructions, agents, skills, prompts, hooks, MCP)
- [ ] Scripts, CI, or tooling
- [ ] Lab site content
## Checks
- [ ] `pwsh ./scripts/Invoke-Validation.ps1` passes locally
- [ ] If I renamed model objects, `pwsh ./scripts/Test-ReportBindings.ps1` passes
- [ ] If I changed data or questions, I ran `pwsh ./scripts/Update-ExpectedResults.ps1` and committed the results
- [ ] No credentials, tenant IDs, workspace IDs, or real customer details
- [ ] No `.pbi/localSettings.json`, `.pbi/cache.abf`, or `.pbix` files
## Business question results (model changes only)
<!-- Paste the output of: pwsh ./scripts/Compare-QuestionRuns.ps1 -Before <label> -After <label> -->
## Lab pages (site changes only)
- [ ] Ten sections in order; `npm run check` passes in `site/`
- [ ] Code shown with `RepoFile`
- [ ] Screenshots follow the checklist in CONTRIBUTING.md

You should see: a pull request from your feature branch into main, with the template sections filled in and no .pbi or local output files in the file list.

Open the PR’s Checks tab, or use GitHub CLI.

Terminal window
gh pr checks --watch
  1. In the GitHub web UI, open the pull request.
  2. Select Checks.
  3. Open Validate (ubuntu-latest) and find Initialize and validate.
  4. Open Validate (windows-latest) and find Pester plus PSScriptAnalyzer.
  5. Open Lab site build if you changed site files.

You should see: green checks for Validate (ubuntu-latest), Validate (windows-latest), Lab site build, and Dependency review. The validate job runs TMDL lint, report bindings, expected answers, questions, runs, customization checks, and hygiene through Initialize-DevEnvironment.ps1.

Needs a paid Copilot plan

GitHub Docs say Copilot code review is requested from the PR sidebar.

  1. Open the pull request on GitHub.com.
  2. In the right sidebar, find Reviewers.
  3. Next to Copilot, select Request.
  4. Wait for the review to finish. GitHub Docs say it usually takes less than 30 seconds, but busy PRs can take longer.
  5. Read each comment. Look for severity labels such as High, Medium, or Low.
  6. If you push more commits later, use the re-request button next to Copilot in Reviewers.

You should see: a Copilot review on the PR and passing checks from step 5. This satisfies the Lab 12 check when the PR comes from a feature branch, not main or master.

Step 7. Compare with the local model-reviewer prompt

Section titled “Step 7. Compare with the local model-reviewer prompt”

Run the local reviewer from VS Code.

  1. Open Copilot Chat: Ctrl+Alt+I (macOS: Ctrl+Cmd+I), or View > Chat.
  2. Pick model-reviewer from the agent dropdown.
  3. Type /review-model-pr base=main and press Enter.
  4. Approve the validation commands after reading them.

The prompt is short because the agent file carries the review criteria:

.github/prompts/review-model-pr.prompt.md
---
name: review-model-pr
description: Review semantic model and report changes on this branch before opening a pull request.
agent: model-reviewer
argument-hint: Base branch (default main)
---
Review the semantic model and report changes on this branch against `${input:base:main}`.
Run the validation, read the diff under `fabric/`, and return your review in the format from your instructions. Put anything that would break the model, the report, or the lab checks under **Blocking**.

Path instructions shape both local and GitHub review. For example, TMDL review should preserve lineageTag, and PBIR review should update every Entity, Property, and queryRef after a rename:

.github/instructions/tmdl.instructions.md (lines 7-17)
- Indent with tabs, one level per nesting depth. Properties of an object sit one tab deeper than the object line.
- Put descriptions on the line directly above the object as `/// text`. Keep them under 200 characters, because Power BI Copilot reads only the first 200.
- Quote names that contain spaces or special characters with single quotes: `column 'Store Key'`, `measure 'MoM %'`. In DAX, refer to columns as `'Table Name'[Column Name]` and to measures as `[Measure Name]`.
- Never change or remove a `lineageTag`. When you add an object, leave `lineageTag` out; Desktop adds one when it saves.
- When you rename a column, keep `sourceColumn` pointing at the CSV header. Rename references in `relationships.tmdl`, measures, sort-by columns, hierarchies, `cultures/en-US.tmdl`, and the report.
- Set `summarizeBy: none` on keys, IDs, years, month numbers, and other numbers that must not be added up. Only the columns listed as `additive` in `rules/naming-conventions.yaml` may summarize.
- Hide keys and sort helpers with `isHidden`.
- Every measure needs a description, a `formatString`, and a `displayFolder` from the list in `rules/naming-conventions.yaml`.
- Relationships to Date, Store, and Medication are many-to-one and single direction. Don't add `crossFilteringBehavior: bothDirections`.
- Don't edit `expressions.tmdl` unless the task is about the data source. The `DataFolderPath` parameter must stay a placeholder path, never a personal path.
- After editing, run `pwsh ./scripts/Invoke-Validation.ps1 -Only tmdl`. Load the `tmdl-authoring` skill for syntax details.
.github/instructions/pbir.instructions.md (lines 7-12)
- The report is PBIR: one `visual.json` per visual under `definition/pages/<page>/visuals/<visual>/`. Folder names are stable IDs; don't rename them.
- A field reference has an `Entity` (table name) and a `Property` (column or measure name). Both must match the semantic model exactly, including spaces and case.
- When the model renames a table, column, or measure, update every `Entity`, `Property`, and `queryRef` that uses the old name. Also update `nativeQueryRef` and any `displayName` that shows the old name.
- Don't change visual positions, sizes, or formatting unless the task asks for it.
- Keep JSON valid and keep the `$schema` line. Two-space indentation, as Desktop writes it.
- After editing, run `pwsh ./scripts/Test-ReportBindings.ps1`. The `pbir-rebinding` skill has the full procedure.

You should see: a local review with Blocking, Suggestions, Questions, and Validation sections when relevant. Compare it with Copilot’s GitHub review. The local agent is usually better at running repo scripts; GitHub code review is better at PR conversation and inline comments.

Now create a controlled failure so you can see CI catch it. You will fix it in the next step.

  1. Open fabric/ContosoPharmacy.SemanticModel/definition/tables/Store.tmdl.
  2. Rename the Region column to Sales Region.
  3. Do not update the report PBIR files.
  4. Commit and push the broken change.
Terminal window
git add fabric/ContosoPharmacy.SemanticModel/definition/tables/Store.tmdl
git commit -m "Test CI report binding failure"
git push

You should see: the PR checks rerun. Validate (ubuntu-latest) or Validate (windows-latest) fails inside the validate job because Test-ReportBindings.ps1 finds report fields that still point at Store[Region].

Restore the column name, push the fix, and wait for checks to turn green again.

Terminal window
git revert --no-edit HEAD
git push
gh pr checks --watch

When checks pass and you’ve read the review, merge the PR from GitHub.com with Merge pull request, or use:

Terminal window
gh pr merge --merge --delete-branch

You should see: the PR returns to green, the deliberate failure commit is neutralized by the fix commit, and the merged PR records CI plus Copilot review evidence.

Step 10. Optional: enable automatic Copilot review

Section titled “Step 10. Optional: enable automatic Copilot review”
Needs a paid Copilot plan Optional

If this is your own repo and your plan allows it, configure automatic reviews so future PRs don’t need a manual request.

  1. On GitHub.com, open your repo.
  2. Select Settings > Rules > Rulesets.
  3. Create or edit a ruleset for pull requests into main.
  4. Add Copilot code review as an automatic review requirement if your UI exposes it.
  5. If the UI offers Review new pushes, enable it only if you want Copilot to re-review after each push.

You should see: future pull requests to main request Copilot review automatically. If you don’t see the option, your plan or organization policy may not allow it.

Run this from the repo root in a PowerShell 7 terminal (Terminal > New Terminal in VS Code):

Terminal window
pwsh ./scripts/lab/Test-LabProgress.ps1 -Lab 12

When everything is done you should see:

Expected output
PASS L12-pull-request A pull request from a feature branch exists with passing checks and a Copilot review
Lab 12: 1 of 1 checks passed.

Each FAIL line prints a hint and a link back to the step on this page that fixes it. The command exits with code 0 when every required check passes.

CheckWhat it looks atIf it fails
L12-pull-requestA pull request from a feature branch exists with passing checks and a Copilot reviewPush your branch, open a PR, and request a review from Copilot. Needs gh signed in. Go to step 6
gh pr create opens against the upstream repo

Check git remote -v. origin should be your copy. If origin points at sabajamalian/fabric-powerbi-ai-devops-demo, rename it to upstream, add your copy as origin, and push again. Lab 01 step 3 shows the intended remote setup.

Dependency review is skipped or missing

The dependency-review job runs only on pull requests. If you are looking at a push to main or a local workflow run, it won’t run. The Lab 12 check only needs a PR with green checks and a Copilot review.

Copilot is not shown under Reviewers

Copilot code review needs a paid plan or organization access, and admins can disable it. Check your GitHub Copilot settings and repository policies. If you’re in an organization, ask an owner whether Copilot code review is enabled.

The deliberate break fails a different validation first

If TMDL lint fails before report bindings, restore the file and make a smaller rename: only change the column object name from Region to Sales Region. Don’t change sourceColumn, lineageTag, indentation, or DAX. Then push again.

Copilot review comments look generic

Check that the PR includes the instruction and skill files on the head branch. GitHub Docs say Copilot code review reads custom instructions, agent instructions, and skills from the PR head branch. Also make sure the PR description explains the model changes and includes the question comparison.

CI on this repo runs without any Fabric tenant. What does that mean the validate job can and can't prove?

Show the answer

It proves the TMDL parses, follows the conventions, and every report field binds, but not that a Copilot answer is correct.

The validate job checks the files: lint, conventions, bindings, data, expected results, and customizations. Answer quality comes from the graded question runs in Labs 05 and 10, and deployment is phase two.

Why did the deliberate column rename fail CI?

Show the answer

The report PBIR files still referenced the old field name.

Report visuals bind by table and field names. Renaming a model column without rebinding PBIR leaves stale report references.

What is the best way to use Copilot code review here?

Show the answer

Request it after checks are running, read the comments, and compare with the local model-reviewer output.

Copilot review is another reviewer. CI still proves the model and repo checks pass.

You pushed the AI-ready branch, opened a PR, watched the real CI jobs, requested Copilot code review, compared it with the local model-reviewer prompt, and proved CI catches a broken report binding. The next lab moves the same style of scoped work to Copilot cloud agent.

Next: Lab 13: Copilot cloud agent

  • Add a short note to the PR description that maps each Lab 09 AI prep file to the business question it helps.
  • Re-request Copilot review after the fix commit and compare whether it comments on the deliberate failure history.
  • Configure automatic Copilot review with a ruleset in your own copy, then open a tiny documentation PR to test it.