@stll/oxlint-config 0.12.1 → 0.14.1
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 +303 -6
- package/dist/cloud-setup-cli.cjs +6 -4
- package/dist/cloud-setup-cli.cjs.map +1 -1
- package/dist/cloud-setup-cli.mjs +3 -1
- package/dist/cloud-setup-cli.mjs.map +1 -1
- package/dist/compiler-packages.cjs +28 -0
- package/dist/compiler-packages.cjs.map +1 -0
- package/dist/compiler-packages.mjs +17 -0
- package/dist/compiler-packages.mjs.map +1 -0
- package/dist/consumer-compat-cli.cjs +1091 -0
- package/dist/consumer-compat-cli.cjs.map +1 -0
- package/dist/consumer-compat-cli.d.cts +1 -0
- package/dist/consumer-compat-cli.d.mts +1 -0
- package/dist/consumer-compat-cli.mjs +1089 -0
- package/dist/consumer-compat-cli.mjs.map +1 -0
- package/dist/publish-build-target-nuxt-helper.cjs +71 -0
- package/dist/publish-build-target-nuxt-helper.cjs.map +1 -0
- package/dist/publish-build-target-nuxt-helper.d.cts +10 -0
- package/dist/publish-build-target-nuxt-helper.d.mts +10 -0
- package/dist/publish-build-target-nuxt-helper.mjs +68 -0
- package/dist/publish-build-target-nuxt-helper.mjs.map +1 -0
- package/dist/publish-build-target-vite-dts-helper.cjs +3 -0
- package/dist/publish-build-target-vite-dts-helper.d.cts +24 -0
- package/dist/publish-build-target-vite-dts-helper.d.mts +24 -0
- package/dist/publish-build-target-vite-dts-helper.mjs +2 -0
- package/dist/publish-build-target-vite-dts-helper2.cjs +372 -0
- package/dist/publish-build-target-vite-dts-helper2.cjs.map +1 -0
- package/dist/publish-build-target-vite-dts-helper2.mjs +359 -0
- package/dist/publish-build-target-vite-dts-helper2.mjs.map +1 -0
- package/dist/publish-contract-cli.cjs +983 -0
- package/dist/publish-contract-cli.cjs.map +1 -0
- package/dist/publish-contract-cli.d.cts +1 -0
- package/dist/publish-contract-cli.d.mts +1 -0
- package/dist/publish-contract-cli.mjs +981 -0
- package/dist/publish-contract-cli.mjs.map +1 -0
- package/dist/publish-contract.cjs +431 -0
- package/dist/publish-contract.cjs.map +1 -0
- package/dist/publish-contract.mjs +396 -0
- package/dist/publish-contract.mjs.map +1 -0
- package/dist/rolldown-runtime.cjs +28 -0
- package/dist/toolchain-changed-cli.cjs +34 -0
- package/dist/toolchain-changed-cli.cjs.map +1 -0
- package/dist/toolchain-changed-cli.d.cts +1 -0
- package/dist/toolchain-changed-cli.d.mts +1 -0
- package/dist/toolchain-changed-cli.mjs +34 -0
- package/dist/toolchain-changed-cli.mjs.map +1 -0
- package/dist/toolchain-changed.cjs +1470 -0
- package/dist/toolchain-changed.cjs.map +1 -0
- package/dist/toolchain-changed.mjs +1463 -0
- package/dist/toolchain-changed.mjs.map +1 -0
- package/dist/toolchain-check-cli.cjs +2 -1
- package/dist/toolchain-check-cli.cjs.map +1 -1
- package/dist/toolchain-check-cli.mjs +2 -1
- package/dist/toolchain-check-cli.mjs.map +1 -1
- package/dist/toolchain-guard.cjs +551 -1166
- package/dist/toolchain-guard.cjs.map +1 -1
- package/dist/toolchain-guard.mjs +529 -1105
- package/dist/toolchain-guard.mjs.map +1 -1
- package/dist/toolchain-inputs.cjs +926 -0
- package/dist/toolchain-inputs.cjs.map +1 -0
- package/dist/toolchain-inputs.mjs +816 -0
- package/dist/toolchain-inputs.mjs.map +1 -0
- package/dist/toolchain-schema.cjs +702 -0
- package/dist/toolchain-schema.cjs.map +1 -0
- package/dist/toolchain-schema.mjs +580 -0
- package/dist/toolchain-schema.mjs.map +1 -0
- package/dist/typecheck-parity-cli.cjs +38 -898
- package/dist/typecheck-parity-cli.cjs.map +1 -1
- package/dist/typecheck-parity-cli.mjs +39 -899
- package/dist/typecheck-parity-cli.mjs.map +1 -1
- package/dist/typecheck-parity.cjs +905 -0
- package/dist/typecheck-parity.cjs.map +1 -0
- package/dist/typecheck-parity.mjs +894 -0
- package/dist/typecheck-parity.mjs.map +1 -0
- package/dist/typecheck-probe-cli.cjs +176 -0
- package/dist/typecheck-probe-cli.cjs.map +1 -0
- package/dist/typecheck-probe-cli.d.cts +1 -0
- package/dist/typecheck-probe-cli.d.mts +1 -0
- package/dist/typecheck-probe-cli.mjs +174 -0
- package/dist/typecheck-probe-cli.mjs.map +1 -0
- package/package.json +43 -5
- package/toolchain.json +9 -1
package/dist/toolchain-guard.mjs
CHANGED
|
@@ -1,758 +1,238 @@
|
|
|
1
|
+
import { _ as nodeSupportRangeMatches, a as isPythonDependencyManifest, c as toolchainInputKind, d as isKubernetesDefinitionPath, f as workspaceContains, g as nodeSelectorMatches, i as isMiseConfigPath, l as containerDocumentImages, m as generateCloudSetup, n as isDependabotGithubActionsPath, o as javascriptDependencyLockfiles, p as cloudSetupPath, r as isDockerDefinitionPath, s as pythonDependencyManifestKind, t as githubAutomationFileKind, u as isComposeDefinitionPath, v as parseCloudSetup } from "./toolchain-inputs.mjs";
|
|
2
|
+
import { _ as parseConsumerFixtures, d as consumerPackageClosure, g as discoverConsumerPackages, i as assertConsumerFixtureKind, n as assertConsumerNodeSupport, o as assertConsumerFixtureSelection } from "./toolchain-schema.mjs";
|
|
1
3
|
import { readFileSync, realpathSync } from "node:fs";
|
|
2
4
|
import { execFileSync } from "node:child_process";
|
|
3
5
|
import path from "node:path";
|
|
4
|
-
import { minVersion, satisfies,
|
|
5
|
-
import picomatch from "picomatch";
|
|
6
|
-
import { parse } from "smol-toml";
|
|
6
|
+
import { minVersion, satisfies, valid, validRange } from "semver";
|
|
7
7
|
import { LineCounter, isAlias, isMap, isNode, isScalar, isSeq, parseAllDocuments, parseDocument } from "yaml";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
# Prevent package maintainer scripts from starting unmanaged system services.
|
|
11
|
-
apt_install() (
|
|
12
|
-
# EXIT runs after function locals expire; this subshell owns the trap state.
|
|
13
|
-
backup=''
|
|
14
|
-
policy_state=untouched
|
|
15
|
-
[[ ! -L /var/lock/stll-cloud-apt.lock ]] || fail 'Invalid package install lock'
|
|
16
|
-
exec 8>/var/lock/stll-cloud-apt.lock
|
|
17
|
-
flock -x -w 120 8 || fail 'Another package installation is active'
|
|
18
|
-
[[ ! -L /usr/sbin/policy-rc.d ]] || fail 'Unsupported service-start policy symlink'
|
|
19
|
-
[[ ! -e /usr/sbin/policy-rc.d || -f /usr/sbin/policy-rc.d ]] || fail 'Unsupported service-start policy file'
|
|
20
|
-
backup="$(mktemp -d)"
|
|
21
|
-
cleanup_package_policy() {
|
|
22
|
-
local status=$?
|
|
23
|
-
trap - EXIT HUP INT TERM
|
|
24
|
-
if [[ "$policy_state" == installed ]]; then
|
|
25
|
-
if [[ -f "$backup/original" ]]; then
|
|
26
|
-
if ! cp -p "$backup/original" /usr/sbin/policy-rc.d; then status=1; fi
|
|
27
|
-
else
|
|
28
|
-
if ! rm -f /usr/sbin/policy-rc.d; then status=1; fi
|
|
29
|
-
fi
|
|
30
|
-
fi
|
|
31
|
-
rm -rf -- "$backup"
|
|
32
|
-
exit "$status"
|
|
33
|
-
}
|
|
34
|
-
trap cleanup_package_policy EXIT
|
|
35
|
-
trap 'exit 129' HUP
|
|
36
|
-
trap 'exit 130' INT
|
|
37
|
-
trap 'exit 143' TERM
|
|
38
|
-
if [[ -e /usr/sbin/policy-rc.d ]]; then cp -p /usr/sbin/policy-rc.d "$backup/original"; fi
|
|
39
|
-
policy_state=installed
|
|
40
|
-
printf '%s\n' '#!/bin/sh' 'exit 101' > /usr/sbin/policy-rc.d
|
|
41
|
-
chmod 755 /usr/sbin/policy-rc.d
|
|
42
|
-
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "$@"
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
`;
|
|
46
|
-
const postgresSourceLine = "deb [signed-by=/usr/share/keyrings/stll-pgdg.asc] https://apt.postgresql.org/pub/repos/apt noble-pgdg main";
|
|
47
|
-
const postgresSigningFingerprint = "B97B0AFCAA1A47F044F244A07FCC7D46ACCC4CF8";
|
|
48
|
-
const postgresSources = String.raw`
|
|
49
|
-
pgdg_key_fingerprint() {
|
|
50
|
-
gpg --batch --show-keys --with-colons "$1" | awk -F: '$1 == "pub" { primary = 1 } $1 == "sub" { primary = 0 } $1 == "fpr" && primary { print toupper($10); primary = 0 }'
|
|
51
|
-
}
|
|
52
|
-
pgdg_sources_ready() {
|
|
53
|
-
local managed="$1" file mode records suite keyring override fingerprint selected_keyring=''
|
|
54
|
-
shift
|
|
55
|
-
PGDG_SOURCE_STATE=missing
|
|
56
|
-
[[ ! -L "$managed" ]] || fail "Managed PGDG source must not be a symlink: $managed"
|
|
57
|
-
for file in "$@"; do
|
|
58
|
-
[[ -e "$file" ]] || continue
|
|
59
|
-
[[ -f "$file" ]] || fail "APT source must be a regular file: $file"
|
|
60
|
-
case "$file" in *.sources) mode=sources ;; *) mode=list ;; esac
|
|
61
|
-
records="$(awk -v mode="$mode" '
|
|
62
|
-
function trim(value) { gsub(/^[[:space:]]+|[[:space:]]+$/, "", value); return value }
|
|
63
|
-
function pgdg_uri(uri) {
|
|
64
|
-
uri = tolower(uri)
|
|
65
|
-
if (uri !~ /^[a-z][a-z0-9+.-]*:\/\//) return 0
|
|
66
|
-
sub(/^[a-z][a-z0-9+.-]*:\/\//, "", uri)
|
|
67
|
-
sub(/[\/?#].*$/, "", uri)
|
|
68
|
-
sub(/^.*@/, "", uri)
|
|
69
|
-
sub(/:.*$/, "", uri)
|
|
70
|
-
return uri == "apt.postgresql.org" || uri == "apt.postgresql.org."
|
|
71
|
-
}
|
|
72
|
-
function contains_pgdg(value, tokens, count, token_index) {
|
|
73
|
-
count = split(value, tokens, /[[:space:]]+/)
|
|
74
|
-
for (token_index = 1; token_index <= count; token_index++) if (pgdg_uri(tokens[token_index])) return 1
|
|
75
|
-
return 0
|
|
76
|
-
}
|
|
77
|
-
function unsafe_field(name) {
|
|
78
|
-
return name ~ /^(trusted|allow-insecure|allow-weak|allow-downgrade-to-insecure)$/
|
|
79
|
-
}
|
|
80
|
-
function disabled_override(value) {
|
|
81
|
-
value = tolower(trim(value))
|
|
82
|
-
gsub(/^["\047]|["\047]$/, "", value)
|
|
83
|
-
return value ~ /^(no|false|without|off|disable|0)$/
|
|
84
|
-
}
|
|
85
|
-
function stanza(override_index) {
|
|
86
|
-
override = 0
|
|
87
|
-
for (override_index = 1; override_index <= override_count; override_index++) {
|
|
88
|
-
if (!disabled_override(override_values[override_index])) override = 1
|
|
89
|
-
delete override_values[override_index]
|
|
90
|
-
}
|
|
91
|
-
if (tolower(trim(enabled)) != "no" && pgdg) print (trim(suites) == "" ? "-" : trim(suites)) "\t" (trim(keyring) == "" ? "-" : trim(keyring)) "\t" (override ? "unsafe" : "safe")
|
|
92
|
-
enabled = ""; pgdg = 0; field = ""; suites = ""; keyring = ""; override = 0; override_count = 0
|
|
93
|
-
}
|
|
94
|
-
/^[[:space:]]*#/ { if (mode == "list" || $0 ~ /^#/) next }
|
|
95
|
-
{
|
|
96
|
-
line = $0
|
|
97
|
-
if (mode == "list") {
|
|
98
|
-
sub(/[[:space:]]*#.*$/, "", line)
|
|
99
|
-
if (line !~ /^[[:space:]]*deb(-src)?[[:space:]]/ || !contains_pgdg(line)) next
|
|
100
|
-
keyring = ""; override = 0
|
|
101
|
-
if (match(line, /\[[^]]*\]/)) {
|
|
102
|
-
options = substr(line, RSTART + 1, RLENGTH - 2)
|
|
103
|
-
count = split(options, tokens, /[[:space:]]+/)
|
|
104
|
-
for (token_index = 1; token_index <= count; token_index++) {
|
|
105
|
-
if (tolower(tokens[token_index]) ~ /^signed-by=/) { keyring = tokens[token_index]; sub(/^[^=]*=/, "", keyring) }
|
|
106
|
-
option_name = tolower(tokens[token_index]); sub(/=.*/, "", option_name)
|
|
107
|
-
option_value = tokens[token_index]; sub(/^[^=]*=/, "", option_value)
|
|
108
|
-
if (unsafe_field(option_name) && !disabled_override(option_value)) override = 1
|
|
109
|
-
}
|
|
110
|
-
sub(/\[[^]]*\]/, "", line)
|
|
111
|
-
}
|
|
112
|
-
count = split(line, tokens, /[[:space:]]+/)
|
|
113
|
-
for (token_index = 1; token_index <= count; token_index++) if (pgdg_uri(tokens[token_index])) print (tokens[token_index + 1] == "" ? "-" : tokens[token_index + 1]) "\t" (keyring == "" ? "-" : keyring) "\t" (override ? "unsafe" : "safe")
|
|
114
|
-
next
|
|
115
|
-
}
|
|
116
|
-
if (line ~ /^[[:space:]]*$/) { stanza(); next }
|
|
117
|
-
if (line ~ /^[[:space:]]/) {
|
|
118
|
-
if (field == "uris" && contains_pgdg(line)) pgdg = 1
|
|
119
|
-
if (field == "enabled") enabled = enabled " " line
|
|
120
|
-
if (field == "suites") suites = suites " " line
|
|
121
|
-
if (field == "signed-by") keyring = keyring " " line
|
|
122
|
-
if (unsafe_field(field)) override_values[override_count] = override_values[override_count] " " line
|
|
123
|
-
next
|
|
124
|
-
}
|
|
125
|
-
field = tolower(line)
|
|
126
|
-
sub(/:.*/, "", field)
|
|
127
|
-
value = line
|
|
128
|
-
sub(/^[^:]*:[[:space:]]*/, "", value)
|
|
129
|
-
if (field == "uris" && contains_pgdg(value)) pgdg = 1
|
|
130
|
-
if (field == "enabled") enabled = value
|
|
131
|
-
if (field == "suites") suites = value
|
|
132
|
-
if (field == "signed-by") keyring = value
|
|
133
|
-
if (unsafe_field(field)) override_values[++override_count] = value
|
|
134
|
-
}
|
|
135
|
-
END { if (mode == "sources") stanza() }
|
|
136
|
-
' "$file")" || fail "Cannot inspect APT source: $file"
|
|
137
|
-
if [[ "$file" == "$managed" && -z "$records" ]]; then
|
|
138
|
-
fail "Managed PGDG source has no active PGDG entry: $file"
|
|
139
|
-
fi
|
|
140
|
-
while IFS=$'\t' read -r suite keyring override; do
|
|
141
|
-
[[ -n "$suite" ]] || continue
|
|
142
|
-
[[ "$override" == safe ]] || fail "PGDG source enables an unsafe trust override: $file"
|
|
143
|
-
[[ "$suite" == noble-pgdg && "$keyring" == /* && ! "$keyring" =~ [[:space:],] ]] || fail "PGDG source requires noble-pgdg and one Signed-By keyring: $file"
|
|
144
|
-
[[ -z "$selected_keyring" || "$selected_keyring" == "$keyring" ]] || fail "Conflicting PGDG Signed-By keyrings: $file"
|
|
145
|
-
fingerprint="$(pgdg_key_fingerprint "$keyring")" || fail "Cannot verify PGDG Signed-By keyring: $file"
|
|
146
|
-
[[ "$fingerprint" == @PGDG_FINGERPRINT@ ]] || fail "PGDG Signed-By fingerprint mismatch: $file"
|
|
147
|
-
selected_keyring="$keyring"
|
|
148
|
-
PGDG_SOURCE_STATE=reuse
|
|
149
|
-
done <<< "$records"
|
|
150
|
-
done
|
|
151
|
-
}
|
|
152
|
-
`;
|
|
153
|
-
const postgresInstall = String.raw`
|
|
154
|
-
postgres_ready() {
|
|
155
|
-
local binary
|
|
156
|
-
for binary in postgres initdb pg_ctl psql createdb; do
|
|
157
|
-
[[ -x "/usr/lib/postgresql/@POSTGRES@/bin/$binary" ]] || return 1
|
|
158
|
-
done
|
|
159
|
-
/usr/lib/postgresql/@POSTGRES@/bin/postgres --version | grep -Eq '^postgres \(PostgreSQL\) @POSTGRES@\.'
|
|
160
|
-
}
|
|
161
|
-
if ! postgres_ready; then
|
|
162
|
-
pgdg_sources_ready /etc/apt/sources.list.d/stll-pgdg.list /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources
|
|
163
|
-
if [[ "$PGDG_SOURCE_STATE" == missing ]]; then
|
|
164
|
-
local key key_fingerprint
|
|
165
|
-
key="$(mktemp)"
|
|
166
|
-
curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' --tlsv1.2 https://www.postgresql.org/media/keys/ACCC4CF8.asc -o "$key"
|
|
167
|
-
key_fingerprint="$(pgdg_key_fingerprint "$key")"
|
|
168
|
-
[[ "$key_fingerprint" == @PGDG_FINGERPRINT@ ]] || fail 'PostgreSQL signing key mismatch'
|
|
169
|
-
install -d -m 755 /usr/share/keyrings
|
|
170
|
-
install -m 644 "$key" /usr/share/keyrings/stll-pgdg.asc
|
|
171
|
-
rm -- "$key"
|
|
172
|
-
printf '%s\n' '@PGDG_SOURCE@' > /etc/apt/sources.list.d/stll-pgdg.list
|
|
173
|
-
fi
|
|
174
|
-
apt-get update
|
|
175
|
-
apt_install postgresql-@POSTGRES@ postgresql-client-@POSTGRES@
|
|
176
|
-
fi
|
|
177
|
-
postgres_ready || fail 'PostgreSQL toolchain is incomplete or differs from the policy major'
|
|
178
|
-
`;
|
|
179
|
-
const valkeyInstall = String.raw`
|
|
180
|
-
if ! command -v valkey-server >/dev/null || [[ "$(valkey-server --version | sed -nE 's/.*v=([0-9.]+).*/\1/p')" != '@VALKEY@' ]] || ! command -v valkey-cli >/dev/null || [[ "$(valkey-cli --version)" != 'valkey-cli @VALKEY@' ]]; then
|
|
181
|
-
local scratch server_url server_sha tools_url tools_sha package
|
|
182
|
-
scratch="$(mktemp -d)"
|
|
183
|
-
chmod 755 "$scratch"
|
|
184
|
-
case "$ARCH" in
|
|
185
|
-
amd64) server_url='@AMD64_SERVER_URL@'; server_sha='@AMD64_SERVER_SHA@'; tools_url='@AMD64_TOOLS_URL@'; tools_sha='@AMD64_TOOLS_SHA@' ;;
|
|
186
|
-
arm64) server_url='@ARM64_SERVER_URL@'; server_sha='@ARM64_SERVER_SHA@'; tools_url='@ARM64_TOOLS_URL@'; tools_sha='@ARM64_TOOLS_SHA@' ;;
|
|
187
|
-
*) fail 'Unsupported Valkey architecture' ;;
|
|
188
|
-
esac
|
|
189
|
-
curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' --tlsv1.2 "$server_url" -o "$scratch/valkey-server.deb"
|
|
190
|
-
curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' --tlsv1.2 "$tools_url" -o "$scratch/valkey-tools.deb"
|
|
191
|
-
(cd "$scratch"; printf '%s %s\n' "$server_sha" valkey-server.deb "$tools_sha" valkey-tools.deb | sha256sum --strict -c -)
|
|
192
|
-
for package in valkey-server valkey-tools; do
|
|
193
|
-
[[ "$(dpkg-deb -f "$scratch/$package.deb" Package)" == "$package" ]] || fail 'Valkey artifact package mismatch'
|
|
194
|
-
[[ "$(dpkg-deb -f "$scratch/$package.deb" Version)" == '@VALKEY@-1.noble' ]] || fail 'Valkey artifact version mismatch'
|
|
195
|
-
[[ "$(dpkg-deb -f "$scratch/$package.deb" Architecture)" == "$ARCH" ]] || fail 'Valkey artifact architecture mismatch'
|
|
196
|
-
done
|
|
197
|
-
apt-get update
|
|
198
|
-
apt_install "$scratch/valkey-server.deb" "$scratch/valkey-tools.deb"
|
|
199
|
-
rm -rf -- "$scratch"
|
|
200
|
-
fi
|
|
201
|
-
[[ "$(valkey-server --version | sed -nE 's/.*v=([0-9.]+).*/\1/p')" == '@VALKEY@' ]] || fail 'Valkey version differs from policy'
|
|
202
|
-
[[ "$(valkey-cli --version)" == 'valkey-cli @VALKEY@' ]] || fail 'Valkey client version differs from policy'
|
|
203
|
-
`;
|
|
204
|
-
const state = String.raw`
|
|
205
|
-
service_account_ready() {
|
|
206
|
-
local uid gid shell
|
|
207
|
-
uid="$(id -u stll-cloud)" || fail 'Service user is unavailable; run install'
|
|
208
|
-
gid="$(id -g stll-cloud)"
|
|
209
|
-
shell="$(getent passwd stll-cloud | cut -d: -f7)"
|
|
210
|
-
[[ "$uid" =~ ^[1-9][0-9]*$ && "$gid" =~ ^[1-9][0-9]*$ && "$shell" == /usr/sbin/nologin ]] || fail 'Service user must be unprivileged with no login shell'
|
|
211
|
-
}
|
|
212
|
-
prepare_state() {
|
|
213
|
-
service_account_ready
|
|
214
|
-
[[ ! -L /var/lib/stll-cloud ]] || fail 'Service state root must not be a symlink'
|
|
215
|
-
if [[ -e /var/lib/stll-cloud ]]; then
|
|
216
|
-
[[ -d /var/lib/stll-cloud && "$(stat -c '%u:%a' /var/lib/stll-cloud)" == 0:755 ]] || fail 'Invalid service state root'
|
|
217
|
-
else
|
|
218
|
-
install -d -m 755 -o root -g root /var/lib/stll-cloud
|
|
219
|
-
fi
|
|
220
|
-
STATE="/var/lib/stll-cloud/$(printf '%s' "$REPO_ROOT" | sha256sum | cut -d' ' -f1)"
|
|
221
|
-
[[ ! -L "$STATE" ]] || fail 'Service state must not be a symlink'
|
|
222
|
-
if [[ -e "$STATE" ]]; then
|
|
223
|
-
[[ -d "$STATE" && "$(stat -c '%u:%a' "$STATE")" == 0:710 && "$(stat -c '%g' "$STATE")" == "$(id -g stll-cloud)" ]] || fail 'Invalid service state ownership'
|
|
224
|
-
else
|
|
225
|
-
install -d -m 710 -o root -g stll-cloud "$STATE"
|
|
226
|
-
fi
|
|
227
|
-
[[ ! -L "$STATE/lock" ]] || fail 'Service lock must not be a symlink'
|
|
228
|
-
exec 9>"$STATE/lock"
|
|
229
|
-
flock -x -w 10 9 || fail 'Another setup process owns the service state'
|
|
230
|
-
}
|
|
231
|
-
`;
|
|
232
|
-
const renderCloudServiceInstall = ({ services, postgres, valkey, valkeyArtifacts }) => {
|
|
233
|
-
const parts = services.map((service) => {
|
|
234
|
-
switch (service) {
|
|
235
|
-
case "postgres": return postgresInstall.replaceAll("@POSTGRES@", postgres).replaceAll("@PGDG_SOURCE@", postgresSourceLine).replaceAll("@PGDG_FINGERPRINT@", postgresSigningFingerprint);
|
|
236
|
-
case "valkey": {
|
|
237
|
-
const replacements = /* @__PURE__ */ new Map([
|
|
238
|
-
["@VALKEY@", valkey],
|
|
239
|
-
["@AMD64_SERVER_URL@", valkeyArtifacts.amd64.server.url],
|
|
240
|
-
["@AMD64_SERVER_SHA@", valkeyArtifacts.amd64.server.sha256],
|
|
241
|
-
["@AMD64_TOOLS_URL@", valkeyArtifacts.amd64.tools.url],
|
|
242
|
-
["@AMD64_TOOLS_SHA@", valkeyArtifacts.amd64.tools.sha256],
|
|
243
|
-
["@ARM64_SERVER_URL@", valkeyArtifacts.arm64.server.url],
|
|
244
|
-
["@ARM64_SERVER_SHA@", valkeyArtifacts.arm64.server.sha256],
|
|
245
|
-
["@ARM64_TOOLS_URL@", valkeyArtifacts.arm64.tools.url],
|
|
246
|
-
["@ARM64_TOOLS_SHA@", valkeyArtifacts.arm64.tools.sha256]
|
|
247
|
-
]);
|
|
248
|
-
return valkeyInstall.replace(/@[A-Z0-9_]+@/g, (token) => {
|
|
249
|
-
const value = replacements.get(token);
|
|
250
|
-
if (value === void 0) throw new Error(`Missing Valkey artifact field: ${token}`);
|
|
251
|
-
return value;
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
default: throw new Error(`Unsupported service: ${String(service)}`);
|
|
255
|
-
}
|
|
256
|
-
});
|
|
257
|
-
const user = services.length === 0 ? "" : " if ! id stll-cloud >/dev/null 2>&1; then useradd --system --user-group --no-create-home --shell /usr/sbin/nologin stll-cloud; fi\n service_account_ready\n";
|
|
258
|
-
return `${packageInstall}\n${services.includes("postgres") ? postgresSources.replaceAll("@PGDG_FINGERPRINT@", postgresSigningFingerprint) : ""}\n${services.length === 0 ? "prepare_state() { :; }\n" : state}\ninstall_services() {\n${user}${parts.join("")}\n :\n}\n`;
|
|
259
|
-
};
|
|
8
|
+
import picomatch from "picomatch";
|
|
9
|
+
import { parse as parse$2 } from "smol-toml";
|
|
260
10
|
//#endregion
|
|
261
|
-
//#region src/
|
|
262
|
-
const
|
|
263
|
-
const
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
11
|
+
//#region src/toolchain-consumer-checks.ts
|
|
12
|
+
const consumerRunnerVersion = "0.14.1";
|
|
13
|
+
const consumerCheckWorkflow = "stella/.github/.github/workflows/package-consumer-compat.yml";
|
|
14
|
+
const record$6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
|
+
const repositoryDirectory = (value) => typeof value === "string" && value !== "" && !value.includes("\\") && !value.includes("${{") && !value.split("").some((character) => {
|
|
16
|
+
const code = character.charCodeAt(0);
|
|
17
|
+
return code < 32 || code === 127;
|
|
18
|
+
}) && !/^[A-Za-z]:/.test(value) && !path.posix.isAbsolute(value) && (value === "." || value.split("/").every((part) => part !== "" && part !== "." && part !== ".."));
|
|
19
|
+
const packageDirectories = (value) => Array.isArray(value) && value.length > 0 && value.every(repositoryDirectory) && new Set(value).size === value.length;
|
|
20
|
+
const cronFields = [
|
|
21
|
+
{
|
|
22
|
+
minimum: 0,
|
|
23
|
+
maximum: 59
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
minimum: 0,
|
|
27
|
+
maximum: 23
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
minimum: 1,
|
|
31
|
+
maximum: 31
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
minimum: 1,
|
|
35
|
+
maximum: 12,
|
|
36
|
+
names: Object.fromEntries([
|
|
37
|
+
"JAN",
|
|
38
|
+
"FEB",
|
|
39
|
+
"MAR",
|
|
40
|
+
"APR",
|
|
41
|
+
"MAY",
|
|
42
|
+
"JUN",
|
|
43
|
+
"JUL",
|
|
44
|
+
"AUG",
|
|
45
|
+
"SEP",
|
|
46
|
+
"OCT",
|
|
47
|
+
"NOV",
|
|
48
|
+
"DEC"
|
|
49
|
+
].map((name, index) => [name, index + 1]))
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
minimum: 0,
|
|
53
|
+
maximum: 6,
|
|
54
|
+
names: Object.fromEntries([
|
|
55
|
+
"SUN",
|
|
56
|
+
"MON",
|
|
57
|
+
"TUE",
|
|
58
|
+
"WED",
|
|
59
|
+
"THU",
|
|
60
|
+
"FRI",
|
|
61
|
+
"SAT"
|
|
62
|
+
].map((name, index) => [name, index]))
|
|
286
63
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
install: cloudInstallCommand,
|
|
293
|
-
envFile
|
|
64
|
+
];
|
|
65
|
+
const cronFieldValues = (text, field) => {
|
|
66
|
+
const value = (token) => {
|
|
67
|
+
const number = /^\d+$/.test(token) ? Number(token) : field.names?.[token.toUpperCase()];
|
|
68
|
+
return number !== void 0 && Number.isSafeInteger(number) && number >= field.minimum && number <= field.maximum ? number : void 0;
|
|
294
69
|
};
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
service_password() {
|
|
316
|
-
local file="$1"
|
|
317
|
-
service_file "$file" 0
|
|
318
|
-
if [[ ! -e "$file" ]]; then
|
|
319
|
-
(umask 077; openssl rand -hex 32 > "$file")
|
|
320
|
-
root chown root:root "$file"
|
|
321
|
-
fi
|
|
322
|
-
[[ "$(cat "$file")" =~ ^[0-9a-f]{64}$ ]] || fail "Invalid service credential state"
|
|
323
|
-
}
|
|
324
|
-
service_process_matches() {
|
|
325
|
-
local pid="$1" executable="$2" owner actual expected
|
|
326
|
-
[[ "$pid" =~ ^[1-9][0-9]*$ && -d "/proc/$pid" ]] || return 1
|
|
327
|
-
owner="$(stat -c '%u' "/proc/$pid" 2>/dev/null)" || return 2
|
|
328
|
-
[[ "$owner" =~ ^[0-9]+$ ]] || return 2
|
|
329
|
-
[[ "$owner" == "$SERVICE_UID" ]] || return 1
|
|
330
|
-
actual="$(readlink -f "/proc/$pid/exe" 2>/dev/null)" || return 2
|
|
331
|
-
expected="$(readlink -f "$executable")" || return 2
|
|
332
|
-
[[ -n "$actual" && -n "$expected" && "$actual" != *' (deleted)' ]] || return 2
|
|
333
|
-
[[ "$actual" == "$expected" ]]
|
|
334
|
-
}
|
|
335
|
-
service_process() {
|
|
336
|
-
service_process_matches "$1" "$2" || fail "Managed service process identity differs from the recorded state"
|
|
337
|
-
}
|
|
338
|
-
service_prune_pid() {
|
|
339
|
-
local file="$1" executable="$2" service="$3" pid identity
|
|
340
|
-
[[ -e "$file" ]] || return 0
|
|
341
|
-
pid="$(head -n 1 "$file")"
|
|
342
|
-
# PostgreSQL encodes a standalone backend with a negative PID.
|
|
343
|
-
if [[ "$service" == postgres ]]; then pid="$(printf '%s' "$pid" | sed 's/^-//')"; fi
|
|
344
|
-
if service_process_matches "$pid" "$executable"; then return 0; else identity=$?; fi
|
|
345
|
-
[[ "$identity" != 2 ]] || fail "Cannot verify recorded live service process; retaining its PID file"
|
|
346
|
-
# Let PostgreSQL check orphan shared memory before removing a dead-PID lock.
|
|
347
|
-
if [[ "$service" == postgres && "$pid" =~ ^[1-9][0-9]*$ && ! -d "/proc/$pid" ]]; then return 0; fi
|
|
348
|
-
if [[ "$service" == postgres ]]; then
|
|
349
|
-
[[ "$pid" =~ ^[1-9][0-9]*$ && "$SERVICE_UID" != 0 ]] || fail "Cannot safely recover PostgreSQL PID state"
|
|
350
|
-
# The root controller is not our nonroot postgres. Preserve the remaining
|
|
351
|
-
# lock rows so PostgreSQL's EPERM/ancestor path still checks orphan memory.
|
|
352
|
-
root sed -i "1s/.*/$$/" "$file"
|
|
353
|
-
return 0
|
|
354
|
-
fi
|
|
355
|
-
# Restored state can name an unrelated live process; never signal that PID.
|
|
356
|
-
root rm -- "$file"
|
|
357
|
-
}
|
|
358
|
-
`;
|
|
359
|
-
const postgresStart = String.raw`
|
|
360
|
-
local pg_bin='/usr/lib/postgresql/@POSTGRES@/bin' pg_data="$STATE/postgres" pg_password pg_details pg_pid pg_version
|
|
361
|
-
service_password "$STATE/postgres.password"
|
|
362
|
-
pg_password="$(cat "$STATE/postgres.password")"
|
|
363
|
-
service_directory "$pg_data"
|
|
364
|
-
for file in PG_VERSION postmaster.pid postgresql.conf pg_hba.conf pg_ident.conf postgresql.auto.conf; do
|
|
365
|
-
service_file "$pg_data/$file" "$SERVICE_UID"
|
|
366
|
-
done
|
|
367
|
-
if [[ ! -e "$pg_data/PG_VERSION" ]]; then
|
|
368
|
-
[[ -z "$(ls -A "$pg_data")" ]] || fail "PostgreSQL data directory is not initialized"
|
|
369
|
-
service_file "$STATE/postgres.init-password" "$SERVICE_UID"
|
|
370
|
-
(umask 077; printf '%s\n' "$pg_password" > "$STATE/postgres.init-password")
|
|
371
|
-
root chown stll-cloud:stll-cloud "$STATE/postgres.init-password"
|
|
372
|
-
service_run env PGHOST=127.0.0.1 PGHOSTADDR=127.0.0.1 "$pg_bin/initdb" -D "$pg_data" --username=stll_cloud --auth-host=scram-sha-256 --auth-local=scram-sha-256 --pwfile="$STATE/postgres.init-password" --encoding=UTF8 --locale=C --set=unix_socket_directories= >/dev/null || fail "PostgreSQL initialization failed"
|
|
373
|
-
root rm "$STATE/postgres.init-password"
|
|
374
|
-
fi
|
|
375
|
-
[[ -f "$pg_data/PG_VERSION" && "$(cat "$pg_data/PG_VERSION")" == '@POSTGRES@' ]] || fail "PostgreSQL data has a different major version"
|
|
376
|
-
service_prune_pid "$pg_data/postmaster.pid" "$pg_bin/postgres" postgres
|
|
377
|
-
if ! service_run env PGHOST=127.0.0.1 PGHOSTADDR=127.0.0.1 "$pg_bin/pg_ctl" -D "$pg_data" status >/dev/null 2>&1; then
|
|
378
|
-
service_run env PGHOST=127.0.0.1 PGHOSTADDR=127.0.0.1 "$pg_bin/pg_ctl" -D "$pg_data" -l "$pg_data/cloud.log" -o "-h 127.0.0.1 -p 55432 -c unix_socket_directories=" -t 10 -w start >/dev/null || fail "PostgreSQL start failed; port 55432 must be available"
|
|
379
|
-
fi
|
|
380
|
-
pg_details="$(env -u PGSERVICE -u PGSERVICEFILE -u PGOPTIONS PGHOSTADDR=127.0.0.1 PGPASSWORD="$pg_password" PGCONNECT_TIMEOUT=2 "$pg_bin/psql" -X -h 127.0.0.1 -p 55432 -U stll_cloud -d postgres -At -v ON_ERROR_STOP=1 -c "SELECT current_setting('data_directory'), current_setting('listen_addresses'), current_setting('port'), current_setting('server_version_num'), current_setting('unix_socket_directories')")" || fail "PostgreSQL authenticated readiness failed"
|
|
381
|
-
pg_version="$(printf '%s\n' "$pg_details" | cut -d '|' -f4)"
|
|
382
|
-
[[ "$pg_details" == "$pg_data|127.0.0.1|55432|$pg_version|" && "$pg_version" =~ ^[0-9]{6}$ ]] || fail "PostgreSQL endpoint does not match managed data and listener"
|
|
383
|
-
(( pg_version / 10000 == @POSTGRES@ )) || fail "PostgreSQL endpoint has a different major version"
|
|
384
|
-
pg_pid="$(head -n 1 "$pg_data/postmaster.pid")"
|
|
385
|
-
service_process "$pg_pid" "$pg_bin/postgres"
|
|
386
|
-
[[ "$(env -u PGSERVICE -u PGSERVICEFILE -u PGOPTIONS PGHOSTADDR=127.0.0.1 PGPASSWORD="$pg_password" PGCONNECT_TIMEOUT=2 "$pg_bin/psql" -X -h 127.0.0.1 -p 55432 -U stll_cloud -d postgres -At -v ON_ERROR_STOP=1 -c "SELECT count(*) FROM pg_database WHERE datname = 'cloud_test'")" == 1 ]] || env -u PGSERVICE -u PGSERVICEFILE -u PGOPTIONS PGHOSTADDR=127.0.0.1 PGPASSWORD="$pg_password" PGCONNECT_TIMEOUT=2 "$pg_bin/createdb" -h 127.0.0.1 -p 55432 -U stll_cloud cloud_test || fail "PostgreSQL test database creation failed"
|
|
387
|
-
DATABASE_URL="postgresql://stll_cloud:$pg_password@127.0.0.1:55432/cloud_test"
|
|
388
|
-
`;
|
|
389
|
-
const valkeyStart = String.raw`
|
|
390
|
-
local vk_data="$STATE/valkey" vk_password vk_config vk_info vk_pid vk_expected ready=0
|
|
391
|
-
service_password "$STATE/valkey.password"
|
|
392
|
-
vk_password="$(cat "$STATE/valkey.password")"
|
|
393
|
-
service_directory "$vk_data"
|
|
394
|
-
vk_config="$vk_data/valkey.conf"
|
|
395
|
-
service_file "$vk_config" "$SERVICE_UID"
|
|
396
|
-
for file in valkey.pid valkey.log dump.rdb; do
|
|
397
|
-
[[ ! -L "$vk_data/$file" ]] || fail "Valkey state must not contain symbolic links"
|
|
398
|
-
if [[ -e "$vk_data/$file" ]]; then
|
|
399
|
-
[[ -f "$vk_data/$file" && "$(stat -c '%u' "$vk_data/$file")" == "$SERVICE_UID" ]] || fail "Invalid Valkey state file"
|
|
400
|
-
fi
|
|
401
|
-
done
|
|
402
|
-
vk_expected="$(printf '%s\n' 'bind 127.0.0.1' 'port 56379' 'protected-mode yes' 'daemonize yes' "requirepass $vk_password" "dir $vk_data" "pidfile $vk_data/valkey.pid" "logfile $vk_data/valkey.log" 'save ""' 'appendonly no')"
|
|
403
|
-
if [[ ! -e "$vk_config" ]]; then
|
|
404
|
-
(umask 077; printf '%s\n' "$vk_expected" > "$vk_config")
|
|
405
|
-
root chown stll-cloud:stll-cloud "$vk_config"
|
|
406
|
-
fi
|
|
407
|
-
[[ "$(cat "$vk_config")" == "$vk_expected" ]] || fail "Valkey configuration differs from managed settings"
|
|
408
|
-
service_prune_pid "$vk_data/valkey.pid" "$(command -v valkey-server)" valkey
|
|
409
|
-
if [[ ! -e "$vk_data/valkey.pid" ]]; then
|
|
410
|
-
service_run valkey-server "$vk_config" || fail "Valkey start failed; port 56379 must be available"
|
|
411
|
-
fi
|
|
412
|
-
for ((attempt=0; attempt<20; attempt++)); do
|
|
413
|
-
if vk_info="$(VALKEYCLI_AUTH="$vk_password" timeout 0.2 valkey-cli -h 127.0.0.1 -p 56379 --raw INFO server 2>/dev/null)"; then
|
|
414
|
-
if [[ "$vk_info" == *$'valkey_version:@VALKEY@\r\n'* && "$vk_info" == *"config_file:$vk_config"$'\r\n'* ]]; then ready=1; break; fi
|
|
415
|
-
fi
|
|
416
|
-
sleep 0.25
|
|
417
|
-
done
|
|
418
|
-
[[ "$ready" == 1 ]] || fail "Valkey authenticated readiness or managed version/configuration check failed"
|
|
419
|
-
[[ -f "$vk_data/valkey.pid" ]] || fail "Valkey managed process identifier is unavailable"
|
|
420
|
-
vk_pid="$(cat "$vk_data/valkey.pid")"
|
|
421
|
-
service_process "$vk_pid" "$(command -v valkey-server)"
|
|
422
|
-
[[ "$vk_info" == *"process_id:$vk_pid"$'\r\n'* ]] || fail "Valkey endpoint has a different process identifier"
|
|
423
|
-
[[ "$(VALKEYCLI_AUTH="$vk_password" timeout 2 valkey-cli -h 127.0.0.1 -p 56379 --raw CONFIG GET bind)" == $'bind\n127.0.0.1' ]] || fail "Valkey endpoint must bind only loopback"
|
|
424
|
-
[[ "$(VALKEYCLI_AUTH="$vk_password" timeout 2 valkey-cli -h 127.0.0.1 -p 56379 --raw CONFIG GET dir)" == "$(printf 'dir\n%s' "$vk_data")" ]] || fail "Valkey endpoint has a different data directory"
|
|
425
|
-
REDIS_URL="redis://:$vk_password@127.0.0.1:56379/0"
|
|
426
|
-
`;
|
|
427
|
-
/** Render only declared services; installation and environment-file ownership stay with the caller. */
|
|
428
|
-
const renderCloudServiceStart = ({ services, postgres, valkey }) => {
|
|
429
|
-
if (!/^[1-9][0-9]*$/.test(postgres) || !/^\d+\.\d+\.\d+$/.test(valkey)) throw new Error("Invalid cloud service runtime policy");
|
|
430
|
-
if (services.length === 0) return "start_services() { :; }\n";
|
|
431
|
-
const bodies = services.map((service) => {
|
|
432
|
-
switch (service) {
|
|
433
|
-
case "postgres": return postgresStart.replaceAll("@POSTGRES@", postgres);
|
|
434
|
-
case "valkey": return valkeyStart.replaceAll("@VALKEY@", valkey);
|
|
435
|
-
default: throw new Error(`Unsupported cloud service: ${String(service)}`);
|
|
436
|
-
}
|
|
437
|
-
});
|
|
438
|
-
return `${common}\nstart_services() {\n local SERVICE_UID file attempt\n SERVICE_UID="$(id -u stll-cloud)"\n [[ -d "$STATE" && ! -L "$STATE" && "$(stat -c '%u' "$STATE")" == 0 ]] || fail "Invalid cloud service state directory"\n${bodies.join("")}\n}\n`;
|
|
439
|
-
};
|
|
440
|
-
//#endregion
|
|
441
|
-
//#region src/toolchain-node.ts
|
|
442
|
-
const numericComponent = "(?:0|[1-9]\\d*)";
|
|
443
|
-
const policyPattern = new RegExp(`^${numericComponent}\\.x$`);
|
|
444
|
-
const selectorPattern = new RegExp(`^(${numericComponent})(?:\\.(?:x|${numericComponent}(?:\\.${numericComponent})?))?$`);
|
|
445
|
-
/** A Node policy selects the complete stable release series of one major. */
|
|
446
|
-
const nodePolicyValid = (value) => policyPattern.test(value) && validRange(value) !== null;
|
|
447
|
-
/** Static selectors may narrow that series, but cannot float across majors. */
|
|
448
|
-
const nodeSelectorMatches = (value, policy) => {
|
|
449
|
-
if (!nodePolicyValid(policy) || typeof value !== "string") return false;
|
|
450
|
-
const major = selectorPattern.exec(value)?.[1];
|
|
451
|
-
return major !== void 0 && major === policy.slice(0, -2) && validRange(value) !== null;
|
|
452
|
-
};
|
|
453
|
-
/** Engines must support every stable release in the approved major series. */
|
|
454
|
-
const nodeSupportRangeMatches = (policy, range) => {
|
|
455
|
-
if (!nodePolicyValid(policy)) return false;
|
|
456
|
-
try {
|
|
457
|
-
return subset(policy, range);
|
|
458
|
-
} catch {
|
|
459
|
-
return false;
|
|
70
|
+
const values = /* @__PURE__ */ new Set();
|
|
71
|
+
for (const part of text.split(",")) {
|
|
72
|
+
const split = part.split("/");
|
|
73
|
+
if (split.length > 2) return void 0;
|
|
74
|
+
const [base, stepText] = split;
|
|
75
|
+
const step = stepText === void 0 ? 1 : Number(stepText);
|
|
76
|
+
if (stepText !== void 0 && !/^\d+$/.test(stepText)) return void 0;
|
|
77
|
+
if (!Number.isSafeInteger(step) || step <= 0 || base === void 0) return void 0;
|
|
78
|
+
const bounds = base.split("-");
|
|
79
|
+
if (bounds.length > 2) return void 0;
|
|
80
|
+
const lower = bounds.at(0);
|
|
81
|
+
if (lower === void 0) return void 0;
|
|
82
|
+
const start = base === "*" ? field.minimum : value(lower);
|
|
83
|
+
const upper = bounds.at(1);
|
|
84
|
+
let end = start;
|
|
85
|
+
if (base === "*" || bounds.length === 1 && stepText !== void 0) end = field.maximum;
|
|
86
|
+
else if (upper !== void 0) end = value(upper);
|
|
87
|
+
if (start === void 0 || end === void 0 || start > end) return void 0;
|
|
88
|
+
for (let selected = start; selected <= end; selected += step) values.add(selected);
|
|
460
89
|
}
|
|
90
|
+
return values;
|
|
461
91
|
};
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
# Generated by stll-cloud-setup. Regenerate with stll-cloud-setup.
|
|
468
|
-
# Networked installation and offline service startup are separate lifecycle steps.
|
|
469
|
-
# Cached filesystems may not preserve processes; run start at every session start.
|
|
470
|
-
# Supported platform: Ubuntu 24.04 (noble), amd64/arm64, root or passwordless sudo.
|
|
471
|
-
set -euo pipefail
|
|
472
|
-
|
|
473
|
-
usage() { echo 'Usage: cloud-setup.sh install|start' >&2; exit 2; }
|
|
474
|
-
[[ "$#" == 1 ]] || usage
|
|
475
|
-
case "$1" in install|start) MODE="$1" ;; *) usage ;; esac
|
|
476
|
-
fail() { echo "cloud-setup: $*" >&2; exit 1; }
|
|
477
|
-
trap 'echo "cloud-setup: command failed at line $LINENO" >&2' ERR
|
|
478
|
-
[[ -f /etc/os-release ]] || fail 'Ubuntu 24.04 is required'
|
|
479
|
-
. /etc/os-release
|
|
480
|
-
[[ "$ID" == ubuntu && "$VERSION_ID" == 24.04 && "$VERSION_CODENAME" == noble ]] || fail 'Only Ubuntu 24.04 (noble) is supported'
|
|
481
|
-
SCRIPT="$(realpath -- "$0")"
|
|
482
|
-
REPO_ROOT="$(cd -- "$(dirname -- "$SCRIPT")/.." && pwd -P)"
|
|
483
|
-
[[ "$SCRIPT" == "$REPO_ROOT/.agents/cloud-setup.sh" ]] || fail 'Run the generated repository script'
|
|
484
|
-
if [[ "$(id -u)" != 0 ]]; then
|
|
485
|
-
command -v sudo >/dev/null && sudo -n true || fail 'Root or passwordless sudo is required'
|
|
486
|
-
exec sudo -n -- bash "$SCRIPT" "$MODE"
|
|
487
|
-
fi
|
|
488
|
-
OUTPUT_UID="$(printenv SUDO_UID || id -u)"
|
|
489
|
-
[[ "$OUTPUT_UID" =~ ^[0-9]+$ ]] || fail 'Invalid invoking user'
|
|
490
|
-
OUTPUT_GID="$(getent passwd "$OUTPUT_UID" | cut -d: -f4)"
|
|
491
|
-
[[ "$OUTPUT_GID" =~ ^[0-9]+$ ]] || fail 'Invoking user is unavailable'
|
|
492
|
-
OUTPUT_USER="$(getent passwd "$OUTPUT_UID" | cut -d: -f1)"
|
|
493
|
-
OUTPUT_HOME="$(getent passwd "$OUTPUT_UID" | cut -d: -f6)"
|
|
494
|
-
[[ -n "$OUTPUT_USER" && "$OUTPUT_HOME" == /* && -d "$OUTPUT_HOME" ]] || fail 'Invoking user requires an existing home directory'
|
|
495
|
-
root() { "$@"; }
|
|
496
|
-
service_run() { runuser -u stll-cloud -- "$@" 9>&-; }
|
|
497
|
-
BUN_VERSION='@BUN@'
|
|
498
|
-
NODE_VERSION='@NODE@'
|
|
499
|
-
NODE_DIR="/opt/stll-cloud/node/$NODE_VERSION"
|
|
500
|
-
BUN_DIR="/opt/stll-cloud/bun/$BUN_VERSION"
|
|
501
|
-
export PATH="$NODE_DIR/bin:$BUN_DIR:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
|
502
|
-
BUN_CACHE="/var/cache/stll-cloud/bun/$OUTPUT_UID"
|
|
503
|
-
@ENV_ASSIGNMENTS@
|
|
504
|
-
[[ -f "$REPO_ROOT/.node-version" && ! -L "$REPO_ROOT/.node-version" ]] || fail 'A regular root .node-version is required'
|
|
505
|
-
[[ "$(awk 'NF { if (++count > 1 || NF != 1) exit 1; value=$1 } END { if (count != 1) exit 1; print value }' "$REPO_ROOT/.node-version")" == "$NODE_VERSION" ]] || fail 'Node selector changed; regenerate cloud-setup.sh'
|
|
506
|
-
cd -- "$REPO_ROOT"
|
|
507
|
-
case "$(uname -m)" in
|
|
508
|
-
x86_64) ARCH=amd64; NODE_ARCH=x64; BUN_ARCH=x64 ;;
|
|
509
|
-
aarch64|arm64) ARCH=arm64; NODE_ARCH=arm64; BUN_ARCH=aarch64 ;;
|
|
510
|
-
*) fail 'Only amd64 and arm64 are supported' ;;
|
|
511
|
-
esac
|
|
512
|
-
|
|
513
|
-
runtime_ready() {
|
|
514
|
-
[[ -x "$NODE_DIR/bin/node" && "$($NODE_DIR/bin/node --version)" == "v$NODE_VERSION" ]] || fail 'Pinned Node is unavailable; run install'
|
|
515
|
-
[[ -x "$BUN_DIR/bun" && "$($BUN_DIR/bun --version)" == "$BUN_VERSION" ]] || fail 'Pinned Bun is unavailable; run install'
|
|
516
|
-
}
|
|
517
|
-
install_runtimes() {
|
|
518
|
-
if ! command -v curl >/dev/null || ! command -v unzip >/dev/null || ! command -v xz >/dev/null || ! command -v openssl >/dev/null || ! command -v gpg >/dev/null; then
|
|
519
|
-
apt-get update
|
|
520
|
-
DEBIAN_FRONTEND=noninteractive apt_install ca-certificates curl unzip xz-utils openssl gnupg util-linux
|
|
521
|
-
fi
|
|
522
|
-
local scratch node_archive bun_archive bun_folder
|
|
523
|
-
scratch="$(mktemp -d)"
|
|
524
|
-
trap 'rm -rf -- "$scratch"' RETURN
|
|
525
|
-
if [[ ! -x "$NODE_DIR/bin/node" ]] || [[ "$($NODE_DIR/bin/node --version)" != "v$NODE_VERSION" ]]; then
|
|
526
|
-
node_archive="node-v$NODE_VERSION-linux-$NODE_ARCH.tar.xz"
|
|
527
|
-
curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' --tlsv1.2 "https://nodejs.org/dist/v$NODE_VERSION/$node_archive" -o "$scratch/$node_archive"
|
|
528
|
-
curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' --tlsv1.2 "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt" -o "$scratch/node-sums"
|
|
529
|
-
(cd "$scratch"; awk -v name="$node_archive" '$2 == name { print; count++ } END { if (count != 1) exit 1 }' node-sums | sha256sum --strict -c -)
|
|
530
|
-
install -d -m 755 "$NODE_DIR"
|
|
531
|
-
tar -xJf "$scratch/$node_archive" --strip-components=1 -C "$NODE_DIR"
|
|
532
|
-
fi
|
|
533
|
-
if [[ ! -x "$BUN_DIR/bun" ]] || [[ "$($BUN_DIR/bun --version)" != "$BUN_VERSION" ]]; then
|
|
534
|
-
bun_folder="bun-linux-$BUN_ARCH"
|
|
535
|
-
bun_archive="$bun_folder.zip"
|
|
536
|
-
curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' --tlsv1.2 "https://github.com/oven-sh/bun/releases/download/bun-v$BUN_VERSION/$bun_archive" -o "$scratch/$bun_archive"
|
|
537
|
-
curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' --tlsv1.2 "https://github.com/oven-sh/bun/releases/download/bun-v$BUN_VERSION/SHASUMS256.txt" -o "$scratch/bun-sums"
|
|
538
|
-
(cd "$scratch"; awk -v name="$bun_archive" '$2 == name || $2 == "*" name { print $1 " " name; count++ } END { if (count != 1) exit 1 }' bun-sums | sha256sum --strict -c -)
|
|
539
|
-
unzip -q "$scratch/$bun_archive" -d "$scratch/bun"
|
|
540
|
-
install -d -m 755 "$BUN_DIR"
|
|
541
|
-
install -m 755 "$scratch/bun/$bun_folder/bun" "$BUN_DIR/bun"
|
|
542
|
-
fi
|
|
543
|
-
runtime_ready
|
|
544
|
-
ln -sfnT -- "$NODE_DIR/bin/node" /usr/local/bin/node
|
|
545
|
-
ln -sfnT -- "$NODE_DIR/bin/npm" /usr/local/bin/npm
|
|
546
|
-
ln -sfnT -- "$NODE_DIR/bin/npx" /usr/local/bin/npx
|
|
547
|
-
ln -sfnT -- "$BUN_DIR/bun" /usr/local/bin/bun
|
|
548
|
-
rm -rf -- "$scratch"
|
|
549
|
-
trap - RETURN
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
dependency_install() {
|
|
553
|
-
local directory
|
|
554
|
-
for directory in /var/cache/stll-cloud /var/cache/stll-cloud/bun; do
|
|
555
|
-
[[ ! -L "$directory" ]] || fail 'Dependency cache parent must not be a symlink'
|
|
556
|
-
if [[ -e "$directory" ]]; then
|
|
557
|
-
[[ -d "$directory" && "$(stat -c '%u' "$directory")" == 0 ]] || fail 'Dependency cache parent must be root-owned'
|
|
558
|
-
fi
|
|
559
|
-
install -d -m 755 -o root -g root "$directory"
|
|
560
|
-
done
|
|
561
|
-
[[ ! -L "$BUN_CACHE" ]] || fail 'Dependency cache must not be a symlink'
|
|
562
|
-
if [[ -e "$BUN_CACHE" ]]; then
|
|
563
|
-
[[ -d "$BUN_CACHE" && "$(stat -c '%u' "$BUN_CACHE")" == "$OUTPUT_UID" ]] || fail 'Dependency cache ownership differs from the invoking user'
|
|
564
|
-
fi
|
|
565
|
-
install -d -m 700 -o "$OUTPUT_UID" -g "$OUTPUT_GID" "$BUN_CACHE"
|
|
566
|
-
runuser -u "$OUTPUT_USER" -- env HOME="$OUTPUT_HOME" BUN_INSTALL_CACHE_DIR="$BUN_CACHE" PATH="$PATH" "$BUN_DIR/bun" install --frozen-lockfile
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
@SERVICE_INSTALL@
|
|
570
|
-
@SERVICE_START@
|
|
571
|
-
|
|
572
|
-
@ENV_FUNCTIONS@
|
|
573
|
-
|
|
574
|
-
if [[ "$MODE" == install ]]; then
|
|
575
|
-
install_runtimes
|
|
576
|
-
install_services
|
|
577
|
-
dependency_install
|
|
578
|
-
exit 0
|
|
579
|
-
fi
|
|
580
|
-
runtime_ready
|
|
581
|
-
@ENV_VALIDATE@
|
|
582
|
-
prepare_state
|
|
583
|
-
start_services
|
|
584
|
-
@ENV_WRITE@
|
|
585
|
-
`;
|
|
586
|
-
const environmentFunctions = String.raw`
|
|
587
|
-
validate_environment() {
|
|
588
|
-
local part target="$REPO_ROOT" env_path="$REPO_ROOT/$ENV_FILE" first_line temporary
|
|
589
|
-
while IFS= read -r part; do
|
|
590
|
-
target="$target/$part"
|
|
591
|
-
[[ ! -L "$target" ]] || fail 'Environment path must not contain symbolic links'
|
|
592
|
-
done < <(printf '%s\n' "$ENV_FILE" | tr '/' '\n')
|
|
593
|
-
[[ -d "$(dirname -- "$env_path")" ]] || fail 'Environment parent directory is missing'
|
|
594
|
-
if [[ -e "$env_path" ]]; then
|
|
595
|
-
[[ -f "$env_path" ]] || fail 'Environment target must be a regular file'
|
|
596
|
-
IFS= read -r first_line < "$env_path" || fail 'Refusing to overwrite an unowned environment file'
|
|
597
|
-
[[ "$first_line" == "$ENV_MARKER" ]] || fail 'Refusing to overwrite an unowned environment file'
|
|
598
|
-
fi
|
|
599
|
-
}
|
|
600
|
-
write_environment() {
|
|
601
|
-
validate_environment
|
|
602
|
-
local env_path="$REPO_ROOT/$ENV_FILE" temporary
|
|
603
|
-
temporary="$(mktemp "$(dirname -- "$env_path")/.stll-cloud-env.XXXXXX")"
|
|
604
|
-
chmod 600 "$temporary"
|
|
605
|
-
{
|
|
606
|
-
printf '%s\n' "$ENV_MARKER" 'NODE_ENV=test'
|
|
607
|
-
if [[ -n "$DATABASE_URL" ]]; then printf 'DATABASE_URL=%s\n' "$DATABASE_URL"; fi
|
|
608
|
-
if [[ -n "$REDIS_URL" ]]; then printf 'REDIS_URL=%s\n' "$REDIS_URL"; fi
|
|
609
|
-
} > "$temporary"
|
|
610
|
-
chown "$OUTPUT_UID:$OUTPUT_GID" "$temporary"
|
|
611
|
-
mv -f -- "$temporary" "$env_path"
|
|
612
|
-
}
|
|
613
|
-
`;
|
|
614
|
-
const shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
615
|
-
const generateCloudSetup = ({ policy, cloud, nodeVersion }) => {
|
|
616
|
-
const selected = parseCloudSetup(cloud);
|
|
617
|
-
if (selected === void 0) throw new Error("cloud declaration is required");
|
|
618
|
-
const exactNode = nodeVersion.trim();
|
|
619
|
-
if (valid(exactNode) !== exactNode || !nodeSelectorMatches(exactNode, policy.node)) throw new Error("cloud requires a tracked exact .node-version within the shared Node series");
|
|
620
|
-
const services = cloudServices.filter((service) => selected.services.includes(service));
|
|
621
|
-
const replacements = /* @__PURE__ */ new Map([
|
|
622
|
-
["@BUN@", policy.bun],
|
|
623
|
-
["@NODE@", exactNode],
|
|
624
|
-
["@ENV_ASSIGNMENTS@", selected.envFile === void 0 ? "" : `ENV_FILE=${shellQuote(selected.envFile)}\nENV_MARKER=${shellQuote(cloudEnvironmentMarker)}`],
|
|
625
|
-
["@ENV_FUNCTIONS@", selected.envFile === void 0 ? "" : environmentFunctions],
|
|
626
|
-
["@ENV_VALIDATE@", selected.envFile === void 0 ? "" : "DATABASE_URL=''\nREDIS_URL=''\nvalidate_environment"],
|
|
627
|
-
["@ENV_WRITE@", selected.envFile === void 0 ? "" : "write_environment"],
|
|
628
|
-
["@SERVICE_INSTALL@", renderCloudServiceInstall({
|
|
629
|
-
services,
|
|
630
|
-
postgres: policy.postgres,
|
|
631
|
-
valkey: policy.valkey,
|
|
632
|
-
valkeyArtifacts: policy.valkeyArtifacts
|
|
633
|
-
})],
|
|
634
|
-
["@SERVICE_START@", renderCloudServiceStart({
|
|
635
|
-
services,
|
|
636
|
-
postgres: policy.postgres,
|
|
637
|
-
valkey: policy.valkey
|
|
638
|
-
})]
|
|
639
|
-
]);
|
|
640
|
-
return script.replace(/@[A-Z0-9_]+@/g, (token) => {
|
|
641
|
-
const value = replacements.get(token);
|
|
642
|
-
if (value === void 0) throw new Error(`Missing cloud setup field: ${token}`);
|
|
643
|
-
return value;
|
|
92
|
+
const validConsumerCron = (text) => {
|
|
93
|
+
const fields = text.trim().split(/\s+/);
|
|
94
|
+
return fields.length === cronFields.length && cronFields.every((field, index) => {
|
|
95
|
+
const fieldText = fields.at(index);
|
|
96
|
+
return fieldText !== void 0 && cronFieldValues(fieldText, field) !== void 0;
|
|
644
97
|
});
|
|
645
98
|
};
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
"template",
|
|
658
|
-
"spec"
|
|
659
|
-
],
|
|
660
|
-
DaemonSet: [
|
|
661
|
-
"spec",
|
|
662
|
-
"template",
|
|
663
|
-
"spec"
|
|
664
|
-
],
|
|
665
|
-
StatefulSet: [
|
|
666
|
-
"spec",
|
|
667
|
-
"template",
|
|
668
|
-
"spec"
|
|
669
|
-
],
|
|
670
|
-
ReplicaSet: [
|
|
671
|
-
"spec",
|
|
672
|
-
"template",
|
|
673
|
-
"spec"
|
|
674
|
-
],
|
|
675
|
-
ReplicationController: [
|
|
676
|
-
"spec",
|
|
677
|
-
"template",
|
|
678
|
-
"spec"
|
|
679
|
-
],
|
|
680
|
-
Job: [
|
|
681
|
-
"spec",
|
|
682
|
-
"template",
|
|
683
|
-
"spec"
|
|
684
|
-
],
|
|
685
|
-
CronJob: [
|
|
686
|
-
"spec",
|
|
687
|
-
"jobTemplate",
|
|
688
|
-
"spec",
|
|
689
|
-
"template",
|
|
690
|
-
"spec"
|
|
691
|
-
]
|
|
692
|
-
};
|
|
693
|
-
/** Shared semantic image enumeration for runtime checks and update-root generation. */
|
|
694
|
-
const containerDocumentImages = (document, file) => {
|
|
695
|
-
if (!record$6(document)) return void 0;
|
|
696
|
-
const services = document["services"];
|
|
697
|
-
if (isComposeDefinitionPath(file) && record$6(services)) {
|
|
698
|
-
const images = [];
|
|
699
|
-
for (const [name, service] of Object.entries(services)) if (record$6(service) && service["image"] !== void 0) images.push({
|
|
700
|
-
image: service["image"],
|
|
701
|
-
path: [
|
|
702
|
-
"services",
|
|
703
|
-
name,
|
|
704
|
-
"image"
|
|
705
|
-
]
|
|
99
|
+
const nightlySchedule = (value) => {
|
|
100
|
+
if (!record$6(value)) return false;
|
|
101
|
+
const schedule = value["schedule"];
|
|
102
|
+
if (!Array.isArray(schedule) || schedule.length === 0) return false;
|
|
103
|
+
const coveredDays = /* @__PURE__ */ new Set();
|
|
104
|
+
for (const entry of schedule) {
|
|
105
|
+
if (!record$6(entry) || typeof entry["cron"] !== "string" || !validConsumerCron(entry["cron"])) return false;
|
|
106
|
+
const fields = entry["cron"].trim().split(/\s+/);
|
|
107
|
+
const selections = cronFields.map((field, index) => {
|
|
108
|
+
const text = fields.at(index);
|
|
109
|
+
return text === void 0 ? void 0 : cronFieldValues(text, field);
|
|
706
110
|
});
|
|
111
|
+
if (selections.at(2)?.size !== 31 || selections.at(3)?.size !== 12) continue;
|
|
112
|
+
for (const day of selections.at(4) ?? []) coveredDays.add(day);
|
|
113
|
+
}
|
|
114
|
+
return coveredDays.size === 7;
|
|
115
|
+
};
|
|
116
|
+
const parseConsumerChecks = (input) => {
|
|
117
|
+
if (input === void 0) return [];
|
|
118
|
+
if (!Array.isArray(input)) throw new Error("consumerChecks must be an array");
|
|
119
|
+
const targets = /* @__PURE__ */ new Set();
|
|
120
|
+
return input.map((entry) => {
|
|
121
|
+
if (!record$6(entry) || Object.keys(entry).some((key) => ![
|
|
122
|
+
"workflow",
|
|
123
|
+
"job",
|
|
124
|
+
"packages",
|
|
125
|
+
"toolingVersion",
|
|
126
|
+
"fixturePath"
|
|
127
|
+
].includes(key)) || typeof entry["workflow"] !== "string" || githubAutomationFileKind(entry["workflow"]) !== "workflow" || !/^\.github\/workflows\/[^/]+\.ya?ml$/.test(entry["workflow"]) || typeof entry["job"] !== "string" || !/^[A-Za-z_][A-Za-z0-9_-]*$/.test(entry["job"]) || !packageDirectories(entry["packages"]) || typeof entry["toolingVersion"] !== "string" || !/^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/.test(entry["toolingVersion"]) || !repositoryDirectory(entry["fixturePath"])) throw new Error("consumerChecks requires canonical workflow, job, unique package directories, exact toolingVersion and repository-relative fixturePath");
|
|
128
|
+
const target = `${entry["workflow"]}:${entry["job"]}`;
|
|
129
|
+
if (targets.has(target)) throw new Error("consumerChecks targets must be unique");
|
|
130
|
+
targets.add(target);
|
|
707
131
|
return {
|
|
708
|
-
|
|
709
|
-
|
|
132
|
+
workflow: entry["workflow"],
|
|
133
|
+
job: entry["job"],
|
|
134
|
+
packages: entry["packages"],
|
|
135
|
+
toolingVersion: entry["toolingVersion"],
|
|
136
|
+
fixturePath: entry["fixturePath"]
|
|
710
137
|
};
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
let spec = resource;
|
|
730
|
-
for (const key of specPath) spec = record$6(spec) ? spec[key] : void 0;
|
|
731
|
-
if (!record$6(spec)) return;
|
|
732
|
-
for (const key of [
|
|
733
|
-
"containers",
|
|
734
|
-
"initContainers",
|
|
735
|
-
"ephemeralContainers"
|
|
736
|
-
]) {
|
|
737
|
-
const containers = spec[key];
|
|
738
|
-
if (!Array.isArray(containers)) continue;
|
|
739
|
-
for (const [index, container] of containers.entries()) if (record$6(container) && container["image"] !== void 0) images.push({
|
|
740
|
-
image: container["image"],
|
|
741
|
-
path: [
|
|
742
|
-
...prefix,
|
|
743
|
-
...specPath,
|
|
744
|
-
key,
|
|
745
|
-
index,
|
|
746
|
-
"image"
|
|
747
|
-
]
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
/** Consumer checks are scoped declarations, never general runtime exceptions. */
|
|
141
|
+
const checkConsumerChecks = ({ declarations, files, policy }) => {
|
|
142
|
+
const diagnostics = [];
|
|
143
|
+
const exercised = /* @__PURE__ */ new Set();
|
|
144
|
+
const add = (message) => diagnostics.push({
|
|
145
|
+
rule: "configuration",
|
|
146
|
+
path: "stll-toolchain.json",
|
|
147
|
+
line: 1,
|
|
148
|
+
message
|
|
149
|
+
});
|
|
150
|
+
for (const [workflow, text] of Object.entries(files)) {
|
|
151
|
+
if (githubAutomationFileKind(workflow) !== "workflow") continue;
|
|
152
|
+
try {
|
|
153
|
+
const document = parseDocument(text, {
|
|
154
|
+
uniqueKeys: true,
|
|
155
|
+
merge: true
|
|
748
156
|
});
|
|
157
|
+
if (document.errors.length > 0) throw new Error("invalid workflow YAML");
|
|
158
|
+
const source = document.toJS({ maxAliasCount: 100 });
|
|
159
|
+
if (!record$6(source) || !record$6(source["jobs"])) continue;
|
|
160
|
+
for (const [jobName, job] of Object.entries(source["jobs"])) {
|
|
161
|
+
if (!record$6(job) || typeof job["uses"] !== "string") continue;
|
|
162
|
+
const [workflowPath, ref] = job["uses"].split("@");
|
|
163
|
+
if (workflowPath?.toLowerCase() !== "stella/.github/.github/workflows/package-consumer-compat.yml") continue;
|
|
164
|
+
const declaration = declarations.find((entry) => entry.workflow === workflow && entry.job === jobName);
|
|
165
|
+
if (declaration === void 0) {
|
|
166
|
+
add(`${workflow}:${jobName} requires a consumerChecks declaration`);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (!nightlySchedule(source["on"])) throw new Error(`${workflow} consumer checks require a nonempty valid on.schedule covering every day`);
|
|
170
|
+
if ("if" in job || "needs" in job || "strategy" in job) throw new Error(`${workflow}:${jobName} consumer checks require a stand-alone unconditional reusable job without if or needs or strategy`);
|
|
171
|
+
exercised.add(declaration);
|
|
172
|
+
const approved = Object.entries(policy.actions).find(([name]) => name.toLowerCase() === consumerCheckWorkflow)?.[1];
|
|
173
|
+
if (job["uses"].split("@").length !== 2 || ref === void 0 || !/^[a-f0-9]{40}$/.test(ref) || approved === void 0 || ref !== approved.sha || "steps" in job || "runs-on" in job) throw new Error(`${workflow}:${jobName} must invoke the approved immutable consumer workflow as a reusable job`);
|
|
174
|
+
const inputs = job["with"];
|
|
175
|
+
if (!record$6(inputs)) throw new Error(`${workflow}:${jobName} requires consumer inputs`);
|
|
176
|
+
const names = /* @__PURE__ */ new Set();
|
|
177
|
+
for (const key of Object.keys(inputs)) {
|
|
178
|
+
const normalized = key.toLowerCase();
|
|
179
|
+
if (names.has(normalized) || ![
|
|
180
|
+
"packages",
|
|
181
|
+
"consumer-node",
|
|
182
|
+
"tooling-version",
|
|
183
|
+
"fixture-path"
|
|
184
|
+
].includes(normalized) || normalized !== key) throw new Error(`${workflow}:${jobName} requires unique canonical consumer input names`);
|
|
185
|
+
names.add(normalized);
|
|
186
|
+
}
|
|
187
|
+
if (inputs["consumer-node"] !== policy.consumerNode) throw new Error(`${workflow}:${jobName} consumer-node must be ${policy.consumerNode}`);
|
|
188
|
+
if (declaration.toolingVersion !== consumerRunnerVersion) throw new Error(`${workflow}:${jobName} toolingVersion must match installed @stll/oxlint-config ${consumerRunnerVersion}`);
|
|
189
|
+
if (inputs["tooling-version"] !== declaration.toolingVersion) throw new Error(`${workflow}:${jobName} tooling-version must match consumerChecks.toolingVersion`);
|
|
190
|
+
if (inputs["fixture-path"] !== declaration.fixturePath) throw new Error(`${workflow}:${jobName} fixture-path must match consumerChecks.fixturePath`);
|
|
191
|
+
const fixtureManifest = path.posix.join(declaration.fixturePath, "consumer-compat.json");
|
|
192
|
+
const fixtureSource = files[fixtureManifest];
|
|
193
|
+
if (fixtureSource === void 0) throw new Error(`consumerChecks fixture configuration must be tracked: ${fixtureManifest}`);
|
|
194
|
+
const fixtures = parseConsumerFixtures(JSON.parse(fixtureSource));
|
|
195
|
+
assertConsumerFixtureSelection({
|
|
196
|
+
selected: declaration.packages,
|
|
197
|
+
fixtures
|
|
198
|
+
});
|
|
199
|
+
const rawPackages = inputs["packages"];
|
|
200
|
+
if (typeof rawPackages !== "string") throw new Error(`${workflow}:${jobName} packages must be a static JSON array`);
|
|
201
|
+
const selected = JSON.parse(rawPackages);
|
|
202
|
+
if (!packageDirectories(selected) || selected.length !== declaration.packages.length || !selected.every((directory) => declaration.packages.includes(directory))) throw new Error(`${workflow}:${jobName} packages must exactly match consumerChecks`);
|
|
203
|
+
const workspacePackages = discoverConsumerPackages(files);
|
|
204
|
+
const packagesByDirectory = new Map([...workspacePackages.values()].map((entry) => [entry.directory, entry]));
|
|
205
|
+
for (const directory of declaration.packages) {
|
|
206
|
+
const manifestPath = path.posix.join(directory, "package.json");
|
|
207
|
+
if (files[manifestPath] === void 0) throw new Error(`consumerChecks package manifest must be tracked: ${manifestPath}`);
|
|
208
|
+
const selectedPackage = packagesByDirectory.get(directory);
|
|
209
|
+
if (selectedPackage === void 0) throw new Error(`consumerChecks package must be a discovered workspace member: ${manifestPath}`);
|
|
210
|
+
const fixture = fixtures.find((candidate) => candidate.package === directory);
|
|
211
|
+
if (fixture === void 0) throw new Error(`missing declared consumer fixture: ${directory}`);
|
|
212
|
+
assertConsumerFixtureKind({
|
|
213
|
+
fixture,
|
|
214
|
+
pkg: selectedPackage
|
|
215
|
+
});
|
|
216
|
+
const manifest = selectedPackage.manifest;
|
|
217
|
+
if (manifest["private"] === true || typeof manifest["name"] !== "string" || manifest["name"].trim() === "") throw new Error(`consumerChecks requires a named published package: ${manifestPath}`);
|
|
218
|
+
const closure = consumerPackageClosure({
|
|
219
|
+
selected: selectedPackage,
|
|
220
|
+
packages: workspacePackages,
|
|
221
|
+
files
|
|
222
|
+
});
|
|
223
|
+
for (const member of closure.values()) assertConsumerNodeSupport({
|
|
224
|
+
manifest: member.manifest,
|
|
225
|
+
node: policy.consumerNode,
|
|
226
|
+
label: path.posix.join(member.directory, "package.json")
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (declarations.some((entry) => entry.workflow === workflow) || text.toLowerCase().includes("stella/.github/.github/workflows/package-consumer-compat.yml")) add(error instanceof Error ? error.message : String(error));
|
|
749
232
|
}
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
return
|
|
753
|
-
ecosystem: "docker",
|
|
754
|
-
images
|
|
755
|
-
};
|
|
233
|
+
}
|
|
234
|
+
for (const declaration of declarations) if (!exercised.has(declaration)) add(`${declaration.workflow}:${declaration.job} consumerChecks declaration has no matching reusable invocation`);
|
|
235
|
+
return diagnostics;
|
|
756
236
|
};
|
|
757
237
|
//#endregion
|
|
758
238
|
//#region src/toolchain-images.ts
|
|
@@ -777,66 +257,6 @@ const canonicalDockerRuntime = (image) => {
|
|
|
777
257
|
return (registry === void 0 ? image : image.slice(registry.length + 1)).replace(/^library\//i, "");
|
|
778
258
|
};
|
|
779
259
|
//#endregion
|
|
780
|
-
//#region src/toolchain-inputs.ts
|
|
781
|
-
const excludedInputPath = (file) => file === "" || file.split("/").some((part) => part === "node_modules" || part === "vendor");
|
|
782
|
-
const javascriptDependencyLockfiles = [
|
|
783
|
-
"bun.lock",
|
|
784
|
-
"package-lock.json",
|
|
785
|
-
"yarn.lock",
|
|
786
|
-
"pnpm-lock.yaml"
|
|
787
|
-
];
|
|
788
|
-
/** Match project, environment, local, and fragment filenames recognized by mise. */
|
|
789
|
-
const isMiseConfigPath = (file) => !excludedInputPath(file) && (/(?:^|\/)\.?mise(?:\.[^./][^/]*)?\.toml$/.test(file) || /(?:^|\/)(?:\.?mise|\.config\/mise)\/config(?:\.[^./][^/]*)?\.toml$/.test(file) || /(?:^|\/)(?:\.?mise|\.config\/mise)\/conf\.d\/[^./][^/]*\.toml$/.test(file));
|
|
790
|
-
/** GitHub action metadata may live at the repository root or in any directory. */
|
|
791
|
-
const githubAutomationFileKind = (file) => {
|
|
792
|
-
if (file === "") return void 0;
|
|
793
|
-
if (/^\.github\/workflows\/[^/]+\.ya?ml$/.test(file)) return "workflow";
|
|
794
|
-
if (/(?:^|\/)action\.ya?ml$/.test(file)) return "action";
|
|
795
|
-
};
|
|
796
|
-
/** Dependabot updates workflows and repository-root action metadata. */
|
|
797
|
-
const isDependabotGithubActionsPath = (file) => {
|
|
798
|
-
const kind = githubAutomationFileKind(file);
|
|
799
|
-
return kind === "workflow" || kind === "action" && !file.includes("/");
|
|
800
|
-
};
|
|
801
|
-
/** Use one filename discriminator for reader selection and Python ecosystem decisions. */
|
|
802
|
-
const pythonDependencyManifestKind = (file) => {
|
|
803
|
-
if (excludedInputPath(file)) return void 0;
|
|
804
|
-
const name = file.split("/").at(-1);
|
|
805
|
-
if (name === "pyproject.toml") return "project";
|
|
806
|
-
if (name === "Pipfile") return "pipfile";
|
|
807
|
-
if (name === "Pipfile.lock") return "pipfile-lock";
|
|
808
|
-
if (name === "setup.py" || name === "setup.cfg") return "setup";
|
|
809
|
-
if (name !== void 0 && /^requirements[^/]*\.(?:txt|in)$/.test(name)) return "requirements";
|
|
810
|
-
};
|
|
811
|
-
const isPythonDependencyManifest = (file) => pythonDependencyManifestKind(file) !== void 0;
|
|
812
|
-
/** Docker definition names use dot-delimited variants, excluding source and documentation. */
|
|
813
|
-
const isDockerDefinitionPath = (file) => {
|
|
814
|
-
if (excludedInputPath(file)) return false;
|
|
815
|
-
const name = file.split("/").at(-1) ?? "";
|
|
816
|
-
if (/\.(?:[cm]?[jt]sx?|mdx?|rst|txt|html?|py|rs|go|java|sh|c|cc|cpp|cxx|h|hpp|cs|rb|php|swift|kt|css|scss)$/i.test(name)) return false;
|
|
817
|
-
return /^(?:[^./]+\.)*(?:dockerfile|containerfile)(?:\.[^./]+)*$/i.test(name);
|
|
818
|
-
};
|
|
819
|
-
/** Lockfiles establish ecosystem presence without reading their dependency graphs. */
|
|
820
|
-
const toolchainInputKind = (file) => {
|
|
821
|
-
if (file === ".agents/cloud-setup.sh") return "config";
|
|
822
|
-
if (githubAutomationFileKind(file) !== void 0) return "config";
|
|
823
|
-
if (excludedInputPath(file)) return void 0;
|
|
824
|
-
if (pythonDependencyManifestKind(file) === "pipfile-lock" || javascriptDependencyLockfiles.some((name) => file.split("/").at(-1) === name) || /(?:^|\/)uv\.lock$/.test(file)) return "presence";
|
|
825
|
-
if (githubAutomationFileKind(file) !== void 0 || isMiseConfigPath(file) || isPythonDependencyManifest(file) || isDockerDefinitionPath(file) || isComposeDefinitionPath(file) || isKubernetesDefinitionPath(file) || /(?:^|\/)(?:package\.json|pnpm-workspace\.yaml|stll-toolchain\.json|\.bun-version|\.node-version|\.nvmrc|\.python-version|rust-toolchain(?:\.toml)?|\.tool-versions|\.?mise\.toml|uv\.toml|\.uv\.toml|\.github\/dependabot\.ya?ml|Cargo\.toml)$/.test(file)) return "config";
|
|
826
|
-
};
|
|
827
|
-
//#endregion
|
|
828
|
-
//#region src/toolchain-workspaces.ts
|
|
829
|
-
/** Match manifest directories with the same positive and negative workspace patterns. */
|
|
830
|
-
const workspaceContains = ({ directory, file, patterns, rootMembership }) => {
|
|
831
|
-
const relative = path.posix.relative(directory, path.posix.dirname(file));
|
|
832
|
-
if (relative === "" && rootMembership === "implicit") return true;
|
|
833
|
-
if (relative === ".." || relative.startsWith("../")) return false;
|
|
834
|
-
if (!Array.isArray(patterns)) return false;
|
|
835
|
-
const normalize = (pattern) => path.posix.normalize(pattern);
|
|
836
|
-
const selected = patterns.filter((value) => typeof value === "string");
|
|
837
|
-
return picomatch(selected.filter((pattern) => !pattern.startsWith("!")).map(normalize))(relative) && !picomatch(selected.filter((pattern) => pattern.startsWith("!")).map((pattern) => normalize(pattern.slice(1))))(relative);
|
|
838
|
-
};
|
|
839
|
-
//#endregion
|
|
840
260
|
//#region src/toolchain-dependabot.ts
|
|
841
261
|
const dependabotRules = ["dependabot-policy"];
|
|
842
262
|
const record$5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -893,7 +313,7 @@ const workspaceRoots = (files, ecosystem) => {
|
|
|
893
313
|
}
|
|
894
314
|
}
|
|
895
315
|
try {
|
|
896
|
-
const parsed = ecosystem === "npm" ? JSON.parse(files[file] ?? "") : parse(files[file] ?? "");
|
|
316
|
+
const parsed = ecosystem === "npm" ? JSON.parse(files[file] ?? "") : parse$2(files[file] ?? "");
|
|
897
317
|
if (!record$5(parsed)) continue;
|
|
898
318
|
if (ecosystem === "npm" && typeof parsed["packageManager"] === "string" && parsed["packageManager"].startsWith("pnpm@")) continue;
|
|
899
319
|
const value = workspaceTable(parsed, ecosystem);
|
|
@@ -949,8 +369,8 @@ const workspaceRoots = (files, ecosystem) => {
|
|
|
949
369
|
if (includes.some((pattern) => picomatch.isMatch(relative, pattern)) && !excludes.some((pattern) => picomatch.isMatch(relative, pattern.slice(1)))) memberDirectories.add(directory);
|
|
950
370
|
}
|
|
951
371
|
for (const directory of memberDirectories) try {
|
|
952
|
-
const parsed = parse(files[path.posix.join(directory, "Cargo.toml")] ?? "");
|
|
953
|
-
const workspace = parse(files[path.posix.join(root, "Cargo.toml")] ?? "")["workspace"];
|
|
372
|
+
const parsed = parse$2(files[path.posix.join(directory, "Cargo.toml")] ?? "");
|
|
373
|
+
const workspace = parse$2(files[path.posix.join(root, "Cargo.toml")] ?? "")["workspace"];
|
|
954
374
|
const inherited = record$5(workspace) ? workspace["dependencies"] : void 0;
|
|
955
375
|
const dependencyOwners = [parsed];
|
|
956
376
|
const targets = parsed["target"];
|
|
@@ -1007,7 +427,7 @@ const pythonEcosystem = (file, files) => {
|
|
|
1007
427
|
if (files[path.posix.join(root, "uv.lock")] !== void 0 || files[path.posix.join(root, "uv.toml")] !== void 0) return "uv";
|
|
1008
428
|
const pyproject = files[path.posix.join(root, "pyproject.toml")];
|
|
1009
429
|
if (pyproject !== void 0) try {
|
|
1010
|
-
const tool = parse(pyproject)["tool"];
|
|
430
|
+
const tool = parse$2(pyproject)["tool"];
|
|
1011
431
|
if (record$5(tool) && record$5(tool["uv"])) return "uv";
|
|
1012
432
|
} catch {}
|
|
1013
433
|
return "pip";
|
|
@@ -1160,8 +580,57 @@ const checkDependabot = ({ files, policy }) => {
|
|
|
1160
580
|
return diagnostics;
|
|
1161
581
|
};
|
|
1162
582
|
//#endregion
|
|
1163
|
-
//#region src/toolchain-
|
|
583
|
+
//#region src/toolchain-dynamic-selectors.ts
|
|
584
|
+
const locatorValidators = {
|
|
585
|
+
image: /^(?:jobs\.[\w-]+\.(?:container|services\.[\w-]+)|services\.[\w.-]+\.image)$/,
|
|
586
|
+
"bun-source": /^(?:jobs\.[\w-]+|runs)\.steps\.[\w-]+$/
|
|
587
|
+
};
|
|
588
|
+
const knownKind = (value) => typeof value === "string" && Object.hasOwn(locatorValidators, value);
|
|
1164
589
|
const record$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
590
|
+
/** One reviewed declaration identifies one unresolved selector, never a rule. */
|
|
591
|
+
const parseDynamicSelectors = (input) => {
|
|
592
|
+
if (input === void 0) return [];
|
|
593
|
+
if (!Array.isArray(input)) throw new Error("dynamicSelectors must be an array");
|
|
594
|
+
const result = [];
|
|
595
|
+
const keys = /* @__PURE__ */ new Set();
|
|
596
|
+
for (const entry of input) {
|
|
597
|
+
if (!record$4(entry) || Object.keys(entry).some((key) => ![
|
|
598
|
+
"path",
|
|
599
|
+
"kind",
|
|
600
|
+
"reason",
|
|
601
|
+
"at",
|
|
602
|
+
"line"
|
|
603
|
+
].includes(key)) || typeof entry["path"] !== "string" || entry["path"] === "" || /[\\:$*?]|[^\S ]/.test(entry["path"]) || entry["path"].split("/").some((part) => part === "" || part === "." || part === "..") || typeof entry["reason"] !== "string" || entry["reason"].trim() === "") throw new Error("each dynamic selector requires an exact repository path, known kind and nonempty reason");
|
|
604
|
+
const kind = entry["kind"];
|
|
605
|
+
if (!knownKind(kind)) throw new Error("unknown dynamic selector kind");
|
|
606
|
+
let selector;
|
|
607
|
+
if (Object.hasOwn(entry, "line") && !Object.hasOwn(entry, "at") && typeof entry["line"] === "number" && Number.isSafeInteger(entry["line"]) && entry["line"] > 0) selector = {
|
|
608
|
+
path: entry["path"],
|
|
609
|
+
kind,
|
|
610
|
+
reason: entry["reason"],
|
|
611
|
+
line: entry["line"]
|
|
612
|
+
};
|
|
613
|
+
else if (!Object.hasOwn(entry, "line") && typeof entry["at"] === "string" && locatorValidators[kind].test(entry["at"])) selector = {
|
|
614
|
+
path: entry["path"],
|
|
615
|
+
kind,
|
|
616
|
+
reason: entry["reason"],
|
|
617
|
+
at: entry["at"]
|
|
618
|
+
};
|
|
619
|
+
else throw new Error("dynamic selector requires one positive line or a kind-specific job/service/step ID locator");
|
|
620
|
+
const key = JSON.stringify([
|
|
621
|
+
selector.path,
|
|
622
|
+
selector.kind,
|
|
623
|
+
selector.at ?? selector.line
|
|
624
|
+
]);
|
|
625
|
+
if (keys.has(key)) throw new Error("dynamic selector locations must be unique");
|
|
626
|
+
keys.add(key);
|
|
627
|
+
result.push(selector);
|
|
628
|
+
}
|
|
629
|
+
return result;
|
|
630
|
+
};
|
|
631
|
+
//#endregion
|
|
632
|
+
//#region src/toolchain-engine-floors.ts
|
|
633
|
+
const record$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1165
634
|
const repositoryPath = (value) => typeof value === "string" && value !== "" && !/^[A-Za-z]:/.test(value) && !/[\\\s]/.test(value) && !Array.from(value).some((character) => character.charCodeAt(0) < 32) && value.split("/").every((part) => part !== "" && part !== "." && part !== "..");
|
|
1166
635
|
const parseEngineFloors = (input) => {
|
|
1167
636
|
if (input === void 0) return [];
|
|
@@ -1169,7 +638,7 @@ const parseEngineFloors = (input) => {
|
|
|
1169
638
|
const result = [];
|
|
1170
639
|
const jobs = /* @__PURE__ */ new Set();
|
|
1171
640
|
for (const entry of input) {
|
|
1172
|
-
if (!record$
|
|
641
|
+
if (!record$3(entry) || Object.keys(entry).some((key) => ![
|
|
1173
642
|
"package",
|
|
1174
643
|
"workflow",
|
|
1175
644
|
"job"
|
|
@@ -1190,16 +659,16 @@ const resolveEngineFloor = (entry, files) => {
|
|
|
1190
659
|
const workflowText = files[entry.workflow];
|
|
1191
660
|
if (manifestText === void 0 || workflowText === void 0) throw new Error("engine floor package and workflow must be tracked");
|
|
1192
661
|
const manifest = JSON.parse(manifestText);
|
|
1193
|
-
if (!record$
|
|
662
|
+
if (!record$3(manifest) || manifest["private"] === true || typeof manifest["name"] !== "string" || manifest["name"].trim() === "" || !record$3(manifest["engines"]) || typeof manifest["engines"]["node"] !== "string") throw new Error("engine floor package must be published and declare engines.node");
|
|
1194
663
|
const range = manifest["engines"]["node"];
|
|
1195
664
|
const minimum = validRange(range) === null ? null : minVersion(range);
|
|
1196
665
|
if (minimum === null || minimum.prerelease.length !== 0) throw new Error("engine floor engines.node must declare a stable minimum");
|
|
1197
666
|
const document = parseDocument(workflowText, { merge: true });
|
|
1198
667
|
if (document.errors.length !== 0) throw new Error("engine floor workflow must be valid YAML");
|
|
1199
668
|
const workflow = document.toJS({ maxAliasCount: 100 });
|
|
1200
|
-
if (!record$
|
|
669
|
+
if (!record$3(workflow) || !record$3(workflow["jobs"])) throw new Error("engine floor job must exist in its tracked workflow");
|
|
1201
670
|
const job = workflow["jobs"][entry.job];
|
|
1202
|
-
if (!record$
|
|
671
|
+
if (!record$3(job)) throw new Error("engine floor job must exist in its tracked workflow");
|
|
1203
672
|
if (!Array.isArray(job["steps"]) || Object.hasOwn(job, "uses")) throw new Error("engine floor job must declare steps and cannot reuse a workflow");
|
|
1204
673
|
return {
|
|
1205
674
|
...entry,
|
|
@@ -1219,7 +688,7 @@ const packageRules = [
|
|
|
1219
688
|
"typescript-layout",
|
|
1220
689
|
"node-engine"
|
|
1221
690
|
];
|
|
1222
|
-
const record$
|
|
691
|
+
const record$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1223
692
|
const shellSubstitutions = (command) => {
|
|
1224
693
|
const frames = [{
|
|
1225
694
|
kind: "root",
|
|
@@ -1284,7 +753,7 @@ const compilerCommandSegments = (command) => {
|
|
|
1284
753
|
const segments = [[]];
|
|
1285
754
|
for (const source of [command, ...shellSubstitutions(command)]) {
|
|
1286
755
|
segments.push([]);
|
|
1287
|
-
const shellWords = source.match(/(?:[^\s"'\\;&|()]|\\[
|
|
756
|
+
const shellWords = source.match(/(?:[^\s"'\\;&|()]|\\[\s\S]|"(?:[^"\\]|\\[\s\S])*"|'[^']*')+|&&|\|\||[();&|\n]/g);
|
|
1288
757
|
for (const word of shellWords ?? []) if (/^(?:&&|\|\||[;&|\n])$/.test(word)) segments.push([]);
|
|
1289
758
|
else segments.at(-1)?.push(word);
|
|
1290
759
|
}
|
|
@@ -1371,29 +840,29 @@ const launcherValueOptions = new Map(Object.entries({
|
|
|
1371
840
|
"--input-type"
|
|
1372
841
|
]
|
|
1373
842
|
}).map(([launcher, options]) => [launcher, new Set(options)]));
|
|
1374
|
-
const
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
continue;
|
|
1383
|
-
}
|
|
1384
|
-
if (quote === void 0 && (character === "'" || character === "\"")) {
|
|
1385
|
-
quote = character;
|
|
1386
|
-
continue;
|
|
1387
|
-
}
|
|
1388
|
-
const next = word.at(index + 1);
|
|
1389
|
-
if (character === "\\" && next !== void 0 && quote !== "'" && (quote === void 0 || /[$`"\\\n]/.test(next))) {
|
|
1390
|
-
result += next;
|
|
1391
|
-
index += 1;
|
|
1392
|
-
} else result += character;
|
|
843
|
+
const decodeShellWord = (word) => {
|
|
844
|
+
let result = "";
|
|
845
|
+
let quote;
|
|
846
|
+
for (let index = 0; index < word.length; index += 1) {
|
|
847
|
+
const character = word.at(index);
|
|
848
|
+
if (character === quote) {
|
|
849
|
+
quote = void 0;
|
|
850
|
+
continue;
|
|
1393
851
|
}
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
852
|
+
if (quote === void 0 && (character === "'" || character === "\"")) {
|
|
853
|
+
quote = character;
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
const next = word.at(index + 1);
|
|
857
|
+
if (character === "\\" && next !== void 0 && quote !== "'" && (quote === void 0 || /[$`"\\\n]/.test(next))) {
|
|
858
|
+
if (next !== "\n") result += next;
|
|
859
|
+
index += 1;
|
|
860
|
+
} else result += character;
|
|
861
|
+
}
|
|
862
|
+
return result;
|
|
863
|
+
};
|
|
864
|
+
const invokesCompiler = (words) => {
|
|
865
|
+
const tokens = words.map(decodeShellWord);
|
|
1397
866
|
const compiler = (word) => /(?:^|\/)(?:tsc|tsgo)(?:\.js)?$/.test(word);
|
|
1398
867
|
let start = 0;
|
|
1399
868
|
const skipOptions = (index, launcher) => {
|
|
@@ -1450,10 +919,10 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1450
919
|
const document = pnpm ? parseDocument(text, { lineCounter }) : void 0;
|
|
1451
920
|
if (document !== void 0 && document.errors.length > 0) throw new Error("invalid YAML");
|
|
1452
921
|
const json = document === void 0 ? JSON.parse(text) : document.toJS();
|
|
1453
|
-
if (!record$
|
|
922
|
+
if (!record$2(json)) throw new Error("manifest must be an object");
|
|
1454
923
|
if (pnpm) {
|
|
1455
|
-
if (json["catalog"] !== void 0 && !record$
|
|
1456
|
-
if (json["catalogs"] !== void 0 && (!record$
|
|
924
|
+
if (json["catalog"] !== void 0 && !record$2(json["catalog"])) throw new Error("catalog must be a mapping");
|
|
925
|
+
if (json["catalogs"] !== void 0 && (!record$2(json["catalogs"]) || Object.values(json["catalogs"]).some((catalog) => !record$2(catalog)))) throw new Error("named catalogs must be mappings");
|
|
1457
926
|
if (json["packages"] !== void 0 && (!Array.isArray(json["packages"]) || !json["packages"].every((pattern) => typeof pattern === "string"))) throw new Error("workspace packages must be patterns");
|
|
1458
927
|
}
|
|
1459
928
|
manifests.set(file, json);
|
|
@@ -1467,13 +936,13 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1467
936
|
catalog: json["overrides"],
|
|
1468
937
|
prefix: ["overrides"]
|
|
1469
938
|
},
|
|
1470
|
-
...record$
|
|
939
|
+
...record$2(json["catalogs"]) ? Object.entries(json["catalogs"]).map(([name, catalog]) => ({
|
|
1471
940
|
catalog,
|
|
1472
941
|
prefix: ["catalogs", name]
|
|
1473
942
|
})) : []
|
|
1474
943
|
];
|
|
1475
944
|
for (const { catalog, prefix } of catalogEntries) {
|
|
1476
|
-
if (!record$
|
|
945
|
+
if (!record$2(catalog)) continue;
|
|
1477
946
|
for (const [name, value] of Object.entries(catalog)) {
|
|
1478
947
|
const node = document.getIn([...prefix, name], true);
|
|
1479
948
|
if (!isNode(node) || node.range === void 0 || node.range === null) continue;
|
|
@@ -1492,7 +961,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1492
961
|
});
|
|
1493
962
|
}
|
|
1494
963
|
}
|
|
1495
|
-
const catalogSources = (json) => [json, ...record$
|
|
964
|
+
const catalogSources = (json) => [json, ...record$2(json["workspaces"]) ? [json["workspaces"]] : []];
|
|
1496
965
|
const pnpmWorkspaceFor = (file) => {
|
|
1497
966
|
let directory = path.posix.dirname(file);
|
|
1498
967
|
while (true) {
|
|
@@ -1515,8 +984,8 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1515
984
|
patterns: workspace["packages"],
|
|
1516
985
|
rootMembership: "implicit"
|
|
1517
986
|
})) return void 0;
|
|
1518
|
-
if (name === "" || name === "default") return workspace["catalog"] ?? (record$
|
|
1519
|
-
return record$
|
|
987
|
+
if (name === "" || name === "default") return workspace["catalog"] ?? (record$2(workspace["catalogs"]) ? workspace["catalogs"]["default"] : void 0);
|
|
988
|
+
return record$2(workspace["catalogs"]) ? workspace["catalogs"][name] : void 0;
|
|
1520
989
|
}
|
|
1521
990
|
let directory = path.posix.dirname(file);
|
|
1522
991
|
while (true) {
|
|
@@ -1524,7 +993,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1524
993
|
if (manifest !== void 0) {
|
|
1525
994
|
for (const source of catalogSources(manifest)) {
|
|
1526
995
|
if (name === "" && source["catalog"] !== void 0) return source["catalog"];
|
|
1527
|
-
if (record$
|
|
996
|
+
if (record$2(source["catalogs"]) && source["catalogs"][name] !== void 0) return source["catalogs"][name];
|
|
1528
997
|
}
|
|
1529
998
|
if (manifest["workspaces"] !== void 0) return void 0;
|
|
1530
999
|
}
|
|
@@ -1538,7 +1007,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1538
1007
|
if (visited.has(value)) return void 0;
|
|
1539
1008
|
visited.add(value);
|
|
1540
1009
|
const catalog = catalogFor(file, value.slice(8));
|
|
1541
|
-
value = record$
|
|
1010
|
+
value = record$2(catalog) ? catalog[name] : void 0;
|
|
1542
1011
|
}
|
|
1543
1012
|
return value;
|
|
1544
1013
|
};
|
|
@@ -1554,7 +1023,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1554
1023
|
const workspaces = manifests.get(path.posix.join(directory, "package.json"))?.["workspaces"];
|
|
1555
1024
|
if (workspaces !== void 0) return {
|
|
1556
1025
|
directory,
|
|
1557
|
-
patterns: record$
|
|
1026
|
+
patterns: record$2(workspaces) ? workspaces["packages"] : workspaces,
|
|
1558
1027
|
source: "json"
|
|
1559
1028
|
};
|
|
1560
1029
|
if (directory === ".") return void 0;
|
|
@@ -1610,7 +1079,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1610
1079
|
"peerDependencies"
|
|
1611
1080
|
]) {
|
|
1612
1081
|
const dependencies = json[section];
|
|
1613
|
-
if (!record$
|
|
1082
|
+
if (!record$2(dependencies)) continue;
|
|
1614
1083
|
for (const [name, value] of Object.entries(dependencies)) {
|
|
1615
1084
|
if (!tsSpecifiers.has(name)) continue;
|
|
1616
1085
|
const resolved = resolve(file, name, value);
|
|
@@ -1672,7 +1141,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1672
1141
|
message: `packageManager Bun version must be ${policy.bun}, found ${JSON.stringify(manager)}`
|
|
1673
1142
|
});
|
|
1674
1143
|
const checkEntries = (entries, location) => {
|
|
1675
|
-
if (!record$
|
|
1144
|
+
if (!record$2(entries)) return;
|
|
1676
1145
|
for (const [name, raw] of Object.entries(entries)) {
|
|
1677
1146
|
const value = resolve(file, name, raw);
|
|
1678
1147
|
const tsAllowed = tsSpecifiers.get(name);
|
|
@@ -1709,12 +1178,12 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1709
1178
|
"devDependencies",
|
|
1710
1179
|
"optionalDependencies"
|
|
1711
1180
|
]) checkEntries(json[key]);
|
|
1712
|
-
if (record$
|
|
1181
|
+
if (record$2(json["peerDependencies"])) {
|
|
1713
1182
|
for (const [name, value] of Object.entries(json["peerDependencies"])) if (tsSpecifiers.has(name) && !peerRange(resolve(file, name, value))) checkEntries({ [name]: value });
|
|
1714
1183
|
}
|
|
1715
1184
|
for (const source of catalogSources(json)) {
|
|
1716
1185
|
checkEntries(source["catalog"]);
|
|
1717
|
-
if (!record$
|
|
1186
|
+
if (!record$2(source["catalogs"])) continue;
|
|
1718
1187
|
for (const catalog of Object.values(source["catalogs"])) checkEntries(catalog);
|
|
1719
1188
|
}
|
|
1720
1189
|
const ownedNames = /* @__PURE__ */ new Set([...pins.keys(), ...tsSpecifiers.keys()]);
|
|
@@ -1726,7 +1195,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1726
1195
|
"optionalDependencies"
|
|
1727
1196
|
]) {
|
|
1728
1197
|
const entries = resolutionRoot[section];
|
|
1729
|
-
if (!record$
|
|
1198
|
+
if (!record$2(entries)) continue;
|
|
1730
1199
|
for (const [name, value] of Object.entries(entries)) resolutionDependencies[name] = resolve(file, name, value);
|
|
1731
1200
|
}
|
|
1732
1201
|
const affectedSpecifiers = /* @__PURE__ */ new Map();
|
|
@@ -1746,7 +1215,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1746
1215
|
"optionalDependencies"
|
|
1747
1216
|
]) {
|
|
1748
1217
|
const entries = member[section];
|
|
1749
|
-
if (!record$
|
|
1218
|
+
if (!record$2(entries)) continue;
|
|
1750
1219
|
for (const [name, value] of Object.entries(entries)) {
|
|
1751
1220
|
if (!tsSpecifiers.has(name)) continue;
|
|
1752
1221
|
const values = affectedSpecifiers.get(name) ?? /* @__PURE__ */ new Set();
|
|
@@ -1756,7 +1225,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1756
1225
|
}
|
|
1757
1226
|
}
|
|
1758
1227
|
const checkResolutionMap = (entries, parent) => {
|
|
1759
|
-
if (!record$
|
|
1228
|
+
if (!record$2(entries)) return;
|
|
1760
1229
|
for (const [selector, replacement] of Object.entries(entries)) {
|
|
1761
1230
|
const target = selector === "." ? parent : selector;
|
|
1762
1231
|
if (target === void 0) continue;
|
|
@@ -1779,7 +1248,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1779
1248
|
});
|
|
1780
1249
|
continue;
|
|
1781
1250
|
}
|
|
1782
|
-
if (record$
|
|
1251
|
+
if (record$2(replacement)) {
|
|
1783
1252
|
checkResolutionMap(replacement, target);
|
|
1784
1253
|
continue;
|
|
1785
1254
|
}
|
|
@@ -1813,7 +1282,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1813
1282
|
};
|
|
1814
1283
|
checkResolutionMap(json["overrides"]);
|
|
1815
1284
|
checkResolutionMap(json["resolutions"]);
|
|
1816
|
-
if (record$
|
|
1285
|
+
if (record$2(json["pnpm"])) checkResolutionMap(json["pnpm"]["overrides"]);
|
|
1817
1286
|
const localEntries = toolchainEntries(file, json);
|
|
1818
1287
|
const usesCompiler = policy.typescriptInstallLayouts.some((layout) => localEntries.some(({ name }) => name === layout.compilerPackage));
|
|
1819
1288
|
let selectedLayout = layoutFor(specifierMap(localEntries));
|
|
@@ -1837,7 +1306,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1837
1306
|
key: "dependencies",
|
|
1838
1307
|
message: "TypeScript compiler and compatibility packages must match one complete declared install layout"
|
|
1839
1308
|
});
|
|
1840
|
-
const command = record$
|
|
1309
|
+
const command = record$2(json["scripts"]) ? json["scripts"]["typecheck"] : void 0;
|
|
1841
1310
|
if (selectedLayout !== void 0 && typeof command === "string") {
|
|
1842
1311
|
const normalizedExpected = selectedLayout.typecheckCommand.replace(/\s+/g, " ").trim();
|
|
1843
1312
|
for (const words of compilerCommandSegments(command)) {
|
|
@@ -1855,7 +1324,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1855
1324
|
}
|
|
1856
1325
|
}
|
|
1857
1326
|
}
|
|
1858
|
-
if (record$
|
|
1327
|
+
if (record$2(json["peerDependencies"])) {
|
|
1859
1328
|
const peerLayout = selectedLayout ?? workspaceLayoutFor(file)?.layout;
|
|
1860
1329
|
for (const [name, raw] of Object.entries(json["peerDependencies"])) {
|
|
1861
1330
|
if (!tsSpecifiers.has(name)) continue;
|
|
@@ -1879,7 +1348,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1879
1348
|
});
|
|
1880
1349
|
}
|
|
1881
1350
|
}
|
|
1882
|
-
if (record$
|
|
1351
|
+
if (record$2(json["engines"]) && json["engines"]["node"] !== void 0 && (typeof json["engines"]["node"] !== "string" || !nodeSupportRangeMatches(policy.node, json["engines"]["node"]))) add({
|
|
1883
1352
|
file,
|
|
1884
1353
|
rule: "node-engine",
|
|
1885
1354
|
key: "node",
|
|
@@ -1901,7 +1370,7 @@ const runtimeRules = [
|
|
|
1901
1370
|
"runtime-workflow",
|
|
1902
1371
|
"action-pins"
|
|
1903
1372
|
];
|
|
1904
|
-
const record$
|
|
1373
|
+
const record$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1905
1374
|
const pythonSelectorMatches = (value, expected) => value === expected || typeof value === "string" && /^\d+\.\d+$/.test(expected) && value.startsWith(`${expected}.`) && /^\d+\.\d+\.\d+$/.test(value);
|
|
1906
1375
|
const compareRelease = (left, right) => {
|
|
1907
1376
|
for (let index = 0; index < Math.max(left.length, right.length); index++) {
|
|
@@ -1971,7 +1440,7 @@ const pythonRequirementMatches = (requirement, version) => {
|
|
|
1971
1440
|
});
|
|
1972
1441
|
};
|
|
1973
1442
|
/** Check runtime declarations only in the files that declare or configure them. */
|
|
1974
|
-
const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, repository, onDelegated, engineFloors = [], onEngineFloor }) => {
|
|
1443
|
+
const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, repository, onDelegated, dynamicSelectors = [], onDynamicSelector, engineFloors = [], onEngineFloor }) => {
|
|
1975
1444
|
const diagnostics = [];
|
|
1976
1445
|
const name = path.posix.basename(file);
|
|
1977
1446
|
const lines = text.split(/\r?\n/);
|
|
@@ -1983,6 +1452,16 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
1983
1452
|
message
|
|
1984
1453
|
});
|
|
1985
1454
|
};
|
|
1455
|
+
let currentLocation;
|
|
1456
|
+
const dynamic = (value) => typeof value === "string" && value.includes("$");
|
|
1457
|
+
const authorizeDynamic = (kind, line, at = currentLocation) => {
|
|
1458
|
+
const matches = dynamicSelectors.filter((entry) => entry.path === file && entry.kind === kind && (entry.at !== void 0 ? entry.at === at : entry.line === line));
|
|
1459
|
+
if (matches.length !== 1) return false;
|
|
1460
|
+
const entry = matches.at(0);
|
|
1461
|
+
if (entry === void 0) return false;
|
|
1462
|
+
onDynamicSelector?.(entry);
|
|
1463
|
+
return true;
|
|
1464
|
+
};
|
|
1986
1465
|
const lineOf = (key) => Math.max(1, lines.findIndex((line) => line.includes(key)) + 1);
|
|
1987
1466
|
const pin = ({ rule, value, expected, label, line = 1, selector }) => {
|
|
1988
1467
|
let matches = value === expected;
|
|
@@ -1996,7 +1475,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
1996
1475
|
};
|
|
1997
1476
|
const toml = (rule) => {
|
|
1998
1477
|
try {
|
|
1999
|
-
return parse(text);
|
|
1478
|
+
return parse$2(text);
|
|
2000
1479
|
} catch {
|
|
2001
1480
|
add({
|
|
2002
1481
|
rule,
|
|
@@ -2055,11 +1534,11 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2055
1534
|
const parsed = toml("rust-version");
|
|
2056
1535
|
if (parsed !== void 0) {
|
|
2057
1536
|
const toolchain = parsed["toolchain"];
|
|
2058
|
-
const components = record$
|
|
1537
|
+
const components = record$1(toolchain) ? toolchain["components"] : void 0;
|
|
2059
1538
|
const expected = Array.isArray(components) && components.includes("rustc-dev") ? policy.rustCompilerDevelopment : policy.rust;
|
|
2060
1539
|
pin({
|
|
2061
1540
|
rule: "rust-version",
|
|
2062
|
-
value: record$
|
|
1541
|
+
value: record$1(toolchain) ? toolchain["channel"] : void 0,
|
|
2063
1542
|
expected,
|
|
2064
1543
|
label: "Rust channel",
|
|
2065
1544
|
line: lineOf("channel")
|
|
@@ -2080,21 +1559,21 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2080
1559
|
if (candidate === file) return parsed;
|
|
2081
1560
|
if (!trackedFiles.has(candidate)) return void 0;
|
|
2082
1561
|
try {
|
|
2083
|
-
return parse(readFile(candidate) ?? "");
|
|
1562
|
+
return parse$2(readFile(candidate) ?? "");
|
|
2084
1563
|
} catch {
|
|
2085
1564
|
return;
|
|
2086
1565
|
}
|
|
2087
1566
|
};
|
|
2088
1567
|
if (parsed !== void 0) {
|
|
2089
1568
|
const workspace = parsed["workspace"];
|
|
2090
|
-
const defaults = record$
|
|
2091
|
-
if (record$
|
|
1569
|
+
const defaults = record$1(workspace) ? workspace["package"] : void 0;
|
|
1570
|
+
if (record$1(defaults) && defaults["rust-version"] !== void 0) rustFloor(defaults["rust-version"]);
|
|
2092
1571
|
const pkg = parsed["package"];
|
|
2093
|
-
const version = record$
|
|
2094
|
-
if (version !== void 0) if (!record$
|
|
1572
|
+
const version = record$1(pkg) ? pkg["rust-version"] : void 0;
|
|
1573
|
+
if (version !== void 0) if (!record$1(version)) rustFloor(version);
|
|
2095
1574
|
else {
|
|
2096
1575
|
let inherited;
|
|
2097
|
-
if (version["workspace"] === true && record$
|
|
1576
|
+
if (version["workspace"] === true && record$1(pkg)) {
|
|
2098
1577
|
const explicit = pkg["workspace"];
|
|
2099
1578
|
let directory = path.posix.dirname(file);
|
|
2100
1579
|
const explicitPath = typeof explicit === "string" && !explicit.includes("\\") && !explicit.includes(":") && !explicit.includes("${{") && !path.posix.isAbsolute(explicit) ? path.posix.normalize(path.posix.join(directory, explicit)) : void 0;
|
|
@@ -2102,9 +1581,9 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2102
1581
|
if (explicitPath !== void 0) directory = explicitPath;
|
|
2103
1582
|
while (true) {
|
|
2104
1583
|
const table = readManifest(path.posix.join(directory, "Cargo.toml"))?.["workspace"];
|
|
2105
|
-
if (record$
|
|
1584
|
+
if (record$1(table)) {
|
|
2106
1585
|
const fields = table["package"];
|
|
2107
|
-
inherited = record$
|
|
1586
|
+
inherited = record$1(fields) ? fields["rust-version"] : void 0;
|
|
2108
1587
|
break;
|
|
2109
1588
|
}
|
|
2110
1589
|
if (explicit !== void 0 || directory === ".") break;
|
|
@@ -2118,7 +1597,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2118
1597
|
}
|
|
2119
1598
|
if (name === "Pipfile") {
|
|
2120
1599
|
const requires = toml("python-version")?.["requires"];
|
|
2121
|
-
if (record$
|
|
1600
|
+
if (record$1(requires)) {
|
|
2122
1601
|
const minor = requires["python_version"];
|
|
2123
1602
|
if (minor !== void 0 && minor !== policy.python.split(".").slice(0, 2).join(".")) add({
|
|
2124
1603
|
rule: "python-version",
|
|
@@ -2137,7 +1616,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2137
1616
|
const parsed = toml("python-version");
|
|
2138
1617
|
if (parsed !== void 0) {
|
|
2139
1618
|
const project = parsed["project"];
|
|
2140
|
-
if (record$
|
|
1619
|
+
if (record$1(project) && project["requires-python"] !== void 0) {
|
|
2141
1620
|
if (!pythonRequirementMatches(project["requires-python"], policy.python)) add({
|
|
2142
1621
|
rule: "python-version",
|
|
2143
1622
|
line: lineOf("requires-python"),
|
|
@@ -2145,9 +1624,9 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2145
1624
|
});
|
|
2146
1625
|
}
|
|
2147
1626
|
const tool = parsed["tool"];
|
|
2148
|
-
const configuredUv = record$
|
|
1627
|
+
const configuredUv = record$1(tool) ? tool["uv"] : void 0;
|
|
2149
1628
|
const uv = name === "uv.toml" ? parsed : configuredUv;
|
|
2150
|
-
if (record$
|
|
1629
|
+
if (record$1(uv) && uv["python"] !== void 0) pin({
|
|
2151
1630
|
rule: "python-version",
|
|
2152
1631
|
value: uv["python"],
|
|
2153
1632
|
expected: policy.python,
|
|
@@ -2158,11 +1637,11 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2158
1637
|
}
|
|
2159
1638
|
if (isMiseConfigPath(file)) {
|
|
2160
1639
|
const tools = toml("runtime-manager")?.["tools"];
|
|
2161
|
-
if (record$
|
|
1640
|
+
if (record$1(tools)) for (const [tool, expected] of Object.entries(versions)) {
|
|
2162
1641
|
const configured = tools[tool];
|
|
2163
1642
|
if (configured !== void 0) pin({
|
|
2164
1643
|
rule: "runtime-manager",
|
|
2165
|
-
value: record$
|
|
1644
|
+
value: record$1(configured) ? configured["version"] : configured,
|
|
2166
1645
|
expected,
|
|
2167
1646
|
selector: nodeTools.has(tool) ? "node" : void 0,
|
|
2168
1647
|
label: `mise ${tool}`,
|
|
@@ -2184,12 +1663,13 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2184
1663
|
line: index + 1
|
|
2185
1664
|
});
|
|
2186
1665
|
});
|
|
2187
|
-
const checkRuntimeImage = ({ image, line, label }) => {
|
|
1666
|
+
const checkRuntimeImage = ({ image, line, label, at }) => {
|
|
2188
1667
|
const unknownRuntime = () => {
|
|
1668
|
+
if (dynamic(image) && authorizeDynamic("image", line, at)) return;
|
|
2189
1669
|
add({
|
|
2190
1670
|
rule: "runtime-docker",
|
|
2191
1671
|
line,
|
|
2192
|
-
message: `cannot determine ${label} runtime; use a static image or a
|
|
1672
|
+
message: `cannot determine ${label} runtime; use a static image or a scoped dynamicSelectors declaration`
|
|
2193
1673
|
});
|
|
2194
1674
|
};
|
|
2195
1675
|
if (typeof image !== "string") {
|
|
@@ -2418,6 +1898,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2418
1898
|
checkRuntimeImage({
|
|
2419
1899
|
image: entry.image,
|
|
2420
1900
|
line,
|
|
1901
|
+
at: entry.path.join("."),
|
|
2421
1902
|
label: classified?.ecosystem === "docker-compose" ? "Compose image" : "Kubernetes image"
|
|
2422
1903
|
});
|
|
2423
1904
|
}
|
|
@@ -2445,15 +1926,29 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2445
1926
|
return diagnostics;
|
|
2446
1927
|
}
|
|
2447
1928
|
const nodeLine = (node) => {
|
|
2448
|
-
if (!record$
|
|
1929
|
+
if (!record$1(node) || !Array.isArray(node["range"])) return 1;
|
|
2449
1930
|
const offset = node["range"].at(0);
|
|
2450
1931
|
return typeof offset === "number" ? text.slice(0, offset).split("\n").length : 1;
|
|
2451
1932
|
};
|
|
2452
1933
|
let checkoutBindings = [];
|
|
2453
1934
|
let checkoutDestinations = /* @__PURE__ */ new Map();
|
|
2454
1935
|
let unknownCheckoutDestination = false;
|
|
1936
|
+
let unreviewableCheckoutSource = false;
|
|
2455
1937
|
const staticRepositoryPath = (value) => typeof value === "string" && value !== "" && !value.includes("${{") && !value.includes("\\") && !value.includes(":") && !path.posix.isAbsolute(value) && !value.split("/").includes("..");
|
|
2456
1938
|
const normalizeCheckoutPath = (prefix) => path.posix.normalize(prefix).replace(/\/$/, "") || ".";
|
|
1939
|
+
const checkoutProvidersFor = (selector) => {
|
|
1940
|
+
if (selector === void 0) return checkoutBindings;
|
|
1941
|
+
let deepest;
|
|
1942
|
+
for (const entry of checkoutBindings) {
|
|
1943
|
+
if (entry.path !== "." && selector !== entry.path && !selector.startsWith(`${entry.path}/`)) continue;
|
|
1944
|
+
if (deepest === void 0 || entry.path !== "." && (deepest.path === "." || entry.path.length > deepest.path.length)) deepest = entry;
|
|
1945
|
+
}
|
|
1946
|
+
return checkoutBindings.filter((entry) => entry.path === deepest?.path);
|
|
1947
|
+
};
|
|
1948
|
+
const hasAmbiguousCheckoutWriters = (selector, providers) => {
|
|
1949
|
+
if (providers.length > 0) return providers.some((entry) => (checkoutDestinations.get(entry.path) ?? 0) > 1);
|
|
1950
|
+
return [...checkoutDestinations].some(([prefix, count]) => count > 1 && (selector === void 0 || prefix === "." || selector === prefix || selector.startsWith(`${prefix}/`)));
|
|
1951
|
+
};
|
|
2457
1952
|
const checkReference = ({ value, tool, line }) => {
|
|
2458
1953
|
const allowed = {
|
|
2459
1954
|
node: [".node-version", ".nvmrc"],
|
|
@@ -2462,8 +1957,28 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2462
1957
|
}[tool];
|
|
2463
1958
|
const rule = tool === "bun" ? "bun-pins" : "runtime-workflow";
|
|
2464
1959
|
const selector = staticRepositoryPath(value) ? path.posix.normalize(value) : void 0;
|
|
2465
|
-
const
|
|
1960
|
+
const providers = checkoutProvidersFor(selector);
|
|
1961
|
+
const binding = selector === void 0 ? void 0 : providers.at(-1);
|
|
1962
|
+
const dynamicSource = providers.some((entry) => entry.dynamicSource);
|
|
1963
|
+
const sparseFailure = providers.find((entry) => {
|
|
1964
|
+
if (entry.sparse.mode === "invalid") return true;
|
|
1965
|
+
if (entry.sparse.mode !== "files" && entry.sparse.mode !== "mixed") return false;
|
|
1966
|
+
if (selector === void 0) return true;
|
|
1967
|
+
const selectedFile = entry.path === "." ? selector : path.posix.relative(entry.path, selector);
|
|
1968
|
+
return !entry.sparse.paths.has(selectedFile);
|
|
1969
|
+
});
|
|
1970
|
+
if (sparseFailure !== void 0) {
|
|
1971
|
+
let missingFile = "the selected version file";
|
|
1972
|
+
if (selector !== void 0) missingFile = sparseFailure.path === "." ? selector : path.posix.relative(sparseFailure.path, selector);
|
|
1973
|
+
add({
|
|
1974
|
+
rule,
|
|
1975
|
+
line,
|
|
1976
|
+
message: `checkout sparse-checkout must explicitly list ${missingFile} without invalid literal patterns`
|
|
1977
|
+
});
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
2466
1980
|
if (selector === void 0) {
|
|
1981
|
+
if (tool === "bun" && dynamic(value) && !unreviewableCheckoutSource && authorizeDynamic("bun-source", line)) return;
|
|
2467
1982
|
add({
|
|
2468
1983
|
rule,
|
|
2469
1984
|
line,
|
|
@@ -2471,7 +1986,8 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2471
1986
|
});
|
|
2472
1987
|
return;
|
|
2473
1988
|
}
|
|
2474
|
-
if (unknownCheckoutDestination ||
|
|
1989
|
+
if (unknownCheckoutDestination || hasAmbiguousCheckoutWriters(selector, providers)) {
|
|
1990
|
+
if (tool === "bun" && dynamicSource && !unreviewableCheckoutSource && authorizeDynamic("bun-source", line)) return;
|
|
2475
1991
|
add({
|
|
2476
1992
|
rule,
|
|
2477
1993
|
line,
|
|
@@ -2480,6 +1996,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2480
1996
|
return;
|
|
2481
1997
|
}
|
|
2482
1998
|
if (binding?.source === "untrusted") {
|
|
1999
|
+
if (tool === "bun" && dynamicSource && !unreviewableCheckoutSource && authorizeDynamic("bun-source", line)) return;
|
|
2483
2000
|
add({
|
|
2484
2001
|
rule,
|
|
2485
2002
|
line: binding.line,
|
|
@@ -2496,13 +2013,15 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2496
2013
|
});
|
|
2497
2014
|
return;
|
|
2498
2015
|
}
|
|
2499
|
-
if (binding !== void 0 && (binding.sparse.mode === "
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2016
|
+
if (binding !== void 0 && (binding.sparse.mode === "dynamic" || binding.sparse.mode === "mixed")) {
|
|
2017
|
+
if (tool !== "bun" || unreviewableCheckoutSource || !authorizeDynamic("bun-source", line)) {
|
|
2018
|
+
add({
|
|
2019
|
+
rule,
|
|
2020
|
+
line,
|
|
2021
|
+
message: `checkout sparse-checkout must explicitly list ${target} without dynamic, glob, or negation patterns`
|
|
2022
|
+
});
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
2506
2025
|
}
|
|
2507
2026
|
if (binding?.source === "delegated") {
|
|
2508
2027
|
onDelegated?.({
|
|
@@ -2521,7 +2040,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2521
2040
|
const manifest = JSON.parse(content ?? "");
|
|
2522
2041
|
pin({
|
|
2523
2042
|
rule,
|
|
2524
|
-
value: record$
|
|
2043
|
+
value: record$1(manifest) ? manifest["packageManager"] : void 0,
|
|
2525
2044
|
expected: `bun@${policy.bun}`,
|
|
2526
2045
|
label: "setup-bun version file packageManager",
|
|
2527
2046
|
line
|
|
@@ -2624,10 +2143,10 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2624
2143
|
return inputs;
|
|
2625
2144
|
};
|
|
2626
2145
|
const getInput = (node, key) => inputsOf(node).get(key.toUpperCase());
|
|
2627
|
-
const
|
|
2628
|
-
if (
|
|
2146
|
+
const expressionAt = (value, start) => {
|
|
2147
|
+
if (!value.startsWith("${{", start)) return void 0;
|
|
2629
2148
|
let quote;
|
|
2630
|
-
for (let index = 3; index < value.length; index++) {
|
|
2149
|
+
for (let index = start + 3; index < value.length; index++) {
|
|
2631
2150
|
const character = value.at(index);
|
|
2632
2151
|
if (quote !== void 0) {
|
|
2633
2152
|
if (character === quote) if (quote === "'" && value.at(index + 1) === "'") index++;
|
|
@@ -2637,9 +2156,33 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2637
2156
|
}
|
|
2638
2157
|
if (character === "'" || character === "\"") quote = character;
|
|
2639
2158
|
else if (value.startsWith("${{", index)) return void 0;
|
|
2640
|
-
else if (character === "}" && value.at(index + 1) === "}") return
|
|
2159
|
+
else if (character === "}" && value.at(index + 1) === "}") return {
|
|
2160
|
+
body: value.slice(start + 3, index).trim(),
|
|
2161
|
+
end: index + 2
|
|
2162
|
+
};
|
|
2641
2163
|
}
|
|
2642
2164
|
};
|
|
2165
|
+
const dynamicRefBody = (value) => {
|
|
2166
|
+
if (typeof value !== "string") return void 0;
|
|
2167
|
+
const expression = expressionAt(value, 0);
|
|
2168
|
+
return expression?.end === value.length ? expression.body : void 0;
|
|
2169
|
+
};
|
|
2170
|
+
const sparseEntryIsValid = (entry) => {
|
|
2171
|
+
let literalPath = "";
|
|
2172
|
+
let position = 0;
|
|
2173
|
+
while (position < entry.length) {
|
|
2174
|
+
const start = entry.indexOf("${{", position);
|
|
2175
|
+
const literal = entry.slice(position, start < 0 ? entry.length : start);
|
|
2176
|
+
if (/[!$*?[\]\\:]/.test(literal) || literal.split("/").includes("..")) return false;
|
|
2177
|
+
literalPath += literal;
|
|
2178
|
+
if (start < 0) break;
|
|
2179
|
+
const expression = expressionAt(entry, start);
|
|
2180
|
+
if (expression === void 0 || expression.body === "") return false;
|
|
2181
|
+
literalPath += "expression";
|
|
2182
|
+
position = expression.end;
|
|
2183
|
+
}
|
|
2184
|
+
return staticRepositoryPath(literalPath);
|
|
2185
|
+
};
|
|
2643
2186
|
const checkoutPrefixOf = (node) => {
|
|
2644
2187
|
const destination = getInput(node, "path");
|
|
2645
2188
|
if (destination === void 0) return ".";
|
|
@@ -2652,11 +2195,19 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2652
2195
|
const value = getInput(node, "sparse-checkout");
|
|
2653
2196
|
const cone = getInput(node, "sparse-checkout-cone-mode");
|
|
2654
2197
|
if (!isScalar(value) || typeof value.value !== "string" || cone !== void 0 && (!isScalar(cone) || cone.value !== true && cone.value !== false && cone.value !== "true" && cone.value !== "false")) return { mode: "invalid" };
|
|
2655
|
-
const paths = value.value.split(/\r?\n/).map((entry) => entry.trim()).filter((entry) => entry !== "");
|
|
2656
|
-
if (paths.length === 0 || paths.some((entry) => !
|
|
2198
|
+
const paths = value.value.split(/\r?\n/).map((entry) => entry.trim().replace(/^\//, "")).filter((entry) => entry !== "");
|
|
2199
|
+
if (paths.length === 0 || paths.some((entry) => !sparseEntryIsValid(entry))) return { mode: "invalid" };
|
|
2200
|
+
const literalPaths = new Set(paths.filter((entry) => !dynamic(entry)));
|
|
2201
|
+
if (paths.some(dynamic)) {
|
|
2202
|
+
if (literalPaths.size === 0) return { mode: "dynamic" };
|
|
2203
|
+
return {
|
|
2204
|
+
mode: "mixed",
|
|
2205
|
+
paths: literalPaths
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2657
2208
|
return {
|
|
2658
2209
|
mode: "files",
|
|
2659
|
-
paths:
|
|
2210
|
+
paths: literalPaths
|
|
2660
2211
|
};
|
|
2661
2212
|
};
|
|
2662
2213
|
let currentEngineFloor;
|
|
@@ -2686,8 +2237,9 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2686
2237
|
}
|
|
2687
2238
|
if (value.startsWith("./")) {
|
|
2688
2239
|
const selector = staticRepositoryPath(value) ? path.posix.normalize(value) : void 0;
|
|
2689
|
-
const
|
|
2690
|
-
|
|
2240
|
+
const providers = checkoutProvidersFor(selector);
|
|
2241
|
+
const binding = selector === void 0 ? void 0 : providers.at(-1);
|
|
2242
|
+
if (selector === void 0 || unknownCheckoutDestination || binding !== void 0 && binding.source !== "tracked" || hasAmbiguousCheckoutWriters(selector, providers)) add({
|
|
2691
2243
|
rule: "action-pins",
|
|
2692
2244
|
line,
|
|
2693
2245
|
message: "local actions require an unambiguous checkout of the current source snapshot"
|
|
@@ -2743,24 +2295,55 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2743
2295
|
const checkoutRepository = getInput(node, "repository");
|
|
2744
2296
|
const checkoutRef = getInput(node, "ref");
|
|
2745
2297
|
const prefix = checkoutPrefixOf(node);
|
|
2298
|
+
const clean = getInput(node, "clean");
|
|
2299
|
+
const cleanIsResolved = clean === void 0 || isScalar(clean) && !dynamic(clean.value);
|
|
2746
2300
|
let repo;
|
|
2747
2301
|
if (checkoutRepository !== void 0) repo = isScalar(checkoutRepository) ? checkoutRepository.value : null;
|
|
2748
2302
|
const self = repo === void 0 || repo === "${{ github.repository }}" || typeof repo === "string" && repository !== void 0 && repo.toLowerCase() === repository.toLowerCase();
|
|
2749
2303
|
const workflowSource = repo === "${{ job.workflow_repository }}" && isScalar(checkoutRef) && checkoutRef.value === "${{ job.workflow_sha }}";
|
|
2750
|
-
const
|
|
2751
|
-
const
|
|
2304
|
+
const triggers = getNode(document.contents, "on");
|
|
2305
|
+
const callerContext = (() => {
|
|
2306
|
+
if (isMap(triggers)) return [...keysOf(triggers)];
|
|
2307
|
+
if (isSeq(triggers)) return triggers.items.map((event) => isScalar(event) ? event.value : void 0);
|
|
2308
|
+
if (isScalar(triggers)) return [triggers.value];
|
|
2309
|
+
return [];
|
|
2310
|
+
})().includes("workflow_call");
|
|
2311
|
+
let refValue = isScalar(checkoutRef) ? checkoutRef.value : void 0;
|
|
2312
|
+
if (callerContext && (repo === void 0 || repo === "${{ github.repository }}") && checkoutRef === void 0) refValue = "${{ github.sha }}";
|
|
2313
|
+
const currentRef = dynamicRefBody(refValue);
|
|
2314
|
+
const currentSource = self && !callerContext && (checkoutRef === void 0 || currentRef === "github.sha");
|
|
2752
2315
|
const dynamicRef = dynamicRefBody(refValue);
|
|
2753
|
-
const delegatedSource = (self || repo === "${{ job.workflow_repository }}") && dynamicRef !== void 0 && dynamicRef !== "" && !/^(?:'(?:[^']|'')*'|"(?:[^"\\]|\\.)*"|(?:true|false|null)|-?(?:0x[\da-f]+|(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?))$/i.test(dynamicRef) && !/^(?:github\s*(?:\.\s*sha|\[\s*['"]sha['"]\s*\])|job\s*(?:\.\s*workflow_sha|\[\s*['"]workflow_sha['"]\s*\]))$/i.test(dynamicRef);
|
|
2316
|
+
const delegatedSource = !currentSource && (self || repo === "${{ job.workflow_repository }}") && dynamicRef !== void 0 && dynamicRef !== "" && !/^(?:'(?:[^']|'')*'|"(?:[^"\\]|\\.)*"|(?:true|false|null)|-?(?:0x[\da-f]+|(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?))$/i.test(dynamicRef) && (!/^(?:github\s*(?:\.\s*sha|\[\s*['"]sha['"]\s*\])|job\s*(?:\.\s*workflow_sha|\[\s*['"]workflow_sha['"]\s*\]))$/i.test(dynamicRef) || callerContext && currentRef === "github.sha");
|
|
2317
|
+
const validSource = cleanIsResolved && ref === approved?.sha && sourceMap({
|
|
2318
|
+
node,
|
|
2319
|
+
key: "if"
|
|
2320
|
+
}) === void 0 && sourceMap({
|
|
2321
|
+
node,
|
|
2322
|
+
key: "continue-on-error"
|
|
2323
|
+
}) === void 0 && (currentSource || workflowSource || delegatedSource);
|
|
2324
|
+
if (!validSource) unreviewableCheckoutSource = true;
|
|
2754
2325
|
if (!staticRepositoryPath(prefix)) checkoutBindings.push({
|
|
2755
2326
|
path: ".",
|
|
2327
|
+
dynamicSource: validSource && dynamic(prefix),
|
|
2756
2328
|
source: "untrusted",
|
|
2757
2329
|
reason: "checkout destination must be a static repository path",
|
|
2758
2330
|
line,
|
|
2759
|
-
sparse:
|
|
2331
|
+
sparse: sparseCheckoutOf(node)
|
|
2760
2332
|
});
|
|
2761
2333
|
else {
|
|
2762
2334
|
const destination = normalizeCheckoutPath(prefix);
|
|
2763
|
-
|
|
2335
|
+
checkoutBindings = checkoutBindings.map((entry) => {
|
|
2336
|
+
if (entry.path === destination || destination !== "." && !entry.path.startsWith(`${destination}/`)) return entry;
|
|
2337
|
+
return {
|
|
2338
|
+
path: entry.path,
|
|
2339
|
+
sparse: { mode: "all" },
|
|
2340
|
+
dynamicSource: false,
|
|
2341
|
+
source: "untrusted",
|
|
2342
|
+
reason: "a later ancestor checkout invalidates this descendant destination",
|
|
2343
|
+
line
|
|
2344
|
+
};
|
|
2345
|
+
});
|
|
2346
|
+
const trustedWriter = cleanIsResolved && ref === approved?.sha && sourceMap({
|
|
2764
2347
|
node,
|
|
2765
2348
|
key: "if"
|
|
2766
2349
|
}) === void 0 && sourceMap({
|
|
@@ -2769,18 +2352,21 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2769
2352
|
}) === void 0 && checkoutDestinations.get(destination) === 1 && !unknownCheckoutDestination;
|
|
2770
2353
|
if (trustedWriter && delegatedSource && typeof refValue === "string") checkoutBindings.push({
|
|
2771
2354
|
path: destination,
|
|
2355
|
+
dynamicSource: validSource && delegatedSource,
|
|
2772
2356
|
sparse: sparseCheckoutOf(node),
|
|
2773
2357
|
source: "delegated",
|
|
2774
2358
|
ref: refValue
|
|
2775
2359
|
});
|
|
2776
2360
|
else if (trustedWriter && (workflowSource || currentSource)) checkoutBindings.push({
|
|
2777
2361
|
path: destination,
|
|
2362
|
+
dynamicSource: validSource && delegatedSource,
|
|
2778
2363
|
sparse: sparseCheckoutOf(node),
|
|
2779
2364
|
source: "tracked"
|
|
2780
2365
|
});
|
|
2781
2366
|
else {
|
|
2782
2367
|
let reason = "checkout does not select a trusted repository source snapshot";
|
|
2783
|
-
if (
|
|
2368
|
+
if (!cleanIsResolved) reason = "checkout clean must be statically resolved";
|
|
2369
|
+
else if (ref !== approved?.sha) reason = "actions/checkout must use the approved action SHA";
|
|
2784
2370
|
else if (sourceMap({
|
|
2785
2371
|
node,
|
|
2786
2372
|
key: "if"
|
|
@@ -2791,6 +2377,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2791
2377
|
else if (checkoutDestinations.get(destination) !== 1 || unknownCheckoutDestination) reason = "checkout destination must have exactly one static writer";
|
|
2792
2378
|
checkoutBindings.push({
|
|
2793
2379
|
path: destination,
|
|
2380
|
+
dynamicSource: validSource && delegatedSource,
|
|
2794
2381
|
sparse: sparseCheckoutOf(node),
|
|
2795
2382
|
source: "untrusted",
|
|
2796
2383
|
reason,
|
|
@@ -2839,21 +2426,23 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2839
2426
|
line: reference === void 0 ? line : nodeLine(reference)
|
|
2840
2427
|
});
|
|
2841
2428
|
};
|
|
2842
|
-
const checkContainer = (node) => {
|
|
2429
|
+
const checkContainer = (node, at) => {
|
|
2843
2430
|
node = resolveNode(node);
|
|
2844
2431
|
if (node === void 0) return;
|
|
2845
2432
|
const image = isMap(node) ? getNode(node, "image") : node;
|
|
2846
2433
|
checkRuntimeImage({
|
|
2847
2434
|
image: isScalar(image) ? image.value : void 0,
|
|
2848
2435
|
line: nodeLine(image ?? node),
|
|
2849
|
-
label: "container image"
|
|
2436
|
+
label: "container image",
|
|
2437
|
+
at
|
|
2850
2438
|
});
|
|
2851
2439
|
};
|
|
2852
|
-
const checkSteps = (node, floor) => {
|
|
2440
|
+
const checkSteps = (node, locationPrefix, floor) => {
|
|
2853
2441
|
currentEngineFloor = void 0;
|
|
2854
2442
|
checkoutBindings = [];
|
|
2855
2443
|
checkoutDestinations = /* @__PURE__ */ new Map();
|
|
2856
2444
|
unknownCheckoutDestination = false;
|
|
2445
|
+
unreviewableCheckoutSource = false;
|
|
2857
2446
|
node = resolveNode(node);
|
|
2858
2447
|
if (!isSeq(node)) return;
|
|
2859
2448
|
for (const step of node.items) {
|
|
@@ -2869,9 +2458,14 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2869
2458
|
}
|
|
2870
2459
|
currentEngineFloor = floor;
|
|
2871
2460
|
try {
|
|
2872
|
-
for (const step of node.items)
|
|
2461
|
+
for (const step of node.items) {
|
|
2462
|
+
const id = getNode(step, "id");
|
|
2463
|
+
currentLocation = isScalar(id) && typeof id.value === "string" ? `${locationPrefix}.steps.${id.value}` : void 0;
|
|
2464
|
+
checkAction(step);
|
|
2465
|
+
}
|
|
2873
2466
|
} finally {
|
|
2874
2467
|
currentEngineFloor = void 0;
|
|
2468
|
+
currentLocation = void 0;
|
|
2875
2469
|
}
|
|
2876
2470
|
};
|
|
2877
2471
|
try {
|
|
@@ -2885,15 +2479,15 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2885
2479
|
currentEngineFloor = void 0;
|
|
2886
2480
|
const job = getNode(jobs, key);
|
|
2887
2481
|
checkAction(job);
|
|
2888
|
-
checkContainer(getNode(job, "container"));
|
|
2482
|
+
checkContainer(getNode(job, "container"), `jobs.${key}.container`);
|
|
2889
2483
|
const services = getNode(job, "services");
|
|
2890
|
-
if (isMap(services)) for (const service of keysOf(services)) checkContainer(getNode(services, service));
|
|
2891
|
-
checkSteps(getNode(job, "steps"), engineFloors.find((entry) => entry.job === key));
|
|
2484
|
+
if (isMap(services)) for (const service of keysOf(services)) checkContainer(getNode(services, service), `jobs.${key}.services.${service}`);
|
|
2485
|
+
checkSteps(getNode(job, "steps"), `jobs.${key}`, engineFloors.find((entry) => entry.job === key));
|
|
2892
2486
|
}
|
|
2893
2487
|
} else {
|
|
2894
2488
|
const runs = getNode(document.contents, "runs");
|
|
2895
2489
|
const using = getNode(runs, "using");
|
|
2896
|
-
if (isScalar(using) && using.value === "composite") checkSteps(getNode(runs, "steps"));
|
|
2490
|
+
if (isScalar(using) && using.value === "composite") checkSteps(getNode(runs, "steps"), "runs");
|
|
2897
2491
|
if (isScalar(using) && using.value === "docker") {
|
|
2898
2492
|
const image = getNode(runs, "image");
|
|
2899
2493
|
const value = isScalar(image) ? image.value : void 0;
|
|
@@ -2925,192 +2519,6 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2925
2519
|
return diagnostics;
|
|
2926
2520
|
};
|
|
2927
2521
|
//#endregion
|
|
2928
|
-
//#region src/toolchain-schema.ts
|
|
2929
|
-
const packagePinKeys = [
|
|
2930
|
-
"oxlint",
|
|
2931
|
-
"oxlint-tsgolint",
|
|
2932
|
-
"@oxlint/plugins",
|
|
2933
|
-
"@stll/oxlint-plugin",
|
|
2934
|
-
"oxfmt",
|
|
2935
|
-
"lefthook"
|
|
2936
|
-
];
|
|
2937
|
-
const valkeyArchitectures = ["amd64", "arm64"];
|
|
2938
|
-
const valkeyArtifactPackages = {
|
|
2939
|
-
server: "valkey-server",
|
|
2940
|
-
tools: "valkey-tools"
|
|
2941
|
-
};
|
|
2942
|
-
const record$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2943
|
-
const artifactRecord = (value, keys) => {
|
|
2944
|
-
if (!record$1(value) || Object.keys(value).some((key) => !keys.includes(key)) || keys.some((key) => !Object.hasOwn(value, key))) throw new Error("valkeyArtifacts requires complete, closed artifact objects");
|
|
2945
|
-
return value;
|
|
2946
|
-
};
|
|
2947
|
-
const parseValkeyArtifacts = (input, version) => {
|
|
2948
|
-
const architectures = artifactRecord(input, valkeyArchitectures);
|
|
2949
|
-
const line = version.split(".").slice(0, 2).join(".");
|
|
2950
|
-
const parseArchitecture = (architecture) => {
|
|
2951
|
-
const packages = artifactRecord(architectures[architecture], Object.keys(valkeyArtifactPackages));
|
|
2952
|
-
const parseArtifact = (kind) => {
|
|
2953
|
-
const artifact = artifactRecord(packages[kind], ["url", "sha256"]);
|
|
2954
|
-
const url = artifact["url"];
|
|
2955
|
-
const sha256 = artifact["sha256"];
|
|
2956
|
-
const expected = `https://download.valkey.io/packaging/valkey-${line}/deb/ubuntu2404/${architecture}/${valkeyArtifactPackages[kind]}_${version}-1.noble_${architecture}.deb`;
|
|
2957
|
-
if (typeof url !== "string" || url !== expected) throw new Error("valkeyArtifacts URL must select the exact official versioned Ubuntu artifact");
|
|
2958
|
-
if (typeof sha256 !== "string" || !/^[a-f0-9]{64}$/.test(sha256)) throw new Error("valkeyArtifacts requires a lowercase SHA256 pin");
|
|
2959
|
-
return {
|
|
2960
|
-
url,
|
|
2961
|
-
sha256
|
|
2962
|
-
};
|
|
2963
|
-
};
|
|
2964
|
-
return {
|
|
2965
|
-
server: parseArtifact("server"),
|
|
2966
|
-
tools: parseArtifact("tools")
|
|
2967
|
-
};
|
|
2968
|
-
};
|
|
2969
|
-
return {
|
|
2970
|
-
amd64: parseArchitecture("amd64"),
|
|
2971
|
-
arm64: parseArchitecture("arm64")
|
|
2972
|
-
};
|
|
2973
|
-
};
|
|
2974
|
-
/** Reject unsupported or incomplete installed policies before inspecting a consumer. */
|
|
2975
|
-
const parseToolchainPolicy = (input) => {
|
|
2976
|
-
if (!record$1(input) || input["schemaVersion"] !== 1) throw new Error("toolchain.json must use schemaVersion 1");
|
|
2977
|
-
const string = (object, key) => {
|
|
2978
|
-
const value = object[key];
|
|
2979
|
-
if (typeof value !== "string" || value.trim() === "") throw new Error(`toolchain.json requires a nonempty ${key}`);
|
|
2980
|
-
return value;
|
|
2981
|
-
};
|
|
2982
|
-
const packages = {};
|
|
2983
|
-
for (const key of packagePinKeys) packages[key] = string(input, key);
|
|
2984
|
-
const bun = string(input, "bun");
|
|
2985
|
-
const typescript = string(input, "typescript");
|
|
2986
|
-
const node = string(input, "node");
|
|
2987
|
-
const python = string(input, "python");
|
|
2988
|
-
const postgres = string(input, "postgres");
|
|
2989
|
-
const valkey = string(input, "valkey");
|
|
2990
|
-
const rust = string(input, "rust");
|
|
2991
|
-
const rustCompilerDevelopment = string(input, "rustCompilerDevelopment");
|
|
2992
|
-
for (const [name, value] of Object.entries({
|
|
2993
|
-
...packages,
|
|
2994
|
-
bun,
|
|
2995
|
-
typescript
|
|
2996
|
-
})) if (valid(value) !== value) throw new Error(`toolchain.json ${name} must be an exact release`);
|
|
2997
|
-
if (!/^[1-9]\d*$/.test(postgres) || !Number.isSafeInteger(Number(postgres))) throw new Error("PostgreSQL must be a canonical positive major");
|
|
2998
|
-
if (valid(valkey) !== valkey || !/^\d+\.\d+\.\d+$/.test(valkey)) throw new Error("Valkey must be an exact stable release");
|
|
2999
|
-
const valkeyArtifacts = parseValkeyArtifacts(input["valkeyArtifacts"], valkey);
|
|
3000
|
-
if (!nodePolicyValid(node)) throw new Error("Node must be a canonical major N.x series");
|
|
3001
|
-
if (!/^\d+\.\d+(?:\.\d+)?$/.test(python)) throw new Error("Python must be an exact minor or patch release");
|
|
3002
|
-
if (!/^\d+\.\d+\.\d+$/.test(rust)) throw new Error("Rust must be an exact stable release");
|
|
3003
|
-
if (!/^nightly-\d{4}-\d{2}-\d{2}$/.test(rustCompilerDevelopment)) throw new Error("Rust compiler development must be a dated nightly");
|
|
3004
|
-
const compatibility = input["typescript6Compatibility"];
|
|
3005
|
-
if (!record$1(compatibility)) throw new Error("missing TypeScript compatibility policy");
|
|
3006
|
-
const version = string(compatibility, "version");
|
|
3007
|
-
const packageAlias = string(compatibility, "packageAlias");
|
|
3008
|
-
if (valid(version) !== version) throw new Error("TypeScript compatibility must be an exact release");
|
|
3009
|
-
const layouts = input["typescriptInstallLayouts"];
|
|
3010
|
-
if (!Array.isArray(layouts) || layouts.length === 0) throw new Error("missing TypeScript install layouts");
|
|
3011
|
-
const typescriptInstallLayouts = layouts.map((layout) => {
|
|
3012
|
-
if (!record$1(layout)) throw new Error("invalid TypeScript install layout");
|
|
3013
|
-
const compilerPackage = string(layout, "compilerPackage");
|
|
3014
|
-
const compilerSpecifier = string(layout, "compilerSpecifier");
|
|
3015
|
-
const typecheckCommand = string(layout, "typecheckCommand");
|
|
3016
|
-
switch (layout["type"]) {
|
|
3017
|
-
case "direct":
|
|
3018
|
-
if (compilerSpecifier !== typescript || compilerPackage !== "typescript") throw new Error("direct TypeScript layout differs from shared version");
|
|
3019
|
-
return {
|
|
3020
|
-
type: "direct",
|
|
3021
|
-
compilerPackage,
|
|
3022
|
-
compilerSpecifier,
|
|
3023
|
-
typecheckCommand
|
|
3024
|
-
};
|
|
3025
|
-
case "split-compatibility": {
|
|
3026
|
-
const compatibilityPackage = string(layout, "compatibilityPackage");
|
|
3027
|
-
const compatibilitySpecifier = string(layout, "compatibilitySpecifier");
|
|
3028
|
-
if (compilerSpecifier !== `npm:typescript@${typescript}` || compatibilitySpecifier !== version) throw new Error("split TypeScript layout differs from shared versions");
|
|
3029
|
-
return {
|
|
3030
|
-
type: "split-compatibility",
|
|
3031
|
-
compilerPackage,
|
|
3032
|
-
compilerSpecifier,
|
|
3033
|
-
compatibilityPackage,
|
|
3034
|
-
compatibilitySpecifier,
|
|
3035
|
-
typecheckCommand
|
|
3036
|
-
};
|
|
3037
|
-
}
|
|
3038
|
-
default: throw new Error("unsupported TypeScript install layout");
|
|
3039
|
-
}
|
|
3040
|
-
});
|
|
3041
|
-
const actionsInput = input["actions"];
|
|
3042
|
-
if (!record$1(actionsInput) || Object.keys(actionsInput).length === 0) throw new Error("missing approved actions");
|
|
3043
|
-
const actions = {};
|
|
3044
|
-
for (const [name, action] of Object.entries(actionsInput)) {
|
|
3045
|
-
if (!record$1(action)) throw new Error(`invalid action policy: ${name}`);
|
|
3046
|
-
const sha = string(action, "sha");
|
|
3047
|
-
const actionVersion = string(action, "version");
|
|
3048
|
-
if (!/^[a-f0-9]{40}$/.test(sha) || !/^v\d+(?:\.\d+)*$/.test(actionVersion)) throw new Error(`invalid action SHA/version: ${name}`);
|
|
3049
|
-
actions[name] = {
|
|
3050
|
-
sha,
|
|
3051
|
-
version: actionVersion
|
|
3052
|
-
};
|
|
3053
|
-
}
|
|
3054
|
-
const dependabotInput = input["dependabot"];
|
|
3055
|
-
if (!record$1(dependabotInput) || !record$1(dependabotInput["schedule"]) || !record$1(dependabotInput["cooldown"]) || !record$1(dependabotInput["groups"])) throw new Error("missing Dependabot policy");
|
|
3056
|
-
const scheduleInput = dependabotInput["schedule"];
|
|
3057
|
-
const schedule = {
|
|
3058
|
-
interval: string(scheduleInput, "interval"),
|
|
3059
|
-
day: string(scheduleInput, "day"),
|
|
3060
|
-
time: string(scheduleInput, "time"),
|
|
3061
|
-
timezone: string(scheduleInput, "timezone")
|
|
3062
|
-
};
|
|
3063
|
-
const defaultDays = dependabotInput["cooldown"]["defaultDays"];
|
|
3064
|
-
if (typeof defaultDays !== "number" || !Number.isInteger(defaultDays) || defaultDays < 5) throw new Error("Dependabot cooldown must be at least five days");
|
|
3065
|
-
const strings = (value) => {
|
|
3066
|
-
if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) throw new Error("Dependabot group values must be string arrays");
|
|
3067
|
-
return value;
|
|
3068
|
-
};
|
|
3069
|
-
const groups = {};
|
|
3070
|
-
for (const [name, group] of Object.entries(dependabotInput["groups"])) {
|
|
3071
|
-
if (!record$1(group)) throw new Error(`invalid Dependabot group: ${name}`);
|
|
3072
|
-
groups[name] = {
|
|
3073
|
-
patterns: strings(group["patterns"]),
|
|
3074
|
-
updateTypes: strings(group["updateTypes"])
|
|
3075
|
-
};
|
|
3076
|
-
}
|
|
3077
|
-
return {
|
|
3078
|
-
bun,
|
|
3079
|
-
typescript,
|
|
3080
|
-
node,
|
|
3081
|
-
postgres,
|
|
3082
|
-
valkey,
|
|
3083
|
-
valkeyArtifacts,
|
|
3084
|
-
python,
|
|
3085
|
-
rust,
|
|
3086
|
-
rustCompilerDevelopment,
|
|
3087
|
-
packages,
|
|
3088
|
-
actions,
|
|
3089
|
-
typescriptInstallLayouts,
|
|
3090
|
-
typescript6Compatibility: {
|
|
3091
|
-
version,
|
|
3092
|
-
packageAlias
|
|
3093
|
-
},
|
|
3094
|
-
dependabot: {
|
|
3095
|
-
schedule,
|
|
3096
|
-
cooldown: { defaultDays },
|
|
3097
|
-
groups,
|
|
3098
|
-
ignoredPackages: [.../* @__PURE__ */ new Set([
|
|
3099
|
-
...Object.keys(packages),
|
|
3100
|
-
...typescriptInstallLayouts.flatMap((layout) => layout.type === "direct" ? [layout.compilerPackage] : [layout.compilerPackage, layout.compatibilityPackage]),
|
|
3101
|
-
packageAlias,
|
|
3102
|
-
"bun-types"
|
|
3103
|
-
])],
|
|
3104
|
-
ignoredActions: Object.keys(actions),
|
|
3105
|
-
ignoredImages: [
|
|
3106
|
-
"node",
|
|
3107
|
-
"python",
|
|
3108
|
-
"oven/bun"
|
|
3109
|
-
]
|
|
3110
|
-
}
|
|
3111
|
-
};
|
|
3112
|
-
};
|
|
3113
|
-
//#endregion
|
|
3114
2522
|
//#region src/toolchain-guard.ts
|
|
3115
2523
|
const toolchainRules = [.../* @__PURE__ */ new Set([
|
|
3116
2524
|
...packageRules,
|
|
@@ -3122,7 +2530,7 @@ const record = (value) => typeof value === "object" && value !== null && !Array.
|
|
|
3122
2530
|
const array = (value) => Array.isArray(value);
|
|
3123
2531
|
/** Repository-owned declarations are explicit; malformed decisions fail closed. */
|
|
3124
2532
|
const parseToolchainConfiguration = (input) => {
|
|
3125
|
-
if (!record(input) || Object.keys(input).some((key) => key !== "optOuts" && key !== "cloud" && key !== "engineFloors") || input["optOuts"] !== void 0 && !array(input["optOuts"])) throw new Error("stll-toolchain.json accepts only optOuts, cloud and
|
|
2533
|
+
if (!record(input) || Object.keys(input).some((key) => key !== "optOuts" && key !== "cloud" && key !== "engineFloors" && key !== "dynamicSelectors" && key !== "consumerChecks") || input["optOuts"] !== void 0 && !array(input["optOuts"])) throw new Error("stll-toolchain.json accepts only optOuts, cloud, engineFloors, dynamicSelectors and consumerChecks declarations");
|
|
3126
2534
|
const disabled = /* @__PURE__ */ new Set();
|
|
3127
2535
|
const optOuts = input["optOuts"] ?? [];
|
|
3128
2536
|
if (!array(optOuts)) throw new Error("optOuts must be an array");
|
|
@@ -3135,7 +2543,9 @@ const parseToolchainConfiguration = (input) => {
|
|
|
3135
2543
|
return {
|
|
3136
2544
|
disabled,
|
|
3137
2545
|
cloud: parseCloudSetup(input["cloud"]),
|
|
3138
|
-
engineFloors: parseEngineFloors(input["engineFloors"])
|
|
2546
|
+
engineFloors: parseEngineFloors(input["engineFloors"]),
|
|
2547
|
+
dynamicSelectors: parseDynamicSelectors(input["dynamicSelectors"]),
|
|
2548
|
+
consumerChecks: parseConsumerChecks(input["consumerChecks"])
|
|
3139
2549
|
};
|
|
3140
2550
|
};
|
|
3141
2551
|
/** Read one tracked configuration snapshot for guards and policy generation. */
|
|
@@ -3198,13 +2608,12 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
|
|
|
3198
2608
|
}).trim();
|
|
3199
2609
|
repository = /^(?:https?:\/\/github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)([^/]+\/[^/]+?)(?:\.git)?\/?$/.exec(origin)?.[1];
|
|
3200
2610
|
} catch {}
|
|
3201
|
-
let
|
|
3202
|
-
let cloud;
|
|
2611
|
+
let configuration = parseToolchainConfiguration({});
|
|
3203
2612
|
const engineFloors = [];
|
|
2613
|
+
let dynamicSelectors = [];
|
|
3204
2614
|
if (files["stll-toolchain.json"] !== void 0) try {
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
cloud = configuration.cloud;
|
|
2615
|
+
configuration = parseToolchainConfiguration(JSON.parse(files["stll-toolchain.json"]));
|
|
2616
|
+
dynamicSelectors = configuration.dynamicSelectors;
|
|
3208
2617
|
for (const entry of configuration.engineFloors) engineFloors.push(resolveEngineFloor(entry, files));
|
|
3209
2618
|
} catch (error) {
|
|
3210
2619
|
diagnostics.push({
|
|
@@ -3214,6 +2623,7 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
|
|
|
3214
2623
|
message: error instanceof Error ? error.message : String(error)
|
|
3215
2624
|
});
|
|
3216
2625
|
}
|
|
2626
|
+
const cloud = configuration.cloud;
|
|
3217
2627
|
const script = files[cloudSetupPath];
|
|
3218
2628
|
if (cloud === void 0 && trackedFiles.has(".agents/cloud-setup.sh")) diagnostics.push({
|
|
3219
2629
|
rule: "cloud-setup-drift",
|
|
@@ -3244,6 +2654,12 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
|
|
|
3244
2654
|
}
|
|
3245
2655
|
const exercisedFloors = /* @__PURE__ */ new Set();
|
|
3246
2656
|
const mismatchedFloors = /* @__PURE__ */ new Set();
|
|
2657
|
+
const selectorMatches = /* @__PURE__ */ new Map();
|
|
2658
|
+
diagnostics.push(...checkConsumerChecks({
|
|
2659
|
+
declarations: configuration.consumerChecks,
|
|
2660
|
+
files,
|
|
2661
|
+
policy
|
|
2662
|
+
}));
|
|
3247
2663
|
diagnostics.push(...checkPackageFiles({
|
|
3248
2664
|
files,
|
|
3249
2665
|
policy
|
|
@@ -3259,6 +2675,8 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
|
|
|
3259
2675
|
}
|
|
3260
2676
|
},
|
|
3261
2677
|
trackedFiles,
|
|
2678
|
+
dynamicSelectors: dynamicSelectors.filter((entry) => entry.path === file),
|
|
2679
|
+
onDynamicSelector: (entry) => selectorMatches.set(entry, (selectorMatches.get(entry) ?? 0) + 1),
|
|
3262
2680
|
repository,
|
|
3263
2681
|
onDelegated,
|
|
3264
2682
|
engineFloors: engineFloors.filter((entry) => entry.workflow === file),
|
|
@@ -3274,13 +2692,19 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
|
|
|
3274
2692
|
line: 1,
|
|
3275
2693
|
message: `engine floor ${entry.workflow}:${entry.job} requires a setup-node exact patch satisfying ${entry.package} engines.node minimum major ${entry.major}`
|
|
3276
2694
|
});
|
|
2695
|
+
for (const entry of dynamicSelectors) if (selectorMatches.get(entry) !== 1) diagnostics.push({
|
|
2696
|
+
rule: "configuration",
|
|
2697
|
+
path: "stll-toolchain.json",
|
|
2698
|
+
line: 1,
|
|
2699
|
+
message: `dynamic selector ${entry.path}:${entry.at ?? entry.line} must match exactly one unresolved ${entry.kind} selector`
|
|
2700
|
+
});
|
|
3277
2701
|
diagnostics.push(...checkDependabot({
|
|
3278
2702
|
files,
|
|
3279
2703
|
policy: policy.dependabot
|
|
3280
2704
|
}));
|
|
3281
|
-
return diagnostics.filter((diagnostic) => !disabled.has(diagnostic.rule));
|
|
2705
|
+
return diagnostics.filter((diagnostic) => !configuration.disabled.has(diagnostic.rule));
|
|
3282
2706
|
};
|
|
3283
2707
|
//#endregion
|
|
3284
|
-
export {
|
|
2708
|
+
export { parseToolchainConfiguration as n, readToolchainInputs as r, checkToolchain as t };
|
|
3285
2709
|
|
|
3286
2710
|
//# sourceMappingURL=toolchain-guard.mjs.map
|