Skip to content

Commit 7683a37

Browse files
jduttonclaude
andauthored
fix: move CI permissions and env to job-level scope (#141)
* fix: move permissions and env from workflow-level to job-level in generated CI Security scanners (e.g., SonarQube) flag workflow-level permissions as a vulnerability because they grant access to all jobs, including the gate job which only checks results and needs no special access. Move ci.permissions and ci.env from workflow-level to individual job definitions (validate and coverage jobs). The all-validation-passed gate job inherits no unnecessary permissions or env vars. Concurrency remains at workflow level as it governs the whole workflow. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: bump version to 0.19.1-rc.2, consolidate changelog Consolidate generate-workflow fixes (job-level permissions, indentation, build auto-detection) into a single changelog entry. Bump all packages from 0.19.1-rc.1 to 0.19.1-rc.2. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c49c20e commit 7683a37

14 files changed

Lines changed: 96 additions & 36 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818

1919
### Fixed
2020

21+
- **`generate-workflow` hardening**`ci.permissions` and `ci.env` are now applied to the `validate` and `coverage` jobs instead of at workflow level, so the `all-validation-passed` gate job no longer inherits unnecessary access (resolves SonarQube workflow-level permissions flag). Also fixed check script indentation and build step auto-detection (no longer matches on step name substring — checks `package.json` for a `build` script instead).
2122
- **`watch-pr` crashes on repos with non-main default branch**`fetchFileChanges` hardcoded `origin/main` for git diff, causing failures on repos using `master`, `develop`, or other base branches. Now uses the PR's actual base branch from GitHub metadata.
22-
- **`generate-workflow` check script indentation** — The `all-validation-passed` gate job's bash script had excessive indentation, now uses standard 2-space indent
23-
- **`generate-workflow` build step auto-detection** — No longer matches on step name substring (e.g., "dotnet build" falsely triggered `npm run build`). Now checks `package.json` for a `build` script instead.
2423

2524
## [0.19.0] - 2026-03-04
2625

docs/skill/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: vibe-validate
3-
version: 0.19.1-rc.1 # Tracks vibe-validate package version
3+
version: 0.19.1-rc.2 # Tracks vibe-validate package version
44
description: Expert guidance for vibe-validate, an LLM-optimized validation orchestration tool. Use when working with vibe-validate commands, configuration, pre-commit workflows, or validation orchestration in TypeScript projects.
55
model: claude-sonnet-4-5
66
tools:

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "vibe-validate",
3-
"version": "0.19.1-rc.1",
3+
"version": "0.19.1-rc.2",
44
"type": "module",
55
"private": true,
66
"description": "Git-aware validation orchestration for vibe coding (LLM-assisted development)",

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@vibe-validate/cli",
3-
"version": "0.19.1-rc.1",
3+
"version": "0.19.1-rc.2",
44
"description": "Command-line interface for vibe-validate validation framework",
55
"type": "module",
66
"main": "./dist/index.js",

packages/cli/src/commands/generate-workflow.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ interface GitHubWorkflowStep {
6666
interface GitHubWorkflowJob {
6767
name: string;
6868
'runs-on': string;
69+
permissions?: Record<string, string>;
6970
needs?: string[];
7071
if?: string;
7172
steps: GitHubWorkflowStep[];
@@ -76,6 +77,7 @@ interface GitHubWorkflowJob {
7677
node: string[];
7778
};
7879
};
80+
env?: Record<string, string>;
7981
}
8082

8183
/**
@@ -84,12 +86,10 @@ interface GitHubWorkflowJob {
8486
interface GitHubWorkflow {
8587
name: string;
8688
on: unknown;
87-
permissions?: Record<string, string>;
8889
concurrency?: {
8990
group: string;
9091
'cancel-in-progress'?: boolean;
9192
};
92-
env?: Record<string, string>;
9393
jobs: Record<string, GitHubWorkflowJob>;
9494
}
9595

@@ -267,15 +267,15 @@ function buildCommonJobSteps(params: {
267267
}
268268

269269
/**
270-
* Build the top-level workflow metadata (permissions, concurrency, env)
271-
* from the vibe-validate config.
270+
* Build the top-level workflow metadata (concurrency only).
271+
*
272+
* Permissions and env are applied at the job level, not the workflow level.
273+
* This avoids granting permissions to jobs that don't need them (e.g., the
274+
* gate job only checks results and needs no special access). SonarQube and
275+
* other security scanners flag workflow-level permissions as a vulnerability.
272276
*/
273-
function buildWorkflowMetadata(config: VibeValidateConfig): Pick<GitHubWorkflow, 'permissions' | 'concurrency' | 'env'> {
274-
const metadata: Pick<GitHubWorkflow, 'permissions' | 'concurrency' | 'env'> = {};
275-
276-
if (config.ci?.permissions) {
277-
metadata.permissions = config.ci.permissions;
278-
}
277+
function buildWorkflowMetadata(config: VibeValidateConfig): Pick<GitHubWorkflow, 'concurrency'> {
278+
const metadata: Pick<GitHubWorkflow, 'concurrency'> = {};
279279

280280
if (config.ci?.concurrency) {
281281
const concurrency: GitHubWorkflow['concurrency'] = {
@@ -287,6 +287,20 @@ function buildWorkflowMetadata(config: VibeValidateConfig): Pick<GitHubWorkflow,
287287
metadata.concurrency = concurrency;
288288
}
289289

290+
return metadata;
291+
}
292+
293+
/**
294+
* Build job-level metadata (permissions, env) from the vibe-validate config.
295+
* Applied to validate and coverage jobs, but NOT to the gate job.
296+
*/
297+
function buildJobMetadata(config: VibeValidateConfig): Pick<GitHubWorkflowJob, 'permissions' | 'env'> {
298+
const metadata: Pick<GitHubWorkflowJob, 'permissions' | 'env'> = {};
299+
300+
if (config.ci?.permissions) {
301+
metadata.permissions = config.ci.permissions;
302+
}
303+
290304
if (config.ci?.env) {
291305
metadata.env = config.ci.env;
292306
}
@@ -348,9 +362,12 @@ export function generateWorkflow(
348362
},
349363
});
350364

365+
const jobMetadata = buildJobMetadata(config);
366+
351367
jobs['validate'] = {
352368
name: 'Run vibe-validate validation',
353369
'runs-on': '${{ matrix.os }}',
370+
...jobMetadata,
354371
steps: jobSteps,
355372
strategy: {
356373
'fail-fast': matrixFailFast,
@@ -393,6 +410,7 @@ export function generateWorkflow(
393410
jobs['validate-coverage'] = {
394411
name: 'Run validation with coverage',
395412
'runs-on': DEFAULT_RUNNER_OS,
413+
...jobMetadata,
396414
steps: coverageSteps,
397415
};
398416
}

packages/cli/test/bin/wrapper.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { executeWrapperSync, type WrapperResultSync } from '../helpers/test-comm
1515
*/
1616

1717
// Test constants
18-
const EXPECTED_VERSION = '0.19.1-rc.1'; // BUMP_VERSION_UPDATE
18+
const EXPECTED_VERSION = '0.19.1-rc.2'; // BUMP_VERSION_UPDATE
1919
const REPO_ROOT = join(__dirname, '../../../..');
2020
const PACKAGES_CORE = join(__dirname, '../../../core');
2121

packages/cli/test/commands/generate-workflow.test.ts

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -751,44 +751,86 @@ describe('generate-workflow command', () => {
751751
});
752752
});
753753

754-
describe('F2: workflow-level env', () => {
755-
it('should add workflow-level env block when ci.env is set', () => {
754+
describe('F2: job-level env', () => {
755+
it('should add env to validate job when ci.env is set', () => {
756756
const config: VibeValidateConfig = {
757757
...baseMockConfig,
758758
ci: { env: { NODE_AUTH_TOKEN: '${{ secrets.NPM_TOKEN }}', CI: 'true' } },
759759
};
760760

761761
const workflow = generateAndParseWorkflow(config, { packageManager: 'pnpm' });
762762

763-
expect(workflow.env).toEqual({
763+
// env should be on the validate job, not at workflow level
764+
expect(workflow.env).toBeUndefined();
765+
expect(workflow.jobs['validate'].env).toEqual({
764766
NODE_AUTH_TOKEN: '${{ secrets.NPM_TOKEN }}',
765767
CI: 'true',
766768
});
767769
});
768770

771+
it('should NOT add env to gate job', () => {
772+
const config: VibeValidateConfig = {
773+
...baseMockConfig,
774+
ci: { env: { NODE_AUTH_TOKEN: '${{ secrets.NPM_TOKEN }}' } },
775+
};
776+
777+
const workflow = generateAndParseWorkflow(config, { packageManager: 'pnpm' });
778+
779+
expect(workflow.jobs['all-validation-passed'].env).toBeUndefined();
780+
});
781+
769782
it('should NOT add env block when ci.env is not set', () => {
770783
const workflow = generateAndParseWorkflow(baseMockConfig, { packageManager: 'pnpm' });
771784

772785
expect(workflow.env).toBeUndefined();
786+
expect(workflow.jobs['validate'].env).toBeUndefined();
773787
});
774788
});
775789

776-
describe('F3: permissions block', () => {
777-
it('should add permissions block when ci.permissions is set', () => {
790+
describe('F3: permissions block (job-level)', () => {
791+
it('should add permissions to validate job when ci.permissions is set', () => {
778792
const config: VibeValidateConfig = {
779793
...baseMockConfig,
780794
ci: { permissions: { contents: 'read', packages: 'write' } },
781795
};
782796

783797
const workflow = generateAndParseWorkflow(config, { packageManager: 'pnpm' });
784798

785-
expect(workflow.permissions).toEqual({ contents: 'read', packages: 'write' });
799+
// permissions should be on the validate job, not at workflow level
800+
expect(workflow.permissions).toBeUndefined();
801+
expect(workflow.jobs['validate'].permissions).toEqual({ contents: 'read', packages: 'write' });
802+
});
803+
804+
it('should NOT add permissions to gate job', () => {
805+
const config: VibeValidateConfig = {
806+
...baseMockConfig,
807+
ci: { permissions: { packages: 'read' } },
808+
};
809+
810+
const workflow = generateAndParseWorkflow(config, { packageManager: 'pnpm' });
811+
812+
expect(workflow.jobs['all-validation-passed'].permissions).toBeUndefined();
813+
});
814+
815+
it('should add permissions to coverage job when enabled', () => {
816+
const config: VibeValidateConfig = {
817+
...baseMockConfig,
818+
ci: { permissions: { packages: 'read' } },
819+
};
820+
821+
const workflow = generateAndParseWorkflow(config, {
822+
packageManager: 'pnpm',
823+
enableCoverage: true,
824+
});
825+
826+
expect(workflow.jobs['validate-coverage'].permissions).toEqual({ packages: 'read' });
786827
});
787828

788829
it('should NOT add permissions block when ci.permissions is not set', () => {
789830
const workflow = generateAndParseWorkflow(baseMockConfig, { packageManager: 'pnpm' });
790831

791832
expect(workflow.permissions).toBeUndefined();
833+
expect(workflow.jobs['validate'].permissions).toBeUndefined();
792834
});
793835
});
794836

@@ -996,7 +1038,7 @@ describe('generate-workflow command', () => {
9961038
});
9971039

9981040
describe('YAML property ordering', () => {
999-
it('should output workflow properties in order: name, on, permissions, concurrency, env, jobs', () => {
1041+
it('should output workflow properties in order: name, on, concurrency, jobs', () => {
10001042
const config: VibeValidateConfig = {
10011043
...baseMockConfig,
10021044
ci: {
@@ -1011,16 +1053,17 @@ describe('generate-workflow command', () => {
10111053
// Find positions of top-level keys in the YAML output
10121054
const namePos = workflowYaml.indexOf('\nname:');
10131055
const onPos = workflowYaml.includes('\n"on":') ? workflowYaml.indexOf('\n"on":') : workflowYaml.indexOf('\non:');
1014-
const permissionsPos = workflowYaml.indexOf('\npermissions:');
10151056
const concurrencyPos = workflowYaml.indexOf('\nconcurrency:');
1016-
const envPos = workflowYaml.indexOf('\nenv:');
10171057
const jobsPos = workflowYaml.indexOf('\njobs:');
10181058

10191059
expect(namePos).toBeLessThan(onPos);
1020-
expect(onPos).toBeLessThan(permissionsPos);
1021-
expect(permissionsPos).toBeLessThan(concurrencyPos);
1022-
expect(concurrencyPos).toBeLessThan(envPos);
1023-
expect(envPos).toBeLessThan(jobsPos);
1060+
expect(onPos).toBeLessThan(concurrencyPos);
1061+
expect(concurrencyPos).toBeLessThan(jobsPos);
1062+
1063+
// permissions and env should NOT appear at workflow level
1064+
// (they are on the validate job instead)
1065+
expect(workflowYaml).not.toMatch(/^permissions:/m);
1066+
expect(workflowYaml).not.toMatch(/^env:/m);
10241067
});
10251068
});
10261069
});

packages/config/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@vibe-validate/config",
3-
"version": "0.19.1-rc.1",
3+
"version": "0.19.1-rc.2",
44
"description": "Configuration system for vibe-validate with TypeScript-first design and config templates",
55
"type": "module",
66
"main": "./dist/index.js",

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@vibe-validate/core",
3-
"version": "0.19.1-rc.1",
3+
"version": "0.19.1-rc.2",
44
"description": "Core validation orchestration engine for vibe-validate",
55
"type": "module",
66
"main": "./dist/index.js",

packages/extractors/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@vibe-validate/extractors",
3-
"version": "0.19.1-rc.1",
3+
"version": "0.19.1-rc.2",
44
"description": "LLM-optimized error extractors for validation output",
55
"type": "module",
66
"main": "./dist/index.js",

0 commit comments

Comments
 (0)