1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255 |
x1
x4
x4
x4
x4
x4
x4
x4
x4
x4
x4
x4
x1
x4
x4
x10
x10
x50
x10
x10
x10
x30
x10
x10
x10
x10
x10
x71
x71
x71
x684
x76
x71
x73
x73
x71
x506
x81
x71
x115
x115
x116
x116
x115
x146
x146
x115
x71
x71
x355
x71
x10
x10
x100
x10
x50
x10
x11
x66
x11
x15
x60
x10
x99
x11
x11
x11
x99
x99
x99
x11
x11
x55
x11
x11
x88
x99
x77
x11
x11
x15
x15
x60
x15
x15
x15
x15
x10
x11
x66
x11
x11
x15
x15
x105
x105
x105
x15
x135
x135
x15
x120
x135
x15
x15
x15
x15
x125
x25
x30
x30
x30
x75
x15
x15
x15
x15
x265
x53
x87
x87
x87
x75
x14
x10
x15
x15
x16
x96
x16
x16
x15
x15
x16
x128
x144
x16
x16
x15
x15
x16
x128
x16
x16
x144
x160
x16
x16
x15
x84
x10
x4
x4
x5
x5
x5
x5
x20
x21
x7
x21
x7
x8
x8
x8
x8
x7
x5 |
I
|
import { Octokit } from "octokit"
import { Logger } from "@libs/logger"
import { assert } from "@std/assert"
import { command } from "@libs/run/command"
import { retry } from "@std/async/retry"
import * as JSONC from "@std/jsonc"
import { expandGlob } from "@std/fs"
import { resolve } from "@std/path"
import type { record } from "@libs/typing"
import { unmap as _unmap } from "../unmap.ts"
export type { Logger }
export type options = {
logger?: Logger
token: string
repository: string
directory?: string
name: string
version: string
map?: string
reactive?: boolean
remove?: boolean
attempts?: number
delay?: number
dryrun?: boolean
}
export async function publish({ logger: log = new Logger(), token, repository, directory, name, version, map, reactive = false, remove = false, attempts = 30, delay = 30000, dryrun = false }: options): Promise<{ name: string; version: string; url: string; changed: boolean }> {
const [owner, repo] = repository.split("/")
log = log.with({ owner, repo, name })
directory = directory?.trim().replace(/\/$/, "")
const api = `https://api.deno.land/webhook/gh/${name}${directory ? `?subdir=${encodeURIComponent(`/${directory}/`)}` : ""}`
const url = `https://deno.land/x/${name}@${version}`
const request = { fetch }
if (dryrun) {
const requested = new Set<string>()
Object.assign(request, {
async fetch(url: string, { method = "GET" } = {}) {
let data = null
switch (true) {
case (method === "GET") && new URLPattern("https://api.github.com/repos/:owner/:repo/hooks").test(url):
data = [{ id: 1, active: false, config: { url: api } }]
break
case (method === "PATCH") && new URLPattern("https://api.github.com/repos/:owner/:repo/hooks/:id").test(url):
data = {}
break
case (method === "GET") && new URLPattern("https://api.github.com/repos/:owner/:repo/hooks/:id/deliveries").test(url):
data = requested.has(url) ? [{ event: "create", delivered_at: new Date().toISOString() }] : []
break
case (method === "GET") && new URLPattern("https://deno.land/x/:package").test(url.replace(/\?.*$/, "")):
data = requested.has(url) ? "200 - OK" : "404 - Not Found"
if (url.includes("already_published")) {
data = "200 - OK"
}
if (url.includes("failure")) {
data = "404 - Not Found"
}
break
}
requested.add(url)
return new Response(JSON.stringify(data), { headers: { "Content-Type": "application/json" } })
},
})
}
const octokit = new Octokit({ auth: token, request, throttle: { enabled: false }, retry: { enabled: !dryrun } })
const check = await request.fetch(`${url}?check`, { headers: { Accept: "text/html" } }).then((r) => r.text())
if (!check.includes("404 - Not Found")) {
log.warn(`package is already published at ${url}, nothing to do`)
return { name, version, url, changed: false }
}
const branch = { current: "", temporary: "" }
if (map) {
branch.current = (await command("git", ["rev-parse", "--abbrev-ref", "HEAD"], { logger: log, throw: true, dryrun })).stdout.trim()
branch.temporary = `x-${name}-${version}-${Date.now()}`
log.debug(`current branch: ${branch.current}`)
log.debug(`temporary branch: ${branch.temporary}`)
await command("git", ["switch", "--create", branch.temporary], { logger: log, throw: true, dryrun })
await command("git", ["push", "origin", branch.temporary], { logger: log, throw: true, dryrun })
await command("git", ["branch", "--set-upstream-to", `origin/${branch.temporary}`], { logger: log, throw: true, dryrun })
log.info(`on ${branch.temporary}`)
log.debug("resolving imports from map")
await unmap({ log, map, dryrun })
log.ok("imports have been resolved")
log.debug(`pushing changes on temporary branch ${branch.temporary} to origin`)
await command("git", ["add", "."], { logger: log, throw: true, dryrun })
await command("git", ["commit", "--message", `build(${name}): deno.land/x@${version}`], { logger: log, throw: true, dryrun })
await command("git", ["push"], { logger: log, throw: true, dryrun })
log.ok(`pushed changes from temporary branch ${branch.temporary} to origin`)
}
log.debug(`searching for hook: ${api}`)
const { data: webhooks } = await octokit.rest.repos.listWebhooks({ owner, repo })
const hook = webhooks.filter(({ config }: { config: { url?: string } }) => config.url === api)[0]!
assert(hook, `Could not find a hook with expected url: ${api}`)
log.ok(`found hook: ${hook.id}`)
if (reactive && (!hook.active)) {
log.debug("hook is inactive prior publishing, activating")
await octokit.rest.repos.updateWebhook({ owner, repo, hook_id: hook.id, active: true })
log.ok("hook is now active")
}
try {
const { stdout: object } = await command("git", ["rev-parse", "HEAD"], { throw: true, dryrun })
const { stdout: email } = await command("git", ["config", "user.email"], { throw: true, dryrun })
const { stdout: author } = await command("git", ["config", "user.name"], { throw: true, dryrun })
log.info(`creating tag ${version} on commit ${object} by ${author} <${email}>`)
await command("git", ["tag", "--force", version], { logger: log, throw: true, dryrun })
await command("git", ["show-ref", "--tags", version], { logger: log, throw: true, dryrun })
log.info(`pushing tag ${version} to origin`)
await command("git", ["pull", "--rebase"], { logger: log, throw: true, dryrun })
await command("git", ["push", "origin", version], { logger: log, throw: true, dryrun })
log.ok(`tag ${version} has been pushed to origin`)
log.debug("waiting for webhook payload delivery")
await retry(async () => {
const { data: deliveries } = await octokit.rest.repos.listWebhookDeliveries({ owner, repo, hook_id: hook.id })
if (!deliveries.some(({ event, delivered_at }: { event: string; delivered_at: string }) => (event === "create") && (new Date().getTime() >= new Date(delivered_at).getTime()))) {
log.wdebug("webhook payload has not been delivered yet")
throw new Error("Webhook payload has not been delivered in the expected time frame")
}
}, { minTimeout: delay, maxTimeout: delay, maxAttempts: attempts })
log.ok("webhook payload has been delivered")
log.debug("waiting for deno.land/x publishing")
await retry(async () => {
const text = await request.fetch(url, { headers: { Accept: "text/html" } }).then((r) => r.text())
if (text.includes("404 - Not Found")) {
log.wdebug("package has not been published yet")
throw new Error("Package has not been published in the expected time frame")
}
}, { minTimeout: delay, maxTimeout: delay, maxAttempts: attempts })
log.ok("published on deno.land/x")
} finally {
if (reactive && (!hook.active)) {
log.debug("hook was inactive prior publishing, restoring state")
await octokit.rest.repos.updateWebhook({ owner, repo, hook_id: hook.id, active: false })
log.ok("hook is now inactive")
}
if (remove) {
log.debug(`removing tag ${version} locally and from origin`)
await command("git", ["tag", "--delete", version], { logger: log, dryrun })
await command("git", ["push", "--delete", "origin", version], { logger: log, dryrun })
log.ok(`tag ${version} has been removed`)
}
if (map) {
log.debug(`switching back to ${branch.current}`)
await command("git", ["switch", branch.current], { logger: log, throw: true, dryrun })
log.info(`back on ${branch.current}`)
log.debug(`deleting temporary branch ${branch.temporary}`)
await command("git", ["branch", "--delete", branch.temporary], { logger: log, throw: true, dryrun })
await command("git", ["push", "origin", "--delete", branch.temporary], { logger: log, throw: true, dryrun })
log.ok(`deleted temporary branch ${branch.temporary}`)
}
}
return { name, version, url, changed: true }
}
async function unmap({ log: logger, map, exclude = [], dryrun }: { log: Logger; map: string; directory?: string; exclude?: string[]; dryrun?: boolean }) {
const root = resolve(".").replaceAll("\\", "/")
const { imports } = JSONC.parse(await Deno.readTextFile(resolve(root, map))) as record<record<string>>
exclude.push("node_modules")
logger.log(`processing files in ${root}`)
for await (const { path } of expandGlob("**/*.ts", { root, exclude })) {
const log = logger.with({ path: path.replaceAll("\\", "/").replace(`${root}/`, "") }).debug("found")
const content = await Deno.readTextFile(path)
const { result, resolved } = _unmap(content, imports, { logger: log })
if (resolved) {
log.debug(`resolved ${resolved} imports`)
} else {
log.wdebug("no imports to resolve")
}
if (!dryrun) {
await Deno.writeTextFile(path, result)
log.debug("file updated")
}
}
}
|