diff --git a/.github/workflows/provider-guide-drift.yml b/.github/workflows/provider-guide-drift.yml new file mode 100644 index 00000000..154ae216 --- /dev/null +++ b/.github/workflows/provider-guide-drift.yml @@ -0,0 +1,41 @@ +name: Provider guide drift check + +on: + pull_request: + branches: + - main + paths: + - "src/provider-guides/**/*.mdx" + - "scripts/drift-check/**" + - "package.json" + - "pnpm-lock.yaml" + - ".github/workflows/provider-guide-drift.yml" + +jobs: + provider-guide-drift: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 8 + + - name: Install dependencies + run: pnpm install + shell: bash + + - name: Run drift check for changed provider guides + shell: bash + run: | + pnpm run drift-check -- \ + --mode per-pr \ + --changed-from "${{ github.event.pull_request.base.sha }}" \ + --changed-to "${{ github.sha }}" \ + --out /tmp/ampersand-provider-guide-drift \ + --fail-on-error diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..e96e446b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,29 @@ +# Agent instructions + +## Provider guides + +Before creating or editing provider guides in `src/provider-guides/`, read: + +- `PROVIDER_GUIDE.md` +- `CONTRIBUTING.md` +- `scripts/drift-check/README.md` + +Keep provider guides aligned with those files even when existing guides differ. + +When provider guide work changes navigation, edit `src/generate-docs.ts` and regenerate `src/docs.json` with: + +```shell +pnpm run gen-docs +``` + +Before finishing provider guide work, run the most specific drift check that applies: + +```shell +pnpm run drift-check -- --mode provider --provider --out /tmp/ampersand-provider-drift +``` + +For broader provider-guide changes, run: + +```shell +pnpm run drift-check -- --out /tmp/ampersand-docs-drift +``` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..e35b80a2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,3 @@ +# Claude instructions + +Follow `AGENTS.md`. diff --git a/PROVIDER_GUIDE.md b/PROVIDER_GUIDE.md new file mode 100644 index 00000000..87fa81d7 --- /dev/null +++ b/PROVIDER_GUIDE.md @@ -0,0 +1,110 @@ +# Provider guide standard + +This guide is for contributors writing or editing provider guides in `src/provider-guides/`. + +## Before you start + +- Read `CONTRIBUTING.md` for shared writing style. +- Check the provider's catalog key and supported actions before writing claims. +- Use this file as the source of truth when existing guides conflict with it. + +## File and provider mapping + +Prefer naming the guide file after the provider catalog key, for example `src/provider-guides/hubspot.mdx`. + +The drift check resolves a guide to a provider in this order: + +1. `provider` in frontmatter +2. `scripts/drift-check/recipes.ts` slug override +3. case-insensitive catalog-key match +4. filename slug + +Use `provider` frontmatter or a slug override when the public guide slug and catalog key are different strings. Case-only differences do not need a slug override. + +Every guide should include frontmatter with a `title`. + +```mdx +--- +title: "HubSpot" +--- +``` + +## Supported actions + +Only list actions that are supported by the generated connector catalog. + +Use these exact links when claiming support. The drift check matches them exactly. + +```md +- [Read Actions](/read-actions) +- [Write Actions](/write-actions) +- [Proxy Actions](/proxy-actions) +- [Subscribe Actions](/subscribe-actions) +- [Search Actions](/search-actions) +``` + +Do not change those link labels to sentence case, such as `[Read actions]`, because the drift check will not detect the claim. + +## Deep connector details + +For deep connectors: + +- Include a supported objects section. +- Link objects to provider documentation when object or schema docs are available. +- State clearly if incremental read is not supported. +- Add a provider-specific sample `amp.yaml` to `amp-labs/samples` and link to it from the guide. + +Sample links should use this shape so drift check can validate them: + +```md +https://github.com/amp-labs/samples/blob/main//amp.yaml +``` + +## Provider setup + +Include enough provider-side setup detail for a developer to complete the integration without guessing. + +Cover these when they apply: + +- how to create or access a developer app +- required OAuth redirect URLs, scopes, API keys, or secrets +- where to find credentials needed in Ampersand +- free trial, sandbox, developer instance, or test account links +- marketplace listing, app review, admin approval, or publication requirements + +Use screenshots or GIFs when setup is visual or multi-step. Capture only the relevant UI and remove private, customer, or internal details. + +## Navigation and generated files + +Provider guide pages are registered manually in `src/generate-docs.ts`. After editing navigation, regenerate `src/docs.json` with: + +```shell +pnpm run gen-docs +``` + +Do not run the full `pnpm run gen` just to update navigation unless you also intend to refresh generated API reference files. + +## Verification + +For one provider guide, run: + +```shell +pnpm run drift-check -- --mode provider --provider --out /tmp/ampersand-provider-drift +``` + +CI also runs a PR-scoped drift check for changed provider guides. Running the provider check locally first catches the same class of guide-to-catalog issues before review. + +For broad changes, run: + +```shell +pnpm run drift-check -- --out /tmp/ampersand-docs-drift +``` + +The drift check currently enforces: + +- undocumented providers +- guides that do not resolve to a catalog entry +- supported-action overclaims +- broken sample links + +Other requirements in this guide still need reviewer judgment. diff --git a/README.md b/README.md index 2397908e..eff79abb 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ pnpm i If you need to add a new page, add it to the `mintConfig` object in `src/generate-docs.ts`, and then follow the steps below for regenerating docs.json. +If you are adding or editing a provider guide, also follow [PROVIDER_GUIDE.md](./PROVIDER_GUIDE.md). + ## Changing the URL of a page If you are changing the URL of a page, be sure to add the old URL to the `redirects` section of the `mintConfig` object in `src/generate-docs.ts`, and then follow the steps below for regenerating docs.json. diff --git a/scripts/drift-check/README.md b/scripts/drift-check/README.md index 18b6d988..f3516518 100644 --- a/scripts/drift-check/README.md +++ b/scripts/drift-check/README.md @@ -9,6 +9,7 @@ pnpm run drift-check # full sweep, default ./drift-repor pnpm run drift-check -- --out /tmp/out # custom output directory pnpm run drift-check -- --mode provider --provider hubspot pnpm run drift-check -- --fail-on-error # exit non-zero on any error finding +pnpm run drift-check -- --mode per-pr --changed-from "$BASE_SHA" --changed-to "$HEAD_SHA" ``` Both `--out X` and `-- --out X` (the pnpm forwarding form) work. Output is written to `drift-report.json` (canonical) and `drift-report.md` (human-readable rollup). @@ -16,7 +17,7 @@ Both `--out X` and `-- --out X` (the pnpm forwarding form) work. Output is writt ## Modes - `full` (default): scan every guide; run the undocumented-provider check. -- `per-pr --changed ...`: scan only the listed `.mdx` files. Skips the undocumented check (needs a global view). +- `per-pr`: scan only changed `.mdx` files; skips the undocumented check (needs a global view). Pass the set as `--changed ...`, or as a commit range with `--changed-from --changed-to ` (resolved via `git diff`, failing closed if the range cannot be computed). - `provider --provider `: scan a single guide. Useful while iterating. ## Finding types diff --git a/scripts/drift-check/index.ts b/scripts/drift-check/index.ts index 6876ddee..5067d191 100644 --- a/scripts/drift-check/index.ts +++ b/scripts/drift-check/index.ts @@ -1,5 +1,6 @@ #!/usr/bin/env tsx import { parseArgs } from 'node:util'; +import { execFileSync } from 'node:child_process'; import path from 'node:path'; import { fetchCatalog, @@ -14,6 +15,50 @@ import { checkSampleLink } from './samples'; import { recipes } from './recipes'; import { writeReport, type Finding, type Report } from './report'; +// per-pr mode takes the changed set either as explicit --changed paths or as a +// commit range (--changed-from/--changed-to) that we resolve with git here. +function resolveChangedPaths(values: { + changed?: string[]; + 'changed-from'?: string; + 'changed-to'?: string; +}): string[] { + if (values.changed && values.changed.length > 0) { + return values.changed; + } + const from = values['changed-from']; + const to = values['changed-to']; + if (from && to) { + return gitChangedFiles(from, to); + } + throw new Error( + 'per-pr mode requires either --changed ... or both --changed-from and --changed-to ', + ); +} + +// Fail closed: a diff that cannot be computed (e.g. a shallow checkout missing a +// commit) must not be read as "nothing changed", which would let the gate pass +// on everything. +function gitChangedFiles(from: string, to: string): string[] { + let out: string; + try { + out = execFileSync( + 'git', + ['diff', '--name-only', '--diff-filter=ACMRT', from, to], + { encoding: 'utf8' }, + ); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + throw new Error( + `Could not compute changed files from "git diff ${from} ${to}": ${message}. ` + + 'Ensure the checkout has full history for both commits (actions/checkout fetch-depth: 0).', + ); + } + return out + .split('\n') + .map((line) => line.trim()) + .filter(Boolean); +} + async function main() { // Accept both `drift-check --out X` and `pnpm run drift-check -- --out X`. const args = process.argv.slice(2); @@ -24,6 +69,8 @@ async function main() { mode: { type: 'string', default: 'full' }, provider: { type: 'string' }, changed: { type: 'string', multiple: true }, + 'changed-from': { type: 'string' }, + 'changed-to': { type: 'string' }, out: { type: 'string', default: './drift-report' }, 'fail-on-error': { type: 'boolean', default: false }, }, @@ -36,8 +83,13 @@ async function main() { let guides = await scanGuides(); if (mode === 'per-pr') { - const changedSet = new Set((values.changed ?? []).map((p) => path.normalize(p))); + const changedSet = new Set( + resolveChangedPaths(values).map((p) => path.normalize(p)), + ); guides = guides.filter((g) => changedSet.has(path.normalize(g.docPath))); + if (guides.length === 0) { + console.log('No changed provider guides to check.'); + } } else if (mode === 'provider') { if (!values.provider) { throw new Error('--provider required with --mode provider');