diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index 16600736..839c4c3a 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -1,5 +1,15 @@ { "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", @@ -10,6 +20,11 @@ "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", diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b4cbb8b6..5ed64e51 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -47,7 +47,7 @@ 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 `images/` directory. +- 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). @@ -55,7 +55,7 @@ Do not deviate from this structure when editing or adding chapter content. | Change Made | Files to Update | |---|---| -| New chapter added | `README.md` (course table), `AGENTS.md` (structure table), `images/learning-path.png` | +| 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 | @@ -66,5 +66,5 @@ Do not deviate from this structure when editing or adding chapter content. | 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 | `images/` directory, any README referencing the image | +| 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 index fd7db1e0..345702fd 100644 --- a/.github/uvs.csv +++ b/.github/uvs.csv @@ -60,3 +60,80 @@ "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 +"07/13",478 +"07/14",497 +"07/15",426 +"07/16",407 +"07/17",474 +"07/18",213 +"07/19",238 diff --git a/.github/views.csv b/.github/views.csv index 82c39575..4f566887 100644 --- a/.github/views.csv +++ b/.github/views.csv @@ -59,3 +59,80 @@ "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 +"07/13",981 +"07/14",980 +"07/15",924 +"07/16",822 +"07/17",1162 +"07/18",485 +"07/19",596 diff --git a/.github/workflows/co-op-translator.yml b/.github/workflows/co-op-translator.yml 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/sync-content-engine.yml b/.github/workflows/sync-content-engine.yml new file mode 100644 index 00000000..208f7d78 --- /dev/null +++ b/.github/workflows/sync-content-engine.yml @@ -0,0 +1,60 @@ +name: Sync content-engine copy + +on: + workflow_dispatch: + push: + branches: + - main + +permissions: + contents: read + +concurrency: + group: sync-content-engine-copy + cancel-in-progress: true + +jobs: + sync-content: + name: Copy content and open content-engine PR + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout course repository + uses: actions/checkout@v4 + with: + path: copilot-cli-for-beginners + + - name: Checkout content-engine repository + uses: actions/checkout@v4 + with: + repository: github/cse-content-engine + token: ${{ secrets.GH_AW_GITHUB_TOKEN }} + path: cse-content-engine + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Copy course content into content-engine + working-directory: copilot-cli-for-beginners + env: + CONTENT_ENGINE_DESTINATION_ROOT: ${{ github.workspace }}/cse-content-engine/content/learning-pathways/copilot-cli-for-beginners + run: npm run copy:content-engine + + - name: Create content-engine pull request + uses: peter-evans/create-pull-request@v7 + with: + path: cse-content-engine + token: ${{ secrets.GH_AW_GITHUB_TOKEN }} + commit-message: "Update Copilot CLI for Beginners content" + title: "Update Copilot CLI for Beginners content" + body: | + This automated PR syncs the latest `github/copilot-cli-for-beginners` + content into the content-engine learning pathway. + + Source commit: ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }} + branch: update/copilot-cli-for-beginners-content + base: main + delete-branch: true diff --git a/.github/workflows/translation-polisher.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 d54b2511..9b6560e3 100644 --- a/.gitignore +++ b/.gitignore @@ -101,4 +101,5 @@ Desktop.ini .plans .plans.vhs-wrapper demo-previews -blog \ No newline at end of file +blog +migration.md \ No newline at end of file diff --git a/00-quick-start/README.md b/00-quick-start/README.md index dcc0f1c8..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! @@ -124,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. @@ -140,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.* @@ -169,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.* 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 59af743b..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. @@ -86,7 +97,7 @@ Once inside the interactive Copilot CLI session, run the following:
๐ŸŽฌ 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,7 +279,7 @@ 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. @@ -324,7 +335,7 @@ Proceed with implementation? [Y/n] ### 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. @@ -372,11 +383,14 @@ These commands are great to learn initially as you're getting started with Copil | `/help` | Show all available commands | When you forget a command | | `/model` | Show or switch AI model | When you want to change the AI model | | `/plan` | Plan your work out before coding | For more complex features | +| `/refine` | Rewrite a rough, stream-of-consciousness prompt into a clear, focused one | When your prompt feels messy and you want better results | | `/research` | Deep research using GitHub and web sources | When you need to investigate a topic before coding | | `/exit` | End the session | When you're done | > ๐Ÿ’ก **`/ask` vs regular chat**: Normally every message you send becomes part of the ongoing conversation and affects future responses. `/ask` is an "off the record" shortcut โ€” perfect for quick one-off questions like `/ask What does YAML mean?` without polluting your session context. +> ๐Ÿ’ก **`/refine` for better prompts**: Not sure if your prompt is clear enough? Type it out as it comes to mind, then run `/refine` to let Copilot rewrite it into a precise, well-structured prompt before sending. This is especially useful when you're new to AI tools and still learning how to write effective prompts. + > ๐Ÿ’ก **Tab-completion**: When typing a slash command, press **Tab** to auto-complete the command name or cycle through available subcommands and arguments. This is especially handy when you can't remember the exact name of a command. That's it for getting started! As you become comfortable, you can explore additional commands. @@ -396,6 +410,7 @@ That's it for getting started! As you become comfortable, you can explore additi | `/env` | Show loaded environment details โ€” what instructions, MCP servers, skills, agents, and plugins are active | | `/init` | Initialize Copilot instructions for your repository | | `/mcp` | Manage MCP server configuration | +| `/settings` | Open an interactive dialog to browse and edit all user settings in one place | | `/skills` | Manage skills for enhanced capabilities | > ๐Ÿ’ก Agents are covered in [Chapter 04](../04-agents-custom-instructions/README.md), skills are covered in [Chapter 05](../05-skills/README.md), and MCP servers are covered in [Chapter 06](../06-mcp-servers/README.md). @@ -436,16 +451,19 @@ That's it for getting started! As you become comfortable, you can explore additi | Command | What It Does | |---------|--------------| | `/clear` | Abandons the current session (no history saved) and starts a fresh conversation | -| `/compact` | Summarize conversation to reduce context usage | +| `/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 | | `/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 | +| `/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. @@ -647,7 +669,7 @@ 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 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 b9710f22..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,6 +314,9 @@ 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 @@ -351,6 +365,27 @@ copilot > /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. Several commands are available to help you stay in control: @@ -379,7 +414,7 @@ Context usage: 62k/200k tokens (31%) ### 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.* @@ -438,7 +473,7 @@ No re-explaining. No re-reading files. Just continue working. # 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).** @@ -550,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.* @@ -588,6 +623,17 @@ 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 | @@ -702,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. @@ -715,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. @@ -881,9 +927,10 @@ 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**: 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` -5. **Permission flags** (`--add-dir`, `--allow-all`) control multi-directory access. Use them wisely! -6. **Image references** (`@screenshot.png`) help debug UI issues visually +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 a7b3f0c3..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, /pr, /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.* @@ -680,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 @@ -692,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. +
--- @@ -770,7 +803,7 @@ 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. @@ -850,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) --- @@ -904,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 8e4a7629..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. @@ -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 95c64d61..7036d2c7 100644 --- a/05-skills/README.md +++ b/05-skills/README.md @@ -1,4 +1,15 @@ -![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?** @@ -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. @@ -101,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) @@ -123,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 | |---------|--------------|-------------| @@ -178,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.* @@ -213,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.* @@ -261,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. @@ -344,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) @@ -458,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` -Use the `/skills` command to manage your installed 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) + +Once you're in an interactive Copilot session, use `/skills` (or its shortcut `/skill`) to manage skills without leaving: | Command | What It Does | |---------|--------------| @@ -474,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 @@ -502,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.* @@ -569,10 +629,10 @@ The easiest way to install a skill from a GitHub repository is using the `gh ski gh skill install github/awesome-copilot # Or install a specific skill directly -gh skill install github/awesome-copilot code-checklist +gh skill install github/awesome-copilot ai-ready # Install for personal use across all projects (user scope) -gh skill install github/awesome-copilot code-checklist --scope user +gh skill install github/awesome-copilot ai-ready --scope user ``` > โš ๏ธ **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. @@ -581,7 +641,7 @@ gh skill install github/awesome-copilot code-checklist --scope user # 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. @@ -848,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 a81bf57b..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 @@ -80,7 +91,7 @@ MCP Servers:
๐ŸŽฌ 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,7 +128,7 @@ 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. 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. @@ -141,7 +152,7 @@ Copilot opens an interactive picker showing available servers. Select one, and t ## MCP Configuration File -MCP servers are configured in `~/.copilot/mcp-config.json` (user-level, applies to all projects) or `.mcp.json` (project-level, placed in the root of your project). If you used `/mcp search` above, the CLI already created or updated this file for you, but it's useful to understand the format for customization. +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. @@ -359,7 +370,7 @@ Save this as `~/.copilot/mcp-config.json` for global access or `.mcp.json` in th 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 --- @@ -557,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* @@ -617,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.* @@ -709,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. @@ -913,6 +924,7 @@ 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 (persists across sessions) | 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 2678d647..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) @@ -109,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. @@ -117,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) --- @@ -375,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 4f2cd85f..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)  @@ -23,12 +34,6 @@ This course is designed for: - **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. @@ -60,7 +65,7 @@ This course focuses on **GitHub Copilot CLI**, bringing AI assistance directly t ## ๐Ÿ“š 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 | |:-------:|-------|-------------------| @@ -108,4 +113,3 @@ The **[GitHub Copilot CLI command reference](https://docs.github.com/en/copilot/ ## 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 55906f74..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. diff --git a/appendices/ci-cd-integration.md b/appendices/ci-cd-integration.md index 176bd717..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. 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/images/qr.png b/assets/qr.png similarity index 100% rename from images/qr.png rename to assets/qr.png 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"