Added Migrate SDM to EDM skill for migration#109
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new Power Pages skill (migrate-sdm-to-edm) to guide SDM→EDM migrations via PAC CLI, including HTML report templates and a Node utility to generate those reports.
Changes:
- Introduces the
migrate-sdm-to-edmskill workflow (SKILL.md) and accompanying design/integration docs. - Adds HTML templates for customization and execution reporting.
- Adds a Node script to generate the HTML reports and registers the skill in skill-tracking mapping.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| plugins/power-pages/skills/migrate-sdm-to-edm/scripts/generate-migration-reports.js | New CLI utility that parses PAC CSV output and renders HTML reports from templates |
| plugins/power-pages/skills/migrate-sdm-to-edm/assets/skill-execution-report.html | New execution report HTML template with placeholders |
| plugins/power-pages/skills/migrate-sdm-to-edm/assets/customization-report.html | New customization report HTML template with placeholders |
| plugins/power-pages/skills/migrate-sdm-to-edm/assets/README.md | Documents template placeholders and intended usage |
| plugins/power-pages/skills/migrate-sdm-to-edm/SKILL.md | New skill definition and phased migration guidance |
| plugins/power-pages/skills/migrate-sdm-to-edm/REPORTS_INTEGRATION.md | Integration guidance for generating and sharing reports in the workflow |
| plugins/power-pages/skills/migrate-sdm-to-edm/DESIGN.md | Design/spec for the migration skill |
| plugins/power-pages/references/skill-tracking-reference.md | Adds the new skill to the skill-name mapping table |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| const { parse: parseCSV } = require('csv-parse/sync'); | ||
|
|
There was a problem hiding this comment.
csv-parse/sync is required here, but there’s no csv-parse dependency anywhere in this repo (no package.json / lockfile references). As-is, running this script will fail with "Cannot find module 'csv-parse/sync'". Either add the dependency in the repo’s Node dependency manifest (and ensure install in the workflow) or replace this with a zero-dependency CSV parser appropriate for the PAC output.
| .replace('{{WEBSITE_ID}}', escapeHtml(args['website-id'] || 'N/A')) | ||
| .replace('{{PORTAL_ID}}', escapeHtml(args['portal-id'] || 'N/A')) | ||
| .replace('{{MIGRATION_STATUS_TEXT}}', 'Completed Successfully') | ||
| .replace('{{REPORT_DATE}}', dateStr) |
There was a problem hiding this comment.
Same issue here: {{REPORT_DATE}} occurs twice in skill-execution-report.html, but this replacement only updates the first instance. Use replaceAll / global replacement for repeated placeholders.
| .replace('{{REPORT_DATE}}', dateStr) | |
| .replaceAll('{{REPORT_DATE}}', dateStr) |
| <tr> | ||
| <td>${item.location || 'N/A'}</td> | ||
| <td><div class="snippet">${escapeHtml(item.snippet || '')}</div></td> | ||
| <td><a href="${item.guidance}" target="_blank">View Guidance</a></td> | ||
| </tr> |
There was a problem hiding this comment.
HTML injection risk: item.location and item.guidance are inserted into the report without escaping/validation. A crafted CSV could inject markup/JS into the generated report. Escape item.location and either strictly validate item.guidance as an allowed URL (e.g., https://learn.microsoft.com/… / go.microsoft.com/fwlink/…) or escape it and render as plain text. Also add rel="noopener noreferrer" when using target="_blank".
| > 1. Open the **Data workspace** in Power Pages | ||
| > 2. Create a new table (e.g., `contoso_webpage`) | ||
| > 3. Add the custom column (e.g., `contoso_pagetype`) to the new table | ||
| > 4. Add a lookup column associated with `powerpagescomponent` |
There was a problem hiding this comment.
This step references powerpagescomponent, but the rest of the skill (and examples below) use powerpagecomponent (no “s”). The inconsistent table name will mislead users during remediation; please correct it to the intended logical name.
| > 4. Add a lookup column associated with `powerpagescomponent` | |
| > 4. Add a lookup column associated with `powerpagecomponent` |
| For each custom column found on an `adx_*` table, instruct the user to: | ||
| 1. Create a new custom table (e.g., `contoso_webpage`) in the Data workspace | ||
| 2. Add the custom column (e.g., `contoso_pagetype`) to the new table | ||
| 3. Add a lookup column associated with `powerpagescomponent` |
There was a problem hiding this comment.
This remediation instruction uses powerpagescomponent, but elsewhere in this doc and in SKILL.md the table is referred to as powerpagecomponent. Please fix the inconsistency to avoid pointing users at a non-existent/incorrect table name.
| 3. Add a lookup column associated with `powerpagescomponent` | |
| 3. Add a lookup column associated with `powerpagecomponent` |
| .replace('{{SITE_NAME}}', escapeHtml(args['site-name'] || 'Unknown')) | ||
| .replace('{{WEBSITE_ID}}', escapeHtml(args['website-id'] || 'N/A')) | ||
| .replace('{{TEMPLATE_NAME}}', escapeHtml(args['template-name'] || 'Unknown')) | ||
| .replace('{{REPORT_DATE}}', new Date().toISOString().split('T')[0]) |
There was a problem hiding this comment.
The placeholder replacement uses String.prototype.replace, which only replaces the first occurrence. {{REPORT_DATE}} appears twice in the customization report template (metadata + footer), so the footer will keep the raw {{REPORT_DATE}}. Use replaceAll (or a global regex) for placeholders that can appear multiple times.
| .replace('{{REPORT_DATE}}', new Date().toISOString().split('T')[0]) | |
| .replaceAll('{{REPORT_DATE}}', new Date().toISOString().split('T')[0]) |
| <div class="phase"> | ||
| <div class="command-label">Step 3: Download Customization Report</div> | ||
| <div class="command-block">pac pages migrate-datamodel --webSiteId "{{WEBSITE_ID}}" --siteCustomizationReportPath "./migration-report"</div> | ||
| <div class="result-item success"> | ||
| <div class="result-title">✓ Success</div> |
There was a problem hiding this comment.
pacCommandsHtml hardcodes --webSiteId "{{WEBSITE_ID}}" inside the generated HTML string. Because this placeholder is not part of the outer HTML template, it will never be replaced and will render literally in the final report. Interpolate the actual website id into this string (escaped) or run placeholder substitution on generated sections too.
| html += ` | ||
| <tr> | ||
| <td>${item.location || 'N/A'}</td> | ||
| <td><div class="snippet">${escapeHtml(item.snippet || '')}</div></td> |
There was a problem hiding this comment.
snippet is computed here but never used, and the generated HTML renders the full item.snippet anyway. This can bloat the report significantly for large snippets and makes the truncation logic dead code. Either use the truncated value in the HTML output or remove the unused variable.
| <td><div class="snippet">${escapeHtml(item.snippet || '')}</div></td> | |
| <td><div class="snippet">${escapeHtml(snippet)}</div></td> |
| // Parse command line arguments | ||
| function parseArgs(args) { | ||
| const result = {}; | ||
| for (let i = 0; i < args.length; i++) { | ||
| if (args[i].startsWith('--')) { | ||
| const key = args[i].replace('--', ''); | ||
| const value = args[i + 1]; | ||
| if (value && !value.startsWith('--')) { | ||
| result[key] = value; | ||
| i++; | ||
| } | ||
| } | ||
| } | ||
| return result; | ||
| } |
There was a problem hiding this comment.
This PR adds a new Node script, but there’s no corresponding node:test coverage. plugins/power-pages/AGENTS.md states that script additions/changes must ship with tests under plugins/power-pages/scripts/tests/. Add a test file that covers CSV parsing + placeholder replacement (including multi-occurrence placeholders) so regressions are caught.
| ### CSV Parsing | ||
|
|
||
| The script uses `csv-parse` (npm package). Ensure it's available: | ||
|
|
||
| ```bash | ||
| npm install csv-parse | ||
| ``` | ||
|
|
||
| Or modify the script to use a different CSV parser if needed. |
There was a problem hiding this comment.
This doc instructs running npm install csv-parse, but the repo doesn’t currently have a Node dependency manifest where this would be captured/installed as part of the skill workflow. Given the script now requires csv-parse/sync, either document the exact supported installation mechanism for this repo (so CI/users get the dependency) or remove the external dependency and update this section accordingly.
No description provided.