diff --git a/.github/instructions/python-standards.instructions.md b/.github/instructions/python-standards.instructions.md new file mode 100644 index 00000000..b249c570 --- /dev/null +++ b/.github/instructions/python-standards.instructions.md @@ -0,0 +1,25 @@ +--- +applyTo: "**/*.py" +--- + +# Python Standards + +These rules apply automatically whenever Copilot works on a Python file in +this project. They never load for other file types, so a conversation about a +Dockerfile or some JSON stays clutter-free. + +## Style + +- Follow PEP 8 style conventions. +- Add type hints to every function signature. +- Prefer f-strings over `%` formatting or `str.format()`. + +## Error Handling + +- Catch specific exceptions; never use a bare `except:`. +- Validate inputs at function boundaries and fail with a clear message. + +## Testing + +- Put pytest tests in `samples/book-app-project/tests/` using `test_*.py` naming. +- Cover the happy path and edge cases (empty input, missing data). diff --git a/.github/scripts/copy-content-engine-files.js b/.github/scripts/copy-content-engine-files.js index 285458af..f55e358b 100644 --- a/.github/scripts/copy-content-engine-files.js +++ b/.github/scripts/copy-content-engine-files.js @@ -4,6 +4,9 @@ * * Usage: * npm run copy:content-engine + * + * Optional: + * CONTENT_ENGINE_DESTINATION_ROOT=/path/to/cse-content-engine/content/learning-pathways/copilot-cli-for-beginners npm run copy:content-engine */ const { @@ -12,13 +15,18 @@ const { mkdirSync, readdirSync, readFileSync, + rmSync, statSync, writeFileSync, } = require('fs'); const { dirname, join, relative, resolve } = require('path'); const sourceRoot = process.cwd(); -const destinationRoot = '/Users/danwahlin/Desktop/projects/cse-content-engine/content/learning-pathways/copilot-cli-for-beginners'; +const defaultDestinationRoot = resolve( + sourceRoot, + '../cse-content-engine/content/learning-pathways/copilot-cli-for-beginners', +); +const destinationRoot = resolve(process.env.CONTENT_ENGINE_DESTINATION_ROOT || defaultDestinationRoot); const destinationParent = dirname(destinationRoot); const contentEngineSchema = { $schema: 'http://json-schema.org/draft-07/schema#', @@ -95,6 +103,11 @@ function ensureSafeDestination() { } } +function resetDestination() { + rmSync(destinationRoot, { recursive: true, force: true }); + log(`Cleared ${destinationRoot}`); +} + function copyFile(sourcePath, destinationPath) { mkdirSync(dirname(destinationPath), { recursive: true }); @@ -333,6 +346,7 @@ function validateMarkdownFrontmatter() { } ensureSafeDestination(); +resetDestination(); copyCourseContent(); validateMarkdownFrontmatter(); validateMarkdownImagePaths(); diff --git a/.github/scripts/filter-co-op-review.js b/.github/scripts/filter-co-op-review.js new file mode 100644 index 00000000..2b36960b --- /dev/null +++ b/.github/scripts/filter-co-op-review.js @@ -0,0 +1,105 @@ +#!/usr/bin/env node + +// Runs `co-op-review` and post-filters its findings so the workflow only fails +// on genuine errors. +// +// Why this exists: the `translate` command and the `co-op-review` command ship +// with *different* exclusion lists. `translate` skips `.github/**` (it is in +// Co-op Translator's EXCLUDED_DIRS), so those files are never translated. But +// `co-op-review` does NOT exclude `.github/**`, so it reports every one of those +// untranslated source files as a "Missing translated file" error and fails the +// build. Those errors are false positives for paths this repo intentionally +// leaves untranslated. The review CLI exposes no way to extend its exclusions, +// so we drop findings under the excluded paths here and fail only on the rest. + +const { spawnSync } = require("child_process"); + +// Source paths that are intentionally excluded from translation. Keep this in +// sync with the excludes in .github/workflows/co-op-translator.yml and the +// guidance in .github/workflows/translation-polisher.md. +const EXCLUDED_PREFIXES = [".github/", "samples/"]; + +const languages = process.argv + .slice(2) + .flatMap((value) => value.split(/\s+/)) + .map((value) => value.trim()) + .filter(Boolean); + +if (languages.length === 0) { + console.error("Usage: node .github/scripts/filter-co-op-review.js "); + process.exit(1); +} + +const result = spawnSync( + "co-op-review", + ["-l", languages.join(" "), "--format", "github"], + { encoding: "utf8" } +); + +if (result.error && result.error.code === "ENOENT") { + console.log("co-op-review is not available in the installed Co-op Translator package; skipping."); + process.exit(0); +} + +const report = result.stdout || ""; +const stderr = result.stderr || ""; + +// Always surface the full report in the job log (and summary when available). +if (report.trim()) { + console.log(report.trimEnd()); +} +if (process.env.GITHUB_STEP_SUMMARY && report.trim()) { + try { + require("fs").appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${report.trimEnd()}\n`); + } catch (err) { + console.error(`Could not write review report to step summary: ${err.message}`); + } +} + +const isExcluded = (filePath) => + EXCLUDED_PREFIXES.some((prefix) => filePath.startsWith(prefix)); + +const genuineErrors = []; +const ignoredErrors = []; + +for (const line of report.split("\n")) { + if (!line.trimStart().startsWith("|")) { + continue; + } + const cells = line.split("|").map((cell) => cell.trim()); + // cells[0] is the empty string before the leading pipe. + const severity = (cells[1] || "").toLowerCase(); + if (severity !== "error" && severity !== "warning") { + continue; // Skip the metrics table, header, and separator rows. + } + if (severity !== "error") { + continue; // Only errors fail the build; warnings are informational. + } + const filePath = (cells[4] || "").replace(/`/g, "").trim(); + if (isExcluded(filePath)) { + ignoredErrors.push(filePath); + } else { + genuineErrors.push({ filePath, message: cells[5] || "" }); + } +} + +if (ignoredErrors.length > 0) { + console.log( + `\nIgnored ${ignoredErrors.length} expected finding(s) for intentionally untranslated paths ` + + `(${EXCLUDED_PREFIXES.join(", ")}).` + ); +} + +if (genuineErrors.length > 0) { + console.error(`\nco-op-review found ${genuineErrors.length} genuine error(s):`); + for (const { filePath, message } of genuineErrors) { + console.error(` - ${filePath}: ${message}`); + } + if (stderr.trim()) { + console.error(stderr.trimEnd()); + } + process.exit(1); +} + +console.log("\nco-op-review passed (no genuine errors)."); +process.exit(0); diff --git a/.github/uvs.csv b/.github/uvs.csv index 8758710e..345702fd 100644 --- a/.github/uvs.csv +++ b/.github/uvs.csv @@ -95,3 +95,45 @@ "06/05",662 "06/06",291 "06/07",299 +"06/08",631 +"06/09",744 +"06/10",624 +"06/11",685 +"06/12",733 +"06/13",288 +"06/14",294 +"06/15",674 +"06/16",425 +"06/17",612 +"06/18",717 +"06/19",568 +"06/20",227 +"06/21",244 +"06/22",650 +"06/23",591 +"06/24",583 +"06/25",565 +"06/26",517 +"06/27",241 +"06/28",287 +"06/29",605 +"06/30",550 +"07/01",492 +"07/02",419 +"07/03",497 +"07/04",198 +"07/05",226 +"07/06",507 +"07/07",441 +"07/08",490 +"07/09",474 +"07/10",463 +"07/11",377 +"07/12",336 +"07/13",478 +"07/14",497 +"07/15",426 +"07/16",407 +"07/17",474 +"07/18",213 +"07/19",238 diff --git a/.github/views.csv b/.github/views.csv index 432f3d1e..4f566887 100644 --- a/.github/views.csv +++ b/.github/views.csv @@ -94,3 +94,45 @@ "06/05",1484 "06/06",665 "06/07",781 +"06/08",1249 +"06/09",1507 +"06/10",1229 +"06/11",1504 +"06/12",1691 +"06/13",693 +"06/14",684 +"06/15",1280 +"06/16",735 +"06/17",1235 +"06/18",1826 +"06/19",1319 +"06/20",589 +"06/21",552 +"06/22",1348 +"06/23",1152 +"06/24",1154 +"06/25",1230 +"06/26",1066 +"06/27",593 +"06/28",590 +"06/29",1184 +"06/30",1116 +"07/01",1035 +"07/02",1032 +"07/03",1199 +"07/04",495 +"07/05",794 +"07/06",2370 +"07/07",2880 +"07/08",4565 +"07/09",4512 +"07/10",904 +"07/11",850 +"07/12",788 +"07/13",981 +"07/14",980 +"07/15",924 +"07/16",822 +"07/17",1162 +"07/18",485 +"07/19",596 diff --git a/.github/workflows/co-op-translator.yml b/.github/workflows/co-op-translator.yml index edff8cc4..a06a9c38 100644 --- a/.github/workflows/co-op-translator.yml +++ b/.github/workflows/co-op-translator.yml @@ -14,7 +14,7 @@ on: - "!translations/**" - "!translated_images/**" - "!.github/**" - - "!samples/skills/**" + - "!samples/**" permissions: contents: write @@ -43,12 +43,12 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.12" @@ -87,32 +87,28 @@ jobs: run: | migrate-links -l "$TRANSLATION_LANGUAGES" -y node .github/scripts/fix-translated-markdown.js "$TRANSLATION_LANGUAGES" - if command -v co-op-review >/dev/null 2>&1; then - co-op-review -l "$TRANSLATION_LANGUAGES" --format github - else - echo "co-op-review is not available in the installed Co-op Translator package; skipping." - fi + node .github/scripts/filter-co-op-review.js "$TRANSLATION_LANGUAGES" - name: Remove excluded translations run: | for language in $TRANSLATION_LANGUAGES; do rm -rf \ "translations/$language/.github" \ - "translations/$language/samples/skills" \ + "translations/$language/samples" \ "translated_images/$language/.github" \ - "translated_images/$language/samples/skills" + "translated_images/$language/samples" done - name: Upload Co-op Translator logs if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: co-op-translator-logs path: logs/ if-no-files-found: ignore - name: Create pull request - uses: peter-evans/create-pull-request@v7 + uses: peter-evans/create-pull-request@v8 with: token: ${{ secrets.GH_AW_GITHUB_TOKEN }} commit-message: "Update translations via Co-op Translator" diff --git a/.github/workflows/sync-content-engine.yml b/.github/workflows/sync-content-engine.yml new file mode 100644 index 00000000..208f7d78 --- /dev/null +++ b/.github/workflows/sync-content-engine.yml @@ -0,0 +1,60 @@ +name: Sync content-engine copy + +on: + workflow_dispatch: + push: + branches: + - main + +permissions: + contents: read + +concurrency: + group: sync-content-engine-copy + cancel-in-progress: true + +jobs: + sync-content: + name: Copy content and open content-engine PR + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout course repository + uses: actions/checkout@v4 + with: + path: copilot-cli-for-beginners + + - name: Checkout content-engine repository + uses: actions/checkout@v4 + with: + repository: github/cse-content-engine + token: ${{ secrets.GH_AW_GITHUB_TOKEN }} + path: cse-content-engine + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Copy course content into content-engine + working-directory: copilot-cli-for-beginners + env: + CONTENT_ENGINE_DESTINATION_ROOT: ${{ github.workspace }}/cse-content-engine/content/learning-pathways/copilot-cli-for-beginners + run: npm run copy:content-engine + + - name: Create content-engine pull request + uses: peter-evans/create-pull-request@v7 + with: + path: cse-content-engine + token: ${{ secrets.GH_AW_GITHUB_TOKEN }} + commit-message: "Update Copilot CLI for Beginners content" + title: "Update Copilot CLI for Beginners content" + body: | + This automated PR syncs the latest `github/copilot-cli-for-beginners` + content into the content-engine learning pathway. + + Source commit: ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }} + branch: update/copilot-cli-for-beginners-content + base: main + delete-branch: true diff --git a/.github/workflows/translation-polisher.md b/.github/workflows/translation-polisher.md index 64a3063c..7eff0d74 100644 --- a/.github/workflows/translation-polisher.md +++ b/.github/workflows/translation-polisher.md @@ -93,7 +93,7 @@ You may edit only translated Markdown files: Exclude these generated translation paths from review, grading, and edits: - `translations/*/.github/**` -- `translations/*/samples/skills/**` +- `translations/*/samples/**` Leave skill definition Markdown in English. @@ -107,11 +107,15 @@ Do not edit: ## Required process -1. Identify the target pull request number and head branch. +> **Shell tooling constraint:** Only the `gh`, `git`, and `node` commands are available in this environment. Do **not** call `python`, `python3`, `sed`, `awk`, or `grep` pipelines — they are not permitted and will fail with a permission error. Filter and process command output yourself instead of piping through unsupported tools. + +1. Identify the target pull request number and head branch, then list the files the pull request changed. Use only the available shell tools: + - List changed files with `git diff --name-only origin/main...HEAD` (the branch is checked out with full history) or `gh pr diff --name-only`. + - From that output, select only files matching `translations/**/*.md`, and ignore `translations/*/.github/**` and `translations/*/samples/**`. Do this selection yourself from the file list rather than piping through `grep`/`sed`. 2. Compare each changed translated Markdown file with its corresponding English source file. - Example: compare `translations/es/README.md` with `README.md`. - Example: compare `translations/es/03-development-workflows/README.md` with `03-development-workflows/README.md`. -3. Focus on files changed by the pull request, not every translated file in the repository. Ignore excluded translation paths under `translations/*/.github/**` and `translations/*/samples/skills/**`. +3. Focus on files changed by the pull request, not every translated file in the repository. Ignore excluded translation paths under `translations/*/.github/**` and `translations/*/samples/**`. 4. If the pull request body already has a managed `## Translation Quality Review` section, identify files with grades below A- and repair those files first. 5. Preserve Markdown structure exactly unless a link or heading fix is required. 6. Apply the shared quality rules and the language quality profile for each target language present in the pull request. diff --git a/01-setup-and-first-steps/README.md b/01-setup-and-first-steps/README.md index 485002ab..13afdafe 100644 --- a/01-setup-and-first-steps/README.md +++ b/01-setup-and-first-steps/README.md @@ -325,7 +325,7 @@ Step 4: Test the flow Proceed with implementation? [Y/n] ``` -**Key insight**: Plan mode lets you review and modify the approach before any code is written. Once a plan is complete, you can even tell Copilot CLI to save it to a file for later reference. For example, "Save this plan to `mark_as_read_plan.md`" would create a markdown file with the plan details. +**Key insight**: Plan mode lets you review and modify the approach before any code is written. While in plan mode, Copilot CLI is **read-only** and will not edit any files or run commands that change your workspace until you approve and move to implementation. This keeps you safely in the "thinking" stage until you're ready. Once a plan is complete, you can even tell Copilot CLI to save it to a file for later reference. For example, "Save this plan to `mark_as_read_plan.md`" would create a markdown file with the plan details. > 💡 **Want something more complex?** Try: `/plan Add search and filter capabilities to the book app`. Plan mode scales from simple features to full applications. @@ -383,11 +383,14 @@ These commands are great to learn initially as you're getting started with Copil | `/help` | Show all available commands | When you forget a command | | `/model` | Show or switch AI model | When you want to change the AI model | | `/plan` | Plan your work out before coding | For more complex features | +| `/refine` | Rewrite a rough, stream-of-consciousness prompt into a clear, focused one | When your prompt feels messy and you want better results | | `/research` | Deep research using GitHub and web sources | When you need to investigate a topic before coding | | `/exit` | End the session | When you're done | > 💡 **`/ask` vs regular chat**: Normally every message you send becomes part of the ongoing conversation and affects future responses. `/ask` is an "off the record" shortcut — perfect for quick one-off questions like `/ask What does YAML mean?` without polluting your session context. +> 💡 **`/refine` for better prompts**: Not sure if your prompt is clear enough? Type it out as it comes to mind, then run `/refine` to let Copilot rewrite it into a precise, well-structured prompt before sending. This is especially useful when you're new to AI tools and still learning how to write effective prompts. + > 💡 **Tab-completion**: When typing a slash command, press **Tab** to auto-complete the command name or cycle through available subcommands and arguments. This is especially handy when you can't remember the exact name of a command. That's it for getting started! As you become comfortable, you can explore additional commands. @@ -407,6 +410,7 @@ That's it for getting started! As you become comfortable, you can explore additi | `/env` | Show loaded environment details — what instructions, MCP servers, skills, agents, and plugins are active | | `/init` | Initialize Copilot instructions for your repository | | `/mcp` | Manage MCP server configuration | +| `/settings` | Open an interactive dialog to browse and edit all user settings in one place | | `/skills` | Manage skills for enhanced capabilities | > 💡 Agents are covered in [Chapter 04](../04-agents-custom-instructions/README.md), skills are covered in [Chapter 05](../05-skills/README.md), and MCP servers are covered in [Chapter 06](../06-mcp-servers/README.md). @@ -458,6 +462,8 @@ That's it for getting started! As you become comfortable, you can explore additi | `/usage` | Display session usage metrics and statistics, including quota progress bars | | `/session` | Show session info and workspace summary; use `/session delete`, `/session delete `, or `/session delete-all` to remove sessions | | `/share` | Export session as a markdown file, GitHub gist, or self-contained HTML file | +| `/every ` | Schedule a prompt to run on a recurring interval (e.g., `/every 1h summarize new commits`). Use natural language for the interval. `/loop` is an alias for `/every`. | +| `/after