@ticatec/omniflow-gitlab 1.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 (34) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +94 -0
  3. package/README_CN.md +95 -0
  4. package/dist/artifacts/GitLabContainerPublisher.d.ts +17 -0
  5. package/dist/artifacts/GitLabContainerPublisher.d.ts.map +1 -0
  6. package/dist/artifacts/GitLabContainerPublisher.js +44 -0
  7. package/dist/artifacts/GitLabContainerPublisher.js.map +1 -0
  8. package/dist/artifacts/GitLabGenericPublisher.d.ts +27 -0
  9. package/dist/artifacts/GitLabGenericPublisher.d.ts.map +1 -0
  10. package/dist/artifacts/GitLabGenericPublisher.js +176 -0
  11. package/dist/artifacts/GitLabGenericPublisher.js.map +1 -0
  12. package/dist/artifacts/GitLabMavenPublisher.d.ts +24 -0
  13. package/dist/artifacts/GitLabMavenPublisher.d.ts.map +1 -0
  14. package/dist/artifacts/GitLabMavenPublisher.js +185 -0
  15. package/dist/artifacts/GitLabMavenPublisher.js.map +1 -0
  16. package/dist/artifacts/GitLabNpmPublisher.d.ts +20 -0
  17. package/dist/artifacts/GitLabNpmPublisher.d.ts.map +1 -0
  18. package/dist/artifacts/GitLabNpmPublisher.js +68 -0
  19. package/dist/artifacts/GitLabNpmPublisher.js.map +1 -0
  20. package/dist/client/GitLabClient.d.ts +59 -0
  21. package/dist/client/GitLabClient.d.ts.map +1 -0
  22. package/dist/client/GitLabClient.js +169 -0
  23. package/dist/client/GitLabClient.js.map +1 -0
  24. package/dist/index.d.ts +29 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +41 -0
  27. package/dist/index.js.map +1 -0
  28. package/package.json +58 -0
  29. package/src/artifacts/GitLabContainerPublisher.ts +50 -0
  30. package/src/artifacts/GitLabGenericPublisher.ts +221 -0
  31. package/src/artifacts/GitLabMavenPublisher.ts +224 -0
  32. package/src/artifacts/GitLabNpmPublisher.ts +90 -0
  33. package/src/client/GitLabClient.ts +200 -0
  34. package/src/index.ts +58 -0
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@ticatec/omniflow-gitlab",
3
+ "version": "1.0.0",
4
+ "description": "All-in-one GitLab artifact provider for OmniFlow: Container Registry, NPM, Maven, and Generic package registries",
5
+ "homepage": "https://github.com/ticatec/omniflow#readme",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/ticatec/omniflow.git",
9
+ "directory": "omniflow-gitlab"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/ticatec/omniflow/issues"
13
+ },
14
+ "author": "Henry Feng <huili.f@gmail.com>",
15
+ "type": "module",
16
+ "main": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ }
23
+ },
24
+ "license": "MIT",
25
+ "dependencies": {
26
+ "@ticatec/omniflow-artifact": "^1.0.0",
27
+ "@ticatec/omniflow-core": "^1.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "@jest/globals": "^30.4.1",
31
+ "@types/jest": "^30.0.0",
32
+ "@types/node": "^22.10.2",
33
+ "jest": "^30.4.2",
34
+ "ts-jest": "^29.4.9",
35
+ "typescript": "^5.7.2"
36
+ },
37
+ "engines": {
38
+ "node": ">=20.0.0"
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "src",
43
+ "README.md",
44
+ "README_CN.md",
45
+ "LICENSE"
46
+ ],
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "scripts": {
51
+ "build": "tsc",
52
+ "clean": "rm -rf dist",
53
+ "prebuild": "npm run clean",
54
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest",
55
+ "typecheck": "tsc --noEmit",
56
+ "publish:npm": "pnpm publish --access public --no-git-checks"
57
+ }
58
+ }
@@ -0,0 +1,50 @@
1
+ import { BaseDockerPublisher } from '@ticatec/omniflow-artifact'
2
+ import type { RegistryConfig } from '@ticatec/omniflow-artifact'
3
+ import { isGitLabHost } from '../client/GitLabClient.js'
4
+
5
+ /**
6
+ * GitLab Container Registry Publisher.
7
+ *
8
+ * Supports both gitlab.com Container Registry (registry.gitlab.com)
9
+ * and self-hosted GitLab container registries.
10
+ */
11
+ export class GitLabContainerPublisher extends BaseDockerPublisher {
12
+ readonly id = 'gitlab-container'
13
+ readonly name = 'GitLab Container Registry'
14
+ readonly platform = 'gitlab'
15
+ override readonly priority = 20
16
+
17
+ override detect(registry: RegistryConfig): boolean {
18
+ if (registry.type !== 'docker' && registry.type !== 'container') {
19
+ return false
20
+ }
21
+
22
+ const regName = (registry.name || '').toLowerCase()
23
+ if (regName === 'gitlab' || regName.startsWith('gitlab-')) {
24
+ return true
25
+ }
26
+
27
+ const server = (registry.server || '').toLowerCase()
28
+ try {
29
+ const parsed = new URL(server.includes('://') ? server : `https://${server}`)
30
+ return isGitLabHost(parsed.hostname)
31
+ } catch {
32
+ return false
33
+ }
34
+ }
35
+
36
+ override buildRepositoryUrl(registry: RegistryConfig, imageName: string): string {
37
+ const host = this.normalizeServerHost(registry.server)
38
+ const cleanImage = imageName.replace(/^\/+/, '')
39
+
40
+ const ns = (registry.namespace || '').replace(/\/+$/, '')
41
+ if (ns) {
42
+ if (cleanImage.startsWith(`${ns}/`)) {
43
+ return `${host}/${cleanImage}`
44
+ }
45
+ return `${host}/${ns}/${cleanImage}`
46
+ }
47
+
48
+ return `${host}/${cleanImage}`
49
+ }
50
+ }
@@ -0,0 +1,221 @@
1
+ import { promises as fs } from 'fs'
2
+ import path from 'path'
3
+ import crypto from 'crypto'
4
+ import { BasePublisher } from '@ticatec/omniflow-artifact'
5
+ import type { ArtifactPayload, DownloadOptions, DownloadResult, PublishOptions, RegistryConfig } from '@ticatec/omniflow-artifact'
6
+ import { GitLabClient, isGitLabHost } from '../client/GitLabClient.js'
7
+
8
+ /**
9
+ * GitLab Generic Package Registry Publisher.
10
+ *
11
+ * Publishes arbitrary binary artifacts, tarballs (tar.gz), or zip files
12
+ * to GitLab's built-in Generic package registry.
13
+ * Endpoint format: PUT https://<gitlab_host>/api/v4/projects/<projectId>/packages/generic/<packageName>/<version>/<filename>
14
+ */
15
+ export class GitLabGenericPublisher extends BasePublisher {
16
+ readonly id = 'gitlab-generic'
17
+ readonly name = 'GitLab Generic Package Registry'
18
+ readonly platform = 'gitlab'
19
+ readonly artifactType = 'generic'
20
+ override readonly priority = 20
21
+
22
+ override detect(registry: RegistryConfig): boolean {
23
+ if (registry.type !== 'generic' && registry.type !== 'archive') {
24
+ return false
25
+ }
26
+
27
+ const regName = (registry.name || '').toLowerCase()
28
+ if (regName === 'gitlab' || regName.startsWith('gitlab-')) {
29
+ return true
30
+ }
31
+
32
+ const server = (registry.server || '').toLowerCase()
33
+ try {
34
+ const parsed = new URL(server.includes('://') ? server : `https://${server}`)
35
+ return isGitLabHost(parsed.hostname)
36
+ } catch {
37
+ return false
38
+ }
39
+ }
40
+
41
+ override buildTargetRef(artifact: ArtifactPayload, _registry: RegistryConfig): string {
42
+ const version = artifact.tags && artifact.tags.length > 0 ? artifact.tags[0] : 'latest'
43
+ const filename = path.basename(artifact.source)
44
+ return `${artifact.name}/${version}/${filename}`
45
+ }
46
+
47
+ override async exists(artifact: ArtifactPayload, registry: RegistryConfig): Promise<boolean> {
48
+ const creds = this.resolveCredentials(registry)
49
+ const token = creds.token || creds.password
50
+ const reg = registry as RegistryConfig & { metadata?: Record<string, unknown> }
51
+ const projectId =
52
+ reg.namespace?.trim() ||
53
+ (reg.options?.projectId as string | undefined)?.trim() ||
54
+ (reg.metadata?.projectId as string | undefined)?.trim()
55
+
56
+ if (!projectId) return false
57
+
58
+ const client = new GitLabClient({
59
+ baseUrl: registry.server,
60
+ token,
61
+ username: creds.username,
62
+ password: creds.password
63
+ })
64
+
65
+ const version = artifact.tags && artifact.tags.length > 0 ? artifact.tags[0] : 'latest'
66
+ const packageName = artifact.name
67
+ const filename = path.basename(artifact.source)
68
+
69
+ const baseUrl = client.getPackageApiUrl(projectId, 'generic')
70
+ const cleanBaseUrl = baseUrl.replace(/\/+$/, '')
71
+ const targetUrl = `${cleanBaseUrl}/${encodeURIComponent(packageName)}/${encodeURIComponent(version)}/${encodeURIComponent(filename)}`
72
+
73
+ try {
74
+ const res = await client.request(targetUrl, { method: 'HEAD' })
75
+ if (res.ok || res.status === 200 || res.status === 204) {
76
+ return true
77
+ }
78
+ if (res.status === 404) {
79
+ return false
80
+ }
81
+ const errText = await client.extractErrorMessage(res)
82
+ throw new Error(`Failed to check artifact existence in GitLab Generic registry (${res.status}): ${errText}`)
83
+ } catch (err: any) {
84
+ if (err?.message?.includes('Failed to check artifact existence in GitLab')) {
85
+ throw err
86
+ }
87
+ throw new Error(
88
+ `Failed to check artifact existence in GitLab Generic registry: ${client.maskSecrets(err?.message || String(err))}`
89
+ )
90
+ }
91
+ }
92
+
93
+ override async download(
94
+ artifact: ArtifactPayload,
95
+ registry: RegistryConfig,
96
+ dest: string,
97
+ options?: DownloadOptions
98
+ ): Promise<DownloadResult> {
99
+ const creds = this.resolveCredentials(registry)
100
+ const reg = registry as RegistryConfig & { metadata?: Record<string, unknown> }
101
+ const projectId =
102
+ reg.namespace?.trim() ||
103
+ (reg.options?.projectId as string | undefined)?.trim() ||
104
+ (reg.metadata?.projectId as string | undefined)?.trim()
105
+ if (!projectId) {
106
+ throw new Error(`GitLab Generic registry requires a valid project ID (specify via registry.namespace)`)
107
+ }
108
+
109
+ const client = new GitLabClient({
110
+ baseUrl: registry.server,
111
+ token: creds.token || creds.password,
112
+ username: creds.username,
113
+ password: creds.password,
114
+ timeoutMs: options?.timeoutMs
115
+ })
116
+
117
+ const version = artifact.tags?.[0] || 'latest'
118
+ const filename = path.basename(artifact.source)
119
+ const baseUrl = client.getPackageApiUrl(projectId, 'generic').replace(/\/+$/, '')
120
+ const targetUrl = `${baseUrl}/${encodeURIComponent(artifact.name)}/${encodeURIComponent(version)}/${encodeURIComponent(filename)}`
121
+ const masked = client.maskSecrets(targetUrl)
122
+
123
+ this.log(` 📥 Downloading ${artifact.name} ${version} from GitLab: ${masked}`)
124
+ const res = await client.request(targetUrl, { method: 'GET' })
125
+ if (!res.ok) {
126
+ throw await this.downloadError(res, `${artifact.name} ${version} (${filename})`, masked)
127
+ }
128
+ return this.saveResponse(res, dest, masked)
129
+ }
130
+
131
+ async login(registry: RegistryConfig, _options?: PublishOptions): Promise<void> {
132
+ const creds = this.resolveCredentials(registry)
133
+ if (!creds.token && !creds.password) {
134
+ this.log(` ℹ No auth token configured for GitLab Generic registry ${registry.server}`)
135
+ }
136
+ }
137
+
138
+ protected async doPublish(
139
+ artifact: ArtifactPayload,
140
+ registry: RegistryConfig,
141
+ options?: PublishOptions
142
+ ): Promise<{
143
+ ref: string
144
+ digest?: string
145
+ metadata?: Record<string, unknown>
146
+ }> {
147
+ const creds = this.resolveCredentials(registry)
148
+ const token = creds.token || creds.password
149
+ const reg = registry as RegistryConfig & { metadata?: Record<string, unknown> }
150
+ const projectId =
151
+ reg.namespace?.trim() ||
152
+ (reg.options?.projectId as string | undefined)?.trim() ||
153
+ (reg.metadata?.projectId as string | undefined)?.trim()
154
+
155
+ if (!projectId) {
156
+ throw new Error(
157
+ `GitLab Generic registry requires a valid project ID (specify via registry.namespace)`
158
+ )
159
+ }
160
+
161
+ const client = new GitLabClient({
162
+ baseUrl: registry.server,
163
+ token,
164
+ username: creds.username,
165
+ password: creds.password
166
+ })
167
+
168
+ const version = artifact.tags && artifact.tags.length > 0 ? artifact.tags[0] : 'latest'
169
+ const packageName = artifact.name
170
+
171
+ let filePath = artifact.source
172
+ const stat = await fs.stat(filePath).catch(() => null)
173
+ if (!stat || !stat.isFile()) {
174
+ throw new Error(`Generic artifact source must be an existing file: ${filePath}`)
175
+ }
176
+
177
+ const filename = path.basename(filePath)
178
+ const fileBuffer = await fs.readFile(filePath)
179
+ const digest = `sha256:${crypto.createHash('sha256').update(fileBuffer).digest('hex')}`
180
+
181
+ const baseUrl = client.getPackageApiUrl(projectId, 'generic')
182
+ const cleanBaseUrl = baseUrl.replace(/\/+$/, '')
183
+ const targetUrl = `${cleanBaseUrl}/${encodeURIComponent(packageName)}/${encodeURIComponent(version)}/${encodeURIComponent(filename)}`
184
+
185
+ this.log(` 📤 Uploading Generic package file to GitLab: ${targetUrl}`)
186
+
187
+ if (options?.dryRun) {
188
+ this.log(` [dryRun] Would PUT ${filename} (${fileBuffer.length} bytes) to ${targetUrl}`)
189
+ return {
190
+ ref: `${packageName}/${version}/${filename}`,
191
+ digest,
192
+ metadata: { targetUrl, size: fileBuffer.length }
193
+ }
194
+ }
195
+
196
+ const res = await client.request(targetUrl, {
197
+ method: 'PUT',
198
+ headers: {
199
+ 'Content-Type': 'application/octet-stream'
200
+ },
201
+ body: fileBuffer
202
+ })
203
+
204
+ if (!res.ok && res.status !== 201 && res.status !== 200) {
205
+ const errText = await client.extractErrorMessage(res)
206
+ throw new Error(`Failed to upload generic package to GitLab (${res.status}): ${errText}`)
207
+ }
208
+
209
+ return {
210
+ ref: `${packageName}/${version}/${filename}`,
211
+ digest,
212
+ metadata: {
213
+ packageName,
214
+ version,
215
+ filename,
216
+ targetUrl,
217
+ size: fileBuffer.length
218
+ }
219
+ }
220
+ }
221
+ }
@@ -0,0 +1,224 @@
1
+ import { promises as fs } from 'fs'
2
+ import path from 'path'
3
+ import crypto from 'crypto'
4
+ import { BasePublisher, resolveMavenArtifactFilenames, mavenPomPathFor, isMavenArtifactFile } from '@ticatec/omniflow-artifact'
5
+ import type { ArtifactPayload, PublishOptions, RegistryConfig } from '@ticatec/omniflow-artifact'
6
+ import { GitLabClient, isGitLabHost } from '../client/GitLabClient.js'
7
+
8
+ /**
9
+ * GitLab Maven Package Registry Publisher.
10
+ *
11
+ * Endpoint format: PUT https://<gitlab_host>/api/v4/projects/<projectId>/packages/maven/{groupId}/{artifactId}/{version}/{filename}
12
+ */
13
+ export class GitLabMavenPublisher extends BasePublisher {
14
+ readonly id = 'gitlab-maven'
15
+ readonly name = 'GitLab Maven Repository'
16
+ readonly platform = 'gitlab'
17
+ readonly artifactType = 'maven'
18
+ override readonly priority = 20
19
+
20
+ override detect(registry: RegistryConfig): boolean {
21
+ if (registry.type !== 'maven' && registry.type !== 'jar') {
22
+ return false
23
+ }
24
+
25
+ const regName = (registry.name || '').toLowerCase()
26
+ if (regName === 'gitlab' || regName.startsWith('gitlab-')) {
27
+ return true
28
+ }
29
+
30
+ const server = (registry.server || '').toLowerCase()
31
+ try {
32
+ const parsed = new URL(server.includes('://') ? server : `https://${server}`)
33
+ return isGitLabHost(parsed.hostname)
34
+ } catch {
35
+ return false
36
+ }
37
+ }
38
+
39
+ override buildTargetRef(artifact: ArtifactPayload, _registry: RegistryConfig): string {
40
+ const version = artifact.tags && artifact.tags.length > 0 ? artifact.tags[0] : '1.0.0'
41
+ const artifactId = (artifact.metadata?.artifactId as string) || artifact.name
42
+ const groupId = (artifact.metadata?.groupId as string)?.trim()
43
+ return groupId ? `${groupId}:${artifactId}:${version}` : `${artifactId}:${version}`
44
+ }
45
+
46
+ override async exists(artifact: ArtifactPayload, registry: RegistryConfig): Promise<boolean> {
47
+ const creds = this.resolveCredentials(registry)
48
+ const token = creds.token || creds.password
49
+ const reg = registry as RegistryConfig & { metadata?: Record<string, unknown> }
50
+ const projectId =
51
+ reg.namespace?.trim() ||
52
+ (reg.options?.projectId as string | undefined)?.trim() ||
53
+ (reg.metadata?.projectId as string | undefined)?.trim()
54
+
55
+ if (!projectId) return false
56
+
57
+ const client = new GitLabClient({
58
+ baseUrl: registry.server,
59
+ token,
60
+ username: creds.username,
61
+ password: creds.password
62
+ })
63
+
64
+ let jarPath = artifact.source
65
+ if (jarPath) {
66
+ const stat = await fs.stat(jarPath).catch(() => null)
67
+ if (stat?.isDirectory()) {
68
+ const files = await fs.readdir(jarPath).catch(() => [] as string[])
69
+ const jarFile = files.find(f => isMavenArtifactFile(f))
70
+ if (jarFile) {
71
+ jarPath = path.join(jarPath, jarFile)
72
+ }
73
+ }
74
+ }
75
+
76
+ const { groupId, artifactId, version, groupPath, jarFilename } = resolveMavenArtifactFilenames(artifact, jarPath, undefined, 'GitLab')
77
+
78
+ const baseUrl = client.getPackageApiUrl(projectId, 'maven')
79
+ const cleanBaseUrl = baseUrl.replace(/\/+$/, '')
80
+ const targetUrl = `${cleanBaseUrl}/${groupPath}/${artifactId}/${version}/${jarFilename}`
81
+
82
+ try {
83
+ const res = await client.request(targetUrl, { method: 'HEAD' })
84
+ if (res.ok || res.status === 200 || res.status === 204) {
85
+ return true
86
+ }
87
+ if (res.status === 404) {
88
+ return false
89
+ }
90
+ const errText = await client.extractErrorMessage(res)
91
+ throw new Error(`Failed to check artifact existence in GitLab Maven registry (${res.status}): ${errText}`)
92
+ } catch (err: any) {
93
+ if (err?.message?.includes('Failed to check artifact existence in GitLab')) {
94
+ throw err
95
+ }
96
+ throw new Error(
97
+ `Failed to check artifact existence in GitLab Maven registry: ${client.maskSecrets(err?.message || String(err))}`
98
+ )
99
+ }
100
+ }
101
+
102
+ async login(registry: RegistryConfig, _options?: PublishOptions): Promise<void> {
103
+ const creds = this.resolveCredentials(registry)
104
+ if (!creds.token && !creds.password) {
105
+ this.log(` ℹ No auth token configured for GitLab Maven repository ${registry.server}`)
106
+ }
107
+ }
108
+
109
+ protected async doPublish(
110
+ artifact: ArtifactPayload,
111
+ registry: RegistryConfig,
112
+ options?: PublishOptions
113
+ ): Promise<{
114
+ ref: string
115
+ digest?: string
116
+ metadata?: Record<string, unknown>
117
+ }> {
118
+ const creds = this.resolveCredentials(registry)
119
+ const token = creds.token || creds.password
120
+ const reg = registry as RegistryConfig & { metadata?: Record<string, unknown> }
121
+ const projectId =
122
+ reg.namespace?.trim() ||
123
+ (reg.options?.projectId as string | undefined)?.trim() ||
124
+ (reg.metadata?.projectId as string | undefined)?.trim()
125
+
126
+ if (!projectId) {
127
+ throw new Error(
128
+ `GitLab Maven registry requires a valid project ID (specify via registry.namespace)`
129
+ )
130
+ }
131
+
132
+ const client = new GitLabClient({
133
+ baseUrl: registry.server,
134
+ token,
135
+ username: creds.username,
136
+ password: creds.password
137
+ })
138
+
139
+ // Locate jar file
140
+ let jarPath = artifact.source
141
+ const stat = await fs.stat(jarPath).catch(() => null)
142
+ if (stat?.isDirectory()) {
143
+ const files = await fs.readdir(jarPath)
144
+ const jarFile = files.find(f => isMavenArtifactFile(f))
145
+ if (!jarFile) {
146
+ throw new Error(`No jar/war artifact found in directory: ${jarPath}`)
147
+ }
148
+ jarPath = path.join(jarPath, jarFile)
149
+ }
150
+
151
+ const { groupId, artifactId, version, groupPath, jarFilename, pomFilename } = resolveMavenArtifactFilenames(artifact, jarPath, undefined, 'GitLab')
152
+
153
+ const jarBuffer = await fs.readFile(jarPath)
154
+ const digest = `sha256:${crypto.createHash('sha256').update(jarBuffer).digest('hex')}`
155
+
156
+ const baseUrl = client.getPackageApiUrl(projectId, 'maven')
157
+ const cleanBaseUrl = baseUrl.replace(/\/+$/, '')
158
+ const targetUrl = `${cleanBaseUrl}/${groupPath}/${artifactId}/${version}/${jarFilename}`
159
+
160
+ this.log(` 📤 Uploading Maven JAR to GitLab: ${targetUrl}`)
161
+
162
+ if (options?.dryRun) {
163
+ this.log(` [dryRun] Would PUT ${jarFilename} (${jarBuffer.length} bytes) to ${targetUrl}`)
164
+ return {
165
+ ref: `${groupId}:${artifactId}:${version}`,
166
+ digest,
167
+ metadata: { targetUrl, size: jarBuffer.length }
168
+ }
169
+ }
170
+
171
+ // The POM goes up first, deliberately.
172
+ //
173
+ // The existence probe checks for the jar/war, so uploading that last is what
174
+ // makes the probe mean "this artifact is complete". With the artifact first, a
175
+ // run that uploaded it and then failed on the POM would leave a coordinate that
176
+ // probes as present: the retry skips it as already published, and the POM never
177
+ // arrives — a jar in the repository forever, with no dependency information.
178
+ // Upload pom if exists
179
+ const pomPath = mavenPomPathFor(jarPath)
180
+ const pomStat = await fs.stat(pomPath).catch(() => null)
181
+ if (pomStat?.isFile()) {
182
+ const pomBuffer = await fs.readFile(pomPath)
183
+ const pomFilename = `${artifactId}-${version}.pom`
184
+ const pomTargetUrl = `${cleanBaseUrl}/${groupPath}/${artifactId}/${version}/${pomFilename}`
185
+ this.log(` 📤 Uploading Maven POM to GitLab: ${pomTargetUrl}`)
186
+
187
+ const pomRes = await client.request(pomTargetUrl, {
188
+ method: 'PUT',
189
+ headers: { 'Content-Type': 'application/xml' },
190
+ body: pomBuffer
191
+ })
192
+
193
+ if (!pomRes.ok && pomRes.status !== 201 && pomRes.status !== 200) {
194
+ const pomErr = await client.extractErrorMessage(pomRes)
195
+ throw new Error(`Failed to upload Maven POM to GitLab (${pomRes.status}): ${pomErr}`)
196
+ }
197
+ }
198
+
199
+ const res = await client.request(targetUrl, {
200
+ method: 'PUT',
201
+ headers: {
202
+ 'Content-Type': 'application/java-archive'
203
+ },
204
+ body: jarBuffer
205
+ })
206
+
207
+ if (!res.ok && res.status !== 201 && res.status !== 200) {
208
+ const errText = await client.extractErrorMessage(res)
209
+ throw new Error(`Failed to upload Maven JAR to GitLab (${res.status}): ${errText}`)
210
+ }
211
+
212
+ return {
213
+ ref: `${groupId}:${artifactId}:${version}`,
214
+ digest,
215
+ metadata: {
216
+ groupId,
217
+ artifactId,
218
+ version,
219
+ targetUrl,
220
+ size: jarBuffer.length
221
+ }
222
+ }
223
+ }
224
+ }
@@ -0,0 +1,90 @@
1
+ import { BaseNpmPublisher } from '@ticatec/omniflow-artifact'
2
+ import type { ArtifactPayload, PublishOptions, RegistryConfig } from '@ticatec/omniflow-artifact'
3
+ import { isGitLabHost } from '../client/GitLabClient.js'
4
+
5
+ /**
6
+ * GitLab NPM Package Registry Publisher.
7
+ *
8
+ * Endpoint format: https://<gitlab_host>/api/v4/projects/<projectId>/packages/npm/
9
+ */
10
+ export class GitLabNpmPublisher extends BaseNpmPublisher {
11
+ readonly id = 'gitlab-npm'
12
+ readonly name = 'GitLab NPM Registry'
13
+ readonly platform = 'gitlab'
14
+ override readonly priority = 20
15
+
16
+ override detect(registry: RegistryConfig): boolean {
17
+ if (registry.type !== 'npm') {
18
+ return false
19
+ }
20
+
21
+ const regName = (registry.name || '').toLowerCase()
22
+ if (regName === 'gitlab' || regName.startsWith('gitlab-')) {
23
+ return true
24
+ }
25
+
26
+ const server = (registry.server || '').toLowerCase()
27
+ try {
28
+ const parsed = new URL(server.includes('://') ? server : `https://${server}`)
29
+ return isGitLabHost(parsed.hostname)
30
+ } catch {
31
+ return false
32
+ }
33
+ }
34
+
35
+ override normalizeRegistryUrl(server: string, _registryOrNamespace?: RegistryConfig | string): string {
36
+ let url = server.trim()
37
+ if (!/^https?:\/\//i.test(url)) {
38
+ url = `https://${url}`
39
+ }
40
+
41
+ const parsed = new URL(url)
42
+ const cleanBase = url.replace(/\/+$/, '')
43
+
44
+ // M6 Escape hatch: if server URL already has path segments containing /packages/npm, use it as-is
45
+ if (parsed.pathname.includes('/packages/npm')) {
46
+ return cleanBase.endsWith('/') ? cleanBase : `${cleanBase}/`
47
+ }
48
+
49
+ // Extract projectId strictly from namespace or options/metadata
50
+ let projectId: string | undefined
51
+ if (typeof _registryOrNamespace === 'string') {
52
+ projectId = _registryOrNamespace.trim()
53
+ } else if (_registryOrNamespace && typeof _registryOrNamespace === 'object') {
54
+ const reg = _registryOrNamespace as RegistryConfig & { metadata?: Record<string, unknown> }
55
+ projectId =
56
+ reg.namespace?.trim() ||
57
+ (reg.options?.projectId as string | undefined)?.trim() ||
58
+ (reg.metadata?.projectId as string | undefined)?.trim()
59
+ }
60
+
61
+ if (!projectId) {
62
+ throw new Error(
63
+ `GitLab NPM registry requires a valid project ID (specify via registry.namespace or full server URL)`
64
+ )
65
+ }
66
+
67
+ return `${cleanBase}/api/v4/projects/${encodeURIComponent(projectId)}/packages/npm/`
68
+ }
69
+
70
+ // NOTE: login() is intentionally NOT overridden here. BaseNpmPublisher handles .npmrc backup,
71
+ // auth token injection, and cleanup(session) in finally to ensure credential cleanup (S1 fix).
72
+
73
+ protected override async doPublish(
74
+ artifact: ArtifactPayload,
75
+ registry: RegistryConfig,
76
+ options?: PublishOptions
77
+ ): Promise<{
78
+ ref: string
79
+ metadata?: Record<string, unknown>
80
+ }> {
81
+ const owner = registry.namespace
82
+ if (owner && !artifact.name.startsWith(`@`)) {
83
+ this.log(
84
+ ` ⚠ Notice: Package "${artifact.name}" is not scoped. GitLab typically recommends package names to be scoped (e.g. @<group>/<package>).`
85
+ )
86
+ }
87
+
88
+ return super.doPublish(artifact, registry, options)
89
+ }
90
+ }