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
112 changes: 112 additions & 0 deletions .github/workflows/sync-sdk-compliance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
name: Sync SDK Compliance Matrix (reusable)

on:
workflow_call:
inputs:
compliance-file:
description: Path to sdk-compliance.yaml in the calling repo
type: string
default: sdk-compliance.yaml
base-branch:
description: Branch to open the pull request against
type: string
default: main
sdk-ref:
description: Ref to check out from supabase/sdk for the spec and script
type: string
default: main
secrets:
app-id:
description: GitHub App ID used to open the pull request (provide this or client-id)
required: false
client-id:
description: GitHub App client ID used to open the pull request (provide this or app-id)
required: false
private-key:
description: GitHub App private key used to open the pull request
required: true

jobs:
sync:
name: Sync new capability IDs
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout SDK repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.base-branch }}
persist-credentials: false

- name: Checkout capability spec
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: supabase/sdk
ref: ${{ inputs.sdk-ref }}
path: _sdk-spec
persist-credentials: false

- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"

- name: Install capability matrix tooling
run: npm ci
working-directory: _sdk-spec/scripts/capability-matrix

- name: Insert new capability IDs
working-directory: _sdk-spec/scripts/capability-matrix
env:
COMPLIANCE_FILE: ${{ inputs.compliance-file }}
run: |
npm run sync-compliance -- \
"$GITHUB_WORKSPACE/$COMPLIANCE_FILE" \
--new-ids-output "$GITHUB_WORKSPACE/new_ids.txt"

- name: Generate token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.app-id }}
client-id: ${{ secrets.client-id }}
private-key: ${{ secrets.private-key }}

- name: Create pull request
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
COMPLIANCE_FILE: ${{ inputs.compliance-file }}
BASE_BRANCH: ${{ inputs.base-branch }}
run: |
if git diff --quiet -- "$COMPLIANCE_FILE"; then
echo "No new capability IDs; nothing to do."
exit 0
fi

branch="chore/sync-compliance-matrix"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
git fetch origin "$branch:refs/remotes/origin/$branch" || true
git checkout -b "$branch"
git add "$COMPLIANCE_FILE"
git commit -m "chore: sync new capability IDs from canonical spec"
git push --force-with-lease origin "$branch"
Comment thread
spydon marked this conversation as resolved.

body="$(printf 'New capability IDs from https://github.com/supabase/sdk were added as `not_implemented`.\n\nTriage each one: set the real status and add symbols/notes before merging.\n\n')"
while IFS= read -r feature_id; do
[ -n "$feature_id" ] && body="${body}- \`${feature_id}\`"$'\n'
done < new_ids.txt

if [ -n "$(gh pr list --head "$branch" --state open --json number --jq '.[0].number')" ]; then
echo "Pull request already open for $branch; pushed the update."
else
gh pr create \
--base "$BASE_BRANCH" \
--head "$branch" \
--title "chore: sync new capability IDs from canonical spec" \
--body "$body"
fi
1 change: 1 addition & 0 deletions scripts/capability-matrix/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"validate": "tsx src/cli.ts validate",
"validate:online": "tsx src/cli.ts validate --online",
"validate-compliance": "tsx src/compliance-cli.ts",
"sync-compliance": "tsx src/sync-compliance-cli.ts",
"normalize-typedoc": "tsx src/normalize-typedoc-cli.ts",
"normalize-symbolgraph": "tsx src/normalize-symbolgraph-cli.ts",
"normalize-griffe": "tsx src/normalize-griffe-cli.ts",
Expand Down
134 changes: 134 additions & 0 deletions scripts/capability-matrix/src/sync-compliance-cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join, resolve } from "node:path";
import { parseDocument, isMap, isScalar } from "yaml";
import { loadAreas } from "./load.js";
import { collectFeatureIds, findMissingFeatureIds } from "./compliance.js";
import type { RawCompliance } from "./compliance.js";

