diff --git a/.github/workflows/check-config-schema.yml b/.github/workflows/check-config-schema.yml new file mode 100644 index 00000000..c2b2948f --- /dev/null +++ b/.github/workflows/check-config-schema.yml @@ -0,0 +1,45 @@ +name: Check Config Schema + +permissions: + contents: read + +on: + pull_request: + paths: + - 'src/types/config.ts' + - 'scripts/generate-config-schema.ts' + push: + branches: + - main + paths: + - 'src/types/config.ts' + - 'scripts/generate-config-schema.ts' + +jobs: + check-config-schema: + name: Verify config schema is up-to-date + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + + - name: Install dependencies + run: yarn install --immutable + + - name: Lint config types (import guard) + run: yarn lint --no-cache -- src/types/config.ts + + - name: Generate config schema + run: yarn generate:config-schema + + - name: Check for uncommitted schema changes + run: | + if ! git diff --exit-code pkg/schema/config.json; then + echo "::error::Config schema is out of date. Run 'yarn generate:config-schema' and commit the changes." + exit 1 + fi + echo "Config schema is up-to-date." diff --git a/eslint.config.mjs b/eslint.config.mjs index 39f4f1ca..3f5bc546 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -28,4 +28,21 @@ export default defineConfig([ ], }, ...baseConfig, + { + files: ['src/types/config.ts'], + rules: { + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + group: ['./*', '../*'], + message: + 'src/types/config.ts must be self-contained with no local imports to ensure reliable schema generation.', + }, + ], + }, + ], + }, + }, ]); diff --git a/package.json b/package.json index ee72ed26..99e693fe 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "spellcheck": "cspell -c cspell.config.json \"**/*.{ts,tsx,js,go,md,mdx,yml,yaml,json,scss,css}\"", "test": "jest --watch --onlyChanged", "test:ci": "jest --passWithNoTests --maxWorkers 4", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "generate:config-schema": "ts-node scripts/generate-config-schema.ts" }, "dependencies": { "@emotion/css": "11.13.5", diff --git a/pkg/plugin/instance.go b/pkg/plugin/instance.go index cdfe6c45..99048fe3 100644 --- a/pkg/plugin/instance.go +++ b/pkg/plugin/instance.go @@ -3,11 +3,13 @@ package plugin import ( "context" "fmt" + "net/http" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" schemas "github.com/grafana/schemads" + "github.com/grafana/github-datasource/pkg/schema" "github.com/grafana/github-datasource/pkg/github" "github.com/grafana/github-datasource/pkg/models" ) @@ -19,6 +21,13 @@ type GitHubInstanceWithSchema struct { } func (g *GitHubInstanceWithSchema) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { + if req.Path == "schema/config" { + return sender.Send(&backend.CallResourceResponse{ + Status: http.StatusOK, + Headers: map[string][]string{"Content-Type": {"application/json"}}, + Body: schema.ConfigSchemaJSON, + }) + } return g.SchemaDatasource.CallResource(ctx, req, sender) } diff --git a/pkg/schema/config.go b/pkg/schema/config.go new file mode 100644 index 00000000..075f3f21 --- /dev/null +++ b/pkg/schema/config.go @@ -0,0 +1,8 @@ +package schema + +import ( + _ "embed" +) + +//go:embed config.json +var ConfigSchemaJSON []byte diff --git a/pkg/schema/config.json b/pkg/schema/config.json new file mode 100644 index 00000000..cbe6dcaf --- /dev/null +++ b/pkg/schema/config.json @@ -0,0 +1,178 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "GitHubDataSourceConfig", + "description": "Configuration schema for the Grafana GitHub data source plugin", + "type": "object", + "properties": { + "jsonData": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "allOf": [ + { + "anyOf": [ + { + "anyOf": [ + { + "type": "object", + "properties": { + "githubPlan": { + "description": "GitHub plan type (basic)", + "type": "string", + "const": "github-basic" + }, + "githubUrl": { + "not": {}, + "description": "Not applicable for GitHub basic plan" + } + }, + "required": [ + "githubUrl" + ], + "additionalProperties": false, + "description": "Configuration for GitHub basic plan" + }, + { + "type": "object", + "properties": { + "githubPlan": { + "type": "string", + "const": "github-enterprise-cloud", + "description": "GitHub plan type (Enterprise Cloud)" + }, + "githubUrl": { + "not": {}, + "description": "Not applicable for GitHub Enterprise Cloud" + } + }, + "required": [ + "githubPlan", + "githubUrl" + ], + "additionalProperties": false, + "description": "Configuration for GitHub Enterprise Cloud plan" + } + ] + }, + { + "type": "object", + "properties": { + "githubPlan": { + "type": "string", + "const": "github-enterprise-server", + "description": "GitHub plan type (Enterprise Server)" + }, + "githubUrl": { + "type": "string", + "description": "The URL of the GitHub Enterprise Server instance" + } + }, + "required": [ + "githubPlan", + "githubUrl" + ], + "additionalProperties": false, + "description": "Configuration for GitHub Enterprise Server plan" + } + ] + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "selectedAuthType": { + "description": "Authentication type (Personal Access Token)", + "type": "string", + "const": "personal-access-token" + }, + "appId": { + "not": {}, + "description": "Not applicable for PAT authentication" + }, + "installationId": { + "not": {}, + "description": "Not applicable for PAT authentication" + } + }, + "required": [ + "appId", + "installationId" + ], + "additionalProperties": false, + "description": "Authentication options for Personal Access Token" + }, + { + "type": "object", + "properties": { + "selectedAuthType": { + "type": "string", + "const": "github-app", + "description": "Authentication type (GitHub App)" + }, + "appId": { + "type": "string", + "description": "The GitHub App ID" + }, + "installationId": { + "type": "string", + "description": "The GitHub App installation ID" + } + }, + "required": [ + "selectedAuthType", + "appId", + "installationId" + ], + "additionalProperties": false, + "description": "Authentication options for GitHub App" + } + ] + } + ], + "description": "GitHub data source configuration options (jsonData)" + }, + "secureJsonData": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "anyOf": [ + { + "type": "object", + "properties": { + "accessToken": { + "type": "string", + "description": "Personal access token for GitHub API authentication" + }, + "privateKey": { + "not": {}, + "description": "Not applicable for PAT authentication" + } + }, + "required": [ + "accessToken", + "privateKey" + ], + "additionalProperties": false, + "description": "Secure data for Personal Access Token authentication" + }, + { + "type": "object", + "properties": { + "accessToken": { + "not": {}, + "description": "Not applicable for GitHub App authentication" + }, + "privateKey": { + "type": "string", + "description": "Private key for GitHub App authentication (PEM format)" + } + }, + "required": [ + "accessToken", + "privateKey" + ], + "additionalProperties": false, + "description": "Secure data for GitHub App authentication" + } + ], + "description": "Secure JSON data for GitHub data source authentication (secureJsonData)" + } + } +} diff --git a/scripts/generate-config-schema.ts b/scripts/generate-config-schema.ts new file mode 100644 index 00000000..954e1074 --- /dev/null +++ b/scripts/generate-config-schema.ts @@ -0,0 +1,23 @@ +import { z } from 'zod'; +import * as fs from 'fs'; +import * as path from 'path'; + +import { GitHubDataSourceOptionsSchema, GitHubSecureJsonDataSchema } from '../src/types/config'; + +const configSchema = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'GitHubDataSourceConfig', + description: 'Configuration schema for the Grafana GitHub data source plugin', + type: 'object' as const, + properties: { + jsonData: z.toJSONSchema(GitHubDataSourceOptionsSchema), + secureJsonData: z.toJSONSchema(GitHubSecureJsonDataSchema), + }, +}; + +const schemaJSON = JSON.stringify(configSchema, null, 2) + '\n'; + +const outPath = path.resolve(__dirname, '..', 'pkg', 'schema', 'config.json'); +fs.mkdirSync(path.dirname(outPath), { recursive: true }); +fs.writeFileSync(outPath, schemaJSON); +console.log(`Config JSON schema written to ${outPath}`); diff --git a/scripts/pre-commit b/scripts/pre-commit new file mode 100755 index 00000000..f19acee7 --- /dev/null +++ b/scripts/pre-commit @@ -0,0 +1,18 @@ +#!/bin/sh +# +# Git pre-commit hook that regenerates the config JSON schema +# when src/types/config.ts is modified. +# +# To install, run from the repository root: +# cp scripts/pre-commit .git/hooks/pre-commit +# chmod +x .git/hooks/pre-commit + +STAGED=$(git diff --cached --name-only) + +if echo "$STAGED" | grep -q "src/types/config.ts"; then + echo "Config types changed — regenerating config JSON schema..." + yarn generate:config-schema + + git add pkg/schema/config.json + echo "Config JSON schema updated and staged." +fi diff --git a/src/types/config.ts b/src/types/config.ts index 6505eb71..151eeac3 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -5,10 +5,10 @@ import type { DataSourceJsonData } from '@grafana/schema'; //#region --- License / Plan schemas --- -const GitHubLicenseTypeSchema = z.enum(['github-basic', 'github-enterprise-cloud', 'github-enterprise-server']); +const GitHubLicenseTypeSchema = z.enum(['github-basic', 'github-enterprise-cloud', 'github-enterprise-server']).describe('The GitHub license/plan type'); export type GitHubLicenseType = z.infer; -const GitHubAuthTypeSchema = z.enum(['personal-access-token', 'github-app']); +const GitHubAuthTypeSchema = z.enum(['personal-access-token', 'github-app']).describe('The GitHub authentication method'); export type GitHubAuthType = z.infer; //#endregion @@ -16,19 +16,19 @@ export type GitHubAuthType = z.infer; //#region --- Plan option schemas --- const GitHubDataSourceBasicOptionsSchema = z.object({ - githubPlan: z.literal('github-basic').optional(), - githubUrl: z.never(), -}); + githubPlan: z.literal('github-basic').optional().describe('GitHub plan type (basic)'), + githubUrl: z.never().describe('Not applicable for GitHub basic plan'), +}).describe('Configuration for GitHub basic plan'); const GitHubDataSourceEnterpriseCloudOptionsSchema = z.object({ - githubPlan: z.literal('github-enterprise-cloud'), - githubUrl: z.never(), -}); + githubPlan: z.literal('github-enterprise-cloud').describe('GitHub plan type (Enterprise Cloud)'), + githubUrl: z.never().describe('Not applicable for GitHub Enterprise Cloud'), +}).describe('Configuration for GitHub Enterprise Cloud plan'); const GitHubDataSourceEnterpriseServerOptionsSchema = z.object({ - githubPlan: z.literal('github-enterprise-server'), - githubUrl: z.string(), -}); + githubPlan: z.literal('github-enterprise-server').describe('GitHub plan type (Enterprise Server)'), + githubUrl: z.string().describe('The URL of the GitHub Enterprise Server instance'), +}).describe('Configuration for GitHub Enterprise Server plan'); const GithubDataSourceCommonOptionsSchema = GitHubDataSourceBasicOptionsSchema .or(GitHubDataSourceEnterpriseCloudOptionsSchema) @@ -39,23 +39,23 @@ const GithubDataSourceCommonOptionsSchema = GitHubDataSourceBasicOptionsSchema //#region --- Auth option schemas --- const GitHubDataSourcePATAuthOptionsSchema = z.object({ - selectedAuthType: z.literal('personal-access-token').optional(), - appId: z.never(), - installationId: z.never(), -}); + selectedAuthType: z.literal('personal-access-token').optional().describe('Authentication type (Personal Access Token)'), + appId: z.never().describe('Not applicable for PAT authentication'), + installationId: z.never().describe('Not applicable for PAT authentication'), +}).describe('Authentication options for Personal Access Token'); const GitHubDataSourceGHAppOptionsSchema = z.object({ - selectedAuthType: z.literal('github-app'), - appId: z.string(), - installationId: z.string(), -}); + selectedAuthType: z.literal('github-app').describe('Authentication type (GitHub App)'), + appId: z.string().describe('The GitHub App ID'), + installationId: z.string().describe('The GitHub App installation ID'), +}).describe('Authentication options for GitHub App'); const GithubDataSourceAuthOptionsSchema = GitHubDataSourcePATAuthOptionsSchema .or(GitHubDataSourceGHAppOptionsSchema) //#endregion -const GitHubDataSourceOptionsSchema = z.intersection(GithubDataSourceCommonOptionsSchema, GithubDataSourceAuthOptionsSchema); +export const GitHubDataSourceOptionsSchema = z.intersection(GithubDataSourceCommonOptionsSchema, GithubDataSourceAuthOptionsSchema).describe('GitHub data source configuration options (jsonData)'); export type GitHubDataSourceOptions = z.infer & DataSourceJsonData; @@ -66,19 +66,20 @@ export type GitHubDataSourceOptions = z.infer;