From 61e6c9298ea01f83ded6e93fc5f4601a974afb7b Mon Sep 17 00:00:00 2001 From: juliamuiruri4 <40116776+juliamuiruri4@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:38:27 +0300 Subject: [PATCH 1/2] Add agentic refresh Phase 0 controls Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .gitattributes | 1 + .github/CODEOWNERS | 2 + .github/agentic-refresh/README.md | 28 + .github/agentic-refresh/asset-policy.md | 16 + .../agentic-refresh/audience-style-guide.md | 16 + .github/agentic-refresh/capabilities.json | 106 +++ .github/agentic-refresh/capability-matrix.md | 81 +++ .github/agentic-refresh/github-app.json | 55 ++ .github/agentic-refresh/governance.md | 80 +++ .github/agentic-refresh/labels.json | 85 +++ .github/agentic-refresh/lifecycle-policy.md | 15 + .github/agentic-refresh/quests.json | 177 ++++++ .github/agentic-refresh/report-contract.md | 25 + .../agentic-refresh/rulesets/human-merge.json | 28 + .github/agentic-refresh/rulesets/main.json | 52 ++ .github/agentic-refresh/stack-policy.md | 37 ++ .github/agentic-refresh/tooling.json | 7 + .github/agents/agentic-workflows.md | 224 +++++++ .github/dependabot.yml | 4 + .github/mcp.json | 11 + .github/skills/agentic-workflows/SKILL.md | 29 + .github/workflows/copilot-setup-steps.yml | 26 + .github/workflows/deploy.yml | 8 +- .../workflows/validate-agentic-workflows.yml | 64 ++ .github/workflows/validate-content.yml | 51 ++ .vscode/settings.json | 5 + scripts/validate-content-links.mjs | 247 +++++++ scripts/validate-refresh-config.mjs | 601 ++++++++++++++++++ 28 files changed, 2077 insertions(+), 4 deletions(-) create mode 100644 .gitattributes create mode 100644 .github/CODEOWNERS create mode 100644 .github/agentic-refresh/README.md create mode 100644 .github/agentic-refresh/asset-policy.md create mode 100644 .github/agentic-refresh/audience-style-guide.md create mode 100644 .github/agentic-refresh/capabilities.json create mode 100644 .github/agentic-refresh/capability-matrix.md create mode 100644 .github/agentic-refresh/github-app.json create mode 100644 .github/agentic-refresh/governance.md create mode 100644 .github/agentic-refresh/labels.json create mode 100644 .github/agentic-refresh/lifecycle-policy.md create mode 100644 .github/agentic-refresh/quests.json create mode 100644 .github/agentic-refresh/report-contract.md create mode 100644 .github/agentic-refresh/rulesets/human-merge.json create mode 100644 .github/agentic-refresh/rulesets/main.json create mode 100644 .github/agentic-refresh/stack-policy.md create mode 100644 .github/agentic-refresh/tooling.json create mode 100644 .github/agents/agentic-workflows.md create mode 100644 .github/mcp.json create mode 100644 .github/skills/agentic-workflows/SKILL.md create mode 100644 .github/workflows/copilot-setup-steps.yml create mode 100644 .github/workflows/validate-agentic-workflows.yml create mode 100644 .github/workflows/validate-content.yml create mode 100644 .vscode/settings.json create mode 100644 scripts/validate-content-links.mjs create mode 100644 scripts/validate-refresh-config.mjs 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..fd922e3 --- /dev/null +++ b/.github/agentic-refresh/README.md @@ -0,0 +1,28 @@ +# Agentic refresh controls + +This directory contains the Phase 0 governance boundary for future quest refresh +automation. + +- [`capabilities.json`](./capabilities.json) keeps every repository report-only + until an administrator confirms all write prerequisites. +- [`quests.json`](./quests.json) defines source allowlists, repository ownership, + approval identity, branch shape, and operational limits. +- [`governance.md`](./governance.md) documents the GitHub App and repository + settings that require administrator action. +- [`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..d4ade6d --- /dev/null +++ b/.github/agentic-refresh/capabilities.json @@ -0,0 +1,106 @@ +{ + "version": 1, + "defaultMode": "report-only", + "lastReviewedAt": "2026-07-23", + "repositories": [ + { + "name": "Azure-Samples/JavaScript-AI-Buildathon", + "status": "blocked", + "writeAutomationAllowed": false, + "capabilities": { + "githubAppInstalled": false, + "copilotCodeReviewEnabled": true, + "aiCreditBudgetAvailable": false, + "repositoryAdministratorAvailable": true, + "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 + }, + "blockers": [ + "An Azure-Samples organization owner must create or approve and install the dedicated GitHub App.", + "The Copilot Code Review AI-credit budget has not been confirmed.", + "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.", + "Automation permission to create refresh/** branches has not been approved." + ] + }, + { + "name": "Azure-Samples/serverless-chat-langchainjs", + "status": "blocked", + "writeAutomationAllowed": false, + "capabilities": { + "githubAppInstalled": false, + "copilotCodeReviewEnabled": true, + "aiCreditBudgetAvailable": false, + "repositoryAdministratorAvailable": false, + "refreshBranchCreationAllowed": false, + "rulesetConfigured": false, + "conversationResolutionRequired": false, + "codeOwnerReviewRequired": false, + "requiredChecksKnown": false + }, + "requiredChecks": [], + "approval": { + "approvedBy": null, + "approvedAt": null, + "evidence": null + }, + "blockers": [ + "No repository administrator confirmation has been recorded.", + "An Azure-Samples organization owner must approve and install the dedicated GitHub App.", + "The Copilot Code Review AI-credit budget has not been confirmed.", + "The active rulesets have not been confirmed to satisfy the refresh governance contract.", + "Automation permission to create refresh/** branches has not been approved." + ] + }, + { + "name": "Azure-Samples/mcp-agent-langchainjs", + "status": "blocked", + "writeAutomationAllowed": false, + "capabilities": { + "githubAppInstalled": false, + "copilotCodeReviewEnabled": true, + "aiCreditBudgetAvailable": false, + "repositoryAdministratorAvailable": false, + "refreshBranchCreationAllowed": false, + "rulesetConfigured": false, + "conversationResolutionRequired": false, + "codeOwnerReviewRequired": false, + "requiredChecksKnown": false + }, + "requiredChecks": [], + "approval": { + "approvedBy": null, + "approvedAt": null, + "evidence": null + }, + "blockers": [ + "No repository administrator confirmation has been recorded.", + "An Azure-Samples organization owner must approve and install the dedicated GitHub App.", + "The Copilot Code Review AI-credit budget has not been confirmed.", + "No qualifying main-branch ruleset or classic branch protection was visible.", + "Automation permission to create refresh/** branches has not been approved." + ] + } + ] +} diff --git a/.github/agentic-refresh/capability-matrix.md b/.github/agentic-refresh/capability-matrix.md new file mode 100644 index 0000000..e3b3365 --- /dev/null +++ b/.github/agentic-refresh/capability-matrix.md @@ -0,0 +1,81 @@ +# Refresh capability matrix + +The machine-readable source of truth is +[`capabilities.json`](./capabilities.json). A repository is **report-only** +unless every required capability is confirmed, an administrator records +approval evidence, and `writeAutomationAllowed` is explicitly set to `true`. + +| Repository | Current mode | Capability status | Administrator approval | +|---|---|---|---| +| `Azure-Samples/JavaScript-AI-Buildathon` | Report-only | Blocked | Missing | +| `Azure-Samples/serverless-chat-langchainjs` | Report-only | Blocked | Missing | +| `Azure-Samples/mcp-agent-langchainjs` | Report-only | Blocked | Missing | + +## Required confirmation + +For each repository, an administrator must confirm: + +1. The dedicated GitHub App is installed for that repository. +2. Copilot Code Review is enabled and an AI-credit budget is available. +3. `refresh/**` branches may be created by the App. +4. The main-branch ruleset requires pull requests, CODEOWNER approval, + conversation resolution, and the repository's known CI checks. +5. The App cannot bypass the ruleset, push to `main`, force-push, delete a + protected branch, or merge. +6. `REFRESH_APPROVER_LOGIN` is set to the exact approved GitHub login. +7. `AGENTIC_REFRESH_APP_ID` and `AGENTIC_REFRESH_APP_PRIVATE_KEY` are configured + without exposing their values in issues, logs, or repository files. + +Record the approver login, UTC timestamp, and a settings or approval reference +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: Julia has repository administration; 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, one CODEOWNER + approval, approval after the latest push, conversation resolution, and to + block deletion and force-push after this Phase 0 change lands. A separate + update-restriction template permits only Julia, through a pull request, to + update `main`; the App is not a bypass actor. GitHub returned HTTP 404 when + that user-bypass ruleset was created through the REST API, so an administrator + must confirm or create it in repository settings. The App, AI-credit budget + confirmation, ruleset activation, and explicit App permission for + `refresh/**` branches are missing. +- Serverless Chat: Julia has write but not administration; two active rulesets + and Copilot Code Review are visible. An administrator must confirm the + rulesets, budget, App installation, and `refresh/**` branch policy. +- MCP Agent: Julia has write but not administration; Copilot Code Review is + visible, but no qualifying ruleset or classic main-branch protection was + visible. An administrator must establish the complete governance boundary. + +Julia cannot access the Azure-Samples organization settings needed to create an +organization-owned App. An organization owner must complete that action. + +## Organization-owner handoff + +Ask an Azure-Samples owner to: + +1. Create an organization-owned **Azure Samples Agentic Refresh** GitHub App + with the repository permissions in `github-app.json` and no organization + permissions. +2. Disable webhooks and restrict installation to the Azure-Samples account. +3. Install it only on `Azure-Samples/JavaScript-AI-Buildathon`. +4. Provide the App ID through repository variable + `AGENTIC_REFRESH_APP_ID`. +5. Store the generated PEM private key directly in repository secret + `AGENTIC_REFRESH_APP_PRIVATE_KEY`; never send it through chat or an issue. +6. Confirm that Copilot Code Review has an available AI-credit budget. +7. Confirm that the App may create `refresh/**` branches but cannot bypass + ruleset `19622585`, write to `main`, or merge. +8. After the Phase 0 pull request merges and both validation checks have run + successfully, activate ruleset `19622585` and the human-only merge ruleset. +9. Confirm that App installation tokens are available only to the pinned + deterministic safe-output action and never to an agent or generic shell step. + +The external repositories stay report-only. Their App installation requires a +separate capability approval after a repository administrator confirms each +ruleset, CI, CODEOWNER, branch, and budget requirement. diff --git a/.github/agentic-refresh/github-app.json b/.github/agentic-refresh/github-app.json new file mode 100644 index 0000000..e630830 --- /dev/null +++ b/.github/agentic-refresh/github-app.json @@ -0,0 +1,55 @@ +{ + "version": 1, + "name": "agentic-refresh", + "installationScope": "selected-repositories", + "repositories": [ + "Azure-Samples/JavaScript-AI-Buildathon", + "Azure-Samples/serverless-chat-langchainjs", + "Azure-Samples/mcp-agent-langchainjs" + ], + "permissions": { + "metadata": "read", + "contents": "write", + "pullRequests": "write", + "issues": "write", + "actions": "read", + "checks": "read" + }, + "forbiddenPermissions": [ + "administration", + "deployments", + "environments", + "organizationAdministration", + "releases" + ], + "credentials": { + "appIdVariable": "AGENTIC_REFRESH_APP_ID", + "privateKeySecret": "AGENTIC_REFRESH_APP_PRIVATE_KEY" + }, + "constraints": { + "allowedBranchPrefix": "refresh/", + "defaultBranchWritesAllowed": false, + "mergeAllowed": false, + "humanMergeRuleset": ".github/agentic-refresh/rulesets/human-merge.json", + "installationTokenMaximumLifetimeMinutes": 60 + }, + "tokenHandling": { + "exposedToAgent": false, + "consumer": "pinned-deterministic-safe-output-action", + "allowedOperations": [ + "createRefreshBranch", + "updateRefreshBranch", + "createPullRequest", + "updatePullRequest", + "createIssue", + "updateIssue", + "comment" + ], + "forbiddenOperations": [ + "mergePullRequest", + "updateDefaultBranch", + "deleteBranch", + "forcePush" + ] + } +} diff --git a/.github/agentic-refresh/governance.md b/.github/agentic-refresh/governance.md new file mode 100644 index 0000000..7ebb085 --- /dev/null +++ b/.github/agentic-refresh/governance.md @@ -0,0 +1,80 @@ +# 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 the target repository is explicitly approved for write +automation. + +## GitHub App + +Create a dedicated GitHub App from [`github-app.json`](./github-app.json): + +1. Select **Only on this account** for installation. +2. Grant only the listed repository permissions. +3. Install it only on repositories whose capability matrix is approved. +4. Store the App ID as `AGENTIC_REFRESH_APP_ID`. +5. Store the private key as `AGENTIC_REFRESH_APP_PRIVATE_KEY`. +6. Never grant administration, deployment, environment, release, organization, + ruleset-bypass, or merge authority. + +Tokens must be installation tokens scoped to one target repository and must +expire within one hour. Workflows may create only `refresh/**` branches. The +agent process must never receive an App token. A pinned deterministic safe-output +action mints and consumes the token only after validating the target repository, +branch, paths, operation count, and patch size. That action exposes no merge, +default-branch update, deletion, or force-push operation. + +## 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). Configure: + +- variable `REFRESH_APPROVER_LOGIN`; +- variable `AGENTIC_REFRESH_APP_ID`; +- secret `AGENTIC_REFRESH_APP_PRIVATE_KEY`. + +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. + +The App must not be a bypass actor. 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. + +The platform update restriction protects the final `main` update. The App must +update `refresh/**` branches to build a stack, so intermediate stack bases cannot +use that restriction. Their no-merge boundary is enforced by token isolation: +only the pinned safe-output action receives the App token, and merge is not an +available output. Changes to that action, workflow policy, or permissions require +the protected automation review path. + +## SAML authorization + +If `gh api repos/Azure-Samples/JavaScript-AI-Buildathon` 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 or authorize the overriding token. + +Do not replace this control with a broader personal access token. 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..ee606b0 --- /dev/null +++ b/.github/agentic-refresh/quests.json @@ -0,0 +1,177 @@ +{ + "version": 1, + "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", + "approvalCommand": "/approve-refresh", + "approverVariable": "REFRESH_APPROVER_LOGIN", + "githubAppIdVariable": "AGENTIC_REFRESH_APP_ID", + "githubAppPrivateKeySecret": "AGENTIC_REFRESH_APP_PRIVATE_KEY", + "branchPattern": "^refresh/[0-9]{4}-W[0-9]{2}/[a-z0-9-]+/(01-core|02-docs|01-sample|02-codetour)$", + "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" + ] + } + }, + { + "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" + ] + } + }, + { + "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", + "Azure-Samples/serverless-chat-langchainjs" + ], + "ownedPaths": { + "Azure-Samples/JavaScript-AI-Buildathon": [ + "03-Run-Serverless-RAG-Support-System/**", + "README.md", + "docs/quests.md" + ], + "Azure-Samples/serverless-chat-langchainjs": [ + ".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" + ] + } + }, + { + "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", + "Azure-Samples/mcp-agent-langchainjs" + ], + "ownedPaths": { + "Azure-Samples/JavaScript-AI-Buildathon": [ + "05-Run-Burger-Ordering-Agent-System/**", + "README.md", + "docs/quests.md" + ], + "Azure-Samples/mcp-agent-langchainjs": [ + ".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..d2c69d8 --- /dev/null +++ b/.github/agentic-refresh/stack-policy.md @@ -0,0 +1,37 @@ +# Stack and branch policy + +All automation branches must match the `branchPattern` in `quests.json`. A stack +is a same-repository pull-request chain; related stacks in different +repositories are linked through the approved child issue. + +Buildathon stack: + +```text +main +└── 01-core + └── 02-docs +``` + +External sample stack: + +```text +main +└── 01-sample + └── 02-codetour +``` + +- 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 and repository. +- Allow at most two active quest implementations across all repositories. +- Never expose the GitHub App token to an agent or generic shell step. Only the + pinned deterministic safe-output action may update `refresh/**` branches or + create pull requests, and it must not implement a merge operation. +- 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. 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..8f23d36 --- /dev/null +++ b/scripts/validate-refresh-config.mjs @@ -0,0 +1,601 @@ +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 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 = [ + "githubAppInstalled", + "copilotCodeReviewEnabled", + "aiCreditBudgetAvailable", + "repositoryAdministratorAvailable", + "refreshBranchCreationAllowed", + "rulesetConfigured", + "conversationResolutionRequired", + "codeOwnerReviewRequired", + "requiredChecksKnown", + ]; + + check(capabilities.version === 1, "capabilities.json version must be 1"); + 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 writable repository allowlist", + ); + + for (const repository of repositories) { + const prefix = `capabilities for ${repository.name}`; + check( + ["unverified", "blocked", "confirmed"].includes(repository.status), + `${prefix} has an invalid status`, + ); + 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.writeAutomationAllowed) { + check( + 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, capabilityRepositories) { + 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", + ], + "Azure-Samples/serverless-chat-langchainjs": [ + ".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, + { + "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", + ], + "Azure-Samples/mcp-agent-langchainjs": [ + ".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 === 1, "quests.json version must be 1"); + 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?.githubAppIdVariable === "AGENTIC_REFRESH_APP_ID", + "GitHub App ID variable name is invalid", + ); + check( + config.governance?.githubAppPrivateKeySecret === + "AGENTIC_REFRESH_APP_PRIVATE_KEY", + "GitHub App private-key secret name is invalid", + ); + 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", + ); + } 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`, + ); + + for (const repository of quest.writableRepositories ?? []) { + check( + capabilityRepositories.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`, + ); + } +} + +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 validateGitHubApp(app, expectedRepositories) { + const exactPermissions = { + metadata: "read", + contents: "write", + pullRequests: "write", + issues: "write", + actions: "read", + checks: "read", + }; + + check(app.version === 1, "github-app.json version must be 1"); + check( + app.installationScope === "selected-repositories", + "GitHub App installation must use selected repositories", + ); + check( + JSON.stringify([...app.repositories].sort()) === + JSON.stringify([...expectedRepositories].sort()), + "GitHub App repositories must match the writable repository allowlist", + ); + check( + JSON.stringify(app.permissions) === JSON.stringify(exactPermissions), + "GitHub App permissions must match the least-privilege policy", + ); + check( + app.constraints?.allowedBranchPrefix === "refresh/", + "GitHub App branch prefix must be refresh/", + ); + check( + app.constraints?.defaultBranchWritesAllowed === false, + "GitHub App must not write to the default branch", + ); + check(app.constraints?.mergeAllowed === false, "GitHub App must not merge"); + check( + app.constraints?.humanMergeRuleset === + ".github/agentic-refresh/rulesets/human-merge.json", + "GitHub App policy must reference the human-only merge ruleset", + ); + check( + app.constraints?.installationTokenMaximumLifetimeMinutes <= 60, + "GitHub App tokens must expire within 60 minutes", + ); + check( + app.tokenHandling?.exposedToAgent === false, + "GitHub App tokens must not be exposed to agents", + ); + check( + app.tokenHandling?.consumer === "pinned-deterministic-safe-output-action", + "GitHub App tokens must be limited to the deterministic safe-output action", + ); + check( + app.tokenHandling?.forbiddenOperations?.includes("mergePullRequest") && + app.tokenHandling.forbiddenOperations.includes("updateDefaultBranch") && + app.tokenHandling.forbiddenOperations.includes("deleteBranch") && + app.tokenHandling.forbiddenOperations.includes("forcePush"), + "GitHub App token policy must forbid merge and destructive operations", + ); +} + +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 app = await readJson(".github/agentic-refresh/github-app.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 Set(capabilities.repositories.map(({ name }) => name))); +validateLabels(labels); +validateGitHubApp(app, expectedRepositories); +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} report-only repository capability records.`, + ); +} From 6034abca7fb75e6e36534f7dcd277e0722e2810c Mon Sep 17 00:00:00 2001 From: juliamuiruri4 <40116776+juliamuiruri4@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:18:07 +0300 Subject: [PATCH 2/2] Revise Phase 0 for repository token Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/agentic-refresh/README.md | 16 +- .github/agentic-refresh/capabilities.json | 58 ++-- .github/agentic-refresh/capability-matrix.md | 114 +++---- .github/agentic-refresh/github-app.json | 55 --- .github/agentic-refresh/github-token.json | 66 ++++ .github/agentic-refresh/governance.md | 79 +++-- .github/agentic-refresh/quests.json | 75 ++-- .github/agentic-refresh/stack-policy.md | 26 +- scripts/validate-refresh-config.mjs | 338 +++++++++++++++---- 9 files changed, 531 insertions(+), 296 deletions(-) delete mode 100644 .github/agentic-refresh/github-app.json create mode 100644 .github/agentic-refresh/github-token.json diff --git a/.github/agentic-refresh/README.md b/.github/agentic-refresh/README.md index fd922e3..94176b3 100644 --- a/.github/agentic-refresh/README.md +++ b/.github/agentic-refresh/README.md @@ -3,12 +3,16 @@ This directory contains the Phase 0 governance boundary for future quest refresh automation. -- [`capabilities.json`](./capabilities.json) keeps every repository report-only - until an administrator confirms all write prerequisites. -- [`quests.json`](./quests.json) defines source allowlists, repository ownership, - approval identity, branch shape, and operational limits. -- [`governance.md`](./governance.md) documents the GitHub App and repository - settings that require administrator action. +- [`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), diff --git a/.github/agentic-refresh/capabilities.json b/.github/agentic-refresh/capabilities.json index d4ade6d..643beb8 100644 --- a/.github/agentic-refresh/capabilities.json +++ b/.github/agentic-refresh/capabilities.json @@ -1,17 +1,22 @@ { - "version": 1, + "version": 2, "defaultMode": "report-only", - "lastReviewedAt": "2026-07-23", + "lastReviewedAt": "2026-07-24", "repositories": [ { "name": "Azure-Samples/JavaScript-AI-Buildathon", + "role": "automation-host", "status": "blocked", "writeAutomationAllowed": false, "capabilities": { - "githubAppInstalled": false, + "repositoryScopedTokenSelected": true, "copilotCodeReviewEnabled": true, + "copilotInferenceAvailable": false, "aiCreditBudgetAvailable": false, "repositoryAdministratorAvailable": true, + "githubTokenIssueWritesConfirmed": false, + "githubTokenPullRequestWritesConfirmed": false, + "explicitDispatchChecksConfirmed": false, "refreshBranchCreationAllowed": false, "rulesetConfigured": false, "conversationResolutionRequired": true, @@ -34,25 +39,31 @@ "copilotCodeReviewWorkflowId": 209015997, "rulesetId": 19622585, "approverVariable": "REFRESH_APPROVER_LOGIN=juliamuiruri4", - "labelsConfigured": true + "labelsConfigured": true, + "phaseZeroPullRequest": 110 }, "blockers": [ - "An Azure-Samples organization owner must create or approve and install the dedicated GitHub App.", - "The Copilot Code Review AI-credit budget has not been confirmed.", + "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.", - "Automation permission to create refresh/** branches has not been approved." + "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", - "status": "blocked", + "role": "external-report-only", + "status": "report-only", "writeAutomationAllowed": false, "capabilities": { - "githubAppInstalled": false, + "repositoryScopedTokenSelected": false, "copilotCodeReviewEnabled": true, + "copilotInferenceAvailable": false, "aiCreditBudgetAvailable": false, "repositoryAdministratorAvailable": false, + "githubTokenIssueWritesConfirmed": false, + "githubTokenPullRequestWritesConfirmed": false, + "explicitDispatchChecksConfirmed": false, "refreshBranchCreationAllowed": false, "rulesetConfigured": false, "conversationResolutionRequired": false, @@ -65,23 +76,23 @@ "approvedAt": null, "evidence": null }, - "blockers": [ - "No repository administrator confirmation has been recorded.", - "An Azure-Samples organization owner must approve and install the dedicated GitHub App.", - "The Copilot Code Review AI-credit budget has not been confirmed.", - "The active rulesets have not been confirmed to satisfy the refresh governance contract.", - "Automation permission to create refresh/** branches has not been approved." - ] + "policyReason": "Cross-repository writes are intentionally disabled; findings require a manual maintainer handoff.", + "blockers": [] }, { "name": "Azure-Samples/mcp-agent-langchainjs", - "status": "blocked", + "role": "external-report-only", + "status": "report-only", "writeAutomationAllowed": false, "capabilities": { - "githubAppInstalled": false, + "repositoryScopedTokenSelected": false, "copilotCodeReviewEnabled": true, + "copilotInferenceAvailable": false, "aiCreditBudgetAvailable": false, "repositoryAdministratorAvailable": false, + "githubTokenIssueWritesConfirmed": false, + "githubTokenPullRequestWritesConfirmed": false, + "explicitDispatchChecksConfirmed": false, "refreshBranchCreationAllowed": false, "rulesetConfigured": false, "conversationResolutionRequired": false, @@ -94,13 +105,8 @@ "approvedAt": null, "evidence": null }, - "blockers": [ - "No repository administrator confirmation has been recorded.", - "An Azure-Samples organization owner must approve and install the dedicated GitHub App.", - "The Copilot Code Review AI-credit budget has not been confirmed.", - "No qualifying main-branch ruleset or classic branch protection was visible.", - "Automation permission to create refresh/** branches has not been approved." - ] + "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 index e3b3365..7b0ae75 100644 --- a/.github/agentic-refresh/capability-matrix.md +++ b/.github/agentic-refresh/capability-matrix.md @@ -1,81 +1,69 @@ # Refresh capability matrix The machine-readable source of truth is -[`capabilities.json`](./capabilities.json). A repository is **report-only** -unless every required capability is confirmed, an administrator records -approval evidence, and `writeAutomationAllowed` is explicitly set to `true`. +[`capabilities.json`](./capabilities.json). -| Repository | Current mode | Capability status | Administrator approval | +- 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` | Report-only | Blocked | Missing | -| `Azure-Samples/serverless-chat-langchainjs` | Report-only | Blocked | Missing | -| `Azure-Samples/mcp-agent-langchainjs` | Report-only | Blocked | Missing | +| `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 | -## Required confirmation +## Buildathon confirmation -For each repository, an administrator must confirm: +Before Buildathon write automation is enabled, confirm: -1. The dedicated GitHub App is installed for that repository. -2. Copilot Code Review is enabled and an AI-credit budget is available. -3. `refresh/**` branches may be created by the App. -4. The main-branch ruleset requires pull requests, CODEOWNER approval, - conversation resolution, and the repository's known CI checks. -5. The App cannot bypass the ruleset, push to `main`, force-push, delete a - protected branch, or merge. -6. `REFRESH_APPROVER_LOGIN` is set to the exact approved GitHub login. -7. `AGENTIC_REFRESH_APP_ID` and `AGENTIC_REFRESH_APP_PRIVATE_KEY` are configured - without exposing their values in issues, logs, or repository files. +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 a settings or approval reference -in `capabilities.json`. Do not replace an unknown capability with an assumption. +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: Julia has repository administration; 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, one CODEOWNER - approval, approval after the latest push, conversation resolution, and to - block deletion and force-push after this Phase 0 change lands. A separate - update-restriction template permits only Julia, through a pull request, to - update `main`; the App is not a bypass actor. GitHub returned HTTP 404 when - that user-bypass ruleset was created through the REST API, so an administrator - must confirm or create it in repository settings. The App, AI-credit budget - confirmation, ruleset activation, and explicit App permission for - `refresh/**` branches are missing. -- Serverless Chat: Julia has write but not administration; two active rulesets - and Copilot Code Review are visible. An administrator must confirm the - rulesets, budget, App installation, and `refresh/**` branch policy. -- MCP Agent: Julia has write but not administration; Copilot Code Review is - visible, but no qualifying ruleset or classic main-branch protection was - visible. An administrator must establish the complete governance boundary. - -Julia cannot access the Azure-Samples organization settings needed to create an -organization-owned App. An organization owner must complete that action. +- 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. -## Organization-owner handoff +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. -Ask an Azure-Samples owner to: +## Activation sequence -1. Create an organization-owned **Azure Samples Agentic Refresh** GitHub App - with the repository permissions in `github-app.json` and no organization - permissions. -2. Disable webhooks and restrict installation to the Azure-Samples account. -3. Install it only on `Azure-Samples/JavaScript-AI-Buildathon`. -4. Provide the App ID through repository variable - `AGENTIC_REFRESH_APP_ID`. -5. Store the generated PEM private key directly in repository secret - `AGENTIC_REFRESH_APP_PRIVATE_KEY`; never send it through chat or an issue. -6. Confirm that Copilot Code Review has an available AI-credit budget. -7. Confirm that the App may create `refresh/**` branches but cannot bypass - ruleset `19622585`, write to `main`, or merge. -8. After the Phase 0 pull request merges and both validation checks have run - successfully, activate ruleset `19622585` and the human-only merge ruleset. -9. Confirm that App installation tokens are available only to the pinned - deterministic safe-output action and never to an agent or generic shell step. +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. -The external repositories stay report-only. Their App installation requires a -separate capability approval after a repository administrator confirms each -ruleset, CI, CODEOWNER, branch, and budget requirement. +External CodeTour findings remain manual handoffs regardless of Buildathon +capability results. diff --git a/.github/agentic-refresh/github-app.json b/.github/agentic-refresh/github-app.json deleted file mode 100644 index e630830..0000000 --- a/.github/agentic-refresh/github-app.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "version": 1, - "name": "agentic-refresh", - "installationScope": "selected-repositories", - "repositories": [ - "Azure-Samples/JavaScript-AI-Buildathon", - "Azure-Samples/serverless-chat-langchainjs", - "Azure-Samples/mcp-agent-langchainjs" - ], - "permissions": { - "metadata": "read", - "contents": "write", - "pullRequests": "write", - "issues": "write", - "actions": "read", - "checks": "read" - }, - "forbiddenPermissions": [ - "administration", - "deployments", - "environments", - "organizationAdministration", - "releases" - ], - "credentials": { - "appIdVariable": "AGENTIC_REFRESH_APP_ID", - "privateKeySecret": "AGENTIC_REFRESH_APP_PRIVATE_KEY" - }, - "constraints": { - "allowedBranchPrefix": "refresh/", - "defaultBranchWritesAllowed": false, - "mergeAllowed": false, - "humanMergeRuleset": ".github/agentic-refresh/rulesets/human-merge.json", - "installationTokenMaximumLifetimeMinutes": 60 - }, - "tokenHandling": { - "exposedToAgent": false, - "consumer": "pinned-deterministic-safe-output-action", - "allowedOperations": [ - "createRefreshBranch", - "updateRefreshBranch", - "createPullRequest", - "updatePullRequest", - "createIssue", - "updateIssue", - "comment" - ], - "forbiddenOperations": [ - "mergePullRequest", - "updateDefaultBranch", - "deleteBranch", - "forcePush" - ] - } -} 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 index 7ebb085..a654897 100644 --- a/.github/agentic-refresh/governance.md +++ b/.github/agentic-refresh/governance.md @@ -5,27 +5,42 @@ 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 the target repository is explicitly approved for write -automation. +report-only mode unless Buildathon is explicitly approved for write automation. +The two external sample repositories remain report-only and require manual +maintainer handoff. -## GitHub App +## Repository-scoped authentication -Create a dedicated GitHub App from [`github-app.json`](./github-app.json): +Use GitHub Actions' built-in `GITHUB_TOKEN` as defined in +[`github-token.json`](./github-token.json): -1. Select **Only on this account** for installation. -2. Grant only the listed repository permissions. -3. Install it only on repositories whose capability matrix is approved. -4. Store the App ID as `AGENTIC_REFRESH_APP_ID`. -5. Store the private key as `AGENTIC_REFRESH_APP_PRIVATE_KEY`. -6. Never grant administration, deployment, environment, release, organization, - ruleset-bypass, or merge authority. +- 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. -Tokens must be installation tokens scoped to one target repository and must -expire within one hour. Workflows may create only `refresh/**` branches. The -agent process must never receive an App token. A pinned deterministic safe-output -action mints and consumes the token only after validating the target repository, -branch, paths, operation count, and patch size. That action exposes no merge, -default-branch update, deletion, or force-push operation. +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 @@ -34,11 +49,8 @@ Apply the disabled templates in [`rulesets/main.json`](./rulesets/main.json) and validation workflows have produced successful check runs. Review the generated settings before changing `enforcement` to `active`. -Create the labels in [`labels.json`](./labels.json). Configure: - -- variable `REFRESH_APPROVER_LOGIN`; -- variable `AGENTIC_REFRESH_APP_ID`; -- secret `AGENTIC_REFRESH_APP_PRIVATE_KEY`. +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: @@ -50,8 +62,8 @@ The main ruleset must require: - `validate-agentic-workflows`; - no force-push or branch deletion. -The App must not be a bypass actor. Phase 3 adds `copilot-review-gate` only after -that check has been proven on every pull-request head SHA. +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 @@ -59,22 +71,23 @@ 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. -The platform update restriction protects the final `main` update. The App must -update `refresh/**` branches to build a stack, so intermediate stack bases cannot -use that restriction. Their no-merge boundary is enforced by token isolation: -only the pinned safe-output action receives the App token, and merge is not an -available output. Changes to that action, workflow policy, or permissions require -the protected automation review path. +## 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 -If `gh api repos/Azure-Samples/JavaScript-AI-Buildathon` returns HTTP 403: +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 or authorize the overriding token. + the override for the audit. -Do not replace this control with a broader personal access token. +Do not copy that OAuth token into repository secrets. diff --git a/.github/agentic-refresh/quests.json b/.github/agentic-refresh/quests.json index ee606b0..4fa7fbf 100644 --- a/.github/agentic-refresh/quests.json +++ b/.github/agentic-refresh/quests.json @@ -1,5 +1,5 @@ { - "version": 1, + "version": 2, "schedule": { "cron": "0 8 * * 1", "timezone": "UTC", @@ -8,11 +8,13 @@ "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", - "githubAppIdVariable": "AGENTIC_REFRESH_APP_ID", - "githubAppPrivateKeySecret": "AGENTIC_REFRESH_APP_PRIVATE_KEY", - "branchPattern": "^refresh/[0-9]{4}-W[0-9]{2}/[a-z0-9-]+/(01-core|02-docs|01-sample|02-codetour)$", + "branchPattern": "^refresh/[0-9]{4}-W[0-9]{2}/[a-z0-9-]+/(01-core|02-docs)$", "maximumActiveImplementations": 2, "maximumAssetRevisionRounds": 2, "maximumCopilotReviewRounds": 2, @@ -52,7 +54,8 @@ "README.md", "docs/quests.md" ] - } + }, + "externalReports": [] }, { "id": 2, @@ -75,7 +78,8 @@ "README.md", "docs/quests.md" ] - } + }, + "externalReports": [] }, { "id": 3, @@ -93,24 +97,30 @@ "microsoft/langchainjs-for-beginners" ], "writableRepositories": [ - "Azure-Samples/JavaScript-AI-Buildathon", - "Azure-Samples/serverless-chat-langchainjs" + "Azure-Samples/JavaScript-AI-Buildathon" ], "ownedPaths": { "Azure-Samples/JavaScript-AI-Buildathon": [ "03-Run-Serverless-RAG-Support-System/**", "README.md", "docs/quests.md" - ], - "Azure-Samples/serverless-chat-langchainjs": [ - ".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" ] - } + }, + "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, @@ -136,7 +146,8 @@ "README.md", "docs/quests.md" ] - } + }, + "externalReports": [] }, { "id": 5, @@ -154,24 +165,30 @@ "Azure-Samples/mcp-agent-langchainjs" ], "writableRepositories": [ - "Azure-Samples/JavaScript-AI-Buildathon", - "Azure-Samples/mcp-agent-langchainjs" + "Azure-Samples/JavaScript-AI-Buildathon" ], "ownedPaths": { "Azure-Samples/JavaScript-AI-Buildathon": [ "05-Run-Burger-Ordering-Agent-System/**", "README.md", "docs/quests.md" - ], - "Azure-Samples/mcp-agent-langchainjs": [ - ".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" ] - } + }, + "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/stack-policy.md b/.github/agentic-refresh/stack-policy.md index d2c69d8..3779b24 100644 --- a/.github/agentic-refresh/stack-policy.md +++ b/.github/agentic-refresh/stack-policy.md @@ -1,8 +1,8 @@ # Stack and branch policy -All automation branches must match the `branchPattern` in `quests.json`. A stack -is a same-repository pull-request chain; related stacks in different -repositories are linked through the approved child issue. +All automation branches must match the `branchPattern` in `quests.json`. The +automation creates stacks only inside +`Azure-Samples/JavaScript-AI-Buildathon`. Buildathon stack: @@ -12,26 +12,20 @@ main └── 02-docs ``` -External sample stack: - -```text -main -└── 01-sample - └── 02-codetour -``` - - 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 and repository. -- Allow at most two active quest implementations across all repositories. -- Never expose the GitHub App token to an agent or generic shell step. Only the - pinned deterministic safe-output action may update `refresh/**` branches or - create pull requests, and it must not implement a merge operation. +- 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/scripts/validate-refresh-config.mjs b/scripts/validate-refresh-config.mjs index 8f23d36..583dc16 100644 --- a/scripts/validate-refresh-config.mjs +++ b/scripts/validate-refresh-config.mjs @@ -33,6 +33,26 @@ 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" && @@ -49,10 +69,14 @@ function isSafeOwnedPath(value) { function validateCapabilities(capabilities, expectedRepositories) { const requiredCapabilities = [ - "githubAppInstalled", + "repositoryScopedTokenSelected", "copilotCodeReviewEnabled", + "copilotInferenceAvailable", "aiCreditBudgetAvailable", "repositoryAdministratorAvailable", + "githubTokenIssueWritesConfirmed", + "githubTokenPullRequestWritesConfirmed", + "explicitDispatchChecksConfirmed", "refreshBranchCreationAllowed", "rulesetConfigured", "conversationResolutionRequired", @@ -60,7 +84,7 @@ function validateCapabilities(capabilities, expectedRepositories) { "requiredChecksKnown", ]; - check(capabilities.version === 1, "capabilities.json version must be 1"); + check(capabilities.version === 2, "capabilities.json version must be 2"); check( capabilities.defaultMode === "report-only", "capabilities.json must default to report-only", @@ -78,15 +102,27 @@ function validateCapabilities(capabilities, expectedRepositories) { check( JSON.stringify(repositories.map(({ name }) => name).sort()) === JSON.stringify([...expectedRepositories].sort()), - "capability repositories must exactly match the writable repository allowlist", + "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"].includes(repository.status), + ["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`, @@ -99,9 +135,42 @@ function validateCapabilities(capabilities, expectedRepositories) { ); } + 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.status === "confirmed", + repository.role === "automation-host" && + repository.status === "confirmed", `${prefix} may allow writes only when status is confirmed`, ); check( @@ -136,7 +205,7 @@ function validateCapabilities(capabilities, expectedRepositories) { } } -function validateQuests(config, capabilityRepositories) { +function validateQuests(config, capabilityByRepository) { const expectedOwnedPaths = new Map([ [ 1, @@ -166,14 +235,6 @@ function validateQuests(config, capabilityRepositories) { "README.md", "docs/quests.md", ], - "Azure-Samples/serverless-chat-langchainjs": [ - ".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", - ], }, ], [ @@ -194,18 +255,51 @@ function validateQuests(config, capabilityRepositories) { "README.md", "docs/quests.md", ], - "Azure-Samples/mcp-agent-langchainjs": [ - ".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 === 1, "quests.json version must be 1"); + 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( @@ -221,13 +315,18 @@ function validateQuests(config, capabilityRepositories) { "approver variable must be REFRESH_APPROVER_LOGIN", ); check( - config.governance?.githubAppIdVariable === "AGENTIC_REFRESH_APP_ID", - "GitHub App ID variable name is invalid", + 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?.githubAppPrivateKeySecret === - "AGENTIC_REFRESH_APP_PRIVATE_KEY", - "GitHub App private-key secret name is invalid", + 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, @@ -252,6 +351,14 @@ function validateQuests(config, capabilityRepositories) { !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"); } @@ -313,10 +420,15 @@ function validateQuests(config, capabilityRepositories) { 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( - capabilityRepositories.has(repository), + capabilityByRepository.has(repository), `${prefix} writable repository is missing from capabilities.json: ${repository}`, ); check( @@ -349,6 +461,40 @@ function validateQuests(config, capabilityRepositories) { 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}`, + ); + } + } } } @@ -393,62 +539,108 @@ function validateLabels(labels) { } } -function validateGitHubApp(app, expectedRepositories) { - const exactPermissions = { - metadata: "read", - contents: "write", - pullRequests: "write", - issues: "write", - actions: "read", - checks: "read", +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(app.version === 1, "github-app.json version must be 1"); + check(policy.version === 1, "github-token.json version must be 1"); + check(policy.token === "GITHUB_TOKEN", "token policy must use GITHUB_TOKEN"); check( - app.installationScope === "selected-repositories", - "GitHub App installation must use selected repositories", + policy.scope === "current-repository-only" && + policy.constraints?.allowedRepository === + "Azure-Samples/JavaScript-AI-Buildathon", + "GITHUB_TOKEN must be scoped to the Buildathon repository", ); check( - JSON.stringify([...app.repositories].sort()) === - JSON.stringify([...expectedRepositories].sort()), - "GitHub App repositories must match the writable repository allowlist", + policy.storedSecretRequired === false && + policy.personalTokenFallbackAllowed === false, + "token policy must not require or permit a stored personal token", ); check( - JSON.stringify(app.permissions) === JSON.stringify(exactPermissions), - "GitHub App permissions must match the least-privilege policy", + policy.crossRepositoryWritesAllowed === false && + policy.externalRepositories?.mode === "report-only" && + policy.externalRepositories?.manualHandoffRequired === true, + "external repositories must remain report-only with manual handoff", ); check( - app.constraints?.allowedBranchPrefix === "refresh/", - "GitHub App branch prefix must be refresh/", + policy.copilotInference?.permission === "copilot-requests: write" && + policy.copilotInference?.personalTokenFallbackAllowed === false, + "Copilot inference must use copilot-requests: write without PAT fallback", ); check( - app.constraints?.defaultBranchWritesAllowed === false, - "GitHub App must not write to the default branch", + sameJson(policy.permissionsByOperation, expectedPermissions), + "token policy permissions must exactly match the reviewed least-privilege groups", ); - check(app.constraints?.mergeAllowed === false, "GitHub App must not merge"); check( - app.constraints?.humanMergeRuleset === - ".github/agentic-refresh/rulesets/human-merge.json", - "GitHub App policy must reference the human-only merge ruleset", + 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( - app.constraints?.installationTokenMaximumLifetimeMinutes <= 60, - "GitHub App tokens must expire within 60 minutes", + 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( - app.tokenHandling?.exposedToAgent === false, - "GitHub App tokens must not be exposed to agents", + sameJson( + [...(policy.writeHandling?.allowedOperations ?? [])].sort(), + [...allowedOperations].sort(), + ), + "token policy allowed operations must exactly match the reviewed safe-output allowlist", ); check( - app.tokenHandling?.consumer === "pinned-deterministic-safe-output-action", - "GitHub App tokens must be limited to the deterministic safe-output action", + forbiddenOperations.every((operation) => + policy.writeHandling?.forbiddenOperations?.includes(operation), + ), + "token policy must forbid merge, destructive, and cross-repository operations", ); check( - app.tokenHandling?.forbiddenOperations?.includes("mergePullRequest") && - app.tokenHandling.forbiddenOperations.includes("updateDefaultBranch") && - app.tokenHandling.forbiddenOperations.includes("deleteBranch") && - app.tokenHandling.forbiddenOperations.includes("forcePush"), - "GitHub App token policy must forbid merge and destructive operations", + 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", ); } @@ -539,7 +731,9 @@ function validatePinnedActions(workflowText, workflowPath) { 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 app = await readJson(".github/agentic-refresh/github-app.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", @@ -561,9 +755,17 @@ const expectedRepositories = new Set([ ]); validateCapabilities(capabilities, expectedRepositories); -validateQuests(quests, new Set(capabilities.repositories.map(({ name }) => name))); +validateQuests( + quests, + new Map( + capabilities.repositories.map((repository) => [ + repository.name, + repository, + ]), + ), +); validateLabels(labels); -validateGitHubApp(app, expectedRepositories); +validateGitHubTokenPolicy(tokenPolicy); validateRuleset(ruleset); validateHumanMergeRuleset(humanMergeRuleset); @@ -596,6 +798,6 @@ if (errors.length > 0) { process.exitCode = 1; } else { console.log( - `Validated ${quests.quests.length} quests and ${capabilities.repositories.length} report-only repository capability records.`, + `Validated ${quests.quests.length} quests and ${capabilities.repositories.length} repository capability records.`, ); }