From 6e2c9712fccbeb77d371de55f1720dc09591aeda Mon Sep 17 00:00:00 2001 From: Phuc Nguyen Date: Wed, 10 Jun 2026 13:08:02 +0700 Subject: [PATCH 1/5] Add GitHub secrets provider --- packages/cli/src/adapter-registry.ts | 4 +- packages/secrets/github/README.md | 35 ++++++ packages/secrets/github/package.json | 37 +++++++ packages/secrets/github/src/index.test.ts | 115 ++++++++++++++++++++ packages/secrets/github/src/index.ts | 127 ++++++++++++++++++++++ packages/secrets/github/tsconfig.json | 5 + pnpm-lock.yaml | 6 + 7 files changed, 327 insertions(+), 2 deletions(-) create mode 100644 packages/secrets/github/README.md create mode 100644 packages/secrets/github/package.json create mode 100644 packages/secrets/github/src/index.test.ts create mode 100644 packages/secrets/github/src/index.ts create mode 100644 packages/secrets/github/tsconfig.json diff --git a/packages/cli/src/adapter-registry.ts b/packages/cli/src/adapter-registry.ts index b7b14b67..eb821a1a 100644 --- a/packages/cli/src/adapter-registry.ts +++ b/packages/cli/src/adapter-registry.ts @@ -95,8 +95,8 @@ export const CATEGORIES: readonly AdapterCategory[] = [ { id: 'secrets', pkgPrefix: '@profullstack/sh1pt-secrets', - description: 'Secrets CLIs — Doppler, dotenvx, 1Password', - adapters: ['doppler', 'dotenvx', 'onepassword'], + description: 'Secrets CLIs — Doppler, dotenvx, GitHub Secrets, 1Password', + adapters: ['doppler', 'dotenvx', 'github', 'onepassword'], }, { id: 'security', diff --git a/packages/secrets/github/README.md b/packages/secrets/github/README.md new file mode 100644 index 00000000..1a468362 --- /dev/null +++ b/packages/secrets/github/README.md @@ -0,0 +1,35 @@ +# GitHub Secrets + +Provides the GitHub Actions secrets module for sh1pt. + +## What it does + +- Lists repository, environment, organization, or user secret names with `gh secret list`. +- Pushes secret values with `gh secret set` without logging secret values. +- Supports GitHub Actions, Agents, Codespaces, and Dependabot secret scopes exposed by GitHub CLI. + +## Package + +- Name: `@profullstack/sh1pt-secrets-github` +- Path: `packages/secrets/github` +- Adapter ID: `secrets-github` +- Homepage: https://sh1pt.com + +## Scripts + +- `build`: `tsc -p tsconfig.json` +- `prepublishOnly`: `pnpm build` +- `typecheck`: `tsc -p tsconfig.json --noEmit` + +## Usage + +```bash +pnpm add @profullstack/sh1pt-secrets-github +``` + +## Development + +```bash +pnpm --filter @profullstack/sh1pt-secrets-github typecheck +pnpm vitest run packages/secrets/github/src/index.test.ts +``` diff --git a/packages/secrets/github/package.json b/packages/secrets/github/package.json new file mode 100644 index 00000000..3b03b7cb --- /dev/null +++ b/packages/secrets/github/package.json @@ -0,0 +1,37 @@ +{ + "name": "@profullstack/sh1pt-secrets-github", + "version": "0.1.15", + "type": "module", + "main": "./src/index.ts", + "scripts": { + "build": "tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "prepublishOnly": "pnpm build" + }, + "dependencies": { + "@profullstack/sh1pt-core": "workspace:*" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/profullstack/sh1pt.git", + "directory": "packages/secrets/github" + }, + "homepage": "https://sh1pt.com", + "bugs": "https://github.com/profullstack/sh1pt/issues", + "files": [ + "dist" + ], + "publishConfig": { + "access": "public", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + } + } +} diff --git a/packages/secrets/github/src/index.test.ts b/packages/secrets/github/src/index.test.ts new file mode 100644 index 00000000..7fcf3a00 --- /dev/null +++ b/packages/secrets/github/src/index.test.ts @@ -0,0 +1,115 @@ +import { smokeTest } from '@profullstack/sh1pt-core/testing'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const { execMock } = vi.hoisted(() => ({ + execMock: vi.fn(), +})); + +vi.mock('@profullstack/sh1pt-core', async () => ({ + ...await vi.importActual('@profullstack/sh1pt-core'), + exec: execMock, +})); + +import adapter from './index.js'; + +smokeTest(adapter, { idPrefix: 'secrets' }); + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe('GitHub secrets provider', () => { + it('lists GitHub secret metadata without attempting to read values', async () => { + execMock.mockResolvedValue({ + exitCode: 0, + stderr: '', + stdout: JSON.stringify([ + { name: 'API_TOKEN', updatedAt: '2026-06-10T00:00:00Z', visibility: 'private' }, + { name: 'DEPLOY_KEY', numSelectedRepos: 2 }, + ]), + }); + + await expect(adapter.pull({ secret: () => undefined, log: () => {} }, { + repo: 'owner/repo', + app: 'actions', + })).resolves.toEqual([ + { key: 'API_TOKEN', path: 'private · 2026-06-10T00:00:00Z' }, + { key: 'DEPLOY_KEY', path: '2 selected repos' }, + ]); + + expect(execMock).toHaveBeenCalledWith('gh', [ + 'secret', + 'list', + '--app', + 'actions', + '--json', + 'name,updatedAt,visibility,selectedReposURL,numSelectedRepos', + '--repo', + 'owner/repo', + ], expect.objectContaining({ throwOnNonZero: true })); + }); + + it('sets repository environment secrets from provided values or the sh1pt vault', async () => { + execMock.mockResolvedValue({ exitCode: 0, stdout: '', stderr: '' }); + const logs: string[] = []; + + await expect(adapter.push({ + secret: (key) => key === 'FROM_VAULT' ? 'vault-value' : undefined, + log: (message) => logs.push(message), + }, [ + { key: 'DIRECT_VALUE', value: 'direct-value' }, + { key: 'FROM_VAULT' }, + ], { + repo: 'owner/repo', + environment: 'production', + })).resolves.toEqual({ count: 2 }); + + expect(execMock).toHaveBeenNthCalledWith(1, 'gh', [ + 'secret', + 'set', + '--app', + 'actions', + '--repo', + 'owner/repo', + '--env', + 'production', + 'DIRECT_VALUE', + '--body', + 'direct-value', + ], expect.objectContaining({ throwOnNonZero: true })); + expect(execMock).toHaveBeenNthCalledWith(2, 'gh', [ + 'secret', + 'set', + '--app', + 'actions', + '--repo', + 'owner/repo', + '--env', + 'production', + 'FROM_VAULT', + '--body', + 'vault-value', + ], expect.objectContaining({ throwOnNonZero: true })); + expect(logs.join('\n')).not.toContain('direct-value'); + expect(logs.join('\n')).not.toContain('vault-value'); + }); + + it('supports organization visibility arguments', async () => { + execMock.mockResolvedValue({ exitCode: 0, stdout: '', stderr: '' }); + + await adapter.push({ secret: () => undefined, log: () => {} }, [ + { key: 'ORG_TOKEN', value: 'token' }, + ], { + org: 'my-org', + visibility: 'selected', + repos: ['repo-a', 'repo-b'], + }); + + expect(execMock).toHaveBeenCalledWith('gh', expect.arrayContaining([ + '--org', + 'my-org', + '--repos', + 'repo-a,repo-b', + ]), expect.any(Object)); + }); +}); diff --git a/packages/secrets/github/src/index.ts b/packages/secrets/github/src/index.ts new file mode 100644 index 00000000..3af45da5 --- /dev/null +++ b/packages/secrets/github/src/index.ts @@ -0,0 +1,127 @@ +import { defineSecretProvider, exec, manualSetup, type SecretRef } from '@profullstack/sh1pt-core'; + +type GitHubSecretApp = 'actions' | 'agents' | 'codespaces' | 'dependabot'; +type GitHubSecretVisibility = 'all' | 'private' | 'selected'; + +interface Config { + app?: GitHubSecretApp; + repo?: string; + environment?: string; + org?: string; + user?: boolean; + visibility?: GitHubSecretVisibility; + repos?: string[]; + noReposSelected?: boolean; +} + +interface GitHubSecretListEntry { + name: string; + updatedAt?: string; + visibility?: string; + selectedReposURL?: string; + numSelectedRepos?: number; +} + +function text(value: string | undefined): string | undefined { + const trimmed = value?.trim(); + return trimmed ? trimmed : undefined; +} + +function app(config: Config): GitHubSecretApp { + return config.app ?? 'actions'; +} + +function targetArgs(config: Config): string[] { + const args: string[] = []; + const repo = text(config.repo); + const environment = text(config.environment); + const org = text(config.org); + + if (repo) args.push('--repo', repo); + if (environment) args.push('--env', environment); + if (org) args.push('--org', org); + if (config.user) args.push('--user'); + + return args; +} + +function orgVisibilityArgs(config: Config): string[] { + if (!text(config.org) && !config.user) return []; + if (config.noReposSelected) return ['--no-repos-selected']; + if (config.repos?.length) return ['--repos', config.repos.join(',')]; + if (config.visibility) return ['--visibility', config.visibility]; + return []; +} + +function assertSecretKey(key: string): string { + const normalized = key.trim(); + if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(normalized)) { + throw new Error(`GitHub secret key must be an environment-style name: ${key}`); + } + return normalized; +} + +function parseSecretList(stdout: string): SecretRef[] { + const body = stdout.trim(); + if (!body) return []; + const entries = JSON.parse(body) as GitHubSecretListEntry[]; + return entries.map((entry) => ({ + key: entry.name, + path: [ + entry.visibility, + entry.numSelectedRepos !== undefined ? `${entry.numSelectedRepos} selected repos` : undefined, + entry.updatedAt, + ].filter(Boolean).join(' · ') || undefined, + })); +} + +export default defineSecretProvider({ + id: 'secrets-github', + label: 'GitHub Secrets', + cli: 'gh', + async connect(ctx, config) { + const scope = text(config.repo) ?? text(config.org) ?? (config.user ? 'user' : 'current repository'); + ctx.log(`gh auth status · scope=${scope}`); + return { accountId: scope }; + }, + async pull(ctx, config): Promise { + const args = [ + 'secret', + 'list', + '--app', + app(config), + '--json', + 'name,updatedAt,visibility,selectedReposURL,numSelectedRepos', + ...targetArgs(config), + ]; + ctx.log(`gh ${args.join(' ')}`); + const result = await exec('gh', args, { log: (message) => ctx.log(message), throwOnNonZero: true }); + return parseSecretList(result.stdout); + }, + async push(ctx, secrets, config) { + const commonArgs = ['secret', 'set', '--app', app(config), ...targetArgs(config), ...orgVisibilityArgs(config)]; + for (const secret of secrets) { + const key = assertSecretKey(secret.key); + const value = secret.value ?? ctx.secret(key); + if (value === undefined) { + throw new Error(`No value provided for GitHub secret ${key}`); + } + ctx.log(`gh ${commonArgs.join(' ')} ${key} --body `); + await exec('gh', [...commonArgs, key, '--body', value], { + log: (message) => ctx.log(message), + throwOnNonZero: true, + }); + } + return { count: secrets.length }; + }, + setup: manualSetup({ + label: 'GitHub CLI', + vendorDocUrl: 'https://cli.github.com/manual/gh_secret', + steps: [ + 'Install GitHub CLI from the official docs', + 'Authenticate with a token that can manage the target secret scope: gh auth login', + 'For repository secrets, configure repo: owner/name', + 'For environment secrets, configure repo plus environment', + ], + }), +}); diff --git a/packages/secrets/github/tsconfig.json b/packages/secrets/github/tsconfig.json new file mode 100644 index 00000000..cf441478 --- /dev/null +++ b/packages/secrets/github/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { "outDir": "dist", "rootDir": "src" }, + "include": ["src/**/*"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 51c7c525..0b91aecd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1379,6 +1379,12 @@ importers: specifier: workspace:* version: link:../../core + packages/secrets/github: + dependencies: + '@profullstack/sh1pt-core': + specifier: workspace:* + version: link:../../core + packages/secrets/onepassword: dependencies: '@profullstack/sh1pt-core': From 3e87f0577116476ad95013036762e06a4e35b9f7 Mon Sep 17 00:00:00 2001 From: Phuc Nguyen Date: Thu, 11 Jun 2026 10:17:24 +0700 Subject: [PATCH 2/5] Tighten GitHub secrets provider edge cases --- packages/secrets/github/src/index.test.ts | 53 +++++++++++++++++++++++ packages/secrets/github/src/index.ts | 30 +++++++++++-- 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/packages/secrets/github/src/index.test.ts b/packages/secrets/github/src/index.test.ts index 7fcf3a00..6f2d3b18 100644 --- a/packages/secrets/github/src/index.test.ts +++ b/packages/secrets/github/src/index.test.ts @@ -19,6 +19,19 @@ beforeEach(() => { }); describe('GitHub secrets provider', () => { + it('checks GitHub CLI authentication before reporting a connection', async () => { + execMock.mockResolvedValue({ exitCode: 0, stdout: '', stderr: '' }); + + await expect(adapter.connect({ secret: () => undefined, log: () => {} }, { + repo: 'owner/repo', + })).resolves.toEqual({ accountId: 'owner/repo' }); + + expect(execMock).toHaveBeenCalledWith('gh', [ + 'auth', + 'status', + ], expect.objectContaining({ throwOnNonZero: true })); + }); + it('lists GitHub secret metadata without attempting to read values', async () => { execMock.mockResolvedValue({ exitCode: 0, @@ -49,6 +62,18 @@ describe('GitHub secrets provider', () => { ], expect.objectContaining({ throwOnNonZero: true })); }); + it('reports invalid GitHub CLI list output with an actionable error', async () => { + execMock.mockResolvedValue({ + exitCode: 0, + stderr: '', + stdout: 'warning: authentication needs attention\n[]', + }); + + await expect(adapter.pull({ secret: () => undefined, log: () => {} }, { + repo: 'owner/repo', + })).rejects.toThrow('Unable to parse `gh secret list --json` output as JSON'); + }); + it('sets repository environment secrets from provided values or the sh1pt vault', async () => { execMock.mockResolvedValue({ exitCode: 0, stdout: '', stderr: '' }); const logs: string[] = []; @@ -112,4 +137,32 @@ describe('GitHub secrets provider', () => { 'repo-a,repo-b', ]), expect.any(Object)); }); + + it('supports repository restrictions for user secrets', async () => { + execMock.mockResolvedValue({ exitCode: 0, stdout: '', stderr: '' }); + + await adapter.push({ secret: () => undefined, log: () => {} }, [ + { key: 'USER_TOKEN', value: 'token' }, + ], { + user: true, + repos: ['owner/repo'], + }); + + expect(execMock).toHaveBeenCalledWith('gh', expect.arrayContaining([ + '--user', + '--repos', + 'owner/repo', + ]), expect.any(Object)); + }); + + it('rejects organization-only visibility options for user secrets', async () => { + await expect(adapter.push({ secret: () => undefined, log: () => {} }, [ + { key: 'USER_TOKEN', value: 'token' }, + ], { + user: true, + noReposSelected: true, + })).rejects.toThrow('GitHub user secrets do not support noReposSelected'); + + expect(execMock).not.toHaveBeenCalled(); + }); }); diff --git a/packages/secrets/github/src/index.ts b/packages/secrets/github/src/index.ts index 3af45da5..eae02b63 100644 --- a/packages/secrets/github/src/index.ts +++ b/packages/secrets/github/src/index.ts @@ -45,8 +45,20 @@ function targetArgs(config: Config): string[] { return args; } -function orgVisibilityArgs(config: Config): string[] { +function scopeVisibilityArgs(config: Config): string[] { if (!text(config.org) && !config.user) return []; + + if (config.user) { + if (config.noReposSelected) { + throw new Error('GitHub user secrets do not support noReposSelected; omit it or use repos to restrict access'); + } + if (config.visibility) { + throw new Error('GitHub user secrets do not support visibility; use repos to restrict Codespaces access'); + } + if (config.repos?.length) return ['--repos', config.repos.join(',')]; + return []; + } + if (config.noReposSelected) return ['--no-repos-selected']; if (config.repos?.length) return ['--repos', config.repos.join(',')]; if (config.visibility) return ['--visibility', config.visibility]; @@ -64,7 +76,18 @@ function assertSecretKey(key: string): string { function parseSecretList(stdout: string): SecretRef[] { const body = stdout.trim(); if (!body) return []; - const entries = JSON.parse(body) as GitHubSecretListEntry[]; + let parsed: unknown; + try { + parsed = JSON.parse(body); + } catch (error) { + throw new Error('Unable to parse `gh secret list --json` output as JSON. Run `gh auth status` and retry.', { + cause: error, + }); + } + if (!Array.isArray(parsed)) { + throw new Error('Expected `gh secret list --json` to return a JSON array.'); + } + const entries = parsed as GitHubSecretListEntry[]; return entries.map((entry) => ({ key: entry.name, path: [ @@ -82,6 +105,7 @@ export default defineSecretProvider({ async connect(ctx, config) { const scope = text(config.repo) ?? text(config.org) ?? (config.user ? 'user' : 'current repository'); ctx.log(`gh auth status · scope=${scope}`); + await exec('gh', ['auth', 'status'], { log: (message) => ctx.log(message), throwOnNonZero: true }); return { accountId: scope }; }, async pull(ctx, config): Promise { @@ -99,7 +123,7 @@ export default defineSecretProvider({ return parseSecretList(result.stdout); }, async push(ctx, secrets, config) { - const commonArgs = ['secret', 'set', '--app', app(config), ...targetArgs(config), ...orgVisibilityArgs(config)]; + const commonArgs = ['secret', 'set', '--app', app(config), ...targetArgs(config), ...scopeVisibilityArgs(config)]; for (const secret of secrets) { const key = assertSecretKey(secret.key); const value = secret.value ?? ctx.secret(key); From 59ad846531a7a31c007cc26855a14271cac314ed Mon Sep 17 00:00:00 2001 From: Phuc Nguyen Date: Thu, 11 Jun 2026 10:32:43 +0700 Subject: [PATCH 3/5] Validate GitHub org secret scope options --- packages/secrets/github/src/index.test.ts | 21 ++++++++++++++++++++- packages/secrets/github/src/index.ts | 3 +++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/secrets/github/src/index.test.ts b/packages/secrets/github/src/index.test.ts index 6f2d3b18..a83937de 100644 --- a/packages/secrets/github/src/index.test.ts +++ b/packages/secrets/github/src/index.test.ts @@ -126,7 +126,6 @@ describe('GitHub secrets provider', () => { { key: 'ORG_TOKEN', value: 'token' }, ], { org: 'my-org', - visibility: 'selected', repos: ['repo-a', 'repo-b'], }); @@ -138,6 +137,26 @@ describe('GitHub secrets provider', () => { ]), expect.any(Object)); }); + it('rejects conflicting organization visibility and repository selection options', async () => { + await expect(adapter.push({ secret: () => undefined, log: () => {} }, [ + { key: 'ORG_TOKEN', value: 'token' }, + ], { + org: 'my-org', + visibility: 'all', + repos: ['repo-a'], + })).rejects.toThrow('GitHub organization secrets cannot combine visibility with explicit repository selection'); + + await expect(adapter.push({ secret: () => undefined, log: () => {} }, [ + { key: 'ORG_TOKEN', value: 'token' }, + ], { + org: 'my-org', + visibility: 'private', + noReposSelected: true, + })).rejects.toThrow('GitHub organization secrets cannot combine visibility with explicit repository selection'); + + expect(execMock).not.toHaveBeenCalled(); + }); + it('supports repository restrictions for user secrets', async () => { execMock.mockResolvedValue({ exitCode: 0, stdout: '', stderr: '' }); diff --git a/packages/secrets/github/src/index.ts b/packages/secrets/github/src/index.ts index eae02b63..dd8e7bea 100644 --- a/packages/secrets/github/src/index.ts +++ b/packages/secrets/github/src/index.ts @@ -59,6 +59,9 @@ function scopeVisibilityArgs(config: Config): string[] { return []; } + if (config.visibility && (config.noReposSelected || config.repos?.length)) { + throw new Error('GitHub organization secrets cannot combine visibility with explicit repository selection'); + } if (config.noReposSelected) return ['--no-repos-selected']; if (config.repos?.length) return ['--repos', config.repos.join(',')]; if (config.visibility) return ['--visibility', config.visibility]; From 5a06ba61348610fbf15639adfda0866184ffdd1d Mon Sep 17 00:00:00 2001 From: Phuc Nguyen Date: Thu, 11 Jun 2026 10:45:52 +0700 Subject: [PATCH 4/5] Handle user-scoped GitHub secrets app --- packages/secrets/github/src/index.test.ts | 11 +++++++++++ packages/secrets/github/src/index.ts | 6 ++++++ 2 files changed, 17 insertions(+) diff --git a/packages/secrets/github/src/index.test.ts b/packages/secrets/github/src/index.test.ts index a83937de..cb265751 100644 --- a/packages/secrets/github/src/index.test.ts +++ b/packages/secrets/github/src/index.test.ts @@ -168,6 +168,8 @@ describe('GitHub secrets provider', () => { }); expect(execMock).toHaveBeenCalledWith('gh', expect.arrayContaining([ + '--app', + 'codespaces', '--user', '--repos', 'owner/repo', @@ -184,4 +186,13 @@ describe('GitHub secrets provider', () => { expect(execMock).not.toHaveBeenCalled(); }); + + it('rejects non-Codespaces apps for user secrets', async () => { + await expect(adapter.pull({ secret: () => undefined, log: () => {} }, { + user: true, + app: 'actions', + })).rejects.toThrow('GitHub user secrets only support the Codespaces app'); + + expect(execMock).not.toHaveBeenCalled(); + }); }); diff --git a/packages/secrets/github/src/index.ts b/packages/secrets/github/src/index.ts index dd8e7bea..944859d4 100644 --- a/packages/secrets/github/src/index.ts +++ b/packages/secrets/github/src/index.ts @@ -28,6 +28,12 @@ function text(value: string | undefined): string | undefined { } function app(config: Config): GitHubSecretApp { + if (config.user) { + if (config.app && config.app !== 'codespaces') { + throw new Error('GitHub user secrets only support the Codespaces app'); + } + return 'codespaces'; + } return config.app ?? 'actions'; } From 9c11fbe090a2e48243a3f1dbf8526334266ee397 Mon Sep 17 00:00:00 2001 From: Phuc Nguyen Date: Thu, 11 Jun 2026 11:08:46 +0700 Subject: [PATCH 5/5] Validate GitHub secret target scopes --- packages/secrets/github/src/index.test.ts | 16 ++++++++++++++++ packages/secrets/github/src/index.ts | 7 +++++++ 2 files changed, 23 insertions(+) diff --git a/packages/secrets/github/src/index.test.ts b/packages/secrets/github/src/index.test.ts index cb265751..b8dbc5b9 100644 --- a/packages/secrets/github/src/index.test.ts +++ b/packages/secrets/github/src/index.test.ts @@ -195,4 +195,20 @@ describe('GitHub secrets provider', () => { expect(execMock).not.toHaveBeenCalled(); }); + + it('rejects mutually exclusive target scopes before calling gh', async () => { + await expect(adapter.pull({ secret: () => undefined, log: () => {} }, { + user: true, + repo: 'owner/repo', + })).rejects.toThrow('GitHub user secrets cannot be combined with repository, environment, or organization scope'); + + await expect(adapter.push({ secret: () => undefined, log: () => {} }, [ + { key: 'ORG_TOKEN', value: 'token' }, + ], { + org: 'my-org', + environment: 'production', + })).rejects.toThrow('GitHub organization secrets cannot be combined with repository or environment scope'); + + expect(execMock).not.toHaveBeenCalled(); + }); }); diff --git a/packages/secrets/github/src/index.ts b/packages/secrets/github/src/index.ts index 944859d4..857d3740 100644 --- a/packages/secrets/github/src/index.ts +++ b/packages/secrets/github/src/index.ts @@ -43,6 +43,13 @@ function targetArgs(config: Config): string[] { const environment = text(config.environment); const org = text(config.org); + if (config.user && (repo || environment || org)) { + throw new Error('GitHub user secrets cannot be combined with repository, environment, or organization scope'); + } + if (org && (repo || environment)) { + throw new Error('GitHub organization secrets cannot be combined with repository or environment scope'); + } + if (repo) args.push('--repo', repo); if (environment) args.push('--env', environment); if (org) args.push('--org', org);