ci: split SDK compliance workflow into one reusable file per language#49
Conversation
grdsdev
left a comment
There was a problem hiding this comment.
Great direction — per-language files are much cleaner than one if-gated mega-workflow. A few things worth addressing before merge:
Security: shell injection on run: inputs
${{ inputs.* }} inside a run: block is template-substituted by GitHub before bash parses it, so a malicious caller can inject arbitrary shell commands. Affected lines:
validate-sdk-compliance-python.ymllines 90, 93 (griffe-packages,griffe-search-paths)validate-sdk-compliance-javascript.ymlline 85 (entrypoint)
Fix: pass inputs through env: variables and reference $ENV_VAR in the script body:
- name: Dump griffe symbols
env:
GRIFFE_PACKAGES: ${{ inputs.griffe-packages }}
GRIFFE_SEARCH_PATHS: ${{ inputs.griffe-search-paths }}
run: |
PKGS=$(echo "$GRIFFE_PACKAGES" | tr ',' ' ' | tr -s ' ')
...Correctness: Swift multi-target packages break the SGDIR assignment
validate-sdk-compliance-swift.yml lines 90–92:
SGDIR=$(find .build -maxdepth 3 -type d -name "symbolgraph")
NFILES=$(ls "$SGDIR"/*.symbols.json 2>/dev/null | wc -l | tr -d ' ')A package with multiple library targets emits multiple symbolgraph/ directories. find returns a newline-separated list; ls "$SGDIR"/*.symbols.json then receives a multi-line string as one malformed path, always gets NFILES=0, and exits with No symbol graphs emitted — even though graphs exist. Fix: operate on the files directly:
mapfile -t SGFILES < <(find .build -maxdepth 4 -name "*.symbols.json")
if [ ${#SGFILES[@]} -eq 0 ]; then echo "::error::No symbol graphs emitted"; exit 1; fi
jq -s '[.[] | .symbols[]]' "${SGFILES[@]}" > "$GITHUB_WORKSPACE/$b-raw.json"Correctness: empty griffe-packages passes required: true
required: true prevents the input from being omitted but not from being passed as ''. With an empty $PKGS, python -m griffe dump -o api-raw-pr.json runs with no package names, writes an empty JSON, and the symbol check silently passes everything. Add a runtime guard:
if [ -z "$PKGS" ]; then echo "::error::griffe-packages is required and must be non-empty"; exit 1; fiCorrectness: pull_request.base.sha is empty on push events
The check job uses ref: ${{ github.event.pull_request.base.sha }}. If a caller triggers this on a push event, that expression is empty and actions/checkout silently falls back to the default branch — _sdk-base and _sdk-pr end up pointing at the same commit, making the diff always report zero new symbols. Add a guard:
check:
if: github.event_name == 'pull_request'Duplication: extract the shared validate job into a common reusable workflow
The validate job (5 steps: checkout SDK, checkout spec, setup-node, npm ci, validate-compliance) is byte-for-byte identical across all four files. The first 5 steps of each check job are also identical. GitHub Actions has supported nested reusable workflows since 2022 — the validate job could be pulled into a fifth shared file:
# .github/workflows/validate-sdk-compliance-validate.yml
on:
workflow_call:
inputs:
compliance-file: { type: string, default: sdk-compliance.yaml }
sdk-ref: { type: string, default: main }
jobs:
validate:
name: Validate compliance file
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10
with:
repository: supabase/sdk
ref: ${{ inputs.sdk-ref }}
path: _sdk-spec
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
node-version: "22"
- run: npm ci
working-directory: _sdk-spec/scripts/capability-matrix
- run: npm run validate-compliance -- "${{ github.workspace }}/${{ inputs.compliance-file }}"
working-directory: _sdk-spec/scripts/capability-matrixEach per-language file then becomes:
validate:
uses: supabase/sdk/.github/workflows/validate-sdk-compliance-validate.yml@main
with:
compliance-file: ${{ inputs.compliance-file }}
sdk-ref: ${{ inputs.sdk-ref }}This reduces the actions/checkout pin from 20 occurrences across 4 files to 2 canonical occurrences — a security patch becomes a single-file change instead of a 4-file sweep.
Minor: griffe-packages description contradicts its normalization
The input says "Space-separated" but the script runs tr ',' ' ' (accepting commas too), while griffe-search-paths correctly says "Comma-separated". Consider unifying: either document both separators as accepted, or pick one and drop the tr for the other.
ec2c755 to
b713266
Compare
…uage Replace the single language-gated reusable workflow with one file per language (swift, javascript, python, dart). Each file hardcodes its runner and steps, dropping the per-step `if: inputs.language == ...` gates and the `language` input. Update README and CLAUDE.md.
GitHub Actions does not support YAML anchors, so factor the duplicated validate job, check-job setup, and final symbol-diff step into three composite actions under .github/actions/sdk-compliance-*. Each per-language workflow now references them, leaving only its language-specific steps inline.
Match the for-b-in-pr-base pattern the swift, python, and dart workflows already use, replacing four explicit PR/base steps with two looped ones.
- Pass workflow inputs through env vars instead of interpolating them directly into run blocks, closing a shell-injection vector (compliance-file, entrypoint, griffe-packages, griffe-search-paths) - Swift: collect symbol-graph files directly so multi-target packages no longer fail the SGDIR glob with a false 'No symbol graphs emitted' - Python: guard against an empty griffe-packages value passing required - Skip the symbol-diff check job on non-pull_request events, where pull_request.base.sha is empty and the diff would be meaningless - Clarify griffe-packages accepts space- or comma-separated names
Carry over the JS handling that #39 introduced on the base branch: the javascript workflow now takes a required typedoc-packages input, enables corepack, installs each branch with pnpm, runs every package's docs:json, and merges the per-package TypeDoc JSON via normalize-typedoc --out — replacing the old single npx-typedoc entrypoint flow.
b713266 to
836ba81
Compare
## Summary Updates the SDK compliance check to use the new **per-language** reusable workflow from `supabase/sdk`. The single `validate-sdk-compliance.yml` (which took a `language:` input and gated every step on it) has been split into one workflow per language ([supabase/sdk#49](supabase/sdk#49)). This drops the `language:` input and points at the language-specific file. > [!IMPORTANT] > Merge only **after** supabase/sdk#49 lands on `main`, since the new workflow file must exist at `@main` for this reference to resolve.
Summary
Splits the single language-gated
validate-sdk-compliance.ymlreusable workflow into one file per language, so we no longer gate every step on thelanguageinput. Shared steps are factored into composite actions to avoid duplication across the per-language files.Before: one
validate-sdk-compliance.ymlwith a requiredlanguageinput andif: inputs.language == '...'on nearly every step in thecheckjob, plus a runtimecasestatement validating the input.After: four self-contained reusable workflows, each with its runner and steps hardcoded and no
languageinput:validate-sdk-compliance-swift.ymlmacos-latest)validate-sdk-compliance-javascript.ymltypedoc-packages(required)validate-sdk-compliance-python.ymlgriffe-packages(required),griffe-search-pathsvalidate-sdk-compliance-dart.ymlThe JavaScript workflow targets the supabase-js pnpm monorepo:
typedoc-packagesis a comma-separated list of package dirs (relative to the SDK root), each defining adocs:jsonscript that owns its TypeDoc entrypoints. The workflow installs with pnpm, runs each package'sdocs:json, then normalizes and merges the per-package TypeDoc JSON into oneParseResultper branch.Avoiding duplication: composite actions
GitHub Actions does not support YAML anchors/aliases or merge keys in workflow files, so the shared pieces are extracted into composite actions under
.github/actions/:sdk-compliance-validatevalidatejob (checkout SDK + spec, setup-node,npm ci, validate compliance file)sdk-compliance-check-setupcheckjob preamble (checkout PR/base/spec, setup-node,npm ci)sdk-compliance-check-symbolscheck-api-symbolsdiff stepEach per-language workflow references these via the full path
supabase/sdk/.github/actions/<name>@mainand keeps only its language-specific steps (symbol extraction + normalization) inline. The full path is required: a./-relative action path inside a reusable workflow resolves against the caller's checkout, not this repo.Important
Because action
uses:cannot take an expression, the composite actions are pinned to@main. The split can therefore only be validated end-to-end once this PR is onmain. Thesdk-refinput still controls which ref the capability-matrix scripts are checked out from.Hardening
The
run:blocks pass workflow inputs throughenv:variables instead of interpolating${{ inputs.* }}directly, closing a shell-injection vector. Other safety fixes:SGDIRglob with a falseNo symbol graphs emitted.griffe-packagesvalue passing therequired: truecheck.typedoc-packagesvalue.checkjob is skipped on non-pull_requestevents, wherepull_request.base.shais empty and the diff would be meaningless.Breaking change for callers
SDK repos must update their
uses:reference fromvalidate-sdk-compliance.yml(withlanguage:) to the per-language file. Thelanguageinput is gone. The Python workflow now takes a requiredgriffe-packagesinput (was a runtime check before), and the JavaScript workflow takes a requiredtypedoc-packagesinput.Caller PRs opened:
Changes
validate-sdk-compliance-{swift,javascript,python,dart}.yml;.github/actions/sdk-compliance-{validate,check-setup,check-symbols}/action.ymlvalidate-sdk-compliance.ymlREADME.md,CLAUDE.md— document the per-language workflows and composite actions, add Swift and JavaScript opt-in examples, reformat README tablesTest plan