Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
40 changes: 40 additions & 0 deletions .github/workflows/sync-codeowners.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Sync CODEOWNERS

on:
push:
branches: [main]
paths:
- "gaps/*/metadata.yml"

jobs:
sync:
name: Sync CODEOWNERS from metadata
runs-on: ubuntu-latest

permissions:
contents: write

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Sync CODEOWNERS
run: node scripts/sync-codeowners.js

- name: Commit if changed
run: |
git diff --quiet CODEOWNERS && exit 0
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add CODEOWNERS
git commit -m "Sync CODEOWNERS from GAP metadata"
git push
1 change: 0 additions & 1 deletion CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
/gaps/GAP-13/ @benjie
21 changes: 12 additions & 9 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ placeholder.
Before filing a GAP you're encouraged to create an issue outlining the topic to
gauge public interest, but doing so is not necessary.

1. Clone the repository and create a folder in the root called `GAP-0`.
1. Clone the repository and create a folder at `gaps/GAP-0`.
2. Add the required files to this folder as described below (`README.md`,
`DRAFT.md` and `metadata.yml`), commit them, and open a pull request (PR).
3. Update the GAP number to match the PR number (`graphql/gaps#10` has PR number
10). Do not zero-pad the PR number.
- Rename the folder from `GAP-0` to `GAP-N` where N is the PR number number.
- Rename the folder from `gaps/GAP-0` to `gaps/GAP-N` where N is the PR
number.
- Update `id` in `metadata.yml` to be the PR number.
- If not yet configured, update the `discussion` path in `metadata.yml` to
point to the PR.
Expand All @@ -48,7 +49,7 @@ the PR will be merged.

### Required files

Each `GAP-N` folder must include:
Each `gaps/GAP-N` folder must include:

- `DRAFT.md` — the working document of the proposal/specification, written in
[`spec-md`](https://spec-md.com/) format
Expand All @@ -68,11 +69,13 @@ title: <title>
# doubt, choose "proposal"
status: proposal | draft | accepted
authors:
- "Your Name <noreply@example.com>"
sponsor: "@githubUername"
# An separate GitHub issue, discussion, or other public forum where discussion
- name: "Your Name"
email: "noreply@example.com"
githubUsername: "@yourGithubUsername"
sponsor: "@githubUsername"
# A separate GitHub issue, discussion, or other public forum where discussion
# of this GAP occurs. Otherwise, this can be set to the URL of the PR in which
# the GAP was submited.
# the GAP was submitted.
discussion: "https://github.com/graphql/graphql-wg/issues/..."
```

Expand Down Expand Up @@ -103,7 +106,7 @@ To release a version of a GAP, copy the current `DRAFT.md` into a `versions`
folder named for the year and month of release:

```bash
cp GAP-N/DRAFT.md GAP-N/versions/YYYY-MM.md
cp gaps/GAP-N/DRAFT.md gaps/GAP-N/versions/YYYY-MM.md
```

Rules:
Expand All @@ -113,7 +116,7 @@ Rules:
then only for trivial typos or exceptional circumstances (e.g. security
issues).

### `GAP-N/versions/YYYY-MM.yml`
### `gaps/GAP-N/versions/YYYY-MM.yml`

This optional file can be created/edited by the TSC or editors to outline the
status of a published release, including a top-of-document notice or errata.
Expand Down
Empty file added gaps/.gitkeep
Empty file.
16 changes: 14 additions & 2 deletions metadata.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,20 @@
"properties": {
"title": { "type": "string" },
"status": { "type": "string", "enum": ["proposal", "draft", "accepted"] },
"authors": { "type": "array", "items": { "type": "string" } },
"sponsor": { "type": "string" },
"authors": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string" },
"githubUsername": { "type": "string", "pattern": "^@" }
},
"required": ["name", "email", "githubUsername"],
"additionalProperties": false
}
},
"sponsor": { "type": "string", "pattern": "^@" },
"discussion": { "type": "string" },
"id": { "type": "integer" },
"related": { "type": "array", "items": { "type": "integer" } },
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"suggest:format": "echo \"\nTo resolve this, run: $(tput bold)npm run format$(tput sgr0)\" && exit 1",
"test:format": "prettier --check . || npm run suggest:format",
"test:spelling": "cspell \"spec/**/*.md\" README.md LICENSE.md",
"test:structure": "find . -maxdepth 1 -type d -name 'GAP-*' -exec ./scripts/validate-structure.js {} \\;"
"test:structure": "find ./gaps -maxdepth 1 -type d -name 'GAP-*' -exec ./scripts/validate-structure.js {} \\;",
"sync:codeowners": "node scripts/sync-codeowners.js"
},
"devDependencies": {
"ajv": "^8.17.1",
Expand Down
34 changes: 34 additions & 0 deletions scripts/sync-codeowners.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env node

import { readdir, readFile, writeFile } from "node:fs/promises";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { parse as parseYaml } from "yaml";

const __dirname = dirname(fileURLToPath(import.meta.url));
const rootDir = join(__dirname, "..");
const gapsDir = join(rootDir, "gaps");

async function getGapDirs() {
const entries = await readdir(gapsDir, { withFileTypes: true });
return entries.filter((d) => d.isDirectory() && /^GAP-[1-9]\d*$/.test(d.name));
}

async function main() {
const dirs = await getGapDirs();
Comment thread
magicmark marked this conversation as resolved.
dirs.sort();

const lines = await Promise.all(
dirs.map(async (dir) => {
const metadataPath = join(gapsDir, dir.name, "metadata.yml");
const metadata = parseYaml(await readFile(metadataPath, "utf8"));
const owners = metadata.authors.map((a) => a.githubUsername.replace(/^@/, ""));
const ownerList = owners.map((o) => `@${o}`).join(" ");
return `/gaps/${dir.name}/ ${ownerList}`;
}),
);

await writeFile(join(rootDir, "CODEOWNERS"), lines.join("\n") + "\n");
}

main();
14 changes: 10 additions & 4 deletions scripts/validate-structure.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Usage: ./scripts/validate-structure.js <gap-directory>
*
* Can be xarg'd over all GAP directories:
* find . -maxdepth 1 -type d -name 'GAP-*' | xargs -I{} node scripts/validate-structure.js {}
* find ./gaps -maxdepth 1 -type d -name 'GAP-*' | xargs -I{} node scripts/validate-structure.js {}
*/

import { existsSync, readFileSync, statSync } from "node:fs";
Expand Down Expand Up @@ -95,12 +95,18 @@ function validateMetadata(dirPath, gapName) {
error(gapName, `metadata.yml validation failed:\n\n${errors}`);
}

// Validate authors have valid email format
// Validate authors have valid email and githubUsername
for (const author of metadata.authors) {
if (!validator.isEmail(author, { allow_display_name: true })) {
if (!validator.isEmail(author.email)) {
error(
gapName,
`metadata.yml invalid author "${author}" - must be "Name <email>" or a valid email`,
`metadata.yml invalid author email "${author.email}" for "${author.name}"`,
);
}
if (!author.githubUsername.startsWith("@")) {
error(
gapName,
`metadata.yml author githubUsername must start with @ (got "${author.githubUsername}" for "${author.name}")`,
);
}
}
Expand Down
Loading