// Inserts canonical capability IDs that are missing from an SDK's
// sdk-compliance.yaml as `not_implemented`. Canonical IDs and compliance keys
// share the `area.group.feature` shape, so each new ID is placed next to its
// existing siblings (same `area.group.` prefix); an ID whose group has no local
// sibling yet is appended under a comment for manual placement.
//
// Entries are added to the parsed YAML document and re-serialized with the yaml
// writer, so the output is always valid YAML regardless of the source layout.
// Re-serializing may collapse hand-wrapped `note:` scalars onto a single line.
// That is fine; the only concern here is keeping the file valid.

function repoRoot(): string {
const here = dirname(fileURLToPath(import.meta.url));
return resolve(here, "..", "..", "..");
}

function parseArguments(argv: string[]): {
compliancePath: string;
newIdsOutput: string;
} {
let compliancePath: string | undefined;
let newIdsOutput = "new_ids.txt";
for (let index = 0; index < argv.length; index++) {
const argument = argv[index];
if (argument === "--new-ids-output") {
newIdsOutput = argv[++index];
} else if (!argument.startsWith("--") && compliancePath === undefined) {
compliancePath = argument;
}
}
if (!compliancePath) {
console.error(
"Usage: sync-compliance-cli.ts <path-to-sdk-compliance.yaml> [--new-ids-output <path>]",
);
process.exit(1);
}
return { compliancePath, newIdsOutput };
}

function main(): void {
const { compliancePath, newIdsOutput } = parseArguments(
process.argv.slice(2),
);

Comment thread
spydon marked this conversation as resolved.
const { areas, findings } = loadAreas(join(repoRoot(), "capabilities"));
if (findings.some((finding) => finding.level === "error")) {
console.error(
"Failed to load canonical capability spec — check this repo's capabilities/*.yaml",
);
process.exit(1);
}
const knownIds = collectFeatureIds(areas);

const text = readFileSync(resolve(compliancePath), "utf8");
const doc = parseDocument(text);
if (doc.errors.length > 0) {
console.error(`YAML parse error: ${doc.errors[0].message}`);
process.exit(1);
}
const raw = doc.toJS() as RawCompliance;

const missing = findMissingFeatureIds(raw, knownIds);
writeFileSync(
resolve(newIdsOutput),
missing.length ? missing.join("\n") + "\n" : "",
);
if (missing.length === 0) {
console.log("No new capability IDs.");
return;
}

const features = doc.get("features", true);
if (!isMap(features)) {
console.error("Compliance file has no `features` map to sync into.");
process.exit(1);
}

const idIndex = new Map<string, number>();
features.items.forEach((pair, index) => {
if (isScalar(pair.key) && typeof pair.key.value === "string") {
idIndex.set(pair.key.value, index);
}
});

const byPrefix = new Map<string, string[]>();
for (const id of missing) {
const prefix = id.slice(0, id.lastIndexOf(".") + 1);
const bucket = byPrefix.get(prefix);
if (bucket) bucket.push(id);
else byPrefix.set(prefix, [id]);
}

const insertions: { index: number; ids: string[] }[] = [];
const orphans: string[] = [];
for (const [prefix, ids] of byPrefix) {
const siblingIndices = [...idIndex.entries()]
.filter(([existingId]) => existingId.startsWith(prefix))
.map(([, index]) => index);
if (siblingIndices.length === 0) {
orphans.push(...ids);
continue;
}
// Insert right after the last existing sibling so the new IDs land in-group.
insertions.push({ index: Math.max(...siblingIndices) + 1, ids });
}

// Splice back-to-front so earlier indices stay valid as items shift.
for (const { index, ids } of insertions.sort((a, b) => b.index - a.index)) {
const pairs = ids.map((id) => doc.createPair(id, "not_implemented"));
features.items.splice(index, 0, ...pairs);
}
Comment thread
spydon marked this conversation as resolved.

if (orphans.length > 0) {
const pairs = orphans.map((id) => doc.createPair(id, "not_implemented"));
(pairs[0].key as { commentBefore?: string }).commentBefore =
" Newly synced from the canonical spec; no local group yet, place manually.";
features.items.push(...pairs);
}

writeFileSync(resolve(compliancePath), doc.toString({ lineWidth: 0 }));
console.log(
`Added ${missing.length} new capability IDs (${orphans.length} without an existing group).`,
);
}

main();