@ticatec/omniflow-github 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 +93 -0
- package/README_CN.md +93 -0
- package/dist/artifacts/GitHubContainerPublisher.d.ts +16 -0
- package/dist/artifacts/GitHubContainerPublisher.d.ts.map +1 -0
- package/dist/artifacts/GitHubContainerPublisher.js +47 -0
- package/dist/artifacts/GitHubContainerPublisher.js.map +1 -0
- package/dist/artifacts/GitHubMavenPublisher.d.ts +25 -0
- package/dist/artifacts/GitHubMavenPublisher.d.ts.map +1 -0
- package/dist/artifacts/GitHubMavenPublisher.js +186 -0
- package/dist/artifacts/GitHubMavenPublisher.js.map +1 -0
- package/dist/artifacts/GitHubNpmPublisher.d.ts +21 -0
- package/dist/artifacts/GitHubNpmPublisher.d.ts.map +1 -0
- package/dist/artifacts/GitHubNpmPublisher.js +49 -0
- package/dist/artifacts/GitHubNpmPublisher.js.map +1 -0
- package/dist/artifacts/GitHubReleasePublisher.d.ts +31 -0
- package/dist/artifacts/GitHubReleasePublisher.d.ts.map +1 -0
- package/dist/artifacts/GitHubReleasePublisher.js +192 -0
- package/dist/artifacts/GitHubReleasePublisher.js.map +1 -0
- package/dist/client/GitHubClient.d.ts +76 -0
- package/dist/client/GitHubClient.d.ts.map +1 -0
- package/dist/client/GitHubClient.js +170 -0
- package/dist/client/GitHubClient.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/package.json +48 -0
- package/src/artifacts/GitHubContainerPublisher.ts +54 -0
- package/src/artifacts/GitHubMavenPublisher.ts +238 -0
- package/src/artifacts/GitHubNpmPublisher.ts +66 -0
- package/src/artifacts/GitHubReleasePublisher.ts +238 -0
- package/src/client/GitHubClient.ts +242 -0
- package/src/index.ts +54 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import {
|
|
2
|
+
maskSecrets,
|
|
3
|
+
safeFetch,
|
|
4
|
+
extractErrorMessage
|
|
5
|
+
} from '@ticatec/omniflow-artifact'
|
|
6
|
+
|
|
7
|
+
export interface GitHubClientConfig {
|
|
8
|
+
baseUrl?: string
|
|
9
|
+
token?: string
|
|
10
|
+
timeoutMs?: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ParsedGitHubRepo {
|
|
14
|
+
host: string
|
|
15
|
+
owner: string
|
|
16
|
+
repo: string
|
|
17
|
+
fullName: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface GitHubReleaseAsset {
|
|
21
|
+
id: number
|
|
22
|
+
name: string
|
|
23
|
+
size?: number
|
|
24
|
+
browser_download_url?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface GitHubRelease {
|
|
28
|
+
id: number
|
|
29
|
+
tag_name: string
|
|
30
|
+
name: string
|
|
31
|
+
upload_url: string
|
|
32
|
+
html_url: string
|
|
33
|
+
assets?: GitHubReleaseAsset[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class GitHubClient {
|
|
37
|
+
readonly baseUrl: string
|
|
38
|
+
readonly token?: string
|
|
39
|
+
readonly timeoutMs: number
|
|
40
|
+
|
|
41
|
+
constructor(config: GitHubClientConfig = {}) {
|
|
42
|
+
let base = (config.baseUrl || 'https://api.github.com').trim().replace(/\/+$/, '')
|
|
43
|
+
if (!/^https?:\/\//i.test(base)) {
|
|
44
|
+
base = `https://${base}`
|
|
45
|
+
}
|
|
46
|
+
this.baseUrl = base
|
|
47
|
+
this.token = config.token
|
|
48
|
+
this.timeoutMs = config.timeoutMs ?? 30000
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get isGitHubCom(): boolean {
|
|
52
|
+
try {
|
|
53
|
+
const parsed = new URL(this.baseUrl)
|
|
54
|
+
return parsed.hostname === 'github.com' || parsed.hostname === 'api.github.com'
|
|
55
|
+
} catch {
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Return sensitive credential strings of length >= 3 for scrubbing.
|
|
62
|
+
*/
|
|
63
|
+
getSecrets(): string[] {
|
|
64
|
+
return [this.token].filter(
|
|
65
|
+
(s): s is string => typeof s === 'string' && s.length >= 3
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
maskSecrets(input: string): string {
|
|
70
|
+
return maskSecrets(input, this.getSecrets())
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async extractErrorMessage(response: Response): Promise<string> {
|
|
74
|
+
return extractErrorMessage(response, this.getSecrets())
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
getAuthHeaders(): Record<string, string> {
|
|
78
|
+
const headers: Record<string, string> = {
|
|
79
|
+
Accept: 'application/vnd.github+json',
|
|
80
|
+
'X-GitHub-Api-Version': '2022-11-28'
|
|
81
|
+
}
|
|
82
|
+
if (this.token) {
|
|
83
|
+
headers['Authorization'] = `Bearer ${this.token}`
|
|
84
|
+
}
|
|
85
|
+
return headers
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getRepoApiUrl(owner: string, repo: string, subpath = ''): string {
|
|
89
|
+
const cleanSubpath = subpath ? (subpath.startsWith('/') ? subpath : `/${subpath}`) : ''
|
|
90
|
+
return `${this.baseUrl}/repos/${owner}/${repo}${cleanSubpath}`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async request(url: string, init: RequestInit = {}): Promise<Response> {
|
|
94
|
+
const headers = {
|
|
95
|
+
...this.getAuthHeaders(),
|
|
96
|
+
...((init.headers as Record<string, string>) || {})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return safeFetch(url, {
|
|
100
|
+
...init,
|
|
101
|
+
headers,
|
|
102
|
+
timeoutMs: this.timeoutMs,
|
|
103
|
+
secrets: this.getSecrets()
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get release by tag. Returns null if not found (404).
|
|
109
|
+
*/
|
|
110
|
+
async getReleaseByTag(owner: string, repo: string, tag: string): Promise<GitHubRelease | null> {
|
|
111
|
+
const url = `${this.getRepoApiUrl(owner, repo)}/releases/tags/${encodeURIComponent(tag)}`
|
|
112
|
+
const res = await this.request(url)
|
|
113
|
+
if (res.status === 404) {
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
if (!res.ok) {
|
|
117
|
+
const errText = await this.extractErrorMessage(res)
|
|
118
|
+
throw new Error(`Failed to query GitHub release for tag "${tag}" (${res.status}): ${errText}`)
|
|
119
|
+
}
|
|
120
|
+
return (await res.json()) as GitHubRelease
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Create a new GitHub Release.
|
|
125
|
+
*/
|
|
126
|
+
async createRelease(
|
|
127
|
+
owner: string,
|
|
128
|
+
repo: string,
|
|
129
|
+
params: {
|
|
130
|
+
tagName: string
|
|
131
|
+
name?: string
|
|
132
|
+
body?: string
|
|
133
|
+
draft?: boolean
|
|
134
|
+
prerelease?: boolean
|
|
135
|
+
}
|
|
136
|
+
): Promise<GitHubRelease> {
|
|
137
|
+
const url = `${this.getRepoApiUrl(owner, repo)}/releases`
|
|
138
|
+
const res = await this.request(url, {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers: {
|
|
141
|
+
'Content-Type': 'application/json'
|
|
142
|
+
},
|
|
143
|
+
body: JSON.stringify({
|
|
144
|
+
tag_name: params.tagName,
|
|
145
|
+
name: params.name || params.tagName,
|
|
146
|
+
body: params.body || '',
|
|
147
|
+
draft: params.draft ?? false,
|
|
148
|
+
prerelease: params.prerelease ?? false
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
if (!res.ok) {
|
|
153
|
+
const err = await this.extractErrorMessage(res)
|
|
154
|
+
throw new Error(`Failed to create GitHub release "${params.tagName}" (${res.status}): ${err}`)
|
|
155
|
+
}
|
|
156
|
+
return (await res.json()) as GitHubRelease
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Upload binary asset to a release.
|
|
161
|
+
*/
|
|
162
|
+
async uploadReleaseAsset(
|
|
163
|
+
uploadUrlTemplate: string,
|
|
164
|
+
fileName: string,
|
|
165
|
+
data: Buffer | Uint8Array,
|
|
166
|
+
contentType = 'application/octet-stream'
|
|
167
|
+
): Promise<{ id: number; name: string; browser_download_url: string }> {
|
|
168
|
+
// uploadUrlTemplate is like: https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets{?name,label}
|
|
169
|
+
const cleanUrl = uploadUrlTemplate.replace(/\{.*?\}$/, '')
|
|
170
|
+
const targetUrl = `${cleanUrl}?name=${encodeURIComponent(fileName)}`
|
|
171
|
+
|
|
172
|
+
const res = await this.request(targetUrl, {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
headers: {
|
|
175
|
+
'Content-Type': contentType,
|
|
176
|
+
'Content-Length': String(data.byteLength)
|
|
177
|
+
},
|
|
178
|
+
body: data as unknown as BodyInit
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
if (!res.ok) {
|
|
182
|
+
const err = await this.extractErrorMessage(res)
|
|
183
|
+
throw new Error(`Failed to upload asset "${fileName}" to GitHub release (${res.status}): ${err}`)
|
|
184
|
+
}
|
|
185
|
+
return (await res.json()) as { id: number; name: string; browser_download_url: string }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Parse GitHub repo URL or slug into host, owner, and repo.
|
|
190
|
+
*/
|
|
191
|
+
static parseRepo(urlOrSlug: string, defaultHost = 'github.com'): ParsedGitHubRepo {
|
|
192
|
+
let clean = urlOrSlug.trim()
|
|
193
|
+
let host = defaultHost
|
|
194
|
+
|
|
195
|
+
if (clean.includes('://')) {
|
|
196
|
+
const parsed = new URL(clean)
|
|
197
|
+
host = parsed.host
|
|
198
|
+
clean = parsed.pathname
|
|
199
|
+
} else if (clean.includes('@') && clean.includes(':')) {
|
|
200
|
+
const parts = clean.split(':')
|
|
201
|
+
host = parts[0].replace(/.*@/, '')
|
|
202
|
+
clean = parts[1]
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
clean = clean.replace(/^\/+/, '').replace(/\.git$/, '')
|
|
206
|
+
const segments = clean.split('/').filter(Boolean)
|
|
207
|
+
if (segments.length < 2) {
|
|
208
|
+
throw new Error(`Invalid GitHub repository path: "${urlOrSlug}". Expected owner/repo format.`)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const owner = segments[0]
|
|
212
|
+
const repo = segments[1]
|
|
213
|
+
const fullName = `${owner}/${repo}`
|
|
214
|
+
|
|
215
|
+
return { host, owner, repo, fullName }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Determine the API base URL from serverUrl and optional contextPath.
|
|
220
|
+
*/
|
|
221
|
+
static resolveApiBase(serverUrl: string, contextPath?: string): string {
|
|
222
|
+
const url = new URL(serverUrl)
|
|
223
|
+
if (url.hostname === 'github.com' || url.hostname === 'www.github.com' || url.hostname === 'api.github.com') {
|
|
224
|
+
return 'https://api.github.com'
|
|
225
|
+
}
|
|
226
|
+
return `${serverUrl.replace(/\/+$/, '')}${contextPath || ''}/api/v3`
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Determine whether a hostname is a genuine GitHub host (avoiding suffix/substring spoofing).
|
|
231
|
+
*/
|
|
232
|
+
static isGitHubHost(hostname: string): boolean {
|
|
233
|
+
const host = hostname.toLowerCase().trim()
|
|
234
|
+
return (
|
|
235
|
+
host === 'github.com' ||
|
|
236
|
+
host.endsWith('.github.com') ||
|
|
237
|
+
host === 'api.github.com' ||
|
|
238
|
+
host === 'ghcr.io' ||
|
|
239
|
+
host.endsWith('.ghcr.io')
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
}
|
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 { GitHubContainerPublisher } from './artifacts/GitHubContainerPublisher.js'
|
|
4
|
+
import { GitHubNpmPublisher } from './artifacts/GitHubNpmPublisher.js'
|
|
5
|
+
import { GitHubMavenPublisher } from './artifacts/GitHubMavenPublisher.js'
|
|
6
|
+
import { GitHubReleasePublisher } from './artifacts/GitHubReleasePublisher.js'
|
|
7
|
+
|
|
8
|
+
export * from './client/GitHubClient.js'
|
|
9
|
+
export * from './artifacts/GitHubContainerPublisher.js'
|
|
10
|
+
export * from './artifacts/GitHubNpmPublisher.js'
|
|
11
|
+
export * from './artifacts/GitHubMavenPublisher.js'
|
|
12
|
+
export * from './artifacts/GitHubReleasePublisher.js'
|
|
13
|
+
|
|
14
|
+
export interface RegisterGitHubOptions {
|
|
15
|
+
artifactRegistry?: ArtifactPublisherRegistry
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Standard OmniFlow Plugin SPI entry point.
|
|
20
|
+
*/
|
|
21
|
+
export function register(ctx: PluginContext): void {
|
|
22
|
+
registerGitHub({ artifactRegistry: ctx.artifactRegistry })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default register
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Register all GitHub artifact publishers into the artifact registry.
|
|
29
|
+
*/
|
|
30
|
+
export function registerGitHub(options: RegisterGitHubOptions = {}): {
|
|
31
|
+
containerPublisher: GitHubContainerPublisher
|
|
32
|
+
npmPublisher: GitHubNpmPublisher
|
|
33
|
+
mavenPublisher: GitHubMavenPublisher
|
|
34
|
+
releasePublisher: GitHubReleasePublisher
|
|
35
|
+
} {
|
|
36
|
+
const containerPublisher = new GitHubContainerPublisher()
|
|
37
|
+
const npmPublisher = new GitHubNpmPublisher()
|
|
38
|
+
const mavenPublisher = new GitHubMavenPublisher()
|
|
39
|
+
const releasePublisher = new GitHubReleasePublisher()
|
|
40
|
+
|
|
41
|
+
if (options.artifactRegistry) {
|
|
42
|
+
options.artifactRegistry.register(containerPublisher)
|
|
43
|
+
options.artifactRegistry.register(npmPublisher)
|
|
44
|
+
options.artifactRegistry.register(mavenPublisher)
|
|
45
|
+
options.artifactRegistry.register(releasePublisher)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
containerPublisher,
|
|
50
|
+
npmPublisher,
|
|
51
|
+
mavenPublisher,
|
|
52
|
+
releasePublisher
|
|
53
|
+
}
|
|
54
|
+
}
|