@shipfox/api-integration-gitea 2.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.
Files changed (119) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/.turbo/turbo-type$colon$emit.log +1 -0
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +96 -0
  5. package/LICENSE +21 -0
  6. package/README.md +61 -0
  7. package/dist/api/client.d.ts +58 -0
  8. package/dist/api/client.d.ts.map +1 -0
  9. package/dist/api/client.js +225 -0
  10. package/dist/api/client.js.map +1 -0
  11. package/dist/config.d.ts +11 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +38 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/core/connect.d.ts +18 -0
  16. package/dist/core/connect.d.ts.map +1 -0
  17. package/dist/core/connect.js +32 -0
  18. package/dist/core/connect.js.map +1 -0
  19. package/dist/core/connection-url.d.ts +2 -0
  20. package/dist/core/connection-url.d.ts.map +1 -0
  21. package/dist/core/connection-url.js +8 -0
  22. package/dist/core/connection-url.js.map +1 -0
  23. package/dist/core/errors.d.ts +10 -0
  24. package/dist/core/errors.d.ts.map +1 -0
  25. package/dist/core/errors.js +17 -0
  26. package/dist/core/errors.js.map +1 -0
  27. package/dist/core/source-control.d.ts +14 -0
  28. package/dist/core/source-control.d.ts.map +1 -0
  29. package/dist/core/source-control.js +177 -0
  30. package/dist/core/source-control.js.map +1 -0
  31. package/dist/core/webhook.d.ts +38 -0
  32. package/dist/core/webhook.d.ts.map +1 -0
  33. package/dist/core/webhook.js +162 -0
  34. package/dist/core/webhook.js.map +1 -0
  35. package/dist/db/connections.d.ts +19 -0
  36. package/dist/db/connections.d.ts.map +1 -0
  37. package/dist/db/connections.js +42 -0
  38. package/dist/db/connections.js.map +1 -0
  39. package/dist/db/db.d.ts +191 -0
  40. package/dist/db/db.d.ts.map +1 -0
  41. package/dist/db/db.js +18 -0
  42. package/dist/db/db.js.map +1 -0
  43. package/dist/db/migrations.d.ts +2 -0
  44. package/dist/db/migrations.d.ts.map +1 -0
  45. package/dist/db/migrations.js +5 -0
  46. package/dist/db/migrations.js.map +1 -0
  47. package/dist/db/schema/common.d.ts +2 -0
  48. package/dist/db/schema/common.d.ts.map +1 -0
  49. package/dist/db/schema/common.js +4 -0
  50. package/dist/db/schema/common.js.map +1 -0
  51. package/dist/db/schema/connections.d.ts +97 -0
  52. package/dist/db/schema/connections.d.ts.map +1 -0
  53. package/dist/db/schema/connections.js +28 -0
  54. package/dist/db/schema/connections.js.map +1 -0
  55. package/dist/index.d.ts +39 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +42 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/presentation/dto/integrations.d.ts +17 -0
  60. package/dist/presentation/dto/integrations.d.ts.map +1 -0
  61. package/dist/presentation/dto/integrations.js +11 -0
  62. package/dist/presentation/dto/integrations.js.map +1 -0
  63. package/dist/presentation/routes/connections.d.ts +13 -0
  64. package/dist/presentation/routes/connections.d.ts.map +1 -0
  65. package/dist/presentation/routes/connections.js +47 -0
  66. package/dist/presentation/routes/connections.js.map +1 -0
  67. package/dist/presentation/routes/errors.d.ts +2 -0
  68. package/dist/presentation/routes/errors.d.ts.map +1 -0
  69. package/dist/presentation/routes/errors.js +36 -0
  70. package/dist/presentation/routes/errors.js.map +1 -0
  71. package/dist/presentation/routes/webhooks.d.ts +11 -0
  72. package/dist/presentation/routes/webhooks.d.ts.map +1 -0
  73. package/dist/presentation/routes/webhooks.js +109 -0
  74. package/dist/presentation/routes/webhooks.js.map +1 -0
  75. package/dist/tsconfig.test.tsbuildinfo +1 -0
  76. package/drizzle/0000_initial.sql +10 -0
  77. package/drizzle/meta/0000_snapshot.json +96 -0
  78. package/drizzle/meta/_journal.json +13 -0
  79. package/drizzle.config.ts +7 -0
  80. package/package.json +66 -0
  81. package/src/api/client.test.ts +367 -0
  82. package/src/api/client.ts +364 -0
  83. package/src/config.ts +42 -0
  84. package/src/connection-external-url.test.ts +36 -0
  85. package/src/core/connect.test.ts +149 -0
  86. package/src/core/connect.ts +52 -0
  87. package/src/core/connection-url.ts +8 -0
  88. package/src/core/errors.ts +17 -0
  89. package/src/core/source-control-clone-url.test.ts +83 -0
  90. package/src/core/source-control.test.ts +349 -0
  91. package/src/core/source-control.ts +231 -0
  92. package/src/core/webhook.test.ts +396 -0
  93. package/src/core/webhook.ts +203 -0
  94. package/src/db/connections.test.ts +69 -0
  95. package/src/db/connections.ts +80 -0
  96. package/src/db/db.ts +18 -0
  97. package/src/db/migrations.ts +4 -0
  98. package/src/db/schema/common.ts +3 -0
  99. package/src/db/schema/connections.ts +32 -0
  100. package/src/index.test.ts +23 -0
  101. package/src/index.ts +83 -0
  102. package/src/presentation/dto/integrations.ts +14 -0
  103. package/src/presentation/routes/connections.test.ts +200 -0
  104. package/src/presentation/routes/connections.ts +63 -0
  105. package/src/presentation/routes/errors.ts +35 -0
  106. package/src/presentation/routes/webhooks.test.ts +188 -0
  107. package/src/presentation/routes/webhooks.ts +111 -0
  108. package/test/env.ts +11 -0
  109. package/test/factories/gitea-connection.ts +19 -0
  110. package/test/factories/index.ts +1 -0
  111. package/test/fixtures/gitea-webhook.ts +237 -0
  112. package/test/globalSetup.ts +14 -0
  113. package/test/index.ts +6 -0
  114. package/test/setup.ts +17 -0
  115. package/tsconfig.build.json +9 -0
  116. package/tsconfig.build.tsbuildinfo +1 -0
  117. package/tsconfig.json +3 -0
  118. package/tsconfig.test.json +9 -0
  119. package/vitest.config.ts +12 -0
