Skip to content

ci: split SDK compliance workflow into one reusable file per language#49

Merged
spydon merged 6 commits into
mainfrom
claude/split-compliance-workflows
Jun 30, 2026
Merged

ci: split SDK compliance workflow into one reusable file per language#49
spydon merged 6 commits into
mainfrom
claude/split-compliance-workflows

Conversation

@spydon

@spydon spydon commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Splits the single language-gated validate-sdk-compliance.yml reusable workflow into one file per language, so we no longer gate every step on the language input. Shared steps are factored into composite actions to avoid duplication across the per-language files.

Before: one validate-sdk-compliance.yml with a required language input and if: inputs.language == '...' on nearly every step in the check job, plus a runtime case statement validating the input.

After: four self-contained reusable workflows, each with its runner and steps hardcoded and no language input:

Language Workflow Extra inputs
Swift validate-sdk-compliance-swift.yml — (runs on macos-latest)
JavaScript / TypeScript validate-sdk-compliance-javascript.yml typedoc-packages (required)
Python validate-sdk-compliance-python.yml griffe-packages (required), griffe-search-paths
Dart validate-sdk-compliance-dart.yml

The JavaScript workflow targets the supabase-js pnpm monorepo: typedoc-packages is a comma-separated list of package dirs (relative to the SDK root), each defining a docs:json script that owns its TypeDoc entrypoints. The workflow installs with pnpm, runs each package's docs:json, then normalizes and merges the per-package TypeDoc JSON into one ParseResult per 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/:

Action Replaces
sdk-compliance-validate the entire validate job (checkout SDK + spec, setup-node, npm ci, validate compliance file)
sdk-compliance-check-setup the check job preamble (checkout PR/base/spec, setup-node, npm ci)
sdk-compliance-check-symbols the final check-api-symbols diff step

Each per-language workflow references these via the full path supabase/sdk/.github/actions/<name>@main and 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 on main. The sdk-ref input still controls which ref the capability-matrix scripts are checked out from.

Hardening

The run: blocks pass workflow inputs through env: variables instead of interpolating ${{ inputs.* }} directly, closing a shell-injection vector. Other safety fixes:

  • 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 the required: true check.
  • JavaScript: guard against an empty typedoc-packages value.
  • The symbol-diff check job is skipped on non-pull_request events, where pull_request.base.sha is empty and the diff would be meaningless.

Breaking change for callers

SDK repos must update their uses: reference from validate-sdk-compliance.yml (with language:) to the per-language file. The language input is gone. The Python workflow now takes a required griffe-packages input (was a runtime check before), and the JavaScript workflow takes a required typedoc-packages input.

Caller PRs opened:

Changes

  • Added: validate-sdk-compliance-{swift,javascript,python,dart}.yml; .github/actions/sdk-compliance-{validate,check-setup,check-symbols}/action.yml
  • Deleted: validate-sdk-compliance.yml
  • Modified: README.md, CLAUDE.md — document the per-language workflows and composite actions, add Swift and JavaScript opt-in examples, reformat README tables

Test plan

  • All workflow and composite-action files parse as valid YAML with the expected structure

@grdsdev grdsdev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.yml lines 90, 93 (griffe-packages, griffe-search-paths)
  • validate-sdk-compliance-javascript.yml line 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; fi

Correctness: 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-matrix

Each 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.

@spydon

spydon commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

@grdsdev fixed in ec2c755! :)

@spydon spydon force-pushed the claude/split-compliance-workflows branch from ec2c755 to b713266 Compare June 29, 2026 12:00
Base automatically changed from claude/zealous-wright-bb6326 to main June 29, 2026 12:21
spydon added 5 commits June 29, 2026 14:37
…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.
@spydon spydon force-pushed the claude/split-compliance-workflows branch from b713266 to 836ba81 Compare June 29, 2026 12:38

@mandarini mandarini left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@spydon can we update PR description to reflect the changes? I think it's a bit outdated! Can we also add the js example in README?

@spydon

spydon commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

@spydon can we update PR description to reflect the changes? I think it's a bit outdated! Can we also add the js example in README?

Fixed in 48ad8fb

@spydon spydon merged commit dedea70 into main Jun 30, 2026
@spydon spydon deleted the claude/split-compliance-workflows branch June 30, 2026 08:09
spydon added a commit to supabase/supabase-flutter that referenced this pull request Jun 30, 2026
## 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants