From 1a8433c31ec569dfd0a49a4df3cc50c62e7ab3a1 Mon Sep 17 00:00:00 2001 From: yesoreyeram <153843+yesoreyeram@users.noreply.github.com> Date: Fri, 13 Mar 2026 23:19:46 +0000 Subject: [PATCH 1/3] strict types for config --- src/DataSource.test.ts | 13 ++++---- src/types/config.ts | 61 ++++++++++++++++++++++++++++++-------- src/views/ConfigEditor.tsx | 16 ++++++---- 3 files changed, 66 insertions(+), 24 deletions(-) diff --git a/src/DataSource.test.ts b/src/DataSource.test.ts index 4b4d014d..8cd842d5 100644 --- a/src/DataSource.test.ts +++ b/src/DataSource.test.ts @@ -3,6 +3,7 @@ import { lastValueFrom, of } from 'rxjs'; import { GithubVariableSupport } from 'variables'; import { GitHubDataSource } from 'DataSource'; import type { GitHubVariableQuery } from 'types/query'; +import type { GitHubDataSourceOptions } from 'types/config'; describe('DataSource', () => { describe('GithubVariableSupport', () => { @@ -16,7 +17,7 @@ describe('DataSource', () => { }), ]; it('should return empty array if data in response is empty array', async () => { - const ds = new GitHubDataSource({} as DataSourceInstanceSettings); + const ds = new GitHubDataSource({} as DataSourceInstanceSettings); const vs = new GithubVariableSupport(ds); const query = {} as GitHubVariableQuery; jest.spyOn(ds, 'query').mockReturnValue(of({ data: [] })); @@ -25,7 +26,7 @@ describe('DataSource', () => { expect(res?.data.map((d) => d.text)).toEqual([]); }); it('should return empty array if no data in response', async () => { - const ds = new GitHubDataSource({} as DataSourceInstanceSettings); + const ds = new GitHubDataSource({} as DataSourceInstanceSettings); const vs = new GithubVariableSupport(ds); const query = {} as GitHubVariableQuery; jest.spyOn(ds, 'query').mockReturnValue(of({} as DataQueryResponse)); @@ -34,7 +35,7 @@ describe('DataSource', () => { expect(res?.data.map((d) => d.text)).toEqual([]); }); it('should return array with values if response has data', async () => { - const ds = new GitHubDataSource({} as DataSourceInstanceSettings); + const ds = new GitHubDataSource({} as DataSourceInstanceSettings); const vs = new GithubVariableSupport(ds); const query = { key: 'test', field: 'test' } as GitHubVariableQuery; const data = [toDataFrame({ fields: [{ name: 'test', values: ['value1', 'value2'] }] })]; @@ -44,7 +45,7 @@ describe('DataSource', () => { expect(res?.data.map((d) => d.text)).toEqual(['value1', 'value2']); }); it('mapping of key', async () => { - const ds = new GitHubDataSource({} as DataSourceInstanceSettings); + const ds = new GitHubDataSource({} as DataSourceInstanceSettings); const vs = new GithubVariableSupport(ds); const query = { key: 'foo' } as GitHubVariableQuery; const data = SAMPLE_RESPONSE_WITH_MULTIPLE_FIELDS; @@ -54,7 +55,7 @@ describe('DataSource', () => { expect(res?.data.map((d) => d.text)).toEqual(['foo1', 'foo2']); }); it('mapping of key and field', async () => { - const ds = new GitHubDataSource({} as DataSourceInstanceSettings); + const ds = new GitHubDataSource({} as DataSourceInstanceSettings); const vs = new GithubVariableSupport(ds); const query = { key: 'bar', field: 'foo' } as GitHubVariableQuery; const data = SAMPLE_RESPONSE_WITH_MULTIPLE_FIELDS; @@ -64,7 +65,7 @@ describe('DataSource', () => { expect(res?.data.map((d) => d.text)).toEqual(['foo1', 'foo2']); }); it('mapping of field', async () => { - const ds = new GitHubDataSource({} as DataSourceInstanceSettings); + const ds = new GitHubDataSource({} as DataSourceInstanceSettings); const vs = new GithubVariableSupport(ds); const query = { field: 'foo' } as GitHubVariableQuery; const data = SAMPLE_RESPONSE_WITH_MULTIPLE_FIELDS; diff --git a/src/types/config.ts b/src/types/config.ts index c76f75dc..49a22191 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -1,19 +1,56 @@ -import type { DataSourceJsonData } from '@grafana/data'; +import type { DataSourceJsonData } from '@grafana/schema'; export type GitHubLicenseType = 'github-basic' | 'github-enterprise-cloud' | 'github-enterprise-server'; export type GitHubAuthType = 'personal-access-token' | 'github-app'; -export type GitHubDataSourceOptions = { - githubPlan?: GitHubLicenseType; - githubUrl?: string; - selectedAuthType?: GitHubAuthType; - appId?: string; - installationId?: string; -} & DataSourceJsonData; +type GithubCommonOptionsBase = { + githubPlan?: T; +} & DataSourceJsonData -export type GitHubSecureJsonDataKeys = - | 'accessToken' // accessToken is set if the user is using a Personal Access Token to connect to GitHub - | 'privateKey'; // privateKey is set if the user is using a GitHub App to connect to GitHub +type GitHubDataSourceBasicOptions = GithubCommonOptionsBase<'github-basic'> & { + githubPlan?: 'github-basic'; + githubUrl: never; +}; -export type GitHubSecureJsonData = Partial>; +type GitHubDataSourceEnterpriseCloudOptions = GithubCommonOptionsBase<'github-enterprise-cloud'> & { + githubPlan: 'github-enterprise-cloud'; + githubUrl: never; +}; + +type GitHubDataSourceEnterpriseServerOptions = GithubCommonOptionsBase<'github-enterprise-server'> & { + githubPlan: 'github-enterprise-server'; + githubUrl: string; +}; + +type GithubDataSourceCommonOptions = (GitHubDataSourceBasicOptions | GitHubDataSourceEnterpriseCloudOptions | GitHubDataSourceEnterpriseServerOptions) + +type GithubDataSourceAuthOptionsBase = { + selectedAuthType?: T +} + +type GitHubDataSourcePATAuthOptions = GithubDataSourceAuthOptionsBase<'personal-access-token'> & { + appId: never; + installationId: string; +}; + +type GitHubDataSourceGHAppOptions = GithubDataSourceAuthOptionsBase<'github-app'> & { + appId: string; + installationId: string; +}; + +type GithubDataSourceAuthOptions = (GitHubDataSourcePATAuthOptions | GitHubDataSourceGHAppOptions) + +export type GitHubDataSourceOptions = GithubDataSourceCommonOptions & GithubDataSourceAuthOptions; + +type GitHubSecureJsonDataAuthPAT = { + accessToken: string; + privateKey?: string; +}; + +type GitHubSecureJsonDataAuthGHApp = { + privateKey: string; + accessToken?: string; +}; + +export type GitHubSecureJsonData = GitHubSecureJsonDataAuthPAT | GitHubSecureJsonDataAuthGHApp; diff --git a/src/views/ConfigEditor.tsx b/src/views/ConfigEditor.tsx index c52f9fab..77a459ab 100644 --- a/src/views/ConfigEditor.tsx +++ b/src/views/ConfigEditor.tsx @@ -4,6 +4,7 @@ import { onUpdateDatasourceJsonDataOption, onUpdateDatasourceSecureJsonDataOption, type DataSourcePluginOptionsEditorProps, + type DataSourceSettings, type GrafanaTheme2, type SelectableValue, } from '@grafana/data'; @@ -29,7 +30,7 @@ export type ConfigEditorProps = DataSourcePluginOptionsEditorProps { const { options, onOptionsChange } = props; const { jsonData, secureJsonData, secureJsonFields } = options; - const secureSettings = secureJsonData || {}; + const secureSettings = (secureJsonData || {}) as Partial; const styles = useStyles2(getStyles); const WIDTH_LONG = 40; @@ -65,7 +66,7 @@ const ConfigEditor = (props: ConfigEditorProps) => { secureJsonData: { ...options.secureJsonData, [prop]: event.target.value, - }, + } as GitHubSecureJsonData, secureJsonFields: { ...options.secureJsonFields, [prop]: set, @@ -80,7 +81,7 @@ const ConfigEditor = (props: ConfigEditorProps) => { const onAuthChange = useCallback( (value: GitHubAuthType) => { - onOptionsChange({ ...options, jsonData: { ...jsonData, selectedAuthType: value } }); + onOptionsChange({ ...options, jsonData: { ...jsonData, selectedAuthType: value } as GitHubDataSourceOptions }); }, [jsonData, onOptionsChange, options] ); @@ -92,7 +93,7 @@ const ConfigEditor = (props: ConfigEditorProps) => { ...jsonData, githubPlan, githubUrl: githubPlan === 'github-enterprise-server' ? jsonData.githubUrl : '', - }, + } as GitHubDataSourceOptions, }); setSelectedLicense(githubPlan); }; @@ -114,7 +115,7 @@ const ConfigEditor = (props: ConfigEditorProps) => { - setIsOpen((x) => !x)}> + setIsOpen((x) => !x)}>

How to create a access token

To create a new fine grained access token, navigate to{' '} @@ -208,7 +209,10 @@ const ConfigEditor = (props: ConfigEditorProps) => { )} {config.secureSocksDSProxyEnabled && ( - + ) => void} + /> )} From d485eaf20b95e124cfdd17e169c070c59e16e7cd Mon Sep 17 00:00:00 2001 From: yesoreyeram <153843+yesoreyeram@users.noreply.github.com> Date: Fri, 13 Mar 2026 23:49:51 +0000 Subject: [PATCH 2/3] zod based types for config --- package.json | 3 +- src/types/config.ts | 109 ++++++++++++++++++++++++++++---------------- yarn.lock | 3 +- 3 files changed, 73 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index 72fa7a55..ee72ed26 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,8 @@ "webpack-cli": "6.0.1", "webpack-livereload-plugin": "3.0.2", "webpack-subresource-integrity": "5.1.0", - "webpack-virtual-modules": "0.6.2" + "webpack-virtual-modules": "0.6.2", + "zod": "4.3.6" }, "resolutions": { "@remix-run/router": "1.23.2", diff --git a/src/types/config.ts b/src/types/config.ts index 49a22191..6505eb71 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -1,56 +1,85 @@ +import { z } from 'zod'; import type { DataSourceJsonData } from '@grafana/schema'; -export type GitHubLicenseType = 'github-basic' | 'github-enterprise-cloud' | 'github-enterprise-server'; +//#region jsonData -export type GitHubAuthType = 'personal-access-token' | 'github-app'; +//#region --- License / Plan schemas --- -type GithubCommonOptionsBase = { - githubPlan?: T; -} & DataSourceJsonData +const GitHubLicenseTypeSchema = z.enum(['github-basic', 'github-enterprise-cloud', 'github-enterprise-server']); +export type GitHubLicenseType = z.infer; -type GitHubDataSourceBasicOptions = GithubCommonOptionsBase<'github-basic'> & { - githubPlan?: 'github-basic'; - githubUrl: never; -}; +const GitHubAuthTypeSchema = z.enum(['personal-access-token', 'github-app']); +export type GitHubAuthType = z.infer; -type GitHubDataSourceEnterpriseCloudOptions = GithubCommonOptionsBase<'github-enterprise-cloud'> & { - githubPlan: 'github-enterprise-cloud'; - githubUrl: never; -}; +//#endregion -type GitHubDataSourceEnterpriseServerOptions = GithubCommonOptionsBase<'github-enterprise-server'> & { - githubPlan: 'github-enterprise-server'; - githubUrl: string; -}; +//#region --- Plan option schemas --- -type GithubDataSourceCommonOptions = (GitHubDataSourceBasicOptions | GitHubDataSourceEnterpriseCloudOptions | GitHubDataSourceEnterpriseServerOptions) +const GitHubDataSourceBasicOptionsSchema = z.object({ + githubPlan: z.literal('github-basic').optional(), + githubUrl: z.never(), +}); -type GithubDataSourceAuthOptionsBase = { - selectedAuthType?: T -} +const GitHubDataSourceEnterpriseCloudOptionsSchema = z.object({ + githubPlan: z.literal('github-enterprise-cloud'), + githubUrl: z.never(), +}); -type GitHubDataSourcePATAuthOptions = GithubDataSourceAuthOptionsBase<'personal-access-token'> & { - appId: never; - installationId: string; -}; +const GitHubDataSourceEnterpriseServerOptionsSchema = z.object({ + githubPlan: z.literal('github-enterprise-server'), + githubUrl: z.string(), +}); -type GitHubDataSourceGHAppOptions = GithubDataSourceAuthOptionsBase<'github-app'> & { - appId: string; - installationId: string; -}; +const GithubDataSourceCommonOptionsSchema = GitHubDataSourceBasicOptionsSchema + .or(GitHubDataSourceEnterpriseCloudOptionsSchema) + .or(GitHubDataSourceEnterpriseServerOptionsSchema); -type GithubDataSourceAuthOptions = (GitHubDataSourcePATAuthOptions | GitHubDataSourceGHAppOptions) +//#endregion -export type GitHubDataSourceOptions = GithubDataSourceCommonOptions & GithubDataSourceAuthOptions; +//#region --- Auth option schemas --- -type GitHubSecureJsonDataAuthPAT = { - accessToken: string; - privateKey?: string; -}; +const GitHubDataSourcePATAuthOptionsSchema = z.object({ + selectedAuthType: z.literal('personal-access-token').optional(), + appId: z.never(), + installationId: z.never(), +}); -type GitHubSecureJsonDataAuthGHApp = { - privateKey: string; - accessToken?: string; -}; +const GitHubDataSourceGHAppOptionsSchema = z.object({ + selectedAuthType: z.literal('github-app'), + appId: z.string(), + installationId: z.string(), +}); -export type GitHubSecureJsonData = GitHubSecureJsonDataAuthPAT | GitHubSecureJsonDataAuthGHApp; +const GithubDataSourceAuthOptionsSchema = GitHubDataSourcePATAuthOptionsSchema + .or(GitHubDataSourceGHAppOptionsSchema) + +//#endregion + +const GitHubDataSourceOptionsSchema = z.intersection(GithubDataSourceCommonOptionsSchema, GithubDataSourceAuthOptionsSchema); + +export type GitHubDataSourceOptions = z.infer & DataSourceJsonData; + +//#endregion + +//#region secureJsonData + +//#region --- Secure JSON data schemas --- + +const GitHubSecureJsonDataAuthPATSchema = z.object({ + accessToken: z.string(), + privateKey: z.never(), +}); + +const GitHubSecureJsonDataAuthGHAppSchema = z.object({ + accessToken: z.never(), + privateKey: z.string(), +}); + +//#endregion + +const GitHubSecureJsonDataSchema = GitHubSecureJsonDataAuthPATSchema + .or(GitHubSecureJsonDataAuthGHAppSchema) + +export type GitHubSecureJsonData = z.infer; + +//#endregion diff --git a/yarn.lock b/yarn.lock index 55536ea7..507b78c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8016,6 +8016,7 @@ __metadata: webpack-livereload-plugin: "npm:3.0.2" webpack-subresource-integrity: "npm:5.1.0" webpack-virtual-modules: "npm:0.6.2" + zod: "npm:4.3.6" languageName: unknown linkType: soft @@ -13989,7 +13990,7 @@ __metadata: languageName: node linkType: hard -"zod@npm:^3.25.0 || ^4.0.0, zod@npm:^4.3.0": +"zod@npm:4.3.6, zod@npm:^3.25.0 || ^4.0.0, zod@npm:^4.3.0": version: 4.3.6 resolution: "zod@npm:4.3.6" checksum: 10c0/860d25a81ab41d33aa25f8d0d07b091a04acb426e605f396227a796e9e800c44723ed96d0f53a512b57be3d1520f45bf69c0cb3b378a232a00787a2609625307 From 5fa3c4b06216ff9c745a5206591e20ce9e298154 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:43:45 +0000 Subject: [PATCH 3/3] Add config JSON schema generation from Zod and expose via resource call (#685) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generate JSON Schema from the Zod config schemas in `src/types/config.ts` and serve it via a backend `/schema/config` resource endpoint. Includes CI validation, a pre-commit hook to keep the schema in sync, and an ESLint guard to ensure config types remain self-contained. ### Schema generation - Export `GitHubDataSourceOptionsSchema` and `GitHubSecureJsonDataSchema` from `src/types/config.ts` - All Zod schema fields include `.describe()` calls so that human-readable descriptions (plan types, auth types, URLs, app IDs, tokens, keys) are propagated into the generated JSON Schema - Add `scripts/generate-config-schema.ts` — uses Zod v4's built-in `z.toJSONSchema()` to produce JSON Schema for both `jsonData` and `secureJsonData` - Outputs to `pkg/schema/config.json` (Go embed) - New npm script: `yarn generate:config-schema` ### Backend resource endpoint - `pkg/schema/config.go` — embeds the generated JSON schema via `//go:embed` - `pkg/plugin/instance.go` — `CallResource` intercepts `schema/config` path and returns the embedded schema; all other paths delegate to `SchemaDatasource` as before ```go if req.Path == "schema/config" { return sender.Send(&backend.CallResourceResponse{ Status: http.StatusOK, Headers: map[string][]string{"Content-Type": {"application/json"}}, Body: schema.ConfigSchemaJSON, }) } ``` ### Local import guard (ESLint) - Added an ESLint `no-restricted-imports` rule in `eslint.config.mjs` scoped to `src/types/config.ts` that blocks any relative imports (`'./*'`, `'../*'`), ensuring all config types remain self-contained in a single file - The CI workflow runs `yarn lint` against `src/types/config.ts` to enforce this guard ### CI workflow - `.github/workflows/check-config-schema.yml` — triggers on changes to `src/types/config.ts` or the generation script, lints config types for import violations, regenerates the schema, and fails if the committed version is stale ### Pre-commit hook - `scripts/pre-commit` — auto-regenerates and stages the schema file when `src/types/config.ts` is modified - Install: `cp scripts/pre-commit .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit` --- 📍 Connect Copilot coding agent with [Jira](https://gh.io/cca-jira-docs), [Azure Boards](https://gh.io/cca-azure-boards-docs) or [Linear](https://gh.io/cca-linear-docs) to delegate work to Copilot in one click without leaving your project management tool. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: yesoreyeram <153843+yesoreyeram@users.noreply.github.com> --- .github/workflows/check-config-schema.yml | 45 ++++++ eslint.config.mjs | 17 +++ package.json | 3 +- pkg/plugin/instance.go | 9 ++ pkg/schema/config.go | 8 + pkg/schema/config.json | 178 ++++++++++++++++++++++ scripts/generate-config-schema.ts | 23 +++ scripts/pre-commit | 18 +++ src/types/config.ts | 55 +++---- 9 files changed, 328 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/check-config-schema.yml create mode 100644 pkg/schema/config.go create mode 100644 pkg/schema/config.json create mode 100644 scripts/generate-config-schema.ts create mode 100755 scripts/pre-commit 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;