@@ -0,0 +1,188 @@
1
+ import {createHmac, randomUUID} from 'node:crypto';
2
+ import {closeApp, createApp} from '@shipfox/node-fastify';
3
+ import type {FastifyInstance} from 'fastify';
4
+ import {giteaPushPayload} from '#test/index.js';
5
+ import {createGiteaWebhookRoutes} from './webhooks.js';
6
+
7
+ // Must match GITEA_WEBHOOK_SECRET in test/env.ts.
8
+ const WEBHOOK_SECRET = 'test-webhook-secret';
9
+
10
+ interface TestApp {
11
+ app: FastifyInstance;
12
+ publishSourcePush: ReturnType<typeof vi.fn>;
13
+ recordDeliveryOnly: ReturnType<typeof vi.fn>;
14
+ getIntegrationConnectionById: ReturnType<typeof vi.fn>;
15
+ }
16
+
17
+ async function createTestApp(): Promise<TestApp> {
18
+ const publishSourcePush = vi.fn(() => Promise.resolve({published: true}));
19
+ const recordDeliveryOnly = vi.fn(() => Promise.resolve());
20
+ const getIntegrationConnectionById = vi.fn();
21
+ const routes = createGiteaWebhookRoutes({
22
+ coreDb: () =>
23
+ ({
24
+ transaction: (fn: (tx: unknown) => Promise<unknown>) => fn({}),
25
+ }) as never,
26
+ publishSourcePush,
27
+ recordDeliveryOnly,
28
+ getIntegrationConnectionById,
29
+ });
30
+ const app = await createApp({routes: [routes], swagger: false});
31
+ await app.ready();
32
+ return {app, publishSourcePush, recordDeliveryOnly, getIntegrationConnectionById};
33
+ }
34
+
35
+ function signedHeaders(rawBody: string, event: string, deliveryId: string) {
36
+ const signature = createHmac('sha256', WEBHOOK_SECRET).update(rawBody).digest('hex');
37
+ return {
38
+ 'content-type': 'application/json',
39
+ 'x-gitea-signature': signature,
40
+ 'x-gitea-event': event,
41
+ 'x-gitea-delivery': deliveryId,
42
+ };
43
+ }
44
+
45
+ describe('Gitea webhook route', () => {
46
+ beforeEach(async () => {
47
+ await closeApp();
48
+ });
49
+
50
+ afterEach(async () => {
51
+ await closeApp();
52
+ });
53
+
54
+ it('accepts a signed non-push event and records the delivery', async () => {
55
+ const {app, publishSourcePush, recordDeliveryOnly} = await createTestApp();
56
+ const deliveryId = randomUUID();
57
+ const body = JSON.stringify({hello: 'world'});
58
+
59
+ const res = await app.inject({
60
+ method: 'POST',
61
+ url: '/webhooks/integrations/gitea',
62
+ headers: signedHeaders(body, 'create', deliveryId),
63
+ payload: body,
64
+ });
65
+
66
+ expect(res.statusCode).toBe(204);
67
+ expect(publishSourcePush).not.toHaveBeenCalled();
68
+ expect(recordDeliveryOnly).toHaveBeenCalledTimes(1);
69
+ expect(recordDeliveryOnly.mock.calls[0]?.[0]).toMatchObject({provider: 'gitea', deliveryId});
70
+ });
71
+
72
+ it('rejects an invalid signature with 401 and persists nothing', async () => {
73
+ const {app, publishSourcePush, recordDeliveryOnly} = await createTestApp();
74
+ const body = JSON.stringify(
75
+ giteaPushPayload({
76
+ owner: 'shipfox',
77
+ repo: 'api',
78
+ ref: 'refs/heads/main',
79
+ defaultBranch: 'main',
80
+ sha: 'zzz',
81
+ }),
82
+ );
83
+
84
+ const res = await app.inject({
85
+ method: 'POST',
86
+ url: '/webhooks/integrations/gitea',
87
+ headers: {
88
+ 'content-type': 'application/json',
89
+ 'x-gitea-signature': 'deadbeef',
90
+ 'x-gitea-event': 'push',
91
+ 'x-gitea-delivery': randomUUID(),
92
+ },
93
+ payload: body,
94
+ });
95
+
96
+ expect(res.statusCode).toBe(401);
97
+ expect(publishSourcePush).not.toHaveBeenCalled();
98
+ expect(recordDeliveryOnly).not.toHaveBeenCalled();
99
+ });
100
+
101
+ it('rejects a request missing the delivery header with 400', async () => {
102
+ const {app} = await createTestApp();
103
+ const body = JSON.stringify({hello: 'world'});
104
+
105
+ const res = await app.inject({
106
+ method: 'POST',
107
+ url: '/webhooks/integrations/gitea',
108
+ headers: {
109
+ 'content-type': 'application/json',
110
+ 'x-gitea-signature': createHmac('sha256', WEBHOOK_SECRET).update(body).digest('hex'),
111
+ 'x-gitea-event': 'push',
112
+ },
113
+ payload: body,
114
+ });
115
+
116
+ expect(res.statusCode).toBe(400);
117
+ });
118
+
119
+ it('rejects a request missing the signature header with 401', async () => {
120
+ const {app} = await createTestApp();
121
+ const body = JSON.stringify({hello: 'world'});
122
+
123
+ const res = await app.inject({
124
+ method: 'POST',
125
+ url: '/webhooks/integrations/gitea',
126
+ headers: {
127
+ 'content-type': 'application/json',
128
+ 'x-gitea-event': 'push',
129
+ 'x-gitea-delivery': randomUUID(),
130
+ },
131
+ payload: body,
132
+ });
133
+
134
+ expect(res.statusCode).toBe(401);
135
+ });
136
+
137
+ it('rejects a request missing the event header with 400', async () => {
138
+ const {app} = await createTestApp();
139
+ const body = JSON.stringify({hello: 'world'});
140
+
141
+ const res = await app.inject({
142
+ method: 'POST',
143
+ url: '/webhooks/integrations/gitea',
144
+ headers: {
145
+ 'content-type': 'application/json',
146
+ 'x-gitea-signature': createHmac('sha256', WEBHOOK_SECRET).update(body).digest('hex'),
147
+ 'x-gitea-delivery': randomUUID(),
148
+ },
149
+ payload: body,
150
+ });
151
+
152
+ expect(res.statusCode).toBe(400);
153
+ });
154
+
155
+ it('rejects malformed JSON after a valid signature with 400', async () => {
156
+ const {app, publishSourcePush, recordDeliveryOnly} = await createTestApp();
157
+ const body = '{not valid json';
158
+
159
+ const res = await app.inject({
160
+ method: 'POST',
161
+ url: '/webhooks/integrations/gitea',
162
+ headers: signedHeaders(body, 'push', randomUUID()),
163
+ payload: body,
164
+ });
165
+
166
+ expect(res.statusCode).toBe(400);
167
+ expect(res.json()).toMatchObject({error: 'malformed JSON'});
168
+ expect(publishSourcePush).not.toHaveBeenCalled();
169
+ expect(recordDeliveryOnly).not.toHaveBeenCalled();
170
+ });
171
+
172
+ it('rejects push payloads that fail schema validation with 400', async () => {
173
+ const {app, publishSourcePush, recordDeliveryOnly} = await createTestApp();
174
+ const body = JSON.stringify({ref: 'refs/heads/main'});
175
+
176
+ const res = await app.inject({
177
+ method: 'POST',
178
+ url: '/webhooks/integrations/gitea',
179
+ headers: signedHeaders(body, 'push', randomUUID()),
180
+ payload: body,
181
+ });
182
+
183
+ expect(res.statusCode).toBe(400);
184
+ expect(res.json()).toMatchObject({error: 'malformed push payload'});
185
+ expect(publishSourcePush).not.toHaveBeenCalled();
186
+ expect(recordDeliveryOnly).not.toHaveBeenCalled();
187
+ });
188
+ });
@@ -0,0 +1,111 @@
1
+ import {Buffer} from 'node:buffer';
2
+ import type {
3
+ GetIntegrationConnectionByIdFn,
4
+ PublishSourcePushFn,
5
+ RecordDeliveryOnlyFn,
6
+ } from '@shipfox/api-integration-core-dto';
7
+ import {
8
+ defineRoute,
9
+ type RouteGroup,
10
+ rawBodyPlugin,
11
+ verifyHexHmacSignature,
12
+ WEBHOOK_BODY_LIMIT,
13
+ } from '@shipfox/node-fastify';
14
+ import {logger} from '@shipfox/node-opentelemetry';
15
+ import type {NodePgDatabase} from 'drizzle-orm/node-postgres';
16
+ import {config} from '#config.js';
17
+ import {
18
+ GiteaWebhookMalformedJsonError,
19
+ GiteaWebhookMalformedPushPayloadError,
20
+ handleGiteaWebhook,
21
+ } from '#core/webhook.js';
22
+
23
+ const SIGNATURE_HEADER = 'x-gitea-signature';
24
+ const EVENT_HEADER = 'x-gitea-event';
25
+ const DELIVERY_HEADER = 'x-gitea-delivery';
26
+
27
+ export interface CreateGiteaWebhookRoutesOptions {
28
+ coreDb: () => NodePgDatabase<Record<string, unknown>>;
29
+ publishSourcePush: PublishSourcePushFn;
30
+ recordDeliveryOnly: RecordDeliveryOnlyFn;
31
+ getIntegrationConnectionById: GetIntegrationConnectionByIdFn;
32
+ }
33
+
34
+ export function createGiteaWebhookRoutes(options: CreateGiteaWebhookRoutesOptions): RouteGroup {
35
+ const pushRoute = defineRoute({
36
+ method: 'POST',
37
+ path: '/',
38
+ auth: [],
39
+ description: 'Gitea org webhook receiver.',
40
+ options: {bodyLimit: WEBHOOK_BODY_LIMIT},
41
+ handler: async (request, reply) => {
42
+ const deliveryId = request.headers[DELIVERY_HEADER];
43
+ const signature = request.headers[SIGNATURE_HEADER];
44
+ const event = request.headers[EVENT_HEADER];
45
+
46
+ if (typeof deliveryId !== 'string' || !deliveryId) {
47
+ reply.code(400);
48
+ return {error: 'missing X-Gitea-Delivery header'};
49
+ }
50
+ if (typeof signature !== 'string' || !signature) {
51
+ reply.code(401);
52
+ return {error: 'missing X-Gitea-Signature header'};
53
+ }
54
+ if (typeof event !== 'string' || !event) {
55
+ reply.code(400);
56
+ return {error: 'missing X-Gitea-Event header'};
57
+ }
58
+
59
+ const body = request.body;
60
+ if (!Buffer.isBuffer(body)) {
61
+ reply.code(400);
62
+ return {error: 'expected raw JSON body'};
63
+ }
64
+ const rawBody = body.toString('utf8');
65
+
66
+ if (!verifyHexHmacSignature({rawBody, signature, secret: config.GITEA_WEBHOOK_SECRET})) {
67
+ reply.code(401);
68
+ return {error: 'invalid signature'};
69
+ }
70
+
71
+ try {
72
+ await options.coreDb().transaction(async (tx) => {
73
+ await handleGiteaWebhook({
74
+ tx,
75
+ deliveryId,
76
+ event,
77
+ rawBody,
78
+ publishSourcePush: options.publishSourcePush,
79
+ recordDeliveryOnly: options.recordDeliveryOnly,
80
+ getIntegrationConnectionById: options.getIntegrationConnectionById,
81
+ });
82
+ });
83
+ } catch (error) {
84
+ if (error instanceof GiteaWebhookMalformedJsonError) {
85
+ logger().warn({deliveryId, err: error}, 'gitea webhook payload JSON parse failed');
86
+ reply.code(400);
87
+ return {error: 'malformed JSON'};
88
+ }
89
+ if (error instanceof GiteaWebhookMalformedPushPayloadError) {
90
+ logger().warn(
91
+ {deliveryId, issues: error.issues},
92
+ 'gitea webhook push payload failed schema validation',
93
+ );
94
+ reply.code(400);
95
+ return {error: 'malformed push payload'};
96
+ }
97
+ throw error;
98
+ }
99
+
100
+ reply.code(204);
101
+ return null;
102
+ },
103
+ });
104
+
105
+ return {
106
+ prefix: '/webhooks/integrations/gitea',
107
+ auth: [],
108
+ plugins: [rawBodyPlugin],
109
+ routes: [pushRoute],
110
+ };
111
+ }
package/test/env.ts ADDED
@@ -0,0 +1,11 @@
1
+ process.env.POSTGRES_HOST ??= 'localhost';
2
+ process.env.POSTGRES_PORT ??= '5432';
3
+ process.env.POSTGRES_USERNAME ??= 'shipfox';
4
+ process.env.POSTGRES_PASSWORD ??= 'password';
5
+ process.env.POSTGRES_DATABASE = 'api_test';
6
+ process.env.POSTGRES_MAX_CONNECTIONS ??= '5';
7
+ process.env.TZ = 'UTC';
8
+ process.env.GITEA_BASE_URL = 'https://gitea.example.com';
9
+ process.env.GITEA_SERVICE_USERNAME = 'shipfox-bot';
10
+ process.env.GITEA_SERVICE_TOKEN = 'test-service-token';
11
+ process.env.GITEA_WEBHOOK_SECRET = 'test-webhook-secret';
@@ -0,0 +1,19 @@
1
+ import {Factory} from 'fishery';
2
+ import {type GiteaConnection, upsertGiteaConnection} from '#db/connections.js';
3
+
4
+ export const giteaConnectionFactory = Factory.define<GiteaConnection>(({sequence, onCreate}) => {
5
+ onCreate((connection) =>
6
+ upsertGiteaConnection({
7
+ connectionId: connection.connectionId,
8
+ org: connection.org,
9
+ }),
10
+ );
11
+
12
+ return {
13
+ id: crypto.randomUUID(),
14
+ connectionId: crypto.randomUUID(),
15
+ org: `org-${sequence + 1}`,
16
+ createdAt: new Date(),
17
+ updatedAt: new Date(),
18
+ };
19
+ });
@@ -0,0 +1 @@
1
+ export {giteaConnectionFactory} from './gitea-connection.js';
@@ -0,0 +1,237 @@
1
+ // Real Gitea 1.26.2 push payload fixture; keep extra fields to catch schema drift.
2
+ export const capturedGiteaPushPayload = {
3
+ ref: 'refs/heads/main',
4
+ before: '119a2bb6566c3ec17be9b1aaa8090055aa776034',
5
+ after: 'abd1bff3de1a1f5c375a3e6bd06324469141819e',
6
+ compare_url:
7
+ 'http://localhost:3000/shipfox/api/compare/119a2bb6566c3ec17be9b1aaa8090055aa776034...abd1bff3de1a1f5c375a3e6bd06324469141819e',
8
+ commits: [
9
+ {
10
+ id: 'abd1bff3de1a1f5c375a3e6bd06324469141819e',
11
+ message: 'Seed test/handler.test.js\n',
12
+ url: 'http://localhost:3000/shipfox/api/commit/abd1bff3de1a1f5c375a3e6bd06324469141819e',
13
+ author: {
14
+ name: 'gitea-admin',
15
+ email: 'gitea-admin@shipfox.local',
16
+ username: 'gitea-admin',
17
+ },
18
+ committer: {
19
+ name: 'gitea-admin',
20
+ email: 'gitea-admin@shipfox.local',
21
+ username: 'gitea-admin',
22
+ },
23
+ verification: null,
24
+ timestamp: '2026-06-21T03:08:17Z',
25
+ added: ['test/handler.test.js'],
26
+ removed: [],
27
+ modified: [],
28
+ },
29
+ ],
30
+ total_commits: 1,
31
+ head_commit: {
32
+ id: 'abd1bff3de1a1f5c375a3e6bd06324469141819e',
33
+ message: 'Seed test/handler.test.js\n',
34
+ url: 'http://localhost:3000/shipfox/api/commit/abd1bff3de1a1f5c375a3e6bd06324469141819e',
35
+ author: {
36
+ name: 'gitea-admin',
37
+ email: 'gitea-admin@shipfox.local',
38
+ username: 'gitea-admin',
39
+ },
40
+ committer: {
41
+ name: 'gitea-admin',
42
+ email: 'gitea-admin@shipfox.local',
43
+ username: 'gitea-admin',
44
+ },
45
+ verification: null,
46
+ timestamp: '2026-06-21T03:08:17Z',
47
+ added: ['test/handler.test.js'],
48
+ removed: [],
49
+ modified: [],
50
+ },
51
+ repository: {
52
+ id: 1,
53
+ owner: {
54
+ id: 3,
55
+ login: 'shipfox',
56
+ login_name: '',
57
+ source_id: 0,
58
+ full_name: '',
59
+ email: '',
60
+ avatar_url: 'http://localhost:3000/avatars/d02dc28a1b62136b6e1d5dbc3700c59d',
61
+ html_url: 'http://localhost:3000/shipfox',
62
+ language: '',
63
+ is_admin: false,
64
+ last_login: '0001-01-01T00:00:00Z',
65
+ created: '2026-06-21T03:08:17Z',
66
+ restricted: false,
67
+ active: false,
68
+ prohibit_login: false,
69
+ location: '',
70
+ website: '',
71
+ description: '',
72
+ visibility: 'public',
73
+ followers_count: 0,
74
+ following_count: 0,
75
+ starred_repos_count: 0,
76
+ username: 'shipfox',
77
+ },
78
+ name: 'api',
79
+ full_name: 'shipfox/api',
80
+ description: '',
81
+ empty: false,
82
+ private: true,
83
+ fork: false,
84
+ template: false,
85
+ mirror: false,
86
+ size: 23,
87
+ language: '',
88
+ languages_url: 'http://localhost:3000/api/v1/repos/shipfox/api/languages',
89
+ html_url: 'http://localhost:3000/shipfox/api',
90
+ url: 'http://localhost:3000/api/v1/repos/shipfox/api',
91
+ link: '',
92
+ ssh_url: 'ssh://git@localhost:2222/shipfox/api.git',
93
+ clone_url: 'http://localhost:3000/shipfox/api.git',
94
+ original_url: '',
95
+ website: '',
96
+ stars_count: 0,
97
+ forks_count: 0,
98
+ watchers_count: 2,
99
+ branch_count: 1,
100
+ open_issues_count: 0,
101
+ open_pr_counter: 0,
102
+ release_counter: 0,
103
+ default_branch: 'main',
104
+ archived: false,
105
+ created_at: '2026-06-21T03:08:17Z',
106
+ updated_at: '2026-06-21T03:08:17Z',
107
+ archived_at: '1970-01-01T00:00:00Z',
108
+ permissions: {
109
+ admin: true,
110
+ push: true,
111
+ pull: true,
112
+ },
113
+ has_code: true,
114
+ has_issues: true,
115
+ internal_tracker: {
116
+ enable_time_tracker: true,
117
+ allow_only_contributors_to_track_time: true,
118
+ enable_issue_dependencies: true,
119
+ },
120
+ has_wiki: true,
121
+ has_pull_requests: true,
122
+ has_projects: true,
123
+ projects_mode: 'all',
124
+ has_releases: true,
125
+ has_packages: true,
126
+ has_actions: true,
127
+ ignore_whitespace_conflicts: false,
128
+ allow_merge_commits: true,
129
+ allow_rebase: true,
130
+ allow_rebase_explicit: true,
131
+ allow_squash_merge: true,
132
+ allow_fast_forward_only_merge: true,
133
+ allow_rebase_update: true,
134
+ allow_manual_merge: false,
135
+ autodetect_manual_merge: false,
136
+ default_delete_branch_after_merge: false,
137
+ default_merge_style: 'merge',
138
+ default_allow_maintainer_edit: true,
139
+ avatar_url: '',
140
+ internal: false,
141
+ mirror_interval: '',
142
+ object_format_name: 'sha1',
143
+ mirror_updated: '0001-01-01T00:00:00Z',
144
+ topics: [],
145
+ licenses: [],
146
+ },
147
+ pusher: {
148
+ id: 1,
149
+ login: 'gitea-admin',
150
+ login_name: '',
151
+ source_id: 0,
152
+ full_name: '',
153
+ email: '1+gitea-admin@noreply.localhost',
154
+ avatar_url: 'http://localhost:3000/avatars/857daf2f94a39d8007c7ac02f123c830',
155
+ html_url: 'http://localhost:3000/gitea-admin',
156
+ language: '',
157
+ is_admin: false,
158
+ last_login: '0001-01-01T00:00:00Z',
159
+ created: '2026-06-21T03:08:17Z',
160
+ restricted: false,
161
+ active: false,
162
+ prohibit_login: false,
163
+ location: '',
164
+ website: '',
165
+ description: '',
166
+ visibility: 'public',
167
+ followers_count: 0,
168
+ following_count: 0,
169
+ starred_repos_count: 0,
170
+ username: 'gitea-admin',
171
+ },
172
+ sender: {
173
+ id: 1,
174
+ login: 'gitea-admin',
175
+ login_name: '',
176
+ source_id: 0,
177
+ full_name: '',
178
+ email: '1+gitea-admin@noreply.localhost',
179
+ avatar_url: 'http://localhost:3000/avatars/857daf2f94a39d8007c7ac02f123c830',
180
+ html_url: 'http://localhost:3000/gitea-admin',
181
+ language: '',
182
+ is_admin: false,
183
+ last_login: '0001-01-01T00:00:00Z',
184
+ created: '2026-06-21T03:08:17Z',
185
+ restricted: false,
186
+ active: false,
187
+ prohibit_login: false,
188
+ location: '',
189
+ website: '',
190
+ description: '',
191
+ visibility: 'public',
192
+ followers_count: 0,
193
+ following_count: 0,
194
+ starred_repos_count: 0,
195
+ username: 'gitea-admin',
196
+ },
197
+ } as const;
198
+
199
+ export interface GiteaPushPayloadOptions {
200
+ owner: string;
201
+ repo: string;
202
+ ref: string;
203
+ defaultBranch: string;
204
+ sha: string;
205
+ }
206
+
207
+ export function giteaPushPayload(options: GiteaPushPayloadOptions) {
208
+ const fullName = `${options.owner}/${options.repo}`;
209
+
210
+ return {
211
+ ...capturedGiteaPushPayload,
212
+ ref: options.ref,
213
+ after: options.sha,
214
+ commits: capturedGiteaPushPayload.commits.map((commit) => ({
215
+ ...commit,
216
+ id: options.sha,
217
+ url: `http://localhost:3000/${fullName}/commit/${options.sha}`,
218
+ })),
219
+ head_commit: {
220
+ ...capturedGiteaPushPayload.head_commit,
221
+ id: options.sha,
222
+ url: `http://localhost:3000/${fullName}/commit/${options.sha}`,
223
+ },
224
+ repository: {
225
+ ...capturedGiteaPushPayload.repository,
226
+ name: options.repo,
227
+ full_name: fullName,
228
+ default_branch: options.defaultBranch,
229
+ owner: {
230
+ ...capturedGiteaPushPayload.repository.owner,
231
+ login: options.owner,
232
+ username: options.owner,
233
+ html_url: `http://localhost:3000/${options.owner}`,
234
+ },
235
+ },
236
+ };
237
+ }
@@ -0,0 +1,14 @@
1
+ import './env.js';
2
+ import {runMigrations} from '@shipfox/node-drizzle';
3
+ import {closePostgresClient, createPostgresClient} from '@shipfox/node-postgres';
4
+ import {closeDb, db} from '#db/db.js';
5
+ import {migrationsPath} from '#db/migrations.js';
6
+
7
+ export async function setup() {
8
+ createPostgresClient();
9
+
10
+ await runMigrations(db(), migrationsPath, '__drizzle_migrations_integrations_gitea');
11
+
12
+ closeDb();
13
+ await closePostgresClient();
14
+ }
package/test/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export {giteaConnectionFactory} from './factories/gitea-connection.js';
2
+ export {
3
+ capturedGiteaPushPayload,
4
+ type GiteaPushPayloadOptions,
5
+ giteaPushPayload,
6
+ } from './fixtures/gitea-webhook.js';
package/test/setup.ts ADDED
@@ -0,0 +1,17 @@
1
+ import './env.js';
2
+ import {closePostgresClient, createPostgresClient} from '@shipfox/node-postgres';
3
+ import {afterAll, afterEach, beforeAll, vi} from '@shipfox/vitest/vi';
4
+ import {closeDb} from '#db/db.js';
5
+
6
+ beforeAll(() => {
7
+ createPostgresClient();
8
+ });
9
+
10
+ afterEach(() => {
11
+ vi.restoreAllMocks();
12
+ });
13
+
14
+ afterAll(async () => {
15
+ closeDb();
16
+ await closePostgresClient();
17
+ });
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "@shipfox/ts-config/node",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist"
6
+ },
7
+ "include": ["src"],
8
+ "exclude": ["**/*.test.tsx", "**/*.test.ts"]
9
+ }