diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index da42f103..c0831c56 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -12,6 +12,6 @@ ] } }, - "postCreateCommand": "pip install pytest && npm install -g @github/copilot && gh extension install github/gh-copilot", + "onCreateCommand": "pip install pytest && npm install -g @github/copilot", "postStartCommand": "echo 'โœ… Python, pytest, GitHub CLI, and Copilot CLI are ready. Run: cd samples/book-app-project && python book_app.py help'" } diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..c1965c21 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.github/workflows/*.lock.yml linguist-generated=true merge=ours \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 00000000..76e0df0a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,65 @@ +name: ๐Ÿ› Bug Report +description: Report an issue with course content, broken examples, or incorrect instructions +labels: [bug] +body: + - type: markdown + attributes: + value: | + Thanks for helping improve this course! Please fill out the details below so we can fix the issue quickly. + + - type: dropdown + id: chapter + attributes: + label: Which chapter is affected? + options: + - "00 - Quick Start" + - "01 - First Steps" + - "02 - Context and Conversations" + - "03 - Development Workflows" + - "04 - Create Specialized AI Assistants" + - "05 - Automate Repetitive Tasks" + - "06 - Connect to GitHub, Databases & APIs" + - "07 - Putting It All Together" + - "Sample code (samples/)" + - "Appendices" + - "Other" + validations: + required: true + + - type: textarea + id: description + attributes: + label: What happened? + description: Describe the issue clearly. If a command or code example didn't work, include the exact text you copied. + placeholder: "When I ran the command from the 'Code Review' section, I got an error..." + validations: + required: true + + - type: textarea + id: expected + attributes: + label: What did you expect to happen? + placeholder: "I expected to see the code review output shown in the example..." + validations: + required: true + + - type: textarea + id: environment + attributes: + label: Environment + description: Your OS, terminal, Copilot CLI version, and Python version + placeholder: | + - OS: macOS 15 / Windows 11 / Ubuntu 24.04 + - Terminal: iTerm2 / Windows Terminal / default + - Copilot CLI version: (run `copilot --version`) + - Python version: (run `python3 --version`) + validations: + required: false + + - type: textarea + id: screenshots + attributes: + label: Screenshots or terminal output + description: If applicable, paste terminal output or attach screenshots + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 00000000..07cc61be --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,48 @@ +name: ๐Ÿ’ก Feature Request +description: Suggest new content, a new chapter topic, or an improvement to the course +labels: [enhancement] +body: + - type: markdown + attributes: + value: | + Have an idea to make this course better? We'd love to hear it! + + - type: dropdown + id: type + attributes: + label: What kind of improvement? + options: + - "New chapter or section" + - "New sample app or exercise" + - "Improve existing content" + - "Add a new agent or skill example" + - "Better explanation of a concept" + - "Other" + validations: + required: true + + - type: textarea + id: description + attributes: + label: Describe your idea + description: What would you like to see added or changed? Why would it help learners? + placeholder: "It would be helpful to have a section on..." + validations: + required: true + + - type: textarea + id: audience + attributes: + label: Who would benefit? + description: Is this for complete beginners, intermediate users, or a specific audience? + placeholder: "Beginners who are new to the terminal..." + validations: + required: false + + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Have you seen this covered elsewhere? Is there a workaround? + validations: + required: false diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json new file mode 100644 index 00000000..839c4c3a --- /dev/null +++ b/.github/aw/actions-lock.json @@ -0,0 +1,34 @@ +{ + "entries": { + "actions/checkout@v6.0.2": { + "repo": "actions/checkout", + "version": "v6.0.2", + "sha": "de0fac2e4500dabe0009e67214ff5f5447ce83dd" + }, + "actions/download-artifact@v8.0.1": { + "repo": "actions/download-artifact", + "version": "v8.0.1", + "sha": "3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c" + }, + "actions/github-script@v8": { + "repo": "actions/github-script", + "version": "v8", + "sha": "ed597411d8f924073f98dfc5c65a23a2325f34cd" + }, + "actions/github-script@v9": { + "repo": "actions/github-script", + "version": "v9", + "sha": "373c709c69115d41ff229c7e5df9f8788daa9553" + }, + "actions/upload-artifact@v7": { + "repo": "actions/upload-artifact", + "version": "v7", + "sha": "043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" + }, + "github/gh-aw-actions/setup@v0.68.1": { + "repo": "github/gh-aw-actions/setup", + "version": "v0.68.1", + "sha": "2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc" + } + } +} diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..5ed64e51 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,70 @@ +# Copilot Instructions + +These instructions guide GitHub Copilot when working in this repository. + +## Project Context + +This is a **beginner-friendly educational course** teaching GitHub Copilot CLI. The repo contains Markdown chapters (00โ€“07), Python/C#/JavaScript sample apps, and supporting assets (images, demo GIFs, glossary). It is **not** a software product โ€” it is technical courseware. + +## Writing Conventions + +- **Audience**: Beginners with no AI/ML experience. Explain every technical term on first use. +- **Tone**: Friendly, encouraging, practical. Avoid jargon without explanation. +- **Examples**: All code blocks and `copilot` commands must be copy-paste ready. Test them mentally before including. +- **Naming**: Use kebab-case for session names, file names, and identifiers (e.g., `book-app-review`, not `book app review`). +- **Command syntax**: Standardize flag format โ€” use `--flag=value` consistently when a value is required, `--flag` when boolean. +- **Precision**: Don't over-specify tool behavior that may vary across shells or OS. Describe what the user will see, not implementation details. +- **Fallbacks**: When referencing tool version requirements (e.g., `gh` CLI version), always include upgrade instructions or a manual alternative. + +## Content Conventions (from PR review patterns) + +These patterns were mined from actual PR review feedback and represent recurring maintainer expectations: + +- When showing multi-step workflows, ensure all prerequisite steps are included (e.g., `git add` before `git diff --staged`). +- When introducing a concept with an example, use consistent naming throughout the section โ€” don't mix kebab-case and quoted names. +- When describing a command's behavior, match the level of specificity in the official release notes โ€” don't state behavior that may differ across environments. +- If a feature requires a minimum tool version, mention the version AND provide a fallback path for users who can't upgrade yet. + +## Sample Code Conventions + +- **Primary sample**: Always use `samples/book-app-project/` (Python) for examples in chapters. +- **Test framework**: pytest โ€” test files go in `samples/book-app-project/tests/` and follow `test_*.py` naming. +- **Python version**: 3.10+ (per `samples/book-app-project/pyproject.toml`). +- **Intentional bugs**: Files in `samples/book-app-buggy/` and `samples/buggy-code/` contain **deliberate bugs** for exercises. Never fix them. + +## Chapter Structure + +Every chapter (00โ€“07) follows the same pattern in its `README.md`: + +1. Real-World Analogy +2. Core Concepts +3. Hands-On Examples +4. Assignment +5. What's Next + +Do not deviate from this structure when editing or adding chapter content. + +## Markdown Formatting + +- Use standard GitHub-Flavored Markdown. +- Images go in the repo-root `assets/` directory. +- Use relative links for cross-chapter references (e.g., `../03-development-workflows/README.md`). +- Emoji usage is encouraged for section headers (matching existing style). + +## Maintenance Matrix + +| Change Made | Files to Update | +|---|---| +| New chapter added | `README.md` (course table), `AGENTS.md` (structure table), `assets/learning-path.png` | +| Chapter content updated | The chapter's `README.md`, verify cross-references in adjacent chapters | +| New sample app variant added | `AGENTS.md` (structure table), `samples/` directory, relevant chapter references | +| Sample app code changed | `samples/book-app-project/tests/` (update/add tests), chapters referencing that code | +| Bug intentionally added to buggy samples | `samples/book-app-buggy/` or `samples/buggy-code/` only โ€” do NOT update tests | +| New skill added | `.github/skills/{skill-name}/SKILL.md`, `samples/skills/` (example copy), Chapter 05 | +| New agent template added | `samples/agents/`, Chapter 04 | +| New MCP config added | `samples/mcp-configs/`, Chapter 06 | +| Glossary term introduced | `GLOSSARY.md` โ€” add definition in alphabetical order | +| npm scripts changed | `package.json`, `AGENTS.md` (build section) | +| Devcontainer updated | `.devcontainer/devcontainer.json`, Chapter 00 (setup instructions) | +| Image or banner changed | `assets/` directory, any README referencing the image | +| Copilot CLI version requirements change | Chapter 00, Chapter 01, `.devcontainer/devcontainer.json` | 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 new file mode 100644 index 00000000..f55e358b --- /dev/null +++ b/.github/scripts/copy-content-engine-files.js @@ -0,0 +1,353 @@ +#!/usr/bin/env node +/** + * Copy the course content subset required by the CSE content engine. + * + * 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 { + cpSync, + existsSync, + mkdirSync, + readdirSync, + readFileSync, + rmSync, + statSync, + writeFileSync, +} = require('fs'); +const { dirname, join, relative, resolve } = require('path'); + +const sourceRoot = process.cwd(); +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#', + type: 'object', + properties: { + aliases: { + type: 'array', + description: 'Relative paths to redirect to this item', + items: { + type: 'string', + description: 'A relative path to redirect to this item', + }, + }, + audience: { + type: 'string', + description: 'The intended audience for the guide', + }, + description: { + type: 'string', + description: 'A brief description of the item', + }, + icon: { + type: 'string', + description: 'An icon to represent the item', + }, + id: { + type: 'string', + description: 'A unique identifier for the guide', + }, + params: { + type: 'object', + description: "Flexible parameters that don't affect presentation", + }, + slug: { + type: 'string', + description: 'A kebab-case identifier', + }, + title: { + type: 'string', + description: 'The display name of the item', + }, + weight: { + type: 'integer', + description: 'The order to display the item in', + }, + }, + required: ['title', 'description', 'weight'], + additionalProperties: true, +}; + +function log(message) { + console.log(` ${message}`); +} + +function fail(message) { + console.error(`\nError: ${message}`); + process.exit(1); +} + +function ensureSafeDestination() { + if (!existsSync(destinationParent)) { + fail(`Destination parent does not exist: ${destinationParent}`); + } + + const resolvedSource = resolve(sourceRoot); + const resolvedDestination = resolve(destinationRoot); + + if (resolvedSource === resolvedDestination) { + fail('Destination cannot be the source repository root.'); + } + + if (resolvedDestination.startsWith(`${resolvedSource}/`)) { + fail('Destination cannot be inside the source repository.'); + } +} + +function resetDestination() { + rmSync(destinationRoot, { recursive: true, force: true }); + log(`Cleared ${destinationRoot}`); +} + +function copyFile(sourcePath, destinationPath) { + mkdirSync(dirname(destinationPath), { recursive: true }); + + if (sourcePath.endsWith('.md')) { + writeFileSync(destinationPath, prepareMarkdownForContentEngine(sourcePath), 'utf8'); + } else { + cpSync(sourcePath, destinationPath); + } + + log(`Copied ${relative(sourceRoot, sourcePath)} -> ${relative(destinationRoot, destinationPath)}`); +} + +function prepareMarkdownForContentEngine(sourcePath) { + const markdown = readFileSync(sourcePath, 'utf8'); + const frontmatter = getMarkdownFrontmatter(markdown); + + if (!frontmatter) { + return markdown; + } + + return markdown.replace(/^\r?\n*/, `---\n${frontmatter}\n---\n\n`); +} + +function getMarkdownFrontmatter(markdown) { + const hiddenFrontmatter = markdown.match(/^/)?.[1]; + const visibleFrontmatter = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1]; + + return (hiddenFrontmatter ?? visibleFrontmatter)?.replace(/\r\n/g, '\n'); +} + +function getFrontmatterField(frontmatter, field) { + return frontmatter.match(new RegExp(`^${field}:.*$`, 'm'))?.[0]; +} + +function writeIndexFromReadme(sourceReadmePath, destinationDirectory, extraFields = []) { + const markdown = readFileSync(sourceReadmePath, 'utf8'); + const frontmatter = getMarkdownFrontmatter(markdown); + + if (!frontmatter) { + fail(`Cannot create index.yml because ${relative(sourceRoot, sourceReadmePath)} has no frontmatter.`); + } + + const indexFields = ['title', 'description', 'slug', 'weight', 'icon'] + .map((field) => getFrontmatterField(frontmatter, field)) + .filter(Boolean); + indexFields.push(...extraFields); + + if (indexFields.length === 0) { + fail(`Cannot create index.yml because ${relative(sourceRoot, sourceReadmePath)} has no index metadata.`); + } + + mkdirSync(destinationDirectory, { recursive: true }); + writeFileSync(join(destinationDirectory, 'index.yml'), `${indexFields.join('\n')}\n`, 'utf8'); + log(`Generated ${relative(destinationRoot, join(destinationDirectory, 'index.yml'))}`); +} + +function writeContentEngineSchema() { + const destinationPath = join(destinationRoot, 'schema.json'); + writeFileSync(destinationPath, `${JSON.stringify(contentEngineSchema, null, 2)}\n`, 'utf8'); + log(`Generated ${relative(destinationRoot, destinationPath)}`); +} + +function copyDirectory(sourcePath, destinationPath) { + if (!existsSync(sourcePath)) { + fail(`Required directory does not exist: ${relative(sourceRoot, sourcePath)}`); + } + + cpSync(sourcePath, destinationPath, { recursive: true }); + log(`Copied ${relative(sourceRoot, sourcePath)}/ -> ${relative(destinationRoot, destinationPath)}/`); +} + +function getChapterFolders() { + return readdirSync(sourceRoot) + .filter((entry) => /^0[0-7]-/.test(entry)) + .filter((entry) => statSync(join(sourceRoot, entry)).isDirectory()) + .sort(); +} + +function stripFragmentAndQuery(target) { + return target.split('#')[0].split('?')[0]; +} + +function isExternalLink(target) { + return /^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith('//') || target.startsWith('#'); +} + +function getChapterLocalMarkdownLinks(chapterPath) { + const readmePath = join(chapterPath, 'README.md'); + const readme = readFileSync(readmePath, 'utf8'); + const links = new Set(); + const patterns = [ + /\[[^\]]+\]\(([^)\s]+\.md(?:#[^)]+)?)(?:\s+"[^"]*")?\)/gi, + /]*\bhref=["']([^"']+\.md(?:#[^"']+)?)["']/gi, + ]; + + for (const pattern of patterns) { + for (const match of readme.matchAll(pattern)) { + const target = stripFragmentAndQuery(match[1]); + if (!target || isExternalLink(target)) { + continue; + } + + const resolvedTarget = resolve(chapterPath, target); + if (dirname(resolvedTarget) === resolve(chapterPath) && resolvedTarget !== resolve(readmePath)) { + links.add(resolvedTarget); + } + } + } + + return [...links].sort(); +} + +function copyAppendices() { + const sourceAppendices = join(sourceRoot, 'appendices'); + const destinationAppendices = join(destinationRoot, 'appendices'); + + if (!existsSync(sourceAppendices)) { + fail('Required appendices directory does not exist.'); + } + + writeIndexFromReadme(join(sourceAppendices, 'README.md'), destinationAppendices); + + for (const markdownFile of findMarkdownFiles(sourceAppendices)) { + copyFile(markdownFile, join(destinationAppendices, relative(sourceAppendices, markdownFile))); + } +} + +function copyCourseContent() { + console.log(`Overlaying course content into:\n${destinationRoot}\n`); + + mkdirSync(destinationRoot, { recursive: true }); + + copyFile(join(sourceRoot, 'README.md'), join(destinationRoot, 'README.md')); + writeContentEngineSchema(); + writeIndexFromReadme(join(sourceRoot, 'README.md'), destinationRoot, ['icon: CopilotIcon']); + copyDirectory(join(sourceRoot, 'assets'), join(destinationRoot, 'assets')); + + for (const chapterFolder of getChapterFolders()) { + const sourceChapter = join(sourceRoot, chapterFolder); + const destinationChapter = join(destinationRoot, chapterFolder); + + mkdirSync(destinationChapter, { recursive: true }); + copyFile(join(sourceChapter, 'README.md'), join(destinationChapter, 'README.md')); + writeIndexFromReadme(join(sourceChapter, 'README.md'), destinationChapter); + copyDirectory(join(sourceChapter, 'assets'), join(destinationChapter, 'assets')); + + for (const linkedMarkdown of getChapterLocalMarkdownLinks(sourceChapter)) { + copyFile(linkedMarkdown, join(destinationChapter, relative(sourceChapter, linkedMarkdown))); + } + } + + copyAppendices(); +} + +function findMarkdownFiles(directory) { + const files = []; + + for (const entry of readdirSync(directory)) { + const path = join(directory, entry); + const stat = statSync(path); + + if (stat.isDirectory()) { + files.push(...findMarkdownFiles(path)); + } else if (entry.endsWith('.md')) { + files.push(path); + } + } + + return files; +} + +function validateMarkdownImagePaths() { + const imagePatterns = [ + /!\[[^\]]*]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, + /]*\bsrc=["']([^"']+)["']/gi, + ]; + const brokenLinks = []; + + for (const markdownFile of findMarkdownFiles(destinationRoot)) { + const markdown = readFileSync(markdownFile, 'utf8'); + + for (const pattern of imagePatterns) { + for (const match of markdown.matchAll(pattern)) { + const target = stripFragmentAndQuery(match[1]); + if (!target || isExternalLink(target)) { + continue; + } + + const resolvedTarget = target.startsWith('/') + ? join(destinationRoot, target.slice(1)) + : resolve(dirname(markdownFile), target); + + if (!existsSync(resolvedTarget)) { + const line = markdown.slice(0, match.index).split('\n').length; + brokenLinks.push(`${relative(destinationRoot, markdownFile)}:${line} -> ${target}`); + } + } + } + } + + if (brokenLinks.length > 0) { + fail(`Broken copied Markdown image references:\n${brokenLinks.join('\n')}`); + } + + console.log('\nValidation passed: all copied Markdown image references resolve.'); +} + +function validateMarkdownFrontmatter() { + const requiredFields = contentEngineSchema.required ?? []; + const missingFrontmatter = []; + + for (const markdownFile of findMarkdownFiles(destinationRoot)) { + const markdown = readFileSync(markdownFile, 'utf8'); + const frontmatter = markdown.match(/^---\n([\s\S]*?)\n---\n/)?.[1]; + const relativePath = relative(destinationRoot, markdownFile); + + if (!frontmatter) { + missingFrontmatter.push(`${relativePath}: missing frontmatter`); + continue; + } + + const missingFields = requiredFields.filter( + (field) => !new RegExp(`^${field}:`, 'm').test(frontmatter), + ); + + if (missingFields.length > 0) { + missingFrontmatter.push(`${relativePath}: missing ${missingFields.join(', ')}`); + } + } + + if (missingFrontmatter.length > 0) { + fail(`Copied Markdown frontmatter does not match schema requirements:\n${missingFrontmatter.join('\n')}`); + } + + console.log('Validation passed: copied Markdown frontmatter includes required schema fields.'); +} + +ensureSafeDestination(); +resetDestination(); +copyCourseContent(); +validateMarkdownFrontmatter(); +validateMarkdownImagePaths(); +console.log('\nDone.'); diff --git a/.github/scripts/create-tapes.js b/.github/scripts/create-tapes.js index 66583962..95e0f69e 100644 --- a/.github/scripts/create-tapes.js +++ b/.github/scripts/create-tapes.js @@ -136,13 +136,13 @@ console.log('๐Ÿ“ Creating tape files from demos.json...\n'); let created = 0; for (const demo of config.demos) { - const imagesDir = join(rootDir, demo.chapter, 'images'); - const tapePath = join(imagesDir, `${demo.name}.tape`); + const assetsDir = join(rootDir, demo.chapter, 'assets'); + const tapePath = join(assetsDir, `${demo.name}.tape`); - // Ensure images directory exists - if (!existsSync(imagesDir)) { - mkdirSync(imagesDir, { recursive: true }); - console.log(` Created: ${demo.chapter}/images/`); + // Ensure assets directory exists + if (!existsSync(assetsDir)) { + mkdirSync(assetsDir, { recursive: true }); + console.log(` Created: ${demo.chapter}/assets/`); } // Generate tape content @@ -150,7 +150,7 @@ for (const demo of config.demos) { // Write tape file writeFileSync(tapePath, content); - console.log(` โœ“ ${demo.chapter}/images/${demo.name}.tape`); + console.log(` โœ“ ${demo.chapter}/assets/${demo.name}.tape`); created++; } 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/scripts/fix-translated-markdown.js b/.github/scripts/fix-translated-markdown.js new file mode 100644 index 00000000..34952f24 --- /dev/null +++ b/.github/scripts/fix-translated-markdown.js @@ -0,0 +1,396 @@ +#!/usr/bin/env node + +const fs = require("fs"); +const path = require("path"); + +const repoRoot = process.cwd(); +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/fix-translated-markdown.js "); + process.exit(1); +} + +const tableHeadersByLanguage = { + es: "| Capรญtulo | Tรญtulo | Lo que construirรกs |", + ko: "| ์žฅ | ์ œ๋ชฉ | ๋งŒ๋“ค ๋‚ด์šฉ |", + ja: "| ็ซ  | ใ‚ฟใ‚คใƒˆใƒซ | ไฝœๆˆใ™ใ‚‹ใ‚‚ใฎ |", + "zh-CN": "| ็ซ ่Š‚ | ๆ ‡้ข˜ | ไฝ ๅฐ†ๆž„ๅปบ็š„ๅ†…ๅฎน |", +}; + +const allFiles = new Set(walkFiles(repoRoot).map(toPosixRelative)); +const errors = []; +let changedFiles = 0; + +for (const language of languages) { + const translationRoot = path.join(repoRoot, "translations", language); + + if (!fs.existsSync(translationRoot)) { + console.log(`No translations found for ${language}; skipping.`); + continue; + } + + const markdownFiles = walkFiles(translationRoot).filter((file) => file.endsWith(".md")); + + for (const filePath of markdownFiles) { + const original = fs.readFileSync(filePath, "utf8"); + const fixed = fixMarkdown(original, filePath, language); + + if (fixed !== original) { + fs.writeFileSync(filePath, fixed); + changedFiles += 1; + console.log(`Fixed translated Markdown: ${toPosixRelative(filePath)}`); + } + + validateMarkdown(fixed, filePath, language); + } +} + +if (errors.length > 0) { + console.error("\nTranslated Markdown validation failed:"); + for (const error of errors) { + console.error(`- ${error}`); + } + process.exit(1); +} + +console.log(`Translated Markdown cleanup complete. Files changed: ${changedFiles}`); + +function fixMarkdown(content, filePath, language) { + let fixed = fixKnownTableHeaders(content, language); + const headingSlugs = getHeadingSlugs(fixed); + const headingSlugList = getHeadingSlugList(fixed); + const sourceFile = getSourceFileForTranslation(filePath, language); + + fixed = fixed.replace(/\[([^\]\n]+)\]\((#[^)]+)\)/g, (match, label, destination) => { + const currentSlug = destination.slice(1); + + if (headingSlugs.has(currentSlug)) { + return match; + } + + const labelSlug = slugifyHeading(label); + + if (headingSlugs.has(labelSlug)) { + return `[${label}](#${labelSlug})`; + } + + const candidates = [...headingSlugs].filter( + (slug) => slug.startsWith(`${labelSlug}-`) || labelSlug.startsWith(`${slug}-`), + ); + + if (candidates.length === 1) { + return `[${label}](#${candidates[0]})`; + } + + const sourceHeadingIndex = getSourceHeadingIndex(sourceFile, currentSlug); + + if (sourceHeadingIndex !== -1 && sourceHeadingIndex < headingSlugList.length) { + return `[${label}](#${headingSlugList[sourceHeadingIndex]})`; + } + + return match; + }); + + return fixed.replace(/(\]\()([^)]+)(\))/g, (_match, prefix, destination, suffix) => { + return `${prefix}${fixDestination(destination, filePath, language)}${suffix}`; + }); +} + +function fixKnownTableHeaders(content, language) { + const translatedHeader = tableHeadersByLanguage[language]; + + if (!translatedHeader) { + return content; + } + + return content.replace( + /^\|\s*Chapter\s*\|\s*Title\s*\|\s*What You'll Build\s*\|$/gm, + translatedHeader, + ); +} + +function fixDestination(destination, filePath, language) { + if (isExternal(destination) || destination.startsWith("#") || destination.startsWith("<")) { + return destination; + } + + const { target, fragment } = splitDestination(destination); + + if (!target || target.startsWith("#")) { + return destination; + } + + const translatedDir = path.posix.dirname(toPosixRelative(filePath)); + const currentTarget = normalizePosix(path.posix.join(translatedDir, target)); + const translatedRoot = `translations/${language}`; + const sourceRelativeFile = path.posix.relative(translatedRoot, toPosixRelative(filePath)); + const sourceDir = path.posix.dirname(sourceRelativeFile); + const sourceTarget = normalizePosix(path.posix.join(sourceDir, target)); + + if (pathExists(currentTarget)) { + return `${target}${fixCrossFileFragment(fragment, sourceTarget, currentTarget)}`; + } + + if (!pathExists(sourceTarget)) { + return destination; + } + + const translatedTarget = normalizePosix(path.posix.join(translatedRoot, sourceTarget)); + const preferredTarget = pathExists(translatedTarget) ? translatedTarget : sourceTarget; + const relativeTarget = path.posix.relative(translatedDir, preferredTarget) || "."; + const normalizedTarget = relativeTarget.startsWith(".") ? relativeTarget : `./${relativeTarget}`; + + return `${normalizedTarget}${fixCrossFileFragment(fragment, sourceTarget, preferredTarget)}`; +} + +function validateMarkdown(content, filePath, language) { + const fileRelative = toPosixRelative(filePath); + const fileDir = path.posix.dirname(fileRelative); + const headingSlugs = getHeadingSlugs(content); + const sourceFile = getSourceFileForTranslation(filePath, language); + + for (const destination of getDestinations(content)) { + if (isExternal(destination) || destination.startsWith("<")) { + continue; + } + + const { target, fragment } = splitDestination(destination); + + if (!target) { + const slug = fragment.slice(1); + + if ( + slug && + !headingSlugs.has(slug) && + getSourceHeadingIndex(sourceFile, slug) !== -1 + ) { + errors.push(`${fileRelative} links to missing heading #${slug}`); + } + + continue; + } + + const resolvedTarget = normalizePosix(path.posix.join(fileDir, target)); + + if (!pathExists(resolvedTarget)) { + const sourceTarget = getSourceTargetForDestination(filePath, language, target); + + if (sourceTarget && !pathExists(sourceTarget)) { + continue; + } + + errors.push(`${fileRelative} links to missing file ${destination}`); + continue; + } + + // Cross-file heading anchors are best-effort fixed above by mapping source + // heading order to translated heading order. Avoid failing on anchors that + // come from generated HTML IDs or pre-existing source content. + } +} + +function getDestinations(content) { + const destinations = []; + const pattern = /\]\(([^)]+)\)/g; + let match; + + while ((match = pattern.exec(content)) !== null) { + destinations.push(match[1]); + } + + return destinations; +} + +function getHeadingSlugs(content) { + return new Set(getHeadingSlugList(content)); +} + +function getHeadingSlugList(content) { + return getHeadingSlugListWith(content, slugifyHeading); +} + +function getSourceHeadingIndex(sourceFile, slug) { + if (!sourceFile || !pathExists(sourceFile)) { + return -1; + } + + const sourceContent = fs.readFileSync(path.join(repoRoot, sourceFile), "utf8"); + const slugLists = [ + getHeadingSlugListWith(sourceContent, slugifyHeading), + getHeadingSlugListWith(sourceContent, slugifyGitHubHeading), + ]; + + for (const slugs of slugLists) { + const index = slugs.indexOf(slug); + + if (index !== -1) { + return index; + } + } + + return -1; +} + +function getHeadingSlugListWith(content, slugifier) { + const slugs = new Map(); + const result = []; + + for (const line of content.split(/\r?\n/)) { + const match = /^(#{1,6})\s+(.+?)\s*$/.exec(line); + + if (!match) { + continue; + } + + const baseSlug = slugifier(match[2]); + const count = slugs.get(baseSlug) || 0; + const slug = count === 0 ? baseSlug : `${baseSlug}-${count}`; + slugs.set(baseSlug, count + 1); + result.push(slug); + } + + return result; +} + +function fixCrossFileFragment(fragment, sourceTarget, translatedTarget) { + if (!fragment || !sourceTarget.endsWith(".md") || !translatedTarget.endsWith(".md")) { + return fragment; + } + + if (!pathExists(sourceTarget) || !pathExists(translatedTarget)) { + return fragment; + } + + const currentSlug = fragment.slice(1); + const translatedContent = fs.readFileSync(path.join(repoRoot, translatedTarget), "utf8"); + const translatedSlugs = getHeadingSlugList(translatedContent); + + if (translatedSlugs.includes(currentSlug)) { + return fragment; + } + + const sourceContent = fs.readFileSync(path.join(repoRoot, sourceTarget), "utf8"); + const sourceSlugs = getHeadingSlugList(sourceContent); + const sourceIndex = sourceSlugs.indexOf(currentSlug); + + if (sourceIndex === -1 || sourceIndex >= translatedSlugs.length) { + return fragment; + } + + return `#${translatedSlugs[sourceIndex]}`; +} + +function getSourceFileForTranslation(filePath, language) { + const translatedRoot = `translations/${language}`; + const sourceFile = path.posix.relative(translatedRoot, toPosixRelative(filePath)); + + if (sourceFile.startsWith("..")) { + return ""; + } + + return sourceFile; +} + +function getSourceTargetForDestination(filePath, language, target) { + const sourceFile = getSourceFileForTranslation(filePath, language); + + if (!sourceFile) { + return ""; + } + + const sourceDir = path.posix.dirname(sourceFile); + return normalizePosix(path.posix.join(sourceDir, target)); +} + +function slugifyHeading(value) { + return value + .replace(/<[^>]+>/g, "") + .replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1") + .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") + .replace(/[`*_~]/g, "") + .trim() + .toLocaleLowerCase() + .replace(/[^\p{Letter}\p{Number}\p{Mark}\s-]/gu, "") + .trim() + .replace(/\s+/g, "-"); +} + +function slugifyGitHubHeading(value) { + return value + .replace(/<[^>]+>/g, "") + .replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1") + .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") + .replace(/[`*_~]/g, "") + .trim() + .toLocaleLowerCase() + .replace(/[^\p{Letter}\p{Number}\p{Mark}\s-]/gu, "") + .replace(/\s/g, "-"); +} + +function splitDestination(destination) { + const hashIndex = destination.indexOf("#"); + + if (hashIndex === -1) { + return { target: destination, fragment: "" }; + } + + return { + target: destination.slice(0, hashIndex), + fragment: destination.slice(hashIndex), + }; +} + +function isExternal(destination) { + return /^(https?:|mailto:|tel:)/i.test(destination); +} + +function pathExists(relativePath) { + if (allFiles.has(relativePath)) { + return true; + } + + if (fs.existsSync(path.join(repoRoot, relativePath))) { + return true; + } + + const indexPath = normalizePosix(path.posix.join(relativePath, "README.md")); + return allFiles.has(indexPath); +} + +function walkFiles(directory) { + if (!fs.existsSync(directory)) { + return []; + } + + const entries = fs.readdirSync(directory, { withFileTypes: true }); + const files = []; + + for (const entry of entries) { + if (entry.name === ".git" || entry.name === "node_modules") { + continue; + } + + const entryPath = path.join(directory, entry.name); + + if (entry.isDirectory()) { + files.push(...walkFiles(entryPath)); + } else if (entry.isFile()) { + files.push(entryPath); + } + } + + return files; +} + +function toPosixRelative(filePath) { + return normalizePosix(path.relative(repoRoot, filePath)); +} + +function normalizePosix(value) { + return value.split(path.sep).join(path.posix.sep).replace(/\\/g, "/"); +} diff --git a/.github/scripts/generate-chapter-headers.py b/.github/scripts/generate-chapter-headers.py index 5c44a927..0520f6be 100644 --- a/.github/scripts/generate-chapter-headers.py +++ b/.github/scripts/generate-chapter-headers.py @@ -23,7 +23,7 @@ # Get project root (parent of scripts folder) SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) PROJECT_ROOT = os.path.dirname(os.path.dirname(SCRIPT_DIR)) -BACKGROUND_IMAGE = os.path.join(PROJECT_ROOT, "images", "chapter-header-bg.png") +BACKGROUND_IMAGE = os.path.join(PROJECT_ROOT, "assets", "chapter-header-bg.png") # Font settings - 25% larger than original 36px FONT_SIZE = 45 @@ -112,8 +112,8 @@ def generate_header(chapter_folder, title, font): y = (height - text_height) // 2 draw.text((x, y), title, fill=(255, 255, 255), font=font) - # Save to chapter's images folder - output_dir = os.path.join(PROJECT_ROOT, chapter_folder, "images") + # Save to chapter's assets folder + output_dir = os.path.join(PROJECT_ROOT, chapter_folder, "assets") os.makedirs(output_dir, exist_ok=True) output_path = os.path.join(output_dir, "chapter-header.png") diff --git a/.github/scripts/generate-demos.js b/.github/scripts/generate-demos.js index 09076750..457ef558 100644 --- a/.github/scripts/generate-demos.js +++ b/.github/scripts/generate-demos.js @@ -2,7 +2,7 @@ /** * Generate course demo GIFs from .tape files * - * This script finds all .tape files in [chapter]/images/ folders and runs VHS + * This script finds all .tape files in [chapter]/assets/ folders and runs VHS * to generate GIFs. VHS is run from the project root so that @file references * in prompts resolve correctly. * @@ -135,7 +135,7 @@ function cleanupCopilotWrapper() { try { rmSync(wrapperDir, { recursive: true }); } catch (e) { /* ignore */ } } -// Find all .tape files in [chapter]/images/ folders +// Find all .tape files in [chapter]/assets/ folders function findTapeFiles(dir, chapterFilter) { const tapeFiles = []; @@ -151,17 +151,17 @@ function findTapeFiles(dir, chapterFilter) { if (!matches) continue; } - const imagesDir = join(fullPath, 'images'); - if (existsSync(imagesDir)) { + const assetsDir = join(fullPath, 'assets'); + if (existsSync(assetsDir)) { try { - const imagesEntries = readdirSync(imagesDir); - for (const file of imagesEntries) { + const assetsEntries = readdirSync(assetsDir); + for (const file of assetsEntries) { if (file.endsWith('.tape')) { - tapeFiles.push(join(imagesDir, file)); + tapeFiles.push(join(assetsDir, file)); } } } catch (e) { - // Can't read images folder, skip + // Can't read assets folder, skip } } } diff --git a/.github/scripts/preview-gifs.js b/.github/scripts/preview-gifs.js index f3fec522..0c095d1e 100644 --- a/.github/scripts/preview-gifs.js +++ b/.github/scripts/preview-gifs.js @@ -32,11 +32,11 @@ function findGifs() { const gifs = []; for (const entry of readdirSync(rootDir)) { if (!/^\d{2}-/.test(entry)) continue; - const imagesDir = join(rootDir, entry, 'images'); - if (!existsSync(imagesDir)) continue; - for (const file of readdirSync(imagesDir)) { + const assetsDir = join(rootDir, entry, 'assets'); + if (!existsSync(assetsDir)) continue; + for (const file of readdirSync(assetsDir)) { if (file.endsWith('-demo.gif')) { - gifs.push({ path: join(imagesDir, file), chapter: entry }); + gifs.push({ path: join(assetsDir, file), chapter: entry }); } } } diff --git a/.github/scripts/verify-gifs.js b/.github/scripts/verify-gifs.js index 93cd1c17..04c94aa0 100644 --- a/.github/scripts/verify-gifs.js +++ b/.github/scripts/verify-gifs.js @@ -41,11 +41,11 @@ function findGifs(dir) { const fullPath = join(dir, entry); const stat = statSync(fullPath); if (stat.isDirectory() && !entry.startsWith('.') && entry !== 'node_modules') { - const imagesDir = join(fullPath, 'images'); - if (existsSync(imagesDir)) { - for (const file of readdirSync(imagesDir)) { + const assetsDir = join(fullPath, 'assets'); + if (existsSync(assetsDir)) { + for (const file of readdirSync(assetsDir)) { if (file.endsWith('-demo.gif')) { - gifs.push(join(imagesDir, file)); + gifs.push(join(assetsDir, file)); } } } diff --git a/.github/uvs.csv b/.github/uvs.csv new file mode 100644 index 00000000..bdcfb483 --- /dev/null +++ b/.github/uvs.csv @@ -0,0 +1,132 @@ +"03/03",197 +"03/04",189 +"03/05",349 +"03/06",359 +"03/07",157 +"03/08",133 +"03/09",1173 +"03/10",2805 +"03/11",4606 +"03/12",2541 +"03/13",2529 +"03/14",974 +"03/15",1034 +"03/16",1984 +"03/17",1684 +"03/18",1390 +"03/19",1499 +"03/20",1789 +"03/21",786 +"03/22",646 +"03/23",1464 +"03/24",1282 +"03/25",1231 +"03/26",1599 +"03/27",1543 +"03/28",878 +"03/29",869 +"03/30",1384 +"03/31",1406 +"04/01",1406 +"04/02",4422 +"04/03",4110 +"04/04",4751 +"04/05",2716 +"04/06",2941 +"04/07",2861 +"04/08",2415 +"04/09",2479 +"04/10",2128 +"04/11",1016 +"04/12",907 +"04/13",1823 +"04/14",1957 +"04/15",2144 +"04/16",1864 +"04/17",1818 +"04/18",772 +"04/19",769 +"04/20",1661 +"04/21",1835 +"04/22",1791 +"04/23",1547 +"04/24",1584 +"04/25",624 +"04/26",695 +"04/27",1277 +"04/28",1155 +"04/29",1196 +"04/30",1191 +"05/01",738 +"05/02",529 +"05/03",571 +"05/04",1171 +"05/05",982 +"05/06",934 +"05/07",996 +"05/08",1034 +"05/09",455 +"05/10",481 +"05/11",923 +"05/12",728 +"05/13",816 +"05/14",803 +"05/15",754 +"05/16",464 +"05/17",440 +"05/18",840 +"05/19",744 +"05/20",791 +"05/21",806 +"05/22",834 +"05/23",373 +"05/24",418 +"05/25",801 +"05/26",778 +"05/27",907 +"05/28",775 +"05/29",731 +"05/30",329 +"05/31",352 +"06/01",681 +"06/02",521 +"06/03",613 +"06/04",516 +"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 diff --git a/.github/views.csv b/.github/views.csv new file mode 100644 index 00000000..16cf0161 --- /dev/null +++ b/.github/views.csv @@ -0,0 +1,131 @@ +"03/04",440 +"03/05",882 +"03/06",901 +"03/07",408 +"03/08",508 +"03/09",2359 +"03/10",6105 +"03/11",10619 +"03/12",5823 +"03/13",5712 +"03/14",2333 +"03/15",2457 +"03/16",4647 +"03/17",4207 +"03/18",3387 +"03/19",3657 +"03/20",4366 +"03/21",2028 +"03/22",1882 +"03/23",3847 +"03/24",3178 +"03/25",3212 +"03/26",4008 +"03/27",3695 +"03/28",2022 +"03/29",2197 +"03/30",3458 +"03/31",3520 +"04/01",3644 +"04/02",8911 +"04/03",8218 +"04/04",9957 +"04/05",5559 +"04/06",6377 +"04/07",6169 +"04/08",5478 +"04/09",5775 +"04/10",4931 +"04/11",2340 +"04/12",2239 +"04/13",4430 +"04/14",4371 +"04/15",4911 +"04/16",4418 +"04/17",4574 +"04/18",1904 +"04/19",2011 +"04/20",3775 +"04/21",4253 +"04/22",4086 +"04/23",3523 +"04/24",3727 +"04/25",1451 +"04/26",1663 +"04/27",2641 +"04/28",2282 +"04/29",2634 +"04/30",2869 +"05/01",2013 +"05/02",1317 +"05/03",1336 +"05/04",2124 +"05/05",2018 +"05/06",1906 +"05/07",2121 +"05/08",2393 +"05/09",1097 +"05/10",1003 +"05/11",1832 +"05/12",1461 +"05/13",1710 +"05/14",1721 +"05/15",1538 +"05/16",1138 +"05/17",1093 +"05/18",1751 +"05/19",1447 +"05/20",1494 +"05/21",1654 +"05/22",1823 +"05/23",1013 +"05/24",1111 +"05/25",1902 +"05/26",1634 +"05/27",1980 +"05/28",1565 +"05/29",1534 +"05/30",840 +"05/31",892 +"06/01",1430 +"06/02",1007 +"06/03",1271 +"06/04",1065 +"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 diff --git a/.github/workflows/co-op-translator.yml b/.github/workflows/co-op-translator.yml new file mode 100644 index 00000000..a06a9c38 --- /dev/null +++ b/.github/workflows/co-op-translator.yml @@ -0,0 +1,126 @@ +name: Co-op Translator + +on: + workflow_dispatch: + push: + branches: + - main + paths: + - "**/*.md" + - "**/*.png" + - "**/*.jpg" + - "**/*.jpeg" + - "**/*.webp" + - "!translations/**" + - "!translated_images/**" + - "!.github/**" + - "!samples/**" + +permissions: + contents: write + pull-requests: write + +jobs: + translate-content: + name: Translate Markdown and Images + runs-on: ubuntu-latest + timeout-minutes: 90 + + env: + TRANSLATION_LANGUAGES: "es" + PYTHONIOENCODING: utf-8 + AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }} + AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }} + AZURE_OPENAI_MODEL_NAME: ${{ secrets.AZURE_OPENAI_MODEL_NAME }} + AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_CHAT_DEPLOYMENT_NAME }} + AZURE_OPENAI_API_VERSION: ${{ secrets.AZURE_OPENAI_API_VERSION }} + AZURE_AI_SERVICE_API_KEY: ${{ secrets.AZURE_AI_SERVICE_API_KEY }} + AZURE_AI_SERVICE_ENDPOINT: ${{ secrets.AZURE_AI_SERVICE_ENDPOINT }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + OPENAI_ORG_ID: ${{ secrets.OPENAI_ORG_ID }} + OPENAI_CHAT_MODEL_ID: ${{ secrets.OPENAI_CHAT_MODEL_ID }} + OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }} + + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Install Co-op Translator + run: | + python -m pip install --upgrade pip + python -m pip install co-op-translator + + - name: Translate Markdown + run: | + for attempt in 1 2 3; do + if translate -l "$TRANSLATION_LANGUAGES" -md -y -s --repo-url "https://github.com/github/copilot-cli-for-beginners.git"; then + exit 0 + fi + echo "Markdown translation attempt $attempt failed." + if [ "$attempt" -lt 3 ]; then + sleep 30 + fi + done + exit 1 + + - name: Translate images + run: | + for attempt in 1 2 3; do + if translate -l "$TRANSLATION_LANGUAGES" -img -y -s --repo-url "https://github.com/github/copilot-cli-for-beginners.git"; then + exit 0 + fi + echo "Image translation attempt $attempt failed." + if [ "$attempt" -lt 3 ]; then + sleep 30 + fi + done + exit 1 + + - name: Normalize and review translations + run: | + migrate-links -l "$TRANSLATION_LANGUAGES" -y + node .github/scripts/fix-translated-markdown.js "$TRANSLATION_LANGUAGES" + 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" \ + "translated_images/$language/.github" \ + "translated_images/$language/samples" + done + + - name: Upload Co-op Translator logs + if: always() + 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@v8 + with: + token: ${{ secrets.GH_AW_GITHUB_TOKEN }} + commit-message: "Update translations via Co-op Translator" + title: "Update translations via Co-op Translator" + body: | + This PR updates Markdown and image translations generated by Co-op Translator. + + Generated content is available in the `translations/` and `translated_images/` directories. + branch: update-translations + base: main + labels: translation, automated-pr + delete-branch: true + add-paths: | + translations/ + translated_images/ diff --git a/.github/workflows/course-updater.lock.yml b/.github/workflows/course-updater.lock.yml new file mode 100644 index 00000000..72d7a585 --- /dev/null +++ b/.github/workflows/course-updater.lock.yml @@ -0,0 +1,1215 @@ +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"7b67212e2086070d4622e72ea2514453d5a2ba0093a2ba686ecb4ba55a530f59","compiler_version":"v0.68.1","strict":true,"agent_id":"copilot"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9"},{"repo":"actions/upload-artifact","sha":"bbbca2ddaa5d8feaa63e36b76fdaad77386f024f","version":"v7"},{"repo":"github/gh-aw-actions/setup","sha":"2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc","version":"v0.68.1"}]} +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.68.1). DO NOT EDIT. +# +# To update this file, edit the corresponding .md file and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# Weekly check (Mondays) for new GitHub Copilot CLI features and updates. Opens a PR if the course content needs updating. +# +# Secrets used: +# - COPILOT_GITHUB_TOKEN +# - GH_AW_CI_TRIGGER_TOKEN +# - GH_AW_GITHUB_MCP_SERVER_TOKEN +# - GH_AW_GITHUB_TOKEN +# - GITHUB_TOKEN +# +# Custom actions used: +# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 +# - actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 +# - github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + +name: "Course Updater" +"on": + schedule: + - cron: "38 11 * * 1" + # Friendly format: weekly on monday (scattered) + workflow_dispatch: + inputs: + aw_context: + default: "" + description: Agent caller context (used internally by Agentic Workflows). + required: false + type: string + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}" + +run-name: "Course Updater" + +jobs: + activation: + runs-on: ubuntu-slim + permissions: + actions: read + contents: read + outputs: + comment_id: "" + comment_repo: "" + lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} + model: ${{ steps.generate_aw_info.outputs.model }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + - name: Generate agentic run info + id: generate_aw_info + env: + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'auto' }} + GH_AW_INFO_VERSION: "1.0.21" + GH_AW_INFO_AGENT_VERSION: "1.0.21" + GH_AW_INFO_CLI_VERSION: "v0.68.1" + GH_AW_INFO_WORKFLOW_NAME: "Course Updater" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.25.18" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_COMPILED_STRICT: "true" + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + await main(core, context); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + sparse-checkout: | + .github + .agents + sparse-checkout-cone-mode: true + fetch-depth: 1 + - name: Check workflow lock file + id: check-lock-file + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_WORKFLOW_FILE: "course-updater.lock.yml" + GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Check compile-agentic version + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_COMPILED_VERSION: "v0.68.1" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + # poutine:ignore untrusted_checkout_exec + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" + { + cat << 'GH_AW_PROMPT_da18aaba3a78fe8f_EOF' + + GH_AW_PROMPT_da18aaba3a78fe8f_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" + cat << 'GH_AW_PROMPT_da18aaba3a78fe8f_EOF' + + Tools: create_pull_request, missing_tool, missing_data, noop + GH_AW_PROMPT_da18aaba3a78fe8f_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" + cat << 'GH_AW_PROMPT_da18aaba3a78fe8f_EOF' + + + The following GitHub context information is available for this workflow: + {{#if __GH_AW_GITHUB_ACTOR__ }} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if __GH_AW_GITHUB_REPOSITORY__ }} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if __GH_AW_GITHUB_WORKSPACE__ }} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} + - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} + - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} + - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} + - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{/if}} + {{#if __GH_AW_GITHUB_RUN_ID__ }} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + GH_AW_PROMPT_da18aaba3a78fe8f_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_da18aaba3a78fe8f_EOF' + + {{#runtime-import .github/workflows/course-updater.md}} + GH_AW_PROMPT_da18aaba3a78fe8f_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Upload activation artifact + if: success() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: activation + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/github_rate_limits.jsonl + if-no-files-found: ignore + retention-days: 1 + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: + contents: read + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: 0 + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_WORKFLOW_ID_SANITIZED: courseupdater + outputs: + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + inference_access_error: ${{ steps.detect-inference-error.outputs.inference_access_error || 'false' }} + model: ${{ needs.activation.outputs.model }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + - name: Set runtime paths + id: set-runtime-paths + run: | + echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" >> "$GITHUB_OUTPUT" + echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" >> "$GITHUB_OUTPUT" + echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" >> "$GITHUB_OUTPUT" + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Create gh-aw temp directory + run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" + - name: Configure gh CLI for GitHub Enterprise + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" + env: + GH_TOKEN: ${{ github.token }} + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request || github.event.issue.pull_request + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.21 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.18 + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + with: + script: | + const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.18 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.18 ghcr.io/github/gh-aw-firewall/squid:0.25.18 ghcr.io/github/gh-aw-mcpg:v0.2.17 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine + - name: Write Safe Outputs Config + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_8267a15d6774039b_EOF' + {"create_pull_request":{"base_branch":"main","labels":["automated-update","copilot-cli-updates"],"max":1,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_path_prefixes":[".github/",".agents/"],"title_prefix":"[bot] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_8267a15d6774039b_EOF + - name: Write Safe Outputs Tools + env: + GH_AW_TOOLS_META_JSON: | + { + "description_suffixes": { + "create_pull_request": " CONSTRAINTS: Maximum 1 pull request(s) can be created. Title will be prefixed with \"[bot] \". Labels [\"automated-update\" \"copilot-cli-updates\"] will be automatically added." + }, + "repo_params": {}, + "dynamic_tools": [] + } + GH_AW_VALIDATION_JSON: | + { + "create_pull_request": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "branch": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "draft": { + "type": "boolean" + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "missing_data": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "context": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "data_type": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "reason": { + "type": "string", + "sanitize": true, + "maxLength": 256 + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + } + } + }, + "report_incomplete": { + "defaultMax": 5, + "fields": { + "details": { + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 1024 + } + } + } + } + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + await main(); + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing vulnerabilities + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT=3001 + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" + + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} + GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p /tmp/gh-aw/mcp-config + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export DEBUG="*" + + export GH_AW_ENGINE="copilot" + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.17' + + mkdir -p /home/runner/.copilot + cat << GH_AW_MCP_CONFIG_1fee4f046759e415_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v0.32.0", + "env": { + "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "repos" + }, + "guard-policies": { + "allow-only": { + "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", + "repos": "$GITHUB_MCP_GUARD_REPOS" + } + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "*" + ] + } + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_1fee4f046759e415_EOF + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Clean git credentials + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + # --allow-tool github + # --allow-tool safeoutputs + # --allow-tool shell(cat) + # --allow-tool shell(curl:*) + # --allow-tool shell(date) + # --allow-tool shell(echo) + # --allow-tool shell(gh:*) + # --allow-tool shell(git add:*) + # --allow-tool shell(git branch:*) + # --allow-tool shell(git checkout:*) + # --allow-tool shell(git commit:*) + # --allow-tool shell(git merge:*) + # --allow-tool shell(git rm:*) + # --allow-tool shell(git status) + # --allow-tool shell(git switch:*) + # --allow-tool shell(grep) + # --allow-tool shell(head) + # --allow-tool shell(ls) + # --allow-tool shell(pwd) + # --allow-tool shell(sort) + # --allow-tool shell(tail) + # --allow-tool shell(uniq) + # --allow-tool shell(wc) + # --allow-tool shell(yq) + # --allow-tool web_fetch + # --allow-tool write + timeout-minutes: 20 + run: | + set -o pipefail + touch /tmp/gh-aw/agent-step-summary.md + (umask 177 && touch /tmp/gh-aw/agent-stdio.log) + # shellcheck disable=SC1003 + sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.18 --skip-pull --enable-api-proxy \ + -- /bin/bash -c 'node ${RUNNER_TEMP}/gh-aw/actions/copilot_driver.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-tool github --allow-tool safeoutputs --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(curl:*)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(gh:*)'\'' --allow-tool '\''shell(git add:*)'\'' --allow-tool '\''shell(git branch:*)'\'' --allow-tool '\''shell(git checkout:*)'\'' --allow-tool '\''shell(git commit:*)'\'' --allow-tool '\''shell(git merge:*)'\'' --allow-tool '\''shell(git rm:*)'\'' --allow-tool '\''shell(git status)'\'' --allow-tool '\''shell(git switch:*)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool web_fetch --allow-tool write --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_PHASE: agent + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_VERSION: v0.68.1 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Detect inference access error + id: detect-inference-error + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/detect_inference_access_error.sh" + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh" + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Append agent step summary + if: always() + run: bash "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh" + - name: Copy Safe Outputs + if: always() + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + run: | + mkdir -p /tmp/gh-aw + cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true + - name: Ingest agent output + id: collect_output + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,localhost,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + id: parse-mcp-gateway + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + - name: Parse token usage for step summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); + - name: Write agent output placeholder if missing + if: always() + run: | + if [ ! -f /tmp/gh-aw/agent_output.json ]; then + echo '{"items":[]}' > /tmp/gh-aw/agent_output.json + fi + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: agent + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/agent_usage.json + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/agent/ + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/safeoutputs.jsonl + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/aw-*.patch + /tmp/gh-aw/aw-*.bundle + if-no-files-found: ignore + - name: Upload firewall audit logs + if: always() + continue-on-error: true + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: firewall-audit-logs + path: | + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/sandbox/firewall/audit/ + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - safe_outputs + if: > + always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || + needs.activation.outputs.stale_lock_file_failed == 'true') + runs-on: ubuntu-slim + permissions: + contents: write + issues: write + pull-requests: write + concurrency: + group: "gh-aw-conclusion-course-updater" + cancel-in-progress: false + outputs: + incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Process No-Op Messages + id: noop + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: "1" + GH_AW_WORKFLOW_NAME: "Course Updater" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_REPORT_AS_ISSUE: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + await main(); + - name: Record missing tool + id: missing_tool + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_MISSING_TOOL_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "Course Updater" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Record incomplete + id: report_incomplete + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "Course Updater" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + await main(); + - name: Handle agent failure + id: handle_agent_failure + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Course Updater" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "course-updater" + GH_AW_ENGINE_ID: "copilot" + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} + GH_AW_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }} + GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} + GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_GROUP_REPORTS: "false" + GH_AW_FAILURE_REPORT_AS_ISSUE: "true" + GH_AW_TIMEOUT_MINUTES: "20" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + + detection: + needs: + - activation + - agent + if: > + always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} + detection_success: ${{ steps.detection_conclusion.outputs.success }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Checkout repository for patch context + if: needs.agent.outputs.has_patch == 'true' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + # --- Threat Detection --- + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.18 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.18 ghcr.io/github/gh-aw-firewall/squid:0.25.18 + - name: Check if detection needed + id: detection_guard + if: always() + env: + OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + run: | + if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then + echo "run_detection=true" >> "$GITHUB_OUTPUT" + echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" + else + echo "run_detection=false" >> "$GITHUB_OUTPUT" + echo "Detection skipped: no agent outputs or patches to analyze" + fi + - name: Clear MCP configuration for detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + rm -f /tmp/gh-aw/mcp-config/mcp-servers.json + rm -f /home/runner/.copilot/mcp-config.json + rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" + - name: Prepare threat detection files + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true + cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true + for f in /tmp/gh-aw/aw-*.patch; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + for f in /tmp/gh-aw/aw-*.bundle; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + echo "Prepared threat detection files:" + ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + - name: Setup threat detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + WORKFLOW_NAME: "Course Updater" + WORKFLOW_DESCRIPTION: "Weekly check (Mondays) for new GitHub Copilot CLI features and updates. Opens a PR if the course content needs updating." + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.21 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.18 + - name: Execute GitHub Copilot CLI + if: always() && steps.detection_guard.outputs.run_detection == 'true' + id: detection_agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 20 + run: | + set -o pipefail + touch /tmp/gh-aw/agent-step-summary.md + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + # shellcheck disable=SC1003 + sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,github.com,host.docker.internal,telemetry.enterprise.githubcopilot.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.18 --skip-pull --enable-api-proxy \ + -- /bin/bash -c 'node ${RUNNER_TEMP}/gh-aw/actions/copilot_driver.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} + GH_AW_PHASE: detection + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_VERSION: v0.68.1 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Upload threat detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: detection + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + - name: Parse and conclude threat detection + id: detection_conclusion + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + + safe_outputs: + needs: + - activation + - agent + - detection + if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' + runs-on: ubuntu-slim + permissions: + contents: write + issues: write + pull-requests: write + timeout-minutes: 15 + env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/course-updater" + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} + GH_AW_ENGINE_ID: "copilot" + GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_WORKFLOW_ID: "course-updater" + GH_AW_WORKFLOW_NAME: "Course Updater" + outputs: + code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} + code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + created_pr_number: ${{ steps.process_safe_outputs.outputs.created_pr_number }} + created_pr_url: ${{ steps.process_safe_outputs.outputs.created_pr_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Download patch artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Checkout repository + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: main + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + persist-credentials: false + fetch-depth: 1 + - name: Configure Git credentials + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GIT_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,localhost,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"base_branch\":\"main\",\"labels\":[\"automated-update\",\"copilot-cli-updates\"],\"max\":1,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"title_prefix\":\"[bot] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + - name: Upload Safe Outputs Items + if: always() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: safe-outputs-items + path: /tmp/gh-aw/safe-output-items.jsonl + if-no-files-found: ignore + diff --git a/.github/workflows/course-updater.md b/.github/workflows/course-updater.md new file mode 100644 index 00000000..ef418b82 --- /dev/null +++ b/.github/workflows/course-updater.md @@ -0,0 +1,70 @@ +--- +name: "Course Updater" +description: "Weekly check (Mondays) for new GitHub Copilot CLI features and updates. Opens a PR if the course content needs updating." +on: + schedule: weekly on monday + workflow_dispatch: +tools: + bash: ["curl", "gh"] + edit: + web-fetch: + github: + toolsets: [repos] +safe-outputs: + allowed-domains: + - github.com + create-pull-request: + labels: [automated-update, copilot-cli-updates] + title-prefix: "[bot] " + base-branch: main +--- + +# Check for Copilot CLI Updates + +You are a documentation maintainer for the Copilot CLI for Beginners repository. Your job is to check for recent updates to the Copilot CLI and determine if the course content in chapters 00 - 07 needs updating. + +## Step 1 โ€” Gather recent Copilot CLI updates + +Use `web-fetch` to read the following pages and extract the latest entries from the past 7 days: + +- https://github.com/github/copilot-cli/blob/main/changelog.md โ€” CLI changelog + +Also use `gh` CLI to check the latest releases and commits in the `github/copilot-cli` repo. + +Look for: + +- New features or capabilities (e.g., new commands, tools, integrations) +- Significant changes to existing features (renames, deprecations) +- New customization options (e.g. instructions, agents, skills, MCP, plugins) + +## Step 2 โ€” Check for existing open PRs to avoid duplicates + +Before doing any content comparison, list all open pull requests in this repo that have the `automated-update` or `copilot-cli-updates` labels. Read their titles and descriptions to understand which features or changes each PR already covers. Build a list of features that are **already addressed** by existing PRs โ€” you must exclude those features from any updates you propose later. If every feature you found in Step 1 is already covered by an open PR, stop here and report that no new updates are needed. + +## Step 3 โ€” Compare against the current course content + +This course targets beginners, so only include content changes that cater to that audience. For example, if a new feature is advanced, marked as experimental, or otherwise doesn't qualify as a "beginner" level feature, don't include it in the course content since we don't want to overwhelm learners. Determine what is most relevant and helpful for beginners learning about the Copilot CLI. + +Read all of the readme files in the repo and compare the features documented there against what you found in Step 1. +Identify: + +- **Missing features** โ€” new capabilities not yet documented +- **Outdated information** โ€” features that have been renamed, deprecated, or significantly changed + +If there is nothing new or everything is already up to date, stop here and report that no updates are needed. + +## Step 4 โ€” Update the course content + +If updates are needed, make a decision on which chapter(s) need to be updated. + +If the new information can be added to existing chapter(s), edit those chapters to include refinements, new sections, or updated information as needed. Remember that this course targets beginners, so ensure that any new content is explained clearly and simply, with examples if possible. + +## Step 5 โ€” Open a pull request + +Create a pull request with your changes, using the `main` branch as the base branch. The PR title should summarize what was updated (e.g., "Add /plan command documentation"). The PR body should list: + +1. What new features or changes were found +2. What sections of the course were updated +3. Links to the source announcements + +The PR should target the `main` branch and include the labels `automated-update` and `copilot-cli-updates`. \ No newline at end of file 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/traffic-updater.lock.yml b/.github/workflows/traffic-updater.lock.yml new file mode 100644 index 00000000..f8414bbd --- /dev/null +++ b/.github/workflows/traffic-updater.lock.yml @@ -0,0 +1,1332 @@ +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"4a44016fa37dfb6a5630988445e95fa68cbf62c081ca0354d225cc9eee4b1060","compiler_version":"v0.68.1","strict":true,"agent_id":"copilot"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9"},{"repo":"actions/upload-artifact","sha":"bbbca2ddaa5d8feaa63e36b76fdaad77386f024f","version":"v7"},{"repo":"github/gh-aw-actions/setup","sha":"2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc","version":"v0.68.1"}]} +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.68.1). DO NOT EDIT. +# +# To update this file, edit the corresponding .md file and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# Weekly collection of repo traffic data (views and unique visitors). Appends the previous week's daily numbers to CSV files. +# +# Secrets used: +# - COPILOT_GITHUB_TOKEN +# - GH_AW_CI_TRIGGER_TOKEN +# - GH_AW_GITHUB_MCP_SERVER_TOKEN +# - GH_AW_GITHUB_TOKEN +# - GITHUB_TOKEN +# +# Custom actions used: +# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 +# - actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 +# - github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + +name: "Traffic Updater" +"on": + schedule: + - cron: "5 19 * * 1" + # Friendly format: weekly on monday (scattered) + workflow_dispatch: + inputs: + aw_context: + default: "" + description: Agent caller context (used internally by Agentic Workflows). + required: false + type: string + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}" + +run-name: "Traffic Updater" + +jobs: + activation: + runs-on: ubuntu-slim + permissions: + actions: read + contents: read + outputs: + comment_id: "" + comment_repo: "" + lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} + model: ${{ steps.generate_aw_info.outputs.model }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + - name: Generate agentic run info + id: generate_aw_info + env: + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'auto' }} + GH_AW_INFO_VERSION: "1.0.21" + GH_AW_INFO_AGENT_VERSION: "1.0.21" + GH_AW_INFO_CLI_VERSION: "v0.68.1" + GH_AW_INFO_WORKFLOW_NAME: "Traffic Updater" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.25.18" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_COMPILED_STRICT: "true" + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + await main(core, context); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + sparse-checkout: | + .github + .agents + sparse-checkout-cone-mode: true + fetch-depth: 1 + - name: Check workflow lock file + id: check-lock-file + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_WORKFLOW_FILE: "traffic-updater.lock.yml" + GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Check compile-agentic version + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_COMPILED_VERSION: "v0.68.1" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + # poutine:ignore untrusted_checkout_exec + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" + { + cat << 'GH_AW_PROMPT_87f03c5f23862041_EOF' + + GH_AW_PROMPT_87f03c5f23862041_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" + cat << 'GH_AW_PROMPT_87f03c5f23862041_EOF' + + Tools: create_pull_request, missing_tool, missing_data, noop + GH_AW_PROMPT_87f03c5f23862041_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" + cat << 'GH_AW_PROMPT_87f03c5f23862041_EOF' + + + The following GitHub context information is available for this workflow: + {{#if __GH_AW_GITHUB_ACTOR__ }} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if __GH_AW_GITHUB_REPOSITORY__ }} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if __GH_AW_GITHUB_WORKSPACE__ }} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} + - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} + - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} + - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} + - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{/if}} + {{#if __GH_AW_GITHUB_RUN_ID__ }} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + GH_AW_PROMPT_87f03c5f23862041_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_87f03c5f23862041_EOF' + + {{#runtime-import .github/workflows/traffic-updater.md}} + GH_AW_PROMPT_87f03c5f23862041_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Upload activation artifact + if: success() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: activation + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/github_rate_limits.jsonl + if-no-files-found: ignore + retention-days: 1 + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: + contents: read + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: 0 + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_WORKFLOW_ID_SANITIZED: trafficupdater + outputs: + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + inference_access_error: ${{ steps.detect-inference-error.outputs.inference_access_error || 'false' }} + model: ${{ needs.activation.outputs.model }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + - name: Set runtime paths + id: set-runtime-paths + run: | + echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" >> "$GITHUB_OUTPUT" + echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" >> "$GITHUB_OUTPUT" + echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" >> "$GITHUB_OUTPUT" + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Create gh-aw temp directory + run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" + - name: Configure gh CLI for GitHub Enterprise + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" + env: + GH_TOKEN: ${{ github.token }} + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request || github.event.issue.pull_request + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.21 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.18 + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + with: + script: | + const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.18 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.18 ghcr.io/github/gh-aw-firewall/squid:0.25.18 ghcr.io/github/gh-aw-mcpg:v0.2.17 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine + - name: Write Safe Outputs Config + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << GH_AW_SAFE_OUTPUTS_CONFIG_b41550a42d321a39_EOF + {"create_pull_request":{"allowed_files":[".github/uvs.csv",".github/views.csv"],"base_branch":"main","fallback_as_issue":false,"github-token":"${GH_AW_GITHUB_TOKEN}","labels":["automated-update","traffic-data"],"max":1,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_files_policy":"allowed","protected_path_prefixes":[".github/",".agents/"],"title_prefix":"[bot] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_b41550a42d321a39_EOF + - name: Write Safe Outputs Tools + env: + GH_AW_TOOLS_META_JSON: | + { + "description_suffixes": { + "create_pull_request": " CONSTRAINTS: Maximum 1 pull request(s) can be created. Title will be prefixed with \"[bot] \". Labels [\"automated-update\" \"traffic-data\"] will be automatically added." + }, + "repo_params": {}, + "dynamic_tools": [] + } + GH_AW_VALIDATION_JSON: | + { + "create_pull_request": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "branch": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "draft": { + "type": "boolean" + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "missing_data": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "context": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "data_type": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "reason": { + "type": "string", + "sanitize": true, + "maxLength": 256 + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + } + } + }, + "report_incomplete": { + "defaultMax": 5, + "fields": { + "details": { + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 1024 + } + } + } + } + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + await main(); + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing vulnerabilities + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT=3001 + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" + + - name: Write MCP Scripts Config + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-scripts/logs" + cat > "${RUNNER_TEMP}/gh-aw/mcp-scripts/tools.json" << 'GH_AW_MCP_SCRIPTS_TOOLS_0f0f2a932da15b92_EOF' + { + "serverName": "mcpscripts", + "version": "1.0.0", + "logDir": "${RUNNER_TEMP}/gh-aw/mcp-scripts/logs", + "tools": [ + { + "name": "fetch-traffic", + "description": "Fetch the last 14 days of traffic views for this repository from the GitHub API. Returns JSON with a views array containing timestamp, count, and uniques per day.", + "inputSchema": { + "properties": {}, + "type": "object" + }, + "handler": "fetch-traffic.sh", + "env": { + "GH_TOKEN": "GH_TOKEN" + }, + "timeout": 60 + } + ] + } + GH_AW_MCP_SCRIPTS_TOOLS_0f0f2a932da15b92_EOF + cat > "${RUNNER_TEMP}/gh-aw/mcp-scripts/mcp-server.cjs" << 'GH_AW_MCP_SCRIPTS_SERVER_4653cd52928bab9f_EOF' + const path = require("path"); + const { startHttpServer } = require("./mcp_scripts_mcp_server_http.cjs"); + const configPath = path.join(__dirname, "tools.json"); + const port = parseInt(process.env.GH_AW_MCP_SCRIPTS_PORT || "3000", 10); + const apiKey = process.env.GH_AW_MCP_SCRIPTS_API_KEY || ""; + startHttpServer(configPath, { + port: port, + stateless: true, + logDir: "${RUNNER_TEMP}/gh-aw/mcp-scripts/logs" + }).catch(error => { + console.error("Failed to start mcp-scripts HTTP server:", error); + process.exit(1); + }); + GH_AW_MCP_SCRIPTS_SERVER_4653cd52928bab9f_EOF + chmod +x "${RUNNER_TEMP}/gh-aw/mcp-scripts/mcp-server.cjs" + + - name: Write MCP Scripts Tool Files + run: | + cat > "${RUNNER_TEMP}/gh-aw/mcp-scripts/fetch-traffic.sh" << 'GH_AW_MCP_SCRIPTS_SH_FETCH-TRAFFIC_ac87bd3a70438ec3_EOF' + #!/bin/bash + # Auto-generated mcp-script tool: fetch-traffic + # Fetch the last 14 days of traffic views for this repository from the GitHub API. Returns JSON with a views array containing timestamp, count, and uniques per day. + + set -euo pipefail + + gh api repos/$GITHUB_REPOSITORY/traffic/views + + + GH_AW_MCP_SCRIPTS_SH_FETCH-TRAFFIC_ac87bd3a70438ec3_EOF + chmod +x "${RUNNER_TEMP}/gh-aw/mcp-scripts/fetch-traffic.sh" + + - name: Generate MCP Scripts Server Config + id: mcp-scripts-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing vulnerabilities + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT=3000 + + # Set outputs for next steps + { + echo "mcp_scripts_api_key=${API_KEY}" + echo "mcp_scripts_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "MCP Scripts server will run on port ${PORT}" + + - name: Start MCP Scripts HTTP Server + id: mcp-scripts-start + env: + DEBUG: '*' + GH_AW_MCP_SCRIPTS_PORT: ${{ steps.mcp-scripts-config.outputs.mcp_scripts_port }} + GH_AW_MCP_SCRIPTS_API_KEY: ${{ steps.mcp-scripts-config.outputs.mcp_scripts_api_key }} + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_MCP_SCRIPTS_PORT + export GH_AW_MCP_SCRIPTS_API_KEY + + bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_scripts_server.sh" + + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GH_AW_MCP_SCRIPTS_API_KEY: ${{ steps.mcp-scripts-start.outputs.api_key }} + GH_AW_MCP_SCRIPTS_PORT: ${{ steps.mcp-scripts-start.outputs.port }} + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} + GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p /tmp/gh-aw/mcp-config + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export DEBUG="*" + + export GH_AW_ENGINE="copilot" + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_MCP_SCRIPTS_PORT -e GH_AW_MCP_SCRIPTS_API_KEY -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -e GH_TOKEN -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.17' + + mkdir -p /home/runner/.copilot + cat << GH_AW_MCP_CONFIG_ecdf13a30f302917_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v0.32.0", + "env": { + "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "repos" + }, + "guard-policies": { + "allow-only": { + "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", + "repos": "$GITHUB_MCP_GUARD_REPOS" + } + } + }, + "mcpscripts": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_MCP_SCRIPTS_PORT", + "headers": { + "Authorization": "\${GH_AW_MCP_SCRIPTS_API_KEY}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "*" + ] + } + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "*" + ] + } + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_ecdf13a30f302917_EOF + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Clean git credentials + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + # --allow-tool github + # --allow-tool mcpscripts + # --allow-tool safeoutputs + # --allow-tool shell(cat) + # --allow-tool shell(date) + # --allow-tool shell(echo) + # --allow-tool shell(git add:*) + # --allow-tool shell(git branch:*) + # --allow-tool shell(git checkout:*) + # --allow-tool shell(git commit:*) + # --allow-tool shell(git merge:*) + # --allow-tool shell(git rm:*) + # --allow-tool shell(git status) + # --allow-tool shell(git switch:*) + # --allow-tool shell(grep) + # --allow-tool shell(head) + # --allow-tool shell(ls) + # --allow-tool shell(pwd) + # --allow-tool shell(sort) + # --allow-tool shell(tail) + # --allow-tool shell(uniq) + # --allow-tool shell(wc) + # --allow-tool shell(yq) + # --allow-tool write + timeout-minutes: 20 + run: | + set -o pipefail + touch /tmp/gh-aw/agent-step-summary.md + (umask 177 && touch /tmp/gh-aw/agent-stdio.log) + # shellcheck disable=SC1003 + sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GH_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.18 --skip-pull --enable-api-proxy \ + -- /bin/bash -c 'node ${RUNNER_TEMP}/gh-aw/actions/copilot_driver.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-tool github --allow-tool mcpscripts --allow-tool safeoutputs --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(git add:*)'\'' --allow-tool '\''shell(git branch:*)'\'' --allow-tool '\''shell(git checkout:*)'\'' --allow-tool '\''shell(git commit:*)'\'' --allow-tool '\''shell(git merge:*)'\'' --allow-tool '\''shell(git rm:*)'\'' --allow-tool '\''shell(git status)'\'' --allow-tool '\''shell(git switch:*)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool write --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_PHASE: agent + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_VERSION: v0.68.1 + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Detect inference access error + id: detect-inference-error + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/detect_inference_access_error.sh" + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh" + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Append agent step summary + if: always() + run: bash "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh" + - name: Copy Safe Outputs + if: always() + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + run: | + mkdir -p /tmp/gh-aw + cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true + - name: Ingest agent output + id: collect_output + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,localhost,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP Scripts logs for step summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_scripts_logs.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + id: parse-mcp-gateway + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + - name: Parse token usage for step summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); + - name: Write agent output placeholder if missing + if: always() + run: | + if [ ! -f /tmp/gh-aw/agent_output.json ]; then + echo '{"items":[]}' > /tmp/gh-aw/agent_output.json + fi + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: agent + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/mcp-scripts/logs/ + /tmp/gh-aw/agent_usage.json + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/agent/ + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/safeoutputs.jsonl + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/aw-*.patch + /tmp/gh-aw/aw-*.bundle + if-no-files-found: ignore + - name: Upload firewall audit logs + if: always() + continue-on-error: true + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: firewall-audit-logs + path: | + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/sandbox/firewall/audit/ + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - safe_outputs + if: > + always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || + needs.activation.outputs.stale_lock_file_failed == 'true') + runs-on: ubuntu-slim + permissions: + contents: write + pull-requests: write + concurrency: + group: "gh-aw-conclusion-traffic-updater" + cancel-in-progress: false + outputs: + incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Process No-Op Messages + id: noop + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: "1" + GH_AW_WORKFLOW_NAME: "Traffic Updater" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_REPORT_AS_ISSUE: "false" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + await main(); + - name: Record missing tool + id: missing_tool + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_MISSING_TOOL_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "Traffic Updater" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Record incomplete + id: report_incomplete + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "Traffic Updater" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + await main(); + - name: Handle agent failure + id: handle_agent_failure + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Traffic Updater" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "traffic-updater" + GH_AW_ENGINE_ID: "copilot" + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} + GH_AW_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }} + GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} + GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_GROUP_REPORTS: "false" + GH_AW_FAILURE_REPORT_AS_ISSUE: "true" + GH_AW_TIMEOUT_MINUTES: "20" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + + detection: + needs: + - activation + - agent + if: > + always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} + detection_success: ${{ steps.detection_conclusion.outputs.success }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Checkout repository for patch context + if: needs.agent.outputs.has_patch == 'true' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + # --- Threat Detection --- + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.18 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.18 ghcr.io/github/gh-aw-firewall/squid:0.25.18 + - name: Check if detection needed + id: detection_guard + if: always() + env: + OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + run: | + if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then + echo "run_detection=true" >> "$GITHUB_OUTPUT" + echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" + else + echo "run_detection=false" >> "$GITHUB_OUTPUT" + echo "Detection skipped: no agent outputs or patches to analyze" + fi + - name: Clear MCP configuration for detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + rm -f /tmp/gh-aw/mcp-config/mcp-servers.json + rm -f /home/runner/.copilot/mcp-config.json + rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" + - name: Prepare threat detection files + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true + cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true + for f in /tmp/gh-aw/aw-*.patch; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + for f in /tmp/gh-aw/aw-*.bundle; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + echo "Prepared threat detection files:" + ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + - name: Setup threat detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + WORKFLOW_NAME: "Traffic Updater" + WORKFLOW_DESCRIPTION: "Weekly collection of repo traffic data (views and unique visitors). Appends the previous week's daily numbers to CSV files." + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.21 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.18 + - name: Execute GitHub Copilot CLI + if: always() && steps.detection_guard.outputs.run_detection == 'true' + id: detection_agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 20 + run: | + set -o pipefail + touch /tmp/gh-aw/agent-step-summary.md + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + # shellcheck disable=SC1003 + sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,github.com,host.docker.internal,telemetry.enterprise.githubcopilot.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.18 --skip-pull --enable-api-proxy \ + -- /bin/bash -c 'node ${RUNNER_TEMP}/gh-aw/actions/copilot_driver.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} + GH_AW_PHASE: detection + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_VERSION: v0.68.1 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Upload threat detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: detection + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + - name: Parse and conclude threat detection + id: detection_conclusion + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + + safe_outputs: + needs: + - activation + - agent + - detection + if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' + runs-on: ubuntu-slim + permissions: + contents: write + pull-requests: write + timeout-minutes: 15 + env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/traffic-updater" + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} + GH_AW_ENGINE_ID: "copilot" + GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_WORKFLOW_ID: "traffic-updater" + GH_AW_WORKFLOW_NAME: "Traffic Updater" + outputs: + code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} + code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + created_pr_number: ${{ steps.process_safe_outputs.outputs.created_pr_number }} + created_pr_url: ${{ steps.process_safe_outputs.outputs.created_pr_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Download patch artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Checkout repository + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: main + token: ${{ secrets.GH_AW_GITHUB_TOKEN }} + persist-credentials: false + fetch-depth: 1 + - name: Configure Git credentials + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GIT_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,localhost,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"allowed_files\":[\".github/uvs.csv\",\".github/views.csv\"],\"base_branch\":\"main\",\"fallback_as_issue\":false,\"github-token\":\"${{ secrets.GH_AW_GITHUB_TOKEN }}\",\"labels\":[\"automated-update\",\"traffic-data\"],\"max\":1,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_files_policy\":\"allowed\",\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"title_prefix\":\"[bot] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + - name: Upload Safe Outputs Items + if: always() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: safe-outputs-items + path: /tmp/gh-aw/safe-output-items.jsonl + if-no-files-found: ignore + diff --git a/.github/workflows/traffic-updater.md b/.github/workflows/traffic-updater.md new file mode 100644 index 00000000..22d94a72 --- /dev/null +++ b/.github/workflows/traffic-updater.md @@ -0,0 +1,86 @@ +--- +name: "Traffic Updater" +description: "Weekly collection of repo traffic data (views and unique visitors). Appends the previous week's daily numbers to CSV files." +on: + schedule: weekly on monday + workflow_dispatch: +tools: + bash: ["date"] + edit: + github: + toolsets: [repos] +mcp-scripts: + fetch-traffic: + description: "Fetch the last 14 days of traffic views for this repository from the GitHub API. Returns JSON with a views array containing timestamp, count, and uniques per day." + run: | + gh api repos/$GITHUB_REPOSITORY/traffic/views + env: + GH_TOKEN: "${{ secrets.GH_AW_GITHUB_TOKEN }}" +safe-outputs: + allowed-domains: + - github.com + noop: + report-as-issue: false + create-pull-request: + labels: [automated-update, traffic-data] + title-prefix: "[bot] " + base-branch: main + fallback-as-issue: false + protected-files: allowed + allowed-files: + - ".github/uvs.csv" + - ".github/views.csv" + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN }} +--- + +# Collect Weekly Repo Traffic + +You are a traffic collection bot for the **copilot-cli-for-beginners** repository. Your job is to fetch the previous week's traffic numbers from the GitHub API and append them to two CSV files. + +## Definitions + +- **Unique visitors** go in `.github/uvs.csv` +- **Total views** go in `.github/views.csv` +- Both files use the format `"MM/DD",count` โ€” one line per day, no header row. +- The workflow can be triggered on any day. It always resumes from where the files left off. + +## Step 1 โ€” Determine the last recorded date + +Read the last line of `.github/uvs.csv` (or `.github/views.csv` โ€” they should be in sync). Parse the `"MM/DD"` date to determine the last day already recorded. Assume the current year for the date. + +If both files are empty, treat the start date as 14 days ago (the maximum the GitHub API provides). + +## Step 2 โ€” Fetch traffic data + +Call the `fetch-traffic` tool (no inputs needed). It returns JSON with a `views` array containing objects with `timestamp`, `count`, and `uniques` for each day in the last 14 days. + +## Step 3 โ€” Filter to new dates only + +From the API response, keep only entries whose date is **after** the last recorded date from Step 1. + +Also exclude **today's date** since the day is not yet complete and the numbers would be partial. + +Format each kept date as `"MM/DD"` (zero-padded month and day, no year). + +If there are no new dates to add, stop here and report that no new data is available. + +## Step 4 โ€” Append new rows + +Append the new rows to the end of each file, keeping the existing data intact: + +- **`.github/uvs.csv`** โ€” append `"MM/DD",{uniques}` for each new day +- **`.github/views.csv`** โ€” append `"MM/DD",{count}` for each new day + +Rows should be in chronological order (earliest date first). + +## Step 5 โ€” Open a pull request + +Create a pull request targeting the `main` branch. The PR title should summarize the date range, e.g.: + +> Add traffic data for week of MM/DD โ€“ MM/DD + +The PR body should include: + +1. The date range collected +2. Total views and unique visitors for the week +3. A short table or list showing the daily breakdown diff --git a/.github/workflows/translation-polisher.lock.yml b/.github/workflows/translation-polisher.lock.yml new file mode 100644 index 00000000..bdc752b1 --- /dev/null +++ b/.github/workflows/translation-polisher.lock.yml @@ -0,0 +1,1327 @@ +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"cbc9f85ce905d99f4478b7d93d8191a4d9b0d24e13640ae360044e3b41ed5a9e","compiler_version":"v0.68.1","strict":true,"agent_id":"copilot"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9"},{"repo":"actions/upload-artifact","sha":"bbbca2ddaa5d8feaa63e36b76fdaad77386f024f","version":"v7"},{"repo":"github/gh-aw-actions/setup","sha":"2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc","version":"v0.68.1"}]} +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.68.1). DO NOT EDIT. +# +# To update this file, edit the corresponding .md file and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# Reviews Co-op Translator pull requests and polishes generated translations without changing source content. +# +# Secrets used: +# - COPILOT_GITHUB_TOKEN +# - GH_AW_CI_TRIGGER_TOKEN +# - GH_AW_GITHUB_MCP_SERVER_TOKEN +# - GH_AW_GITHUB_TOKEN +# - GITHUB_TOKEN +# +# Custom actions used: +# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 +# - actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 +# - github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + +name: "Translation Polisher" +"on": + pull_request: + types: + - opened + - synchronize + - reopened + - ready_for_review + workflow_dispatch: + inputs: + aw_context: + default: "" + description: Agent caller context (used internally by Agentic Workflows). + required: false + type: string + workflow_run: + # zizmor: ignore[dangerous-triggers] - workflow_run trigger is secured with role and fork validation + branches: + - main + types: + - completed + workflows: + - Co-op Translator + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref || github.run_id }}" + cancel-in-progress: true + +run-name: "Translation Polisher" + +jobs: + activation: + needs: pre_activation + # zizmor: ignore[dangerous-triggers] - workflow_run trigger is secured with role and fork validation + if: > + (needs.pre_activation.outputs.activated == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.id == github.repository_id)) && + (github.event_name != 'workflow_run' || github.event.workflow_run.repository.id == github.repository_id && + (!(github.event.workflow_run.repository.fork))) + runs-on: ubuntu-slim + permissions: + actions: read + contents: read + outputs: + body: ${{ steps.sanitized.outputs.body }} + comment_id: "" + comment_repo: "" + lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} + model: ${{ steps.generate_aw_info.outputs.model }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} + text: ${{ steps.sanitized.outputs.text }} + title: ${{ steps.sanitized.outputs.title }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }} + - name: Generate agentic run info + id: generate_aw_info + env: + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'auto' }} + GH_AW_INFO_VERSION: "1.0.21" + GH_AW_INFO_AGENT_VERSION: "1.0.21" + GH_AW_INFO_CLI_VERSION: "v0.68.1" + GH_AW_INFO_WORKFLOW_NAME: "Translation Polisher" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.25.18" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_COMPILED_STRICT: "true" + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + await main(core, context); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + sparse-checkout: | + .github + .agents + sparse-checkout-cone-mode: true + fetch-depth: 1 + - name: Check workflow lock file + id: check-lock-file + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_WORKFLOW_FILE: "translation-polisher.lock.yml" + GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Check compile-agentic version + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_COMPILED_VERSION: "v0.68.1" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + await main(); + - name: Compute current body text + id: sanitized + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/compute_text.cjs'); + await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + # poutine:ignore untrusted_checkout_exec + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" + { + cat << 'GH_AW_PROMPT_007b898c8d34eb6e_EOF' + + GH_AW_PROMPT_007b898c8d34eb6e_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" + cat << 'GH_AW_PROMPT_007b898c8d34eb6e_EOF' + + Tools: update_pull_request, add_labels, push_to_pull_request_branch, missing_tool, missing_data, noop + GH_AW_PROMPT_007b898c8d34eb6e_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_push_to_pr_branch.md" + cat << 'GH_AW_PROMPT_007b898c8d34eb6e_EOF' + + + The following GitHub context information is available for this workflow: + {{#if __GH_AW_GITHUB_ACTOR__ }} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if __GH_AW_GITHUB_REPOSITORY__ }} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if __GH_AW_GITHUB_WORKSPACE__ }} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} + - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} + - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} + - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} + - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{/if}} + {{#if __GH_AW_GITHUB_RUN_ID__ }} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + - **checkouts**: The following repositories have been checked out and are available in the workspace: + - `$GITHUB_WORKSPACE` โ†’ `__GH_AW_GITHUB_REPOSITORY__` (cwd) [full history, all branches available as remote-tracking refs] [additional refs fetched: *] + - **Note**: If a branch you need is not in the list above and is not listed as an additional fetched ref, it has NOT been checked out. For private repositories you cannot fetch it without proper authentication. If the branch is required and not available, exit with an error and ask the user to add it to the `fetch:` option of the `checkout:` configuration (e.g., `fetch: ["refs/pulls/open/*"]` for all open PR refs, or `fetch: ["main", "feature/my-branch"]` for specific branches). + + + GH_AW_PROMPT_007b898c8d34eb6e_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_007b898c8d34eb6e_EOF' + + {{#runtime-import .github/workflows/translation-polisher.md}} + GH_AW_PROMPT_007b898c8d34eb6e_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Upload activation artifact + if: success() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: activation + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/github_rate_limits.jsonl + if-no-files-found: ignore + retention-days: 1 + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: + contents: read + issues: read + pull-requests: read + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: 0 + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_WORKFLOW_ID_SANITIZED: translationpolisher + outputs: + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + inference_access_error: ${{ steps.detect-inference-error.outputs.inference_access_error || 'false' }} + model: ${{ needs.activation.outputs.model }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + - name: Set runtime paths + id: set-runtime-paths + run: | + echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" >> "$GITHUB_OUTPUT" + echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" >> "$GITHUB_OUTPUT" + echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" >> "$GITHUB_OUTPUT" + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + fetch-depth: 0 + - name: Fetch additional refs + env: + GH_AW_FETCH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + header=$(printf "x-access-token:%s" "${GH_AW_FETCH_TOKEN}" | base64 -w 0) + git -c "http.extraheader=Authorization: Basic ${header}" fetch origin '+refs/heads/*:refs/remotes/origin/*' + - name: Create gh-aw temp directory + run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" + - name: Configure gh CLI for GitHub Enterprise + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" + env: + GH_TOKEN: ${{ github.token }} + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request || github.event.issue.pull_request + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.21 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.18 + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + with: + script: | + const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.18 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.18 ghcr.io/github/gh-aw-firewall/squid:0.25.18 ghcr.io/github/gh-aw-mcpg:v0.2.17 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine + - name: Write Safe Outputs Config + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << GH_AW_SAFE_OUTPUTS_CONFIG_fa8fe511dd276b01_EOF + {"add_labels":{"allowed":["translation-polished"],"github-token":"${GH_AW_GITHUB_TOKEN}","max":1},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"push_to_pull_request_branch":{"allowed_files":["translations/**/*.md","translations/**/.co-op-translator.json"],"github-token":"${GH_AW_GITHUB_TOKEN}","if_no_changes":"ignore","labels":["translation","automated-pr"],"max":1,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_files_policy":"allowed","protected_path_prefixes":[".github/",".agents/"],"target":"*"},"report_incomplete":{},"update_pull_request":{"allow_body":true,"allow_title":false,"github-token":"${GH_AW_GITHUB_TOKEN}","max":1,"target":"*"}} + GH_AW_SAFE_OUTPUTS_CONFIG_fa8fe511dd276b01_EOF + - name: Write Safe Outputs Tools + env: + GH_AW_TOOLS_META_JSON: | + { + "description_suffixes": { + "add_labels": " CONSTRAINTS: Maximum 1 label(s) can be added. Only these labels are allowed: [\"translation-polished\"].", + "push_to_pull_request_branch": " CONSTRAINTS: Maximum 1 push(es) can be made.", + "update_pull_request": " CONSTRAINTS: Maximum 1 pull request(s) can be updated. Target: *." + }, + "repo_params": {}, + "dynamic_tools": [] + } + GH_AW_VALIDATION_JSON: | + { + "add_labels": { + "defaultMax": 5, + "fields": { + "item_number": { + "issueNumberOrTemporaryId": true + }, + "labels": { + "required": true, + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + } + } + }, + "missing_data": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "context": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "data_type": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "reason": { + "type": "string", + "sanitize": true, + "maxLength": 256 + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + } + } + }, + "push_to_pull_request_branch": { + "defaultMax": 1, + "fields": { + "branch": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "pull_request_number": { + "issueOrPRNumber": true + } + } + }, + "report_incomplete": { + "defaultMax": 5, + "fields": { + "details": { + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 1024 + } + } + }, + "update_pull_request": { + "defaultMax": 1, + "fields": { + "body": { + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "draft": { + "type": "boolean" + }, + "operation": { + "type": "string", + "enum": [ + "replace", + "append", + "prepend" + ] + }, + "pull_request_number": { + "issueOrPRNumber": true + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "title": { + "type": "string", + "sanitize": true, + "maxLength": 256 + } + }, + "customValidation": "requiresOneOf:title,body" + } + } + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + await main(); + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing vulnerabilities + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT=3001 + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" + + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} + GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p /tmp/gh-aw/mcp-config + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export DEBUG="*" + + export GH_AW_ENGINE="copilot" + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.17' + + mkdir -p /home/runner/.copilot + cat << GH_AW_MCP_CONFIG_592a00f4d05d7e7b_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v0.32.0", + "env": { + "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "repos,pull_requests" + }, + "guard-policies": { + "allow-only": { + "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", + "repos": "$GITHUB_MCP_GUARD_REPOS" + } + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "*" + ] + } + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_592a00f4d05d7e7b_EOF + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Clean git credentials + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + # --allow-tool github + # --allow-tool safeoutputs + # --allow-tool shell(cat) + # --allow-tool shell(date) + # --allow-tool shell(echo) + # --allow-tool shell(gh:*) + # --allow-tool shell(git add:*) + # --allow-tool shell(git branch:*) + # --allow-tool shell(git checkout:*) + # --allow-tool shell(git commit:*) + # --allow-tool shell(git merge:*) + # --allow-tool shell(git rm:*) + # --allow-tool shell(git status) + # --allow-tool shell(git switch:*) + # --allow-tool shell(git:*) + # --allow-tool shell(grep) + # --allow-tool shell(head) + # --allow-tool shell(ls) + # --allow-tool shell(node) + # --allow-tool shell(pwd) + # --allow-tool shell(sort) + # --allow-tool shell(tail) + # --allow-tool shell(uniq) + # --allow-tool shell(wc) + # --allow-tool shell(yq) + # --allow-tool write + timeout-minutes: 20 + run: | + set -o pipefail + touch /tmp/gh-aw/agent-step-summary.md + (umask 177 && touch /tmp/gh-aw/agent-stdio.log) + # shellcheck disable=SC1003 + sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.18 --skip-pull --enable-api-proxy \ + -- /bin/bash -c 'node ${RUNNER_TEMP}/gh-aw/actions/copilot_driver.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-tool github --allow-tool safeoutputs --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(gh:*)'\'' --allow-tool '\''shell(git add:*)'\'' --allow-tool '\''shell(git branch:*)'\'' --allow-tool '\''shell(git checkout:*)'\'' --allow-tool '\''shell(git commit:*)'\'' --allow-tool '\''shell(git merge:*)'\'' --allow-tool '\''shell(git rm:*)'\'' --allow-tool '\''shell(git status)'\'' --allow-tool '\''shell(git switch:*)'\'' --allow-tool '\''shell(git:*)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(node)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool write --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_PHASE: agent + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_VERSION: v0.68.1 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Detect inference access error + id: detect-inference-error + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/detect_inference_access_error.sh" + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh" + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Append agent step summary + if: always() + run: bash "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh" + - name: Copy Safe Outputs + if: always() + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + run: | + mkdir -p /tmp/gh-aw + cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true + - name: Ingest agent output + id: collect_output + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,localhost,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + id: parse-mcp-gateway + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + - name: Parse token usage for step summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); + - name: Write agent output placeholder if missing + if: always() + run: | + if [ ! -f /tmp/gh-aw/agent_output.json ]; then + echo '{"items":[]}' > /tmp/gh-aw/agent_output.json + fi + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: agent + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/agent_usage.json + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/agent/ + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/safeoutputs.jsonl + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/aw-*.patch + /tmp/gh-aw/aw-*.bundle + if-no-files-found: ignore + - name: Upload firewall audit logs + if: always() + continue-on-error: true + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: firewall-audit-logs + path: | + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/sandbox/firewall/audit/ + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - safe_outputs + if: > + always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || + needs.activation.outputs.stale_lock_file_failed == 'true') + runs-on: ubuntu-slim + permissions: + contents: write + issues: write + pull-requests: write + concurrency: + group: "gh-aw-conclusion-translation-polisher" + cancel-in-progress: false + outputs: + incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Process No-Op Messages + id: noop + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: "1" + GH_AW_WORKFLOW_NAME: "Translation Polisher" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_REPORT_AS_ISSUE: "false" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + await main(); + - name: Record missing tool + id: missing_tool + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_MISSING_TOOL_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "Translation Polisher" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Record incomplete + id: report_incomplete + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "Translation Polisher" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + await main(); + - name: Handle agent failure + id: handle_agent_failure + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Translation Polisher" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "translation-polisher" + GH_AW_ENGINE_ID: "copilot" + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} + GH_AW_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }} + GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} + GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_GROUP_REPORTS: "false" + GH_AW_FAILURE_REPORT_AS_ISSUE: "true" + GH_AW_TIMEOUT_MINUTES: "20" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + + detection: + needs: + - activation + - agent + if: > + always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} + detection_success: ${{ steps.detection_conclusion.outputs.success }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Checkout repository for patch context + if: needs.agent.outputs.has_patch == 'true' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + # --- Threat Detection --- + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.18 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.18 ghcr.io/github/gh-aw-firewall/squid:0.25.18 + - name: Check if detection needed + id: detection_guard + if: always() + env: + OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + run: | + if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then + echo "run_detection=true" >> "$GITHUB_OUTPUT" + echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" + else + echo "run_detection=false" >> "$GITHUB_OUTPUT" + echo "Detection skipped: no agent outputs or patches to analyze" + fi + - name: Clear MCP configuration for detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + rm -f /tmp/gh-aw/mcp-config/mcp-servers.json + rm -f /home/runner/.copilot/mcp-config.json + rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" + - name: Prepare threat detection files + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true + cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true + for f in /tmp/gh-aw/aw-*.patch; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + for f in /tmp/gh-aw/aw-*.bundle; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + echo "Prepared threat detection files:" + ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + - name: Setup threat detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + WORKFLOW_NAME: "Translation Polisher" + WORKFLOW_DESCRIPTION: "Reviews Co-op Translator pull requests and polishes generated translations without changing source content." + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.21 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.18 + - name: Execute GitHub Copilot CLI + if: always() && steps.detection_guard.outputs.run_detection == 'true' + id: detection_agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 20 + run: | + set -o pipefail + touch /tmp/gh-aw/agent-step-summary.md + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + # shellcheck disable=SC1003 + sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,github.com,host.docker.internal,telemetry.enterprise.githubcopilot.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.18 --skip-pull --enable-api-proxy \ + -- /bin/bash -c 'node ${RUNNER_TEMP}/gh-aw/actions/copilot_driver.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} + GH_AW_PHASE: detection + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_VERSION: v0.68.1 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Upload threat detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: detection + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + - name: Parse and conclude threat detection + id: detection_conclusion + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + + pre_activation: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.id == github.repository_id + runs-on: ubuntu-slim + outputs: + activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} + matched_command: '' + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + - name: Check team membership for workflow + id: check_membership + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_REQUIRED_ROLES: "admin,maintainer,write" + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_membership.cjs'); + await main(); + + safe_outputs: + needs: + - activation + - agent + - detection + if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' + runs-on: ubuntu-slim + permissions: + contents: write + issues: write + pull-requests: write + timeout-minutes: 15 + env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/translation-polisher" + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} + GH_AW_ENGINE_ID: "copilot" + GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_WORKFLOW_ID: "translation-polisher" + GH_AW_WORKFLOW_NAME: "Translation Polisher" + outputs: + code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} + code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + push_commit_sha: ${{ steps.process_safe_outputs.outputs.push_commit_sha }} + push_commit_url: ${{ steps.process_safe_outputs.outputs.push_commit_url }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Download patch artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Checkout repository + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.base_ref || github.event.pull_request.base.ref || github.ref_name || github.event.repository.default_branch }} + token: ${{ secrets.GH_AW_GITHUB_TOKEN }} + persist-credentials: false + fetch-depth: 1 + - name: Configure Git credentials + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GIT_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,localhost,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_labels\":{\"allowed\":[\"translation-polished\"],\"github-token\":\"${{ secrets.GH_AW_GITHUB_TOKEN }}\",\"max\":1},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"push_to_pull_request_branch\":{\"allowed_files\":[\"translations/**/*.md\",\"translations/**/.co-op-translator.json\"],\"github-token\":\"${{ secrets.GH_AW_GITHUB_TOKEN }}\",\"if_no_changes\":\"ignore\",\"labels\":[\"translation\",\"automated-pr\"],\"max\":1,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_files_policy\":\"allowed\",\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"target\":\"*\"},\"report_incomplete\":{},\"update_pull_request\":{\"allow_body\":true,\"allow_title\":false,\"github-token\":\"${{ secrets.GH_AW_GITHUB_TOKEN }}\",\"max\":1,\"target\":\"*\"}}" + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + - name: Upload Safe Outputs Items + if: always() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: safe-outputs-items + path: /tmp/gh-aw/safe-output-items.jsonl + if-no-files-found: ignore + diff --git a/.github/workflows/translation-polisher.md b/.github/workflows/translation-polisher.md new file mode 100644 index 00000000..7eff0d74 --- /dev/null +++ b/.github/workflows/translation-polisher.md @@ -0,0 +1,270 @@ +--- +name: "Translation Polisher" +description: "Reviews Co-op Translator pull requests and polishes generated translations without changing source content." +on: + workflow_run: + workflows: ["Co-op Translator"] + types: [completed] + branches: [main] + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + workflow_dispatch: +permissions: + contents: read + pull-requests: read + issues: read +engine: copilot +tools: + bash: ["gh", "git", "node"] + edit: + github: + toolsets: [repos, pull_requests] +checkout: + fetch-depth: 0 + fetch: ["*"] +network: defaults +safe-outputs: + allowed-domains: + - github.com + noop: + report-as-issue: false + add-labels: + allowed: [translation-polished] + max: 1 + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN }} + update-pull-request: + target: "*" + title: false + body: true + max: 1 + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN }} + push-to-pull-request-branch: + target: "*" + labels: [translation, automated-pr] + protected-files: allowed + allowed-files: + - "translations/**/*.md" + - "translations/**/.co-op-translator.json" + if-no-changes: "ignore" + max: 1 + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN }} +--- + +# Polish Co-op Translator PRs + +You are a translation editor for the **GitHub Copilot CLI for Beginners** course. Your job is to review and polish Markdown files generated by Co-op Translator, while preserving the repository's translation structure and source content. + +## Scope + +Only work on Co-op Translator pull requests. + +A pull request is in scope when all of these are true: + +1. The pull request title starts with `Update translations via Co-op Translator`. +2. The pull request has both labels: `translation` and `automated-pr`. +3. The pull request changes files under `translations/`. + +If this workflow is triggered by `workflow_run` or `workflow_dispatch`, find the current open pull request with head branch `update-translations`. If no matching pull request exists, stop with a no-op. + +If this workflow is triggered by `pull_request`, inspect the triggering pull request. If it is not in scope, stop with a no-op. + +## Loop prevention + +Before editing, inspect the pull request's latest commit, current diff, labels, and body. + +Stop with a no-op only if all of these are true: + +1. The translated Markdown already satisfies the quality checklist below and no file changes are needed. +2. The pull request body already contains an up-to-date managed `## Translation Quality Review` section with one grade row for every changed translated Markdown file. +3. Every existing grade in the managed review section is **A- or higher**. + +If the latest commit appears to be from this Translation Polisher workflow but the pull request body is missing the `## Translation Quality Review` section, do not edit files. Still review and grade the changed translated Markdown files and update the pull request body. + +If the pull request body already contains a managed `## Translation Quality Review` section and any row is graded **B+ or lower**, treat those files as required repair targets. Review and polish those files again before deciding whether to push changes or report blocking issues in the managed pull request body section. + +Do not add churn. If the translation is already good enough and the PR body already has current A- or higher grades, leave it unchanged. + +## Files you may change + +You may edit only translated Markdown files: + +- `translations/**/*.md` + +Exclude these generated translation paths from review, grading, and edits: + +- `translations/*/.github/**` +- `translations/*/samples/**` + +Leave skill definition Markdown in English. + +Do not edit: + +- English source files +- `.co-op-translator.json` +- workflow files +- scripts +- sample source code outside translated Markdown + +## Required process + +> **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/**`. +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. +7. Run an untranslated learner-facing text pass: + - Check headings, visible table headers, navigation tables, list labels, callout/admonition labels, and human-facing link labels. + - Translate leftover English when it is learner-facing prose. + - Preserve product names, commands, file paths, branch names, package names, URLs, badge URLs, code identifiers, and GitHub UI labels that learners must recognize. +8. Run the deterministic cleanup script after edits: + + ```bash + node .github/scripts/fix-translated-markdown.js "" + ``` + +9. Perform a final review of each changed target-language file against its English source file. Grade each file using A, A-, B+, B, B-, C, D, or F. +10. Continue improving the translation until every changed target-language file earns **A- or higher**. +11. If any changed target-language file remains below A- after reasonable polishing, do not push changes and do not add the `translation-polished` label. Update the pull request body with `Translation status: Needs polish` and include the blocking issues and current file grades in the managed review section. +12. Review your final diff. If it contains anything outside `translations/**/*.md` or Co-op metadata files named `translations/**/.co-op-translator.json`, revert those changes. +13. Push your changes to the target pull request branch using the safe output only when every changed target-language file is A- or higher. +14. Update the pull request body with a final per-file grade table using the instructions in **Pull request body update**. +15. Add the `translation-polished` label only when every changed target-language file is A- or higher. +16. Do not add a pull request comment. The managed pull request body section is the source of truth for review status, grades, and notes. + +## Safe output limits + +Emit each safe output type at most once: + +- At most one `push_to_pull_request_branch`. +- At most one `update_pull_request`. +- At most one `add_labels`, and only when the `translation-polished` label is not already present on the pull request. +- Do not emit `add_comment`; comments are not an allowed safe output for this workflow. + +Do not emit duplicate branch push, pull request update, label, or comment requests. If a label is already present, do not emit an `add_labels` request for it. If you emit `add_labels`, include the target pull request number as `item_number`. Put the quality summary and polishing summary in the single managed pull request body section. + +## Quality checklist + +For every translated Markdown file you edit: + +- Preserve all code blocks exactly unless the original English text inside the code block is instructional prose that should intentionally be translated. +- Preserve command names, file paths, package names, product names, URLs, and badge URLs. +- Preserve Markdown tables, lists, blockquotes, headings, and admonitions. +- Preserve links and image destinations. Translate only the human-facing link label when appropriate. +- Translate human-facing prose naturally for the target language. +- Translate visible headings, table headings, navigation labels, list labels, callout labels, and link labels when they are human-facing content. +- Keep the beginner-friendly tone of the English source. +- Avoid literal phrasing that sounds unnatural in the target language. +- Do not remove the Co-op Translator disclaimer. +- Do not edit translation metadata. + +## Final translation review rubric + +Before pushing, review each changed translated Markdown file against its corresponding English source and assign a grade. + +Grade **A- or higher** only when all of these are true: + +- The translation preserves the meaning, scope, warnings, and calls to action from the English source. +- The Markdown structure, links, images, headings, tables, badges, and code blocks are intact. +- Human-facing prose, navigation labels, table headings, and link labels are translated where appropriate. +- Product names, commands, file paths, URLs, package names, and UI labels are preserved when they should be. +- The text sounds natural to a technical learner in the target language, not like a literal sentence-by-sentence translation. +- Terminology is consistent within the file and across the same target language. +- The tone remains beginner-friendly, practical, and encouraging. + +Use **B+ or lower** if any visible learner-facing text remains unnecessarily in English, if phrasing is noticeably awkward, if terminology is inconsistent, or if important nuance is missing. Keep polishing until every changed translated Markdown file is A- or higher. + +## Language quality profiles + +Apply the profile only when that language is present in the pull request. + +### Shared rules for all languages + +- Preserve product names such as **GitHub Copilot CLI**, **GitHub Codespaces**, and **Azure AI Foundry** unless an official localized name is clearly standard in the target-language ecosystem. +- Preserve commands, code, file paths, URLs, badge URLs, package names, branch names, and repository names. +- Translate human-facing link labels, table headings, navigation labels, and instructional prose. +- Keep English technical terms only when they are common in the target language, are official UI labels, or are product/feature names. +- Prefer natural beginner-friendly phrasing over literal translation. +- Use consistent terminology within each file and across the same language. +- Do not over-localize acronyms or terms that target-language developers normally use in English. + +### Spanish (`es`) + +- Use clear, neutral Spanish for a broad technical audience. +- Prefer natural active voice over passive constructions. +- Localize beginner-facing concepts such as issue and pull request when clarity improves, but keep GitHub UI terms in English when they refer to the UI label. +- Keep common technical acronyms such as API. +- Avoid overly literal phrasing. For example, prefer natural wording such as `potenciar`, `colega experto`, and `donde se encuentra cada una` when the sentence context calls for it. + +### Korean (`ko`) + +- Use polite, clear technical Korean appropriate for educational documentation. +- Keep product names in English unless there is a clear official Korean name. +- Prefer commonly used Korean developer terminology for concepts, but do not translate CLI commands, file paths, Git branch names, package names, or GitHub UI labels that learners must recognize. +- Avoid overly formal or machine-translated sentence endings; keep instructions direct and approachable. + +### Japanese (`ja`) + +- Use clear technical Japanese with a polite instructional tone. +- Keep product names in English unless there is a clear official Japanese name. +- Prefer standard Japanese developer terms and natural sentence structure. +- Avoid overly literal English word order. +- Do not translate commands, file paths, Git branch names, package names, or GitHub UI labels that learners must recognize. + +### Simplified Chinese (`zh-CN`) + +- Use Simplified Chinese. +- Use clear mainland Chinese technical documentation style. +- Keep product names in English unless there is a clear official Simplified Chinese name. +- Avoid Taiwan/Hong Kong traditional terminology. +- Do not translate commands, file paths, Git branch names, package names, or GitHub UI labels that learners must recognize. + +## Pull request body update + +After the final review, update the pull request body with a managed translation-quality section. Replace only the managed block between these exact lowercase markers: + +```markdown + + +``` + +The body must include exactly one managed block and exactly one section inside that block with this heading: + +```markdown +## Translation Quality Review +``` + +If an older unmarked `## Translation Quality Review` section already exists, replace it with the marked block. Do not append duplicates. Do not change the marker casing. Do not place generated workflow footers, integrity notes, or unrelated comments inside the managed block. + +Do not use any other marker names or casing. In particular, never use `TRANSLATION-REVIEW-START`, `TRANSLATION-REVIEW-END`, `TRANSLATION-QUALITY-REVIEW-START`, or uppercase marker variants. + +Use this format: + +```markdown + +## Translation Quality Review + +**Translation status:** Accepted +**Files reviewed:** 34 total, 34 accepted, 0 needs polish + +| Language | File | Final grade | Notes | +|---|---|---:|---| +| es | `translations/es/README.md` | A- | Preserves structure and reads naturally after polish. | + +All changed translated Markdown files must be graded **A- or higher** before this PR is marked `translation-polished`. + +``` + +Use `Translation status: Accepted` only when every changed translated Markdown file is graded A- or higher. Otherwise use `Translation status: Needs polish`, include counts for total files, accepted files, and files that need polish, and keep the `translation-polished` label off the PR. + +Include one row for every changed translated Markdown file in the target pull request. Keep notes concise and specific. For below-threshold files, the note must state the highest-impact issue to fix. + +## Pull request comments + +Do not add pull request comments. Use only the managed `## Translation Quality Review` pull request body section for grades, notes, and status. diff --git a/.gitignore b/.gitignore index b79ec11a..9b6560e3 100644 --- a/.gitignore +++ b/.gitignore @@ -100,4 +100,6 @@ Desktop.ini .claude .plans .plans.vhs-wrapper -demo-previews \ No newline at end of file +demo-previews +blog +migration.md \ No newline at end of file diff --git a/00-quick-start/README.md b/00-quick-start/README.md index 5b4612f0..3891c9a9 100644 --- a/00-quick-start/README.md +++ b/00-quick-start/README.md @@ -1,4 +1,15 @@ -![Chapter 00: Quick Start](images/chapter-header.png) + + +![Chapter 00: Quick Start](assets/chapter-header.png) Welcome! In this chapter, you'll get GitHub Copilot CLI (Command Line Interface) installed, signed in with your GitHub account, and verified that everything works. This is a quick setup chapter. Once you're up and running, the real demos start in Chapter 01! @@ -36,9 +47,9 @@ If you see "You don't have access to GitHub Copilot," you'll need to use the fre > โฑ๏ธ **Time estimate**: Installation takes 2-5 minutes. Authentication adds another 1-2 minutes. -### Recommended: GitHub Codespaces (Zero Setup) +### GitHub Codespaces (Zero Setup) -If you don't want to install any of the prerequisites, you can use GitHub Codespaces, which has the GitHub Copilot CLI ready to go (you'll need to sign in), pre-installs Python 3.13, pytest, and the GitHub CLI. +If you don't want to install any of the prerequisites you can use GitHub Codespaces, which has the GitHub Copilot CLI ready to go (you'll need to sign in), and pre-installs Python and pytest. 1. [Fork this repository](https://github.com/github/copilot-cli-for-beginners/fork) to your GitHub account 2. Select **Code** > **Codespaces** > **Create codespace on main** @@ -47,41 +58,71 @@ If you don't want to install any of the prerequisites, you can use GitHub Codesp > ๐Ÿ’ก **Verify in Codespace**: Run `cd samples/book-app-project && python book_app.py help` to confirm Python and the sample app are working. -### Alternative: Local Installation +### Local Installation -> ๐Ÿ’ก **Not sure which to pick?** Use `npm` if you have Node.js installed. Otherwise, choose the option that matches your system. +Follow these steps if you'd like to run Copilot CLI on your local machine with the course samples. -> ๐Ÿ’ก **Python required for demos**: The course uses a Python sample app. If you're working locally, install [Python 3.10+](https://www.python.org/downloads/) before starting the demos. +1. Clone the repo to get the course samples on your machine: -> **Note:** While the primary examples shown throughout the course use Python (`samples/book-app-project`), JavaScript (`samples/book-app-project-js`) and C# (`samples/book-app-project-cs`) versions are also available if you prefer to work with those languages. Each sample has a README with instructions for running the app in that language. + ```bash + git clone https://github.com/github/copilot-cli-for-beginners + cd copilot-cli-for-beginners + ``` -Choose the method that works for your system: +2. Install Copilot CLI using one of the following options. -### All Platforms (npm) + > ๐Ÿ’ก **Not sure which to pick?** Use `npm` if you have Node.js installed. Otherwise, choose the option that matches your system. -```bash -# If you have Node.js installed, this is a quick way to get the CLI -npm install -g @github/copilot -``` + ### All Platforms (npm) -### macOS/Linux (Homebrew) + ```bash + # If you have Node.js installed, this is a quick way to get the CLI + npm install -g @github/copilot + ``` -```bash -brew install copilot-cli -``` + ### macOS/Linux (Homebrew) -### Windows (WinGet) + ```bash + brew install copilot-cli + ``` -```bash -winget install GitHub.Copilot -``` + ### Windows (WinGet) -### macOS/Linux (Install Script) + ```bash + winget install GitHub.Copilot + ``` -```bash -curl -fsSL https://gh.io/copilot-install | bash + ### macOS/Linux (Install Script) + + ```bash + curl -fsSL https://gh.io/copilot-install | bash + ``` + +
+Optional: Enable shell tab completion + +Shell tab completion lets you press **Tab** to complete `copilot` subcommands, command options, and some option values. This is optional, but it can be handy once you're comfortable using the CLI. + +Copilot CLI currently supports completion scripts for Bash, Zsh, and Fish: + +```shell +# Bash, current session only +source <(copilot completion bash) + +# Bash, persistent on Linux +copilot completion bash | sudo tee /etc/bash_completion.d/copilot + +# Zsh +copilot completion zsh > "${fpath[1]}/_copilot" + +# Fish +copilot completion fish > ~/.config/fish/completions/copilot.fish ``` +Restart your shell after adding persistent completion. PowerShell is supported for running Copilot CLI on Windows, but `copilot completion` currently supports only Bash, Zsh, and Fish. + +
+ --- ## Authentication @@ -94,7 +135,7 @@ copilot You'll be asked to trust the folder containing the repository (if you haven't already). You can trust it one time or across all future sessions. -Trusting files in a folder with the Copilot CLI +Trusting files in a folder with the Copilot CLI After trusting the folder, you can sign in with your GitHub account. @@ -110,7 +151,7 @@ After trusting the folder, you can sign in with your GitHub account. 4. Select "Authorize" to grant GitHub Copilot CLI access 5. Return to your terminal - you're now signed in! -Device Authorization Flow - showing the 5-step process from terminal login to signed-in confirmation +Device Authorization Flow - showing the 5-step process from terminal login to signed-in confirmation *The device authorization flow: your terminal generates a code, you verify it in the browser, and Copilot CLI is authenticated.* @@ -139,7 +180,7 @@ After you receive a response, you can exit the CLI:
๐ŸŽฌ See it in action! -![Hello Demo](images/hello-demo.gif) +![Hello Demo](assets/hello-demo.gif) *Demo output varies. Your model, tools, and responses will differ from what's shown here.* @@ -153,7 +194,7 @@ After you receive a response, you can exit the CLI: The course provides a sample app that you'll explore and improve throughout the course using the CLI *(You can see the code for this in /samples/book-app-project)*. Check that the *Python book collection terminal app* works before you get started. Run `python` or `python3` depending on your system. -> **Note:** While the primary examples shown throughout the course use Python (`samples/book-app-project`), JavaScript (`samples/book-app-project-js`) and C# (`samples/book-app-project-cs`) versions are also available if you prefer to work with those languages. Each sample has a README with instructions for running the app in that language. +> **Note:** The primary examples shown throughout the course use Python (`samples/book-app-project`) so you'll need to have [Python 3.10+](https://www.python.org/downloads/) available on your local machine if you chose that option (the Codespace already has it installed). JavaScript (`samples/book-app-project-js`) and C# (`samples/book-app-project-cs`) versions are also available if you prefer to work with those languages. Each sample has a README with instructions for running the app in that language. ```bash cd samples/book-app-project diff --git a/00-quick-start/images/auth-device-flow.png b/00-quick-start/assets/auth-device-flow.png similarity index 100% rename from 00-quick-start/images/auth-device-flow.png rename to 00-quick-start/assets/auth-device-flow.png diff --git a/00-quick-start/images/chapter-header.png b/00-quick-start/assets/chapter-header.png similarity index 100% rename from 00-quick-start/images/chapter-header.png rename to 00-quick-start/assets/chapter-header.png diff --git a/00-quick-start/images/copilot-trust.png b/00-quick-start/assets/copilot-trust.png similarity index 100% rename from 00-quick-start/images/copilot-trust.png rename to 00-quick-start/assets/copilot-trust.png diff --git a/00-quick-start/images/hello-demo.gif b/00-quick-start/assets/hello-demo.gif similarity index 100% rename from 00-quick-start/images/hello-demo.gif rename to 00-quick-start/assets/hello-demo.gif diff --git a/00-quick-start/images/hello-demo.tape b/00-quick-start/assets/hello-demo.tape similarity index 100% rename from 00-quick-start/images/hello-demo.tape rename to 00-quick-start/assets/hello-demo.tape diff --git a/01-setup-and-first-steps/README.md b/01-setup-and-first-steps/README.md index 75157a45..ba53741b 100644 --- a/01-setup-and-first-steps/README.md +++ b/01-setup-and-first-steps/README.md @@ -1,4 +1,15 @@ -![Chapter 01: First Steps](images/chapter-header.png) + + +![Chapter 01: First Steps](assets/chapter-header.png) > **Watch AI find bugs instantly, explain confusing code, and generate working scripts. Then learn three different ways to use GitHub Copilot CLI.** @@ -20,7 +31,7 @@ By the end of this chapter, you'll be able to: # Your First Copilot CLI Experience -Developer sitting at a desk with code on the monitor and glowing particles representing AI assistance +Developer sitting at a desk with code on the monitor and glowing particles representing AI assistance Jump right in and see what Copilot CLI can do. @@ -62,7 +73,7 @@ Now let's see why developers are calling this "having a senior engineer on speed ### Demo 1: Code Review in Seconds -The course includes sample files with intentional code quality issues. Let's review one: +The course includes sample files with intentional code quality issues. If you're working on your local machine and haven't already cloned the repo, please run the `git clone` command below, navigate to the `copilot-cli-for-beginners` folder, and then run the `copilot` command. ```bash # Clone the course repository if you're working locally and haven't already @@ -73,20 +84,20 @@ cd copilot-cli-for-beginners copilot ``` -Once inside the interactive session: +Once inside the interactive Copilot CLI session, run the following: ``` > Review @samples/book-app-project/book_app.py for code quality issues and suggest improvements ``` -> ๐Ÿ’ก **What's the `@`?** The `@` symbol tells Copilot CLI to read a file. You'll learn all about this in Chapter 02. For now, just copy the command exactly as shown. +> ๐Ÿ’ก **What's the `@` symbol used for?** The `@` symbol tells Copilot CLI to read a file. You'll learn all about this in Chapter 02. For now, just copy the command exactly as shown. ---
๐ŸŽฌ See it in action! -![Code Review Demo](images/code-review-demo.gif) +![Code Review Demo](assets/code-review-demo.gif) *Demo output varies. Your model, tools, and responses will differ from what's shown here.* @@ -111,7 +122,7 @@ Ever stared at code wondering what it does? Try this in your Copilot CLI session
๐ŸŽฌ See it in action! -![Explain Code Demo](images/explain-code-demo.gif) +![Explain Code Demo](assets/explain-code-demo.gif) *Demo output varies. Your model, tools, and responses will differ from what's shown here.* @@ -167,7 +178,7 @@ Need a function you'd otherwise spend 15 minutes googling? Still in your session
๐ŸŽฌ See it in action! -![Generate Code Demo](images/generate-code-demo.gif) +![Generate Code Demo](assets/generate-code-demo.gif) *Demo output varies. Your model, tools, and responses will differ from what's shown here.* @@ -189,7 +200,7 @@ When you're done exploring, exit the session: # Modes and Commands -Futuristic control panel with glowing screens, dials, and equalizers representing Copilot CLI modes and commands +Futuristic control panel with glowing screens, dials, and equalizers representing Copilot CLI modes and commands You've just seen what Copilot CLI can do. Now let's understand *how* to use these capabilities effectively. The key is knowing which of the three interaction modes to use for different situations. @@ -209,7 +220,7 @@ Think of using GitHub Copilot CLI like going out to eat. From planning the trip Just like dining out, you'll naturally learn when each approach feels right. -Three Ways to Use GitHub Copilot CLI - Plan Mode (GPS route to restaurant), Interactive Mode (talking to waiter), Programmatic Mode (drive-through) +Three Ways to Use GitHub Copilot CLI - Plan Mode (GPS route to restaurant), Interactive Mode (talking to waiter), Programmatic Mode (drive-through) *Choose your mode based on the task: Plan for mapping it out first, Interactive for back-and-forth collaboration, Programmatic for quick one-shot results* @@ -230,7 +241,7 @@ Once you're comfortable, try: ### Mode 1: Interactive Mode (start here) -Interactive Mode - Like talking to a waiter who can answer questions and adjust the order +Interactive Mode - Like talking to a waiter who can answer questions and adjust the order **Best for**: Exploration, iteration, multi-turn conversations. Like talking to a waiter who can answer questions, take feedback, and adjust the order on the fly. @@ -268,13 +279,11 @@ Notice how each prompt builds on the previous answer. You're having a conversati ### Mode 2: Plan Mode -Plan Mode - Like planning a route before a trip using GPS +Plan Mode - Like planning a route before a trip using GPS **Best for**: Complex tasks where you want to review the approach before execution. Similar to planning a route before a trip using GPS. -Plan mode helps you create a step-by-step plan before writing any code. Use the `/plan` command or press **Shift+Tab** to cycle into Plan Mode: - -> ๐Ÿ’ก **Tip**: **Shift+Tab** cycles between modes: Interactive โ†’ Plan โ†’ Autopilot. Press it anytime during an interactive session to switch modes without typing a command. +Plan mode helps you create a step-by-step plan before writing any code. Use the `/plan` command, press **Shift+Tab** to cycle into Plan Mode: ```bash copilot @@ -282,6 +291,14 @@ copilot > /plan Add a "mark as read" command to the book app ``` +> ๐Ÿ’ก **Tip**: **Shift+Tab** cycles between modes: Interactive โ†’ Plan โ†’ Autopilot. Press it anytime during an interactive session to switch modes without typing a command. + +You can also launch Copilot CLI directly in plan mode using the `--plan` flag: + +```bash +copilot --plan +``` + **Plan mode output:** (your output may differ) ``` @@ -312,13 +329,13 @@ Proceed with implementation? [Y/n] > ๐Ÿ’ก **Want something more complex?** Try: `/plan Add search and filter capabilities to the book app`. Plan mode scales from simple features to full applications. -> ๐Ÿ“š **Autopilot mode**: You may have noticed Shift+Tab cycles through a third mode called **Autopilot**. In autopilot mode, Copilot works through an entire plan without waiting for your input after each step โ€” like handing a task to a colleague and saying "let me know when you're finished." The typical workflow is plan โ†’ accept โ†’ autopilot, which means you need to be good at writing plans first. Get comfortable with Interactive and Plan modes, then see the [official docs](https://docs.github.com/copilot/concepts/agents/copilot-cli/autopilot) when you're ready. +> ๐Ÿ“š **Autopilot mode**: You may have noticed Shift+Tab cycles through a third mode called **Autopilot**. In autopilot mode, Copilot works through an entire plan without waiting for your input after each step โ€” like handing a task to a colleague and saying "let me know when you're finished." The typical workflow is plan โ†’ accept โ†’ autopilot, which means you need to be good at writing plans first. You can also launch directly into autopilot with `copilot --autopilot`. Get comfortable with Interactive and Plan modes first, then see the [official docs](https://docs.github.com/copilot/concepts/agents/copilot-cli/autopilot) when you're ready. --- ### Mode 3: Programmatic Mode -Programmatic Mode - Like using a drive-through for a quick order +Programmatic Mode - Like using a drive-through for a quick order **Best for**: Automation, scripts, CI/CD, single-shot commands. Like using a drive-through for a quick order without needing to talk to a waiter. @@ -357,17 +374,25 @@ copilot --allow-all -p "Review @myfile.py for issues" ## Essential Slash Commands -These commands work in interactive mode. **Start with just these four** - they cover 90% of daily use: +These commands are great to learn initially as you're getting started with Copilot CLI: | Command | What It Does | When to Use | |---------|--------------|-------------| -| `/help` | Show all available commands | When you forget a command | +| `/ask` | Ask a quick question without it affecting your conversation history | When you want a quick answer without derailing your current task | | `/clear` | Clear conversation and start fresh | When switching topics | +| `/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 | -| `/model` | Show or switch AI model | When you want to change the AI model | | `/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. > ๐Ÿ“š **Official Documentation**: [CLI command reference](https://docs.github.com/copilot/reference/cli-command-reference) for the complete list of commands and flags. @@ -375,26 +400,28 @@ That's it for getting started! As you become comfortable, you can explore additi
๐Ÿ“š Additional Commands (click to expand) -> ๐Ÿ’ก The five commands above cover a lot of what you'll do on a daily use. This reference is here for when you're ready to explore more. +> ๐Ÿ’ก The essential commands above cover a lot of what you'll do on a daily use. This reference is here for when you're ready to explore more. ### Agent Environment | Command | What It Does | |---------|--------------| -| `/init` | Initialize Copilot instructions for your repository | | `/agent` | Browse and select from available agents | -| `/skills` | Manage skills for enhanced capabilities | +| `/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 | -> ๐Ÿ’ก Skills are covered in detail in [Chapter 05](../05-skills/README.md). MCP servers are covered in [Chapter 06](../06-mcp-servers/README.md). +> ๐Ÿ’ก 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). ### Models and Subagents | Command | What It Does | |---------|--------------| -| `/model` | Show or switch AI model | -| `/delegate` | Hand off task to Copilot coding agent on GitHub (agent in the cloud) | +| `/delegate` | Hand off task to GitHub Copilot cloud agent | | `/fleet` | Split a complex task into parallel subtasks for faster completion | +| `/model` | Show or switch AI model | | `/tasks` | View background subagents and detached shell sessions | ### Code @@ -402,41 +429,58 @@ That's it for getting started! As you become comfortable, you can explore additi | Command | What It Does | |---------|--------------| | `/diff` | Review the changes made in the current directory | -| `/review` | Run the code-review agent to analyze changes | +| `/pr` | Operate on pull requests for the current branch | | `/research` | Run deep research investigation using GitHub and web sources | +| `/review` | Run the code-review agent to analyze changes | | `/terminal-setup` | Enable multiline input support (shift+enter and ctrl+enter) | ### Permissions | Command | What It Does | |---------|--------------| -| `/allow-all` | Auto-approve all permission prompts for this session | | `/add-dir ` | Add a directory to allowed list | -| `/list-dirs` | Show all allowed directories | +| `/allow-all [on\|off\|show]` | Auto-approve all permission prompts; use `on` to enable, `off` to disable, `show` to check current status | +| `/yolo` | Quick alias for `/allow-all on` โ€” auto-approves all permission prompts. | | `/cwd`, `/cd [directory]` | View or change working directory | +| `/list-dirs` | Show all allowed directories | -> โš ๏ธ **Use with caution**: `/allow-all` skips confirmation prompts. Great for trusted projects, but be careful with untrusted code. +> โš ๏ธ **Use with caution**: `/allow-all` and `/yolo` skip confirmation prompts. Great for trusted projects, but be careful with untrusted code. ### Session | Command | What It Does | |---------|--------------| -| `/resume` | Switch to a different session (optionally specify session ID) | -| `/rename` | Rename the current session | +| `/clear` | Abandons the current session (no history saved) and starts a fresh conversation | +| `/compact` | Summarize conversation to reduce context usage (optionally add focus instructions, e.g. `/compact focus on the bug list`) | | `/context` | Show context window token usage and visualization | -| `/usage` | Display session usage metrics and statistics | -| `/session` | Show session info and workspace summary | -| `/compact` | Summarize conversation to reduce context usage | -| `/share` | Export session as markdown file or GitHub gist | +| `/keep-alive` | Prevent your system from sleeping while Copilot CLI is active โ€” handy for long-running tasks on a laptop | +| `/memory [on\|off\|show]` | Enable, disable, or view persistent memory โ€” facts and preferences remembered across all sessions | +| `/new` | Ends the current session (saving it to history for search/resume) and starts a fresh conversation. | +| `/resume` | Switch to a different session (optionally specify session ID or name) | +| `/rename` | Rename the current session (omit the name to auto-generate one) | +| `/rewind` | Open a timeline picker to roll back to any earlier point in the conversation | +| `/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
--- # Practice -Warm desk setup with monitor showing code, lamp, coffee cup, and headphones ready for hands-on practice +Warm desk setup with monitor showing code, lamp, coffee cup, and headphones ready for hands-on practice Time to put what you've learned into action. @@ -544,6 +592,28 @@ After completing the demos, try these variations: --- +## ๐Ÿ’ก Tip: Control Your CLI Session from Web or Mobile + +GitHub Copilot CLI supports **remote sessions**, letting you monitor and interact with a running CLI session from a web browser (on desktop or mobile) or the GitHub Mobile app without being physically at your terminal. + +Start a remote session with the `--remote` flag: + +```bash +copilot --remote +``` + +Copilot CLI will display a link and provide access to a QR code. Open the link on your phone or in a desktop browser tab to watch the session in real time, send follow-up prompts, review plans, and steer the agent remotely. Sessions are user-specific so you can only access your own Copilot CLI sessions. + +You can also enable remote access from inside an active session at any time: + +``` +> /remote +``` + +Additional details about remote sessions can be found in the [Copilot CLI docs](https://docs.github.com/copilot/how-tos/copilot-cli/steer-remotely). + +--- + ## ๐Ÿ“ Assignment ### Main Challenge: Improve the Book App Utilities @@ -551,7 +621,7 @@ After completing the demos, try these variations: The hands-on examples focused on reviewing and refactoring `book_app.py`. Now practice the same skills on a different file, `utils.py`: 1. Start an interactive session: `copilot` -2. Ask Copilot CLI to summarize the file: `@samples/book-app-project/utils.py What does each function in this file do?` +2. Ask Copilot CLI to summarize the file: "Summarize @samples/book-app-project/utils.py and explain what each function in this file does" 3. Ask it to add input validation: "Add validation to `get_user_choice()` so it handles empty input and non-numeric entries" 4. Ask it to improve error handling: "What happens if `get_book_details()` receives an empty string for the title? Add guards for that." 5. Ask for a docstring: "Add a comprehensive docstring to `get_book_details()` with parameter descriptions and return values" @@ -599,7 +669,8 @@ The examples used `/plan` for a search feature and `-p` for batch reviews. Now t |---------|--------------|-----| | Typing `exit` instead of `/exit` | Copilot CLI treats "exit" as a prompt, not a command | Slash commands always start with `/` | | Using `-p` for multi-turn conversations | Each `-p` call is isolated with no memory of previous calls | Use interactive mode (`copilot`) for conversations that build on context | -| Forgetting quotes around prompts with `$` or `!` | Shell interprets special characters before Copilot CLI sees them | Wrap prompts in quotes: `copilot -p "What does $HOME mean?"` | +| Forgetting quotes around prompts with `$` or `!` | Shell interprets special characters before Copilot CLI sees them | Wrap prompts in single quotes: `copilot -p 'What does $HOME mean?'` | +| Pressing Esc once to cancel a running task | A single Esc no longer cancels in-flight work (to prevent accidents) | Press **Esc twice** to cancel while Copilot CLI is processing | ### Troubleshooting @@ -620,7 +691,7 @@ The examples used `/plan` for a search feature and `-p` for batch reviews. Now t 1. **Interactive mode** is for exploration and iteration - context carries forward. It's like having a conversation with someone who remembers what you've said up to that point. 2. **Plan mode** is normally for more involved tasks. Review before implementation. 3. **Programmatic mode** is for automation. No interaction needed. -4. **Four essential commands** (`/help`, `/clear`, `/plan`, `/exit`) cover most daily use. +4. **Essential commands** (`/ask`, `/help`, `/clear`, `/plan`, `/research`, `/model`, `/exit`) cover most daily use. > ๐Ÿ“‹ **Quick Reference**: See the [GitHub Copilot CLI command reference](https://docs.github.com/en/copilot/reference/cli-command-reference) for a complete list of commands and shortcuts. diff --git a/01-setup-and-first-steps/images/chapter-header.png b/01-setup-and-first-steps/assets/chapter-header.png similarity index 100% rename from 01-setup-and-first-steps/images/chapter-header.png rename to 01-setup-and-first-steps/assets/chapter-header.png diff --git a/01-setup-and-first-steps/images/code-review-demo.gif b/01-setup-and-first-steps/assets/code-review-demo.gif similarity index 100% rename from 01-setup-and-first-steps/images/code-review-demo.gif rename to 01-setup-and-first-steps/assets/code-review-demo.gif diff --git a/01-setup-and-first-steps/images/code-review-demo.tape b/01-setup-and-first-steps/assets/code-review-demo.tape similarity index 100% rename from 01-setup-and-first-steps/images/code-review-demo.tape rename to 01-setup-and-first-steps/assets/code-review-demo.tape diff --git a/01-setup-and-first-steps/images/explain-code-demo.gif b/01-setup-and-first-steps/assets/explain-code-demo.gif similarity index 100% rename from 01-setup-and-first-steps/images/explain-code-demo.gif rename to 01-setup-and-first-steps/assets/explain-code-demo.gif diff --git a/01-setup-and-first-steps/images/explain-code-demo.tape b/01-setup-and-first-steps/assets/explain-code-demo.tape similarity index 100% rename from 01-setup-and-first-steps/images/explain-code-demo.tape rename to 01-setup-and-first-steps/assets/explain-code-demo.tape diff --git a/01-setup-and-first-steps/images/first-copilot-experience.png b/01-setup-and-first-steps/assets/first-copilot-experience.png similarity index 100% rename from 01-setup-and-first-steps/images/first-copilot-experience.png rename to 01-setup-and-first-steps/assets/first-copilot-experience.png diff --git a/01-setup-and-first-steps/images/generate-code-demo.gif b/01-setup-and-first-steps/assets/generate-code-demo.gif similarity index 100% rename from 01-setup-and-first-steps/images/generate-code-demo.gif rename to 01-setup-and-first-steps/assets/generate-code-demo.gif diff --git a/01-setup-and-first-steps/images/generate-code-demo.tape b/01-setup-and-first-steps/assets/generate-code-demo.tape similarity index 100% rename from 01-setup-and-first-steps/images/generate-code-demo.tape rename to 01-setup-and-first-steps/assets/generate-code-demo.tape diff --git a/01-setup-and-first-steps/images/interactive-mode.png b/01-setup-and-first-steps/assets/interactive-mode.png similarity index 100% rename from 01-setup-and-first-steps/images/interactive-mode.png rename to 01-setup-and-first-steps/assets/interactive-mode.png diff --git a/01-setup-and-first-steps/images/modes-and-commands.png b/01-setup-and-first-steps/assets/modes-and-commands.png similarity index 100% rename from 01-setup-and-first-steps/images/modes-and-commands.png rename to 01-setup-and-first-steps/assets/modes-and-commands.png diff --git a/01-setup-and-first-steps/images/ordering-food-analogy.png b/01-setup-and-first-steps/assets/ordering-food-analogy.png similarity index 100% rename from 01-setup-and-first-steps/images/ordering-food-analogy.png rename to 01-setup-and-first-steps/assets/ordering-food-analogy.png diff --git a/01-setup-and-first-steps/images/plan-mode.png b/01-setup-and-first-steps/assets/plan-mode.png similarity index 100% rename from 01-setup-and-first-steps/images/plan-mode.png rename to 01-setup-and-first-steps/assets/plan-mode.png diff --git a/01-setup-and-first-steps/images/programmatic-mode.png b/01-setup-and-first-steps/assets/programmatic-mode.png similarity index 100% rename from 01-setup-and-first-steps/images/programmatic-mode.png rename to 01-setup-and-first-steps/assets/programmatic-mode.png diff --git a/02-context-conversations/README.md b/02-context-conversations/README.md index 8aa50233..60397cbb 100644 --- a/02-context-conversations/README.md +++ b/02-context-conversations/README.md @@ -1,4 +1,15 @@ -![Chapter 02: Context and Conversations](images/chapter-header.png) + + +![Chapter 02: Context and Conversations](assets/chapter-header.png) > **What if AI could see your entire codebase, not just one file at a time?** @@ -20,7 +31,7 @@ By the end of this chapter, you'll be able to: ## ๐Ÿงฉ Real-World Analogy: Working with a Colleague -Context Makes the Difference - Without vs With Context +Context Makes the Difference - Without vs With Context *Just like your colleagues, Copilot CLI isn't a mind reader. Providing more information helps humans and Copilot alike provide targeted support!* @@ -36,7 +47,7 @@ To provide context to Copilot CLI use *the `@` syntax* to point Copilot CLI at s # Essential: Basic Context -Glowing code blocks connected by light trails representing how context flows through Copilot CLI conversations +Glowing code blocks connected by light trails representing how context flows through Copilot CLI conversations This section covers everything you need to work effectively with context. Master these basics first. @@ -89,7 +100,7 @@ copilot
๐ŸŽฌ See it in action! -![File Context Demo](images/file-context-demo.gif) +![File Context Demo](assets/file-context-demo.gif) *Demo output varies. Your model, tools, and responses will differ from what's shown here.* @@ -119,7 +130,7 @@ copilot This is where context becomes a superpower. Single-file analysis is useful. Cross-file analysis is transformative. -Cross-File Intelligence - comparing single-file vs cross-file analysis showing how analyzing files together reveals bugs, data flow, and patterns invisible in isolation +Cross-File Intelligence - comparing single-file vs cross-file analysis showing how analyzing files together reveals bugs, data flow, and patterns invisible in isolation ### Demo: Find Bugs That Span Multiple Files @@ -142,7 +153,7 @@ copilot
๐ŸŽฌ See it in action! -![Multi-File Demo](images/multi-file-demo.gif) +![Multi-File Demo](assets/multi-file-demo.gif) *Demo output varies. Your model, tools, and responses will differ from what's shown here.* @@ -185,7 +196,7 @@ Cross-Module Analysis ### Demo: Understand a Codebase in 60 Seconds -Split-screen comparison showing manual code review taking 1 hour versus AI-assisted analysis taking 10 seconds +Split-screen comparison showing manual code review taking 1 hour versus AI-assisted analysis taking 10 seconds New to a project? Learn about it quickly using Copilot CLI. @@ -253,7 +264,7 @@ copilot
๐ŸŽฌ See a multi-turn conversation in action! -![Multi-Turn Demo](images/multi-turn-demo.gif) +![Multi-Turn Demo](assets/multi-turn-demo.gif) *Demo output varies. Your model, tools, and responses will differ from what's shown here.* @@ -303,13 +314,19 @@ copilot --continue # Pick from a list of sessions interactively copilot --resume +# -r is a shorthand for --resume (saves some typing!) +copilot -r + # Or resume a specific session by ID -copilot --resume abc123 +copilot --resume=abc123 + +# Or resume by the name you gave the session +copilot --resume="my book app review" ``` > ๐Ÿ’ก **How do I find a session ID?** You don't need to memorize them. Running `copilot --resume` without an ID shows an interactive list of your previous sessions, their names, IDs, and when they were last active. Just pick the one you want. > -> **What about multiple terminals?** Each terminal window is its own session with its own context. If you have Copilot CLI open in three terminals, that's three separate sessions. Running `--resume` from any terminal lets you browse all of them. The `--continue` flag grabs whichever session was closed most recently, no matter which terminal it was in. +> **What about multiple terminals?** Each terminal window is its own session with its own context. If you have Copilot CLI open in three terminals, that's three separate sessions. Running `--resume` from any terminal lets you browse all of them. The `--continue` flag grabs the session from the current working directory first; if none exists there, it picks the most recently active session. > > **Can I switch sessions without restarting?** Yes. Use the `/resume` slash command from inside an active session: > ``` @@ -319,46 +336,94 @@ copilot --resume abc123 ### Organize Your Sessions -Give sessions meaningful names so you can find them later: +Give sessions meaningful names so you can find them later. You can name a session when you start it, or rename it at any time while inside the session: ```bash +# Name a session right when you start it +copilot --name book-app-review + +# Or rename the current session from inside copilot > /rename book-app-review # Session renamed for easier identification ``` +Once a session is named, you can resume it directly by name without browsing through a list: + +```bash +copilot --resume=book-app-review +``` + +To clean up sessions you no longer need, use `/session delete` from inside a session: + +```bash +copilot + +> /session delete # Deletes the current session +> /session delete abc123 # Deletes a specific session by ID +> /session delete-all # Deletes all sessions (use with care!) +``` + +### Persistent Memory Across Sessions + +Sessions save your conversation history, but **memory** goes one step further and lets Copilot CLI remember preferences and facts *across all sessions*, not just within a single one. + +```bash +copilot + +> /memory show +# Shows what Copilot CLI currently remembers about you and your project + +> /memory on +# Enables memory (on by default if your account supports it) + +> /memory off +# Disables memory (useful if you prefer a fresh slate each time) +``` + +For example, if you tell Copilot CLI "I always prefer pytest for Python testing", it can remember that preference and apply it automatically in future sessions. All without you having to repeat it. + +> ๐Ÿ’ก **Memory vs. Sessions**: Sessions save conversation history so you can resume a specific task. Memory saves reusable repository facts and user preferences that Copilot can apply in future work. Think of sessions as task notebooks, and memory as reusable context Copilot can carry forward. + ### Check and Manage Context -As you add files and conversation, Copilot CLI's [context window](../GLOSSARY.md#context-window) fills up. Two commands help you stay in control: +As you add files and conversation, Copilot CLI's [context window](../GLOSSARY.md#context-window) fills up. Several commands are available to help you stay in control: ```bash copilot > /context -Context usage: 45,000 / 128,000 tokens (35%) +Context usage: 62k/200k tokens (31%) > /clear -# Wipes context and starts fresh. Use when switching topics +# Abandons the current session (no history saved) and starts a fresh conversation + +> /new +# Ends the current session (saving it to history for search/resume) and starts a fresh conversation + +> /rewind +# Opens a timeline picker allowing you to roll back to an earlier point in your conversation ``` -> ๐Ÿ’ก **When to use `/clear`**: If you've been reviewing `books.py` and want to switch to discussing `utils.py`, run `/clear` first. Otherwise stale context from the old topic may confuse responses. +> ๐Ÿ’ก **When to use `/clear` or `/new`**: If you've been reviewing books.py and want to switch to discussing utils.py, run /new first (or /clear if you don't need the session history). Otherwise stale context from the old topic may confuse responses. + +> ๐Ÿ’ก **Made a mistake or want to try a different approach?** Use `/rewind` (or press Esc twice) to open a **timeline picker** that lets you roll back to any earlier point in your conversation, not just the most recent one. This is useful when you went down the wrong path and want to backtrack without starting over entirely. --- ### Pick Up Where You Left Off -Timeline showing how GitHub Copilot CLI sessions persist across days - start on Monday, resume on Wednesday with full context restored +Timeline showing how GitHub Copilot CLI sessions persist across days - start on Monday, resume on Wednesday with full context restored *Sessions auto-save when you exit. Resume days later with full context: files, issues, and progress all remembered.* Imagine this workflow across multiple days: ```bash -# Monday: Start book app review -copilot +# Monday: Start book app review with a name right from the beginning +copilot --name book-app-review -> /rename book-app-review > @samples/book-app-project/books.py > Review and number all code quality issues @@ -376,8 +441,8 @@ Quality Issues Found: ``` ```bash -# Wednesday: Resume exactly where you left off -copilot --continue +# Wednesday: Resume exactly where you left off, by name +copilot --resume=book-app-review > What issues remain unfixed from our book app review? @@ -402,13 +467,13 @@ No re-explaining. No re-reading files. Just continue working. --- -**๐ŸŽ‰ You now know the essentials!** The `@` syntax, session management (`--continue`/`--resume`/`/rename`), and context commands (`/context`/`/clear`) are enough to be highly productive. Everything below is optional. Return to it when you're ready. +**๐ŸŽ‰ You now know the essentials!** The `@` syntax, session management (`--name`/`--continue`/`--resume`/`/rename`), and context commands (`/context`/`/clear`) are enough to be highly productive. Everything below is optional. Return to it when you're ready. --- # Optional: Going Deeper -Abstract crystal cave in blue and purple tones representing deeper exploration of context concepts +Abstract crystal cave in blue and purple tones representing deeper exploration of context concepts These topics build on the essentials above. **Pick what interests you, or skip ahead to [Practice](#practice).** @@ -462,6 +527,10 @@ copilot > /share gist # Creates a GitHub gist with the session + +> /share html +# Exports session as a self-contained interactive HTML file +# Useful for sharing polished session reports with teammates or saving for reference ```
@@ -516,7 +585,7 @@ You already know `/context` and `/clear` from the essentials. Here's the deeper Every AI has a "context window," which is the amount of text it can consider at once. -Context Window Visualization +Context Window Visualization *The context window is like a desk: it can only hold so much at once. Files, conversation history, and system prompts all take space.* @@ -554,20 +623,32 @@ copilot # Your key findings and decisions are preserved ``` +You can also give `/compact` optional focus instructions to shape what gets prioritized in the summary: + +```bash +copilot + +> /compact focus on the list of bugs we found and decisions made +# Summarizes history, keeping bug list and decisions prominent +``` + +> ๐Ÿ’ก **When to use focus instructions**: If your conversation covered many topics, focus instructions help `/compact` retain the parts most relevant to your next steps so you don't lose the thread. + #### Context Efficiency Tips | Situation | Action | Why | |-----------|--------|-----| | Starting new topic | `/clear` | Removes irrelevant context | +| Went down wrong path | `/rewind` | Roll back to any earlier point | | Long conversation | `/compact` | Summarizes history, frees tokens | | Need specific file | `@file.py` not `@folder/` | Loads only what you need | -| Hitting limits | Start new session | Fresh 128K context | +| Hitting limits | `/new` or `/clear` | Fresh context | | Multiple topics | Use `/rename` per topic | Easy to resume right session | #### Best Practices for Large Codebases 1. **Be specific**: `@samples/book-app-project/books.py` instead of `@samples/book-app-project/` -2. **Clear between topics**: Use `/clear` when switching focus +2. **Clear context between topics**: Use `/new` or `/clear` when switching focus 3. **Use `/compact`**: Summarize conversation to free up context 4. **Use multiple sessions**: One session per feature or topic @@ -667,9 +748,9 @@ You can include images in your conversations using the `@` syntax, or simply **p ```bash copilot -> @images/screenshot.png What is happening in this image? +> @assets/screenshot.png What is happening in this image? -> @images/mockup.png Write the HTML and CSS to match this design. Place it in a new file called index.html and put the CSS in styles.css. +> @assets/mockup.png Write the HTML and CSS to match this design. Place it in a new file called index.html and put the CSS in styles.css. ``` > ๐Ÿ“– **Learn more**: See [Additional Context Features](../appendices/additional-context.md#working-with-images) for supported formats, practical use cases, and tips for combining images with code. @@ -680,7 +761,7 @@ copilot # Practice -Warm desk setup with monitor showing code, lamp, coffee cup, and headphones ready for hands-on practice +Warm desk setup with monitor showing code, lamp, coffee cup, and headphones ready for hands-on practice Time to apply your context and session management skills. @@ -845,10 +926,11 @@ copilot --add-dir /path/to/directory 1. **`@` syntax** gives Copilot CLI context about files, directories, and images 2. **Multi-turn conversations** build on each other as context accumulates -3. **Sessions auto-save**: use `--continue` or `--resume` to pick up where you left off -4. **Context windows** have limits: manage them with `/context`, `/clear`, and `/compact` -5. **Permission flags** (`--add-dir`, `--allow-all`) control multi-directory access. Use them wisely! -6. **Image references** (`@screenshot.png`) help debug UI issues visually +3. **Sessions auto-save**: name them at startup with `--name`, resume by name with `--resume=`, or use `--continue` to pick up the most recent session +4. **Context windows** have limits: manage them with `/clear`, `/compact`, `/context`, `/new`, and `/rewind`. Use `/compact focus on ` to shape what gets kept in the summary +5. **Persistent memory** (`/memory`) lets Copilot CLI remember preferences and facts across *all* sessions โ€” not just the current one +6. **Permission flags** (`--add-dir`, `--allow-all`) control multi-directory access. Use them wisely! +7. **Image references** (`@screenshot.png`) help debug UI issues visually > ๐Ÿ“š **Official Documentation**: [Use Copilot CLI](https://docs.github.com/copilot/how-tos/copilot-cli/use-copilot-cli) for the complete reference on context, sessions, and working with files. diff --git a/02-context-conversations/images/chapter-header.png b/02-context-conversations/assets/chapter-header.png similarity index 100% rename from 02-context-conversations/images/chapter-header.png rename to 02-context-conversations/assets/chapter-header.png diff --git a/02-context-conversations/images/codebase-understanding.png b/02-context-conversations/assets/codebase-understanding.png similarity index 100% rename from 02-context-conversations/images/codebase-understanding.png rename to 02-context-conversations/assets/codebase-understanding.png diff --git a/02-context-conversations/images/colleague-context-analogy.png b/02-context-conversations/assets/colleague-context-analogy.png similarity index 100% rename from 02-context-conversations/images/colleague-context-analogy.png rename to 02-context-conversations/assets/colleague-context-analogy.png diff --git a/02-context-conversations/images/context-window-visualization.png b/02-context-conversations/assets/context-window-visualization.png similarity index 100% rename from 02-context-conversations/images/context-window-visualization.png rename to 02-context-conversations/assets/context-window-visualization.png diff --git a/02-context-conversations/images/cross-file-intelligence.png b/02-context-conversations/assets/cross-file-intelligence.png similarity index 100% rename from 02-context-conversations/images/cross-file-intelligence.png rename to 02-context-conversations/assets/cross-file-intelligence.png diff --git a/02-context-conversations/images/essential-basic-context.png b/02-context-conversations/assets/essential-basic-context.png similarity index 100% rename from 02-context-conversations/images/essential-basic-context.png rename to 02-context-conversations/assets/essential-basic-context.png diff --git a/02-context-conversations/images/file-context-demo.gif b/02-context-conversations/assets/file-context-demo.gif similarity index 100% rename from 02-context-conversations/images/file-context-demo.gif rename to 02-context-conversations/assets/file-context-demo.gif diff --git a/02-context-conversations/images/file-context-demo.tape b/02-context-conversations/assets/file-context-demo.tape similarity index 100% rename from 02-context-conversations/images/file-context-demo.tape rename to 02-context-conversations/assets/file-context-demo.tape diff --git a/02-context-conversations/images/multi-file-demo.gif b/02-context-conversations/assets/multi-file-demo.gif similarity index 100% rename from 02-context-conversations/images/multi-file-demo.gif rename to 02-context-conversations/assets/multi-file-demo.gif diff --git a/02-context-conversations/images/multi-file-demo.tape b/02-context-conversations/assets/multi-file-demo.tape similarity index 100% rename from 02-context-conversations/images/multi-file-demo.tape rename to 02-context-conversations/assets/multi-file-demo.tape diff --git a/02-context-conversations/images/multi-turn-demo.gif b/02-context-conversations/assets/multi-turn-demo.gif similarity index 100% rename from 02-context-conversations/images/multi-turn-demo.gif rename to 02-context-conversations/assets/multi-turn-demo.gif diff --git a/02-context-conversations/images/multi-turn-demo.tape b/02-context-conversations/assets/multi-turn-demo.tape similarity index 100% rename from 02-context-conversations/images/multi-turn-demo.tape rename to 02-context-conversations/assets/multi-turn-demo.tape diff --git a/02-context-conversations/images/optional-going-deeper.png b/02-context-conversations/assets/optional-going-deeper.png similarity index 100% rename from 02-context-conversations/images/optional-going-deeper.png rename to 02-context-conversations/assets/optional-going-deeper.png diff --git a/02-context-conversations/images/session-persistence-timeline.png b/02-context-conversations/assets/session-persistence-timeline.png similarity index 100% rename from 02-context-conversations/images/session-persistence-timeline.png rename to 02-context-conversations/assets/session-persistence-timeline.png diff --git a/03-development-workflows/README.md b/03-development-workflows/README.md index d78affc1..0116c50f 100644 --- a/03-development-workflows/README.md +++ b/03-development-workflows/README.md @@ -1,4 +1,15 @@ -![Chapter 03: Development Workflows](images/chapter-header.png) + + +![Chapter 03: Development Workflows](assets/chapter-header.png) > **What if the AI could find bugs you didn't even know to ask about?** @@ -22,7 +33,7 @@ By the end of this chapter, you'll be able to: A carpenter doesn't just know how to use tools, they have *workflows* for different jobs: -Craftsman workshop showing three workflow lanes: Building Furniture (Measure, Cut, Assemble, Finish), Fixing Damage (Assess, Remove, Repair, Match), and Quality Check (Inspect, Test Joints, Check Alignment) +Craftsman workshop showing three workflow lanes: Building Furniture (Measure, Cut, Assemble, Finish), Fixing Damage (Assess, Remove, Repair, Match), and Quality Check (Inspect, Test Joints, Check Alignment) Similarly, developers have workflows for different tasks. GitHub Copilot CLI enhances each of these workflows, making you more efficient and effective in your daily coding tasks. @@ -30,7 +41,7 @@ Similarly, developers have workflows for different tasks. GitHub Copilot CLI enh # The Five Workflows -Five glowing neon icons representing code review, testing, debugging, refactoring, and git integration workflows +Five glowing neon icons representing code review, testing, debugging, refactoring, and git integration workflows Each workflow below is self-contained. Pick the ones that match your current needs, or work through them all. @@ -40,7 +51,7 @@ Each workflow below is self-contained. Pick the ones that match your current nee This chapter covers five workflows that developers typically use. **However, you don't need to read them all at once!** Each workflow is self-contained in a collapsible section below. Pick the ones that match what you need and that fits best with your current project. You can always come back and explore the others later. -Five Development Workflows: Code Review, Refactoring, Debugging, Test Generation, and Git Integration shown as horizontal swimlanes +Five Development Workflows: Code Review, Refactoring, Debugging, Test Generation, and Git Integration shown as horizontal swimlanes | I want to... | Jump to | |---|---| @@ -60,7 +71,7 @@ This chapter covers five workflows that developers typically use. **However, you
Workflow 1: Code Review - Review files, use the /review agent, create severity checklists -Code review workflow: review, identify issues, prioritize, generate checklist. +Code review workflow: review, identify issues, prioritize, generate checklist. ### Basic Review @@ -77,7 +88,7 @@ copilot
๐ŸŽฌ See it in action! -![Code Review Demo](images/code-review-demo.gif) +![Code Review Demo](assets/code-review-demo.gif) *Demo output varies. Your model, tools, and responses will differ from what's shown here.* @@ -185,7 +196,7 @@ copilot
Workflow 2: Refactoring - Restructure code, separate concerns, improve error handling -Refactoring workflow: assess code, plan changes, implement, verify behavior. +Refactoring workflow: assess code, plan changes, implement, verify behavior. ### Simple Refactoring @@ -210,7 +221,7 @@ copilot
๐ŸŽฌ See it in action! -![Refactor Demo](images/refactor-demo.gif) +![Refactor Demo](assets/refactor-demo.gif) *Demo output varies. Your model, tools, and responses will differ from what's shown here.* @@ -278,7 +289,7 @@ copilot
Workflow 3: Debugging - Track down bugs, security audits, trace issues across files -Debugging workflow: understand error, locate root cause, fix, test. +Debugging workflow: understand error, locate root cause, fix, test. ### Simple Debugging @@ -306,7 +317,7 @@ copilot
๐ŸŽฌ See it in action! -![Fix Bug Demo](images/fix-bug-demo.gif) +![Fix Bug Demo](assets/fix-bug-demo.gif) *Demo output varies. Your model, tools, and responses will differ from what's shown here.* @@ -420,7 +431,7 @@ copilot
Workflow 4: Test Generation - Generate comprehensive tests and edge cases automatically -Test Generation workflow: analyze function, generate tests, include edge cases, run. +Test Generation workflow: analyze function, generate tests, include edge cases, run. > **Try this first:** `@samples/book-app-project/books.py Generate pytest tests for all functions including edge cases` @@ -450,7 +461,7 @@ copilot
๐ŸŽฌ See it in action! -![Test Generation Demo](images/test-gen-demo.gif) +![Test Generation Demo](assets/test-gen-demo.gif) *Demo output varies. Your model, tools, and responses will differ from what's shown here.* @@ -571,9 +582,9 @@ copilot
-Workflow 5: Git Integration - Commit messages, PR descriptions, /delegate, and /diff +Workflow 5: Git Integration - Commit messages, PR descriptions, /pr, /delegate, /diff, and /branch -Git Integration workflow: stage changes, generate message, commit, create PR. +Git Integration workflow: stage changes, generate message, commit, create PR. > ๐Ÿ’ก **This workflow assumes basic git familiarity** (staging, committing, branches). If git is new to you, try the other four workflows first. @@ -604,7 +615,7 @@ copilot -p "Generate a conventional commit message for: $(git diff --staged)"
๐ŸŽฌ See it in action! -![Git Integration Demo](images/git-integration-demo.gif) +![Git Integration Demo](assets/git-integration-demo.gif) *Demo output varies. Your model, tools, and responses will differ from what's shown here.* @@ -637,6 +648,16 @@ Include: - Breaking changes? (yes/no)" ``` +### Using /pr in Interactive Mode for the Current Branch + +If you're working with a branch in Copilot CLI's interactive mode, you can use the `/pr` command to work with pull requests. Use `/pr` to view a PR, create a new PR, fix an existing PR, or let Copilot CLI auto-decide based on the branch state. + +```bash +copilot + +> /pr [view|create|fix|auto] +``` + ### Review Before Push Use `git diff main..HEAD` inside a `-p` prompt for a quick pre-push sanity check across all branch changes. @@ -649,7 +670,7 @@ $(git diff main..HEAD)" ### Using /delegate for Background Tasks -The `/delegate` command hands off work to the Copilot coding agent on GitHub. Use the `/delegate` slash command (or the `&` shortcut) to offload a well-defined task to a background agent. +The `/delegate` command hands off work to the GitHub Copilot cloud agent. Use the `/delegate` slash command (or the `&` shortcut) to offload a well-defined task to a background agent. ```bash copilot @@ -670,7 +691,7 @@ This is great for well-defined tasks you want completed while you focus on other ### Using /diff to Review Session Changes -The `/diff` command shows all changes made during your current session. Use this slash command to see a visual diff of everything Copilot CLI has modified before you commit. +The `/diff` command shows all changes made during your current session. Use this slash command to see a visual diff of everything Copilot CLI has modified before you commit. It also works in folders that aren't git repositories. ```bash copilot @@ -682,6 +703,28 @@ copilot # Great for reviewing before committing ``` +### Branching Your Session with /branch or /fork + +Sometimes you want to explore two different approaches to a problem without losing your original conversation. The `/branch` command (also available as `/fork`) creates a copy of your current session so you can try a different direction and then compare results. + +```bash +copilot + +> Fix the find_by_author function to support partial matches + +# You want to try a different approach โ€” branch first! +> /branch + +# Now you're in a new session copy. Try your alternative approach: +> Fix find_by_author using a different regex-based strategy + +# If you don't like the result, switch back to your original session using /session +``` + +> ๐Ÿ’ก **`/branch` and `/fork` are the same**: Both commands do identical things. `/branch` was added as a more intuitive name. Use whichever makes more sense to you. + +> ๐Ÿ’ก **When to branch**: Branching is great when you're unsure which approach is better and want to keep both options open. +
--- @@ -714,7 +757,7 @@ copilot > Users report: 'Finding books by author name doesn't work for partial names' > @samples/book-app-project/books.py Analyze and identify the likely cause -# 2. Debug the issue (continuing in same session) +# 2. Debug the issue and fix (continuing in same session) > Based on the analysis, show me the find_by_author function and explain the issue > Fix the find_by_author function to handle partial name matches @@ -726,10 +769,23 @@ copilot > - Case-insensitive matching > - Author name not found -# 4. Generate commit message +# Exit the interactive session + +> /exit + +# 4. Run git add + +# Stage the changes so git diff --staged has something to work with +git add . + +# 5. Generate commit message copilot -p "Generate commit message for: $(git diff --staged)" -# Output: "fix(books): support partial author name search" +# Example Output: "fix(books): support partial author name search" + +# 6. Commit changes (optional) + +git commit -m "" ``` ### Bug Fix Workflow Summary @@ -737,16 +793,17 @@ copilot -p "Generate commit message for: $(git diff --staged)" | Step | Action | Copilot Command | |------|--------|-----------------| | 1 | Understand the bug | `> [describe bug] @relevant-file.py Analyze the likely cause` | -| 2 | Get detailed analysis | `> Show me the function and explain the issue` | -| 3 | Implement the fix | `> Fix the [specific issue]` | -| 4 | Generate tests | `> Generate tests for [specific scenarios]` | -| 5 | Commit | `copilot -p "Generate commit message for: $(git diff --staged)"` | +| 2 | Analysis and fix | `> Show me the function and fix the issue` | +| 3 | Generate tests | `> Generate tests for [specific scenarios]` | +| 4 | Stage changes | `git add .` | +| 5 | Generate commit message | `copilot -p "Generate commit message for: $(git diff --staged)"` | +| 6 | Commit changes| `git commit -m ""` | --- # Practice -Warm desk setup with monitor showing code, lamp, coffee cup, and headphones ready for hands-on practice +Warm desk setup with monitor showing code, lamp, coffee cup, and headphones ready for hands-on practice Now it's your turn to apply these workflows. @@ -826,7 +883,7 @@ copilot The exercise shows developers how to use GitHub Copilot CLI to create issues, generate code, and collaborate from the terminal while building a Node.js calculator app. You'll install the CLI, use templates and agents, and practice iterative, command-line driven development. -##### [Start the "Create applications with the Copilot CLI" Skills Exercise](https://github.com/skills/create-applications-with-the-copilot-cli) +##### [Start the "Create applications with the Copilot CLI" Skills Exercise](https://github.com/skills/create-applications-with-the-copilot-cli) --- @@ -880,7 +937,7 @@ copilot ## ๐Ÿ”‘ Key Takeaways -Specialized Workflows for Every Task: Code Review, Refactoring, Debugging, Testing, and Git Integration +Specialized Workflows for Every Task: Code Review, Refactoring, Debugging, Testing, and Git Integration 1. **Code review** becomes comprehensive with specific prompts 2. **Refactoring** is safer when you generate tests first diff --git a/03-development-workflows/images/carpenter-workflow-steps.png b/03-development-workflows/assets/carpenter-workflow-steps.png similarity index 100% rename from 03-development-workflows/images/carpenter-workflow-steps.png rename to 03-development-workflows/assets/carpenter-workflow-steps.png diff --git a/03-development-workflows/images/chapter-header.png b/03-development-workflows/assets/chapter-header.png similarity index 100% rename from 03-development-workflows/images/chapter-header.png rename to 03-development-workflows/assets/chapter-header.png diff --git a/03-development-workflows/images/code-review-demo.gif b/03-development-workflows/assets/code-review-demo.gif similarity index 100% rename from 03-development-workflows/images/code-review-demo.gif rename to 03-development-workflows/assets/code-review-demo.gif diff --git a/03-development-workflows/images/code-review-demo.tape b/03-development-workflows/assets/code-review-demo.tape similarity index 100% rename from 03-development-workflows/images/code-review-demo.tape rename to 03-development-workflows/assets/code-review-demo.tape diff --git a/03-development-workflows/images/code-review-swimlane-single.png b/03-development-workflows/assets/code-review-swimlane-single.png similarity index 100% rename from 03-development-workflows/images/code-review-swimlane-single.png rename to 03-development-workflows/assets/code-review-swimlane-single.png diff --git a/03-development-workflows/images/debugging-swimlane-single.png b/03-development-workflows/assets/debugging-swimlane-single.png similarity index 100% rename from 03-development-workflows/images/debugging-swimlane-single.png rename to 03-development-workflows/assets/debugging-swimlane-single.png diff --git a/03-development-workflows/images/five-workflows-swimlane.png b/03-development-workflows/assets/five-workflows-swimlane.png similarity index 100% rename from 03-development-workflows/images/five-workflows-swimlane.png rename to 03-development-workflows/assets/five-workflows-swimlane.png diff --git a/03-development-workflows/images/five-workflows.png b/03-development-workflows/assets/five-workflows.png similarity index 100% rename from 03-development-workflows/images/five-workflows.png rename to 03-development-workflows/assets/five-workflows.png diff --git a/03-development-workflows/images/fix-bug-demo.gif b/03-development-workflows/assets/fix-bug-demo.gif similarity index 100% rename from 03-development-workflows/images/fix-bug-demo.gif rename to 03-development-workflows/assets/fix-bug-demo.gif diff --git a/03-development-workflows/images/fix-bug-demo.tape b/03-development-workflows/assets/fix-bug-demo.tape similarity index 100% rename from 03-development-workflows/images/fix-bug-demo.tape rename to 03-development-workflows/assets/fix-bug-demo.tape diff --git a/03-development-workflows/images/git-integration-demo.gif b/03-development-workflows/assets/git-integration-demo.gif similarity index 100% rename from 03-development-workflows/images/git-integration-demo.gif rename to 03-development-workflows/assets/git-integration-demo.gif diff --git a/03-development-workflows/images/git-integration-demo.tape b/03-development-workflows/assets/git-integration-demo.tape similarity index 100% rename from 03-development-workflows/images/git-integration-demo.tape rename to 03-development-workflows/assets/git-integration-demo.tape diff --git a/03-development-workflows/images/git-integration-swimlane-single.png b/03-development-workflows/assets/git-integration-swimlane-single.png similarity index 100% rename from 03-development-workflows/images/git-integration-swimlane-single.png rename to 03-development-workflows/assets/git-integration-swimlane-single.png diff --git a/03-development-workflows/images/refactor-demo.gif b/03-development-workflows/assets/refactor-demo.gif similarity index 100% rename from 03-development-workflows/images/refactor-demo.gif rename to 03-development-workflows/assets/refactor-demo.gif diff --git a/03-development-workflows/images/refactor-demo.tape b/03-development-workflows/assets/refactor-demo.tape similarity index 100% rename from 03-development-workflows/images/refactor-demo.tape rename to 03-development-workflows/assets/refactor-demo.tape diff --git a/03-development-workflows/images/refactoring-swimlane-single.png b/03-development-workflows/assets/refactoring-swimlane-single.png similarity index 100% rename from 03-development-workflows/images/refactoring-swimlane-single.png rename to 03-development-workflows/assets/refactoring-swimlane-single.png diff --git a/03-development-workflows/images/request.json b/03-development-workflows/assets/request.json similarity index 100% rename from 03-development-workflows/images/request.json rename to 03-development-workflows/assets/request.json diff --git a/03-development-workflows/images/specialized-workflows.png b/03-development-workflows/assets/specialized-workflows.png similarity index 100% rename from 03-development-workflows/images/specialized-workflows.png rename to 03-development-workflows/assets/specialized-workflows.png diff --git a/03-development-workflows/images/test-gen-demo.gif b/03-development-workflows/assets/test-gen-demo.gif similarity index 100% rename from 03-development-workflows/images/test-gen-demo.gif rename to 03-development-workflows/assets/test-gen-demo.gif diff --git a/03-development-workflows/images/test-gen-demo.tape b/03-development-workflows/assets/test-gen-demo.tape similarity index 100% rename from 03-development-workflows/images/test-gen-demo.tape rename to 03-development-workflows/assets/test-gen-demo.tape diff --git a/03-development-workflows/images/test-gen-swimlane-single.png b/03-development-workflows/assets/test-gen-swimlane-single.png similarity index 100% rename from 03-development-workflows/images/test-gen-swimlane-single.png rename to 03-development-workflows/assets/test-gen-swimlane-single.png diff --git a/04-agents-custom-instructions/README.md b/04-agents-custom-instructions/README.md index a84d8894..44c93a93 100644 --- a/04-agents-custom-instructions/README.md +++ b/04-agents-custom-instructions/README.md @@ -1,4 +1,15 @@ -![Chapter 04: Agents and Custom Instructions](images/chapter-header.png) + + +![Chapter 04: Agents and Custom Instructions](assets/chapter-header.png) > **What if you could hire a Python code reviewer, testing expert, and security reviewer... all in one tool?** @@ -32,7 +43,7 @@ When you need help with your house, you don't call one "general helper." You cal Agents work the same way. Instead of a generic AI, use agents that focus on specific tasks and know the right process to follow. Set up the instructions once, then reuse them whenever you need that specialty: code review, testing, security, documentation. -Hiring Specialists Analogy - Just as you call specialized tradespeople for house repairs, AI agents are specialized for specific tasks like code review, testing, security, and documentation +Hiring Specialists Analogy - Just as you call specialized tradespeople for house repairs, AI agents are specialized for specific tasks like code review, testing, security, and documentation --- @@ -105,7 +116,7 @@ What about the Task Agent? It works behind the scenes to manage and track what i You can simply define your own agents to be part of your workflow! Define once, then direct! -Four colorful AI robots standing together, each with different tools representing specialized agent capabilities +Four colorful AI robots standing together, each with different tools representing specialized agent capabilities ## ๐Ÿ—‚๏ธ Add your agents @@ -185,11 +196,13 @@ copilot --agent python-reviewer > ๐Ÿ’ก **Switching agents**: You can switch to a different agent at any time by using `/agent` or `--agent` again. To return to the standard Copilot CLI experience, use `/agent` and select **no agent**. +> ๐Ÿ’ก **Agent mode is session-scoped**: The agent you select applies only to the current session. When you start a new session with `/new`, `/clear`, or by opening a fresh terminal, Copilot returns to its default mode โ€” your agent selection does not carry over automatically. This means each session starts with a clean slate, which is a good habit to keep your work focused. + --- # Going Deeper with Agents -Robot being assembled on a workbench surrounded by components and tools representing custom agent creation +Robot being assembled on a workbench surrounded by components and tools representing custom agent creation > ๐Ÿ’ก **This section is optional.** The built-in agents (`/plan`, `/review`) are powerful enough for most workflows. Create custom agents when you need specialized expertise that's consistently applied across your work. @@ -337,7 +350,7 @@ copilot
๐ŸŽฌ See it in action! -![Python Reviewer Demo](images/python-reviewer-demo.gif) +![Python Reviewer Demo](assets/python-reviewer-demo.gif) *Demo output varies - your model, tools, and responses will differ from what's shown here.* @@ -386,7 +399,7 @@ Think of it this way: agents are specialists you call on, and instruction files You already know the two main locations (see [Where to put agent files](#where-to-put-agent-files) above). Use this decision tree to choose: -Decision tree for where to put agent files: experimenting โ†’ current folder, team use โ†’ .github/agents/, everywhere โ†’ ~/.copilot/agents/ +Decision tree for where to put agent files: experimenting โ†’ current folder, team use โ†’ .github/agents/, everywhere โ†’ ~/.copilot/agents/ **Start simple:** Create a single `*.agent.md` file in your project folder. Move it to a permanent location once you're happy with it. @@ -422,6 +435,7 @@ Copilot will scan your project and create tailored instruction files. You can ed | `AGENTS.md` | Project root or nested | **Cross-platform standard** - works with Copilot and other AI assistants | | `.github/copilot-instructions.md` | Project | GitHub Copilot specific | | `.github/instructions/*.instructions.md` | Project | Granular, topic-specific instructions | +| `~/.copilot/instructions/**/*.instructions.md` | User (all projects) | Personal instructions that apply everywhere, across all your repos | | `CLAUDE.md`, `GEMINI.md` | Project root | Supported for compatibility | > ๐ŸŽฏ **Just getting started?** Use `AGENTS.md` for project instructions. You can explore the other formats later as needed. @@ -430,7 +444,7 @@ Copilot will scan your project and create tailored instruction files. You can ed `AGENTS.md` is the recommended format. It's an [open standard](https://agents.md/) that works across Copilot and other AI coding tools. Place it in your repository root and Copilot reads it automatically. This project's own [AGENTS.md](../AGENTS.md) is a working example. -A typical `AGENTS.md` describes your project context, code style, security requirements, and testing standards. Use `/init` to generate one, or write your own following the pattern in our example file. +A typical `AGENTS.md` describes your project context, code style, security requirements, and testing standards. Write your own following the pattern in our example file. ### Custom Instruction Files (.instructions.md) @@ -446,6 +460,48 @@ For teams that want more granular control, split instructions into topic-specifi > ๐Ÿ’ก **Note**: Instruction files work with any language. This example uses Python to match our course project, but you can create similar files for TypeScript, Go, Rust, or any technology your team uses. +#### Scoping Instructions with `applyTo` + +By default, an instruction file applies to every conversation. To limit it to specific file types, add an `applyTo` field in YAML frontmatter (the block between `---` markers at the very top of the file): + +```markdown +--- +applyTo: "**/*.py" +--- +# Python Standards +Always follow PEP 8 style conventions. +Use type hints in all function signatures. +``` + +With `applyTo: "**/*.py"`, Copilot only loads that instruction file when you are working with Python files. Instructions for Python style never clutter a conversation about, say, a Dockerfile or a SQL query. + +Here are some common patterns: + +| `applyTo` value | When it applies | +|---|---| +| `"**/*.py"` | Any Python file | +| `"**/*.{ts,tsx}"` | TypeScript and TSX files | +| `"tests/**"` | Any file inside a `tests/` folder | +| (no frontmatter) | Every conversation โ€” the default | + +> ๐Ÿ’ก **Tip**: Wrap the glob pattern in quotes (e.g., `"**/*.py"`) to ensure it is interpreted correctly across all operating systems and shells. + +#### Importing Other Files with `@` + +You can reference another file inside `AGENTS.md` or any instruction file using `@filepath` syntax. Copilot expands the reference and includes that file's content automatically, so you can keep your main file short while storing the details elsewhere: + +```markdown + +# Project Instructions + +@.github/instructions/python-standards.instructions.md +@.github/instructions/test-standards.instructions.md +``` + +This is handy when your instructions grow large. Split them into focused files and `@`-import them from a single `AGENTS.md`. The same syntax works inside `.github/copilot-instructions.md` and other instruction files too. + +> ๐Ÿ’ก **Tip**: Use `@`-imports to share a common base file across multiple instruction files. For example, you could have a `@.github/instructions/shared-rules.md` that every other instruction file pulls in. + **Finding community instruction files**: Browse [github/awesome-copilot](https://github.com/github/awesome-copilot) for pre-made instruction files covering .NET, Angular, Azure, Python, Docker, and many more technologies. ### Disabling Custom Instructions @@ -539,7 +595,7 @@ For community agents, see [github/awesome-copilot](https://github.com/github/awe # Practice -Warm desk setup with monitor showing code, lamp, coffee cup, and headphones ready for hands-on practice +Warm desk setup with monitor showing code, lamp, coffee cup, and headphones ready for hands-on practice Create your own agents and see them in action. diff --git a/04-agents-custom-instructions/images/agent-file-placement-decision-tree.png b/04-agents-custom-instructions/assets/agent-file-placement-decision-tree.png similarity index 100% rename from 04-agents-custom-instructions/images/agent-file-placement-decision-tree.png rename to 04-agents-custom-instructions/assets/agent-file-placement-decision-tree.png diff --git a/04-agents-custom-instructions/images/chapter-header.png b/04-agents-custom-instructions/assets/chapter-header.png similarity index 100% rename from 04-agents-custom-instructions/images/chapter-header.png rename to 04-agents-custom-instructions/assets/chapter-header.png diff --git a/04-agents-custom-instructions/images/creating-custom-agents.png b/04-agents-custom-instructions/assets/creating-custom-agents.png similarity index 100% rename from 04-agents-custom-instructions/images/creating-custom-agents.png rename to 04-agents-custom-instructions/assets/creating-custom-agents.png diff --git a/04-agents-custom-instructions/images/hiring-specialists-analogy.png b/04-agents-custom-instructions/assets/hiring-specialists-analogy.png similarity index 100% rename from 04-agents-custom-instructions/images/hiring-specialists-analogy.png rename to 04-agents-custom-instructions/assets/hiring-specialists-analogy.png diff --git a/04-agents-custom-instructions/images/python-reviewer-demo.gif b/04-agents-custom-instructions/assets/python-reviewer-demo.gif similarity index 100% rename from 04-agents-custom-instructions/images/python-reviewer-demo.gif rename to 04-agents-custom-instructions/assets/python-reviewer-demo.gif diff --git a/04-agents-custom-instructions/images/python-reviewer-demo.tape b/04-agents-custom-instructions/assets/python-reviewer-demo.tape similarity index 100% rename from 04-agents-custom-instructions/images/python-reviewer-demo.tape rename to 04-agents-custom-instructions/assets/python-reviewer-demo.tape diff --git a/04-agents-custom-instructions/images/using-agents.png b/04-agents-custom-instructions/assets/using-agents.png similarity index 100% rename from 04-agents-custom-instructions/images/using-agents.png rename to 04-agents-custom-instructions/assets/using-agents.png diff --git a/05-skills/README.md b/05-skills/README.md index b2150150..7036d2c7 100644 --- a/05-skills/README.md +++ b/05-skills/README.md @@ -1,8 +1,19 @@ -![Chapter 05: Skills System](images/chapter-header.png) + + +![Chapter 05: Skills System](assets/chapter-header.png) > **What if Copilot could automatically apply your team's best practices without you having to explain them every time?** -In this chapter, you'll learn about Agent Skills: folders of instructions that Copilot automatically loads when relevant to your task. While agents change *how* Copilot thinks, skills teach Copilot *specific ways to complete tasks*. You'll create a security audit skill that Copilot applies whenever you ask about security, build team-standard review criteria that ensure consistent code quality, and learn how skills work across Copilot CLI, VS Code, and the Copilot coding agent. +In this chapter, you'll learn about Agent Skills: folders of instructions that Copilot automatically loads when relevant to your task. While agents change *how* Copilot thinks, skills teach Copilot *specific ways to complete tasks*. You'll create a security audit skill that Copilot applies whenever you ask about security, build team-standard review criteria that ensure consistent code quality, and learn how skills work across Copilot CLI, VS Code, and the GitHub Copilot cloud agent. ## ๐ŸŽฏ Learning Objectives @@ -21,7 +32,7 @@ By the end of this chapter, you'll be able to: ## ๐Ÿงฉ Real-World Analogy: Power Tools A general-purpose drill is useful, but specialized attachments make it powerful. -Power Tools - Skills Extend Copilot's Capabilities +Power Tools - Skills Extend Copilot's Capabilities Skills work the same way. Just like swapping drill bits for different jobs, you can add skills to Copilot for different tasks: @@ -41,7 +52,7 @@ Skills work the same way. Just like swapping drill bits for different jobs, you # How Skills Work -Glowing RPG-style skill icons connected by light trails on a starfield background representing Copilot skills +Glowing RPG-style skill icons connected by light trails on a starfield background representing Copilot skills Learn what skills are, why they matter, and how they differ from agents and MCP. @@ -54,7 +65,9 @@ Learn what skills are, why they matter, and how they differ from agents and MCP. copilot > /skills list ``` - This shows all skills Copilot can find in your project and personal folders. + This shows all skills Copilot can find, including any **built-in skills** that ship with the CLI itself, plus skills from your project and personal folders. + + > ๐Ÿ’ก **Built-in skills**: The Copilot CLI comes with skills pre-installed out of the box. For example, the `customizing-copilot-cloud-agents-environment` skill provides a guide for customizing the Copilot cloud agent's environment. You don't need to create or install anything to use these. Run `/skills list` to see what's available. 2. **Look at a real skill file:** Check out our provided [code-checklist SKILL.md](../.github/skills/code-checklist/SKILL.md) to see the pattern. It's just YAML frontmatter plus markdown instructions. @@ -99,6 +112,20 @@ While auto-triggering is the primary way skills work, you can also **invoke skil This gives you explicit control when you want to ensure a specific skill is used. +#### Combining Multiple Skills in One Message + +You can invoke **more than one skill in a single message**, and the skill slash command can appear anywhere in your prompt โ€” not just at the beginning. This is handy when you want two different checks done in one go: + +```bash +> Check @samples/book-app-project/book_app.py with /code-checklist and also run /generate-tests for it + +> Review the auth module /security-audit then /code-checklist the result +``` + +Copilot will apply each named skill in the same response, saving you from sending multiple separate messages. + +> ๐Ÿ’ก **Tip**: Put the skill slash commands wherever they feel most natural in your sentence. You can put them at the start, middle, or end of your message. + > ๐Ÿ“ **Skills vs Agents Invocation**: Don't confuse skill invocation with agent invocation: > - **Skills**: `/skill-name `, e.g., `/code-checklist Check this file` > - **Agents**: `/agent` (select from list) or `copilot --agent ` (command line) @@ -121,7 +148,7 @@ Skills are just one piece of GitHub Copilot's extensibility model. Here's how th > *Don't worry about MCP quite yet. We'll cover it in [Chapter 06](../06-mcp-servers/). It's included here so you can see how skills fit into the overall picture.* -Comparison diagram showing the differences between Agents, Skills, and MCP Servers and how they combine into your workflow +Comparison diagram showing the differences between Agents, Skills, and MCP Servers and how they combine into your workflow | Feature | What It Does | When to Use | |---------|--------------|-------------| @@ -176,7 +203,7 @@ copilot 3. Automatically loads your team's quality checklist 4. Applies all checks without you listing them -How Skills Auto-Trigger - 4-step flow showing how Copilot automatically matches your prompt to the right skill +How Skills Auto-Trigger - 4-step flow showing how Copilot automatically matches your prompt to the right skill *Just ask naturally. Copilot matches your prompt to the right skill and applies it automatically.* @@ -211,7 +238,7 @@ copilot
๐ŸŽฌ See it in action! -![Skill Trigger Demo](images/skill-trigger-demo.gif) +![Skill Trigger Demo](assets/skill-trigger-demo.gif) *Demo output varies. Your model, tools, and responses will differ from what's shown here.* @@ -259,7 +286,7 @@ PR Review: feature/user-auth # Creating Custom Skills -Human and robotic hands building a wall of glowing LEGO-like blocks representing skill creation and management +Human and robotic hands building a wall of glowing LEGO-like blocks representing skill creation and management Build your own skills from SKILL.md files. @@ -342,6 +369,9 @@ Provide issues as a numbered list with severity: | `name` | **Yes** | Unique identifier (lowercase, hyphens for spaces) | | `description` | **Yes** | What the skill does and when Copilot should use it | | `license` | No | License that applies to this skill | +| `argument-hint` | No | Short hint shown to users describing what argument the skill expects (e.g., `"file path or code snippet"`) | + +> ๐Ÿ’ก **What is `argument-hint`?** When users invoke a skill directly (e.g., `/security-audit`), the `argument-hint` text appears as a placeholder showing what to type next โ€” like a mini help prompt. For example, setting `argument-hint: "file path to review"` tells the user to provide a file path after the skill name. > ๐Ÿ“– **Official docs**: [About Agent Skills](https://docs.github.com/copilot/concepts/agents/about-agent-skills) @@ -456,13 +486,33 @@ copilot --agent code-reviewer Discover installed skills, find community skills, and share your own. -Managing and Sharing Skills - showing the discover, use, create, and share cycle for CLI skills +Managing and Sharing Skills - showing the discover, use, create, and share cycle for CLI skills --- -## Managing Skills with the `/skills` Command +## Managing Skills with the `copilot skill` Command and `/skills` + +Copilot CLI gives you two ways to manage skills. You can do it directly from the terminal before starting Copilot or from inside a Copilot session. + +### Option 1: `copilot skill` (Terminal Command) + +The `copilot skill` subcommand lets you manage skills directly from your terminal, without opening an interactive Copilot session. This is handy for scripting, quick checks, or adding skills before you start working. + +```bash +# See all installed skills +copilot skill list + +# Add a skill from a local file, URL, or directory +copilot skill add .github/skills/my-skill/SKILL.md +copilot skill add https://example.com/skills/security-audit/SKILL.md + +# Remove a skill by name +copilot skill remove security-audit +``` + +### Option 2: `/skills` (Inside Copilot Session) -Use the `/skills` command to manage your installed skills: +Once you're in an interactive Copilot session, use `/skills` (or its shortcut `/skill`) to manage skills without leaving: | Command | What It Does | |---------|--------------| @@ -472,11 +522,23 @@ Use the `/skills` command to manage your installed skills: | `/skills remove ` | Disable or uninstall a skill | | `/skills reload` | Reload skills after editing SKILL.md files | +> ๐Ÿ’ก **`/skill` shortcut**: You can type `/skill` instead of `/skills` โ€” they're interchangeable. For example, `/skill list` works the same as `/skills list`. + > ๐Ÿ’ก **Remember**: You don't need to "activate" skills for each prompt. Once installed, skills are **automatically triggered** when your prompt matches their description. These commands are for managing which skills are available, not for using them. ### Example: View Your Skills ```bash +# From the terminal (no interactive session needed): +copilot skill list + +Available skills: +- security-audit: Security-focused code review checking OWASP Top 10 +- generate-tests: Generate comprehensive unit tests with edge cases +- code-checklist: Team code quality checklist +... + +# Or from inside a Copilot session: copilot > /skills list @@ -500,7 +562,7 @@ Description: Security-focused code review checking OWASP Top 10 vulnerabilities
See it in action! -![List Skills Demo](images/list-skills-demo.gif) +![List Skills Demo](assets/list-skills-demo.gif) *Demo output varies. Your model, tools, and responses will differ from what's shown here.* @@ -544,7 +606,13 @@ copilot # Install a plugin from the marketplace ``` -Plugins can bundle multiple capabilities together - a single plugin might include related skills, agents, and MCP server configurations that work together. +To keep your local plugin catalog current, refresh it with: + +```bash +copilot plugin marketplace update +``` + +Plugins can bundle multiple capabilities together. A single plugin might include related skills, agents, and MCP server configurations that work together. ### Community Skill Repositories @@ -552,28 +620,28 @@ Pre-made skills are also available from community repositories: - **[Awesome Copilot](https://github.com/github/awesome-copilot)** - Official GitHub Copilot resources including skills documentation and examples -### Installing a Community Skill Manually +### Installing a Community Skill with GitHub CLI -If you find a skill in a GitHub repository, copy its folder into your skills directory: +The easiest way to install a skill from a GitHub repository is using the `gh skill install` command (requires [GitHub CLI v2.90.0+](https://github.blog/changelog/2026-04-16-manage-agent-skills-with-github-cli/)): ```bash -# Clone the awesome-copilot repository -git clone https://github.com/github/awesome-copilot.git /tmp/awesome-copilot +# Browse and interactively select a skill from awesome-copilot +gh skill install github/awesome-copilot -# Copy a specific skill to your project -cp -r /tmp/awesome-copilot/skills/code-checklist .github/skills/ +# Or install a specific skill directly +gh skill install github/awesome-copilot ai-ready -# Or for personal use across all projects -cp -r /tmp/awesome-copilot/skills/code-checklist ~/.copilot/skills/ +# Install for personal use across all projects (user scope) +gh skill install github/awesome-copilot ai-ready --scope user ``` -> โš ๏ธ **Review before installing**: Always read a skill's `SKILL.md` before copying it into your project. Skills control what Copilot does, and a malicious skill could instruct it to run harmful commands or modify code in unexpected ways. +> โš ๏ธ **Review before installing**: Always read a skill's `SKILL.md` before installing it. Skills control what Copilot does, and a malicious skill could instruct it to run harmful commands or modify code in unexpected ways. --- # Practice -Warm desk setup with monitor showing code, lamp, coffee cup, and headphones ready for hands-on practice +Warm desk setup with monitor showing code, lamp, coffee cup, and headphones ready for hands-on practice Apply what you've learned by building and testing your own skills. @@ -840,9 +908,10 @@ Run `/skills reload` after creating or editing skills to ensure changes are pick 1. **Skills are automatic**: Copilot loads them when your prompt matches the skill's description 2. **Direct invocation**: You can also invoke skills directly with `/skill-name` as a slash command -3. **SKILL.md format**: YAML frontmatter (name, description, optional license) plus markdown instructions +3. **SKILL.md format**: YAML frontmatter (name, description, optional license, argument-hint) plus markdown instructions 4. **Location matters**: `.github/skills/` for project/team sharing, `~/.copilot/skills/` for personal use 5. **Description is key**: Write descriptions that match how you naturally ask questions +6. **Two ways to manage skills**: Use `copilot skill` from the terminal or `/skills` (shortcut: `/skill`) inside a session > ๐Ÿ“‹ **Quick Reference**: See the [GitHub Copilot CLI command reference](https://docs.github.com/en/copilot/reference/cli-command-reference) for a complete list of commands and shortcuts. diff --git a/05-skills/images/chapter-header.png b/05-skills/assets/chapter-header.png similarity index 100% rename from 05-skills/images/chapter-header.png rename to 05-skills/assets/chapter-header.png diff --git a/05-skills/images/creating-managing-skills.png b/05-skills/assets/creating-managing-skills.png similarity index 100% rename from 05-skills/images/creating-managing-skills.png rename to 05-skills/assets/creating-managing-skills.png diff --git a/05-skills/images/how-skills-work.png b/05-skills/assets/how-skills-work.png similarity index 100% rename from 05-skills/images/how-skills-work.png rename to 05-skills/assets/how-skills-work.png diff --git a/05-skills/images/list-skills-demo.gif b/05-skills/assets/list-skills-demo.gif similarity index 100% rename from 05-skills/images/list-skills-demo.gif rename to 05-skills/assets/list-skills-demo.gif diff --git a/05-skills/images/list-skills-demo.tape b/05-skills/assets/list-skills-demo.tape similarity index 100% rename from 05-skills/images/list-skills-demo.tape rename to 05-skills/assets/list-skills-demo.tape diff --git a/05-skills/images/managing-sharing-skills.png b/05-skills/assets/managing-sharing-skills.png similarity index 100% rename from 05-skills/images/managing-sharing-skills.png rename to 05-skills/assets/managing-sharing-skills.png diff --git a/05-skills/images/power-tools-analogy.png b/05-skills/assets/power-tools-analogy.png similarity index 100% rename from 05-skills/images/power-tools-analogy.png rename to 05-skills/assets/power-tools-analogy.png diff --git a/05-skills/images/skill-auto-discovery-flow.png b/05-skills/assets/skill-auto-discovery-flow.png similarity index 100% rename from 05-skills/images/skill-auto-discovery-flow.png rename to 05-skills/assets/skill-auto-discovery-flow.png diff --git a/05-skills/images/skill-trigger-demo.gif b/05-skills/assets/skill-trigger-demo.gif similarity index 100% rename from 05-skills/images/skill-trigger-demo.gif rename to 05-skills/assets/skill-trigger-demo.gif diff --git a/05-skills/images/skill-trigger-demo.tape b/05-skills/assets/skill-trigger-demo.tape similarity index 100% rename from 05-skills/images/skill-trigger-demo.tape rename to 05-skills/assets/skill-trigger-demo.tape diff --git a/05-skills/images/skills-agents-mcp-comparison.png b/05-skills/assets/skills-agents-mcp-comparison.png similarity index 100% rename from 05-skills/images/skills-agents-mcp-comparison.png rename to 05-skills/assets/skills-agents-mcp-comparison.png diff --git a/06-mcp-servers/README.md b/06-mcp-servers/README.md index 5bcddf85..bfbc7f80 100644 --- a/06-mcp-servers/README.md +++ b/06-mcp-servers/README.md @@ -1,4 +1,15 @@ -![Chapter 06: MCP Servers](images/chapter-header.png) + + +![Chapter 06: MCP Servers](assets/chapter-header.png) > **What if Copilot could read your GitHub issues, check your database, and create PRs... all from the terminal?** @@ -24,7 +35,7 @@ By the end of this chapter, you'll be able to: ## ๐Ÿงฉ Real-World Analogy: Browser Extensions -MCP Servers are like Browser Extensions +MCP Servers are like Browser Extensions Think of MCP servers like browser extensions. Your browser on its own can display web pages, but extensions connect it to extra services: @@ -42,7 +53,7 @@ Without extensions, your browser is still useful, but with them, it becomes a po --- -Power cable connecting with bright electrical spark surrounded by floating tech icons representing MCP server connections +Power cable connecting with bright electrical spark surrounded by floating tech icons representing MCP server connections # Quick Start: MCP in 30 Seconds @@ -75,12 +86,12 @@ MCP Servers: > ๐Ÿ’ก **Only seeing the GitHub server?** That's expected! If you haven't added any additional MCP servers yet, GitHub is the only one listed. You'll add more in the next section. -> ๐Ÿ“š **Want to see all `/mcp` commands?** There are additional commands for adding, editing, enabling, and deleting servers. See the [full command reference](#-additional-mcp-commands) at the end of this chapter. +> ๐Ÿ“š **Want to see all MCP management commands?** You can manage servers with `/mcp` slash commands inside chat, or with `copilot mcp` directly from your terminal. See the [full command reference](#-additional-mcp-commands) at the end of this chapter.
๐ŸŽฌ See it in action! -![MCP Status Demo](images/mcp-status-demo.gif) +![MCP Status Demo](assets/mcp-status-demo.gif) *Demo output varies. Your model, tools, and responses will differ from what's shown here.* @@ -117,15 +128,33 @@ MCP makes Copilot aware of your actual development environment. # Configuring MCP Servers -Hands adjusting knobs and sliders on a professional audio mixing board representing MCP server configuration +Hands adjusting knobs and sliders on a professional audio mixing board representing MCP server configuration -Now that you've seen MCP in action, let's set up additional servers. This section covers the configuration file format and how to add new servers. +Now that you've seen MCP in action, let's set up additional servers. You can add servers in two ways: **from the built-in registry** (easiest โ€” guided setup right in the CLI) or by **editing the config file** manually (more flexible). Start with the registry option if you're not sure which to choose. + +--- + +## Installing MCP Servers from the Registry + +The CLI has a built-in MCP server registry that lets you discover and install popular servers with a guided setup โ€” no JSON editing required. + +```bash +copilot + +> /mcp search +``` + +Copilot opens an interactive picker showing available servers. Select one, and the CLI walks you through any required configuration (API keys, paths, etc.) and adds it to your config automatically. + +> ๐Ÿ’ก **Why use the registry?** It's the easiest way to get started โ€” you don't need to know the npm package name, command arguments, or JSON structure. The CLI handles all of that for you. --- ## MCP Configuration File -MCP servers are configured in `~/.copilot/mcp-config.json` (user-level, applies to all projects) or `.vscode/mcp.json` (project-level, applies to just the current workspace). +MCP servers can be configured at the user level in `~/.copilot/mcp-config.json`, which applies across projects, at the project level in `.mcp.json`, or in the workspace config file `.github/mcp.json`. `.github/mcp.json` is auto-loaded alongside `.mcp.json`. If you used `/mcp search`, the CLI created or updated your user-level `~/.copilot/mcp-config.json`, but understanding the JSON format is useful when you want to customize or share project-level MCP configuration. + +> โš ๏ธ **Note**: `.vscode/mcp.json` is no longer supported as an MCP config source. If you have an existing `.vscode/mcp.json`, migrate it to `.mcp.json` in your project root. The CLI will show a migration hint if it detects an old config file. ```json { @@ -333,7 +362,7 @@ Here's a full `mcp-config.json` with filesystem and Context7 servers: } ``` -Save this as `~/.copilot/mcp-config.json` for global access or `.vscode/mcp.json` for project-specific configuration. +Save this as `~/.copilot/mcp-config.json` for global access or `.mcp.json` in the project root for project-specific configuration. --- @@ -341,7 +370,7 @@ Save this as `~/.copilot/mcp-config.json` for global access or `.vscode/mcp.json Now that you have MCP servers configured, let's see what they can do. -Using MCP Servers - Hub-and-spoke diagram showing a Developer CLI connected to GitHub, Filesystem, Context7, and Custom/Web Fetch servers +Using MCP Servers - Hub-and-spoke diagram showing a Developer CLI connected to GitHub, Filesystem, Context7, and Custom/Web Fetch servers --- @@ -539,7 +568,7 @@ copilot These workflows show why developers say "I never want to work without this again." Each example combines multiple MCP servers in a single session. -Issue to PR Workflow using MCP - Shows the complete flow from getting a GitHub issue through creating a pull request +Issue to PR Workflow using MCP - Shows the complete flow from getting a GitHub issue through creating a pull request *Complete MCP workflow: GitHub MCP retrieves repo data, Filesystem MCP finds code, Context7 MCP provides best practices, and Copilot handles analysis* @@ -599,7 +628,7 @@ Suggestions:
๐ŸŽฌ See the MCP workflow in action! -![MCP Workflow Demo](images/mcp-workflow-demo.gif) +![MCP Workflow Demo](assets/mcp-workflow-demo.gif) *Demo output varies. Your model, tools, and responses will differ from what's shown here.* @@ -691,7 +720,7 @@ Recommendations: # Practice -Warm desk setup with monitor showing code, lamp, coffee cup, and headphones ready for hands-on practice +Warm desk setup with monitor showing code, lamp, coffee cup, and headphones ready for hands-on practice **๐ŸŽ‰ You now know the essentials!** You understand MCP, you've seen how to configure servers, and you've seen real workflows in action. Now it's time to try it yourself. @@ -840,7 +869,7 @@ Ready to go deeper? Follow the [Custom MCP Server Guide](mcp-custom-server.md) t | Mistake | What Happens | Fix | |---------|--------------|-----| | Not knowing GitHub MCP is built-in | Trying to install/configure it manually | GitHub MCP is included by default. Just try: "List the recent commits in this repo" | -| Looking for config in wrong location | Can't find or edit MCP settings | User-level config is in `~/.copilot/mcp-config.json`, project-level is `.vscode/mcp.json` | +| Looking for config in wrong location | Can't find or edit MCP settings | User-level config is in `~/.copilot/mcp-config.json`, project-level is `.mcp.json` in the project root | | Invalid JSON in config file | MCP servers fail to load | Use `/mcp show` to check configuration; validate JSON syntax | | Forgetting to authenticate MCP servers | "Authentication failed" errors | Some MCPs need separate auth. Check each server's requirements | @@ -883,19 +912,42 @@ If a server is disabled, see the [additional `/mcp` commands](#-additional-mcp-c ---
-๐Ÿ“š Additional /mcp Commands (click to expand) +๐Ÿ“š Additional MCP Commands (click to expand) -Beyond `/mcp show`, there are several other commands for managing your MCP servers: +You can manage MCP servers in two ways: using **slash commands inside a chat session**, or using the **`copilot mcp` command directly in your terminal** (no chat session needed). + +### Option 1: Slash commands (inside a chat session) + +These work when you're already inside `copilot`: | Command | What It Does | |---------|--------------| | `/mcp show` | Show all configured MCP servers and their status | +| `/mcp list` | Show currently attached MCP servers and their status; can be run while Copilot is working | | `/mcp add` | Interactive setup for adding a new server | | `/mcp edit ` | Edit an existing server configuration | -| `/mcp enable ` | Enable a disabled server | -| `/mcp disable ` | Temporarily disable a server | +| `/mcp enable ` | Enable a disabled server (persists across sessions) | +| `/mcp disable ` | Disable a server (persists across sessions) | | `/mcp delete ` | Remove a server permanently | +| `/mcp auth ` | Re-authenticate with an MCP server that uses OAuth (e.g., after switching accounts) | + +### Option 2: `copilot mcp` command (from your terminal) + +You can also manage MCP servers directly from your terminal without starting a chat session first: + +```bash +# List all configured MCP servers +copilot mcp list + +# Enable a server +copilot mcp enable filesystem + +# Disable a server +copilot mcp disable context7 +``` + +> ๐Ÿ’ก **When to use which?** Use `/mcp` slash commands when you're already in a chat session. Use `copilot mcp` from the terminal when you want to quickly check or change your server settings before starting a session. For most of this course, `/mcp show` is all you need. The other commands become useful as you manage more servers over time. @@ -911,7 +963,7 @@ For most of this course, `/mcp show` is all you need. The other commands become 2. **GitHub MCP is built-in** - no configuration needed, just `/login` 3. **Filesystem and Context7** are configured via `~/.copilot/mcp-config.json` 4. **Multi-server workflows** combine data from multiple sources in a single session -5. **Check server status** with `/mcp show` (additional commands available for managing servers) +5. **Manage servers two ways**: use `/mcp` slash commands inside chat, or `copilot mcp` from the terminal 6. **Custom servers** let you connect any API (optional, covered in the appendix guide) > ๐Ÿ“‹ **Quick Reference**: See the [GitHub Copilot CLI command reference](https://docs.github.com/en/copilot/reference/cli-command-reference) for a complete list of commands and shortcuts. diff --git a/06-mcp-servers/images/browser-extensions-analogy.png b/06-mcp-servers/assets/browser-extensions-analogy.png similarity index 100% rename from 06-mcp-servers/images/browser-extensions-analogy.png rename to 06-mcp-servers/assets/browser-extensions-analogy.png diff --git a/06-mcp-servers/images/chapter-header.png b/06-mcp-servers/assets/chapter-header.png similarity index 100% rename from 06-mcp-servers/images/chapter-header.png rename to 06-mcp-servers/assets/chapter-header.png diff --git a/06-mcp-servers/images/configuring-mcp-servers.png b/06-mcp-servers/assets/configuring-mcp-servers.png similarity index 100% rename from 06-mcp-servers/images/configuring-mcp-servers.png rename to 06-mcp-servers/assets/configuring-mcp-servers.png diff --git a/06-mcp-servers/images/issue-to-pr-workflow.png b/06-mcp-servers/assets/issue-to-pr-workflow.png similarity index 100% rename from 06-mcp-servers/images/issue-to-pr-workflow.png rename to 06-mcp-servers/assets/issue-to-pr-workflow.png diff --git a/06-mcp-servers/images/mcp-status-demo.gif b/06-mcp-servers/assets/mcp-status-demo.gif similarity index 100% rename from 06-mcp-servers/images/mcp-status-demo.gif rename to 06-mcp-servers/assets/mcp-status-demo.gif diff --git a/06-mcp-servers/images/mcp-status-demo.tape b/06-mcp-servers/assets/mcp-status-demo.tape similarity index 100% rename from 06-mcp-servers/images/mcp-status-demo.tape rename to 06-mcp-servers/assets/mcp-status-demo.tape diff --git a/06-mcp-servers/images/mcp-workflow-demo.gif b/06-mcp-servers/assets/mcp-workflow-demo.gif similarity index 100% rename from 06-mcp-servers/images/mcp-workflow-demo.gif rename to 06-mcp-servers/assets/mcp-workflow-demo.gif diff --git a/06-mcp-servers/images/mcp-workflow-demo.tape b/06-mcp-servers/assets/mcp-workflow-demo.tape similarity index 100% rename from 06-mcp-servers/images/mcp-workflow-demo.tape rename to 06-mcp-servers/assets/mcp-workflow-demo.tape diff --git a/06-mcp-servers/images/multi-server-workflow.png b/06-mcp-servers/assets/multi-server-workflow.png similarity index 100% rename from 06-mcp-servers/images/multi-server-workflow.png rename to 06-mcp-servers/assets/multi-server-workflow.png diff --git a/06-mcp-servers/images/quick-start-mcp.png b/06-mcp-servers/assets/quick-start-mcp.png similarity index 100% rename from 06-mcp-servers/images/quick-start-mcp.png rename to 06-mcp-servers/assets/quick-start-mcp.png diff --git a/06-mcp-servers/images/using-mcp-servers.png b/06-mcp-servers/assets/using-mcp-servers.png similarity index 100% rename from 06-mcp-servers/images/using-mcp-servers.png rename to 06-mcp-servers/assets/using-mcp-servers.png diff --git a/06-mcp-servers/mcp-custom-server.md b/06-mcp-servers/mcp-custom-server.md index 03166f6a..beb96668 100644 --- a/06-mcp-servers/mcp-custom-server.md +++ b/06-mcp-servers/mcp-custom-server.md @@ -1,3 +1,14 @@ + + # Building a Custom MCP Server > โš ๏ธ **This content is completely optional.** You can be highly productive with Copilot CLI using only the pre-built MCP servers (GitHub, filesystem, Context7). This guide is for developers who want to connect Copilot to custom internal APIs. See the [MCP for Beginners course](https://github.com/microsoft/mcp-for-beginners) for more details. diff --git a/07-putting-it-together/README.md b/07-putting-it-together/README.md index ae81ce78..829c7999 100644 --- a/07-putting-it-together/README.md +++ b/07-putting-it-together/README.md @@ -1,4 +1,15 @@ -![Chapter 07: Putting It All Together](images/chapter-header.png) + + +![Chapter 07: Putting It All Together](assets/chapter-header.png) > **Everything you learned combines here. Go from idea to merged PR in a single session.** @@ -21,7 +32,7 @@ By the end of this chapter, you'll be able to: ## ๐Ÿงฉ Real-World Analogy: The Orchestra -Orchestra Analogy - Unified Workflow +Orchestra Analogy - Unified Workflow A symphony orchestra has many sections: - **Strings** provide the foundation (like your core workflows) @@ -92,7 +103,10 @@ copilot # Review the changes > /review -# If review passes, generate a PR (uses GitHub MCP covered earlier in the course) +# If review passes, use /pr to operate on the pull request for the current branch +> /pr [view|create|fix|auto] + +# Or ask naturally if you want Copilot to draft it from the terminal > Create a pull request titled "Feature: Add list unread books command" ``` @@ -106,7 +120,7 @@ copilot # Additional Workflows -People assembling a colorful giant jigsaw puzzle with gears, representing how agents, skills, and MCP combine into unified workflows +People assembling a colorful giant jigsaw puzzle with gears, representing how agents, skills, and MCP combine into unified workflows For power users who completed Chapters 04-06, these workflows show how agents, skills, and MCP multiply your effectiveness. @@ -114,7 +128,7 @@ For power users who completed Chapters 04-06, these workflows show how agents, s Here's the mental model for combining everything: -The Integration Pattern - A 4-phase workflow: Gather Context (MCP), Analyze and Plan (Agents), Execute (Skills + Manual), Complete (MCP) +The Integration Pattern - A 4-phase workflow: Gather Context (MCP), Analyze and Plan (Agents), Execute (Skills + Manual), Complete (MCP) --- @@ -372,7 +386,7 @@ For teams with existing CI/CD pipelines, you can automate Copilot reviews on eve # Practice -Warm desk setup with monitor showing code, lamp, coffee cup, and headphones ready for hands-on practice +Warm desk setup with monitor showing code, lamp, coffee cup, and headphones ready for hands-on practice Put the complete workflow into practice. diff --git a/07-putting-it-together/images/chapter-header.png b/07-putting-it-together/assets/chapter-header.png similarity index 100% rename from 07-putting-it-together/images/chapter-header.png rename to 07-putting-it-together/assets/chapter-header.png diff --git a/07-putting-it-together/images/combined-workflows.png b/07-putting-it-together/assets/combined-workflows.png similarity index 100% rename from 07-putting-it-together/images/combined-workflows.png rename to 07-putting-it-together/assets/combined-workflows.png diff --git a/07-putting-it-together/images/integration-pattern.png b/07-putting-it-together/assets/integration-pattern.png similarity index 100% rename from 07-putting-it-together/images/integration-pattern.png rename to 07-putting-it-together/assets/integration-pattern.png diff --git a/07-putting-it-together/images/orchestra-analogy.png b/07-putting-it-together/assets/orchestra-analogy.png similarity index 100% rename from 07-putting-it-together/images/orchestra-analogy.png rename to 07-putting-it-together/assets/orchestra-analogy.png diff --git a/GLOSSARY.md b/GLOSSARY.md index e1da765c..72fff8bb 100644 --- a/GLOSSARY.md +++ b/GLOSSARY.md @@ -76,6 +76,12 @@ Model Context Protocol. A standard for connecting AI assistants to external data --- +### Memory (Copilot CLI) + +A feature that lets Copilot CLI remember facts and preferences *across all sessions*, not just within a single one. Unlike session history (which saves a specific conversation), memory persists globally and is applied automatically in future sessions. Managed with the `/memory` slash command (`/memory on`, `/memory off`, `/memory show`). Memory can be scoped to your user account (visible across all repositories) or to a specific repository (shared with collaborators). + +--- + ## N ### npx diff --git a/README.md b/README.md index 1d9ae0d9..d6b8433d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,15 @@ -![GitHub Copilot CLI for Beginners](./images/copilot-banner.png) + + +![GitHub Copilot CLI for Beginners](./assets/copilot-banner.png) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)  [![Open project in GitHub Codespaces](https://img.shields.io/badge/Codespaces-Open-blue?style=flat-square&logo=github)](https://codespaces.new/github/copilot-cli-for-beginners?hide_repo_select=true&ref=main&quickstart=true)  @@ -15,18 +26,14 @@ GitHub Copilot CLI brings AI assistance directly to your terminal. Instead of sw Think of it as having a knowledgeable colleague available 24/7 who can read your code, explain confusing patterns, and help you work faster! +> ๐Ÿ“˜ **Prefer a web experience?** You can follow this course right here on GitHub, or view it on [Awesome Copilot](https://awesome-copilot.github.com/learning-hub/cli-for-beginners/) for a more traditional browsing experience. + This course is designed for: - **Software Developers** who want to use AI from the command line - **Terminal users** who prefer keyboard-driven workflows over IDE integrations - **Teams looking to standardize** AI-assisted code review and development practices - - - GitHub Copilot Dev Days - Find or host an event - - - ## ๐ŸŽฏ What You'll Learn This hands-on course takes you from zero to productive with GitHub Copilot CLI. You'll work with a single Python book collection app throughout all chapters, progressively improving it using AI-assisted workflows. By the end, you'll confidently use AI to review code, generate tests, debug issues, and automate workflows: all from your terminal. @@ -52,13 +59,13 @@ GitHub Copilot has evolved into a family of AI-powered tools. Here's where each | [**GitHub Copilot CLI**](https://docs.github.com/copilot/how-tos/copilot-cli/cli-getting-started)
(this course) | Your terminal | Terminal-native AI coding assistant | | [**GitHub Copilot**](https://docs.github.com/copilot) | VS Code, Visual Studio, JetBrains, etc. | Agent mode, chat, inline suggestions | | [**Copilot on GitHub.com**](https://github.com/copilot) | GitHub | Immersive chat about your repos, create agents, and more | -| [**GitHub Copilot coding agent**](https://docs.github.com/copilot/using-github-copilot/using-copilot-coding-agent-to-work-on-tasks) | GitHub | Assign issues to agents, get PRs back | +| [**GitHub Copilot cloud agent**](https://docs.github.com/copilot/using-github-copilot/using-copilot-coding-agent-to-work-on-tasks) | GitHub | Assign issues to agents, get PRs back | This course focuses on **GitHub Copilot CLI**, bringing AI assistance directly to your terminal. ## ๐Ÿ“š Course Structure -![GitHub Copilot CLI Learning Path](images/learning-path.png) +![GitHub Copilot CLI Learning Path](assets/learning-path.png) | Chapter | Title | What You'll Build | |:-------:|-------|-------------------| @@ -90,10 +97,19 @@ The **[GitHub Copilot CLI command reference](https://docs.github.com/en/copilot/ ## ๐Ÿ™‹ Getting Help - ๐Ÿ› **Found a bug?** [Open an Issue](https://github.com/github/copilot-cli-for-beginners/issues) -- ๐Ÿค **Want to contribute?** PRs welcome! - ๐Ÿ“š **Official Docs:** [GitHub Copilot CLI Documentation](https://docs.github.com/copilot/concepts/agents/about-copilot-cli) +## Contributing + +> **Note**: The code used in the course is designed to generate specific types of output during reviews, explanations, and debugging so we aren't able to accept PRs that change the existing code. + +**How to contribute:** + +1. Fork this repository and clone it to your machine +2. Create a feature branch (`git checkout -b my-improvement`) +3. Make your changes +4. Submit a pull request + ## License This project is licensed under the terms of the MIT open source license. Please refer to the [LICENSE](./LICENSE) file for the full terms. - diff --git a/appendices/README.md b/appendices/README.md index 30b93f2c..0e016943 100644 --- a/appendices/README.md +++ b/appendices/README.md @@ -1,3 +1,14 @@ + + # Appendices These appendices cover additional topics that extend the core course content. They're optional reading for when you need these specific capabilities. diff --git a/appendices/additional-context.md b/appendices/additional-context.md index 6d22a104..6f13e377 100644 --- a/appendices/additional-context.md +++ b/appendices/additional-context.md @@ -1,3 +1,14 @@ + + # Additional Context Features > ๐Ÿ“– **Prerequisite**: Complete [Chapter 02: Context and Conversations](../02-context-conversations/README.md) before reading this appendix. @@ -111,6 +122,9 @@ copilot > /list-dirs # See all allowed directories + +> /yolo +# Quick alias for /allow-all on โ€” auto-approves all permission prompts ``` ### For Automation diff --git a/appendices/ci-cd-integration.md b/appendices/ci-cd-integration.md index ba150ed6..921e2957 100644 --- a/appendices/ci-cd-integration.md +++ b/appendices/ci-cd-integration.md @@ -1,3 +1,14 @@ + + # CI/CD Integration > ๐Ÿ“– **Prerequisite**: Complete [Chapter 07: Putting It All Together](../07-putting-it-together/README.md) before reading this appendix. @@ -123,7 +134,7 @@ For consistent reviews across your team, create a shared configuration: ## Alternative: PR Review Bot -For more sophisticated review workflows, consider using the Copilot coding agent: +For more sophisticated review workflows, consider using the GitHub Copilot cloud agent: ```yaml # .github/workflows/copilot-agent-review.yml diff --git a/images/chapter-header-bg.png b/assets/chapter-header-bg.png similarity index 100% rename from images/chapter-header-bg.png rename to assets/chapter-header-bg.png diff --git a/images/copilot-banner.png b/assets/copilot-banner.png similarity index 100% rename from images/copilot-banner.png rename to assets/copilot-banner.png diff --git a/images/copilot-dev-days.png b/assets/copilot-dev-days.png similarity index 100% rename from images/copilot-dev-days.png rename to assets/copilot-dev-days.png diff --git a/images/github-skills-logo.png b/assets/github-skills-logo.png similarity index 100% rename from images/github-skills-logo.png rename to assets/github-skills-logo.png diff --git a/images/learning-path.png b/assets/learning-path.png similarity index 100% rename from images/learning-path.png rename to assets/learning-path.png diff --git a/images/mockup.png b/assets/mockup.png similarity index 100% rename from images/mockup.png rename to assets/mockup.png diff --git a/images/practice.png b/assets/practice.png similarity index 100% rename from images/practice.png rename to assets/practice.png diff --git a/assets/qr.png b/assets/qr.png new file mode 100644 index 00000000..6f859ac6 Binary files /dev/null and b/assets/qr.png differ diff --git a/images/quick-reference-header.png b/assets/quick-reference-header.png similarity index 100% rename from images/quick-reference-header.png rename to assets/quick-reference-header.png diff --git a/images/screenshot.png b/assets/screenshot.png similarity index 100% rename from images/screenshot.png rename to assets/screenshot.png diff --git a/package.json b/package.json index 26a84cf4..10eff8eb 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "create:tapes": "node .github/scripts/create-tapes.js", "generate:vhs": "node .github/scripts/generate-demos.js", "verify:gifs": "node .github/scripts/verify-gifs.js", + "copy:content-engine": "node .github/scripts/copy-content-engine-files.js", "generate:demos": "npm run create:tapes && npm run generate:vhs && npm run verify:gifs", "release": "npm run generate:demos", "release:ci": "npm run generate:headers"