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
78 changes: 73 additions & 5 deletions .github/workflows/validate-sdk-compliance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,21 @@ on:
type: string
default: sdk-compliance.yaml
language:
description: SDK language for public API check — "swift", "javascript", or "dart"
description: SDK language for public API check — "swift", "javascript", "python", or "dart"
type: string
required: true
sdk-ref:
description: Ref to checkout from supabase/sdk for scripts (defaults to main; override only during pre-merge testing)
type: string
default: main
griffe-packages:
description: Space-separated griffe package names to dump (python only)
type: string
default: ""
griffe-search-paths:
description: Comma-separated search paths for griffe relative to repo root (python only)
type: string
default: ""

jobs:
validate:
Expand Down Expand Up @@ -84,6 +92,66 @@ jobs:
run: npm ci
working-directory: _sdk-spec/scripts/capability-matrix

- name: Set up Python
if: inputs.language == 'python'
uses: actions/setup-python@39cd14951b08e74b54015ebf0246ef4cc7b4ce8 # v5.5.0
with:
python-version: "3.x"

- name: Install griffe
if: inputs.language == 'python'
run: pip install griffe

- name: Run griffe on PR branch
if: inputs.language == 'python'
run: |
if [ -z "${{ inputs.griffe-packages }}" ]; then
echo "::error::griffe-packages input is required when language is python"
exit 1
fi
SEARCH_FLAGS=""
if [ -n "${{ inputs.griffe-search-paths }}" ]; then
SEARCH_FLAGS=$(echo "${{ inputs.griffe-search-paths }}" | tr ',' '\n' \
| while read -r p; do echo "--search $GITHUB_WORKSPACE/_sdk-pr/$p"; done \
| tr '\n' ' ')
fi
PKGS=$(echo "${{ inputs.griffe-packages }}" | tr ',' ' ' | tr -s ' ')
python -m griffe dump $PKGS $SEARCH_FLAGS \
-o "$GITHUB_WORKSPACE/api-raw-pr.json"

- name: Normalize griffe output (PR branch)
if: inputs.language == 'python'
run: |
npm run --silent normalize-griffe -- "$GITHUB_WORKSPACE/api-raw-pr.json" \
--project-root "$GITHUB_WORKSPACE/_sdk-pr" \
> "$GITHUB_WORKSPACE/pr-symbols.json"
working-directory: _sdk-spec/scripts/capability-matrix

- name: Run griffe on base branch
if: inputs.language == 'python'
run: |
if [ -z "${{ inputs.griffe-packages }}" ]; then
echo "::error::griffe-packages input is required when language is python"
exit 1
fi
SEARCH_FLAGS=""
if [ -n "${{ inputs.griffe-search-paths }}" ]; then
SEARCH_FLAGS=$(echo "${{ inputs.griffe-search-paths }}" | tr ',' '\n' \
| while read -r p; do echo "--search $GITHUB_WORKSPACE/_sdk-base/$p"; done \
| tr '\n' ' ')
fi
PKGS=$(echo "${{ inputs.griffe-packages }}" | tr ',' ' ' | tr -s ' ')
python -m griffe dump $PKGS $SEARCH_FLAGS \
-o "$GITHUB_WORKSPACE/api-raw-base.json"

- name: Normalize griffe output (base branch)
if: inputs.language == 'python'
run: |
npm run --silent normalize-griffe -- "$GITHUB_WORKSPACE/api-raw-base.json" \
--project-root "$GITHUB_WORKSPACE/_sdk-base" \
> "$GITHUB_WORKSPACE/base-symbols.json"
working-directory: _sdk-spec/scripts/capability-matrix

# The Dart parser is a small package:analyzer tool run directly with
# `dart run`. It parses syntactically, so the target packages need no
# `pub get` — only the Dart SDK and the tool's own dependencies.
Expand Down Expand Up @@ -140,23 +208,23 @@ jobs:
working-directory: _sdk-spec/scripts/capability-matrix

- name: Resolve parse command
if: inputs.language != 'swift' && inputs.language != 'dart'
if: inputs.language != 'python' && inputs.language != 'swift' && inputs.language != 'dart'
id: resolve
run: |
case "${{ inputs.language }}" in
javascript) echo "cmd=parse-ts" >> "$GITHUB_OUTPUT" ;;
*) echo "::error::Unsupported language '${{ inputs.language }}'. Supported values: swift, javascript, dart"; exit 1 ;;
*) echo "::error::Unsupported language '${{ inputs.language }}'. Supported values: swift, dart, python, javascript"; exit 1 ;;
esac

