Skip to content

CODEOWNERS from Governance

The governance section of promptscript.yaml names who owns the setup and which teams review each approval role. On its own it is only data. prs governance codeowners writes it into the CODEOWNERS file of the repository, so the Git host asks the right team for a review before a pull request merges.

The command reads the config and the project, makes no network call and writes one section of the CODEOWNERS file. Everything above that section stays as you wrote it.

Configuration

governance:
admins: ['@acme/prompt-platform']
roles:
legal: ['@acme/legal']
security: ['@acme/appsec']
codeowners:
output: .github/CODEOWNERS # default
protect: [.github/workflows]
prompts: true # default
FieldTypeRequiredDescription
adminsstring[]YesOwners of the config, the approvals root, the releases and the CODEOWNERS files
rolesobjectNoApproval roles, each with the owners that review it. At most 32 roles
codeowners.outputstringNo.github/CODEOWNERS (default), CODEOWNERS, docs/CODEOWNERS or .gitlab/CODEOWNERS
codeowners.protectstring[]NoMore paths the admins own, relative to the repository root (tools or ./tools)
codeowners.promptsbooleanNoWrite a line for each .prs file that defines prompts. Default true

Owners are Git host identities: a user (@jane), a team (@acme/legal, GitLab subgroups allowed) or an email address of at most 254 characters. Prefer teams, they change less often. Role names are lowercase letters, digits and -, starting with a letter, at most 32 characters, and no Windows device name such as con.

Under extends, admins, each role’s owners and codeowners.protect are joined with the base file, so a project can add owners but not remove them. codeowners.prompts and codeowners.output can be set by the extending file. See Extending Configuration.

Generate the file

With the config above and this prompt:

@meta {
id: "acme-support"
syntax: "1.6.0"
owner: "@acme/support"
}
@prompts {
refund-reply: {
description: "Answer a refund request"
reviewers: ["@acme/payments"]
system: "You answer refund requests."
user: "Customer wrote: {{input.message}}"
inputs: {
message: { type: "string", description: "Customer message" }
}
}
}
Try in Playground
$ prs governance codeowners
✓ .github/CODEOWNERS: created

.github/CODEOWNERS then holds:

# promptscript-generated: prs governance codeowners | project: . | do not edit below
/.promptscript/project.prs @acme/payments @acme/support
/.github/workflows @acme/prompt-platform
/.promptscript/approvals @acme/prompt-platform
/.promptscript/approvals/legal @acme/legal
/.promptscript/approvals/security @acme/appsec
/.promptscript/releases @acme/prompt-platform
/.promptscript/vendor @acme/prompt-platform
/.promptscriptrc.yaml @acme/prompt-platform
/.promptscriptrc.yml @acme/prompt-platform
/promptscript.lock @acme/prompt-platform
/promptscript.yaml @acme/prompt-platform
/promptscript.yml @acme/prompt-platform
/.github/CODEOWNERS @acme/prompt-platform
/CODEOWNERS @acme/prompt-platform
/docs/CODEOWNERS @acme/prompt-platform
/.gitlab/CODEOWNERS @acme/prompt-platform

Commit the file. A second run changes nothing.

The lines

Paths start at the repository root, so a project in a subdirectory gets lines like /services/support/.promptscript/approvals. The lines come in this order, sorted inside each group:

PathOwners
each .prs file of the repository that defines a promptthe effective owner and reviewers of its prompts; the admins when they differ, or when the file feeds a prompt of other owners
every other .prs file of the repository the prompts are built fromadmins
codeowners.protectadmins
.promptscript/approvalsadmins
.promptscript/approvals/<role>, for each configured rolethe owners of the role
.promptscript/releases and .promptscript/vendoradmins
every config file name the loader tries, promptscript.lock, and every extends fileadmins
the four CODEOWNERS locationsadmins

On GitHub the last line that matches a path wins. So the lines that pick the authority come last: approvals, roles, releases, config and the CODEOWNERS files. A protect path can take over a prompt file, but not a role directory or the config. The generated section also wins over the lines you wrote above it. The config names, promptscript.lock and the four CODEOWNERS locations are listed whether the files exist or not, so nobody can add a file there that picks other owners.

A .prs line takes owner and reviewers the way the prompt bundle does: from the entry, else from @meta. A prompt without either gets a warning, and the admins review its file.

The files a prompt is built from without defining it, an @inherit parent, a @use import or a file of a composed block, belong to the admins. They change the prompt as much as its own file, and their @meta can change its owners. Files outside the repository, like the registry cache, get no line; vendored registry files are under .promptscript/vendor. With codeowners.prompts: false the command writes no .prs lines and does not resolve the project.

A file can define prompts and also feed other prompts. The command follows where every value of a prompt comes from: its entry, the blocks it composes, and the @meta of the entry file and its @inherit parents, which reach every prompt. When a file feeds a prompt whose owners differ from the owners of the file’s own prompts, the admins review that file, with a warning. Any one owner on a line can approve a change, so adding the admins next to the owners would not protect the other prompt. The same goes for a file with prompts of different owners. Keep shared blocks and @meta defaults in files without prompts, and give prompts of different owners their own files, so each file keeps its owners.

Approval directories belong to the role teams, not to the prompt owners. Otherwise the owners of a prompt could merge a legal sign-off without legal.

Only the project the command runs in gets lines. Other projects in the same repository get none, and one CODEOWNERS file has one managed project.

GitLab

For .gitlab/CODEOWNERS the section starts with a [PromptScript] section header right after the marker. GitLab reads a pattern without a glob or a trailing slash as one exact file, so every directory line is written twice, as /.promptscript/approvals/legal and /.promptscript/approvals/legal/, and so is every protect path. GitLab applies every section, so a line you wrote above the marker that matches a prompt file applies next to the generated one: its owners are asked to review too.

