Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b09e48e
docs: position Custo as central distributor engine
Copilot Jul 25, 2026
c9c8f31
feat: add org-agnostic sync engine
Copilot Jul 25, 2026
ddb0ef1
feat: add scheduled sync workflow
Copilot Jul 25, 2026
426e424
feat: add MVP AGENTS.md file set for Module repositories
Copilot Jul 25, 2026
0938def
fix: use -ApiEndpoint for Invoke-GitHubAPI
Copilot Jul 25, 2026
8f23d8e
feat: discover subscribing repos across all accessible orgs
Copilot Jul 25, 2026
eee57a6
feat: sync enterprise custom-property schema from file sets
Copilot Jul 25, 2026
3ecaa2d
refactor: make sync pipeline policy-first by enterprise then org
Copilot Jul 25, 2026
b7e3f06
feat: support enterprise policy APIs via CUSTO_ENTERPRISE_PAT
Copilot Jul 25, 2026
471dc89
feat: add layered policy documents and policy engine execution
Copilot Jul 25, 2026
0f5f504
feat: add enterprise-mapped policy engine folder hierarchy
Copilot Jul 25, 2026
79b2cf4
feat: expand enterprise capability configs for properties and rulesets
Copilot Jul 25, 2026
8bba4c7
refactor: align layered policy paths
MariusStorhaug Jul 25, 2026
9d74edb
refactor: enforce explicit enterprise auth modes
MariusStorhaug Jul 25, 2026
b9f9c0c
fix: correct CapabilityCatalog param wiring in policy engine
MariusStorhaug Jul 25, 2026
511c553
fix: make policy diffs order-insensitive and drift-accurate
MariusStorhaug Jul 25, 2026
670a043
feat: isolate per-org failures and add run report
MariusStorhaug Jul 25, 2026
3655b9d
test: add policy schemas, Pester tests, and CI gate
MariusStorhaug Jul 25, 2026
7a184a6
docs: reconcile README with implementation and PAT risk
MariusStorhaug Jul 25, 2026
4977259
test: make path and temp fixtures cross-platform
MariusStorhaug Jul 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: CI

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
test:
name: Lint and test
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1

- name: Run Pester tests
shell: pwsh
run: |
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
Install-Module Pester -MinimumVersion 5.5.0 -Force -SkipPublisherCheck
$config = New-PesterConfiguration
$config.Run.Path = './tests'
$config.Run.Exit = $true
$config.Output.Verbosity = 'Detailed'
Invoke-Pester -Configuration $config

- name: Run PSScriptAnalyzer
shell: pwsh
run: |
Install-Module PSScriptAnalyzer -Force -SkipPublisherCheck
$results = Invoke-ScriptAnalyzer -Path ./scripts -Recurse -Severity Error
if ($results) {
$results | Format-Table -AutoSize
throw "PSScriptAnalyzer reported $($results.Count) error-level finding(s)."
}
Write-Host 'PSScriptAnalyzer: no error-level findings.'
26 changes: 26 additions & 0 deletions .github/workflows/sync-files.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Sync Managed Files

on:
schedule:
- cron: '0 6 * * *' # Daily at 06:00 UTC
workflow_dispatch:

permissions:
contents: read

jobs:
sync-files:
name: Sync files to subscribing repositories
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1

- name: Sync managed files
uses: PSModule/GitHub-Script@e3b0111c93df3686061cb2c65054f9216ed265e5 # main
env:
CUSTO_ENTERPRISE_PAT: ${{ secrets.CUSTO_ENTERPRISE_PAT }}
with:
Script: ./scripts/Sync-Files.ps1
ClientID: ${{ secrets.CUSTO_BOT_CLIENT_ID }}
PrivateKey: ${{ secrets.CUSTO_BOT_PRIVATE_KEY }}
59 changes: 59 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Agents

## Main directive

Everything here is a work in progress and can be improved. If you find a problem, fix it if it's
small; otherwise register it as an issue in this repo.

