@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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/api-integration-gitea",
3
3
  "license": "MIT",
4
- "version": "10.2.0",
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": "10.2.0",
23
- "@shipfox/api-integration-spi": "0.2.2",
24
- "@shipfox/api-integration-gitea-dto": "9.0.2",
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.4",
27
- "@shipfox/node-fastify": "0.4.0",
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.4.4"
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'))),