@shipfox/api-integration-gitea 11.0.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": "11.0.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": "11.0.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