@ticatec/omniflow-aliyun 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 +100 -0
- package/README_CN.md +100 -0
- package/dist/client/AliyunClient.d.ts +7 -0
- package/dist/client/AliyunClient.d.ts.map +1 -0
- package/dist/client/AliyunClient.js +6 -0
- package/dist/client/AliyunClient.js.map +1 -0
- package/dist/docker/AliyunAcrPublisher.d.ts +22 -0
- package/dist/docker/AliyunAcrPublisher.d.ts.map +1 -0
- package/dist/docker/AliyunAcrPublisher.js +54 -0
- package/dist/docker/AliyunAcrPublisher.js.map +1 -0
- package/dist/generic/AliyunGenericPublisher.d.ts +35 -0
- package/dist/generic/AliyunGenericPublisher.d.ts.map +1 -0
- package/dist/generic/AliyunGenericPublisher.js +212 -0
- package/dist/generic/AliyunGenericPublisher.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/maven/AliyunMavenPublisher.d.ts +41 -0
- package/dist/maven/AliyunMavenPublisher.d.ts.map +1 -0
- package/dist/maven/AliyunMavenPublisher.js +261 -0
- package/dist/maven/AliyunMavenPublisher.js.map +1 -0
- package/dist/npm/AliyunNpmPublisher.d.ts +17 -0
- package/dist/npm/AliyunNpmPublisher.d.ts.map +1 -0
- package/dist/npm/AliyunNpmPublisher.js +63 -0
- package/dist/npm/AliyunNpmPublisher.js.map +1 -0
- package/package.json +58 -0
- package/src/client/AliyunClient.ts +14 -0
- package/src/docker/AliyunAcrPublisher.ts +61 -0
- package/src/generic/AliyunGenericPublisher.ts +273 -0
- package/src/index.ts +56 -0
- package/src/maven/AliyunMavenPublisher.ts +330 -0
- package/src/npm/AliyunNpmPublisher.ts +76 -0
|
@@ -0,0 +1,273 @@
|
|
|
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 { safeFetch, extractErrorMessage, maskSecrets } from '../client/AliyunClient.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Aliyun Generic Packages & Object Storage Archive Publisher.
|
|
10
|
+
*
|
|
11
|
+
* Supports publishing tar.gz/zip frontend builds or binary packages to Aliyun Packages.
|
|
12
|
+
*
|
|
13
|
+
* Endpoint format: https://packages.aliyun.com/generic/repository/<repoId>/
|
|
14
|
+
* Or direct custom console URL: https://packages.aliyun.com/<orgId>/generic/<repoId>/
|
|
15
|
+
*/
|
|
16
|
+
export class AliyunGenericPublisher extends BasePublisher {
|
|
17
|
+
readonly id = 'aliyun-generic'
|
|
18
|
+
readonly name = 'Aliyun Generic Archive'
|
|
19
|
+
readonly platform = 'aliyun-generic'
|
|
20
|
+
readonly artifactType = 'generic'
|
|
21
|
+
override readonly priority = 20
|
|
22
|
+
|
|
23
|
+
override detect(registry: RegistryConfig): boolean {
|
|
24
|
+
if (registry.type !== 'generic' && registry.type !== 'archive') {
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const regName = (registry.name || '').toLowerCase()
|
|
29
|
+
if (regName === 'aliyun' || regName.startsWith('aliyun-')) {
|
|
30
|
+
return true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const server = (registry.server || '').toLowerCase()
|
|
34
|
+
try {
|
|
35
|
+
const parsed = new URL(server.includes('://') ? server : `https://${server}`)
|
|
36
|
+
const host = parsed.hostname.toLowerCase()
|
|
37
|
+
const isAliyun =
|
|
38
|
+
host === 'aliyun.com' ||
|
|
39
|
+
host.endsWith('.aliyun.com') ||
|
|
40
|
+
host === 'aliyuncs.com' ||
|
|
41
|
+
host.endsWith('.aliyuncs.com')
|
|
42
|
+
return isAliyun
|
|
43
|
+
} catch {
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async login(registry: RegistryConfig, _options?: PublishOptions): Promise<void> {
|
|
49
|
+
const creds = this.resolveCredentials(registry)
|
|
50
|
+
if (!creds.token && !creds.username) {
|
|
51
|
+
this.log(` ℹ No credentials configured for Aliyun Generic storage ${registry.server}`)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the base repository URL.
|
|
57
|
+
* If server already contains path segments (e.g. console URL), respect it directly (M6 fix).
|
|
58
|
+
* If bare host, requires a valid namespace/repoId (M4 fix).
|
|
59
|
+
*/
|
|
60
|
+
protected resolveBaseRepoUrl(registry: RegistryConfig): string {
|
|
61
|
+
let serverUrl = registry.server.trim()
|
|
62
|
+
if (!/^https?:\/\//i.test(serverUrl)) {
|
|
63
|
+
serverUrl = `https://${serverUrl}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const parsed = new URL(serverUrl)
|
|
67
|
+
const cleanServer = serverUrl.replace(/\/+$/, '')
|
|
68
|
+
|
|
69
|
+
// M6 Escape hatch: if server contains path segments beyond '/', use it as-is
|
|
70
|
+
if (parsed.pathname !== '/' && parsed.pathname !== '') {
|
|
71
|
+
return cleanServer
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const reg = registry as RegistryConfig & { metadata?: Record<string, unknown> }
|
|
75
|
+
const repoId =
|
|
76
|
+
reg.namespace?.trim() ||
|
|
77
|
+
(reg.options?.repoId as string | undefined)?.trim() ||
|
|
78
|
+
(reg.metadata?.repoId as string | undefined)?.trim()
|
|
79
|
+
if (!repoId) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Aliyun Generic publisher requires a valid repository ID (specify via registry.namespace or full server URL)`
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return `${cleanServer}/generic/repository/${encodeURIComponent(repoId)}`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
protected buildAuthHeaders(
|
|
89
|
+
username?: string,
|
|
90
|
+
token?: string,
|
|
91
|
+
secrets: string[] = []
|
|
92
|
+
): Record<string, string> {
|
|
93
|
+
const headers: Record<string, string> = {}
|
|
94
|
+
if (username && token) {
|
|
95
|
+
const basic = Buffer.from(`${username}:${token}`).toString('base64')
|
|
96
|
+
if (basic.length >= 3) {
|
|
97
|
+
secrets.push(basic)
|
|
98
|
+
}
|
|
99
|
+
headers['Authorization'] = `Basic ${basic}`
|
|
100
|
+
} else if (token) {
|
|
101
|
+
if (token.includes(':')) {
|
|
102
|
+
const basic = Buffer.from(token).toString('base64')
|
|
103
|
+
if (basic.length >= 3) {
|
|
104
|
+
secrets.push(basic)
|
|
105
|
+
}
|
|
106
|
+
headers['Authorization'] = `Basic ${basic}`
|
|
107
|
+
} else {
|
|
108
|
+
headers['Authorization'] = `Bearer ${token}`
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return headers
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
override buildTargetRef(artifact: ArtifactPayload, _registry: RegistryConfig): string {
|
|
115
|
+
const version = artifact.tags && artifact.tags.length > 0 ? artifact.tags[0] : 'latest'
|
|
116
|
+
const filename = artifact.source ? path.basename(artifact.source) : 'artifact'
|
|
117
|
+
return `${artifact.name}/${version}/${filename}`
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
override async exists(
|
|
121
|
+
artifact: ArtifactPayload,
|
|
122
|
+
registry: RegistryConfig,
|
|
123
|
+
options?: PublishOptions
|
|
124
|
+
): Promise<boolean> {
|
|
125
|
+
const creds = this.resolveCredentials(registry)
|
|
126
|
+
const token = creds.token || creds.password
|
|
127
|
+
const username = creds.username
|
|
128
|
+
const secrets = [token, creds.password].filter(
|
|
129
|
+
(s): s is string => typeof s === 'string' && s.length >= 3
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
const version = artifact.tags && artifact.tags.length > 0 ? artifact.tags[0] : 'latest'
|
|
133
|
+
const packageName = artifact.name
|
|
134
|
+
const filename = artifact.source ? path.basename(artifact.source) : `${packageName}.tar.gz`
|
|
135
|
+
|
|
136
|
+
const baseRepoUrl = this.resolveBaseRepoUrl(registry)
|
|
137
|
+
const probeUrl = `${baseRepoUrl}/${encodeURIComponent(packageName)}/${encodeURIComponent(version)}/${encodeURIComponent(filename)}`
|
|
138
|
+
|
|
139
|
+
const headers = this.buildAuthHeaders(username, token, secrets)
|
|
140
|
+
const timeoutMs = options?.timeoutMs ?? 10000
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const res = await safeFetch(probeUrl, {
|
|
144
|
+
method: 'HEAD',
|
|
145
|
+
headers,
|
|
146
|
+
timeoutMs,
|
|
147
|
+
secrets
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
if (res.ok || res.status === 200 || res.status === 204) {
|
|
151
|
+
return true
|
|
152
|
+
}
|
|
153
|
+
if (res.status === 404) {
|
|
154
|
+
return false
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const errText = await extractErrorMessage(res, secrets)
|
|
158
|
+
throw new Error(`Failed to check Generic artifact existence (${res.status}): ${errText}`)
|
|
159
|
+
} catch (err: any) {
|
|
160
|
+
if (err?.message?.includes('Failed to check Generic artifact existence')) {
|
|
161
|
+
throw err
|
|
162
|
+
}
|
|
163
|
+
throw new Error(`Failed to check Generic artifact existence: ${maskSecrets(err?.message || String(err), secrets)}`)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
override async download(
|
|
168
|
+
artifact: ArtifactPayload,
|
|
169
|
+
registry: RegistryConfig,
|
|
170
|
+
dest: string,
|
|
171
|
+
options?: DownloadOptions
|
|
172
|
+
): Promise<DownloadResult> {
|
|
173
|
+
const creds = this.resolveCredentials(registry)
|
|
174
|
+
const token = creds.token || creds.password
|
|
175
|
+
const secrets = [token, creds.password].filter(
|
|
176
|
+
(s): s is string => typeof s === 'string' && s.length >= 3
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
const version = artifact.tags?.[0] || 'latest'
|
|
180
|
+
const filename = artifact.source ? path.basename(artifact.source) : `${artifact.name}.tar.gz`
|
|
181
|
+
const baseRepoUrl = this.resolveBaseRepoUrl(registry)
|
|
182
|
+
const targetUrl = `${baseRepoUrl}/${encodeURIComponent(artifact.name)}/${encodeURIComponent(version)}/${encodeURIComponent(filename)}`
|
|
183
|
+
const masked = maskSecrets(targetUrl, secrets)
|
|
184
|
+
|
|
185
|
+
this.log(` 📥 Downloading ${artifact.name} ${version} from Aliyun: ${masked}`)
|
|
186
|
+
const res = await safeFetch(targetUrl, {
|
|
187
|
+
method: 'GET',
|
|
188
|
+
headers: this.buildAuthHeaders(creds.username, token, secrets),
|
|
189
|
+
timeoutMs: options?.timeoutMs ?? 30000,
|
|
190
|
+
secrets
|
|
191
|
+
})
|
|
192
|
+
if (!res.ok) {
|
|
193
|
+
throw await this.downloadError(res, `${artifact.name} ${version} (${filename})`, masked)
|
|
194
|
+
}
|
|
195
|
+
return this.saveResponse(res, dest, masked)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
protected async doPublish(
|
|
199
|
+
artifact: ArtifactPayload,
|
|
200
|
+
registry: RegistryConfig,
|
|
201
|
+
options?: PublishOptions
|
|
202
|
+
): Promise<{
|
|
203
|
+
ref: string
|
|
204
|
+
digest?: string
|
|
205
|
+
metadata?: Record<string, unknown>
|
|
206
|
+
}> {
|
|
207
|
+
const creds = this.resolveCredentials(registry)
|
|
208
|
+
const token = creds.token || creds.password
|
|
209
|
+
const username = creds.username
|
|
210
|
+
const secrets = [token, creds.password].filter(
|
|
211
|
+
(s): s is string => typeof s === 'string' && s.length >= 3
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
const version = artifact.tags && artifact.tags.length > 0 ? artifact.tags[0] : 'latest'
|
|
215
|
+
const packageName = artifact.name
|
|
216
|
+
|
|
217
|
+
let filePath = artifact.source
|
|
218
|
+
const stat = await fs.stat(filePath).catch(() => null)
|
|
219
|
+
if (!stat || !stat.isFile()) {
|
|
220
|
+
throw new Error(`Generic artifact source must be an existing file: ${filePath}`)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const filename = path.basename(filePath)
|
|
224
|
+
const fileBuffer = await fs.readFile(filePath)
|
|
225
|
+
const digest = `sha256:${crypto.createHash('sha256').update(fileBuffer).digest('hex')}`
|
|
226
|
+
|
|
227
|
+
const baseRepoUrl = this.resolveBaseRepoUrl(registry)
|
|
228
|
+
const targetUrl = `${baseRepoUrl}/${encodeURIComponent(packageName)}/${encodeURIComponent(version)}/${encodeURIComponent(filename)}`
|
|
229
|
+
|
|
230
|
+
this.log(` 📤 Uploading Generic package to Aliyun: ${targetUrl}`)
|
|
231
|
+
|
|
232
|
+
if (options?.dryRun) {
|
|
233
|
+
this.log(` [dryRun] Would PUT ${filename} (${fileBuffer.length} bytes) to ${targetUrl}`)
|
|
234
|
+
return {
|
|
235
|
+
ref: `${packageName}/${version}/${filename}`,
|
|
236
|
+
digest,
|
|
237
|
+
metadata: { targetUrl, size: fileBuffer.length }
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const headers: Record<string, string> = {
|
|
242
|
+
'Content-Type': 'application/octet-stream',
|
|
243
|
+
...this.buildAuthHeaders(username, token, secrets)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const timeoutMs = options?.timeoutMs ?? 30000
|
|
247
|
+
|
|
248
|
+
const res = await safeFetch(targetUrl, {
|
|
249
|
+
method: 'PUT',
|
|
250
|
+
headers,
|
|
251
|
+
body: fileBuffer,
|
|
252
|
+
timeoutMs,
|
|
253
|
+
secrets
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
if (!res.ok && res.status !== 201 && res.status !== 200) {
|
|
257
|
+
const errText = await extractErrorMessage(res, secrets)
|
|
258
|
+
throw new Error(`Failed to upload Generic package to Aliyun (${res.status}): ${errText}`)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
ref: `${packageName}/${version}/${filename}`,
|
|
263
|
+
digest,
|
|
264
|
+
metadata: {
|
|
265
|
+
packageName,
|
|
266
|
+
version,
|
|
267
|
+
filename,
|
|
268
|
+
targetUrl,
|
|
269
|
+
size: fileBuffer.length
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { PluginContext } from '@ticatec/omniflow-core'
|
|
2
|
+
import type { ArtifactPublisherRegistry } from '@ticatec/omniflow-artifact'
|
|
3
|
+
import { AliyunAcrPublisher } from './docker/AliyunAcrPublisher.js'
|
|
4
|
+
import { AliyunNpmPublisher } from './npm/AliyunNpmPublisher.js'
|
|
5
|
+
import { AliyunMavenPublisher } from './maven/AliyunMavenPublisher.js'
|
|
6
|
+
import { AliyunGenericPublisher } from './generic/AliyunGenericPublisher.js'
|
|
7
|
+
|
|
8
|
+
export * from './docker/AliyunAcrPublisher.js'
|
|
9
|
+
export * from './npm/AliyunNpmPublisher.js'
|
|
10
|
+
export * from './maven/AliyunMavenPublisher.js'
|
|
11
|
+
export * from './generic/AliyunGenericPublisher.js'
|
|
12
|
+
export * from './client/AliyunClient.js'
|
|
13
|
+
|
|
14
|
+
export interface RegisterAliyunOptions {
|
|
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
|
+
registerAliyun({ artifactRegistry: artifactsEnabled ? ctx.artifactRegistry : undefined })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default register
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Register all official Aliyun artifact publishers (ACR, NPM, Maven, Generic) into the target registry.
|
|
30
|
+
*/
|
|
31
|
+
export function registerAliyun(options: RegisterAliyunOptions = {}): {
|
|
32
|
+
acrPublisher: AliyunAcrPublisher
|
|
33
|
+
npmPublisher: AliyunNpmPublisher
|
|
34
|
+
mavenPublisher: AliyunMavenPublisher
|
|
35
|
+
genericPublisher: AliyunGenericPublisher
|
|
36
|
+
} {
|
|
37
|
+
const acrPublisher = new AliyunAcrPublisher()
|
|
38
|
+
const npmPublisher = new AliyunNpmPublisher()
|
|
39
|
+
const mavenPublisher = new AliyunMavenPublisher()
|
|
40
|
+
const genericPublisher = new AliyunGenericPublisher()
|
|
41
|
+
|
|
42
|
+
if (options.artifactRegistry) {
|
|
43
|
+
options.artifactRegistry.register(acrPublisher)
|
|
44
|
+
options.artifactRegistry.register(npmPublisher)
|
|
45
|
+
options.artifactRegistry.register(mavenPublisher)
|
|
46
|
+
options.artifactRegistry.register(genericPublisher)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
acrPublisher,
|
|
51
|
+
npmPublisher,
|
|
52
|
+
mavenPublisher,
|
|
53
|
+
genericPublisher
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
@@ -0,0 +1,330 @@
|
|
|
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 { safeFetch, extractErrorMessage, maskSecrets } from '../client/AliyunClient.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Aliyun Apsara DevOps (云效) Maven Package Registry Publisher.
|
|
10
|
+
*
|
|
11
|
+
* Endpoint format: https://packages.aliyun.com/maven/repository/<repoId>/
|
|
12
|
+
* Or direct custom console URL: https://packages.aliyun.com/<orgId>/maven/<repoId>/
|
|
13
|
+
*/
|
|
14
|
+
export class AliyunMavenPublisher extends BasePublisher {
|
|
15
|
+
readonly id = 'aliyun-maven'
|
|
16
|
+
readonly name = 'Aliyun Packages Maven'
|
|
17
|
+
readonly platform = 'aliyun-maven'
|
|
18
|
+
readonly artifactType = 'maven'
|
|
19
|
+
override readonly priority = 20
|
|
20
|
+
|
|
21
|
+
override detect(registry: RegistryConfig): boolean {
|
|
22
|
+
if (registry.type !== 'maven' && registry.type !== 'jar') {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const regName = (registry.name || '').toLowerCase()
|
|
27
|
+
if (regName === 'aliyun' || regName.startsWith('aliyun-')) {
|
|
28
|
+
return true
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const server = (registry.server || '').toLowerCase()
|
|
32
|
+
try {
|
|
33
|
+
const parsed = new URL(server.includes('://') ? server : `https://${server}`)
|
|
34
|
+
const host = parsed.hostname.toLowerCase()
|
|
35
|
+
const segments = host.split('.')
|
|
36
|
+
const isAliyun = host === 'aliyun.com' || host.endsWith('.aliyun.com')
|
|
37
|
+
const isPackages = segments.includes('packages') || host === 'packages.aliyun.com'
|
|
38
|
+
return isAliyun && isPackages
|
|
39
|
+
} catch {
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async login(registry: RegistryConfig, _options?: PublishOptions): Promise<void> {
|
|
45
|
+
const creds = this.resolveCredentials(registry)
|
|
46
|
+
if (!creds.token && !creds.username) {
|
|
47
|
+
this.log(` ℹ No credentials configured for Aliyun Maven repository ${registry.server}`)
|
|
48
|
+
} else if (creds.token && !creds.username && !creds.token.includes(':')) {
|
|
49
|
+
this.warn(` ⚠ Aliyun Maven repository typically requires username + password/token for Basic authentication`)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve Maven coordinates (groupId, artifactId, version) from artifact.
|
|
55
|
+
* Throws an error if groupId is missing rather than guessing 'com.aliyun' (M4 fix).
|
|
56
|
+
*/
|
|
57
|
+
protected resolveCoordinates(artifact: ArtifactPayload): {
|
|
58
|
+
groupId: string
|
|
59
|
+
artifactId: string
|
|
60
|
+
version: string
|
|
61
|
+
} {
|
|
62
|
+
let groupId = (artifact.metadata?.groupId as string | undefined)?.trim()
|
|
63
|
+
let artifactId = (artifact.metadata?.artifactId as string | undefined)?.trim()
|
|
64
|
+
|
|
65
|
+
if (!groupId && artifact.name.includes(':')) {
|
|
66
|
+
const parts = artifact.name.split(':')
|
|
67
|
+
groupId = parts[0].trim()
|
|
68
|
+
artifactId = artifactId || parts[1].trim()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!groupId) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Aliyun Maven publisher requires a valid 'groupId' (specify via artifact.metadata.groupId or 'groupId:artifactId' name)`
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!artifactId) {
|
|
78
|
+
artifactId = artifact.name.trim()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const version =
|
|
82
|
+
artifact.tags && artifact.tags.length > 0 ? artifact.tags[0] : (artifact.metadata?.version as string) || '1.0.0'
|
|
83
|
+
|
|
84
|
+
return { groupId, artifactId, version }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolve the base repository URL.
|
|
89
|
+
* If server already contains path segments (e.g. console URL), respect it directly (M6 fix).
|
|
90
|
+
* If bare host, requires a valid namespace/repoId (M4 fix).
|
|
91
|
+
*/
|
|
92
|
+
protected resolveBaseRepoUrl(registry: RegistryConfig): string {
|
|
93
|
+
let serverUrl = registry.server.trim()
|
|
94
|
+
if (!/^https?:\/\//i.test(serverUrl)) {
|
|
95
|
+
serverUrl = `https://${serverUrl}`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const parsed = new URL(serverUrl)
|
|
99
|
+
const cleanServer = serverUrl.replace(/\/+$/, '')
|
|
100
|
+
|
|
101
|
+
// M6 Escape hatch: if server contains path segments beyond '/', use it as-is
|
|
102
|
+
if (parsed.pathname !== '/' && parsed.pathname !== '') {
|
|
103
|
+
return cleanServer
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const reg = registry as RegistryConfig & { metadata?: Record<string, unknown> }
|
|
107
|
+
const repoId =
|
|
108
|
+
reg.namespace?.trim() ||
|
|
109
|
+
(reg.options?.repoId as string | undefined)?.trim() ||
|
|
110
|
+
(reg.metadata?.repoId as string | undefined)?.trim()
|
|
111
|
+
if (!repoId) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Aliyun Maven publisher requires a valid repository ID (specify via registry.namespace or full server URL)`
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return `${cleanServer}/maven/repository/${encodeURIComponent(repoId)}`
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
protected buildAuthHeaders(
|
|
121
|
+
username?: string,
|
|
122
|
+
token?: string,
|
|
123
|
+
secrets: string[] = []
|
|
124
|
+
): Record<string, string> {
|
|
125
|
+
const headers: Record<string, string> = {}
|
|
126
|
+
if (username && token) {
|
|
127
|
+
const basic = Buffer.from(`${username}:${token}`).toString('base64')
|
|
128
|
+
if (basic.length >= 3) {
|
|
129
|
+
secrets.push(basic)
|
|
130
|
+
}
|
|
131
|
+
headers['Authorization'] = `Basic ${basic}`
|
|
132
|
+
} else if (token) {
|
|
133
|
+
if (token.includes(':')) {
|
|
134
|
+
const basic = Buffer.from(token).toString('base64')
|
|
135
|
+
if (basic.length >= 3) {
|
|
136
|
+
secrets.push(basic)
|
|
137
|
+
}
|
|
138
|
+
headers['Authorization'] = `Basic ${basic}`
|
|
139
|
+
} else {
|
|
140
|
+
headers['Authorization'] = `Bearer ${token}`
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return headers
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
override buildTargetRef(artifact: ArtifactPayload, _registry: RegistryConfig): string {
|
|
147
|
+
try {
|
|
148
|
+
const { groupId, artifactId, version } = this.resolveCoordinates(artifact)
|
|
149
|
+
return `${groupId}:${artifactId}:${version}`
|
|
150
|
+
} catch {
|
|
151
|
+
const version = artifact.tags && artifact.tags.length > 0 ? artifact.tags[0] : '1.0.0'
|
|
152
|
+
return `${artifact.name}:${version}`
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
override async exists(
|
|
157
|
+
artifact: ArtifactPayload,
|
|
158
|
+
registry: RegistryConfig,
|
|
159
|
+
options?: PublishOptions
|
|
160
|
+
): Promise<boolean> {
|
|
161
|
+
const creds = this.resolveCredentials(registry)
|
|
162
|
+
const token = creds.token || creds.password
|
|
163
|
+
const username = creds.username
|
|
164
|
+
const secrets = [token, creds.password].filter(
|
|
165
|
+
(s): s is string => typeof s === 'string' && s.length >= 3
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
let jarPath = artifact.source
|
|
169
|
+
if (jarPath) {
|
|
170
|
+
const stat = await fs.stat(jarPath).catch(() => null)
|
|
171
|
+
if (stat?.isDirectory()) {
|
|
172
|
+
const files = await fs.readdir(jarPath).catch(() => [] as string[])
|
|
173
|
+
const jarFile = files.find(
|
|
174
|
+
f => isMavenArtifactFile(f)
|
|
175
|
+
)
|
|
176
|
+
if (jarFile) {
|
|
177
|
+
jarPath = path.join(jarPath, jarFile)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const { groupId, artifactId, version, groupPath, jarFilename } = resolveMavenArtifactFilenames(artifact, jarPath, undefined, 'Aliyun')
|
|
183
|
+
|
|
184
|
+
const baseRepoUrl = this.resolveBaseRepoUrl(registry)
|
|
185
|
+
const probeUrl = `${baseRepoUrl}/${groupPath}/${artifactId}/${version}/${jarFilename}`
|
|
186
|
+
|
|
187
|
+
const headers = this.buildAuthHeaders(username, token, secrets)
|
|
188
|
+
const timeoutMs = options?.timeoutMs ?? 10000
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const res = await safeFetch(probeUrl, {
|
|
192
|
+
method: 'HEAD',
|
|
193
|
+
headers,
|
|
194
|
+
timeoutMs,
|
|
195
|
+
secrets
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
if (res.ok || res.status === 200 || res.status === 204) {
|
|
199
|
+
return true
|
|
200
|
+
}
|
|
201
|
+
if (res.status === 404) {
|
|
202
|
+
return false
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Fail-close on authorization or server errors
|
|
206
|
+
const errText = await extractErrorMessage(res, secrets)
|
|
207
|
+
throw new Error(`Failed to check Maven artifact existence (${res.status}): ${errText}`)
|
|
208
|
+
} catch (err: any) {
|
|
209
|
+
if (err?.message?.includes('Failed to check Maven artifact existence')) {
|
|
210
|
+
throw err
|
|
211
|
+
}
|
|
212
|
+
throw new Error(`Failed to check Maven artifact existence: ${maskSecrets(err?.message || String(err), secrets)}`)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
protected async doPublish(
|
|
217
|
+
artifact: ArtifactPayload,
|
|
218
|
+
registry: RegistryConfig,
|
|
219
|
+
options?: PublishOptions
|
|
220
|
+
): Promise<{
|
|
221
|
+
ref: string
|
|
222
|
+
digest?: string
|
|
223
|
+
metadata?: Record<string, unknown>
|
|
224
|
+
}> {
|
|
225
|
+
const creds = this.resolveCredentials(registry)
|
|
226
|
+
const token = creds.token || creds.password
|
|
227
|
+
const username = creds.username
|
|
228
|
+
const secrets = [token, creds.password].filter(
|
|
229
|
+
(s): s is string => typeof s === 'string' && s.length >= 3
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
// Locate jar file
|
|
233
|
+
let jarPath = artifact.source
|
|
234
|
+
const stat = await fs.stat(jarPath).catch(() => null)
|
|
235
|
+
if (stat?.isDirectory()) {
|
|
236
|
+
const files = await fs.readdir(jarPath)
|
|
237
|
+
const jarFile = files.find(
|
|
238
|
+
f => isMavenArtifactFile(f)
|
|
239
|
+
)
|
|
240
|
+
if (!jarFile) {
|
|
241
|
+
throw new Error(`No jar/war artifact found in directory: ${jarPath}`)
|
|
242
|
+
}
|
|
243
|
+
jarPath = path.join(jarPath, jarFile)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const { groupId, artifactId, version, groupPath, jarFilename, pomFilename } = resolveMavenArtifactFilenames(artifact, jarPath, undefined, 'Aliyun')
|
|
247
|
+
|
|
248
|
+
const jarBuffer = await fs.readFile(jarPath)
|
|
249
|
+
const digest = `sha256:${crypto.createHash('sha256').update(jarBuffer).digest('hex')}`
|
|
250
|
+
|
|
251
|
+
const baseRepoUrl = this.resolveBaseRepoUrl(registry)
|
|
252
|
+
const targetUrl = `${baseRepoUrl}/${groupPath}/${artifactId}/${version}/${jarFilename}`
|
|
253
|
+
|
|
254
|
+
this.log(` 📤 Uploading Maven JAR to Aliyun Packages: ${targetUrl}`)
|
|
255
|
+
|
|
256
|
+
if (options?.dryRun) {
|
|
257
|
+
this.log(` [dryRun] Would PUT ${jarFilename} (${jarBuffer.length} bytes) to ${targetUrl}`)
|
|
258
|
+
return {
|
|
259
|
+
ref: `${groupId}:${artifactId}:${version}`,
|
|
260
|
+
digest,
|
|
261
|
+
metadata: { targetUrl, size: jarBuffer.length }
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const headers: Record<string, string> = {
|
|
266
|
+
'Content-Type': 'application/java-archive',
|
|
267
|
+
...this.buildAuthHeaders(username, token, secrets)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const timeoutMs = options?.timeoutMs ?? 30000
|
|
271
|
+
|
|
272
|
+
// The POM goes up first, deliberately.
|
|
273
|
+
//
|
|
274
|
+
// The existence probe checks for the jar/war, so uploading that last is what
|
|
275
|
+
// makes the probe mean "this artifact is complete". With the artifact first, a
|
|
276
|
+
// run that uploaded it and then failed on the POM would leave a coordinate that
|
|
277
|
+
// probes as present: the retry skips it as already published, and the POM never
|
|
278
|
+
// arrives — a jar in the repository forever, with no dependency information.
|
|
279
|
+
// Check if matching .pom exists and upload it as well (M2 verification)
|
|
280
|
+
const pomPath = mavenPomPathFor(jarPath)
|
|
281
|
+
const pomStat = await fs.stat(pomPath).catch(() => null)
|
|
282
|
+
if (pomStat?.isFile()) {
|
|
283
|
+
const pomBuffer = await fs.readFile(pomPath)
|
|
284
|
+
const pomFilename = `${artifactId}-${version}.pom`
|
|
285
|
+
const pomTargetUrl = `${baseRepoUrl}/${groupPath}/${artifactId}/${version}/${pomFilename}`
|
|
286
|
+
this.log(` 📤 Uploading Maven POM to Aliyun Packages: ${pomTargetUrl}`)
|
|
287
|
+
|
|
288
|
+
const pomRes = await safeFetch(pomTargetUrl, {
|
|
289
|
+
method: 'PUT',
|
|
290
|
+
headers: {
|
|
291
|
+
...headers,
|
|
292
|
+
'Content-Type': 'application/xml'
|
|
293
|
+
},
|
|
294
|
+
body: pomBuffer,
|
|
295
|
+
timeoutMs,
|
|
296
|
+
secrets
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
if (!pomRes.ok && pomRes.status !== 201 && pomRes.status !== 200) {
|
|
300
|
+
const errText = await extractErrorMessage(pomRes, secrets)
|
|
301
|
+
throw new Error(`Failed to upload Maven POM to Aliyun Packages (${pomRes.status}): ${errText}`)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const res = await safeFetch(targetUrl, {
|
|
306
|
+
method: 'PUT',
|
|
307
|
+
headers,
|
|
308
|
+
body: jarBuffer,
|
|
309
|
+
timeoutMs,
|
|
310
|
+
secrets
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
if (!res.ok && res.status !== 201 && res.status !== 200) {
|
|
314
|
+
const errText = await extractErrorMessage(res, secrets)
|
|
315
|
+
throw new Error(`Failed to upload Maven JAR to Aliyun Packages (${res.status}): ${errText}`)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
ref: `${groupId}:${artifactId}:${version}`,
|
|
320
|
+
digest,
|
|
321
|
+
metadata: {
|
|
322
|
+
groupId,
|
|
323
|
+
artifactId,
|
|
324
|
+
version,
|
|
325
|
+
targetUrl,
|
|
326
|
+
size: jarBuffer.length
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|