-
Notifications
You must be signed in to change notification settings - Fork 2
ci: add reusable workflow to sync new capability IDs into SDK compliance files #65
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+247
−0
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
1321786
ci: add reusable workflow to sync new capability IDs into SDK complia…
spydon 889650a
ci: accept client-id and make app-id optional for the sync workflow
spydon cc8197e
ci: port compliance sync script to TypeScript to match matrix tooling
spydon 57da07a
ci: harden sync workflow — late token, no persisted creds, base-branc…
spydon a6b4cba
refactor: derive sync insertion points from the YAML syntax tree; dro…
spydon 18b6648
refactor(sync): insert IDs via the yaml writer instead of raw text sp…
spydon File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | ||
|
|
||
| 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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), | ||
| ); | ||
|
|
||
|
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); | ||
| } | ||
|
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(); | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.