@uknows/setup 0.1.0 → 0.2.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/README.md +99 -32
- package/lib/automate.mjs +1299 -0
- package/lib/check.mjs +149 -33
- package/lib/ci.mjs +21 -0
- package/lib/cli.mjs +114 -29
- package/lib/cloudid.mjs +456 -0
- package/lib/code.mjs +147 -0
- package/lib/context.mjs +126 -0
- package/lib/contract-docs.mjs +1780 -198
- package/lib/contract.mjs +93 -10
- package/lib/detect.mjs +96 -44
- package/lib/dockerfile.mjs +83 -0
- package/lib/endpoint.mjs +36 -5
- package/lib/engine.mjs +1272 -126
- package/lib/existing.mjs +448 -54
- package/lib/git.mjs +53 -0
- package/lib/hooks/UknowsOpenTelemetry.cs +118 -0
- package/lib/hooks/pins.mjs +28 -0
- package/lib/hooks/uknows-otel.cjs +82 -0
- package/lib/hooks/uknows_otel.py +179 -0
- package/lib/hosts.mjs +521 -16
- package/lib/locks.mjs +353 -0
- package/lib/manifest.mjs +27 -7
- package/lib/members.mjs +269 -0
- package/lib/plan.mjs +130 -0
- package/lib/settings.mjs +216 -28
- package/lib/signin.mjs +259 -0
- package/lib/stacks/azure.mjs +345 -0
- package/lib/stacks/channel.mjs +125 -0
- package/lib/stacks/dotnet.mjs +336 -29
- package/lib/stacks/functions-hooks.mjs +228 -0
- package/lib/stacks/functions.mjs +148 -0
- package/lib/stacks/guided.mjs +153 -30
- package/lib/stacks/index.mjs +10 -2
- package/lib/stacks/java.mjs +14 -13
- package/lib/stacks/node.mjs +142 -19
- package/lib/stacks/otel.mjs +56 -4
- package/lib/stacks/python-bootstrap.mjs +27 -7
- package/lib/stacks/python-lock.mjs +105 -0
- package/lib/stacks/python-sets.mjs +143 -0
- package/lib/stacks/python.mjs +261 -63
- package/lib/toml.mjs +64 -0
- package/lib/types.mjs +52 -5
- package/lib/ui.mjs +139 -20
- package/lib/updates.mjs +398 -0
- package/lib/versions.mjs +115 -0
- package/lib/workspace.mjs +4 -1
- package/package.json +3 -8
package/lib/context.mjs
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The build context of a Dockerfile, read from the repo: which folder `docker build` sends, where the app is in
|
|
3
|
+
* the image (the WORKDIR of the final stage), and whether .dockerignore leaves a file out. Static reading only.
|
|
4
|
+
* A wrong guess makes the build fail at COPY, so every function returns null when it is not sure.
|
|
5
|
+
*/
|
|
6
|
+
import { posix } from 'node:path'
|
|
7
|
+
import { copySources } from './detect.mjs'
|
|
8
|
+
|
|
9
|
+
const GLOB = /[*?[]/
|
|
10
|
+
const dirOf = (path) => posix.dirname(path)
|
|
11
|
+
const at = (dir, name) => (dir === '.' ? name : `${dir}/${name}`)
|
|
12
|
+
const unique = (list) => [...new Set(list)]
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The folder that `docker build` sends for this Dockerfile: the Dockerfile folder, the service folder or the repo
|
|
16
|
+
* root. The COPY and ADD sources decide: a folder fits when every source that exists in any of these folders exists
|
|
17
|
+
* in it. Sources that exist nowhere (built before docker build runs) do not count. null unless exactly 1 folder fits.
|
|
18
|
+
* @param {Pick<import('./workspace.mjs').RepoView, 'exists'|'list'>} view
|
|
19
|
+
* @param {string} dockerfile Repo path of the Dockerfile.
|
|
20
|
+
* @param {import('./dockerfile.mjs').DockerfileInfo} info
|
|
21
|
+
* @param {string} serviceDir
|
|
22
|
+
* @returns {string|null}
|
|
23
|
+
*/
|
|
24
|
+
export function buildContext(view, dockerfile, info, serviceDir) {
|
|
25
|
+
const candidates = unique([dirOf(dockerfile), serviceDir, '.'])
|
|
26
|
+
const sources = copySources(info).map((src) => src.replace(/^\/+/, '')).filter(Boolean)
|
|
27
|
+
const found = (dir, src) => present(view, dir, src)
|
|
28
|
+
const counted = sources.filter((src) => candidates.some((dir) => found(dir, src)))
|
|
29
|
+
const fits = candidates.filter((dir) => counted.every((src) => found(dir, src)))
|
|
30
|
+
return fits.length === 1 ? fits[0] : null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** True when src exists in dir. A pattern (package*.json) counts when a name in its folder matches it. */
|
|
34
|
+
function present(view, dir, src) {
|
|
35
|
+
const path = posix.normalize(at(dir, src))
|
|
36
|
+
if (path.startsWith('../') || path === '..') return false
|
|
37
|
+
if (!GLOB.test(path)) return view.exists(path)
|
|
38
|
+
const folder = dirOf(path)
|
|
39
|
+
if (GLOB.test(folder)) return false
|
|
40
|
+
const re = globRegex(posix.basename(path))
|
|
41
|
+
return view.list(folder).some((name) => re.test(name))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The WORKDIR of the final stage, with the stages it builds on (FROM <stage>). null when it is not set in the
|
|
46
|
+
* Dockerfile, uses a variable, or is relative to a WORKDIR of the base image.
|
|
47
|
+
* @param {import('./dockerfile.mjs').DockerfileInfo} info
|
|
48
|
+
* @returns {string|null}
|
|
49
|
+
*/
|
|
50
|
+
export function finalWorkdir(info) {
|
|
51
|
+
const chain = []
|
|
52
|
+
for (let i = info.stages.length - 1, hops = 0; i >= 0 && hops < 20; hops++) {
|
|
53
|
+
chain.unshift(info.stages[i])
|
|
54
|
+
const base = info.stages[i].image.toLowerCase()
|
|
55
|
+
i = info.stages.findLastIndex((s, j) => j < i && s.name?.toLowerCase() === base)
|
|
56
|
+
}
|
|
57
|
+
let dir = null
|
|
58
|
+
for (const x of chain.flatMap((stage) => stage.instructions).filter((y) => y.keyword === 'WORKDIR')) {
|
|
59
|
+
const value = x.args.trim().replace(/^(["'])(.*)\1$/, '$2')
|
|
60
|
+
if (!value || /[$\s]/.test(value)) return null
|
|
61
|
+
dir = value.startsWith('/') ? value : dir === null ? null : posix.join(dir, value)
|
|
62
|
+
}
|
|
63
|
+
return dir === null ? null : posix.normalize(dir).replace(/(?<=.)\/+$/, '')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The ignore file that leaves path (relative to the context) out of the build context, or null when none does.
|
|
68
|
+
* BuildKit reads <Dockerfile>.dockerignore next to the Dockerfile first. The legacy builder reads only
|
|
69
|
+
* .dockerignore in the context. Both count, so the file reaches the image with either builder.
|
|
70
|
+
* @param {Pick<import('./workspace.mjs').RepoView, 'read'>} view
|
|
71
|
+
* @param {string} context
|
|
72
|
+
* @param {string} dockerfile
|
|
73
|
+
* @param {string} path
|
|
74
|
+
* @returns {string|null}
|
|
75
|
+
*/
|
|
76
|
+
export function ignoredBy(view, context, dockerfile, path) {
|
|
77
|
+
for (const file of unique([`${dockerfile}.dockerignore`, at(context, '.dockerignore')])) {
|
|
78
|
+
let text = null
|
|
79
|
+
try { text = view.read(file) } catch { /* not readable */ }
|
|
80
|
+
if (text !== null && dockerIgnored(path, text)) return file
|
|
81
|
+
}
|
|
82
|
+
return null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* True when the .dockerignore text leaves path out, as Docker reads it: 1 pattern per line, # comments, ! for an
|
|
87
|
+
* exception, ** for any number of folders. A pattern that matches a folder leaves out all files in it. The last
|
|
88
|
+
* pattern that matches wins.
|
|
89
|
+
* @param {string} path Relative to the context, with '/'.
|
|
90
|
+
* @param {string} text
|
|
91
|
+
*/
|
|
92
|
+
export function dockerIgnored(path, text) {
|
|
93
|
+
const parts = path.split('/')
|
|
94
|
+
let ignored = false
|
|
95
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
96
|
+
const line = raw.trim()
|
|
97
|
+
if (!line || line.startsWith('#')) continue
|
|
98
|
+
const exception = line.startsWith('!')
|
|
99
|
+
const pattern = posix.normalize((exception ? line.slice(1) : line).trim().replace(/^\/+/, '')).replace(/\/+$/, '')
|
|
100
|
+
if (!pattern) continue
|
|
101
|
+
const re = pattern === '.' ? /^/ : globRegex(pattern)
|
|
102
|
+
if (parts.some((_, i) => re.test(parts.slice(0, i + 1).join('/')))) ignored = !exception
|
|
103
|
+
}
|
|
104
|
+
return ignored
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** A Docker path pattern as a regex: * and ? stay in 1 folder, ** crosses folders, [..] is a class. */
|
|
108
|
+
function globRegex(pattern) {
|
|
109
|
+
let out = ''
|
|
110
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
111
|
+
const ch = pattern[i]
|
|
112
|
+
if (ch === '*' && pattern[i + 1] === '*') {
|
|
113
|
+
const slash = pattern[i + 2] === '/'
|
|
114
|
+
out += slash ? '(?:.*/)?' : '.*'
|
|
115
|
+
i += slash ? 2 : 1
|
|
116
|
+
} else if (ch === '*') out += '[^/]*'
|
|
117
|
+
else if (ch === '?') out += '[^/]'
|
|
118
|
+
else if (ch === '[') {
|
|
119
|
+
const end = pattern.indexOf(']', i + 1)
|
|
120
|
+
if (end < 0) { out += '\\['; continue }
|
|
121
|
+
out += `[${pattern.slice(i + 1, end).replace(/^!/, '^').replace(/\\/g, '\\\\')}]`
|
|
122
|
+
i = end
|
|
123
|
+
} else out += ch.replace(/[.+^${}()|\\]/g, '\\$&')
|
|
124
|
+
}
|
|
125
|
+
return new RegExp(`^${out}$`)
|
|
126
|
+
}
|