diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c1965c2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.github/workflows/*.lock.yml linguist-generated=true merge=ours \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..4489dcb --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Julia is the merge authority; Bethany is the independent fallback reviewer. +* @juliamuiruri4 @BethanyJep diff --git a/.github/agentic-refresh/README.md b/.github/agentic-refresh/README.md new file mode 100644 index 0000000..94176b3 --- /dev/null +++ b/.github/agentic-refresh/README.md @@ -0,0 +1,32 @@ +# Agentic refresh controls + +This directory contains the Phase 0 governance boundary for future quest refresh +automation. + +- [`capabilities.json`](./capabilities.json) keeps Buildathon writes disabled + until the repository-scoped token and dispatch spikes pass. External + repositories are permanently report-only under this strategy. +- [`quests.json`](./quests.json) defines source allowlists, Buildathon ownership, + external observation paths, approval identity, branch shape, and operational + limits. +- [`github-token.json`](./github-token.json) defines the built-in + `GITHUB_TOKEN` permission and event model without storing a PAT or App key. +- [`governance.md`](./governance.md) documents the repository settings and + technical spikes required before enabling Buildathon write automation. +- [`lifecycle-policy.md`](./lifecycle-policy.md), + [`audience-style-guide.md`](./audience-style-guide.md), + [`report-contract.md`](./report-contract.md), + [`stack-policy.md`](./stack-policy.md), and + [`asset-policy.md`](./asset-policy.md) define the non-negotiable content and + workflow contracts. + +Run the local Phase 0 checks with: + +```bash +node scripts/validate-refresh-config.mjs +node scripts/validate-content-links.mjs +mkdocs build --strict +gh aw compile --strict --validate +``` + +No scheduled or write-capable agentic workflow is enabled in Phase 0. diff --git a/.github/agentic-refresh/asset-policy.md b/.github/agentic-refresh/asset-policy.md new file mode 100644 index 0000000..0376790 --- /dev/null +++ b/.github/agentic-refresh/asset-policy.md @@ -0,0 +1,16 @@ +# Asset policy + +Asset work begins with an approved brief and stays inside the quest's owned +paths. + +- Prefer existing assets or text when they explain the step adequately. +- Use original SVG for static diagrams and Remotion only when motion adds + instructional value. +- Preserve editable source and deterministic rendering commands. +- Record dimensions, file size, alt text, source files, and render commands in + an asset manifest. +- Verify light and dark readability, current branding, and the absence of + credentials or personal data. +- Use controlled product screenshots and document any manual validation. +- Keep asset revisions on the same `01-core` branch initially. +- Stop after two automated revision rounds and request human guidance. diff --git a/.github/agentic-refresh/audience-style-guide.md b/.github/agentic-refresh/audience-style-guide.md new file mode 100644 index 0000000..a3ed86f --- /dev/null +++ b/.github/agentic-refresh/audience-style-guide.md @@ -0,0 +1,16 @@ +# Audience and style guide + +Content serves beginner and intermediate JavaScript and TypeScript developers. + +- State prerequisites, expected cost, cleanup, and platform limitations before + a learner starts a billable or environment-specific step. +- Prefer a short working path, then use progressive disclosure for optional or + expensive exploration. +- Explain product terms at first use and avoid unexplained architecture jumps. +- Keep commands and code copyable, language-appropriate, and consistent with + the target repository. +- Separate verified product facts from repository observations and proposed + teaching choices. +- Do not claim a lifecycle, feature, or recommended pattern without an + allowlisted citation. +- Preserve the quest scenario unless an approved plan explicitly changes it. diff --git a/.github/agentic-refresh/capabilities.json b/.github/agentic-refresh/capabilities.json new file mode 100644 index 0000000..643beb8 --- /dev/null +++ b/.github/agentic-refresh/capabilities.json @@ -0,0 +1,112 @@ +{ + "version": 2, + "defaultMode": "report-only", + "lastReviewedAt": "2026-07-24", + "repositories": [ + { + "name": "Azure-Samples/JavaScript-AI-Buildathon", + "role": "automation-host", + "status": "blocked", + "writeAutomationAllowed": false, + "capabilities": { + "repositoryScopedTokenSelected": true, + "copilotCodeReviewEnabled": true, + "copilotInferenceAvailable": false, + "aiCreditBudgetAvailable": false, + "repositoryAdministratorAvailable": true, + "githubTokenIssueWritesConfirmed": false, + "githubTokenPullRequestWritesConfirmed": false, + "explicitDispatchChecksConfirmed": false, + "refreshBranchCreationAllowed": false, + "rulesetConfigured": false, + "conversationResolutionRequired": true, + "codeOwnerReviewRequired": false, + "requiredChecksKnown": true + }, + "requiredChecks": [ + "validate-content", + "validate-agentic-workflows" + ], + "pendingRequiredChecks": [ + "copilot-review-gate" + ], + "approval": { + "approvedBy": null, + "approvedAt": null, + "evidence": null + }, + "auditEvidence": { + "copilotCodeReviewWorkflowId": 209015997, + "rulesetId": 19622585, + "approverVariable": "REFRESH_APPROVER_LOGIN=juliamuiruri4", + "labelsConfigured": true, + "phaseZeroPullRequest": 110 + }, + "blockers": [ + "The Copilot inference permission and AI-credit budget have not been confirmed.", + "A manual trial must prove that GITHUB_TOKEN can create Buildathon issues, refresh/** branches, and pull requests under the organization policy.", + "A required-check spike must prove explicit workflow dispatch after GITHUB_TOKEN-created pull requests and branch updates.", + "Ruleset 19622585 must be activated after CODEOWNERS and both Phase 0 validation checks are merged and have successful check runs.", + "The human-only merge ruleset template must be created or confirmed in repository settings; the REST API returned 404 when user bypass was requested." + ] + }, + { + "name": "Azure-Samples/serverless-chat-langchainjs", + "role": "external-report-only", + "status": "report-only", + "writeAutomationAllowed": false, + "capabilities": { + "repositoryScopedTokenSelected": false, + "copilotCodeReviewEnabled": true, + "copilotInferenceAvailable": false, + "aiCreditBudgetAvailable": false, + "repositoryAdministratorAvailable": false, + "githubTokenIssueWritesConfirmed": false, + "githubTokenPullRequestWritesConfirmed": false, + "explicitDispatchChecksConfirmed": false, + "refreshBranchCreationAllowed": false, + "rulesetConfigured": false, + "conversationResolutionRequired": false, + "codeOwnerReviewRequired": false, + "requiredChecksKnown": false + }, + "requiredChecks": [], + "approval": { + "approvedBy": null, + "approvedAt": null, + "evidence": null + }, + "policyReason": "Cross-repository writes are intentionally disabled; findings require a manual maintainer handoff.", + "blockers": [] + }, + { + "name": "Azure-Samples/mcp-agent-langchainjs", + "role": "external-report-only", + "status": "report-only", + "writeAutomationAllowed": false, + "capabilities": { + "repositoryScopedTokenSelected": false, + "copilotCodeReviewEnabled": true, + "copilotInferenceAvailable": false, + "aiCreditBudgetAvailable": false, + "repositoryAdministratorAvailable": false, + "githubTokenIssueWritesConfirmed": false, + "githubTokenPullRequestWritesConfirmed": false, + "explicitDispatchChecksConfirmed": false, + "refreshBranchCreationAllowed": false, + "rulesetConfigured": false, + "conversationResolutionRequired": false, + "codeOwnerReviewRequired": false, + "requiredChecksKnown": false + }, + "requiredChecks": [], + "approval": { + "approvedBy": null, + "approvedAt": null, + "evidence": null + }, + "policyReason": "Cross-repository writes are intentionally disabled; findings require a manual maintainer handoff.", + "blockers": [] + } + ] +} diff --git a/.github/agentic-refresh/capability-matrix.md b/.github/agentic-refresh/capability-matrix.md new file mode 100644 index 0000000..7b0ae75 --- /dev/null +++ b/.github/agentic-refresh/capability-matrix.md @@ -0,0 +1,69 @@ +# Refresh capability matrix + +The machine-readable source of truth is +[`capabilities.json`](./capabilities.json). + +- Buildathon stays **report-only** until every same-repository write and dispatch + capability is confirmed, approval evidence is recorded, and + `writeAutomationAllowed` is explicitly set to `true`. +- The two external sample repositories are intentionally **report-only** and do + not have an automated write-enablement path under this strategy. + +| Repository | Role | Current mode | Capability status | +|---|---|---|---| +| `Azure-Samples/JavaScript-AI-Buildathon` | Automation host | Report-only | Blocked | +| `Azure-Samples/serverless-chat-langchainjs` | External evidence | Report-only | Policy | +| `Azure-Samples/mcp-agent-langchainjs` | External evidence | Report-only | Policy | + +## Buildathon confirmation + +Before Buildathon write automation is enabled, confirm: + +1. Copilot inference works with `copilot-requests: write` and the organization + has an available AI-credit budget. +2. `GITHUB_TOKEN` can create a Buildathon issue through a bounded safe output. +3. `GITHUB_TOKEN` can create and update only `refresh/**` branches and open a + Buildathon pull request through bounded safe outputs. +4. Explicit workflow dispatch produces every required check for the current head + SHA after token-created PRs and subsequent automated updates. +5. Copilot Code Review can be explicitly requested for the current head SHA. +6. The main ruleset requires pull requests, CODEOWNER approval, conversation + resolution, and the known required checks. +7. The workflow cannot push to `main`, merge, force-push, delete branches, or + write to another repository. +8. `REFRESH_APPROVER_LOGIN` is set to the exact approved GitHub login. + +Record the approver login, UTC timestamp, and trial evidence in +`capabilities.json`. Do not replace an unknown capability with an assumption. + +## Audit findings + +The SSO-authorized keyring token completed a read-only audit on July 23, 2026. + +- Buildathon: Copilot Code Review is active; refresh labels and + `REFRESH_APPROVER_LOGIN` are configured. Active branch protection requires one + approval and conversation resolution. Disabled ruleset `19622585` is prepared + to require the Phase 0 checks, CODEOWNER approval, approval after the latest + push, conversation resolution, and to block deletion and force-push. A + separate human-only update template still requires administrator confirmation + because the user-bypass REST request returned HTTP 404. +- Serverless Chat and MCP Agent remain external evidence sources only. No + workflow in Buildathon may write to them. + +The unavailable organization-owned GitHub App is no longer a blocker or planned +dependency. Built-in-token event suppression and repository write permissions +are the new technical gates. + +## Activation sequence + +1. Merge the Phase 0 pull request. +2. Confirm or create the human-only merge ruleset in repository settings. +3. Activate ruleset `19622585` and the human-only merge ruleset. +4. Run read-only discovery trials. +5. Run the bounded `GITHUB_TOKEN` issue, branch, PR, explicit-dispatch, and + Copilot-review spike. +6. Enable issue-only reporting if issue writes pass. +7. Enable Buildathon implementation only after every write/review gate passes. + +External CodeTour findings remain manual handoffs regardless of Buildathon +capability results. diff --git a/.github/agentic-refresh/github-token.json b/.github/agentic-refresh/github-token.json new file mode 100644 index 0000000..622dccc --- /dev/null +++ b/.github/agentic-refresh/github-token.json @@ -0,0 +1,66 @@ +{ + "version": 1, + "token": "GITHUB_TOKEN", + "scope": "current-repository-only", + "storedSecretRequired": false, + "personalTokenFallbackAllowed": false, + "crossRepositoryWritesAllowed": false, + "copilotInference": { + "permission": "copilot-requests: write", + "personalTokenFallbackAllowed": false + }, + "permissionsByOperation": { + "discoveryReport": { + "contents": "read", + "issues": "write", + "copilot-requests": "write" + }, + "implementation": { + "contents": "write", + "issues": "write", + "pull-requests": "write", + "copilot-requests": "write" + }, + "validationDispatch": { + "actions": "write", + "contents": "read" + } + }, + "constraints": { + "allowedRepository": "Azure-Samples/JavaScript-AI-Buildathon", + "allowedBranchPrefix": "refresh/", + "defaultBranchWritesAllowed": false, + "mergeAllowed": false, + "humanMergeRuleset": ".github/agentic-refresh/rulesets/human-merge.json" + }, + "writeHandling": { + "agentGitHubTools": "read-only", + "consumer": "gh-aw-safe-outputs", + "allowedOperations": [ + "createRefreshBranch", + "updateRefreshBranch", + "createPullRequest", + "updatePullRequest", + "createIssue", + "updateIssue", + "comment" + ], + "forbiddenOperations": [ + "mergePullRequest", + "updateDefaultBranch", + "deleteBranch", + "forcePush", + "writeExternalRepository" + ] + }, + "eventModel": { + "tokenGeneratedEventsTriggerWorkflows": false, + "explicitDispatchRequiredAfterPullRequestCreation": true, + "explicitDispatchRequiredAfterRefreshBranchUpdate": true, + "requiredCheckDispatchMustBeProvenBeforeWrites": true + }, + "externalRepositories": { + "mode": "report-only", + "manualHandoffRequired": true + } +} diff --git a/.github/agentic-refresh/governance.md b/.github/agentic-refresh/governance.md new file mode 100644 index 0000000..a654897 --- /dev/null +++ b/.github/agentic-refresh/governance.md @@ -0,0 +1,93 @@ +# Agentic refresh governance + +## Safety boundary + +Phase 0 creates policy, validation, and authoring support only. It does not +enable scheduled discovery, issue creation, content edits, branch creation, pull +requests, or merge. Any later workflow must read `capabilities.json` and stop in +report-only mode unless Buildathon is explicitly approved for write automation. +The two external sample repositories remain report-only and require manual +maintainer handoff. + +## Repository-scoped authentication + +Use GitHub Actions' built-in `GITHUB_TOKEN` as defined in +[`github-token.json`](./github-token.json): + +- no stored PAT, OAuth token, App ID, or private key; +- token scope is limited to + `Azure-Samples/JavaScript-AI-Buildathon`; +- Copilot inference uses `copilot-requests: write`; +- discovery/report jobs receive only the permissions they need; +- implementation writes are limited to `refresh/**` through gh-aw safe outputs; +- no cross-repository write, default-branch update, deletion, force-push, or + merge operation is allowed. + +The current local `GH_TOKEN` is an OAuth `gho_` token and must not be stored as a +workflow secret. `GH_AW_GITHUB_TOKEN`, `COPILOT_GITHUB_TOKEN`, and personal PAT +fallbacks are prohibited by this strategy. + +## Event-suppression constraint + +Events created with `GITHUB_TOKEN` generally do not start new workflow runs. +Before Buildathon writes are enabled, a throwaway spike must prove: + +1. issue creation through a safe output; +2. `refresh/**` branch and pull-request creation through a safe output; +3. explicit `workflow_dispatch` of required validation for the current PR head + SHA after creation and after every automated branch update; +4. explicit Copilot Code Review request for the current head SHA; +5. review/thread events still update `copilot-review-gate`; +6. no write occurs when any explicit dispatch or review request fails. + +Failure keeps the system in issue-only or fully report-only mode. + +## Repository settings + +Apply the disabled templates in [`rulesets/main.json`](./rulesets/main.json) and +[`rulesets/human-merge.json`](./rulesets/human-merge.json) only after both +validation workflows have produced successful check runs. Review the generated +settings before changing `enforcement` to `active`. + +Create the labels in [`labels.json`](./labels.json) and configure only the +repository variable `REFRESH_APPROVER_LOGIN`. No credential secret is required. + +The main ruleset must require: + +- pull requests; +- one CODEOWNER approval; +- approval after the latest push; +- conversation resolution; +- `validate-content`; +- `validate-agentic-workflows`; +- no force-push or branch deletion. + +Phase 3 adds `copilot-review-gate` only after that check has been proven on every +pull-request head SHA. + +The human-only ruleset contains only an update restriction. Julia is its sole +initial bypass actor and may bypass it only through a pull request. The main +protections are in a separate ruleset with no bypass actors, so the human merge +actor cannot bypass required checks, review, conversation resolution, or +force-push and deletion protections. + +## External repository handoff + +Discovery may inspect only the allowlisted public source URLs and observed paths +in `quests.json`. It may record an external CodeTour finding in the Buildathon +child issue, but it cannot create an external issue, branch, commit, or pull +request. A human maintainer decides whether and how to transfer the finding. + +## SAML authorization + +SAML authorization is needed only for local audits and human GitHub operations. +It is not a production workflow dependency. If `gh api` returns HTTP 403: + +1. Run `gh auth status` and identify the active token. +2. Open the SSO authorization URL printed by `gh`. +3. Authorize that exact token for the Microsoft Open Source enterprise. +4. Re-run the API request. +5. If an environment `GH_TOKEN` overrides an authorized keyring token, remove + the override for the audit. + +Do not copy that OAuth token into repository secrets. diff --git a/.github/agentic-refresh/labels.json b/.github/agentic-refresh/labels.json new file mode 100644 index 0000000..d7eaa99 --- /dev/null +++ b/.github/agentic-refresh/labels.json @@ -0,0 +1,85 @@ +{ + "version": 1, + "labels": [ + { + "name": "refresh/plan", + "color": "1d76db", + "description": "Agentic refresh plan" + }, + { + "name": "refresh/awaiting-approval", + "color": "fbca04", + "description": "Waiting for the configured human approver" + }, + { + "name": "refresh/approved", + "color": "0e8a16", + "description": "Refresh plan approved by the configured human" + }, + { + "name": "refresh/implementing", + "color": "5319e7", + "description": "Approved refresh is being implemented" + }, + { + "name": "refresh/asset-review", + "color": "bfdadc", + "description": "Assets are awaiting bounded review" + }, + { + "name": "refresh/copilot-review", + "color": "7057ff", + "description": "Copilot findings are being adjudicated" + }, + { + "name": "refresh/human-review", + "color": "0052cc", + "description": "Core stack is awaiting human review" + }, + { + "name": "refresh/docs", + "color": "c5def5", + "description": "Documentation-owner layer is active" + }, + { + "name": "refresh/final-review", + "color": "006b75", + "description": "Highest stack layer is awaiting final human review" + }, + { + "name": "refresh/blocked", + "color": "b60205", + "description": "Refresh stopped pending human guidance" + }, + { + "name": "refresh/completed", + "color": "0e8a16", + "description": "Human-merged refresh completed" + }, + { + "name": "quest/1-foundry-local", + "color": "d4c5f9", + "description": "Foundry Local quest" + }, + { + "name": "quest/2-microsoft-foundry", + "color": "d4c5f9", + "description": "Microsoft Foundry quest" + }, + { + "name": "quest/3-agentic-rag", + "color": "d4c5f9", + "description": "Agentic RAG quest" + }, + { + "name": "quest/4-foundry-toolkit", + "color": "d4c5f9", + "description": "Foundry Toolkit quest" + }, + { + "name": "quest/5-context-engineering", + "color": "d4c5f9", + "description": "Context Engineering quest" + } + ] +} diff --git a/.github/agentic-refresh/lifecycle-policy.md b/.github/agentic-refresh/lifecycle-policy.md new file mode 100644 index 0000000..fb40093 --- /dev/null +++ b/.github/agentic-refresh/lifecycle-policy.md @@ -0,0 +1,15 @@ +# Product lifecycle policy + +Every material product or capability finding must include: + +```text +Lifecycle: GA | Public preview | Private preview | Not stated by allowed sources +Evidence URL: +Evidence location: +Observed wording: +``` + +Do not infer GA from a missing preview badge. Do not apply one capability's +status to an entire product. If allowed sources disagree, report both positions +and block implementation for that claim. `Not stated by allowed sources` is a +valid result and must not be replaced with a guess. diff --git a/.github/agentic-refresh/quests.json b/.github/agentic-refresh/quests.json new file mode 100644 index 0000000..4fa7fbf --- /dev/null +++ b/.github/agentic-refresh/quests.json @@ -0,0 +1,194 @@ +{ + "version": 2, + "schedule": { + "cron": "0 8 * * 1", + "timezone": "UTC", + "description": "Monday 08:00 UTC, equivalent to fixed GMT+1 09:00" + }, + "governance": { + "defaultMode": "report-only", + "capabilitiesFile": ".github/agentic-refresh/capabilities.json", + "tokenPolicyFile": ".github/agentic-refresh/github-token.json", + "tokenModel": "repository-scoped-github-token", + "crossRepositoryWritesAllowed": false, + "externalRepositoryMode": "report-only", + "approvalCommand": "/approve-refresh", + "approverVariable": "REFRESH_APPROVER_LOGIN", + "branchPattern": "^refresh/[0-9]{4}-W[0-9]{2}/[a-z0-9-]+/(01-core|02-docs)$", + "maximumActiveImplementations": 2, + "maximumAssetRevisionRounds": 2, + "maximumCopilotReviewRounds": 2, + "protectedPaths": [ + ".github/workflows/**", + ".github/agents/**", + ".github/skills/**", + ".github/agentic-refresh/**", + ".github/CODEOWNERS", + "scripts/**", + "requirements.txt" + ] + }, + "transportDomains": [ + "api.github.com", + "raw.githubusercontent.com" + ], + "quests": [ + { + "id": 1, + "slug": "foundry-local", + "title": "Foundry Local", + "agent": "foundry-local-quest-master", + "officialSourcePrefixes": [ + "https://www.foundrylocal.ai/", + "https://github.com/microsoft/foundry-local" + ], + "sourceRepositories": [ + "microsoft/foundry-local" + ], + "writableRepositories": [ + "Azure-Samples/JavaScript-AI-Buildathon" + ], + "ownedPaths": { + "Azure-Samples/JavaScript-AI-Buildathon": [ + "01-Local-AI-Development/**", + "README.md", + "docs/quests.md" + ] + }, + "externalReports": [] + }, + { + "id": 2, + "slug": "microsoft-foundry", + "title": "Microsoft Foundry", + "agent": "microsoft-foundry-quest-master", + "officialSourcePrefixes": [ + "https://learn.microsoft.com/en-us/azure/foundry/", + "https://github.com/microsoft/microsoft-foundry-e2e-js" + ], + "sourceRepositories": [ + "microsoft/microsoft-foundry-e2e-js" + ], + "writableRepositories": [ + "Azure-Samples/JavaScript-AI-Buildathon" + ], + "ownedPaths": { + "Azure-Samples/JavaScript-AI-Buildathon": [ + "02-E2E-Model-Development-on-Foundry/**", + "README.md", + "docs/quests.md" + ] + }, + "externalReports": [] + }, + { + "id": 3, + "slug": "agentic-rag", + "title": "LangChain.js Agentic RAG", + "agent": "agentic-rag-quest-master", + "officialSourcePrefixes": [ + "https://github.com/Azure-Samples/serverless-chat-langchainjs", + "https://github.com/microsoft/langchainjs-for-beginners", + "https://docs.langchain.com/oss/javascript/langgraph/agentic-rag", + "https://docs.langchain.com/" + ], + "sourceRepositories": [ + "Azure-Samples/serverless-chat-langchainjs", + "microsoft/langchainjs-for-beginners" + ], + "writableRepositories": [ + "Azure-Samples/JavaScript-AI-Buildathon" + ], + "ownedPaths": { + "Azure-Samples/JavaScript-AI-Buildathon": [ + "03-Run-Serverless-RAG-Support-System/**", + "README.md", + "docs/quests.md" + ] + }, + "externalReports": [ + { + "repository": "Azure-Samples/serverless-chat-langchainjs", + "mode": "report-only", + "handoff": "manual-maintainer", + "observedPaths": [ + ".tours/1-rag-overview.tour", + ".tours/2-document-ingestion.tour", + ".tours/3-vector-storage.tour", + ".tours/4-query-retrieval.tour", + ".tours/5-response-generation.tour", + ".tours/6-streaming-chat-history.tour" + ] + } + ] + }, + { + "id": 4, + "slug": "foundry-toolkit", + "title": "Microsoft Foundry Toolkit", + "agent": "foundry-toolkit-quest-master", + "officialSourcePrefixes": [ + "https://github.com/microsoft/azure-skills/blob/main/skills/microsoft-foundry/SKILL.md", + "https://github.com/microsoft/foundry-toolkit", + "https://code.visualstudio.com/docs/intelligentapps/overview", + "https://marketplace.visualstudio.com/items/ms-windows-ai-studio.windows-ai-studio/changelog" + ], + "sourceRepositories": [ + "microsoft/azure-skills", + "microsoft/foundry-toolkit" + ], + "writableRepositories": [ + "Azure-Samples/JavaScript-AI-Buildathon" + ], + "ownedPaths": { + "Azure-Samples/JavaScript-AI-Buildathon": [ + "04-Build-Agents-with-AIToolKit/**", + "README.md", + "docs/quests.md" + ] + }, + "externalReports": [] + }, + { + "id": 5, + "slug": "context-engineering", + "title": "Context Engineering in Agents", + "agent": "context-engineering-quest-master", + "officialSourcePrefixes": [ + "https://modelcontextprotocol.io/docs/getting-started/intro", + "https://docs.langchain.com/oss/javascript/langchain/mcp", + "https://github.com/microsoft/mcp-for-beginners", + "https://github.com/Azure-Samples/mcp-agent-langchainjs" + ], + "sourceRepositories": [ + "microsoft/mcp-for-beginners", + "Azure-Samples/mcp-agent-langchainjs" + ], + "writableRepositories": [ + "Azure-Samples/JavaScript-AI-Buildathon" + ], + "ownedPaths": { + "Azure-Samples/JavaScript-AI-Buildathon": [ + "05-Run-Burger-Ordering-Agent-System/**", + "README.md", + "docs/quests.md" + ] + }, + "externalReports": [ + { + "repository": "Azure-Samples/mcp-agent-langchainjs", + "mode": "report-only", + "handoff": "manual-maintainer", + "observedPaths": [ + ".tours/1-introduction.tour", + ".tours/2-designing-agents.tour", + ".tours/3-building-mcp-tools.tour", + ".tours/4-building-agent-api.tour", + ".tours/5-backend-api-design.tour", + ".tours/6-infrastructure-deployment.tour" + ] + } + ] + } + ] +} diff --git a/.github/agentic-refresh/report-contract.md b/.github/agentic-refresh/report-contract.md new file mode 100644 index 0000000..58a69d3 --- /dev/null +++ b/.github/agentic-refresh/report-contract.md @@ -0,0 +1,25 @@ +# Weekly report contract + +Each weekly run produces one digest covering all five quests. It creates a child +plan only for a material change. + +A child plan must contain: + +1. Quest and ISO week key. +2. Executive summary. +3. Current quest status. +4. Official-source delta table, including lifecycle. +5. Evidence URLs and source fingerprints. +6. Learner impact: `high`, `medium`, or `low`. +7. Current-content drift with file and section references. +8. Proposed changes grouped by repository and owned path. +9. Asset and CodeTour impact. +10. Stack map and deterministic branch names. +11. Validation plan. +12. Risks, uncertainty, and human decisions. +13. The exact approval instruction `/approve-refresh`. + +Reports must distinguish source facts, repository observations, inferred +impact, proposed changes, and unresolved uncertainty. Search snippets are not +evidence. No implementation may start from a digest or an unapproved child +plan. diff --git a/.github/agentic-refresh/rulesets/human-merge.json b/.github/agentic-refresh/rulesets/human-merge.json new file mode 100644 index 0000000..76dc54c --- /dev/null +++ b/.github/agentic-refresh/rulesets/human-merge.json @@ -0,0 +1,28 @@ +{ + "name": "Agentic refresh human-only merges", + "target": "branch", + "enforcement": "disabled", + "conditions": { + "ref_name": { + "include": [ + "~DEFAULT_BRANCH" + ], + "exclude": [] + } + }, + "rules": [ + { + "type": "update", + "parameters": { + "update_allows_fetch_and_merge": false + } + } + ], + "bypass_actors": [ + { + "actor_id": 40116776, + "actor_type": "User", + "bypass_mode": "pull_request" + } + ] +} diff --git a/.github/agentic-refresh/rulesets/main.json b/.github/agentic-refresh/rulesets/main.json new file mode 100644 index 0000000..3a30f39 --- /dev/null +++ b/.github/agentic-refresh/rulesets/main.json @@ -0,0 +1,52 @@ +{ + "name": "Agentic refresh main protections", + "target": "branch", + "enforcement": "disabled", + "conditions": { + "ref_name": { + "include": [ + "~DEFAULT_BRANCH" + ], + "exclude": [] + } + }, + "rules": [ + { + "type": "deletion" + }, + { + "type": "non_fast_forward" + }, + { + "type": "pull_request", + "parameters": { + "allowed_merge_methods": [ + "merge", + "squash", + "rebase" + ], + "dismiss_stale_reviews_on_push": true, + "require_code_owner_review": true, + "require_last_push_approval": true, + "required_approving_review_count": 1, + "required_review_thread_resolution": true + } + }, + { + "type": "required_status_checks", + "parameters": { + "do_not_enforce_on_create": false, + "strict_required_status_checks_policy": true, + "required_status_checks": [ + { + "context": "validate-content" + }, + { + "context": "validate-agentic-workflows" + } + ] + } + } + ], + "bypass_actors": [] +} diff --git a/.github/agentic-refresh/stack-policy.md b/.github/agentic-refresh/stack-policy.md new file mode 100644 index 0000000..3779b24 --- /dev/null +++ b/.github/agentic-refresh/stack-policy.md @@ -0,0 +1,31 @@ +# Stack and branch policy + +All automation branches must match the `branchPattern` in `quests.json`. The +automation creates stacks only inside +`Azure-Samples/JavaScript-AI-Buildathon`. + +Buildathon stack: + +```text +main +└── 01-core + └── 02-docs +``` + +- Each pull request targets the branch immediately below it. +- Skip an optional layer when it has no logical change; never create an empty + layer. +- Keep pull requests draft while an agent is editing. +- Allow one open implementation stack per quest. +- Allow at most two active quest implementations. +- Use only the repository-scoped `GITHUB_TOKEN` and gh-aw safe outputs. +- Explicitly dispatch required checks after each token-created PR or automated + branch update; stop if the current head SHA does not receive every check. +- Keep intermediate stack layers unmerged until Julia performs the approved + human stack merge. +- Never push to or merge `main`. +- Only a human may merge the highest approved layer. +- Workflow, agent, skill, governance, CODEOWNER, and validation-script files are + outside content-refresh ownership. +- External CodeTour findings are recorded in the Buildathon child issue for + manual maintainer handoff; no external stack is created. diff --git a/.github/agentic-refresh/tooling.json b/.github/agentic-refresh/tooling.json new file mode 100644 index 0000000..d4f3180 --- /dev/null +++ b/.github/agentic-refresh/tooling.json @@ -0,0 +1,7 @@ +{ + "version": 1, + "ghAwVersion": "v0.77.5", + "ghAwSetupAction": "github/gh-aw-actions/setup-cli@3ea13c02d765410340d533515cb31a7eef2baaf0", + "checkoutAction": "actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803", + "pythonVersion": "3.12" +} diff --git a/.github/agents/agentic-workflows.md b/.github/agents/agentic-workflows.md new file mode 100644 index 0000000..9a2e013 --- /dev/null +++ b/.github/agents/agentic-workflows.md @@ -0,0 +1,224 @@ +--- +name: Agentic Workflows +description: GitHub Agentic Workflows (gh-aw) - Create, debug, and upgrade AI-powered workflows with intelligent prompt routing. +disable-model-invocation: true +--- + +# GitHub Agentic Workflows Agent + +This agent helps you work with **GitHub Agentic Workflows (gh-aw)**, a CLI extension for creating AI-powered workflows in natural language using markdown files. + +## What This Agent Does + +This is a **dispatcher agent** that routes your request to the appropriate specialized prompt based on your task: + +- **Creating new workflows**: Routes to `create` prompt +- **Updating existing workflows**: Routes to `update` prompt +- **Debugging workflows**: Routes to `debug` prompt +- **Upgrading workflows**: Routes to `upgrade-agentic-workflows` prompt +- **Creating report-generating workflows**: Routes to `report` prompt — consult this whenever the workflow posts status updates, audits, analyses, or any structured output as issues, discussions, or comments +- **Creating shared components**: Routes to `create-shared-agentic-workflow` prompt +- **Fixing Dependabot PRs**: Routes to `dependabot` prompt — use this when Dependabot opens PRs that modify generated manifest files (`.github/workflows/package.json`, `.github/workflows/requirements.txt`, `.github/workflows/go.mod`). Never merge those PRs directly; instead update the source `.md` files and rerun `gh aw compile --dependabot` to bundle all fixes +- **Analyzing test coverage**: Routes to `test-coverage` prompt — consult this whenever the workflow reads, analyzes, or reports on test coverage data from PRs or CI runs +- **Rendering ASCII charts in markdown**: Routes to `asciicharts` guide — consult this whenever the workflow needs compact charts that render reliably in GitHub issues, comments, or discussions +- **CLI commands and triggering workflows**: Routes to `cli-commands` guide — consult this whenever the user asks how to run, compile, debug, or manage workflows from the command line, or when they need the MCP tool equivalent of a `gh aw` command +- **Reducing token consumption / cost optimization**: Routes to `token-optimization` guide — consult this whenever the user asks how to reduce token usage, lower costs, speed up workflows, or measure the impact of prompt changes with experiments +- **Choosing workflow architectures and design patterns**: Routes to `patterns` guide — consult this whenever the user asks for strategy, architecture, operating models, or pattern selection for agentic workflows + +Workflows may optionally include: + +- **Project tracking / monitoring** (GitHub Projects updates, status reporting) +- **Orchestration / coordination** (one workflow assigning agents or dispatching and coordinating other workflows) + +## Files This Applies To + +- Workflow files: `.github/workflows/*.md` and `.github/workflows/**/*.md` +- Workflow lock files: `.github/workflows/*.lock.yml` +- Shared components: `.github/workflows/shared/*.md` +- Configuration: `.github/aw/github-agentic-workflows.md` + +## Problems This Solves + +- **Workflow Creation**: Design secure, validated agentic workflows with proper triggers, tools, and permissions +- **Workflow Debugging**: Analyze logs, identify missing tools, investigate failures, and fix configuration issues +- **Version Upgrades**: Migrate workflows to new gh-aw versions, apply codemods, fix breaking changes +- **Component Design**: Create reusable shared workflow components that wrap MCP servers + +## How to Use + +When you interact with this agent, it will: + +1. **Understand your intent** - Determine what kind of task you're trying to accomplish +2. **Route to the right prompt** - Load the specialized prompt file for your task +3. **Execute the task** - Follow the detailed instructions in the loaded prompt + +## Available Prompts + +### Create New Workflow +**Load when**: User wants to create a new workflow from scratch, add automation, or design a workflow that doesn't exist yet + +**Prompt file**: `.github/aw/create-agentic-workflow.md` + +**Use cases**: +- "Create a workflow that triages issues" +- "I need a workflow to label pull requests" +- "Design a weekly research automation" + +### Update Existing Workflow +**Load when**: User wants to modify, improve, or refactor an existing workflow + +**Prompt file**: `.github/aw/update-agentic-workflow.md` + +**Use cases**: +- "Add web-fetch tool to the issue-classifier workflow" +- "Update the PR reviewer to use discussions instead of issues" +- "Improve the prompt for the weekly-research workflow" + +### Debug Workflow +**Load when**: User needs to investigate, audit, debug, or understand a workflow, troubleshoot issues, analyze logs, or fix errors + +**Prompt file**: `.github/aw/debug-agentic-workflow.md` + +**Use cases**: +- "Why is this workflow failing?" +- "Analyze the logs for workflow X" +- "Investigate missing tool calls in run #12345" + +### Upgrade Agentic Workflows +**Load when**: User wants to upgrade workflows to a new gh-aw version or fix deprecations + +**Prompt file**: `.github/aw/upgrade-agentic-workflows.md` + +**Use cases**: +- "Upgrade all workflows to the latest version" +- "Fix deprecated fields in workflows" +- "Apply breaking changes from the new release" + +### Create a Report-Generating Workflow +**Load when**: The workflow being created or updated produces reports — recurring status updates, audit summaries, analyses, or any structured output posted as a GitHub issue, discussion, or comment + +**Prompt file**: `.github/aw/report.md` + +**Use cases**: +- "Create a weekly CI health report" +- "Post a daily security audit to Discussions" +- "Add a status update comment to open PRs" + +### Create Shared Agentic Workflow +**Load when**: User wants to create a reusable workflow component or wrap an MCP server + +**Prompt file**: `.github/aw/create-shared-agentic-workflow.md` + +**Use cases**: +- "Create a shared component for Notion integration" +- "Wrap the Slack MCP server as a reusable component" +- "Design a shared workflow for database queries" + +### Fix Dependabot PRs +**Load when**: User needs to close or fix open Dependabot PRs that update dependencies in generated manifest files (`.github/workflows/package.json`, `.github/workflows/requirements.txt`, `.github/workflows/go.mod`) + +**Prompt file**: `.github/aw/dependabot.md` + +**Use cases**: +- "Fix the open Dependabot PRs for npm dependencies" +- "Bundle and close the Dependabot PRs for workflow dependencies" +- "Update @playwright/test to fix the Dependabot PR" + +### Analyze Test Coverage +**Load when**: The workflow reads, analyzes, or reports test coverage — whether triggered by a PR, a schedule, or a slash command. Always consult this prompt before designing the coverage data strategy. + +**Prompt file**: `.github/aw/test-coverage.md` + +**Use cases**: +- "Create a workflow that comments coverage on PRs" +- "Analyze coverage trends over time" +- "Add a coverage gate that blocks PRs below a threshold" + +### CLI Commands Reference +**Load when**: The user asks how to run, compile, debug, or manage workflows from the command line; needs the MCP tool equivalent of a `gh aw` command; or is in a restricted environment (e.g., Copilot Cloud) without direct CLI access. + +**Reference file**: `.github/aw/cli-commands.md` + +**Use cases**: +- "How do I trigger workflow X on the main branch?" +- "What's the MCP equivalent of `gh aw logs`?" +- "I'm in Copilot Cloud — how do I compile a workflow?" +- "Show me all available gh aw commands" + +### Token Consumption Optimization +**Load when**: The user asks how to reduce token usage, lower workflow costs, make a workflow faster or cheaper, or measure the impact of prompt or configuration changes. + +**Reference file**: `.github/aw/token-optimization.md` + +**Use cases**: +- "How do I reduce the token cost of this workflow?" +- "My workflow is too expensive — how do I optimize it?" +- "How do I compare token usage between two runs?" +- "Should I use gh-proxy or the MCP server?" +- "How do I use sub-agents to reduce costs?" +- "How do I measure the impact of a prompt change?" + +### Workflow Pattern Selection +**Load when**: The user asks for architecture, strategy, operating model selection, or pattern recommendations for building agentic workflows. + +**Reference file**: `.github/aw/patterns.md` + +**Use cases**: +- "Which pattern should I use for multi-repo rollout?" +- "How should I structure this workflow architecture?" +- "What pattern fits slash-command triage?" +- "Should this be DispatchOps or DailyOps?" + +## Instructions + +When a user interacts with you: + +1. **Identify the task type** from the user's request +2. **Load the appropriate prompt** from the repository paths listed above +3. **Follow the loaded prompt's instructions** exactly +4. **If uncertain**, ask clarifying questions to determine the right prompt + +## Quick Reference + +```bash +# Initialize repository for agentic workflows +gh aw init + +# Generate the lock file for a workflow +gh aw compile [workflow-name] + +# Trigger a workflow on demand (preferred over gh workflow run) +gh aw run # interactive input collection +gh aw run --ref main # run on a specific branch + +# Debug workflow runs +gh aw logs [workflow-name] +gh aw audit + +# Upgrade workflows +gh aw fix --write +gh aw compile --validate +``` + +## Key Features of gh-aw + +- **Natural Language Workflows**: Write workflows in markdown with YAML frontmatter +- **AI Engine Support**: Copilot, Claude, Codex, or custom engines +- **MCP Server Integration**: Connect to Model Context Protocol servers for tools +- **Safe Outputs**: Structured communication between AI and GitHub API +- **Strict Mode**: Security-first validation and sandboxing +- **Shared Components**: Reusable workflow building blocks +- **Repo Memory**: Persistent git-backed storage for agents +- **Sandboxed Execution**: All workflows run in the Agent Workflow Firewall (AWF) sandbox, enabling full `bash` and `edit` tools by default + +## Important Notes + +- Always reference the instructions file at `.github/aw/github-agentic-workflows.md` for complete documentation +- Use the MCP tool `agentic-workflows` when running in GitHub Copilot Cloud +- Workflows must be compiled to `.lock.yml` files before running in GitHub Actions +- **Bash tools are enabled by default** - Don't restrict bash commands unnecessarily since workflows are sandboxed by the AWF +- Follow security best practices: minimal permissions, explicit network access, no template injection +- **Network configuration**: Use ecosystem identifiers (`node`, `python`, `go`, etc.) or explicit FQDNs in `network.allowed`. Bare shorthands like `npm` or `pypi` are **not** valid. See `.github/aw/network.md` for the full list of valid ecosystem identifiers and domain patterns. +- **Single-file output**: When creating a workflow, produce exactly **one** workflow `.md` file. Do not create separate documentation files (architecture docs, runbooks, usage guides, etc.). If documentation is needed, add a brief `## Usage` section inside the workflow file itself. +- **Triggering runs**: Always use `gh aw run ` to trigger a workflow on demand — not `gh workflow run .lock.yml`. `gh aw run` handles workflow resolution by short name, input parsing and validation, and correct run-tracking for agentic workflows. Use `--ref ` to run on a specific branch. +- **CLI commands reference**: For a complete guide on all `gh aw` commands and their MCP tool equivalents (for restricted environments), see `.github/aw/cli-commands.md` diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8ac6b8c..3b5ca19 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,3 +4,7 @@ updates: directory: "/" schedule: interval: "monthly" + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/mcp.json b/.github/mcp.json new file mode 100644 index 0000000..b953af2 --- /dev/null +++ b/.github/mcp.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "github-agentic-workflows": { + "command": "gh", + "args": [ + "aw", + "mcp-server" + ] + } + } +} \ No newline at end of file diff --git a/.github/skills/agentic-workflows/SKILL.md b/.github/skills/agentic-workflows/SKILL.md new file mode 100644 index 0000000..2be813b --- /dev/null +++ b/.github/skills/agentic-workflows/SKILL.md @@ -0,0 +1,29 @@ +--- +name: agentic-workflows +description: Route gh-aw workflow create/debug/upgrade requests to the right prompts. +--- + +# Agentic Workflows Router + +Use this skill when a user asks to create, update, debug, or upgrade GitHub Agentic Workflows in this repository. + +This skill is a dispatcher: identify the task type, load the matching `.github/aw/*.md` file, and follow it directly. Keep responses concise and ask a clarifying question if the correct prompt is unclear. + +Read only the files you need: +Load these files from `github/gh-aw` (they are not available locally). + +After loading the matching workflow prompt, follow it directly: +- Create new workflows: `.github/aw/create-agentic-workflow.md` +- Update existing workflows: `.github/aw/update-agentic-workflow.md` +- Debug, audit, or investigate workflows: `.github/aw/debug-agentic-workflow.md` +- Upgrade workflows and fix deprecations: `.github/aw/upgrade-agentic-workflows.md` +- Create shared components or MCP wrappers: `.github/aw/create-shared-agentic-workflow.md` +- Create report-generating workflows: `.github/aw/report.md` +- Fix Dependabot manifest PRs: `.github/aw/dependabot.md` +- Analyze coverage workflows: `.github/aw/test-coverage.md` +- Render compact markdown charts: `.github/aw/asciicharts.md` +- Map CLI commands to MCP usage: `.github/aw/cli-commands.md` +- Choose workflow architecture and patterns: `.github/aw/patterns.md` +- Optimize token usage and cost: `.github/aw/token-optimization.md` + +When the task involves OTEL, OTLP, traces, observability backends, or telemetry-driven analysis, also read and follow `skills/otel-queries/SKILL.md` after loading the matching workflow prompt. diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 0000000..02f2af0 --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,26 @@ +name: "Copilot Setup Steps" + +# This workflow configures the environment for GitHub Copilot Agent with gh-aw MCP server +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + # The job MUST be called 'copilot-setup-steps' to be recognized by GitHub Copilot Agent + copilot-setup-steps: + runs-on: ubuntu-latest + + # Set minimal permissions for setup steps + # Copilot Agent receives its own token with appropriate permissions + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + - name: Install gh-aw extension + uses: github/gh-aw-actions/setup-cli@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + with: + version: v0.77.5 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index cb348aa..12ea51e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -17,9 +17,9 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - - uses: actions/setup-python@v6 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: "3.12" cache: pip @@ -28,7 +28,7 @@ jobs: - run: mkdocs build --strict - - uses: actions/upload-pages-artifact@v4 + - uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4 with: path: site @@ -40,4 +40,4 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} steps: - id: deployment - uses: actions/deploy-pages@v5 \ No newline at end of file + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5 \ No newline at end of file diff --git a/.github/workflows/validate-agentic-workflows.yml b/.github/workflows/validate-agentic-workflows.yml new file mode 100644 index 0000000..6f1baf3 --- /dev/null +++ b/.github/workflows/validate-agentic-workflows.yml @@ -0,0 +1,64 @@ +name: Validate Agentic Workflows + +on: + pull_request: + push: + branches: + - main + paths: + - ".gitattributes" + - ".github/agents/**" + - ".github/agentic-refresh/**" + - ".github/mcp.json" + - ".github/skills/**" + - ".github/workflows/**" + - "scripts/validate-refresh-config.mjs" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: validate-agentic-workflows-${{ github.ref }} + cancel-in-progress: true + +jobs: + validate: + name: validate-agentic-workflows + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout repository + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + + - name: Install gh-aw + uses: github/gh-aw-actions/setup-cli@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + with: + version: v0.77.5 + + - name: Validate compiled workflows + shell: bash + run: | + orphaned=0 + while IFS= read -r lock_file; do + source_file="${lock_file%.lock.yml}.md" + if [[ ! -f "$source_file" ]]; then + echo "Orphaned executable workflow: $lock_file" + orphaned=1 + fi + done < <(find .github/workflows -maxdepth 1 -name '*.lock.yml' -print) + if [[ "$orphaned" -ne 0 ]]; then + exit 1 + fi + + if find .github/workflows -maxdepth 1 -name '*.md' -print -quit | grep -q .; then + gh aw compile --strict --validate --no-check-update --purge + changes="$(git status --porcelain --untracked-files=all -- .github/workflows)" + if [[ -n "$changes" ]]; then + printf '%s\n' "$changes" + echo "Compile generated workflow changes. Commit every synchronized .lock.yml file." + exit 1 + fi + else + echo "No agentic workflow definitions exist in Phase 0." + fi diff --git a/.github/workflows/validate-content.yml b/.github/workflows/validate-content.yml new file mode 100644 index 0000000..6cd7bf9 --- /dev/null +++ b/.github/workflows/validate-content.yml @@ -0,0 +1,51 @@ +name: Validate Content + +on: + pull_request: + push: + branches: + - main + paths: + - "**/*.md" + - "**/*.json" + - "docs/**" + - "mkdocs.yml" + - "requirements.txt" + - "scripts/validate-content-links.mjs" + - "scripts/validate-refresh-config.mjs" + - ".github/workflows/validate-content.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: validate-content-${{ github.ref }} + cancel-in-progress: true + +jobs: + validate: + name: validate-content + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout repository + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + + - name: Validate refresh configuration + run: node scripts/validate-refresh-config.mjs + + - name: Validate local content links + run: node scripts/validate-content-links.mjs + + - name: Set up Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: "3.12" + cache: pip + + - name: Install documentation dependencies + run: python -m pip install --requirement requirements.txt + + - name: Build documentation + run: mkdocs build --strict diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..dbd4bd7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "github.copilot.enable": { + "markdown": true + } +} \ No newline at end of file diff --git a/scripts/validate-content-links.mjs b/scripts/validate-content-links.mjs new file mode 100644 index 0000000..2e32902 --- /dev/null +++ b/scripts/validate-content-links.mjs @@ -0,0 +1,247 @@ +import { access, readdir, readFile } from "node:fs/promises"; +import { dirname, extname, relative, resolve, sep } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const ignoredDirectories = new Set([ + ".git", + ".playwright-mcp", + "node_modules", + "site", +]); +const markdownFiles = []; +const failures = []; + +async function collectMarkdownFiles(directory) { + for (const entry of await readdir(directory, { withFileTypes: true })) { + if (entry.isDirectory() && ignoredDirectories.has(entry.name)) { + continue; + } + + const entryPath = resolve(directory, entry.name); + if (entry.isDirectory()) { + await collectMarkdownFiles(entryPath); + } else if (entry.isFile() && extname(entry.name).toLowerCase() === ".md") { + markdownFiles.push(entryPath); + } + } +} + +function stripCodeFences(content) { + let insideFence = false; + + return content + .split("\n") + .map((line) => { + if (/^\s*(```|~~~)/.test(line)) { + insideFence = !insideFence; + return ""; + } + return insideFence ? "" : line; + }) + .join("\n"); +} + +function normalizeTarget(rawTarget) { + let target = rawTarget.trim(); + + if (target.startsWith("<") && target.endsWith(">")) { + target = target.slice(1, -1); + } + + target = target.replace(/\s+["'][^"']*["']\s*$/, ""); + return target; +} + +function isExternalOrTemplate(target) { + return ( + target.length === 0 || + target.startsWith("//") || + target.startsWith("{{") || + target.startsWith("${{") || + /^[a-z][a-z0-9+.-]*:/i.test(target) + ); +} + +async function resolveLocalTarget(sourceFile, rawPath, displayTarget) { + if (!rawPath) { + return sourceFile; + } + + let decodedTarget; + try { + decodedTarget = decodeURIComponent(rawPath); + } catch { + failures.push( + `${relative(root, sourceFile)} contains an invalid encoded link: ${displayTarget}`, + ); + return sourceFile; + } + + const candidate = decodedTarget.startsWith("/") + ? resolve(root, `.${decodedTarget}`) + : resolve(dirname(sourceFile), decodedTarget); + const relativeCandidate = relative(root, candidate); + + if ( + relativeCandidate === ".." || + relativeCandidate.startsWith(`..${sep}`) + ) { + failures.push( + `${relative(root, sourceFile)} links outside the repository: ${displayTarget}`, + ); + return sourceFile; + } + + const candidates = + extname(candidate) === "" + ? [`${candidate}.md`, resolve(candidate, "index.md"), candidate] + : [candidate]; + + for (const candidatePath of candidates) { + try { + await access(candidatePath); + return candidatePath; + } catch { + // Try the next MkDocs-compatible target shape. + } + } + + return null; +} + +async function targetExists(sourceFile, rawTarget, documents) { + const target = normalizeTarget(rawTarget); + if (isExternalOrTemplate(target)) { + return true; + } + + const hashIndex = target.indexOf("#"); + const pathAndQuery = hashIndex === -1 ? target : target.slice(0, hashIndex); + const rawFragment = hashIndex === -1 ? null : target.slice(hashIndex + 1); + const rawPath = pathAndQuery.split("?", 1)[0]; + const targetFile = await resolveLocalTarget(sourceFile, rawPath, target); + + if (!targetFile) { + return false; + } + + if (rawFragment === null || rawFragment.length === 0) { + return true; + } + + let fragment; + try { + fragment = decodeURIComponent(rawFragment); + } catch { + failures.push( + `${relative(root, sourceFile)} contains an invalid encoded fragment: ${target}`, + ); + return true; + } + + if (extname(targetFile).toLowerCase() !== ".md") { + return true; + } + + const anchors = documents.get(targetFile)?.anchors; + if (!anchors?.has(fragment)) { + failures.push( + `${relative(root, sourceFile)} has a missing local fragment: ${target}`, + ); + return true; + } + + return true; +} + +function headingAnchor(heading) { + const explicitAnchor = heading.match( + /\{[^}]*#([A-Za-z][\w:.-]*)[^}]*\}\s*$/, + ); + if (explicitAnchor) { + return explicitAnchor[1]; + } + + return heading + .replace(/\{[^}]*\}\s*$/, "") + .replace(/!\[([^\]]*)]\([^)]*\)/g, "$1") + .replace(/\[([^\]]+)]\([^)]*\)/g, "$1") + .replace(/<[^>]+>/g, "") + .replace(/[`*_~]/g, "") + .trim() + .toLowerCase() + .replace(/[^\w\s-]/g, "") + .replace(/\s+/g, "-") + .replace(/-+/g, "-"); +} + +await collectMarkdownFiles(root); + +const documents = new Map(); + +for (const filePath of markdownFiles.sort()) { + const content = stripCodeFences(await readFile(filePath, "utf8")); + const anchors = new Set(); + const headingPattern = /^#{1,6}\s+(.+?)\s*#*\s*$/gm; + const htmlAnchorPattern = /\b(?:id|name)=["']([^"']+)["']/gi; + + for (const match of content.matchAll(headingPattern)) { + const anchor = headingAnchor(match[1]); + if (!anchor) { + continue; + } + if (anchors.has(anchor)) { + failures.push( + `${relative(root, filePath)} has a duplicate heading anchor: #${anchor}`, + ); + } + anchors.add(anchor); + } + + for (const match of content.matchAll(htmlAnchorPattern)) { + if (anchors.has(match[1])) { + failures.push( + `${relative(root, filePath)} has a duplicate explicit anchor: #${match[1]}`, + ); + } + anchors.add(match[1]); + } + + documents.set(filePath, { anchors, content }); +} + +for (const [filePath, document] of documents) { + const targets = []; + const inlineLinkPattern = /!?\[[^\]]*]\(([^)\n]+)\)/g; + const referenceLinkPattern = /^\s*\[[^\]]+]:\s*(\S+)/gm; + const htmlLinkPattern = /\b(?:href|src)=["']([^"']+)["']/gi; + + for (const pattern of [ + inlineLinkPattern, + referenceLinkPattern, + htmlLinkPattern, + ]) { + for (const match of document.content.matchAll(pattern)) { + targets.push(match[1]); + } + } + + for (const target of targets) { + if (!(await targetExists(filePath, target, documents))) { + failures.push( + `${relative(root, filePath)} has a missing local target: ${normalizeTarget(target)}`, + ); + } + } +} + +if (failures.length > 0) { + console.error("Local content link validation failed:"); + for (const failure of failures) { + console.error(`- ${failure}`); + } + process.exitCode = 1; +} else { + console.log(`Validated local links in ${markdownFiles.length} Markdown files.`); +} diff --git a/scripts/validate-refresh-config.mjs b/scripts/validate-refresh-config.mjs new file mode 100644 index 0000000..583dc16 --- /dev/null +++ b/scripts/validate-refresh-config.mjs @@ -0,0 +1,803 @@ +import { readdir, readFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const errors = []; + +function check(condition, message) { + if (!condition) { + errors.push(message); + } +} + +async function readJson(relativePath) { + const filePath = resolve(root, relativePath); + + try { + return JSON.parse(await readFile(filePath, "utf8")); + } catch (error) { + throw new Error(`Unable to read ${relativePath}: ${error.message}`); + } +} + +async function readText(relativePath) { + try { + return await readFile(resolve(root, relativePath), "utf8"); + } catch (error) { + throw new Error(`Unable to read ${relativePath}: ${error.message}`); + } +} + +function hasUniqueValues(values) { + return new Set(values).size === values.length; +} + +function canonicalize(value) { + if (Array.isArray(value)) { + return value.map(canonicalize); + } + + if (value !== null && typeof value === "object") { + return Object.fromEntries( + Object.entries(value) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, nestedValue]) => [key, canonicalize(nestedValue)]), + ); + } + + return value; +} + +function sameJson(left, right) { + return JSON.stringify(canonicalize(left)) === JSON.stringify(canonicalize(right)); +} + +function isSafeOwnedPath(value) { + return ( + typeof value === "string" && + value.length > 0 && + !value.startsWith("/") && + !value.includes("..") && + !value.includes("\\") && + !/[*?[\]{}]/.test(value.replace(/\/\*\*$/, "")) && + (value.endsWith("/**") || !value.includes("*")) && + !value.startsWith(".github/") && + !value.startsWith("scripts/") + ); +} + +function validateCapabilities(capabilities, expectedRepositories) { + const requiredCapabilities = [ + "repositoryScopedTokenSelected", + "copilotCodeReviewEnabled", + "copilotInferenceAvailable", + "aiCreditBudgetAvailable", + "repositoryAdministratorAvailable", + "githubTokenIssueWritesConfirmed", + "githubTokenPullRequestWritesConfirmed", + "explicitDispatchChecksConfirmed", + "refreshBranchCreationAllowed", + "rulesetConfigured", + "conversationResolutionRequired", + "codeOwnerReviewRequired", + "requiredChecksKnown", + ]; + + check(capabilities.version === 2, "capabilities.json version must be 2"); + check( + capabilities.defaultMode === "report-only", + "capabilities.json must default to report-only", + ); + check( + Array.isArray(capabilities.repositories), + "capabilities.json repositories must be an array", + ); + + const repositories = capabilities.repositories ?? []; + check( + hasUniqueValues(repositories.map(({ name }) => name)), + "capability repository names must be unique", + ); + check( + JSON.stringify(repositories.map(({ name }) => name).sort()) === + JSON.stringify([...expectedRepositories].sort()), + "capability repositories must exactly match the automation and external evidence repositories", + ); + check( + repositories.filter(({ role }) => role === "automation-host").length === 1 && + repositories.find(({ role }) => role === "automation-host")?.name === + "Azure-Samples/JavaScript-AI-Buildathon", + "Buildathon must be the only automation host", + ); + + for (const repository of repositories) { + const prefix = `capabilities for ${repository.name}`; + check( + ["unverified", "blocked", "confirmed", "report-only"].includes( + repository.status, + ), + `${prefix} has an invalid status`, + ); + check( + ["automation-host", "external-report-only"].includes(repository.role), + `${prefix} has an invalid role`, + ); + check( + typeof repository.writeAutomationAllowed === "boolean", + `${prefix} must declare writeAutomationAllowed`, + ); + + for (const key of requiredCapabilities) { + check( + typeof repository.capabilities?.[key] === "boolean", + `${prefix} must declare boolean ${key}`, + ); + } + + if (repository.role === "external-report-only") { + check( + repository.status === "report-only" && + repository.writeAutomationAllowed === false, + `${prefix} must remain report-only with writes disabled`, + ); + check( + repository.capabilities.repositoryScopedTokenSelected === false, + `${prefix} must not select a Buildathon workflow token`, + ); + check( + [ + "githubTokenIssueWritesConfirmed", + "githubTokenPullRequestWritesConfirmed", + "explicitDispatchChecksConfirmed", + "refreshBranchCreationAllowed", + "rulesetConfigured", + ].every((key) => repository.capabilities[key] === false), + `${prefix} must not declare external write capabilities`, + ); + check( + typeof repository.policyReason === "string" && + repository.policyReason.length > 0, + `${prefix} must explain the report-only policy`, + ); + } else { + check( + repository.capabilities.repositoryScopedTokenSelected === true, + `${prefix} must select the repository-scoped GITHUB_TOKEN`, + ); + } + + if (repository.writeAutomationAllowed) { + check( + repository.role === "automation-host" && + repository.status === "confirmed", + `${prefix} may allow writes only when status is confirmed`, + ); + check( + requiredCapabilities.every((key) => repository.capabilities[key]), + `${prefix} may allow writes only when every capability is true`, + ); + check( + Array.isArray(repository.requiredChecks) && + repository.requiredChecks.length > 0, + `${prefix} must list required CI checks before writes are allowed`, + ); + check( + typeof repository.approval?.approvedBy === "string" && + repository.approval.approvedBy.length > 0, + `${prefix} must record the administrator approver`, + ); + check( + typeof repository.approval?.approvedAt === "string" && + !Number.isNaN(Date.parse(repository.approval.approvedAt)), + `${prefix} must record a valid approval timestamp`, + ); + check( + typeof repository.approval?.evidence === "string" && + repository.approval.evidence.length > 0, + `${prefix} must record approval evidence`, + ); + check( + Array.isArray(repository.blockers) && repository.blockers.length === 0, + `${prefix} cannot allow writes while blockers remain`, + ); + } + } +} + +function validateQuests(config, capabilityByRepository) { + const expectedOwnedPaths = new Map([ + [ + 1, + { + "Azure-Samples/JavaScript-AI-Buildathon": [ + "01-Local-AI-Development/**", + "README.md", + "docs/quests.md", + ], + }, + ], + [ + 2, + { + "Azure-Samples/JavaScript-AI-Buildathon": [ + "02-E2E-Model-Development-on-Foundry/**", + "README.md", + "docs/quests.md", + ], + }, + ], + [ + 3, + { + "Azure-Samples/JavaScript-AI-Buildathon": [ + "03-Run-Serverless-RAG-Support-System/**", + "README.md", + "docs/quests.md", + ], + }, + ], + [ + 4, + { + "Azure-Samples/JavaScript-AI-Buildathon": [ + "04-Build-Agents-with-AIToolKit/**", + "README.md", + "docs/quests.md", + ], + }, + ], + [ + 5, + { + "Azure-Samples/JavaScript-AI-Buildathon": [ + "05-Run-Burger-Ordering-Agent-System/**", + "README.md", + "docs/quests.md", + ], + }, + ], + ]); + const expectedExternalReports = new Map([ + [1, []], + [2, []], + [ + 3, + [ + { + repository: "Azure-Samples/serverless-chat-langchainjs", + mode: "report-only", + handoff: "manual-maintainer", + observedPaths: [ + ".tours/1-rag-overview.tour", + ".tours/2-document-ingestion.tour", + ".tours/3-vector-storage.tour", + ".tours/4-query-retrieval.tour", + ".tours/5-response-generation.tour", + ".tours/6-streaming-chat-history.tour", + ], + }, + ], + ], + [4, []], + [ + 5, + [ + { + repository: "Azure-Samples/mcp-agent-langchainjs", + mode: "report-only", + handoff: "manual-maintainer", + observedPaths: [ + ".tours/1-introduction.tour", + ".tours/2-designing-agents.tour", + ".tours/3-building-mcp-tools.tour", + ".tours/4-building-agent-api.tour", + ".tours/5-backend-api-design.tour", + ".tours/6-infrastructure-deployment.tour", + ], + }, + ], + ], + ]); + check(config.version === 2, "quests.json version must be 2"); + check(config.schedule?.cron === "0 8 * * 1", "weekly cron must be 0 8 * * 1"); + check(config.schedule?.timezone === "UTC", "weekly schedule timezone must be UTC"); + check( + config.governance?.defaultMode === "report-only", + "quest governance must default to report-only", + ); + check( + config.governance?.approvalCommand === "/approve-refresh", + "approval command must be /approve-refresh", + ); + check( + config.governance?.approverVariable === "REFRESH_APPROVER_LOGIN", + "approver variable must be REFRESH_APPROVER_LOGIN", + ); + check( + config.governance?.tokenPolicyFile === + ".github/agentic-refresh/github-token.json", + "token policy file must reference github-token.json", + ); + check( + config.governance?.tokenModel === "repository-scoped-github-token", + "token model must use the repository-scoped GITHUB_TOKEN", + ); + check( + config.governance?.crossRepositoryWritesAllowed === false && + config.governance?.externalRepositoryMode === "report-only", + "cross-repository writes must be disabled and external repositories report-only", + ); + check( + config.governance?.maximumActiveImplementations === 2, + "maximum active implementations must be 2", + ); + check( + config.governance?.maximumAssetRevisionRounds === 2, + "maximum asset revision rounds must be 2", + ); + check( + config.governance?.maximumCopilotReviewRounds === 2, + "maximum Copilot review rounds must be 2", + ); + + try { + const branchPattern = new RegExp(config.governance?.branchPattern); + check( + branchPattern.test("refresh/2026-W30/foundry-local/01-core"), + "branch pattern must accept the core branch shape", + ); + check( + !branchPattern.test("main"), + "branch pattern must reject the default branch", + ); + check( + !branchPattern.test("refresh/2026-W30/agentic-rag/01-sample"), + "branch pattern must reject external sample branches", + ); + check( + !branchPattern.test("refresh/2026-W30/agentic-rag/02-codetour"), + "branch pattern must reject external CodeTour branches", + ); + } catch { + errors.push("branch pattern must be a valid regular expression"); + } + + const quests = config.quests ?? []; + check(quests.length === 5, "quests.json must define exactly five quests"); + check( + JSON.stringify(quests.map(({ id }) => id).sort()) === + JSON.stringify([1, 2, 3, 4, 5]), + "quest IDs must be exactly 1 through 5", + ); + check( + hasUniqueValues(quests.map(({ slug }) => slug)), + "quest slugs must be unique", + ); + + for (const quest of quests) { + const prefix = `quest ${quest.id}`; + check( + typeof quest.slug === "string" && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(quest.slug), + `${prefix} has an invalid slug`, + ); + check( + typeof quest.agent === "string" && quest.agent.endsWith("-quest-master"), + `${prefix} must use a quest-master agent`, + ); + check( + Array.isArray(quest.officialSourcePrefixes) && + quest.officialSourcePrefixes.length > 0, + `${prefix} must define official source prefixes`, + ); + check( + hasUniqueValues(quest.officialSourcePrefixes ?? []), + `${prefix} official source prefixes must be unique`, + ); + + for (const source of quest.officialSourcePrefixes ?? []) { + check( + typeof source === "string" && + source.startsWith("https://") && + !source.includes("*"), + `${prefix} has an invalid official source prefix: ${source}`, + ); + } + + for (const repository of quest.sourceRepositories ?? []) { + check( + /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository), + `${prefix} has an invalid source repository: ${repository}`, + ); + } + + check( + Array.isArray(quest.writableRepositories) && + quest.writableRepositories.length > 0, + `${prefix} must define writable repositories`, + ); + check( + hasUniqueValues(quest.writableRepositories ?? []), + `${prefix} writable repositories must be unique`, + ); + check( + JSON.stringify(quest.writableRepositories) === + JSON.stringify(["Azure-Samples/JavaScript-AI-Buildathon"]), + `${prefix} may write only to the Buildathon repository`, + ); + + for (const repository of quest.writableRepositories ?? []) { + check( + capabilityByRepository.has(repository), + `${prefix} writable repository is missing from capabilities.json: ${repository}`, + ); + check( + Array.isArray(quest.ownedPaths?.[repository]) && + quest.ownedPaths[repository].length > 0, + `${prefix} must define owned paths for ${repository}`, + ); + } + + for (const [repository, paths] of Object.entries(quest.ownedPaths ?? {})) { + check( + quest.writableRepositories?.includes(repository), + `${prefix} has owned paths for a non-writable repository: ${repository}`, + ); + check( + hasUniqueValues(paths), + `${prefix} owned paths for ${repository} must be unique`, + ); + + for (const ownedPath of paths) { + check( + isSafeOwnedPath(ownedPath), + `${prefix} has unsafe owned path: ${ownedPath}`, + ); + } + } + + check( + JSON.stringify(quest.ownedPaths) === + JSON.stringify(expectedOwnedPaths.get(quest.id)), + `${prefix} owned paths must exactly match the reviewed path allowlist`, + ); + check( + JSON.stringify(quest.externalReports) === + JSON.stringify(expectedExternalReports.get(quest.id)), + `${prefix} external reports must exactly match the reviewed report-only allowlist`, + ); + + for (const externalReport of quest.externalReports ?? []) { + check( + externalReport.mode === "report-only" && + externalReport.handoff === "manual-maintainer", + `${prefix} external reports must require manual maintainer handoff`, + ); + check( + quest.sourceRepositories?.includes(externalReport.repository), + `${prefix} external report repository must be an allowlisted source repository`, + ); + check( + !quest.writableRepositories?.includes(externalReport.repository), + `${prefix} external report repository must not be writable`, + ); + check( + capabilityByRepository.get(externalReport.repository)?.role === + "external-report-only" && + capabilityByRepository.get(externalReport.repository) + ?.writeAutomationAllowed === false, + `${prefix} external report repository must have a report-only capability record`, + ); + for (const observedPath of externalReport.observedPaths ?? []) { + check( + isSafeOwnedPath(observedPath), + `${prefix} has unsafe external observed path: ${observedPath}`, + ); + } + } + } +} + +function validateLabels(labels) { + const requiredLabels = [ + "refresh/plan", + "refresh/awaiting-approval", + "refresh/approved", + "refresh/implementing", + "refresh/asset-review", + "refresh/copilot-review", + "refresh/human-review", + "refresh/docs", + "refresh/final-review", + "refresh/blocked", + "refresh/completed", + "quest/1-foundry-local", + "quest/2-microsoft-foundry", + "quest/3-agentic-rag", + "quest/4-foundry-toolkit", + "quest/5-context-engineering", + ]; + const configuredLabels = labels.labels?.map(({ name }) => name) ?? []; + + check(labels.version === 1, "labels.json version must be 1"); + check( + hasUniqueValues(configuredLabels), + "label names must be unique", + ); + check( + JSON.stringify([...configuredLabels].sort()) === + JSON.stringify([...requiredLabels].sort()), + "labels.json must define the complete refresh state and quest label set", + ); + + for (const label of labels.labels ?? []) { + check(/^[0-9a-f]{6}$/i.test(label.color), `label ${label.name} has invalid color`); + check( + typeof label.description === "string" && label.description.length > 0, + `label ${label.name} must have a description`, + ); + } +} + +function validateGitHubTokenPolicy(policy) { + const expectedPermissions = { + discoveryReport: { + contents: "read", + issues: "write", + "copilot-requests": "write", + }, + implementation: { + contents: "write", + issues: "write", + "pull-requests": "write", + "copilot-requests": "write", + }, + validationDispatch: { + actions: "write", + contents: "read", + }, + }; + const allowedOperations = [ + "createRefreshBranch", + "updateRefreshBranch", + "createPullRequest", + "updatePullRequest", + "createIssue", + "updateIssue", + "comment", + ]; + const forbiddenOperations = [ + "mergePullRequest", + "updateDefaultBranch", + "deleteBranch", + "forcePush", + "writeExternalRepository", + ]; + + check(policy.version === 1, "github-token.json version must be 1"); + check(policy.token === "GITHUB_TOKEN", "token policy must use GITHUB_TOKEN"); + check( + policy.scope === "current-repository-only" && + policy.constraints?.allowedRepository === + "Azure-Samples/JavaScript-AI-Buildathon", + "GITHUB_TOKEN must be scoped to the Buildathon repository", + ); + check( + policy.storedSecretRequired === false && + policy.personalTokenFallbackAllowed === false, + "token policy must not require or permit a stored personal token", + ); + check( + policy.crossRepositoryWritesAllowed === false && + policy.externalRepositories?.mode === "report-only" && + policy.externalRepositories?.manualHandoffRequired === true, + "external repositories must remain report-only with manual handoff", + ); + check( + policy.copilotInference?.permission === "copilot-requests: write" && + policy.copilotInference?.personalTokenFallbackAllowed === false, + "Copilot inference must use copilot-requests: write without PAT fallback", + ); + check( + sameJson(policy.permissionsByOperation, expectedPermissions), + "token policy permissions must exactly match the reviewed least-privilege groups", + ); + check( + policy.constraints?.allowedBranchPrefix === "refresh/" && + policy.constraints?.defaultBranchWritesAllowed === false && + policy.constraints?.mergeAllowed === false, + "token policy must restrict branches and forbid default-branch writes and merge", + ); + check( + policy.writeHandling?.agentGitHubTools === "read-only" && + policy.writeHandling?.consumer === "gh-aw-safe-outputs", + "agent tools must be read-only and writes limited to gh-aw safe outputs", + ); + check( + sameJson( + [...(policy.writeHandling?.allowedOperations ?? [])].sort(), + [...allowedOperations].sort(), + ), + "token policy allowed operations must exactly match the reviewed safe-output allowlist", + ); + check( + forbiddenOperations.every((operation) => + policy.writeHandling?.forbiddenOperations?.includes(operation), + ), + "token policy must forbid merge, destructive, and cross-repository operations", + ); + check( + allowedOperations.every( + (operation) => + !policy.writeHandling?.forbiddenOperations?.includes(operation), + ), + "token policy allowed and forbidden operations must not overlap", + ); + check( + policy.eventModel?.tokenGeneratedEventsTriggerWorkflows === false && + policy.eventModel?.explicitDispatchRequiredAfterPullRequestCreation === + true && + policy.eventModel?.explicitDispatchRequiredAfterRefreshBranchUpdate === + true && + policy.eventModel?.requiredCheckDispatchMustBeProvenBeforeWrites === true, + "token policy must account for GITHUB_TOKEN event suppression", + ); +} + +function validateRuleset(ruleset) { + const ruleTypes = new Set(ruleset.rules?.map(({ type }) => type)); + const statusRule = ruleset.rules?.find( + ({ type }) => type === "required_status_checks", + ); + const statusContexts = + statusRule?.parameters?.required_status_checks?.map(({ context }) => context) ?? + []; + const pullRequestRule = ruleset.rules?.find( + ({ type }) => type === "pull_request", + ); + + check(ruleset.enforcement === "disabled", "ruleset template must be disabled"); + check( + ruleset.conditions?.ref_name?.include?.includes("~DEFAULT_BRANCH"), + "ruleset template must target the default branch", + ); + check(ruleTypes.has("deletion"), "ruleset must block branch deletion"); + check(ruleTypes.has("non_fast_forward"), "ruleset must block force-pushes"); + check(ruleTypes.has("pull_request"), "ruleset must require pull requests"); + check( + pullRequestRule?.parameters?.require_code_owner_review === true, + "ruleset must require CODEOWNER review", + ); + check( + pullRequestRule?.parameters?.required_review_thread_resolution === true, + "ruleset must require conversation resolution", + ); + check( + statusContexts.includes("validate-content") && + statusContexts.includes("validate-agentic-workflows"), + "ruleset must require both Phase 0 validation checks", + ); + check( + Array.isArray(ruleset.bypass_actors) && ruleset.bypass_actors.length === 0, + "main protection ruleset must not have bypass actors", + ); +} + +function validateHumanMergeRuleset(ruleset) { + const updateRule = ruleset.rules?.find(({ type }) => type === "update"); + const bypassActor = ruleset.bypass_actors?.[0]; + + check( + ruleset.enforcement === "disabled", + "human merge ruleset template must be disabled", + ); + check( + ruleset.conditions?.ref_name?.include?.includes("~DEFAULT_BRANCH"), + "human merge ruleset must target the default branch", + ); + check( + ruleset.rules?.length === 1 && updateRule, + "human merge ruleset must contain only an update restriction", + ); + check( + updateRule?.parameters?.update_allows_fetch_and_merge === false, + "human merge update restriction must not allow fetch-and-merge updates", + ); + check( + ruleset.bypass_actors?.length === 1 && + bypassActor?.actor_id === 40116776 && + bypassActor?.actor_type === "User" && + bypassActor?.bypass_mode === "pull_request", + "human merge ruleset must allow only Julia through pull requests", + ); +} + +function validatePinnedActions(workflowText, workflowPath) { + const actionPattern = /^\s*(?:-\s*)?uses\s*:\s*([^#\s]+).*$/gm; + + for (const match of workflowText.matchAll(actionPattern)) { + const action = match[1]; + if (action.startsWith("./") || action.startsWith("docker://")) { + continue; + } + + check( + /@[0-9a-f]{40}$/i.test(action), + `${workflowPath} must pin action to a full commit SHA: ${action}`, + ); + } +} + +const capabilities = await readJson(".github/agentic-refresh/capabilities.json"); +const quests = await readJson(".github/agentic-refresh/quests.json"); +const labels = await readJson(".github/agentic-refresh/labels.json"); +const tokenPolicy = await readJson( + ".github/agentic-refresh/github-token.json", +); +const ruleset = await readJson(".github/agentic-refresh/rulesets/main.json"); +const humanMergeRuleset = await readJson( + ".github/agentic-refresh/rulesets/human-merge.json", +); +const tooling = await readJson(".github/agentic-refresh/tooling.json"); +const codeowners = await readText(".github/CODEOWNERS"); +const copilotSetup = await readText( + ".github/workflows/copilot-setup-steps.yml", +); +const workflowDirectory = ".github/workflows"; +const workflowFiles = (await readdir(resolve(root, workflowDirectory))) + .filter((name) => /\.ya?ml$/i.test(name)) + .sort(); + +const expectedRepositories = new Set([ + "Azure-Samples/JavaScript-AI-Buildathon", + "Azure-Samples/serverless-chat-langchainjs", + "Azure-Samples/mcp-agent-langchainjs", +]); + +validateCapabilities(capabilities, expectedRepositories); +validateQuests( + quests, + new Map( + capabilities.repositories.map((repository) => [ + repository.name, + repository, + ]), + ), +); +validateLabels(labels); +validateGitHubTokenPolicy(tokenPolicy); +validateRuleset(ruleset); +validateHumanMergeRuleset(humanMergeRuleset); + +check( + tooling.ghAwVersion === "v0.77.5", + "tooling.json must pin gh-aw v0.77.5", +); +check( + copilotSetup.includes(tooling.ghAwSetupAction) && + copilotSetup.includes(`version: ${tooling.ghAwVersion}`), + "Copilot setup must use the pinned gh-aw action and version", +); +check( + codeowners.includes("* @juliamuiruri4 @BethanyJep"), + "CODEOWNERS must include Julia and the independent fallback reviewer", +); + +for (const workflowFile of workflowFiles) { + validatePinnedActions( + await readText(`${workflowDirectory}/${workflowFile}`), + workflowFile, + ); +} + +if (errors.length > 0) { + console.error("Agentic refresh configuration is invalid:"); + for (const error of errors) { + console.error(`- ${error}`); + } + process.exitCode = 1; +} else { + console.log( + `Validated ${quests.quests.length} quests and ${capabilities.repositories.length} repository capability records.`, + ); +}