@teambit/ci 1.0.485 → 1.0.487
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/ci.docs.mdx +344 -83
- package/commands/sync.cmd.ts +77 -0
- package/dist/ci.docs.mdx +344 -83
- package/dist/ci.main.runtime.d.ts +67 -6
- package/dist/ci.main.runtime.js +152 -25
- package/dist/ci.main.runtime.js.map +1 -1
- package/dist/commands/sync.cmd.d.ts +34 -0
- package/dist/commands/sync.cmd.js +57 -0
- package/dist/commands/sync.cmd.js.map +1 -0
- package/dist/git.d.ts +11 -1
- package/dist/git.js +11 -1
- package/dist/git.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +26 -1
- package/dist/index.js.map +1 -1
- package/dist/{preview-1785858957533.js → preview-1785883495880.js} +1 -1
- package/dist/sync/git-host-provider.d.ts +47 -0
- package/dist/sync/git-host-provider.js +54 -0
- package/dist/sync/git-host-provider.js.map +1 -0
- package/dist/sync/git-host-provider.spec.d.ts +1 -0
- package/dist/sync/git-host-provider.spec.js +171 -0
- package/dist/sync/git-host-provider.spec.js.map +1 -0
- package/dist/sync/git-ops.d.ts +171 -0
- package/dist/sync/git-ops.js +354 -0
- package/dist/sync/git-ops.js.map +1 -0
- package/dist/sync/git-ops.spec.d.ts +1 -0
- package/dist/sync/git-ops.spec.js +476 -0
- package/dist/sync/git-ops.spec.js.map +1 -0
- package/dist/sync/github-client.d.ts +79 -0
- package/dist/sync/github-client.js +198 -0
- package/dist/sync/github-client.js.map +1 -0
- package/dist/sync/github-client.spec.d.ts +1 -0
- package/dist/sync/github-client.spec.js +205 -0
- package/dist/sync/github-client.spec.js.map +1 -0
- package/dist/sync/init-scaffold.d.ts +40 -0
- package/dist/sync/init-scaffold.js +326 -0
- package/dist/sync/init-scaffold.js.map +1 -0
- package/dist/sync/init-scaffold.spec.d.ts +1 -0
- package/dist/sync/init-scaffold.spec.js +169 -0
- package/dist/sync/init-scaffold.spec.js.map +1 -0
- package/dist/sync/lane-sync-executor.d.ts +256 -0
- package/dist/sync/lane-sync-executor.js +1249 -0
- package/dist/sync/lane-sync-executor.js.map +1 -0
- package/dist/sync/lane-sync-executor.spec.d.ts +1 -0
- package/dist/sync/lane-sync-executor.spec.js +416 -0
- package/dist/sync/lane-sync-executor.spec.js.map +1 -0
- package/dist/sync/main-sync-executor.d.ts +62 -0
- package/dist/sync/main-sync-executor.js +305 -0
- package/dist/sync/main-sync-executor.js.map +1 -0
- package/dist/sync/sync-config.d.ts +83 -0
- package/dist/sync/sync-config.js +187 -0
- package/dist/sync/sync-config.js.map +1 -0
- package/dist/sync/sync-config.spec.d.ts +1 -0
- package/dist/sync/sync-config.spec.js +254 -0
- package/dist/sync/sync-config.spec.js.map +1 -0
- package/dist/sync/sync-orchestrator.d.ts +76 -0
- package/dist/sync/sync-orchestrator.js +396 -0
- package/dist/sync/sync-orchestrator.js.map +1 -0
- package/dist/sync/sync-orchestrator.spec.d.ts +1 -0
- package/dist/sync/sync-orchestrator.spec.js +155 -0
- package/dist/sync/sync-orchestrator.spec.js.map +1 -0
- package/dist/sync/sync-planner.d.ts +60 -0
- package/dist/sync/sync-planner.js +114 -0
- package/dist/sync/sync-planner.js.map +1 -0
- package/dist/sync/sync-planner.spec.d.ts +1 -0
- package/dist/sync/sync-planner.spec.js +210 -0
- package/dist/sync/sync-planner.spec.js.map +1 -0
- package/dist/sync/sync-state.d.ts +94 -0
- package/dist/sync/sync-state.js +178 -0
- package/dist/sync/sync-state.js.map +1 -0
- package/dist/sync/sync-state.spec.d.ts +1 -0
- package/dist/sync/sync-state.spec.js +271 -0
- package/dist/sync/sync-state.spec.js.map +1 -0
- package/dist/sync/workspace-lane.d.ts +13 -0
- package/dist/sync/workspace-lane.js +27 -0
- package/dist/sync/workspace-lane.js.map +1 -0
- package/package.json +20 -15
- package/sync/git-host-provider.spec.ts +197 -0
- package/sync/git-host-provider.ts +89 -0
- package/sync/git-ops.spec.ts +506 -0
- package/sync/git-ops.ts +334 -0
- package/sync/github-client.spec.ts +174 -0
- package/sync/github-client.ts +209 -0
- package/sync/init-scaffold.spec.ts +142 -0
- package/sync/init-scaffold.ts +340 -0
- package/sync/lane-sync-executor.spec.ts +389 -0
- package/sync/lane-sync-executor.ts +1363 -0
- package/sync/main-sync-executor.ts +321 -0
- package/sync/sync-config.spec.ts +283 -0
- package/sync/sync-config.ts +214 -0
- package/sync/sync-orchestrator.spec.ts +120 -0
- package/sync/sync-orchestrator.ts +386 -0
- package/sync/sync-planner.spec.ts +126 -0
- package/sync/sync-planner.ts +99 -0
- package/sync/sync-state.spec.ts +263 -0
- package/sync/sync-state.ts +176 -0
- package/sync/workspace-lane.ts +20 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type { GitHostProvider, PrInfo } from './git-host-provider';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The `owner/repo` out of a github.com remote URL, anchored to the URL authority. `:digits` is a port
|
|
5
|
+
* only under a scheme; in the scp-like form (`git@github.com:12345/repo`) the colon starts the PATH
|
|
6
|
+
* and an all-digits owner is a legal GitHub username.
|
|
7
|
+
*/
|
|
8
|
+
export function parseGitHubRepo(remoteUrl: string): string | undefined {
|
|
9
|
+
const url = remoteUrl.trim();
|
|
10
|
+
const hasScheme = /^[a-z+]+:\/\//i.test(url);
|
|
11
|
+
const match = hasScheme
|
|
12
|
+
? url.match(/(?:\/\/|@)github\.com(?::\d+)?\/([^/]+\/[^/]+?)(?:\.git)?$/i)
|
|
13
|
+
: url.match(/(?:^|@)github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/i);
|
|
14
|
+
return match?.[1];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The host a git remote URL points at, lower-cased, or undefined when the URL has no authority. */
|
|
18
|
+
function remoteHost(remoteUrl: string): string | undefined {
|
|
19
|
+
return remoteUrl
|
|
20
|
+
.trim()
|
|
21
|
+
.match(/^(?:[a-z+]+:\/\/)?(?:[^@/]+@)?([^:/]+)/i)?.[1]
|
|
22
|
+
?.toLowerCase();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Does this remote point at GitHub? A host test only — an unparseable github.com remote is still
|
|
27
|
+
* GitHub's to claim. Host equality, not substring: `github.com` can be a path segment on another host.
|
|
28
|
+
*/
|
|
29
|
+
export function isGitHubRemote(remoteUrl: string): boolean {
|
|
30
|
+
return remoteHost(remoteUrl) === 'github.com';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const API = 'https://api.github.com';
|
|
34
|
+
|
|
35
|
+
/** Warning sink; a plain callback rather than a `Logger` so this module needs no logger aspect. */
|
|
36
|
+
export type WarnFn = (message: string) => void;
|
|
37
|
+
|
|
38
|
+
const noopWarn: WarnFn = () => {};
|
|
39
|
+
|
|
40
|
+
export class GitHubClient implements GitHostProvider {
|
|
41
|
+
readonly name = 'github';
|
|
42
|
+
|
|
43
|
+
private token: string;
|
|
44
|
+
/** `owner/repo`. Readable so callers (and tests) can see *which* repository a resolved client speaks to. */
|
|
45
|
+
readonly repo: string;
|
|
46
|
+
private fetchImpl: typeof fetch;
|
|
47
|
+
|
|
48
|
+
constructor(opts: { token: string; repo: string; fetchImpl?: typeof fetch }) {
|
|
49
|
+
this.token = opts.token;
|
|
50
|
+
this.repo = opts.repo;
|
|
51
|
+
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The client this environment describes, or undefined when it describes none. `origin` outranks
|
|
56
|
+
* `GITHUB_REPOSITORY`: every PR operation names a branch that only exists on `origin`, so if the two
|
|
57
|
+
* disagree the origin-parsed repository wins and the mismatch is warned about once.
|
|
58
|
+
*/
|
|
59
|
+
static fromEnv(gitRemoteUrl?: string, warn: WarnFn = noopWarn): GitHubClient | undefined {
|
|
60
|
+
// BIT_GITHUB_TOKEN is the override; GITHUB_TOKEN is the ambient default Actions injects everywhere.
|
|
61
|
+
const token = process.env.BIT_GITHUB_TOKEN || process.env.GITHUB_TOKEN;
|
|
62
|
+
// A remote that is demonstrably not GitHub's is a hard no, whatever the environment says —
|
|
63
|
+
// GITHUB_REPOSITORY is set on every Actions job, including ones whose origin is another host.
|
|
64
|
+
if (gitRemoteUrl && !isGitHubRemote(gitRemoteUrl)) return undefined;
|
|
65
|
+
const repoFromRemote = gitRemoteUrl ? parseGitHubRepo(gitRemoteUrl) : undefined;
|
|
66
|
+
const repoFromEnv = process.env.GITHUB_REPOSITORY;
|
|
67
|
+
const repo = repoFromRemote || repoFromEnv;
|
|
68
|
+
if (!token || !repo) return undefined;
|
|
69
|
+
// GitHub owner/repo names are case-insensitive.
|
|
70
|
+
if (repoFromRemote && repoFromEnv && repoFromRemote.toLowerCase() !== repoFromEnv.toLowerCase()) {
|
|
71
|
+
warn(
|
|
72
|
+
`bit ci sync: GITHUB_REPOSITORY is "${repoFromEnv}" but the "origin" remote points at ` +
|
|
73
|
+
`"${repoFromRemote}" — using "${repoFromRemote}", the repository this run pushes its branches to.`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return new GitHubClient({ token, repo });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
matchesRemote(remoteUrl: string): boolean {
|
|
80
|
+
return isGitHubRemote(remoteUrl);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** A constructed client always has both halves — it cannot be built without them. */
|
|
84
|
+
isConfigured(): boolean {
|
|
85
|
+
return Boolean(this.token && this.repo);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private async request(method: string, path: string, body?: unknown): Promise<any> {
|
|
89
|
+
const res = await this.fetchImpl(`${API}/repos/${this.repo}${path}`, {
|
|
90
|
+
method,
|
|
91
|
+
headers: {
|
|
92
|
+
authorization: `Bearer ${this.token}`,
|
|
93
|
+
accept: 'application/vnd.github+json',
|
|
94
|
+
'content-type': 'application/json',
|
|
95
|
+
'x-github-api-version': '2022-11-28',
|
|
96
|
+
},
|
|
97
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
98
|
+
});
|
|
99
|
+
if (!res.ok) {
|
|
100
|
+
const text = await res.text().catch(() => '');
|
|
101
|
+
throw new Error(`GitHub API ${method} ${path} failed: ${res.status} ${text}`);
|
|
102
|
+
}
|
|
103
|
+
return res.status === 204 ? undefined : res.json();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private toPrInfo(raw: any): PrInfo {
|
|
107
|
+
return {
|
|
108
|
+
number: raw.number,
|
|
109
|
+
state: raw.state,
|
|
110
|
+
labels: (raw.labels ?? []).map((l: any) => (typeof l === 'string' ? l : l.name)),
|
|
111
|
+
headRef: raw.head?.ref,
|
|
112
|
+
htmlUrl: raw.html_url,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async findPrByBranch(branch: string): Promise<PrInfo | undefined> {
|
|
117
|
+
const owner = this.repo.split('/')[0];
|
|
118
|
+
const list = await this.request('GET', `/pulls?state=open&head=${encodeURIComponent(`${owner}:${branch}`)}`);
|
|
119
|
+
return list.length ? this.toPrInfo(list[0]) : undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async createPr(opts: { head: string; base: string; title: string; body: string }): Promise<PrInfo> {
|
|
123
|
+
return this.toPrInfo(await this.request('POST', '/pulls', opts));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async closePr(prNumber: number, comment?: string): Promise<void> {
|
|
127
|
+
if (comment) await this.comment(prNumber, comment);
|
|
128
|
+
await this.request('PATCH', `/pulls/${prNumber}`, { state: 'closed' });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async comment(prNumber: number, body: string): Promise<void> {
|
|
132
|
+
await this.request('POST', `/issues/${prNumber}/comments`, { body });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async addLabel(prNumber: number, label: string): Promise<void> {
|
|
136
|
+
await this.request('POST', `/issues/${prNumber}/labels`, { labels: [label] });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The registrable GitHub provider: resolves its `GitHubClient` from the environment lazily, since
|
|
142
|
+
* registration happens at aspect load, before credentials are known. A resolved client is memoized;
|
|
143
|
+
* an unresolved one is retried so a later call that does carry the remote URL can still succeed.
|
|
144
|
+
*/
|
|
145
|
+
export class GitHubHostProvider implements GitHostProvider {
|
|
146
|
+
readonly name = 'github';
|
|
147
|
+
|
|
148
|
+
private client: GitHubClient | undefined;
|
|
149
|
+
|
|
150
|
+
/** The last `origin` URL seen, so a PR method can still recover `owner/repo` from it. */
|
|
151
|
+
private remoteHint: string | undefined;
|
|
152
|
+
|
|
153
|
+
constructor(private onWarning: WarnFn = noopWarn) {}
|
|
154
|
+
|
|
155
|
+
matchesRemote(remoteUrl: string): boolean {
|
|
156
|
+
const matches = isGitHubRemote(remoteUrl);
|
|
157
|
+
// Only remember a remote that is ours — a foreign hint could later pair with GITHUB_REPOSITORY
|
|
158
|
+
// into a client for the wrong repository.
|
|
159
|
+
if (matches) this.remoteHint = remoteUrl;
|
|
160
|
+
return matches;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* A known non-GitHub remote can never count as configured, however complete the environment looks:
|
|
165
|
+
* on Actions the env is always set, so answering "configured" for a remote this provider declined to
|
|
166
|
+
* claim would make it the sole-configured fallback and aim PRs at the wrong host's repository.
|
|
167
|
+
*/
|
|
168
|
+
isConfigured(remoteUrl?: string): boolean {
|
|
169
|
+
if (remoteUrl && !isGitHubRemote(remoteUrl)) return false;
|
|
170
|
+
if (remoteUrl) this.remoteHint = remoteUrl;
|
|
171
|
+
return Boolean(this.resolveClient(remoteUrl));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async findPrByBranch(branch: string): Promise<PrInfo | undefined> {
|
|
175
|
+
return this.requireClient().findPrByBranch(branch);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async createPr(opts: { head: string; base: string; title: string; body: string }): Promise<PrInfo> {
|
|
179
|
+
return this.requireClient().createPr(opts);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async closePr(prNumber: number, comment?: string): Promise<void> {
|
|
183
|
+
return this.requireClient().closePr(prNumber, comment);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async comment(prNumber: number, body: string): Promise<void> {
|
|
187
|
+
return this.requireClient().comment(prNumber, body);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async addLabel(prNumber: number, label: string): Promise<void> {
|
|
191
|
+
return this.requireClient().addLabel(prNumber, label);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private resolveClient(remoteUrl?: string): GitHubClient | undefined {
|
|
195
|
+
if (!this.client) this.client = GitHubClient.fromEnv(remoteUrl ?? this.remoteHint, this.onWarning);
|
|
196
|
+
return this.client;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private requireClient(): GitHubClient {
|
|
200
|
+
const client = this.resolveClient();
|
|
201
|
+
if (!client) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
`the github host provider is not configured: set BIT_GITHUB_TOKEN (or GITHUB_TOKEN), and either ` +
|
|
204
|
+
`GITHUB_REPOSITORY or a github.com "origin" remote`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
return client;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import YAML from 'yaml';
|
|
6
|
+
import {
|
|
7
|
+
WORKFLOW_RELATIVE_PATHS,
|
|
8
|
+
renderBitSyncWorkflow,
|
|
9
|
+
renderBitReleaseWorkflow,
|
|
10
|
+
scaffoldWorkflowFiles,
|
|
11
|
+
deriveOwnerRepo,
|
|
12
|
+
renderInitChecklist,
|
|
13
|
+
} from './init-scaffold';
|
|
14
|
+
|
|
15
|
+
const yamlLoad = (src: string) => YAML.parse(src);
|
|
16
|
+
|
|
17
|
+
/** YAML 1.1-schema parsers resolve the plain scalar key `on` to `true`, so accept either key. */
|
|
18
|
+
function onSection(rendered: string): any {
|
|
19
|
+
const doc = yamlLoad(rendered) as any;
|
|
20
|
+
return doc.on ?? doc[true as any];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parsed, not substring-matched: the substitution point sits inside a YAML flow sequence, where `,` and
|
|
25
|
+
* `]` are structural and a substring match is blind to exactly the damage an unquoted value does. `$` is
|
|
26
|
+
* git-legal and `$&`/`$'`/`$$` are special in a string replacement, so those must arrive verbatim too.
|
|
27
|
+
*/
|
|
28
|
+
const BRANCHES = ['main', 'develop', 'release/2026', 'release/main', 'a,b]c', "it's/a-branch", "a$'b", 'a$&b', 'a$$b', 'a$`b']; // prettier-ignore
|
|
29
|
+
|
|
30
|
+
describe('init-scaffold', () => {
|
|
31
|
+
describe('the rendered workflows', () => {
|
|
32
|
+
it('put the default branch in as exactly one element of each template’s branch filter', () => {
|
|
33
|
+
BRANCHES.forEach((branch) => {
|
|
34
|
+
expect(onSection(renderBitSyncWorkflow(branch)).push['branches-ignore'], branch).to.deep.equal([
|
|
35
|
+
branch,
|
|
36
|
+
'bit-sync/**',
|
|
37
|
+
]);
|
|
38
|
+
expect(onSection(renderBitReleaseWorkflow(branch)).pull_request.branches, branch).to.deep.equal([branch]);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('quote the value rather than escape it, and touch nothing else in the document', () => {
|
|
43
|
+
const sync = renderBitSyncWorkflow('develop');
|
|
44
|
+
const release = renderBitReleaseWorkflow('develop');
|
|
45
|
+
expect(sync).to.contain("branches-ignore: ['develop', 'bit-sync/**']");
|
|
46
|
+
expect(release).to.contain("branches: ['develop']");
|
|
47
|
+
// a naive replace-all of "main" would rewrite every one of these
|
|
48
|
+
expect(sync).to.contain('main-sync-branch: bit-sync/main');
|
|
49
|
+
expect(sync).to.contain('bit-main-export');
|
|
50
|
+
expect(sync).to.contain('main export)');
|
|
51
|
+
expect(sync).to.contain('${{ secrets.BIT_CONFIG_ACCESS_TOKEN }}');
|
|
52
|
+
expect(release).to.contain("github.event.pull_request.head.ref != 'bit-sync/main'");
|
|
53
|
+
expect(release).to.contain('main-sync-branch: bit-sync/main');
|
|
54
|
+
// YAML single-quote escaping, and the rest of the document intact
|
|
55
|
+
expect(renderBitSyncWorkflow("it's/a-branch")).to.contain("'it''s/a-branch'");
|
|
56
|
+
expect(yamlLoad(renderBitSyncWorkflow('release/main')).jobs).to.be.an('object');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('scaffoldWorkflowFiles', () => {
|
|
61
|
+
let workspaceDir: string;
|
|
62
|
+
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'init-scaffold-'));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
fs.rmSync(workspaceDir, { recursive: true, force: true });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('writes both workflow files with the default branch substituted, creating .github/workflows/', () => {
|
|
72
|
+
expect(scaffoldWorkflowFiles(workspaceDir, 'develop')).to.deep.equal([
|
|
73
|
+
{ relativePath: WORKFLOW_RELATIVE_PATHS.sync, status: 'written' },
|
|
74
|
+
{ relativePath: WORKFLOW_RELATIVE_PATHS.release, status: 'written' },
|
|
75
|
+
]);
|
|
76
|
+
const read = (rel: string) => fs.readFileSync(path.join(workspaceDir, rel), 'utf8');
|
|
77
|
+
expect(read(WORKFLOW_RELATIVE_PATHS.sync)).to.contain("branches-ignore: ['develop', 'bit-sync/**']");
|
|
78
|
+
expect(read(WORKFLOW_RELATIVE_PATHS.release)).to.contain("branches: ['develop']");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('is idempotent: a second run skips both files rather than overwriting them', () => {
|
|
82
|
+
scaffoldWorkflowFiles(workspaceDir, 'develop');
|
|
83
|
+
expect(scaffoldWorkflowFiles(workspaceDir, 'develop')).to.deep.equal([
|
|
84
|
+
{ relativePath: WORKFLOW_RELATIVE_PATHS.sync, status: 'skipped' },
|
|
85
|
+
{ relativePath: WORKFLOW_RELATIVE_PATHS.release, status: 'skipped' },
|
|
86
|
+
]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('skips only the file that already exists, writing the other one', () => {
|
|
90
|
+
const syncAbsPath = path.join(workspaceDir, WORKFLOW_RELATIVE_PATHS.sync);
|
|
91
|
+
fs.mkdirSync(path.dirname(syncAbsPath), { recursive: true });
|
|
92
|
+
fs.writeFileSync(syncAbsPath, 'hand-edited content, do not touch\n');
|
|
93
|
+
expect(scaffoldWorkflowFiles(workspaceDir, 'develop')).to.deep.equal([
|
|
94
|
+
{ relativePath: WORKFLOW_RELATIVE_PATHS.sync, status: 'skipped' },
|
|
95
|
+
{ relativePath: WORKFLOW_RELATIVE_PATHS.release, status: 'written' },
|
|
96
|
+
]);
|
|
97
|
+
expect(fs.readFileSync(syncAbsPath, 'utf8')).to.equal('hand-edited content, do not touch\n');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('deriveOwnerRepo', () => {
|
|
102
|
+
const REMOTES: Array<[string | undefined, { owner: string; repo: string } | undefined]> = [
|
|
103
|
+
['git@github.com:acme/shop.git', { owner: 'acme', repo: 'shop' }],
|
|
104
|
+
['https://github.com/acme/shop.git', { owner: 'acme', repo: 'shop' }],
|
|
105
|
+
['https://github.com/acme/shop', { owner: 'acme', repo: 'shop' }],
|
|
106
|
+
['https://gitlab.com/acme/shop.git', undefined],
|
|
107
|
+
// a local/bare remote has no host at all
|
|
108
|
+
['/path/to/bare.git', undefined],
|
|
109
|
+
['file:///path/to/bare.git', undefined],
|
|
110
|
+
[undefined, undefined],
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
it('parses a GitHub remote in either form, and nothing else', () => {
|
|
114
|
+
REMOTES.forEach(([remote, expected]) => expect(deriveOwnerRepo(remote), String(remote)).to.deep.equal(expected));
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('renderInitChecklist', () => {
|
|
119
|
+
it('renders a real dispatch URL when owner/repo were derived, a placeholder when not', () => {
|
|
120
|
+
const derived = renderInitChecklist({ owner: 'acme', repo: 'shop' });
|
|
121
|
+
expect(derived).to.contain('https://api.github.com/repos/acme/shop/dispatches');
|
|
122
|
+
expect(derived).to.not.contain('<owner>/<repo>');
|
|
123
|
+
expect(renderInitChecklist(undefined)).to.contain('https://api.github.com/repos/<owner>/<repo>/dispatches');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('always includes the secrets, permissions, webhook and fetch-depth checklist items', () => {
|
|
127
|
+
const checklist = renderInitChecklist(undefined);
|
|
128
|
+
[
|
|
129
|
+
'BIT_CONFIG_ACCESS_TOKEN',
|
|
130
|
+
'BIT_SYNC_GH_TOKEN',
|
|
131
|
+
'contents: write',
|
|
132
|
+
'Components > Export',
|
|
133
|
+
'Authorization: Bearer <PAT>',
|
|
134
|
+
'Accept: application/vnd.github+json',
|
|
135
|
+
'"event_type":"bit-export"',
|
|
136
|
+
'{{laneId}}',
|
|
137
|
+
'drops its custom headers',
|
|
138
|
+
'fetch-depth: 0',
|
|
139
|
+
].forEach((item) => expect(checklist, item).to.contain(item));
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|