@superblocksteam/shared 0.9602.0 → 0.9603.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/types/codeSecurity/index.d.ts +38 -0
- package/dist/types/codeSecurity/index.d.ts.map +1 -0
- package/dist/types/codeSecurity/index.js +10 -0
- package/dist/types/codeSecurity/index.js.map +1 -0
- package/dist/types/deployment/deployQueueStatus.d.ts +31 -0
- package/dist/types/deployment/deployQueueStatus.d.ts.map +1 -1
- package/dist/types/deployment/index.d.ts +1 -1
- package/dist/types/deployment/index.d.ts.map +1 -1
- package/dist/types/deployment/index.js.map +1 -1
- package/dist/types/index.d.ts +8 -5
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +8 -5
- package/dist/types/index.js.map +1 -1
- package/dist/types/pagination/index.d.ts +7 -0
- package/dist/types/pagination/index.d.ts.map +1 -0
- package/dist/types/pagination/index.js +3 -0
- package/dist/types/pagination/index.js.map +1 -0
- package/dist/types/publishPipeline/index.d.ts +78 -0
- package/dist/types/publishPipeline/index.d.ts.map +1 -0
- package/dist/types/publishPipeline/index.js +56 -0
- package/dist/types/publishPipeline/index.js.map +1 -0
- package/dist/types/publishPipeline/plan.d.ts +18 -0
- package/dist/types/publishPipeline/plan.d.ts.map +1 -0
- package/dist/types/publishPipeline/plan.js +110 -0
- package/dist/types/publishPipeline/plan.js.map +1 -0
- package/dist/types/publishPipeline/plan.test.d.ts +2 -0
- package/dist/types/publishPipeline/plan.test.d.ts.map +1 -0
- package/dist/types/publishPipeline/plan.test.js +181 -0
- package/dist/types/publishPipeline/plan.test.js.map +1 -0
- package/dist/types/publishPipeline/stepIds.test.d.ts +2 -0
- package/dist/types/publishPipeline/stepIds.test.d.ts.map +1 -0
- package/dist/types/publishPipeline/stepIds.test.js +34 -0
- package/dist/types/publishPipeline/stepIds.test.js.map +1 -0
- package/dist/types/rbac/index.d.ts +2 -2
- package/dist/types/rbac/index.d.ts.map +1 -1
- package/dist/types/rbac/index.js +2 -2
- package/dist/types/rbac/index.js.map +1 -1
- package/dist-esm/types/codeSecurity/index.d.ts +38 -0
- package/dist-esm/types/codeSecurity/index.d.ts.map +1 -0
- package/dist-esm/types/codeSecurity/index.js +7 -0
- package/dist-esm/types/codeSecurity/index.js.map +1 -0
- package/dist-esm/types/deployment/deployQueueStatus.d.ts +31 -0
- package/dist-esm/types/deployment/deployQueueStatus.d.ts.map +1 -1
- package/dist-esm/types/deployment/index.d.ts +1 -1
- package/dist-esm/types/deployment/index.d.ts.map +1 -1
- package/dist-esm/types/deployment/index.js.map +1 -1
- package/dist-esm/types/index.d.ts +8 -5
- package/dist-esm/types/index.d.ts.map +1 -1
- package/dist-esm/types/index.js +8 -5
- package/dist-esm/types/index.js.map +1 -1
- package/dist-esm/types/pagination/index.d.ts +7 -0
- package/dist-esm/types/pagination/index.d.ts.map +1 -0
- package/dist-esm/types/pagination/index.js +2 -0
- package/dist-esm/types/pagination/index.js.map +1 -0
- package/dist-esm/types/publishPipeline/index.d.ts +78 -0
- package/dist-esm/types/publishPipeline/index.d.ts.map +1 -0
- package/dist-esm/types/publishPipeline/index.js +39 -0
- package/dist-esm/types/publishPipeline/index.js.map +1 -0
- package/dist-esm/types/publishPipeline/plan.d.ts +18 -0
- package/dist-esm/types/publishPipeline/plan.d.ts.map +1 -0
- package/dist-esm/types/publishPipeline/plan.js +104 -0
- package/dist-esm/types/publishPipeline/plan.js.map +1 -0
- package/dist-esm/types/publishPipeline/plan.test.d.ts +2 -0
- package/dist-esm/types/publishPipeline/plan.test.d.ts.map +1 -0
- package/dist-esm/types/publishPipeline/plan.test.js +179 -0
- package/dist-esm/types/publishPipeline/plan.test.js.map +1 -0
- package/dist-esm/types/publishPipeline/stepIds.test.d.ts +2 -0
- package/dist-esm/types/publishPipeline/stepIds.test.d.ts.map +1 -0
- package/dist-esm/types/publishPipeline/stepIds.test.js +32 -0
- package/dist-esm/types/publishPipeline/stepIds.test.js.map +1 -0
- package/dist-esm/types/rbac/index.d.ts +2 -2
- package/dist-esm/types/rbac/index.d.ts.map +1 -1
- package/dist-esm/types/rbac/index.js +2 -2
- package/dist-esm/types/rbac/index.js.map +1 -1
- package/package.json +1 -1
- package/src/types/codeSecurity/index.ts +51 -0
- package/src/types/deployment/deployQueueStatus.ts +35 -0
- package/src/types/deployment/index.ts +8 -1
- package/src/types/index.ts +8 -5
- package/src/types/pagination/index.ts +7 -0
- package/src/types/publishPipeline/index.ts +104 -0
- package/src/types/publishPipeline/plan.test.ts +200 -0
- package/src/types/publishPipeline/plan.ts +114 -0
- package/src/types/publishPipeline/stepIds.test.ts +35 -0
- package/src/types/rbac/index.ts +2 -2
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Wire contract for the code-security dependency-scan endpoints, shared by the
|
|
2
|
+
// server handlers and the UI so the two can't drift. The taxonomy below is
|
|
3
|
+
// mirrored by the PackageVulnerability DB entity (its columns import these).
|
|
4
|
+
|
|
5
|
+
export type Ecosystem = 'npm';
|
|
6
|
+
export type VulnerabilitySource = 'osv';
|
|
7
|
+
export type AdvisoryKind = 'vulnerability' | 'malware';
|
|
8
|
+
|
|
9
|
+
// Qualitative bands, ascending, then `unknown` (no scorable CVSS vector and no
|
|
10
|
+
// qualitative rating). Doubles as the runtime allowlist for untrusted severity input.
|
|
11
|
+
export const SEVERITY_LEVELS = ['none', 'low', 'medium', 'high', 'critical', 'unknown'] as const;
|
|
12
|
+
export type SeverityLevel = (typeof SEVERITY_LEVELS)[number];
|
|
13
|
+
|
|
14
|
+
// needs_review: version couldn't be pinned, so a "clear" verdict would be unearned.
|
|
15
|
+
export type DependencyScanStatus = 'clear' | 'needs_review' | 'unresolved';
|
|
16
|
+
|
|
17
|
+
export interface AdvisoryDetail {
|
|
18
|
+
advisoryId: string;
|
|
19
|
+
source: VulnerabilitySource;
|
|
20
|
+
kind: AdvisoryKind;
|
|
21
|
+
severity: SeverityLevel;
|
|
22
|
+
summary: string | null;
|
|
23
|
+
packageName: string;
|
|
24
|
+
currentVersion: string;
|
|
25
|
+
fixedVersion: string | null;
|
|
26
|
+
lowConfidence: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PerAppVuln {
|
|
30
|
+
appId: string;
|
|
31
|
+
name: string;
|
|
32
|
+
owner: string | null;
|
|
33
|
+
published: boolean;
|
|
34
|
+
visibility: 'private' | 'public';
|
|
35
|
+
status: DependencyScanStatus;
|
|
36
|
+
totalCount: number;
|
|
37
|
+
topSeverity: SeverityLevel | null;
|
|
38
|
+
severityCounts: Partial<Record<SeverityLevel, number>>;
|
|
39
|
+
advisories: AdvisoryDetail[];
|
|
40
|
+
lowConfidence: boolean;
|
|
41
|
+
// Caller may undeploy this app (APPLICATIONS:DEPLOY). Set per-request on the
|
|
42
|
+
// list handler — not part of the org scan cache.
|
|
43
|
+
canDeploy: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface DependencyScanSummary {
|
|
47
|
+
overallSeverityCounts: Partial<Record<SeverityLevel, number>>;
|
|
48
|
+
publishedPublicWithVulns: number;
|
|
49
|
+
publishedWithVulns: number;
|
|
50
|
+
totalProjects: number;
|
|
51
|
+
}
|
|
@@ -9,3 +9,38 @@ export enum DeployQueueStatus {
|
|
|
9
9
|
|
|
10
10
|
/** Status returned by the deploy-status polling endpoint. "none" means no queue entry exists. */
|
|
11
11
|
export type DeployStatusResponseStatus = 'none' | DeployQueueStatus;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Identifies a sub-stage of the native-DB publish lifecycle that the UI
|
|
15
|
+
* surfaces independently of the overall deploy queue status. `provisioning`
|
|
16
|
+
* tracks `ensure_database` work; `migrating` tracks `migrate_schema` work.
|
|
17
|
+
*/
|
|
18
|
+
export type LifecycleStageKind = 'provisioning' | 'migrating';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Aggregated state of a lifecycle stage across all prod bindings for a
|
|
22
|
+
* commit. Worst-case wins (any failed → failed, any in_progress →
|
|
23
|
+
* in_progress, all complete → complete, otherwise pending).
|
|
24
|
+
*/
|
|
25
|
+
export type LifecycleStageState = 'pending' | 'in_progress' | 'complete' | 'failed';
|
|
26
|
+
|
|
27
|
+
export interface LifecycleStageProgress {
|
|
28
|
+
kind: LifecycleStageKind;
|
|
29
|
+
state: LifecycleStageState;
|
|
30
|
+
errorCode?: string;
|
|
31
|
+
errorMessage?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Native-DB lifecycle progress for a publish. Sent alongside the deploy
|
|
36
|
+
* queue status so the UI can render provisioning and migration steps.
|
|
37
|
+
* Omitted from responses when no native-DB requirements exist for the commit.
|
|
38
|
+
*
|
|
39
|
+
* `stages` lists only lifecycle work queued for THIS commit — republishes
|
|
40
|
+
* that reuse an already-provisioned / already-migrated DB may return
|
|
41
|
+
* `hasNativeDb: true` with an empty `stages` array.
|
|
42
|
+
*/
|
|
43
|
+
export interface DeployLifecycleProgress {
|
|
44
|
+
hasNativeDb: boolean;
|
|
45
|
+
stages: LifecycleStageProgress[];
|
|
46
|
+
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
DeployQueueStatus,
|
|
3
|
+
type DeployLifecycleProgress,
|
|
4
|
+
type DeployStatusResponseStatus,
|
|
5
|
+
type LifecycleStageKind,
|
|
6
|
+
type LifecycleStageProgress,
|
|
7
|
+
type LifecycleStageState
|
|
8
|
+
} from './deployQueueStatus.js';
|
|
2
9
|
|
|
3
10
|
export interface DeploymentDto {
|
|
4
11
|
commitId: string | null;
|
package/src/types/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from './audit/index.js';
|
|
|
6
6
|
export * from './auth/index.js';
|
|
7
7
|
export * from './billing/index.js';
|
|
8
8
|
export * from './ai/index.js';
|
|
9
|
+
export * from './codeSecurity/index.js';
|
|
9
10
|
export * from './commit/index.js';
|
|
10
11
|
export * from './branch/index.js';
|
|
11
12
|
export * from './common/index.js';
|
|
@@ -22,22 +23,24 @@ export * from './lock/index.js';
|
|
|
22
23
|
export * from './observability/index.js';
|
|
23
24
|
export * from './organization/index.js';
|
|
24
25
|
export * from './page/index.js';
|
|
26
|
+
export * from './pagination/index.js';
|
|
25
27
|
export * from './policyGate/index.js';
|
|
26
28
|
export * from './plugin/index.js';
|
|
27
29
|
export * from './profile/index.js';
|
|
30
|
+
export * from './publishPipeline/index.js';
|
|
28
31
|
export * from './rbac/index.js';
|
|
32
|
+
export * from './remoteCommit/index.js';
|
|
33
|
+
export * from './repoConnection/index.js';
|
|
34
|
+
export * from './repository/index.js';
|
|
29
35
|
export * from './response/index.js';
|
|
30
36
|
export * from './reviewPolicy/index.js';
|
|
31
|
-
export * from './remoteCommit/index.js';
|
|
32
37
|
export * from './scim/index.js';
|
|
33
38
|
export * from './signingKeyRotationJob/index.js';
|
|
39
|
+
export * from './template/index.js';
|
|
40
|
+
export * from './uiBlock/index.js';
|
|
34
41
|
export * from './user/index.js';
|
|
35
42
|
export * from './userToken/index.js';
|
|
36
43
|
export * from './version/index.js';
|
|
37
|
-
export * from './repoConnection/index.js';
|
|
38
|
-
export * from './repository/index.js';
|
|
39
44
|
export * from './git/index.js';
|
|
40
|
-
export * from './template/index.js';
|
|
41
45
|
export * from './metadata/index.js';
|
|
42
46
|
export * from './resource/index.js';
|
|
43
|
-
export * from './uiBlock/index.js';
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared contract for the publish pipeline: one ordered list of steps the
|
|
3
|
+
* server computes for the UI status poll (GET publish-pipeline). Domains
|
|
4
|
+
* project into these types; rich payloads ride in opaque `detail` and are only
|
|
5
|
+
* read by that domain's UI renderer.
|
|
6
|
+
*
|
|
7
|
+
* Status-only — it does not start execution and does not gate deploy.
|
|
8
|
+
* Mutations (publish-readiness/start, POST deployments) start work and keep
|
|
9
|
+
* their own authoritative checks (build, policy, database lifecycle reconcile);
|
|
10
|
+
* this plan only reflects progress for the rail.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Which domain owns a step. Extend as new gates are added. */
|
|
14
|
+
export type PublishStepKind = 'build' | 'database_migration' | 'database_provisioning' | 'policy_gates' | 'publish';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Domain-agnostic status. Ordered here by roll-up precedence (worst first).
|
|
18
|
+
* `blocked` means this step's own check failed (remediation). Waiting on an
|
|
19
|
+
* earlier step is `pending` with optional `blockedBy`.
|
|
20
|
+
* `skipped` is a visible N/A row (e.g. Build when the app has no native-git remote).
|
|
21
|
+
*/
|
|
22
|
+
export type PublishStepStatus =
|
|
23
|
+
| 'failed'
|
|
24
|
+
| 'blocked'
|
|
25
|
+
| 'action_required'
|
|
26
|
+
| 'stale'
|
|
27
|
+
| 'in_progress'
|
|
28
|
+
| 'pending'
|
|
29
|
+
| 'advisory'
|
|
30
|
+
| 'passed'
|
|
31
|
+
| 'skipped';
|
|
32
|
+
|
|
33
|
+
export type BuildPublishStepDetail = {
|
|
34
|
+
buildStatus: string | null;
|
|
35
|
+
error?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type PublishDeployStepDetail = {
|
|
39
|
+
error?: string;
|
|
40
|
+
isLive?: boolean;
|
|
41
|
+
queueStatus: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type PublishStep = {
|
|
45
|
+
/** Stable unique id in the plan (a PublishStepId; typed as string so unknown ids still flow through). */
|
|
46
|
+
id: string;
|
|
47
|
+
kind: PublishStepKind;
|
|
48
|
+
status: PublishStepStatus;
|
|
49
|
+
/** Human label for the step row (server-derived). */
|
|
50
|
+
displayName: string;
|
|
51
|
+
/** Static help text under the display name (not an error). */
|
|
52
|
+
description?: string;
|
|
53
|
+
/** Short subtitle / one-line error. */
|
|
54
|
+
summary?: string;
|
|
55
|
+
/** When status is pending and waiting on earlier steps. */
|
|
56
|
+
blockedBy?: PublishStepKind[];
|
|
57
|
+
/** Machine code when blocked/failed (e.g. POLICY_GATE_BLOCKED). */
|
|
58
|
+
blockingReason?: string;
|
|
59
|
+
/** Opaque domain payload; generic pipeline must not read this. */
|
|
60
|
+
detail?: unknown;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type PublishPlanTarget = {
|
|
64
|
+
applicationId: string;
|
|
65
|
+
commitId: string;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type PublishPlan = {
|
|
69
|
+
status: PublishStepStatus;
|
|
70
|
+
steps: PublishStep[];
|
|
71
|
+
target: PublishPlanTarget;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Single source of truth for publish-plan step ids. These are wire values: the
|
|
76
|
+
* server emits them on each step and the UI matches on them to wire custom
|
|
77
|
+
* detail panels. Keys are lowerCamelCase; values are the lowercase snake_case
|
|
78
|
+
* wire literals. Add new steps here and they become available to both sides.
|
|
79
|
+
*/
|
|
80
|
+
export const PublishStepId = {
|
|
81
|
+
build: 'build',
|
|
82
|
+
databaseMigration: 'database_migration',
|
|
83
|
+
databaseProvisioning: 'database_provisioning',
|
|
84
|
+
policyGatesBeforeBuild: 'policy_gates:before_build',
|
|
85
|
+
policyGatesBeforeDeploy: 'policy_gates:before_deploy',
|
|
86
|
+
publish: 'publish'
|
|
87
|
+
} as const;
|
|
88
|
+
|
|
89
|
+
export type PublishStepId = (typeof PublishStepId)[keyof typeof PublishStepId];
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Fixed spine order. Composer filters to steps that were emitted; order is always
|
|
93
|
+
* this sequence. Policy may emit before_build and/or before_deploy.
|
|
94
|
+
*/
|
|
95
|
+
export const PUBLISH_STEP_SPINE: readonly PublishStepId[] = [
|
|
96
|
+
PublishStepId.policyGatesBeforeBuild,
|
|
97
|
+
PublishStepId.build,
|
|
98
|
+
PublishStepId.policyGatesBeforeDeploy,
|
|
99
|
+
PublishStepId.databaseProvisioning,
|
|
100
|
+
PublishStepId.databaseMigration,
|
|
101
|
+
PublishStepId.publish
|
|
102
|
+
] as const;
|
|
103
|
+
|
|
104
|
+
export * from './plan.js';
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { PublishStep, PublishStepKind, PublishStepStatus } from './index.js';
|
|
4
|
+
import { applyBlockedByEligibility, buildPublishPlan, orderPublishSteps, rollUpPublishPlan } from './plan.js';
|
|
5
|
+
|
|
6
|
+
const TARGET = { applicationId: 'app-1', commitId: 'commit-1' };
|
|
7
|
+
|
|
8
|
+
function step(overrides: Partial<PublishStep> & Pick<PublishStep, 'id'>): PublishStep {
|
|
9
|
+
return {
|
|
10
|
+
displayName: overrides.displayName ?? overrides.id,
|
|
11
|
+
kind: overrides.kind ?? 'build',
|
|
12
|
+
status: overrides.status ?? 'passed',
|
|
13
|
+
...overrides
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('rollUpPublishPlan', () => {
|
|
18
|
+
test('an empty plan is passed', () => {
|
|
19
|
+
expect(rollUpPublishPlan([])).toBe('passed');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('the worst status wins by precedence', () => {
|
|
23
|
+
expect(
|
|
24
|
+
rollUpPublishPlan([step({ id: 'a', status: 'passed' }), step({ id: 'b', status: 'advisory' }), step({ id: 'c', status: 'blocked' })])
|
|
25
|
+
).toBe('blocked');
|
|
26
|
+
expect(rollUpPublishPlan([step({ id: 'a', status: 'in_progress' }), step({ id: 'b', status: 'failed' })])).toBe('failed');
|
|
27
|
+
expect(rollUpPublishPlan([step({ id: 'a', status: 'advisory' }), step({ id: 'b', status: 'passed' })])).toBe('advisory');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('skipped never masks a real status', () => {
|
|
31
|
+
expect(rollUpPublishPlan([step({ id: 'a', status: 'skipped' }), step({ id: 'b', status: 'passed' })])).toBe('passed');
|
|
32
|
+
expect(rollUpPublishPlan([step({ id: 'a', status: 'skipped' })])).toBe('skipped');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('precedence order is exhaustive and worst-first', () => {
|
|
36
|
+
const precedence: PublishStepStatus[] = [
|
|
37
|
+
'failed',
|
|
38
|
+
'blocked',
|
|
39
|
+
'action_required',
|
|
40
|
+
'stale',
|
|
41
|
+
'in_progress',
|
|
42
|
+
'pending',
|
|
43
|
+
'advisory',
|
|
44
|
+
'passed',
|
|
45
|
+
'skipped'
|
|
46
|
+
];
|
|
47
|
+
for (let i = 0; i < precedence.length; i += 1) {
|
|
48
|
+
const worse = precedence[i];
|
|
49
|
+
const better = precedence[i + 1];
|
|
50
|
+
const steps = better
|
|
51
|
+
? [step({ id: 'better', status: better }), step({ id: 'worse', status: worse })]
|
|
52
|
+
: [step({ id: 'only', status: worse })];
|
|
53
|
+
expect(rollUpPublishPlan(steps)).toBe(worse);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('orderPublishSteps', () => {
|
|
59
|
+
test('orders steps by the publish spine regardless of input order', () => {
|
|
60
|
+
const ordered = orderPublishSteps([
|
|
61
|
+
step({ id: 'publish', kind: 'publish' }),
|
|
62
|
+
step({ id: 'policy_gates:before_build', kind: 'policy_gates' }),
|
|
63
|
+
step({ id: 'build', kind: 'build' }),
|
|
64
|
+
step({ id: 'database_provisioning', kind: 'database_provisioning' }),
|
|
65
|
+
step({ id: 'policy_gates:before_deploy', kind: 'policy_gates' }),
|
|
66
|
+
step({ id: 'database_migration', kind: 'database_migration' })
|
|
67
|
+
]);
|
|
68
|
+
expect(ordered.map((s) => s.id)).toEqual([
|
|
69
|
+
'policy_gates:before_build',
|
|
70
|
+
'build',
|
|
71
|
+
'policy_gates:before_deploy',
|
|
72
|
+
'database_provisioning',
|
|
73
|
+
'database_migration',
|
|
74
|
+
'publish'
|
|
75
|
+
]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('omits ids that are not present', () => {
|
|
79
|
+
const ordered = orderPublishSteps([step({ id: 'build', kind: 'build' }), step({ id: 'publish', kind: 'publish' })]);
|
|
80
|
+
expect(ordered.map((s) => s.id)).toEqual(['build', 'publish']);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('applyBlockedByEligibility', () => {
|
|
85
|
+
test('marks later pending steps as blockedBy earlier unclear steps', () => {
|
|
86
|
+
const steps = applyBlockedByEligibility([
|
|
87
|
+
step({ id: 'build', kind: 'build', status: 'in_progress' }),
|
|
88
|
+
step({ id: 'publish', kind: 'publish', status: 'pending' })
|
|
89
|
+
]);
|
|
90
|
+
expect(steps[1].blockedBy).toEqual<PublishStepKind[]>(['build']);
|
|
91
|
+
expect(steps[1].status).toBe('pending');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('does not overwrite a step that already failed', () => {
|
|
95
|
+
const steps = applyBlockedByEligibility([
|
|
96
|
+
step({ id: 'build', kind: 'build', status: 'failed' }),
|
|
97
|
+
step({ id: 'publish', kind: 'publish', status: 'failed' })
|
|
98
|
+
]);
|
|
99
|
+
expect(steps[1].blockedBy).toBeUndefined();
|
|
100
|
+
expect(steps[1].status).toBe('failed');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('holds an in_progress publish as pending until earlier policy gates clear', () => {
|
|
104
|
+
const steps = applyBlockedByEligibility([
|
|
105
|
+
step({
|
|
106
|
+
id: 'policy_gates:before_deploy',
|
|
107
|
+
kind: 'policy_gates',
|
|
108
|
+
status: 'in_progress',
|
|
109
|
+
displayName: 'Policy Agents'
|
|
110
|
+
}),
|
|
111
|
+
step({
|
|
112
|
+
id: 'publish',
|
|
113
|
+
kind: 'publish',
|
|
114
|
+
status: 'in_progress',
|
|
115
|
+
displayName: 'Publishing…'
|
|
116
|
+
})
|
|
117
|
+
]);
|
|
118
|
+
expect(steps[1].status).toBe('pending');
|
|
119
|
+
expect(steps[1].blockedBy).toEqual<PublishStepKind[]>(['policy_gates']);
|
|
120
|
+
expect(steps[1].displayName).toBe('Publish');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('clears blockedBy when all earlier steps are clear', () => {
|
|
124
|
+
const steps = applyBlockedByEligibility([
|
|
125
|
+
step({ id: 'build', kind: 'build', status: 'passed' }),
|
|
126
|
+
step({ id: 'publish', kind: 'publish', status: 'pending', blockedBy: ['build'] })
|
|
127
|
+
]);
|
|
128
|
+
expect(steps[1].blockedBy).toBeUndefined();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('does not block policy gates on an unfinished build (they run concurrently)', () => {
|
|
132
|
+
const steps = applyBlockedByEligibility([
|
|
133
|
+
step({ id: 'build', kind: 'build', status: 'in_progress' }),
|
|
134
|
+
step({
|
|
135
|
+
id: 'policy_gates:before_deploy',
|
|
136
|
+
kind: 'policy_gates',
|
|
137
|
+
status: 'in_progress',
|
|
138
|
+
displayName: 'Policy Agents'
|
|
139
|
+
})
|
|
140
|
+
]);
|
|
141
|
+
expect(steps[1].blockedBy).toBeUndefined();
|
|
142
|
+
expect(steps[1].status).toBe('in_progress');
|
|
143
|
+
expect(steps[1].displayName).toBe('Policy Agents');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('keeps a pending policy gate idle rather than waiting on the build', () => {
|
|
147
|
+
const steps = applyBlockedByEligibility([
|
|
148
|
+
step({ id: 'build', kind: 'build', status: 'in_progress' }),
|
|
149
|
+
step({ id: 'policy_gates:before_deploy', kind: 'policy_gates', status: 'pending' })
|
|
150
|
+
]);
|
|
151
|
+
expect(steps[1].blockedBy).toBeUndefined();
|
|
152
|
+
expect(steps[1].status).toBe('pending');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('still blocks publish on both the build and policy gates', () => {
|
|
156
|
+
const steps = applyBlockedByEligibility([
|
|
157
|
+
step({ id: 'build', kind: 'build', status: 'in_progress' }),
|
|
158
|
+
step({ id: 'policy_gates:before_deploy', kind: 'policy_gates', status: 'in_progress' }),
|
|
159
|
+
step({ id: 'publish', kind: 'publish', status: 'pending' })
|
|
160
|
+
]);
|
|
161
|
+
expect(steps[2].blockedBy).toEqual<PublishStepKind[]>(['build', 'policy_gates']);
|
|
162
|
+
expect(steps[2].status).toBe('pending');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('lists each unclear kind at most once in blockedBy', () => {
|
|
166
|
+
// Two earlier steps can share a kind (e.g. multiple database_* rows). The
|
|
167
|
+
// rail waits on kinds, not step ids, so blockedBy must not repeat a kind.
|
|
168
|
+
const steps = applyBlockedByEligibility([
|
|
169
|
+
step({
|
|
170
|
+
id: 'database_provisioning:a',
|
|
171
|
+
kind: 'database_provisioning',
|
|
172
|
+
status: 'in_progress'
|
|
173
|
+
}),
|
|
174
|
+
step({
|
|
175
|
+
id: 'database_provisioning:b',
|
|
176
|
+
kind: 'database_provisioning',
|
|
177
|
+
status: 'pending'
|
|
178
|
+
}),
|
|
179
|
+
step({ id: 'publish', kind: 'publish', status: 'pending' })
|
|
180
|
+
]);
|
|
181
|
+
expect(steps[2].blockedBy).toEqual<PublishStepKind[]>(['database_provisioning']);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('buildPublishPlan', () => {
|
|
186
|
+
test('orders, applies eligibility, and rolls up', () => {
|
|
187
|
+
const plan = buildPublishPlan(TARGET, [
|
|
188
|
+
step({ id: 'publish', kind: 'publish', status: 'pending' }),
|
|
189
|
+
step({ id: 'build', kind: 'build', status: 'in_progress' })
|
|
190
|
+
]);
|
|
191
|
+
expect(plan.target).toEqual(TARGET);
|
|
192
|
+
expect(plan.steps.map((s) => s.id)).toEqual(['build', 'publish']);
|
|
193
|
+
expect(plan.steps[1].blockedBy).toEqual(['build']);
|
|
194
|
+
expect(plan.status).toBe('in_progress');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('an empty plan is passed', () => {
|
|
198
|
+
expect(buildPublishPlan(TARGET, []).status).toBe('passed');
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { PublishPlan, PublishPlanTarget, PublishStep, PublishStepKind, PublishStepStatus } from './index.js';
|
|
2
|
+
import { PUBLISH_STEP_SPINE } from './index.js';
|
|
3
|
+
|
|
4
|
+
/** Worst-first. When summarizing many steps into one status, first match wins. */
|
|
5
|
+
const STATUS_PRECEDENCE: PublishStepStatus[] = [
|
|
6
|
+
'failed',
|
|
7
|
+
'blocked',
|
|
8
|
+
'action_required',
|
|
9
|
+
'stale',
|
|
10
|
+
'in_progress',
|
|
11
|
+
'pending',
|
|
12
|
+
'advisory',
|
|
13
|
+
'passed',
|
|
14
|
+
'skipped'
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const CLEAR_STATUSES = new Set<PublishStepStatus>(['passed', 'advisory', 'skipped']);
|
|
18
|
+
|
|
19
|
+
/** Collapse ordered steps into the single status shown on the plan. Empty = passed. */
|
|
20
|
+
export function rollUpPublishPlan(steps: PublishStep[]): PublishStepStatus {
|
|
21
|
+
const present = new Set(steps.map((s) => s.status));
|
|
22
|
+
return STATUS_PRECEDENCE.find((s) => present.has(s)) ?? 'passed';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Order steps by the fixed spine; unknown ids are appended in input order. */
|
|
26
|
+
export function orderPublishSteps(steps: PublishStep[]): PublishStep[] {
|
|
27
|
+
const byId = new Map(steps.map((s) => [s.id, s]));
|
|
28
|
+
const ordered: PublishStep[] = [];
|
|
29
|
+
for (const id of PUBLISH_STEP_SPINE) {
|
|
30
|
+
const step = byId.get(id);
|
|
31
|
+
if (step) {
|
|
32
|
+
ordered.push(step);
|
|
33
|
+
byId.delete(id);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
for (const step of steps) {
|
|
37
|
+
if (byId.has(step.id)) {
|
|
38
|
+
ordered.push(step);
|
|
39
|
+
byId.delete(step.id);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return ordered;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isClear(status: PublishStepStatus): boolean {
|
|
46
|
+
return CLEAR_STATUSES.has(status);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Idle label when a step is held waiting on earlier work (not actively running). */
|
|
50
|
+
const PENDING_DISPLAY_NAMES: Record<PublishStepKind, string> = {
|
|
51
|
+
build: 'Build',
|
|
52
|
+
database_migration: 'Database migration',
|
|
53
|
+
database_provisioning: 'Database provisioning',
|
|
54
|
+
policy_gates: 'Policy Agents',
|
|
55
|
+
publish: 'Publish'
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Kinds that execute concurrently and therefore must not appear as each other's
|
|
60
|
+
* prerequisite. The SABS build and the policy agents both key off the commit and
|
|
61
|
+
* run in parallel — policy agents analyze the committed code directly and do not
|
|
62
|
+
* wait for the build artifact — so neither should show "Waiting on" the other in
|
|
63
|
+
* the rail. They both still gate the final publish.
|
|
64
|
+
*/
|
|
65
|
+
const CONCURRENT_KINDS = new Set<PublishStepKind>(['build', 'policy_gates']);
|
|
66
|
+
|
|
67
|
+
/** An earlier unclear step gates a later step unless the two run concurrently. */
|
|
68
|
+
function earlierKindBlocks(earlier: PublishStepKind, current: PublishStepKind): boolean {
|
|
69
|
+
return !(CONCURRENT_KINDS.has(earlier) && CONCURRENT_KINDS.has(current));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* For each step that is not yet clear, set `blockedBy` to kinds of earlier
|
|
74
|
+
* steps that are not clear and that actually gate it (concurrent steps such as
|
|
75
|
+
* build and policy gates do not gate each other). Premature `in_progress` (e.g.
|
|
76
|
+
* deploy already QUEUED while policy gates still run) is held as `pending` so
|
|
77
|
+
* the rail does not show "Publishing…" ahead of unfinished prerequisites. Failed
|
|
78
|
+
* and other terminal statuses are left alone. Clears blockedBy when the gating
|
|
79
|
+
* steps clear.
|
|
80
|
+
*/
|
|
81
|
+
export function applyBlockedByEligibility(steps: PublishStep[]): PublishStep[] {
|
|
82
|
+
// Kind set (not a list): multiple earlier steps can share a kind, but the
|
|
83
|
+
// rail waits on kinds — blockedBy must not repeat the same kind.
|
|
84
|
+
const unclearKinds = new Set<PublishStepKind>();
|
|
85
|
+
return steps.map((step) => {
|
|
86
|
+
const blockers = [...unclearKinds].filter((kind) => earlierKindBlocks(kind, step.kind));
|
|
87
|
+
if (blockers.length > 0 && (step.status === 'pending' || step.status === 'in_progress')) {
|
|
88
|
+
const next: PublishStep = {
|
|
89
|
+
...step,
|
|
90
|
+
status: 'pending',
|
|
91
|
+
blockedBy: blockers,
|
|
92
|
+
displayName: PENDING_DISPLAY_NAMES[step.kind]
|
|
93
|
+
};
|
|
94
|
+
unclearKinds.add(step.kind);
|
|
95
|
+
return next;
|
|
96
|
+
}
|
|
97
|
+
if (step.status === 'pending') {
|
|
98
|
+
const rest = { ...step };
|
|
99
|
+
delete rest.blockedBy;
|
|
100
|
+
unclearKinds.add(step.kind);
|
|
101
|
+
return rest;
|
|
102
|
+
}
|
|
103
|
+
if (!isClear(step.status)) {
|
|
104
|
+
unclearKinds.add(step.kind);
|
|
105
|
+
}
|
|
106
|
+
return step;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Order, apply eligibility, roll up. */
|
|
111
|
+
export function buildPublishPlan(target: PublishPlanTarget, steps: PublishStep[]): PublishPlan {
|
|
112
|
+
const ordered = applyBlockedByEligibility(orderPublishSteps(steps));
|
|
113
|
+
return { status: rollUpPublishPlan(ordered), steps: ordered, target };
|
|
114
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { PUBLISH_STEP_SPINE, PublishStepId } from './index.js';
|
|
4
|
+
|
|
5
|
+
describe('PublishStepId', () => {
|
|
6
|
+
test('exposes the stable wire id for every publish step', () => {
|
|
7
|
+
expect(PublishStepId).toEqual({
|
|
8
|
+
build: 'build',
|
|
9
|
+
databaseMigration: 'database_migration',
|
|
10
|
+
databaseProvisioning: 'database_provisioning',
|
|
11
|
+
policyGatesBeforeBuild: 'policy_gates:before_build',
|
|
12
|
+
policyGatesBeforeDeploy: 'policy_gates:before_deploy',
|
|
13
|
+
publish: 'publish'
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('the spine is made up only of known step ids, in fixed order', () => {
|
|
18
|
+
expect(PUBLISH_STEP_SPINE).toEqual([
|
|
19
|
+
PublishStepId.policyGatesBeforeBuild,
|
|
20
|
+
PublishStepId.build,
|
|
21
|
+
PublishStepId.policyGatesBeforeDeploy,
|
|
22
|
+
PublishStepId.databaseProvisioning,
|
|
23
|
+
PublishStepId.databaseMigration,
|
|
24
|
+
PublishStepId.publish
|
|
25
|
+
]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('every known step id appears in the spine exactly once', () => {
|
|
29
|
+
const ids = Object.values(PublishStepId);
|
|
30
|
+
for (const id of ids) {
|
|
31
|
+
expect(PUBLISH_STEP_SPINE.filter((spineId) => spineId === id)).toHaveLength(1);
|
|
32
|
+
}
|
|
33
|
+
expect(PUBLISH_STEP_SPINE).toHaveLength(ids.length);
|
|
34
|
+
});
|
|
35
|
+
});
|
package/src/types/rbac/index.ts
CHANGED
|
@@ -152,7 +152,7 @@ export enum ResourceTypeEnum {
|
|
|
152
152
|
// ORGANIZATION_REQUESTS = 'org.requests',
|
|
153
153
|
ORGANIZATION_USERS = 'org.users',
|
|
154
154
|
ORG_KNOWLEDGE = 'org_knowledge',
|
|
155
|
-
|
|
155
|
+
POLICIES = 'policies',
|
|
156
156
|
PROFILES = 'profiles',
|
|
157
157
|
REPOSITORIES = 'repos',
|
|
158
158
|
ROLES = 'roles',
|
|
@@ -225,7 +225,7 @@ export const ActionTypeByResourceType = {
|
|
|
225
225
|
PROFILES: {
|
|
226
226
|
MANAGE: ActionTypeEnum.MANAGE
|
|
227
227
|
},
|
|
228
|
-
|
|
228
|
+
POLICIES: {
|
|
229
229
|
MANAGE: ActionTypeEnum.MANAGE,
|
|
230
230
|
VIEW: ActionTypeEnum.VIEW
|
|
231
231
|
},
|