@ticatec/omniflow-nexus 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 +94 -0
- package/dist/client/NexusClient.d.ts +42 -0
- package/dist/client/NexusClient.d.ts.map +1 -0
- package/dist/client/NexusClient.js +106 -0
- package/dist/client/NexusClient.js.map +1 -0
- package/dist/docker/NexusDockerPublisher.d.ts +18 -0
- package/dist/docker/NexusDockerPublisher.d.ts.map +1 -0
- package/dist/docker/NexusDockerPublisher.js +56 -0
- package/dist/docker/NexusDockerPublisher.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/maven/NexusMavenPublisher.d.ts +25 -0
- package/dist/maven/NexusMavenPublisher.d.ts.map +1 -0
- package/dist/maven/NexusMavenPublisher.js +194 -0
- package/dist/maven/NexusMavenPublisher.js.map +1 -0
- package/dist/npm/NexusNpmPublisher.d.ts +17 -0
- package/dist/npm/NexusNpmPublisher.d.ts.map +1 -0
- package/dist/npm/NexusNpmPublisher.js +67 -0
- package/dist/npm/NexusNpmPublisher.js.map +1 -0
- package/dist/raw/NexusRawPublisher.d.ts +28 -0
- package/dist/raw/NexusRawPublisher.d.ts.map +1 -0
- package/dist/raw/NexusRawPublisher.js +174 -0
- package/dist/raw/NexusRawPublisher.js.map +1 -0
- package/package.json +48 -0
- package/src/client/NexusClient.ts +143 -0
- package/src/docker/NexusDockerPublisher.ts +66 -0
- package/src/index.ts +54 -0
- package/src/maven/NexusMavenPublisher.ts +247 -0
- package/src/npm/NexusNpmPublisher.ts +81 -0
- package/src/raw/NexusRawPublisher.ts +217 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import {
|
|
2
|
+
maskSecrets,
|
|
3
|
+
safeFetch,
|
|
4
|
+
extractErrorMessage,
|
|
5
|
+
type SafeFetchOptions
|
|
6
|
+
} from '@ticatec/omniflow-artifact'
|
|
7
|
+
|
|
8
|
+
export interface NexusClientConfig {
|
|
9
|
+
baseUrl: string
|
|
10
|
+
username?: string
|
|
11
|
+
password?: string
|
|
12
|
+
token?: string
|
|
13
|
+
timeoutMs?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class NexusClient {
|
|
17
|
+
readonly baseUrl: string
|
|
18
|
+
readonly username?: string
|
|
19
|
+
readonly password?: string
|
|
20
|
+
readonly token?: string
|
|
21
|
+
readonly timeoutMs: number
|
|
22
|
+
|
|
23
|
+
constructor(config: NexusClientConfig) {
|
|
24
|
+
let base = config.baseUrl.trim().replace(/\/+$/, '')
|
|
25
|
+
if (!/^https?:\/\//i.test(base)) {
|
|
26
|
+
base = `https://${base}`
|
|
27
|
+
}
|
|
28
|
+
this.baseUrl = base
|
|
29
|
+
this.username = config.username
|
|
30
|
+
this.password = config.password
|
|
31
|
+
this.token = config.token
|
|
32
|
+
this.timeoutMs = config.timeoutMs ?? 30000
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get host(): string {
|
|
36
|
+
const parsed = new URL(this.baseUrl)
|
|
37
|
+
return parsed.host
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get hostname(): string {
|
|
41
|
+
const parsed = new URL(this.baseUrl)
|
|
42
|
+
return parsed.hostname
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Return sensitive credential strings of length >= 3 for scrubbing.
|
|
47
|
+
* Username is intentionally excluded to preserve debugging visibility.
|
|
48
|
+
*/
|
|
49
|
+
getSecrets(): string[] {
|
|
50
|
+
const secrets = [this.token, this.password].filter(
|
|
51
|
+
(s): s is string => typeof s === 'string' && s.length >= 3
|
|
52
|
+
)
|
|
53
|
+
if (this.username && this.password) {
|
|
54
|
+
const basic = Buffer.from(`${this.username}:${this.password}`).toString('base64')
|
|
55
|
+
if (basic.length >= 3) {
|
|
56
|
+
secrets.push(basic)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return secrets
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
maskSecrets(input: string): string {
|
|
63
|
+
return maskSecrets(input, this.getSecrets())
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async extractErrorMessage(response: Response): Promise<string> {
|
|
67
|
+
return extractErrorMessage(response, this.getSecrets())
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
getAuthHeaders(): Record<string, string> {
|
|
71
|
+
const headers: Record<string, string> = {}
|
|
72
|
+
if (this.token) {
|
|
73
|
+
headers['Authorization'] = `Bearer ${this.token}`
|
|
74
|
+
} else if (this.username && this.password) {
|
|
75
|
+
const basic = Buffer.from(`${this.username}:${this.password}`).toString('base64')
|
|
76
|
+
headers['Authorization'] = `Basic ${basic}`
|
|
77
|
+
}
|
|
78
|
+
return headers
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Resolve root repository URL for a given repository name.
|
|
83
|
+
* Handles cases where baseUrl might already contain "/repository/<name>".
|
|
84
|
+
*/
|
|
85
|
+
getRepositoryBaseUrl(repoName = ''): string {
|
|
86
|
+
let cleanBase = this.baseUrl.replace(/\/+$/, '')
|
|
87
|
+
|
|
88
|
+
if (cleanBase.includes('/repository/')) {
|
|
89
|
+
const idx = cleanBase.indexOf('/repository/')
|
|
90
|
+
const root = cleanBase.substring(0, idx)
|
|
91
|
+
const existingRepo = cleanBase.substring(idx + '/repository/'.length).split('/')[0]
|
|
92
|
+
const targetRepo = repoName || existingRepo
|
|
93
|
+
return `${root}/repository/${targetRepo}`
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!repoName) {
|
|
97
|
+
return `${cleanBase}/repository`
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return `${cleanBase}/repository/${repoName.replace(/^\/+/, '').replace(/\/+$/, '')}`
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Build URL for Maven artifact.
|
|
105
|
+
*/
|
|
106
|
+
getMavenArtifactUrl(
|
|
107
|
+
repoName: string,
|
|
108
|
+
groupId: string,
|
|
109
|
+
artifactId: string,
|
|
110
|
+
version: string,
|
|
111
|
+
filename: string
|
|
112
|
+
): string {
|
|
113
|
+
const base = this.getRepositoryBaseUrl(repoName)
|
|
114
|
+
const groupPath = groupId.replace(/\./g, '/')
|
|
115
|
+
return `${base}/${groupPath}/${artifactId}/${version}/${filename}`
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Build URL for Raw (Generic) artifact.
|
|
120
|
+
*/
|
|
121
|
+
getRawArtifactUrl(repoName: string, subpath: string, filename: string): string {
|
|
122
|
+
const base = this.getRepositoryBaseUrl(repoName)
|
|
123
|
+
const cleanSubpath = subpath ? subpath.replace(/^\/+/, '').replace(/\/+$/, '') : ''
|
|
124
|
+
return cleanSubpath ? `${base}/${cleanSubpath}/${filename}` : `${base}/${filename}`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async request(url: string, init: RequestInit = {}): Promise<Response> {
|
|
128
|
+
const headers = {
|
|
129
|
+
...this.getAuthHeaders(),
|
|
130
|
+
...(init.headers as Record<string, string> || {})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return safeFetch(url, {
|
|
134
|
+
...init,
|
|
135
|
+
headers,
|
|
136
|
+
timeoutMs: this.timeoutMs,
|
|
137
|
+
secrets: this.getSecrets()
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export { maskSecrets, safeFetch, extractErrorMessage, type SafeFetchOptions }
|
|
143
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { BaseDockerPublisher } from '@ticatec/omniflow-artifact'
|
|
2
|
+
import type { RegistryConfig } from '@ticatec/omniflow-artifact'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sonatype Nexus 3 Docker Registry Publisher.
|
|
6
|
+
*
|
|
7
|
+
* Supports Nexus Docker hosted repositories via:
|
|
8
|
+
* 1. Dedicated HTTP connector port (e.g. nexus.example.com:8082/my-app:tag)
|
|
9
|
+
* 2. Repository path routing (e.g. nexus.example.com/repository/docker-hosted/my-app:tag)
|
|
10
|
+
*/
|
|
11
|
+
export class NexusDockerPublisher extends BaseDockerPublisher {
|
|
12
|
+
readonly id = 'nexus-docker'
|
|
13
|
+
readonly name = 'Nexus 3 Docker Registry'
|
|
14
|
+
readonly platform = 'nexus'
|
|
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 === 'nexus' || regName.startsWith('nexus-') || regName === 'nexus-docker') {
|
|
24
|
+
return true
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const server = (registry.server || '').toLowerCase()
|
|
28
|
+
try {
|
|
29
|
+
const parsed = new URL(server.includes('://') ? server : `https://${server}`)
|
|
30
|
+
const host = parsed.hostname.toLowerCase()
|
|
31
|
+
|
|
32
|
+
// Exclude other known platforms
|
|
33
|
+
if (
|
|
34
|
+
host === 'aliyun.com' ||
|
|
35
|
+
host.endsWith('.aliyun.com') ||
|
|
36
|
+
host.endsWith('.aliyuncs.com') ||
|
|
37
|
+
host === 'gitlab.com' ||
|
|
38
|
+
host.endsWith('.gitlab.com') ||
|
|
39
|
+
host === 'github.com' ||
|
|
40
|
+
host.endsWith('.github.com')
|
|
41
|
+
) {
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return host.includes('nexus') || parsed.pathname.includes('/repository/docker')
|
|
46
|
+
} catch {
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
override buildRepositoryUrl(registry: RegistryConfig, imageName: string): string {
|
|
52
|
+
let host = this.normalizeServerHost(registry.server)
|
|
53
|
+
const cleanImage = imageName.replace(/^\/+/, '')
|
|
54
|
+
|
|
55
|
+
// Check if server or namespace includes repository path
|
|
56
|
+
const ns = (registry.namespace || '').replace(/\/+$/, '')
|
|
57
|
+
if (ns) {
|
|
58
|
+
if (cleanImage.startsWith(`${ns}/`)) {
|
|
59
|
+
return `${host}/${cleanImage}`
|
|
60
|
+
}
|
|
61
|
+
return `${host}/${ns}/${cleanImage}`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return `${host}/${cleanImage}`
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { PluginContext } from '@ticatec/omniflow-core'
|
|
2
|
+
import type { ArtifactPublisherRegistry } from '@ticatec/omniflow-artifact'
|
|
3
|
+
import { NexusMavenPublisher } from './maven/NexusMavenPublisher.js'
|
|
4
|
+
import { NexusNpmPublisher } from './npm/NexusNpmPublisher.js'
|
|
5
|
+
import { NexusDockerPublisher } from './docker/NexusDockerPublisher.js'
|
|
6
|
+
import { NexusRawPublisher } from './raw/NexusRawPublisher.js'
|
|
7
|
+
|
|
8
|
+
export * from './client/NexusClient.js'
|
|
9
|
+
export * from './maven/NexusMavenPublisher.js'
|
|
10
|
+
export * from './npm/NexusNpmPublisher.js'
|
|
11
|
+
export * from './docker/NexusDockerPublisher.js'
|
|
12
|
+
export * from './raw/NexusRawPublisher.js'
|
|
13
|
+
|
|
14
|
+
export interface RegisterNexusOptions {
|
|
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
|
+
registerNexus({ artifactRegistry: ctx.artifactRegistry })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default register
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Register all Sonatype Nexus 3 artifact publishers into the target registry.
|
|
29
|
+
*/
|
|
30
|
+
export function registerNexus(options: RegisterNexusOptions = {}): {
|
|
31
|
+
mavenPublisher: NexusMavenPublisher
|
|
32
|
+
npmPublisher: NexusNpmPublisher
|
|
33
|
+
dockerPublisher: NexusDockerPublisher
|
|
34
|
+
rawPublisher: NexusRawPublisher
|
|
35
|
+
} {
|
|
36
|
+
const mavenPublisher = new NexusMavenPublisher()
|
|
37
|
+
const npmPublisher = new NexusNpmPublisher()
|
|
38
|
+
const dockerPublisher = new NexusDockerPublisher()
|
|
39
|
+
const rawPublisher = new NexusRawPublisher()
|
|
40
|
+
|
|
41
|
+
if (options.artifactRegistry) {
|
|
42
|
+
options.artifactRegistry.register(mavenPublisher)
|
|
43
|
+
options.artifactRegistry.register(npmPublisher)
|
|
44
|
+
options.artifactRegistry.register(dockerPublisher)
|
|
45
|
+
options.artifactRegistry.register(rawPublisher)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
mavenPublisher,
|
|
50
|
+
npmPublisher,
|
|
51
|
+
dockerPublisher,
|
|
52
|
+
rawPublisher
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
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 { NexusClient } from '../client/NexusClient.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sonatype Nexus 3 Maven Repository Publisher.
|
|
10
|
+
*
|
|
11
|
+
* Endpoint format: PUT {server}/repository/{repoName}/{groupId-as-path}/{artifactId}/{version}/{filename}
|
|
12
|
+
* Typically used with repositories like "maven-releases" or "maven-snapshots".
|
|
13
|
+
*/
|
|
14
|
+
export class NexusMavenPublisher extends BasePublisher {
|
|
15
|
+
readonly id = 'nexus-maven'
|
|
16
|
+
readonly name = 'Nexus 3 Maven Repository'
|
|
17
|
+
readonly platform = 'nexus'
|
|
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 === 'nexus' || regName.startsWith('nexus-')) {
|
|
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
|
+
|
|
36
|
+
// Exclude other known platforms
|
|
37
|
+
if (
|
|
38
|
+
host === 'aliyun.com' ||
|
|
39
|
+
host.endsWith('.aliyun.com') ||
|
|
40
|
+
host.endsWith('.aliyuncs.com') ||
|
|
41
|
+
host === 'gitlab.com' ||
|
|
42
|
+
host.endsWith('.gitlab.com') ||
|
|
43
|
+
host === 'github.com' ||
|
|
44
|
+
host.endsWith('.github.com')
|
|
45
|
+
) {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return host.includes('nexus') || parsed.pathname.includes('/repository/')
|
|
50
|
+
} catch {
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async login(registry: RegistryConfig, _options?: PublishOptions): Promise<void> {
|
|
56
|
+
const creds = this.resolveCredentials(registry)
|
|
57
|
+
if (!creds.token && (!creds.username || !creds.password)) {
|
|
58
|
+
this.log(` ℹ No auth credentials configured for Nexus Maven repository ${registry.server}`)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
override buildTargetRef(artifact: ArtifactPayload, _registry: RegistryConfig): string {
|
|
63
|
+
try {
|
|
64
|
+
const { groupId, artifactId, version } = resolveMavenArtifactFilenames(artifact, undefined, undefined, 'Nexus')
|
|
65
|
+
return `${groupId}:${artifactId}:${version}`
|
|
66
|
+
} catch {
|
|
67
|
+
const version = artifact.tags && artifact.tags.length > 0 ? artifact.tags[0] : '1.0.0'
|
|
68
|
+
return `${artifact.name}:${version}`
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
override async exists(
|
|
73
|
+
artifact: ArtifactPayload,
|
|
74
|
+
registry: RegistryConfig,
|
|
75
|
+
options?: PublishOptions
|
|
76
|
+
): Promise<boolean> {
|
|
77
|
+
const creds = this.resolveCredentials(registry)
|
|
78
|
+
const client = new NexusClient({
|
|
79
|
+
baseUrl: registry.server,
|
|
80
|
+
username: creds.username,
|
|
81
|
+
password: creds.password,
|
|
82
|
+
token: creds.token,
|
|
83
|
+
timeoutMs: options?.timeoutMs
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
let jarPath = artifact.source
|
|
87
|
+
if (jarPath) {
|
|
88
|
+
const stat = await fs.stat(jarPath).catch(() => null)
|
|
89
|
+
if (stat?.isDirectory()) {
|
|
90
|
+
const files = await fs.readdir(jarPath).catch(() => [] as string[])
|
|
91
|
+
const jarFile = files.find(
|
|
92
|
+
f => isMavenArtifactFile(f)
|
|
93
|
+
)
|
|
94
|
+
if (jarFile) {
|
|
95
|
+
jarPath = path.join(jarPath, jarFile)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const { groupId, artifactId, version, jarFilename } = resolveMavenArtifactFilenames(
|
|
101
|
+
artifact,
|
|
102
|
+
jarPath,
|
|
103
|
+
undefined,
|
|
104
|
+
'Nexus'
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
const defaultRepo = version.toUpperCase().endsWith('-SNAPSHOT') ? 'maven-snapshots' : 'maven-releases'
|
|
108
|
+
const repoName =
|
|
109
|
+
registry.namespace?.trim() ||
|
|
110
|
+
(registry.options?.repository as string | undefined)?.trim() ||
|
|
111
|
+
((registry as any).metadata?.repository as string | undefined)?.trim() ||
|
|
112
|
+
defaultRepo
|
|
113
|
+
|
|
114
|
+
const targetUrl = client.getMavenArtifactUrl(repoName, groupId, artifactId, version, jarFilename)
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const res = await client.request(targetUrl, { method: 'HEAD' })
|
|
118
|
+
if (res.ok || res.status === 200 || res.status === 204) {
|
|
119
|
+
return true
|
|
120
|
+
}
|
|
121
|
+
if (res.status === 404) {
|
|
122
|
+
return false
|
|
123
|
+
}
|
|
124
|
+
const err = await client.extractErrorMessage(res)
|
|
125
|
+
throw new Error(`Failed to check Maven artifact existence (${res.status}): ${err}`)
|
|
126
|
+
} catch (err: any) {
|
|
127
|
+
if (err?.message?.includes('Failed to check Maven artifact existence')) {
|
|
128
|
+
throw err
|
|
129
|
+
}
|
|
130
|
+
throw new Error(`Failed to check Maven artifact existence: ${client.maskSecrets(err?.message || String(err))}`)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
protected async doPublish(
|
|
135
|
+
artifact: ArtifactPayload,
|
|
136
|
+
registry: RegistryConfig,
|
|
137
|
+
options?: PublishOptions
|
|
138
|
+
): Promise<{
|
|
139
|
+
ref: string
|
|
140
|
+
digest?: string
|
|
141
|
+
metadata?: Record<string, unknown>
|
|
142
|
+
}> {
|
|
143
|
+
const creds = this.resolveCredentials(registry)
|
|
144
|
+
const client = new NexusClient({
|
|
145
|
+
baseUrl: registry.server,
|
|
146
|
+
username: creds.username,
|
|
147
|
+
password: creds.password,
|
|
148
|
+
token: creds.token,
|
|
149
|
+
timeoutMs: options?.timeoutMs
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// Locate jar file
|
|
153
|
+
let jarPath = artifact.source
|
|
154
|
+
const stat = await fs.stat(jarPath).catch(() => null)
|
|
155
|
+
if (stat?.isDirectory()) {
|
|
156
|
+
const files = await fs.readdir(jarPath)
|
|
157
|
+
const jarFile = files.find(f => isMavenArtifactFile(f))
|
|
158
|
+
if (!jarFile) {
|
|
159
|
+
throw new Error(`No jar/war artifact found in directory: ${jarPath}`)
|
|
160
|
+
}
|
|
161
|
+
jarPath = path.join(jarPath, jarFile)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const { groupId, artifactId, version, jarFilename, pomFilename } = resolveMavenArtifactFilenames(
|
|
165
|
+
artifact,
|
|
166
|
+
jarPath,
|
|
167
|
+
undefined,
|
|
168
|
+
'Nexus'
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
const defaultRepo = version.toUpperCase().endsWith('-SNAPSHOT') ? 'maven-snapshots' : 'maven-releases'
|
|
172
|
+
const repoName =
|
|
173
|
+
registry.namespace?.trim() ||
|
|
174
|
+
(registry.options?.repository as string | undefined)?.trim() ||
|
|
175
|
+
((registry as any).metadata?.repository as string | undefined)?.trim() ||
|
|
176
|
+
defaultRepo
|
|
177
|
+
|
|
178
|
+
const jarBuffer = await fs.readFile(jarPath)
|
|
179
|
+
const digest = `sha256:${crypto.createHash('sha256').update(jarBuffer).digest('hex')}`
|
|
180
|
+
|
|
181
|
+
const targetUrl = client.getMavenArtifactUrl(repoName, groupId, artifactId, version, jarFilename)
|
|
182
|
+
this.log(` 📤 Uploading Maven JAR to Nexus: ${client.maskSecrets(targetUrl)}`)
|
|
183
|
+
|
|
184
|
+
if (options?.dryRun) {
|
|
185
|
+
this.log(` [dryRun] Would PUT ${jarFilename} (${jarBuffer.length} bytes) to ${client.maskSecrets(targetUrl)}`)
|
|
186
|
+
return {
|
|
187
|
+
ref: `${groupId}:${artifactId}:${version}`,
|
|
188
|
+
digest,
|
|
189
|
+
metadata: { repoName, targetUrl, size: jarBuffer.length }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// The POM goes up first, deliberately.
|
|
194
|
+
//
|
|
195
|
+
// The existence probe checks for the jar/war, so uploading that last is what
|
|
196
|
+
// makes the probe mean "this artifact is complete". With the artifact first, a
|
|
197
|
+
// run that uploaded it and then failed on the POM would leave a coordinate that
|
|
198
|
+
// probes as present: the retry skips it as already published, and the POM never
|
|
199
|
+
// arrives — a jar in the repository forever, with no dependency information.
|
|
200
|
+
// Upload pom if exists
|
|
201
|
+
const pomPath = mavenPomPathFor(jarPath)
|
|
202
|
+
const pomStat = await fs.stat(pomPath).catch(() => null)
|
|
203
|
+
if (pomStat?.isFile()) {
|
|
204
|
+
const pomBuffer = await fs.readFile(pomPath)
|
|
205
|
+
const pomTargetUrl = client.getMavenArtifactUrl(repoName, groupId, artifactId, version, pomFilename)
|
|
206
|
+
this.log(` 📤 Uploading Maven POM to Nexus: ${client.maskSecrets(pomTargetUrl)}`)
|
|
207
|
+
|
|
208
|
+
const pomRes = await client.request(pomTargetUrl, {
|
|
209
|
+
method: 'PUT',
|
|
210
|
+
headers: { 'Content-Type': 'application/xml' },
|
|
211
|
+
body: pomBuffer as unknown as BodyInit
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
if (!pomRes.ok && pomRes.status !== 201 && pomRes.status !== 200) {
|
|
215
|
+
const pomErr = await client.extractErrorMessage(pomRes)
|
|
216
|
+
throw new Error(`Failed to upload Maven POM to Nexus (${pomRes.status}): ${pomErr}`)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const res = await client.request(targetUrl, {
|
|
221
|
+
method: 'PUT',
|
|
222
|
+
headers: {
|
|
223
|
+
'Content-Type': 'application/java-archive'
|
|
224
|
+
},
|
|
225
|
+
body: jarBuffer as unknown as BodyInit
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
if (!res.ok && res.status !== 201 && res.status !== 200) {
|
|
229
|
+
const errText = await client.extractErrorMessage(res)
|
|
230
|
+
throw new Error(`Failed to upload Maven JAR to Nexus (${res.status}): ${errText}`)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
ref: `${groupId}:${artifactId}:${version}`,
|
|
235
|
+
digest,
|
|
236
|
+
metadata: {
|
|
237
|
+
groupId,
|
|
238
|
+
artifactId,
|
|
239
|
+
version,
|
|
240
|
+
repoName,
|
|
241
|
+
targetUrl,
|
|
242
|
+
size: jarBuffer.length
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { BaseNpmPublisher } from '@ticatec/omniflow-artifact'
|
|
2
|
+
import type { RegistryConfig } from '@ticatec/omniflow-artifact'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sonatype Nexus 3 NPM Package Registry Publisher.
|
|
6
|
+
*
|
|
7
|
+
* Endpoint format: {server}/repository/{repoName}/
|
|
8
|
+
* Typically used with repositories like "npm-hosted".
|
|
9
|
+
*/
|
|
10
|
+
export class NexusNpmPublisher extends BaseNpmPublisher {
|
|
11
|
+
readonly id = 'nexus-npm'
|
|
12
|
+
readonly name = 'Nexus 3 NPM Repository'
|
|
13
|
+
readonly platform = 'nexus'
|
|
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 === 'nexus' || regName.startsWith('nexus-')) {
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const server = (registry.server || '').toLowerCase()
|
|
27
|
+
try {
|
|
28
|
+
const parsed = new URL(server.includes('://') ? server : `https://${server}`)
|
|
29
|
+
const host = parsed.hostname.toLowerCase()
|
|
30
|
+
|
|
31
|
+
// Exclude other known platforms
|
|
32
|
+
if (
|
|
33
|
+
host === 'aliyun.com' ||
|
|
34
|
+
host.endsWith('.aliyun.com') ||
|
|
35
|
+
host.endsWith('.aliyuncs.com') ||
|
|
36
|
+
host === 'gitlab.com' ||
|
|
37
|
+
host.endsWith('.gitlab.com') ||
|
|
38
|
+
host === 'github.com' ||
|
|
39
|
+
host.endsWith('.github.com')
|
|
40
|
+
) {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return host.includes('nexus') || parsed.pathname.includes('/repository/')
|
|
45
|
+
} catch {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override normalizeRegistryUrl(
|
|
51
|
+
server: string,
|
|
52
|
+
_registryOrNamespace?: RegistryConfig | string
|
|
53
|
+
): string {
|
|
54
|
+
let url = server.trim()
|
|
55
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
56
|
+
url = `https://${url}`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (url.includes('/repository/')) {
|
|
60
|
+
return url.endsWith('/') ? url : `${url}/`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let repoName: string | undefined
|
|
64
|
+
if (typeof _registryOrNamespace === 'string') {
|
|
65
|
+
repoName = _registryOrNamespace.trim()
|
|
66
|
+
} else if (_registryOrNamespace && typeof _registryOrNamespace === 'object') {
|
|
67
|
+
const reg = _registryOrNamespace as RegistryConfig & { metadata?: Record<string, unknown> }
|
|
68
|
+
repoName =
|
|
69
|
+
reg.namespace?.trim() ||
|
|
70
|
+
(reg.options?.repository as string | undefined)?.trim() ||
|
|
71
|
+
(reg.options?.repoName as string | undefined)?.trim() ||
|
|
72
|
+
(reg.metadata?.repository as string | undefined)?.trim() ||
|
|
73
|
+
(reg.metadata?.repoName as string | undefined)?.trim()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const cleanBase = url.replace(/\/+$/, '')
|
|
77
|
+
const targetRepo = repoName ? repoName.replace(/^\/+/, '').replace(/\/+$/, '') : 'npm-hosted'
|
|
78
|
+
return `${cleanBase}/repository/${targetRepo}/`
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|