@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.
- package/LICENSE +21 -0
- package/README.md +94 -0
- package/README_CN.md +95 -0
- package/dist/artifacts/GitLabContainerPublisher.d.ts +17 -0
- package/dist/artifacts/GitLabContainerPublisher.d.ts.map +1 -0
- package/dist/artifacts/GitLabContainerPublisher.js +44 -0
- package/dist/artifacts/GitLabContainerPublisher.js.map +1 -0
- package/dist/artifacts/GitLabGenericPublisher.d.ts +27 -0
- package/dist/artifacts/GitLabGenericPublisher.d.ts.map +1 -0
- package/dist/artifacts/GitLabGenericPublisher.js +176 -0
- package/dist/artifacts/GitLabGenericPublisher.js.map +1 -0
- package/dist/artifacts/GitLabMavenPublisher.d.ts +24 -0
- package/dist/artifacts/GitLabMavenPublisher.d.ts.map +1 -0
- package/dist/artifacts/GitLabMavenPublisher.js +185 -0
- package/dist/artifacts/GitLabMavenPublisher.js.map +1 -0
- package/dist/artifacts/GitLabNpmPublisher.d.ts +20 -0
- package/dist/artifacts/GitLabNpmPublisher.d.ts.map +1 -0
- package/dist/artifacts/GitLabNpmPublisher.js +68 -0
- package/dist/artifacts/GitLabNpmPublisher.js.map +1 -0
- package/dist/client/GitLabClient.d.ts +59 -0
- package/dist/client/GitLabClient.d.ts.map +1 -0
- package/dist/client/GitLabClient.js +169 -0
- package/dist/client/GitLabClient.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
- package/src/artifacts/GitLabContainerPublisher.ts +50 -0
- package/src/artifacts/GitLabGenericPublisher.ts +221 -0
- package/src/artifacts/GitLabMavenPublisher.ts +224 -0
- package/src/artifacts/GitLabNpmPublisher.ts +90 -0
- package/src/client/GitLabClient.ts +200 -0
- 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
|
+
}
|