- name: Parse PR branch
if: inputs.language != 'swift' && inputs.language != 'dart'
if: inputs.language != 'python' && inputs.language != 'swift' && inputs.language != 'dart'
run: |
npm run --silent ${{ steps.resolve.outputs.cmd }} -- "$GITHUB_WORKSPACE/_sdk-pr" \
> "$GITHUB_WORKSPACE/pr-symbols.json"
working-directory: _sdk-spec/scripts/capability-matrix

- name: Parse base branch
if: inputs.language != 'swift' && inputs.language != 'dart'
if: inputs.language != 'python' && inputs.language != 'swift' && inputs.language != 'dart'
run: |
npm run --silent ${{ steps.resolve.outputs.cmd }} -- "$GITHUB_WORKSPACE/_sdk-base" \
> "$GITHUB_WORKSPACE/base-symbols.json"
Expand Down
1 change: 1 addition & 0 deletions scripts/capability-matrix/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"validate-compliance": "tsx src/compliance-cli.ts",
"parse-ts": "tsx src/parse-ts.ts",
"normalize-symbolgraph": "tsx src/normalize-symbolgraph-cli.ts",
"normalize-griffe": "tsx src/normalize-griffe-cli.ts",
"check-api-symbols": "tsx src/check-api-symbols.ts",
"aggregate": "tsx src/aggregate.ts",
"report": "tsx src/cli.ts report",
Expand Down
27 changes: 27 additions & 0 deletions scripts/capability-matrix/src/normalize-griffe-cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { readFileSync } from "node:fs";
import { normalizeGriffe, type GriffeOutput } from "./normalize-griffe.js";

async function main(): Promise<void> {
const [, , filePath, ...rest] = process.argv;
if (!filePath) {
console.error("Usage: normalize-griffe <api-raw.json> [--project-root <path>]");
process.exit(1);
}

const rootIdx = rest.indexOf("--project-root");
const projectRoot = rootIdx >= 0 ? rest[rootIdx + 1] : undefined;

try {
const raw: GriffeOutput = JSON.parse(readFileSync(filePath, "utf8"));
const result = normalizeGriffe(raw, projectRoot);
console.log(JSON.stringify(result, null, 2));
} catch (e) {
console.error(`Error: ${(e as Error).message}`);
process.exit(1);
}
}

main().catch((e) => {
console.error(e);
process.exit(1);
});
75 changes: 75 additions & 0 deletions scripts/capability-matrix/src/normalize-griffe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { basename, relative } from "node:path";
import type { ParsedSymbol, ParseResult } from "./ts-parser.js";
import { loadIgnore, type Ignore } from "./parse-ignore.js";

export type { ParsedSymbol, ParseResult };

export interface GriffeNode {
kind: "module" | "class" | "function" | "attribute";
filepath?: string;
labels?: string[] | null;
members?: Record<string, GriffeNode>;
}

export type GriffeOutput = Record<string, GriffeNode>;

export function normalizeGriffe(raw: GriffeOutput, projectRoot = ""): ParseResult {
const ig = projectRoot ? loadIgnore(projectRoot) : null;
const symbols: ParsedSymbol[] = [];

for (const [, moduleNode] of Object.entries(raw)) {
walkNode("", moduleNode, "", [], symbols, ig, projectRoot);
}

return { symbols };
}

function walkNode(
name: string,
node: GriffeNode,
inheritedFile: string,
classStack: string[],
symbols: ParsedSymbol[],
ig: Ignore | null,
projectRoot: string,
): void {
const file = node.filepath
? (projectRoot ? relative(projectRoot, node.filepath) : basename(node.filepath))
: inheritedFile;

if (node.kind === "module") {
for (const [childName, child] of Object.entries(node.members ?? {})) {
walkNode(childName, child, file, classStack, symbols, ig, projectRoot);
}
return;
}

if (name.startsWith("_")) return;

if (node.kind === "class") {
emit(symbols, ig, { name: qual(classStack, name), kind: "class", file });
for (const [childName, child] of Object.entries(node.members ?? {})) {
walkNode(childName, child, file, [...classStack, name], symbols, ig, projectRoot);
}
return;
}

if (node.kind === "function") {
const kind = classStack.length > 0 ? ("method" as const) : ("function" as const);
emit(symbols, ig, { name: qual(classStack, name), kind, file });
return;
}

if (node.kind === "attribute" && node.labels?.includes("property")) {
emit(symbols, ig, { name: qual(classStack, name), kind: "property", file });
}
}

function qual(classStack: string[], name: string): string {
return classStack.length > 0 ? `${classStack.join(".")}.${name}` : name;
}

function emit(symbols: ParsedSymbol[], ig: Ignore | null, sym: ParsedSymbol): void {
if (ig && sym.file && ig.ignores(sym.file)) return;
symbols.push(sym);
}
Loading