@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
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { safeFetch, maskSecrets, extractErrorMessage } from '@ticatec/omniflow-artifact'
|
|
2
|
+
|
|
3
|
+
export interface GitLabClientConfig {
|
|
4
|
+
baseUrl: string
|
|
5
|
+
token?: string
|
|
6
|
+
username?: string
|
|
7
|
+
password?: string
|
|
8
|
+
timeoutMs?: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ParsedGitLabRepo {
|
|
12
|
+
host: string
|
|
13
|
+
owner: string
|
|
14
|
+
repo: string
|
|
15
|
+
projectPath: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Detect whether a hostname is official GitLab SaaS or a valid self-hosted GitLab instance,
|
|
20
|
+
* strictly rejecting spoofed hostnames (e.g. gitlab.com.evil.net, gitlab.com.cn).
|
|
21
|
+
*/
|
|
22
|
+
export function isGitLabHost(hostname: string): boolean {
|
|
23
|
+
const host = hostname.toLowerCase()
|
|
24
|
+
if (host === 'gitlab.com' || host === 'registry.gitlab.com' || host.endsWith('.gitlab.com')) {
|
|
25
|
+
return true
|
|
26
|
+
}
|
|
27
|
+
const segments = host.split('.')
|
|
28
|
+
// Reject subdomain spoofing where "gitlab.com" appears as subdomains of a foreign host
|
|
29
|
+
const gitlabIdx = segments.indexOf('gitlab')
|
|
30
|
+
if (gitlabIdx !== -1 && segments[gitlabIdx + 1] === 'com') {
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
return segments.includes('gitlab')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class GitLabClient {
|
|
37
|
+
readonly baseUrl: string
|
|
38
|
+
readonly token?: string
|
|
39
|
+
readonly username?: string
|
|
40
|
+
readonly password?: string
|
|
41
|
+
readonly timeoutMs: number
|
|
42
|
+
|
|
43
|
+
constructor(config: GitLabClientConfig) {
|
|
44
|
+
let base = config.baseUrl.trim().replace(/\/+$/, '')
|
|
45
|
+
if (!/^https?:\/\//i.test(base)) {
|
|
46
|
+
base = `https://${base}`
|
|
47
|
+
}
|
|
48
|
+
this.baseUrl = base
|
|
49
|
+
this.token = config.token
|
|
50
|
+
this.username = config.username
|
|
51
|
+
this.password = config.password
|
|
52
|
+
this.timeoutMs = config.timeoutMs ?? 30000
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get host(): string {
|
|
56
|
+
const parsed = new URL(this.baseUrl)
|
|
57
|
+
return parsed.host
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get hostname(): string {
|
|
61
|
+
const parsed = new URL(this.baseUrl)
|
|
62
|
+
return parsed.hostname
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Return sensitive credential strings of length >= 3 for scrubbing.
|
|
67
|
+
* Username is intentionally excluded to preserve debugging visibility.
|
|
68
|
+
*/
|
|
69
|
+
getSecrets(): string[] {
|
|
70
|
+
const secrets = [this.token, this.password].filter(
|
|
71
|
+
(s): s is string => typeof s === 'string' && s.length >= 3
|
|
72
|
+
)
|
|
73
|
+
if (this.username && this.password) {
|
|
74
|
+
const basic = Buffer.from(`${this.username}:${this.password}`).toString('base64')
|
|
75
|
+
if (basic.length >= 3) {
|
|
76
|
+
secrets.push(basic)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return secrets
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Mask secrets in text using shared utility.
|
|
84
|
+
*/
|
|
85
|
+
maskSecrets(text: string): string {
|
|
86
|
+
return maskSecrets(text, this.getSecrets())
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getAuthHeaders(): Record<string, string> {
|
|
90
|
+
const headers: Record<string, string> = {
|
|
91
|
+
Accept: 'application/json'
|
|
92
|
+
}
|
|
93
|
+
if (this.token) {
|
|
94
|
+
headers['PRIVATE-TOKEN'] = this.token
|
|
95
|
+
} else if (this.username && this.password) {
|
|
96
|
+
const basic = Buffer.from(`${this.username}:${this.password}`).toString('base64')
|
|
97
|
+
headers['Authorization'] = `Basic ${basic}`
|
|
98
|
+
} else if (this.password) {
|
|
99
|
+
headers['PRIVATE-TOKEN'] = this.password
|
|
100
|
+
}
|
|
101
|
+
return headers
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Return GitLab API v4 project endpoint URL with URL-encoded project path/ID.
|
|
106
|
+
*/
|
|
107
|
+
getProjectApiUrl(projectPathOrId: string, subpath = ''): string {
|
|
108
|
+
const encoded = encodeURIComponent(projectPathOrId.replace(/^\/+/, '').replace(/\/+$/, ''))
|
|
109
|
+
const cleanSubpath = subpath ? (subpath.startsWith('/') ? subpath : `/${subpath}`) : ''
|
|
110
|
+
return `${this.baseUrl}/api/v4/projects/${encoded}${cleanSubpath}`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Return package registry endpoint for specific package type.
|
|
115
|
+
*/
|
|
116
|
+
getPackageApiUrl(projectPathOrId: string, packageType: 'npm' | 'maven' | 'generic', subpath = ''): string {
|
|
117
|
+
const projectUrl = this.getProjectApiUrl(projectPathOrId)
|
|
118
|
+
const cleanSubpath = subpath ? (subpath.startsWith('/') ? subpath : `/${subpath}`) : ''
|
|
119
|
+
|
|
120
|
+
switch (packageType) {
|
|
121
|
+
case 'npm':
|
|
122
|
+
// NPM registry endpoint must end with trailing slash
|
|
123
|
+
return `${projectUrl}/packages/npm/`
|
|
124
|
+
case 'maven':
|
|
125
|
+
return `${projectUrl}/packages/maven${cleanSubpath}`
|
|
126
|
+
case 'generic':
|
|
127
|
+
return `${projectUrl}/packages/generic${cleanSubpath}`
|
|
128
|
+
default:
|
|
129
|
+
throw new Error(`Unsupported GitLab package type: ${packageType}`)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Standard HTTP request wrapper with timeout and credential masking.
|
|
135
|
+
*/
|
|
136
|
+
async request(endpointOrUrl: string, init: RequestInit = {}): Promise<Response> {
|
|
137
|
+
const url = endpointOrUrl.startsWith('http://') || endpointOrUrl.startsWith('https://')
|
|
138
|
+
? endpointOrUrl
|
|
139
|
+
: `${this.baseUrl}${endpointOrUrl.startsWith('/') ? endpointOrUrl : `/${endpointOrUrl}`}`
|
|
140
|
+
|
|
141
|
+
const headers = {
|
|
142
|
+
...this.getAuthHeaders(),
|
|
143
|
+
...(init.headers as Record<string, string> || {})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return await safeFetch(url, {
|
|
147
|
+
...init,
|
|
148
|
+
headers,
|
|
149
|
+
timeoutMs: this.timeoutMs,
|
|
150
|
+
secrets: this.getSecrets()
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Safely extract, truncate (max 500 chars), and mask error text from a response.
|
|
156
|
+
*/
|
|
157
|
+
async extractErrorMessage(res: Response): Promise<string> {
|
|
158
|
+
return extractErrorMessage(res, this.getSecrets())
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Parse repository URL or slug into host, owner, and repo.
|
|
163
|
+
*/
|
|
164
|
+
static parseRepo(urlOrSlug: string, defaultHost = 'gitlab.com'): ParsedGitLabRepo {
|
|
165
|
+
let clean = urlOrSlug.trim()
|
|
166
|
+
let host = defaultHost
|
|
167
|
+
|
|
168
|
+
if (clean.includes('://')) {
|
|
169
|
+
const parsed = new URL(clean)
|
|
170
|
+
host = parsed.host
|
|
171
|
+
clean = decodeURIComponent(parsed.pathname)
|
|
172
|
+
} else if (clean.includes('@') && clean.includes(':')) {
|
|
173
|
+
const colonIdx = clean.indexOf(':')
|
|
174
|
+
const hostPart = clean.slice(0, colonIdx).replace(/.*@/, '')
|
|
175
|
+
let pathPart = clean.slice(colonIdx + 1)
|
|
176
|
+
const portMatch = pathPart.match(/^(\d+)\/(.*)/)
|
|
177
|
+
if (portMatch) {
|
|
178
|
+
host = `${hostPart}:${portMatch[1]}`
|
|
179
|
+
pathPart = portMatch[2]
|
|
180
|
+
} else {
|
|
181
|
+
host = hostPart
|
|
182
|
+
}
|
|
183
|
+
clean = decodeURIComponent(pathPart)
|
|
184
|
+
} else {
|
|
185
|
+
clean = decodeURIComponent(clean)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
clean = clean.replace(/^\/+/, '').replace(/\.git$/, '')
|
|
189
|
+
const segments = clean.split('/').filter(Boolean)
|
|
190
|
+
if (segments.length < 2) {
|
|
191
|
+
throw new Error(`Invalid repository path: "${urlOrSlug}". Expected owner/repo format.`)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const owner = segments.slice(0, -1).join('/')
|
|
195
|
+
const repo = segments[segments.length - 1]
|
|
196
|
+
const projectPath = segments.join('/')
|
|
197
|
+
|
|
198
|
+
return { host, owner, repo, projectPath }
|
|
199
|
+
}
|
|
200
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { PluginContext } from '@ticatec/omniflow-core'
|
|
2
|
+
import type { ArtifactPublisherRegistry } from '@ticatec/omniflow-artifact'
|
|
3
|
+
import { GitLabContainerPublisher } from './artifacts/GitLabContainerPublisher.js'
|
|
4
|
+
import { GitLabNpmPublisher } from './artifacts/GitLabNpmPublisher.js'
|
|
5
|
+
import { GitLabMavenPublisher } from './artifacts/GitLabMavenPublisher.js'
|
|
6
|
+
import { GitLabGenericPublisher } from './artifacts/GitLabGenericPublisher.js'
|
|
7
|
+
|
|
8
|
+
export * from './client/GitLabClient.js'
|
|
9
|
+
export * from './artifacts/GitLabContainerPublisher.js'
|
|
10
|
+
export * from './artifacts/GitLabNpmPublisher.js'
|
|
11
|
+
export * from './artifacts/GitLabMavenPublisher.js'
|
|
12
|
+
export * from './artifacts/GitLabGenericPublisher.js'
|
|
13
|
+
|
|
14
|
+
export interface RegisterGitLabOptions {
|
|
15
|
+
artifactRegistry?: ArtifactPublisherRegistry
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Standard OmniFlow Plugin SPI entry point.
|
|
20
|
+
*/
|
|
21
|
+
export function register(ctx: PluginContext, options: Record<string, unknown> = {}): void {
|
|
22
|
+
const artifactsEnabled = options.artifacts !== false
|
|
23
|
+
|
|
24
|
+
registerGitLab({
|
|
25
|
+
artifactRegistry: artifactsEnabled ? ctx.artifactRegistry : undefined
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default register
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Register all GitLab artifact publishers into the artifact registry.
|
|
33
|
+
*/
|
|
34
|
+
export function registerGitLab(options: RegisterGitLabOptions = {}): {
|
|
35
|
+
containerPublisher: GitLabContainerPublisher
|
|
36
|
+
npmPublisher: GitLabNpmPublisher
|
|
37
|
+
mavenPublisher: GitLabMavenPublisher
|
|
38
|
+
genericPublisher: GitLabGenericPublisher
|
|
39
|
+
} {
|
|
40
|
+
const containerPublisher = new GitLabContainerPublisher()
|
|
41
|
+
const npmPublisher = new GitLabNpmPublisher()
|
|
42
|
+
const mavenPublisher = new GitLabMavenPublisher()
|
|
43
|
+
const genericPublisher = new GitLabGenericPublisher()
|
|
44
|
+
|
|
45
|
+
if (options.artifactRegistry) {
|
|
46
|
+
options.artifactRegistry.register(containerPublisher)
|
|
47
|
+
options.artifactRegistry.register(npmPublisher)
|
|
48
|
+
options.artifactRegistry.register(mavenPublisher)
|
|
49
|
+
options.artifactRegistry.register(genericPublisher)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
containerPublisher,
|
|
54
|
+
npmPublisher,
|
|
55
|
+
mavenPublisher,
|
|
56
|
+
genericPublisher
|
|
57
|
+
}
|
|
58
|
+
}
|