@shipfox/api-integration-gitea 10.2.0 → 12.0.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +28 -0
- package/dist/core/source-control.d.ts +2 -1
- package/dist/core/source-control.d.ts.map +1 -1
- package/dist/core/source-control.js +59 -1
- package/dist/core/source-control.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/src/core/source-control.test.ts +115 -0
- package/src/core/source-control.ts +74 -0
- package/src/presentation/routes/connections.test.ts +4 -4
- package/tsconfig.build.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/api-integration-gitea",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "12.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"drizzle-orm": "^0.45.2",
|
|
22
|
-
"@shipfox/api-auth-context": "
|
|
23
|
-
"@shipfox/api-integration-spi": "0.
|
|
24
|
-
"@shipfox/api-integration-gitea-dto": "
|
|
22
|
+
"@shipfox/api-auth-context": "12.0.0",
|
|
23
|
+
"@shipfox/api-integration-spi": "1.0.0",
|
|
24
|
+
"@shipfox/api-integration-gitea-dto": "12.0.0",
|
|
25
25
|
"@shipfox/config": "1.2.4",
|
|
26
|
-
"@shipfox/node-drizzle": "0.3.
|
|
27
|
-
"@shipfox/node-fastify": "0.4.
|
|
26
|
+
"@shipfox/node-drizzle": "0.3.5",
|
|
27
|
+
"@shipfox/node-fastify": "0.4.1",
|
|
28
28
|
"@shipfox/node-opentelemetry": "0.6.3",
|
|
29
|
-
"@shipfox/node-postgres": "0.
|
|
29
|
+
"@shipfox/node-postgres": "0.5.0"
|
|
30
30
|
},
|
|
31
31
|
"imports": {
|
|
32
32
|
"#*": "./dist/*"
|
|
@@ -3,6 +3,8 @@ import type {GiteaApiClient, GiteaRepository} from '#api/client.js';
|
|
|
3
3
|
import {GiteaIntegrationProviderError} from './errors.js';
|
|
4
4
|
import {GiteaSourceControlProvider} from './source-control.js';
|
|
5
5
|
|
|
6
|
+
const VALID_COMMIT = 'a'.repeat(40);
|
|
7
|
+
|
|
6
8
|
const REPOSITORY: GiteaRepository = {
|
|
7
9
|
ownerLogin: 'shipfox',
|
|
8
10
|
name: 'platform',
|
|
@@ -53,6 +55,119 @@ function connection(): IntegrationConnection<'gitea'> {
|
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
describe('GiteaSourceControlProvider', () => {
|
|
58
|
+
it('normalizes push trigger references', () => {
|
|
59
|
+
const provider = new GiteaSourceControlProvider(giteaClient());
|
|
60
|
+
|
|
61
|
+
const result = provider.resolveTriggerReference({
|
|
62
|
+
ref: 'refs/heads/feature/review',
|
|
63
|
+
after: VALID_COMMIT,
|
|
64
|
+
repository: {full_name: 'shipfox/platform'},
|
|
65
|
+
sender: {login: 'noe'},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(result).toEqual({
|
|
69
|
+
externalRepositoryId: 'gitea:shipfox/platform',
|
|
70
|
+
ref: 'refs/heads/feature/review',
|
|
71
|
+
commit: VALID_COMMIT,
|
|
72
|
+
actor: 'noe',
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('falls back to the sender username when no login is present', () => {
|
|
77
|
+
const provider = new GiteaSourceControlProvider(giteaClient());
|
|
78
|
+
|
|
79
|
+
const result = provider.resolveTriggerReference({
|
|
80
|
+
ref: 'refs/heads/main',
|
|
81
|
+
after: VALID_COMMIT,
|
|
82
|
+
repository: {full_name: 'shipfox/platform'},
|
|
83
|
+
sender: {username: 'noe'},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(result).toMatchObject({actor: 'noe'});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('resolves a null actor when the payload names no sender', () => {
|
|
90
|
+
const provider = new GiteaSourceControlProvider(giteaClient());
|
|
91
|
+
|
|
92
|
+
const result = provider.resolveTriggerReference({
|
|
93
|
+
ref: 'refs/heads/main',
|
|
94
|
+
after: VALID_COMMIT,
|
|
95
|
+
repository: {full_name: 'shipfox/platform'},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(result).toMatchObject({actor: null});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('normalizes pull-request trigger references from the head repository', () => {
|
|
102
|
+
const provider = new GiteaSourceControlProvider(giteaClient());
|
|
103
|
+
|
|
104
|
+
const result = provider.resolveTriggerReference({
|
|
105
|
+
repository: {full_name: 'shipfox/platform'},
|
|
106
|
+
sender: {login: 'noe'},
|
|
107
|
+
pull_request: {
|
|
108
|
+
number: 17,
|
|
109
|
+
head: {
|
|
110
|
+
sha: VALID_COMMIT,
|
|
111
|
+
repo: {full_name: 'shipfox/platform'},
|
|
112
|
+
},
|
|
113
|
+
base: {repo: {full_name: 'shipfox/platform'}},
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(result).toEqual({
|
|
118
|
+
externalRepositoryId: 'gitea:shipfox/platform',
|
|
119
|
+
ref: 'refs/pull/17/head',
|
|
120
|
+
commit: VALID_COMMIT,
|
|
121
|
+
actor: 'noe',
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it.each([
|
|
126
|
+
undefined,
|
|
127
|
+
{},
|
|
128
|
+
{ref: 'refs/heads/main', after: VALID_COMMIT},
|
|
129
|
+
{
|
|
130
|
+
ref: 'refs/heads/feature branch',
|
|
131
|
+
after: VALID_COMMIT,
|
|
132
|
+
repository: {full_name: 'shipfox/platform'},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
ref: 'refs/heads/feature..branch',
|
|
136
|
+
after: VALID_COMMIT,
|
|
137
|
+
repository: {full_name: 'shipfox/platform'},
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
ref: 'refs/heads/main',
|
|
141
|
+
after: '0'.repeat(40),
|
|
142
|
+
repository: {full_name: 'shipfox/platform'},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
ref: 'refs/heads/main',
|
|
146
|
+
after: 'not-a-commit',
|
|
147
|
+
repository: {full_name: 'shipfox/platform'},
|
|
148
|
+
},
|
|
149
|
+
{ref: 'refs/heads/main', after: VALID_COMMIT, repository: {full_name: 'shipfox'}},
|
|
150
|
+
{
|
|
151
|
+
ref: 'refs/heads/main',
|
|
152
|
+
after: VALID_COMMIT,
|
|
153
|
+
repository: {full_name: 'shipfox/platform/extra'},
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
repository: {full_name: 'shipfox/platform'},
|
|
157
|
+
pull_request: {
|
|
158
|
+
number: 17,
|
|
159
|
+
head: {sha: VALID_COMMIT, repo: {full_name: 'fork/platform'}},
|
|
160
|
+
base: {repo: {full_name: 'shipfox/platform'}},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
])('returns null for an unresolvable or unsafe trigger payload', (payload) => {
|
|
164
|
+
const provider = new GiteaSourceControlProvider(giteaClient());
|
|
165
|
+
|
|
166
|
+
const result = provider.resolveTriggerReference(payload);
|
|
167
|
+
|
|
168
|
+
expect(result).toBeNull();
|
|
169
|
+
});
|
|
170
|
+
|
|
56
171
|
it('lists org repositories scoped to the connection account', async () => {
|
|
57
172
|
const gitea = giteaClient();
|
|
58
173
|
const provider = new GiteaSourceControlProvider(gitea);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {Buffer} from 'node:buffer';
|
|
2
2
|
import {giteaProviderKind} from '@shipfox/api-integration-gitea-dto';
|
|
3
3
|
import {
|
|
4
|
+
asRecord,
|
|
4
5
|
buildProviderRepositoryId,
|
|
5
6
|
type CheckoutSpec,
|
|
6
7
|
type CreateCheckoutSpecInput,
|
|
@@ -8,15 +9,21 @@ import {
|
|
|
8
9
|
type FilePage,
|
|
9
10
|
type FileSnapshot,
|
|
10
11
|
type IntegrationConnection,
|
|
12
|
+
isRecord,
|
|
13
|
+
isValidGitObjectId,
|
|
14
|
+
isValidTriggerRef,
|
|
11
15
|
type ListFilesInput,
|
|
12
16
|
type ListRepositoriesInput,
|
|
13
17
|
MAX_REPOSITORY_FILE_BYTES,
|
|
18
|
+
nonEmptyString,
|
|
14
19
|
parseProviderRepositoryId,
|
|
20
|
+
positiveInteger,
|
|
15
21
|
type RepositoryPage,
|
|
16
22
|
type RepositorySnapshot,
|
|
17
23
|
type RepositoryVisibility,
|
|
18
24
|
type ResolveRepositoryInput,
|
|
19
25
|
type SourceControlProvider,
|
|
26
|
+
type TriggerReference,
|
|
20
27
|
} from '@shipfox/api-integration-spi';
|
|
21
28
|
import type {GiteaApiClient, GiteaRepository} from '#api/client.js';
|
|
22
29
|
import {config, giteaCloneBaseOrigin} from '#config.js';
|
|
@@ -134,6 +141,48 @@ export class GiteaSourceControlProvider
|
|
|
134
141
|
};
|
|
135
142
|
}
|
|
136
143
|
|
|
144
|
+
resolveTriggerReference(payload: unknown): TriggerReference | null {
|
|
145
|
+
if (!isRecord(payload)) return null;
|
|
146
|
+
|
|
147
|
+
const actor = giteaEventActor(payload);
|
|
148
|
+
const pullRequest = asRecord(payload.pull_request);
|
|
149
|
+
if (pullRequest) {
|
|
150
|
+
const head = asRecord(pullRequest.head);
|
|
151
|
+
const repository = asRecord(head?.repo);
|
|
152
|
+
const base = asRecord(pullRequest.base);
|
|
153
|
+
const baseRepository = asRecord(base?.repo) ?? asRecord(payload.repository);
|
|
154
|
+
if (!repository || !baseRepository || !sameGiteaRepository(repository, baseRepository)) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
const repositoryId = giteaRepositoryId(repository);
|
|
158
|
+
const number = positiveInteger(pullRequest.number);
|
|
159
|
+
const commit = nonEmptyString(head?.sha);
|
|
160
|
+
const ref = number === null ? null : `refs/pull/${number}/head`;
|
|
161
|
+
if (!repositoryId || !ref || !commit) return null;
|
|
162
|
+
const hasValidReference = isValidGitObjectId(commit) && isValidTriggerRef(ref);
|
|
163
|
+
if (!hasValidReference) return null;
|
|
164
|
+
return {
|
|
165
|
+
externalRepositoryId: buildProviderRepositoryId(giteaProviderKind, repositoryId),
|
|
166
|
+
ref,
|
|
167
|
+
commit,
|
|
168
|
+
actor,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const repositoryId = giteaRepositoryId(asRecord(payload.repository));
|
|
173
|
+
const ref = nonEmptyString(payload.ref);
|
|
174
|
+
const commit = nonEmptyString(payload.after);
|
|
175
|
+
if (!repositoryId || !ref || !commit) return null;
|
|
176
|
+
const hasValidReference = isValidGitObjectId(commit) && isValidTriggerRef(ref);
|
|
177
|
+
if (!hasValidReference) return null;
|
|
178
|
+
return {
|
|
179
|
+
externalRepositoryId: buildProviderRepositoryId(giteaProviderKind, repositoryId),
|
|
180
|
+
ref,
|
|
181
|
+
commit,
|
|
182
|
+
actor,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
137
186
|
async createCheckoutSpec(
|
|
138
187
|
input: CreateCheckoutSpecInput<GiteaIntegrationConnection>,
|
|
139
188
|
): Promise<CheckoutSpec> {
|
|
@@ -160,6 +209,31 @@ export class GiteaSourceControlProvider
|
|
|
160
209
|
}
|
|
161
210
|
}
|
|
162
211
|
|
|
212
|
+
function giteaRepositoryId(repository: Record<string, unknown> | null): string | null {
|
|
213
|
+
const fullName = nonEmptyString(repository?.full_name);
|
|
214
|
+
if (!fullName) return null;
|
|
215
|
+
const separatorIndex = fullName.indexOf('/');
|
|
216
|
+
const owner = separatorIndex > 0 ? fullName.slice(0, separatorIndex) : '';
|
|
217
|
+
const repo = separatorIndex > 0 ? fullName.slice(separatorIndex + 1) : '';
|
|
218
|
+
return owner && repo && !repo.includes('/') ? `${owner}/${repo}` : null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Gitea mirrors GitHub's webhook envelope but names the handle `username` on some events
|
|
222
|
+
// and `login` on others, so both are read before giving up on an actor.
|
|
223
|
+
function giteaEventActor(payload: Record<string, unknown>): string | null {
|
|
224
|
+
const sender = asRecord(payload.sender);
|
|
225
|
+
return nonEmptyString(sender?.login) ?? nonEmptyString(sender?.username);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function sameGiteaRepository(
|
|
229
|
+
first: Record<string, unknown>,
|
|
230
|
+
second: Record<string, unknown>,
|
|
231
|
+
): boolean {
|
|
232
|
+
const firstId = giteaRepositoryId(first);
|
|
233
|
+
const secondId = giteaRepositoryId(second);
|
|
234
|
+
return Boolean(firstId && secondId && firstId.toLowerCase() === secondId.toLowerCase());
|
|
235
|
+
}
|
|
236
|
+
|
|
163
237
|
function createCheckoutRepositoryUrl(cloneUrl: string): string {
|
|
164
238
|
if (!giteaCloneBaseOrigin) return cloneUrl;
|
|
165
239
|
|
|
@@ -116,7 +116,7 @@ describe('Gitea connection routes', () => {
|
|
|
116
116
|
it('connects an org and returns the connection DTO', async () => {
|
|
117
117
|
const app = await createTestApp();
|
|
118
118
|
const workspaceId = crypto.randomUUID();
|
|
119
|
-
authenticatedMemberships = [{workspaceId, role: 'admin'}];
|
|
119
|
+
authenticatedMemberships = [{workspaceId, role: 'admin', workspaceStatus: 'active'}];
|
|
120
120
|
|
|
121
121
|
const res = await app.inject({
|
|
122
122
|
method: 'POST',
|
|
@@ -137,7 +137,7 @@ describe('Gitea connection routes', () => {
|
|
|
137
137
|
gitea: giteaClient({organizationExists: vi.fn(() => Promise.resolve(false))}),
|
|
138
138
|
});
|
|
139
139
|
const workspaceId = crypto.randomUUID();
|
|
140
|
-
authenticatedMemberships = [{workspaceId, role: 'admin'}];
|
|
140
|
+
authenticatedMemberships = [{workspaceId, role: 'admin', workspaceStatus: 'active'}];
|
|
141
141
|
|
|
142
142
|
const res = await app.inject({
|
|
143
143
|
method: 'POST',
|
|
@@ -152,7 +152,7 @@ describe('Gitea connection routes', () => {
|
|
|
152
152
|
|
|
153
153
|
it('returns 409 when the org is already linked to another workspace', async () => {
|
|
154
154
|
const workspaceId = crypto.randomUUID();
|
|
155
|
-
authenticatedMemberships = [{workspaceId, role: 'admin'}];
|
|
155
|
+
authenticatedMemberships = [{workspaceId, role: 'admin', workspaceStatus: 'active'}];
|
|
156
156
|
const app = await createTestApp({
|
|
157
157
|
existingConnection: {
|
|
158
158
|
id: crypto.randomUUID(),
|
|
@@ -180,7 +180,7 @@ describe('Gitea connection routes', () => {
|
|
|
180
180
|
|
|
181
181
|
it('returns 409 when connection slug allocation conflicts repeatedly', async () => {
|
|
182
182
|
const workspaceId = crypto.randomUUID();
|
|
183
|
-
authenticatedMemberships = [{workspaceId, role: 'admin'}];
|
|
183
|
+
authenticatedMemberships = [{workspaceId, role: 'admin', workspaceStatus: 'active'}];
|
|
184
184
|
const app = await createTestApp({
|
|
185
185
|
connectGiteaConnection: vi.fn(() =>
|
|
186
186
|
Promise.reject(new ConnectionSlugConflictError(new Error('duplicate slug'))),
|