Read [MSXOrg/docs AGENTS.md](https://github.com/MSXOrg/docs/blob/main/AGENTS.md) first for
ecosystem-wide onboarding. This file only covers what's specific to Custo.

## What this repo is

Custo is the distribution runtime described in [README.md](README.md). Read that first for the
architecture and the source-of-truth separation between standards, initiative docs, and this
runtime.

## Operator runbook: enabling live rollout

The MVP file set (`Repos/Module/AGENTS.md/`) and workflow are in place, but the scheduled sync
cannot run against real repositories until an operator completes these steps:

1. **Configure GitHub App credentials.** Add `CUSTO_BOT_CLIENT_ID` and `CUSTO_BOT_PRIVATE_KEY`
as repository secrets on `MSXOrg/Custo`, using the same `PSModule's Custo` GitHub App
(app id `1320343`, slug `psmodule-s-custo`) already installed on the `PSModule` organization.
If that app's credentials are not available, generate a new private key for it or provision an
equivalent app with `contents:write`, `pull_requests:write`, and
`repository_custom_properties:read` on target repositories, plus enterprise custom-properties
write/admin permission if enterprise schema sync is enabled.
For capabilities configured with `authMode: enterprise-pat`, also add
`CUSTO_ENTERPRISE_PAT` and grant it enterprise-admin scope.
2. **Confirm app installation scope.** Verify the app is installed on the `PSModule` organization
with access to the module repositories that should receive `AGENTS.md`
(`gh api orgs/PSModule/installations`).
3. **Set custom properties on target repositories.** Each subscribing module repository needs
`Type = Module` and `SubscribeTo` including `AGENTS.md` set at the repository level.
Custo now maintains allowed values at enterprise level from `Repos/` file sets via policy
documents in `PolicyEngine/Policies/{enterprise}/`.
4. **Dry-run via `workflow_dispatch`.** Trigger the `Sync Managed Files` workflow manually first
and review the summary before relying on the daily schedule.

## Policy documents

Policy configuration is declared in `PolicyEngine/` and executed in fixed layer order:
1. enterprise
2. organization
3. repository

Current default enterprise mapping (`MSXOrg`):
- `PolicyEngine/Capabilities/enterprise/repo-custom-property.capability.json`
- `PolicyEngine/Capabilities/enterprise/repo-rulesets.capability.json`
- `PolicyEngine/Capabilities/organization/none.capability.json`
- `PolicyEngine/Capabilities/repository/file-subscription-service.capability.json`
- `PolicyEngine/Policies/MSXOrg/enterprise/repo-custom-property.policy.json`
- `PolicyEngine/Policies/MSXOrg/enterprise/repo-rulesets.policy.json`
- `PolicyEngine/Policies/MSXOrg/organization/_default/none.policy.json`
- `PolicyEngine/Policies/MSXOrg/repository/_default/file-subscription-service.policy.json`

Until step 1 is complete, the workflow will fail at the authentication step
(`Connect-GitHub App`) — this is the current, expected blocker for this MVP.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"version": "1.0",
"name": "Enterprise repository custom-property schema",
"layer": "enterprise",
"capability": "repo-custom-property",
"authMode": "github-app",
"description": "Maintains enterprise-level repository custom-property definitions and allowed values.",
"configSchema": {
"required": ["enterprise"],
"properties": {
"enterprise": {
"type": "string",
"description": "Enterprise slug to maintain property definitions on."
},
"typePropertyName": {
"type": "string",
"default": "Type",
"description": "Repository type property name."
},
"subscriptionPropertyName": {
"type": "string",
"default": "SubscribeTo",
"description": "Managed file-subscription property name."
},
"required": {
"type": "boolean",
"default": false,
"description": "Whether repositories must explicitly set these property values."
},
"valuesEditableBy": {
"type": "string",
"enum": ["org_actors", "org_and_repo_actors", null],
"default": "org_actors",
"description": "Who can edit property values."
},
"requireExplicitValues": {
"type": "boolean",
"default": false,
"description": "Whether setting property values is mandatory."
}
}
}
}
21 changes: 21 additions & 0 deletions PolicyEngine/Capabilities/enterprise/repo-rulesets.capability.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"version": "1.0",
"name": "Enterprise repository rulesets",
"layer": "enterprise",
"capability": "repo-rulesets",
"authMode": "enterprise-pat",
"description": "Maintains enterprise-level repository rulesets.",
"configSchema": {
"required": ["enterprise", "rulesets"],
"properties": {
"enterprise": {
"type": "string",
"description": "Enterprise slug to maintain rulesets on."
},
"rulesets": {
"type": "array",
"description": "Ruleset definitions mapped directly to GitHub enterprise ruleset payloads."
}
}
}
}
8 changes: 8 additions & 0 deletions PolicyEngine/Capabilities/organization/none.capability.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"version": "1.0",
"name": "Organization policy placeholder",
"layer": "organization",
"capability": "none",
"authMode": "github-app",
"description": "Placeholder capability for future organization-level policies."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"version": "1.0",
"name": "Repository file subscription service",
"layer": "repository",
"capability": "file-subscription-service",
"authMode": "github-app",
"description": "Synchronizes managed files into subscribing repositories via pull requests."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"version": "1.0",
"name": "Enterprise repo custom property policy",
"layer": "enterprise",
"capability": "repo-custom-property",
"order": 10,
"enabled": true,
"config": {
"enterprise": "MSXOrg",
"typePropertyName": "Type",
"subscriptionPropertyName": "SubscribeTo",
"required": false,
"valuesEditableBy": "org_actors",
"requireExplicitValues": false
}
}
66 changes: 66 additions & 0 deletions PolicyEngine/Policies/MSXOrg/enterprise/repo-rulesets.policy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"version": "1.0",
"name": "Enterprise repo ruleset policy",
"layer": "enterprise",
"capability": "repo-rulesets",
"order": 20,
"enabled": true,
"config": {
"enterprise": "MSXOrg",
"rulesets": [
{
"name": "Custo default branch policy",
"target": "branch",
"enforcement": "disabled",
"conditions": {
"organization_name_and_repository_property": {
"organization_name": {
"include": [
"~ALL"
],
"exclude": []
},
"repository_property": {
"include": [
{
"name": "Type",
"property_values": [
"Module"
],
"source": "custom"
}
],
"exclude": []
},
"ref_name": {
"include": [
"~DEFAULT_BRANCH"
],
"exclude": []
}
}
},
"rules": [
{
"type": "required_linear_history"
},
{
"type": "pull_request",
"parameters": {
"dismiss_stale_reviews_on_push": true,
"require_code_owner_review": false,
"require_last_push_approval": false,
"required_approving_review_count": 1,
"required_review_thread_resolution": true,
"allowed_merge_methods": [
"merge",
"squash",
"rebase"
]
}
}
]
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"version": "1.0",
"name": "Organization policy placeholder",
"layer": "organization",
"capability": "none",
"order": 10,
"enabled": true,
"config": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"version": "1.0",
"name": "Repository file subscription service",
"layer": "repository",
"capability": "file-subscription-service",
"order": 10,
"enabled": true,
"config": {
"branchName": "managed-files/update",
"commitMessage": "chore: sync managed files",
"prTitle": "⚙️ [Maintenance]: Sync managed files",
"prLabel": "NoRelease"
}
}
43 changes: 43 additions & 0 deletions PolicyEngine/schemas/capability.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://msxorg.github.io/custo/schemas/capability.schema.json",
"title": "Custo capability document",
"description": "Defines what a policy capability does and how it authenticates. One document per Layer.Capability.",
"type": "object",
"required": ["version", "name", "layer", "capability", "authMode"],
"properties": {
"version": {
"type": "string",
"description": "Document schema version."
},
"name": {
"type": "string",
"minLength": 1,
"description": "Human-readable capability name."
},
"layer": {
"type": "string",
"enum": ["enterprise", "organization", "repository"],
"description": "Policy layer this capability runs at."
},
"capability": {
"type": "string",
"minLength": 1,
"pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
"description": "Capability identifier (kebab-case) unique within its layer."
},
"authMode": {
"type": "string",
"enum": ["github-app", "enterprise-pat"],
"description": "Explicit authentication mode. github-app uses the installation token; enterprise-pat uses CUSTO_ENTERPRISE_PAT for enterprise-admin endpoints the app cannot reach."
},
"description": {
"type": "string"
},
"configSchema": {
"type": "object",
"description": "Optional inline description of the config object accepted by matching policy documents."
}
},
"additionalProperties": true
}
Loading
Loading