@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.
Files changed (34) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +93 -0
  3. package/README_CN.md +93 -0
  4. package/dist/artifacts/GitHubContainerPublisher.d.ts +16 -0
  5. package/dist/artifacts/GitHubContainerPublisher.d.ts.map +1 -0
  6. package/dist/artifacts/GitHubContainerPublisher.js +47 -0
  7. package/dist/artifacts/GitHubContainerPublisher.js.map +1 -0
  8. package/dist/artifacts/GitHubMavenPublisher.d.ts +25 -0
  9. package/dist/artifacts/GitHubMavenPublisher.d.ts.map +1 -0
  10. package/dist/artifacts/GitHubMavenPublisher.js +186 -0
  11. package/dist/artifacts/GitHubMavenPublisher.js.map +1 -0
  12. package/dist/artifacts/GitHubNpmPublisher.d.ts +21 -0
  13. package/dist/artifacts/GitHubNpmPublisher.d.ts.map +1 -0
  14. package/dist/artifacts/GitHubNpmPublisher.js +49 -0
  15. package/dist/artifacts/GitHubNpmPublisher.js.map +1 -0
  16. package/dist/artifacts/GitHubReleasePublisher.d.ts +31 -0
  17. package/dist/artifacts/GitHubReleasePublisher.d.ts.map +1 -0
  18. package/dist/artifacts/GitHubReleasePublisher.js +192 -0
  19. package/dist/artifacts/GitHubReleasePublisher.js.map +1 -0
  20. package/dist/client/GitHubClient.d.ts +76 -0
  21. package/dist/client/GitHubClient.d.ts.map +1 -0
  22. package/dist/client/GitHubClient.js +170 -0
  23. package/dist/client/GitHubClient.js.map +1 -0
  24. package/dist/index.d.ts +29 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +38 -0
  27. package/dist/index.js.map +1 -0
  28. package/package.json +48 -0
  29. package/src/artifacts/GitHubContainerPublisher.ts +54 -0
  30. package/src/artifacts/GitHubMavenPublisher.ts +238 -0
  31. package/src/artifacts/GitHubNpmPublisher.ts +66 -0
  32. package/src/artifacts/GitHubReleasePublisher.ts +238 -0
  33. package/src/client/GitHubClient.ts +242 -0
  34. 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
+ }