@bridge-ds/core 0.2.0 → 0.3.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/bridge.config.schema.json +74 -5
- package/dist/change-drafts.d.ts +29 -0
- package/dist/{component-spec-B0V_D3gM.js → component-spec-IRFGJ7XK.js} +607 -119
- package/dist/component-spec-IRFGJ7XK.js.map +1 -0
- package/dist/config.d.ts +16 -2
- package/dist/docs-ai.d.ts +116 -0
- package/dist/docs-draft.d.ts +30 -4
- package/dist/draft-exchange.d.ts +135 -14
- package/dist/explorations-github.d.ts +40 -5
- package/dist/explorations.d.ts +34 -28
- package/dist/github-client.d.ts +120 -2
- package/dist/index.d.ts +7 -0
- package/dist/index.js +1470 -446
- package/dist/index.js.map +1 -1
- package/dist/node/index.js +1 -1
- package/dist/previews-github.d.ts +22 -0
- package/dist/previews.d.ts +102 -0
- package/dist/protection-github.d.ts +25 -0
- package/dist/protection.d.ts +139 -0
- package/dist/requests-github.d.ts +21 -0
- package/dist/requests.d.ts +16 -1
- package/dist/rights.d.ts +63 -0
- package/dist/teams.d.ts +149 -0
- package/package.json +1 -1
- package/dist/component-spec-B0V_D3gM.js.map +0 -1
package/dist/node/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join, resolve, relative, dirname, sep, posix } from "node:path";
|
|
5
|
-
import { a as assertSafeBranch, c as collectHistory, p as packageDirCandidates, b as parseChangelog, m as mergeReleases, r as releasesFromTags, d as buildReport, e as prNumberFromSubject, l as loadConfig, h as hashSpec, C as COMPONENT_SPECS_VERSION, f as CONFIG_FILE, g as layoutOptions, v as variantKeyOf, i as buildTokenSet, t as tokenCssVar, j as iconComponentName, k as COLOR_FIELDS } from "../component-spec-
|
|
5
|
+
import { a as assertSafeBranch, c as collectHistory, p as packageDirCandidates, b as parseChangelog, m as mergeReleases, r as releasesFromTags, d as buildReport, e as prNumberFromSubject, l as loadConfig, h as hashSpec, C as COMPONENT_SPECS_VERSION, f as CONFIG_FILE, g as layoutOptions, v as variantKeyOf, i as buildTokenSet, t as tokenCssVar, j as iconComponentName, k as COLOR_FIELDS } from "../component-spec-IRFGJ7XK.js";
|
|
6
6
|
import { s as selectTokenFiles, g as globToRegExp, L as LayeredTokenSet } from "../layered-DGate4nv.js";
|
|
7
7
|
import { statSync, readFileSync, existsSync, writeFileSync, readdirSync } from "node:fs";
|
|
8
8
|
import { n as isAuditable, x as planCssFixes, c as DesignSystemIndex, g as buildAuditReport, i as compareWithBaseline, a as AUDIT_IGNORE_DIRS } from "../report-tNzTriGF.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { BridgeConfig } from './config';
|
|
2
|
+
import type { GitHubClient } from './github-client';
|
|
3
|
+
import { type BranchPreview, type PreviewApps } from './previews';
|
|
4
|
+
export interface BranchPreviewOptions {
|
|
5
|
+
/** Branch and head commit when the caller knows them (skips reading the pull request). */
|
|
6
|
+
branch?: string | undefined;
|
|
7
|
+
sha?: string | undefined;
|
|
8
|
+
/** The pull request's comments when the caller already read them (skips one read). */
|
|
9
|
+
comments?: {
|
|
10
|
+
body?: string | null | undefined;
|
|
11
|
+
}[] | undefined;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The preview of each app configured in `previews.apps` for a pull request (by number) or a commit (by sha): the URL
|
|
15
|
+
* Vercel reports (deployments, then commit statuses, then its PR comment), the template only when Vercel cannot have
|
|
16
|
+
* shortened it, and whether the build of that commit is ready. A failed read leaves that source out, never throws for it.
|
|
17
|
+
*/
|
|
18
|
+
export declare function branchPreviewUrls(gh: GitHubClient, ref: number | string, config: Pick<BridgeConfig, 'previews'> | {
|
|
19
|
+
previews?: {
|
|
20
|
+
apps?: PreviewApps | undefined;
|
|
21
|
+
} | undefined;
|
|
22
|
+
} | undefined, opts?: BranchPreviewOptions): Promise<BranchPreview[]>;
|
package/dist/previews.d.ts
CHANGED
|
@@ -74,6 +74,108 @@ export interface PreviewCommentInput {
|
|
|
74
74
|
apps: PreviewApps;
|
|
75
75
|
/** An exploration (`explore/*` branch or `exploration` label): staging only, never merged as is (docs/EXPLORATIONS.md). */
|
|
76
76
|
exploration?: boolean | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* The previews as Vercel reports them (`resolveBranchPreviews` over the deployments, statuses and the Vercel
|
|
79
|
+
* comment). Without it, only template URLs Vercel cannot have shortened are written.
|
|
80
|
+
*/
|
|
81
|
+
previews?: BranchPreview[] | undefined;
|
|
77
82
|
}
|
|
78
83
|
/** The sticky PR comment: install commands for each package, then the per-branch previews. Markdown, no HTML. */
|
|
79
84
|
export declare function previewComment(input: PreviewCommentInput): string;
|
|
85
|
+
/** What the sticky comment says about installing a snapshot, read back by tools (plugin, Studio). */
|
|
86
|
+
export interface PreviewCommentInstall {
|
|
87
|
+
/** The command of the first ```sh block, exactly as the workflow wrote it. Null when publishing failed. */
|
|
88
|
+
command: string | null;
|
|
89
|
+
/** The commit the snapshot was built from (`Snapshot of \`1a2b3c4\``), or null when the comment does not say. */
|
|
90
|
+
sha7: string | null;
|
|
91
|
+
/** Where the snapshots live, from the command's URLs. */
|
|
92
|
+
source: 'pkg.pr.new' | 'release' | 'other' | 'none';
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The install command of a `<!-- bridge-branch-preview -->` comment: the first ```sh block, its lines joined (a
|
|
96
|
+
* trailing `\` continues a line). Null when the body is not that comment or has no command (the publish failed).
|
|
97
|
+
* This is the command that works: pkg.pr.new when the workflow published there, the prerelease tarballs otherwise.
|
|
98
|
+
*/
|
|
99
|
+
export declare function installCommandFromPreviewComment(body: string | null | undefined): string | null;
|
|
100
|
+
/** Reads a sticky preview comment back: its command, the commit it was built from, where the snapshots live. */
|
|
101
|
+
export declare function readPreviewComment(body: string | null | undefined): PreviewCommentInstall | null;
|
|
102
|
+
/** The sticky preview comment among a pull request's comments (the last one, if a run ever posted twice). */
|
|
103
|
+
export declare function findPreviewComment<T extends {
|
|
104
|
+
body?: string | null | undefined;
|
|
105
|
+
}>(comments: T[]): T | undefined;
|
|
106
|
+
/** Where a preview URL comes from, most trusted first. */
|
|
107
|
+
export type PreviewUrlSource = 'deployment' | 'status' | 'vercel-comment' | 'template';
|
|
108
|
+
/** Whether the preview of the latest commit can be opened. `unknown` when only the template says where it is. */
|
|
109
|
+
export type PreviewState = 'ready' | 'building' | 'failed' | 'unknown';
|
|
110
|
+
export interface BranchPreview {
|
|
111
|
+
app: keyof PreviewApps;
|
|
112
|
+
label: string;
|
|
113
|
+
/** Null when nothing reports it yet and the template cannot be trusted (a host label over 63 characters). */
|
|
114
|
+
url: string | null;
|
|
115
|
+
state: PreviewState;
|
|
116
|
+
source: PreviewUrlSource | null;
|
|
117
|
+
}
|
|
118
|
+
/** A GitHub deployment with its latest status, as `GitHubClient.listDeployments` returns it. */
|
|
119
|
+
export interface DeploymentLike {
|
|
120
|
+
environment: string;
|
|
121
|
+
state?: string | null | undefined;
|
|
122
|
+
environmentUrl?: string | null | undefined;
|
|
123
|
+
}
|
|
124
|
+
/** A commit status (`GET /commits/{sha}/statuses`), newest first like GitHub returns them. */
|
|
125
|
+
export interface CommitStatusLike {
|
|
126
|
+
context: string;
|
|
127
|
+
state: string;
|
|
128
|
+
target_url?: string | null | undefined;
|
|
129
|
+
}
|
|
130
|
+
/** One project of the Vercel bot comment. */
|
|
131
|
+
export interface VercelCommentProject {
|
|
132
|
+
name: string;
|
|
133
|
+
/** https URL of the branch alias, as Vercel built it (shortened when long). */
|
|
134
|
+
previewUrl: string | null;
|
|
135
|
+
/** `DEPLOYED`, `PENDING`, `BUILDING`, `IGNORED`, `FAILED`... as Vercel writes it. */
|
|
136
|
+
status: string | null;
|
|
137
|
+
}
|
|
138
|
+
/** True for the comment the Vercel GitHub app posts (and edits) on a pull request. */
|
|
139
|
+
export declare const isVercelComment: (body: string | null | undefined) => boolean;
|
|
140
|
+
/**
|
|
141
|
+
* The projects of a Vercel bot comment, from its hidden header `[vc]: #<hash>:<base64 JSON>` (projects[].name and
|
|
142
|
+
* previewUrl). Null when the body is not that comment or the header does not decode.
|
|
143
|
+
*/
|
|
144
|
+
export declare function parseVercelComment(body: string | null | undefined): VercelCommentProject[] | null;
|
|
145
|
+
/** The Vercel bot comment among a pull request's comments (the last one, if there were ever two). */
|
|
146
|
+
export declare function findVercelComment<T extends {
|
|
147
|
+
body?: string | null | undefined;
|
|
148
|
+
}>(comments: T[]): T | undefined;
|
|
149
|
+
/**
|
|
150
|
+
* The app (`docs`, `storybook`, `admin`) a Vercel project serves, from the host its template starts with: project
|
|
151
|
+
* `bridge-admin` serves the app whose template host starts with `bridge-admin-git-` (Vercel's branch alias shape),
|
|
152
|
+
* else with `bridge-admin-` when only one template does. Null when no template names it.
|
|
153
|
+
*/
|
|
154
|
+
export declare function appForProject(project: string, apps: PreviewApps): keyof PreviewApps | null;
|
|
155
|
+
/** The project a deployment environment or a status context names: `Preview – bridge-admin` -> `bridge-admin`. */
|
|
156
|
+
export declare function projectOfLabel(label: string): string | null;
|
|
157
|
+
export interface BranchPreviewSources {
|
|
158
|
+
/** GitHub deployments of the head commit, each with its latest status. */
|
|
159
|
+
deployments?: DeploymentLike[] | undefined;
|
|
160
|
+
/** Commit statuses of the head commit, newest first. */
|
|
161
|
+
statuses?: CommitStatusLike[] | undefined;
|
|
162
|
+
/** The pull request's comments (the Vercel bot comment is read from them). */
|
|
163
|
+
comments?: {
|
|
164
|
+
body?: string | null | undefined;
|
|
165
|
+
}[] | undefined;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The preview of each configured app, real URL first: a GitHub deployment's environment URL, then a Vercel commit
|
|
169
|
+
* status pointing at the deployment (not at the inspector), then the Vercel bot comment, and the config template only
|
|
170
|
+
* when its host labels fit in 63 characters (else Vercel shortened it: url null until a real source reports it).
|
|
171
|
+
* The state comes from the freshest signal: commit status, then deployment, then the comment.
|
|
172
|
+
*/
|
|
173
|
+
export declare function resolveBranchPreviews(apps: PreviewApps, ctx: PreviewContext, sources?: BranchPreviewSources): BranchPreview[];
|
|
174
|
+
/** The previews that have a URL, as links (`{ app, label, url }`), in the stable app order. */
|
|
175
|
+
export declare const previewLinksOf: (previews: BranchPreview[]) => {
|
|
176
|
+
app: keyof PreviewApps;
|
|
177
|
+
label: string;
|
|
178
|
+
url: string;
|
|
179
|
+
}[];
|
|
180
|
+
/** True when every configured app has its URL and a finished build (ready or failed): polling can stop. */
|
|
181
|
+
export declare const previewsSettled: (previews: BranchPreview[]) => boolean;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type GitHubClient } from './github-client';
|
|
2
|
+
import { type ProtectionOptions, type ProtectionState, type Ruleset } from './protection';
|
|
3
|
+
/**
|
|
4
|
+
* The protection of `defaultBranch`. Rulesets are listed (read access is enough), then read one by one for their
|
|
5
|
+
* rules; classic branch protection is read too. Whatever cannot be read counts as unknown, never as unprotected.
|
|
6
|
+
*/
|
|
7
|
+
export declare function loadProtection(client: GitHubClient, defaultBranch: string): Promise<ProtectionState>;
|
|
8
|
+
/** The checks to require: the default branch's latest check runs, kept to those the last merged pull request ran. */
|
|
9
|
+
export declare function loadRequiredChecks(client: GitHubClient, defaultBranch: string): Promise<string[]>;
|
|
10
|
+
/**
|
|
11
|
+
* Turns protection on: creates the Bridge ruleset, or replaces it when one with its name exists (Complete
|
|
12
|
+
* protection). Needs a token with admin on the repository; GitHub's refusal is thrown as a GitHubError.
|
|
13
|
+
*/
|
|
14
|
+
export declare function protectLibrary(client: GitHubClient, defaultBranch: string, opts?: Partial<ProtectionOptions> & {
|
|
15
|
+
state?: ProtectionState;
|
|
16
|
+
}): Promise<Ruleset>;
|
|
17
|
+
/** True when GitHub refused the ruleset because of the repository's plan, not the token. */
|
|
18
|
+
export declare const isPlanRefusal: (err: unknown) => boolean;
|
|
19
|
+
/** Where rulesets are managed on GitHub. */
|
|
20
|
+
export declare const rulesSettingsUrl: (owner: string, repo: string) => string;
|
|
21
|
+
/**
|
|
22
|
+
* The one sentence for a refused "Protect the library": the plan, the token's permission, or admin access.
|
|
23
|
+
* Null for any other failure (the caller shows GitHub's message).
|
|
24
|
+
*/
|
|
25
|
+
export declare function protectRefusal(err: unknown): string | null;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/** A ruleset rule as GitHub returns and accepts it. */
|
|
2
|
+
export interface RulesetRule {
|
|
3
|
+
type: string;
|
|
4
|
+
parameters?: Record<string, unknown>;
|
|
5
|
+
}
|
|
6
|
+
export interface RulesetBypassActor {
|
|
7
|
+
actor_id: number | null;
|
|
8
|
+
actor_type: 'RepositoryRole' | 'Team' | 'Integration' | 'OrganizationAdmin' | 'DeployKey';
|
|
9
|
+
bypass_mode: 'always' | 'pull_request';
|
|
10
|
+
}
|
|
11
|
+
/** A repository ruleset (GET /repos/{o}/{r}/rulesets/{id}). The list endpoint leaves out `rules`. */
|
|
12
|
+
export interface Ruleset {
|
|
13
|
+
id?: number;
|
|
14
|
+
name: string;
|
|
15
|
+
target?: 'branch' | 'tag' | 'push' | string;
|
|
16
|
+
enforcement: 'active' | 'evaluate' | 'disabled' | string;
|
|
17
|
+
conditions?: {
|
|
18
|
+
ref_name?: {
|
|
19
|
+
include?: string[];
|
|
20
|
+
exclude?: string[];
|
|
21
|
+
};
|
|
22
|
+
} | null;
|
|
23
|
+
rules?: RulesetRule[];
|
|
24
|
+
bypass_actors?: RulesetBypassActor[];
|
|
25
|
+
/** Where the ruleset is edited on GitHub. */
|
|
26
|
+
html_url?: string;
|
|
27
|
+
_links?: {
|
|
28
|
+
html?: {
|
|
29
|
+
href?: string;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** The body of POST /repos/{o}/{r}/rulesets. */
|
|
34
|
+
export interface RulesetPayload {
|
|
35
|
+
name: string;
|
|
36
|
+
target: 'branch';
|
|
37
|
+
enforcement: 'active';
|
|
38
|
+
conditions: {
|
|
39
|
+
ref_name: {
|
|
40
|
+
include: string[];
|
|
41
|
+
exclude: string[];
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
rules: RulesetRule[];
|
|
45
|
+
bypass_actors: RulesetBypassActor[];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Classic branch protection (GET /repos/{o}/{r}/branches/{b}/protection), the fields Bridge reads.
|
|
49
|
+
* `null` means the branch has none; `undefined` means it could not be read (no token, no admin access).
|
|
50
|
+
*/
|
|
51
|
+
export interface ClassicProtection {
|
|
52
|
+
required_pull_request_reviews?: {
|
|
53
|
+
required_approving_review_count?: number;
|
|
54
|
+
} | null;
|
|
55
|
+
required_status_checks?: {
|
|
56
|
+
contexts?: string[];
|
|
57
|
+
checks?: {
|
|
58
|
+
context: string;
|
|
59
|
+
}[];
|
|
60
|
+
} | null;
|
|
61
|
+
allow_force_pushes?: {
|
|
62
|
+
enabled: boolean;
|
|
63
|
+
} | null;
|
|
64
|
+
allow_deletions?: {
|
|
65
|
+
enabled: boolean;
|
|
66
|
+
} | null;
|
|
67
|
+
}
|
|
68
|
+
/** What protecting the library takes, in the order Studio names them. */
|
|
69
|
+
export type ProtectionPart = 'review' | 'checks' | 'history' | 'deletion';
|
|
70
|
+
export declare const PROTECTION_PARTS: readonly ProtectionPart[];
|
|
71
|
+
export type ProtectionKind = 'protected' | 'partial' | 'unprotected' | 'unknown';
|
|
72
|
+
export interface ProtectionState {
|
|
73
|
+
kind: ProtectionKind;
|
|
74
|
+
/** Parts not enforced on the default branch (empty when protected; every part when unprotected or unknown). */
|
|
75
|
+
missing: ProtectionPart[];
|
|
76
|
+
/** The Bridge ruleset (by name), when one exists, active or not: "Complete protection" updates it in place. */
|
|
77
|
+
bridgeRulesetId?: number;
|
|
78
|
+
/** Where to see the rule that protects the branch on GitHub, when one does. */
|
|
79
|
+
url?: string;
|
|
80
|
+
}
|
|
81
|
+
/** The name Bridge gives its ruleset. Bridge finds it again by this name. */
|
|
82
|
+
export declare const protectionRulesetName: (defaultBranch: string) => string;
|
|
83
|
+
/** GitHub's built-in repository role id for "Admin", used in bypass_actors. */
|
|
84
|
+
export declare const ADMIN_ROLE_ID = 5;
|
|
85
|
+
export interface ProtectionOptions {
|
|
86
|
+
/** Check names that must pass before a merge (see requiredCheckNames). Empty: no checks rule. */
|
|
87
|
+
checks: string[];
|
|
88
|
+
/** Approvals a pull request needs. Never below 1. */
|
|
89
|
+
approvals: number;
|
|
90
|
+
/** Let repository admins merge a pull request without its approvals (the owner override). */
|
|
91
|
+
adminBypass: boolean;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The ruleset that protects the library. Choices:
|
|
95
|
+
* - Target `~DEFAULT_BRANCH`, not a branch name: it follows a rename of the default branch.
|
|
96
|
+
* - `deletion` and `non_fast_forward`: nobody deletes the default branch or rewrites its history.
|
|
97
|
+
* - `pull_request` with at least 1 approval: every change goes through a reviewed pull request, direct pushes are
|
|
98
|
+
* refused whoever pushes. Stale approvals are dismissed on a new push, so what was approved is what merges.
|
|
99
|
+
* Code owner review and "approval of the most recent push" stay off: CODEOWNERS often holds placeholders, and a
|
|
100
|
+
* team of two must still be able to ship.
|
|
101
|
+
* - `required_status_checks`: the CI jobs that ran on recent pull requests must pass. Not strict (the branch does
|
|
102
|
+
* not have to be up to date), which keeps merges from queueing behind each other.
|
|
103
|
+
* - Bypass: repository admins in `pull_request` mode only. They keep the owner override (merge a pull request
|
|
104
|
+
* whose approvals are missing) but cannot push to the default branch either.
|
|
105
|
+
*/
|
|
106
|
+
export declare function protectionRuleset(config: {
|
|
107
|
+
repo?: {
|
|
108
|
+
defaultBranch?: string;
|
|
109
|
+
} | undefined;
|
|
110
|
+
} | null | undefined, opts: ProtectionOptions): RulesetPayload;
|
|
111
|
+
/** Minimal check run shape (github-client CheckRun fits). `app` is the GitHub App slug when known. */
|
|
112
|
+
export interface CheckRunLike {
|
|
113
|
+
name: string;
|
|
114
|
+
status?: string;
|
|
115
|
+
conclusion: string | null;
|
|
116
|
+
app?: string | undefined;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The check names to require, read from the repository's own CI, never hardcoded: the check runs of the default
|
|
120
|
+
* branch's latest commit, without Vercel (deploy previews, not tests) and without skipped ones. When the head of a
|
|
121
|
+
* recent pull request is given, only checks that also ran there are kept: a job that runs on pushes to the default
|
|
122
|
+
* branch only (a release job) never reports on a pull request, and requiring it would block every merge.
|
|
123
|
+
*/
|
|
124
|
+
export declare function requiredCheckNames(defaultBranchRuns: CheckRunLike[], pullRequestRuns?: CheckRunLike[]): string[];
|
|
125
|
+
/** True when a branch ruleset's conditions cover the default branch. */
|
|
126
|
+
export declare function rulesetTargetsDefault(ruleset: Ruleset, defaultBranch: string): boolean;
|
|
127
|
+
/**
|
|
128
|
+
* Whether the default branch is protected, from full rulesets (with `rules`; null when they could not be read) and
|
|
129
|
+
* classic branch protection (null when the branch has none, undefined when it could not be read).
|
|
130
|
+
* - protected: review, checks, no history rewrite and no deletion are all enforced.
|
|
131
|
+
* - partial: some of them are.
|
|
132
|
+
* - unprotected: none of them, and both sources were read.
|
|
133
|
+
* - unknown: nothing could be read, or nothing was found in one source while the other could not be read.
|
|
134
|
+
*/
|
|
135
|
+
export declare function protectionState(rulesets: Ruleset[] | null | undefined, branchProtection: ClassicProtection | null | undefined, defaultBranch: string): ProtectionState;
|
|
136
|
+
/** Plain words for each part, for designers: no "ruleset", "branch" or "force push". */
|
|
137
|
+
export declare const PROTECTION_PART_PHRASE: Record<ProtectionPart, string>;
|
|
138
|
+
/** The one sentence Studio, the CLI and the MCP server show for a protection state. */
|
|
139
|
+
export declare function protectionSentence(state: ProtectionState): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { GitHubClient } from './github-client';
|
|
2
|
+
export interface RequestExplorationLink {
|
|
3
|
+
number: number;
|
|
4
|
+
branch: string;
|
|
5
|
+
/** Who is now on it, when someone was assigned in the same step. */
|
|
6
|
+
assignees: string[];
|
|
7
|
+
/** Sentences about the parts that did not go through (label refused): the link itself holds. */
|
|
8
|
+
warnings: string[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Links a request to an exploration branch, the way `bridge request create --exploration` files one: the hidden block
|
|
12
|
+
* gets `exploration`, the issue gets the `exploration` label, and whoever starts the exploration is assigned, so the
|
|
13
|
+
* request reads "in progress" everywhere. Implementing it then happens on that branch (`request implement --branch`).
|
|
14
|
+
*/
|
|
15
|
+
export declare function linkRequestToExplorationOnGitHub(gh: GitHubClient, n: number, branch: string, opts?: {
|
|
16
|
+
assignee?: string | null;
|
|
17
|
+
}): Promise<RequestExplorationLink>;
|
|
18
|
+
/** Assigns someone to a request. Throws when GitHub does not keep the assignee (no access to the repository). */
|
|
19
|
+
export declare function assignRequestOnGitHub(gh: GitHubClient, n: number, login: string): Promise<string[]>;
|
|
20
|
+
/** Declines a request: the reason as a comment the requester is notified of, then the issue closed as not planned. */
|
|
21
|
+
export declare function declineRequestOnGitHub(gh: GitHubClient, n: number, reason: string, by?: string | null): Promise<void>;
|
package/dist/requests.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type RequestKind = 'new-component' | 'new-variant' | 'prop-change' | 'visual' | 'token-binding' | 'bug' | 'other';
|
|
1
|
+
export type RequestKind = 'new-component' | 'new-variant' | 'prop-change' | 'structure' | 'visual' | 'token-binding' | 'bug' | 'other';
|
|
2
2
|
export type RequestSource = 'figma' | 'studio' | 'cli' | 'mcp';
|
|
3
3
|
export type RequestStatus = 'requested' | 'in-progress' | 'done' | 'declined';
|
|
4
4
|
export declare const REQUEST_KINDS: {
|
|
@@ -22,6 +22,11 @@ export interface ComponentChangeRequest {
|
|
|
22
22
|
title: string;
|
|
23
23
|
/** What and why, in the requester's words. */
|
|
24
24
|
summary: string;
|
|
25
|
+
/**
|
|
26
|
+
* What changed for developers in one sentence ("Icon added after the label in primary sm: Pencil, 16px"): written
|
|
27
|
+
* by Claude from the before and after images, or drafted from the detected changes. Optional.
|
|
28
|
+
*/
|
|
29
|
+
line?: string | undefined;
|
|
25
30
|
changes: RequestChange[];
|
|
26
31
|
/** Where the design lives, when it comes from Figma. */
|
|
27
32
|
figma?: {
|
|
@@ -57,6 +62,16 @@ export declare function composeRequestIssue(r: ComponentChangeRequest, opts?: {
|
|
|
57
62
|
}): RequestIssue;
|
|
58
63
|
/** The request inside an issue body, or undefined when the body has no valid Bridge block. */
|
|
59
64
|
export declare function parseRequestIssue(body: string | null | undefined): ComponentChangeRequest | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* The issue body with the request linked to an exploration branch: the hidden block gets `exploration` (what the
|
|
67
|
+
* Explorations view, the MCP server and the CLI read) and the readable part says where to implement it. The rest of the
|
|
68
|
+
* body stays as people wrote it. Undefined when the body has no Bridge block (nothing to link).
|
|
69
|
+
*/
|
|
70
|
+
export declare function withRequestExploration(body: string | null | undefined, branch: string): string | undefined;
|
|
71
|
+
/** Problems with a decline reason, as sentences that say how to fix them. */
|
|
72
|
+
export declare function declineReasonProblems(reason: string): string[];
|
|
73
|
+
/** The comment posted on a declined request: who declined it and why, in the decliner's words. */
|
|
74
|
+
export declare function declineComment(reason: string, by?: string | null): string;
|
|
60
75
|
/**
|
|
61
76
|
* Where a request stands: open with no linked pull request = requested; open with an open linked pull request =
|
|
62
77
|
* in progress; closed as completed or by a merged pull request = done; closed otherwise = declined.
|
package/dist/rights.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Owners } from './config';
|
|
2
|
+
import { type TeamState, type TeamStates } from './teams';
|
|
3
|
+
/** An owner group from `governance.owners`. */
|
|
4
|
+
export type OwnerGroup = 'design' | 'tech';
|
|
5
|
+
export declare const OWNER_GROUPS: readonly OwnerGroup[];
|
|
6
|
+
/** The three rights, each held by one or more owner groups. */
|
|
7
|
+
export interface Rights {
|
|
8
|
+
approveDesign: OwnerGroup[];
|
|
9
|
+
approveTech: OwnerGroup[];
|
|
10
|
+
ship: OwnerGroup[];
|
|
11
|
+
}
|
|
12
|
+
export type Right = keyof Rights;
|
|
13
|
+
export declare const RIGHTS: readonly Right[];
|
|
14
|
+
/** Plain names of each right, for Settings and the gate messages. "Ship" means publish to the products. */
|
|
15
|
+
export declare const RIGHT_LABEL: Record<Right, string>;
|
|
16
|
+
export declare const DEFAULT_RIGHTS: Rights;
|
|
17
|
+
/** Rights with every missing entry filled from DEFAULT_RIGHTS, groups deduplicated in design, tech order. */
|
|
18
|
+
export declare function normalizeRights(input: Partial<Record<Right, readonly string[]>> | null | undefined): Rights;
|
|
19
|
+
/** Problems in a raw `governance.rights` value, in the words loadConfig reports. Empty: valid. */
|
|
20
|
+
export declare function rightsProblems(raw: unknown): string[];
|
|
21
|
+
/** The entries (people and teams) holding a right: the union of the owner lists of its groups. Empty when those lists are empty. */
|
|
22
|
+
export declare function holdersOf(right: Right, owners: Owners, rights: Rights | null | undefined): string[];
|
|
23
|
+
/**
|
|
24
|
+
* Owner lists to count approvals against (summarizeApprovals): who may approve design, who may approve tech.
|
|
25
|
+
* Without `rights`, the owner lists as they are.
|
|
26
|
+
*/
|
|
27
|
+
export declare function approverLists(owners: Owners, rights: Rights | null | undefined, teams?: TeamStates | null): Owners;
|
|
28
|
+
/** approverLists before team expansion: the entries as the config names them, for sentences. */
|
|
29
|
+
export declare function approverEntries(owners: Owners, rights: Rights | null | undefined): Owners;
|
|
30
|
+
/** Who may ship, or null when nobody is singled out (no `rights`, or its groups list no logins): anyone with write access. */
|
|
31
|
+
export interface ShipPolicy {
|
|
32
|
+
/** Logins allowed to ship: people, and team members once read. A team not read keeps its "@org/slug" key. */
|
|
33
|
+
logins: string[];
|
|
34
|
+
/** Owner groups holding the right, for the sentence ("the tech owner", "a design or tech owner"). */
|
|
35
|
+
groups: OwnerGroup[];
|
|
36
|
+
/** The entries as the config names them (people and teams), for the sentence. Absent: `logins`. */
|
|
37
|
+
entries?: string[];
|
|
38
|
+
/** Team states known when the policy was built, for the sentence ("@acme/design (4 people)"). */
|
|
39
|
+
teams?: TeamStates;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Who may ship. With `teams` (TeamDirectory.resolve), team members count; a team that cannot be read still holds
|
|
43
|
+
* the right, so the policy is never dropped to "anyone with write access" because a team was not read.
|
|
44
|
+
*/
|
|
45
|
+
export declare function shipPolicy(owners: Owners, rights: Rights | null | undefined, teams?: TeamStates | null): ShipPolicy | null;
|
|
46
|
+
/** Teams in a ship policy that could not be read: the gate names them with the fix. */
|
|
47
|
+
export declare function unreadableShipTeams(policy: ShipPolicy | null | undefined): TeamState[];
|
|
48
|
+
/** Whether `login` may ship. No policy: yes (GitHub write access still decides). */
|
|
49
|
+
export declare function canShip(login: string | null | undefined, policy: ShipPolicy | null | undefined): boolean;
|
|
50
|
+
/** "the tech owner", "the design owner", "a design or tech owner". */
|
|
51
|
+
export declare function groupsPhrase(groups: readonly OwnerGroup[]): string;
|
|
52
|
+
/** The sentence a blocked viewer reads: "Only the tech owner can ship this." */
|
|
53
|
+
/** Teams are named as teams, with their size when known: "Only the tech owner can ship this (@acme/web (4 people))." */
|
|
54
|
+
export declare function whoCanShipSentence(policy: ShipPolicy): string;
|
|
55
|
+
/** Visual before and after: on every pull request that touches tokens or components, or only when asked. */
|
|
56
|
+
export type VisualImpactMode = 'auto' | 'on-demand';
|
|
57
|
+
export interface VisualImpactConfig {
|
|
58
|
+
mode: VisualImpactMode;
|
|
59
|
+
}
|
|
60
|
+
export declare const DEFAULT_VISUAL_IMPACT: VisualImpactConfig;
|
|
61
|
+
/** Label that asks the visual impact workflow to run in on-demand mode. */
|
|
62
|
+
export declare const VISUAL_CHECK_LABEL = "visual-check";
|
|
63
|
+
export declare function visualImpactProblems(raw: unknown): string[];
|
package/dist/teams.d.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { Owners } from './config';
|
|
2
|
+
export type OwnerEntry = {
|
|
3
|
+
kind: 'person';
|
|
4
|
+
login: string;
|
|
5
|
+
} | {
|
|
6
|
+
kind: 'team';
|
|
7
|
+
org: string;
|
|
8
|
+
slug: string;
|
|
9
|
+
};
|
|
10
|
+
/** A person or a team, or null when `raw` is neither. Accepts a leading @ on both. */
|
|
11
|
+
export declare function parseOwnerEntry(raw: string): OwnerEntry | null;
|
|
12
|
+
/** "@acme/design" for a team, "peterbono" for a person: the form Bridge writes back to bridge.config.json. */
|
|
13
|
+
export declare function formatOwnerEntry(e: OwnerEntry): string;
|
|
14
|
+
/** Whether a raw entry names a team. */
|
|
15
|
+
export declare const isTeamEntry: (raw: string) => boolean;
|
|
16
|
+
/** Case-insensitive key of a team: "@acme/design". */
|
|
17
|
+
export declare const teamKey: (org: string, slug: string) => string;
|
|
18
|
+
/** Key of a raw team entry, undefined for a person or a malformed entry. */
|
|
19
|
+
export declare function teamKeyOf(raw: string): string | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Problems in the raw owner lists, in the words loadConfig reports. Each malformed entry is named with the two
|
|
22
|
+
* accepted forms, so the fix is in the message.
|
|
23
|
+
*/
|
|
24
|
+
export declare function ownerEntryProblems(raw: unknown): string[];
|
|
25
|
+
/** Why a team's members cannot be read, each with its own fix (teamUnreadableMessage). */
|
|
26
|
+
export type TeamUnreadableReason =
|
|
27
|
+
/** The repository owner (or the org named) is a personal account: GitHub teams only exist in organizations. */
|
|
28
|
+
'personal-account'
|
|
29
|
+
/** The token works but may not read the organization's teams (classic: read:org; fine-grained: Members, read). */
|
|
30
|
+
| 'missing-permission'
|
|
31
|
+
/** No token, or GitHub refused it: GitHub never shows team members anonymously. */
|
|
32
|
+
| 'no-token'
|
|
33
|
+
/** GitHub says the team does not exist, or hides it from this token. */
|
|
34
|
+
| 'not-found'
|
|
35
|
+
/** GitHub did not answer (network, rate limit, server error). */
|
|
36
|
+
| 'unreachable';
|
|
37
|
+
export interface TeamRef {
|
|
38
|
+
/** "@acme/design", lowercase. */
|
|
39
|
+
key: string;
|
|
40
|
+
org: string;
|
|
41
|
+
slug: string;
|
|
42
|
+
}
|
|
43
|
+
export type TeamState = (TeamRef & {
|
|
44
|
+
status: 'ok';
|
|
45
|
+
members: string[];
|
|
46
|
+
}) | (TeamRef & {
|
|
47
|
+
status: 'unreadable';
|
|
48
|
+
reason: TeamUnreadableReason;
|
|
49
|
+
message: string;
|
|
50
|
+
});
|
|
51
|
+
/** Team states by key ("@acme/design"). */
|
|
52
|
+
export type TeamStates = Record<string, TeamState>;
|
|
53
|
+
/** The one sentence that says why a team cannot be read and how to fix it. */
|
|
54
|
+
export declare function teamUnreadableMessage(team: {
|
|
55
|
+
org: string;
|
|
56
|
+
slug: string;
|
|
57
|
+
}, reason: TeamUnreadableReason): string;
|
|
58
|
+
/** Distinct teams named in the owner lists, in order. */
|
|
59
|
+
export declare function teamsIn(owners: Partial<Owners> | readonly string[]): TeamRef[];
|
|
60
|
+
/**
|
|
61
|
+
* Logins of a list of entries: people as they are, a readable team as its members. An unreadable team, or one not
|
|
62
|
+
* resolved yet, stays as its "@org/slug" key: never empty, never matching a login.
|
|
63
|
+
*/
|
|
64
|
+
export declare function expandEntries(entries: readonly string[], teams?: TeamStates | null): string[];
|
|
65
|
+
/** Both owner lists expanded (expandEntries). */
|
|
66
|
+
export declare function expandOwners(owners: Owners, teams?: TeamStates | null): Owners;
|
|
67
|
+
/** How an entry reads in a sentence: "@peterbono", "@acme/design (4 people)", "@acme/design" while unknown. */
|
|
68
|
+
export declare function entryLabel(raw: string, teams?: TeamStates | null): string;
|
|
69
|
+
/**
|
|
70
|
+
* The entries to name for a set of logins (someone who can still act): each person among them, each team with a
|
|
71
|
+
* member among them, and each team whose members are unknown (it may hold one). Labels as entryLabel.
|
|
72
|
+
*/
|
|
73
|
+
export declare function entriesFor(entries: readonly string[], logins: readonly string[], teams?: TeamStates | null): string[];
|
|
74
|
+
/** Teams that could not be read, in order. */
|
|
75
|
+
export declare function unreadableTeams(teams: TeamStates | null | undefined): Extract<TeamState, {
|
|
76
|
+
status: 'unreadable';
|
|
77
|
+
}>[];
|
|
78
|
+
/** What TeamDirectory needs from a GitHub client. GitHubClient implements it. */
|
|
79
|
+
export interface TeamReader {
|
|
80
|
+
/** Whether a token is sent: GitHub never lists team members anonymously. */
|
|
81
|
+
readonly hasToken: boolean;
|
|
82
|
+
/** Every member login of a team (GET /orgs/{org}/teams/{slug}/members, all pages). */
|
|
83
|
+
listTeamMembers(org: string, slug: string): Promise<string[]>;
|
|
84
|
+
/** A user's membership of a team (GET /orgs/{org}/teams/{slug}/memberships/{user}): "active", "pending", or null when not a member. */
|
|
85
|
+
getTeamMembership(org: string, slug: string, login: string): Promise<'active' | 'pending' | null>;
|
|
86
|
+
/** "User" or "Organization" (GET /users/{login}), null when unknown. */
|
|
87
|
+
getAccountType(login: string): Promise<'User' | 'Organization' | null>;
|
|
88
|
+
/** The organization's teams the token can see (GET /orgs/{org}/teams, all pages). */
|
|
89
|
+
listOrgTeams(org: string): Promise<{
|
|
90
|
+
slug: string;
|
|
91
|
+
name: string;
|
|
92
|
+
}[]>;
|
|
93
|
+
}
|
|
94
|
+
export interface TeamDirectoryOptions {
|
|
95
|
+
/** How long a team read is kept, in ms. Default 5 minutes. */
|
|
96
|
+
ttlMs?: number;
|
|
97
|
+
now?: () => number;
|
|
98
|
+
}
|
|
99
|
+
/** Whether one person may act through a list of entries, and which teams could not be checked. */
|
|
100
|
+
export interface MembershipResult {
|
|
101
|
+
member: boolean;
|
|
102
|
+
/** Teams whose membership could not be read: when `member` is false, the answer may be wrong for them. */
|
|
103
|
+
unreadable: Extract<TeamState, {
|
|
104
|
+
status: 'unreadable';
|
|
105
|
+
}>[];
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Expands teams to logins through a TeamReader, with a cache: each team is read once per `ttlMs`, concurrent reads
|
|
109
|
+
* of one team share the request. Every failure becomes an "unreadable" state with its reason.
|
|
110
|
+
*/
|
|
111
|
+
export declare class TeamDirectory {
|
|
112
|
+
private readonly reader;
|
|
113
|
+
private readonly teams;
|
|
114
|
+
private readonly accounts;
|
|
115
|
+
private readonly memberships;
|
|
116
|
+
private readonly ttl;
|
|
117
|
+
private readonly now;
|
|
118
|
+
constructor(reader: TeamReader, opts?: TeamDirectoryOptions);
|
|
119
|
+
private accountType;
|
|
120
|
+
/** The reason a read of `org` failed, as a state. A 404 on a personal account says so; a 403 is a missing permission. */
|
|
121
|
+
private failure;
|
|
122
|
+
/** Members of one team, or why they cannot be read. Cached. */
|
|
123
|
+
team(org: string, slug: string): Promise<TeamState>;
|
|
124
|
+
/** Every team named in the owner lists, read in parallel. Empty object when no team is named (no request). */
|
|
125
|
+
resolve(owners: Partial<Owners> | readonly string[]): Promise<TeamStates>;
|
|
126
|
+
/**
|
|
127
|
+
* Whether `login` is an active member of one team: from the cached member list when there is one, else one
|
|
128
|
+
* membership read (GET /orgs/{org}/teams/{slug}/memberships/{user}). An unreadable team returns its state.
|
|
129
|
+
*/
|
|
130
|
+
isMember(org: string, slug: string, login: string): Promise<boolean | TeamState>;
|
|
131
|
+
/**
|
|
132
|
+
* Whether `login` holds a right given to `entries` (people and teams): a person entry matches directly, a team
|
|
133
|
+
* through its membership. Teams are only read when no person entry matches.
|
|
134
|
+
*/
|
|
135
|
+
memberOfAny(entries: readonly string[], login: string): Promise<MembershipResult>;
|
|
136
|
+
/** The organization's teams for a picker, or why they cannot be listed (a personal account has none). */
|
|
137
|
+
orgTeams(org: string): Promise<{
|
|
138
|
+
status: 'ok';
|
|
139
|
+
teams: {
|
|
140
|
+
slug: string;
|
|
141
|
+
name: string;
|
|
142
|
+
}[];
|
|
143
|
+
} | {
|
|
144
|
+
status: 'unreadable';
|
|
145
|
+
reason: TeamUnreadableReason;
|
|
146
|
+
}>;
|
|
147
|
+
/** Drops every cached read (after the token changes). */
|
|
148
|
+
clear(): void;
|
|
149
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bridge-ds/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Bridge core: bridge.config.json, DTCG token loading (tiers and layered modes), alias resolution, WCAG contrast, diffs, minimal-diff edits, PR body, the shared risk tier classifier, changesets and a fetch-based GitHub client. No Figma API, no DOM; Node-only helpers (git, reading a product folder) under @bridge-ds/core/node; the adoption audit under @bridge-ds/core/audit.",
|
|
6
6
|
"license": "MIT",
|