Rules for the file

  • The command owns the file from its marker line to the end and keeps every byte above it, a byte order mark and non-UTF-8 bytes included. The marker must be a full line of its own, with nothing after it.
  • When the file does not exist, the command creates it.
  • When the file has no marker, the marker of another project, a marker with trailing spaces, or two marker lines, the command changes nothing and exits with code 2 (PS5106), or 1 with --check. The message names the marker line to use.
  • A file with a NUL byte is refused with PS5106 too: GitLab reads it as binary and ignores all of it.
  • In .gitlab/CODEOWNERS a [PromptScript] section above the marker is refused with PS5106 too, in any case and with or without ^. GitLab merges sections of the same name, so its lines, an exception like !/.promptscript/approvals/** included, would change the generated ones.
  • On GitHub, when a line you wrote above the marker matches a .prs file that a generated line covers, the command warns: your line no longer applies to that file. A pattern with more than three wildcards (* or ?) gets no such warning: checking it against long file names would take too long.
  • A host reads only one CODEOWNERS file. GitHub looks in .github/, the root, then docs/. GitLab looks in the root, docs/, then .gitlab/. When a file exists at a location read before the output, the command warns.
  • A file that differs only in CRLF line ends, as a checkout with core.autocrlf writes it, counts as current.
  • Every owner must be a Git host identity. A bad value in governance is PS5101. A bad owner or reviewer on a prompt is PS5107 and lists every bad value.
  • Paths may hold only letters, digits, ., _, -, /, @ and +. A space or anything else is PS5107, since hosts read it differently: some take \ as an escaped space, others end the path there and read the rest as owners.
  • CODEOWNERS matches the path Git has, byte for byte, never the target of a link. A prompt file, a source file, a config file, the lockfile, the approvals, releases or vendor directory, or the output that goes through a symbolic link is PS5107, and so is a file whose case differs from the path Git has. A file Git does not track, like an extends file under node_modules/, gets a warning: CODEOWNERS cannot own it. Put its directory and the files that install it, like package.json and the lockfile, in protect. --check fails on such a file unless a protect path covers it. A protect path on the untracked directory, like node_modules/@acme, only silences that check: package.json and the lockfile that install it still need their own protect lines.
  • In every refused case nothing is written.

Check it in CI

--check writes nothing. It exits with code 1 when the file does not exist, differs from what the command would write, when a file at an earlier location would make the host ignore it, or when a file the prompts or the config need is not tracked by Git and no protect path covers it.

$ prs governance codeowners --check
✗ .github/CODEOWNERS is out of date. Run prs governance codeowners and commit the result.
.github/workflows/codeowners.yml
name: CODEOWNERS
on:
pull_request:
branches: [main]
jobs:
codeowners:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
- name: Install the CLI version the lockfile pins
run: npm ci
- name: CODEOWNERS is generated from governance
run: npx --no-install prs governance codeowners --check
- name: GitHub reads every line
run: test "$(gh api "repos/${{ github.repository }}/codeowners/errors?ref=${{ github.event.pull_request.head.sha }}" --jq '.errors | length')" = 0
env:
GH_TOKEN: ${{ github.token }}

The recipe expects @promptscript/cli in the devDependencies of package.json, so the check runs the version your lockfile pins. The last step asks GitHub about the CODEOWNERS file of the pull request head and fails when GitHub cannot read one of its lines. GitHub skips such lines and does not fail on them.

The command resolves the project read-only and fetches nothing. A project with a Git registry needs it in .promptscript/vendor (prs vendor sync, then commit it) or in the cache of the runner, else the check fails.

Host settings

The file binds only when the host enforces it. On GitHub, protect the default branch and turn on:

  • require a pull request before merging, with review from code owners;
  • require approval of the most recent reviewable push;
  • dismiss stale pull request approvals when new commits are pushed;
  • do not allow bypassing the settings, for administrators too;
  • block force pushes;
  • require the --check job as a status check.

The last one matters. A change to an @inherit parent, a @use import or an extends file can change who owns a prompt file or which files are listed. CODEOWNERS sees only the file names in a pull request, so such a change is caught only when the --check job must pass before merge.

A file in .github/workflows can change what CI checks. The protect line gives those files to the admins. A ruleset with a required workflow protects the check itself.

What CODEOWNERS does not give you

  • GitHub matches CODEOWNERS paths byte for byte and case-sensitively. The role lines cover exactly .promptscript/approvals/<role>, in lowercase. A file at .promptscript/approvals/Legal/... or under a look-alike Unicode name is not matched by the legal line, even on a disk that ignores case. The same holds for the releases, the vendor directory and the config names.
  • Any one listed owner can approve a line. A line does not ask for two teams. So adding an owner is the dangerous direction: every owner you add to a prompt, a role or the admins can approve alone.
  • GitLab sections with approval counts and optional sections are not generated.
  • The lines decide who must review an approval record. The release gates then count the committed records, so a required-approvals policy only binds when the role directories have their owners and code owner review is required.

Options

OptionDescriptionDefault
--output <path>CODEOWNERS file, one of the four locationscodeowners.output, else .github/CODEOWNERS
--checkWrite nothing, exit 1 when the file is not up to date-
--format <format>text or jsontext
-c, --config <path>Path to a custom config file-
--cwd <dir>Working directory (project root)-

Exit codes: 0 written or current, 1 error or outdated with --check, 2 the file is not managed by prs (PS5106). With --check every failure exits with 1, PS5106 included.