@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.cjs
CHANGED
|
@@ -1,782 +1,241 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __copyProps = (to, from, except, desc) => {
|
|
9
|
-
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
-
key = keys[i];
|
|
11
|
-
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
-
get: ((k) => from[k]).bind(null, key),
|
|
13
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
-
value: mod,
|
|
20
|
-
enumerable: true
|
|
21
|
-
}) : target, mod));
|
|
22
|
-
//#endregion
|
|
1
|
+
const require_rolldown_runtime = require("./rolldown-runtime.cjs");
|
|
2
|
+
const require_toolchain_inputs = require("./toolchain-inputs.cjs");
|
|
3
|
+
const require_toolchain_schema = require("./toolchain-schema.cjs");
|
|
23
4
|
let node_fs = require("node:fs");
|
|
24
5
|
let node_child_process = require("node:child_process");
|
|
25
6
|
let node_path = require("node:path");
|
|
26
|
-
node_path = __toESM(node_path, 1);
|
|
7
|
+
node_path = require_rolldown_runtime.__toESM(node_path, 1);
|
|
27
8
|
let semver = require("semver");
|
|
9
|
+
let yaml = require("yaml");
|
|
28
10
|
let picomatch = require("picomatch");
|
|
29
|
-
picomatch = __toESM(picomatch, 1);
|
|
11
|
+
picomatch = require_rolldown_runtime.__toESM(picomatch, 1);
|
|
30
12
|
let smol_toml = require("smol-toml");
|
|
31
|
-
let yaml = require("yaml");
|
|
32
|
-
//#region src/cloud-setup-install.ts
|
|
33
|
-
const packageInstall = String.raw`
|
|
34
|
-
# Prevent package maintainer scripts from starting unmanaged system services.
|
|
35
|
-
apt_install() (
|
|
36
|
-
# EXIT runs after function locals expire; this subshell owns the trap state.
|
|
37
|
-
backup=''
|
|
38
|
-
policy_state=untouched
|
|
39
|
-
[[ ! -L /var/lock/stll-cloud-apt.lock ]] || fail 'Invalid package install lock'
|
|
40
|
-
exec 8>/var/lock/stll-cloud-apt.lock
|
|
41
|
-
flock -x -w 120 8 || fail 'Another package installation is active'
|
|
42
|
-
[[ ! -L /usr/sbin/policy-rc.d ]] || fail 'Unsupported service-start policy symlink'
|
|
43
|
-
[[ ! -e /usr/sbin/policy-rc.d || -f /usr/sbin/policy-rc.d ]] || fail 'Unsupported service-start policy file'
|
|
44
|
-
backup="$(mktemp -d)"
|
|
45
|
-
cleanup_package_policy() {
|
|
46
|
-
local status=$?
|
|
47
|
-
trap - EXIT HUP INT TERM
|
|
48
|
-
if [[ "$policy_state" == installed ]]; then
|
|
49
|
-
if [[ -f "$backup/original" ]]; then
|
|
50
|
-
if ! cp -p "$backup/original" /usr/sbin/policy-rc.d; then status=1; fi
|
|
51
|
-
else
|
|
52
|
-
if ! rm -f /usr/sbin/policy-rc.d; then status=1; fi
|
|
53
|
-
fi
|
|
54
|
-
fi
|
|
55
|
-
rm -rf -- "$backup"
|
|
56
|
-
exit "$status"
|
|
57
|
-
}
|
|
58
|
-
trap cleanup_package_policy EXIT
|
|
59
|
-
trap 'exit 129' HUP
|
|
60
|
-
trap 'exit 130' INT
|
|
61
|
-
trap 'exit 143' TERM
|
|
62
|
-
if [[ -e /usr/sbin/policy-rc.d ]]; then cp -p /usr/sbin/policy-rc.d "$backup/original"; fi
|
|
63
|
-
policy_state=installed
|
|
64
|
-
printf '%s\n' '#!/bin/sh' 'exit 101' > /usr/sbin/policy-rc.d
|
|
65
|
-
chmod 755 /usr/sbin/policy-rc.d
|
|
66
|
-
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "$@"
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
`;
|
|
70
|
-
const postgresSourceLine = "deb [signed-by=/usr/share/keyrings/stll-pgdg.asc] https://apt.postgresql.org/pub/repos/apt noble-pgdg main";
|
|
71
|
-
const postgresSigningFingerprint = "B97B0AFCAA1A47F044F244A07FCC7D46ACCC4CF8";
|
|
72
|
-
const postgresSources = String.raw`
|
|
73
|
-
pgdg_key_fingerprint() {
|
|
74
|
-
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 }'
|
|
75
|
-
}
|
|
76
|
-
pgdg_sources_ready() {
|
|
77
|
-
local managed="$1" file mode records suite keyring override fingerprint selected_keyring=''
|
|
78
|
-
shift
|
|
79
|
-
PGDG_SOURCE_STATE=missing
|
|
80
|
-
[[ ! -L "$managed" ]] || fail "Managed PGDG source must not be a symlink: $managed"
|
|
81
|
-
for file in "$@"; do
|
|
82
|
-
[[ -e "$file" ]] || continue
|
|
83
|
-
[[ -f "$file" ]] || fail "APT source must be a regular file: $file"
|
|
84
|
-
case "$file" in *.sources) mode=sources ;; *) mode=list ;; esac
|
|
85
|
-
records="$(awk -v mode="$mode" '
|
|
86
|
-
function trim(value) { gsub(/^[[:space:]]+|[[:space:]]+$/, "", value); return value }
|
|
87
|
-
function pgdg_uri(uri) {
|
|
88
|
-
uri = tolower(uri)
|
|
89
|
-
if (uri !~ /^[a-z][a-z0-9+.-]*:\/\//) return 0
|
|
90
|
-
sub(/^[a-z][a-z0-9+.-]*:\/\//, "", uri)
|
|
91
|
-
sub(/[\/?#].*$/, "", uri)
|
|
92
|
-
sub(/^.*@/, "", uri)
|
|
93
|
-
sub(/:.*$/, "", uri)
|
|
94
|
-
return uri == "apt.postgresql.org" || uri == "apt.postgresql.org."
|
|
95
|
-
}
|
|
96
|
-
function contains_pgdg(value, tokens, count, token_index) {
|
|
97
|
-
count = split(value, tokens, /[[:space:]]+/)
|
|
98
|
-
for (token_index = 1; token_index <= count; token_index++) if (pgdg_uri(tokens[token_index])) return 1
|
|
99
|
-
return 0
|
|
100
|
-
}
|
|
101
|
-
function unsafe_field(name) {
|
|
102
|
-
return name ~ /^(trusted|allow-insecure|allow-weak|allow-downgrade-to-insecure)$/
|
|
103
|
-
}
|
|
104
|
-
function disabled_override(value) {
|
|
105
|
-
value = tolower(trim(value))
|
|
106
|
-
gsub(/^["\047]|["\047]$/, "", value)
|
|
107
|
-
return value ~ /^(no|false|without|off|disable|0)$/
|
|
108
|
-
}
|
|
109
|
-
function stanza(override_index) {
|
|
110
|
-
override = 0
|
|
111
|
-
for (override_index = 1; override_index <= override_count; override_index++) {
|
|
112
|
-
if (!disabled_override(override_values[override_index])) override = 1
|
|
113
|
-
delete override_values[override_index]
|
|
114
|
-
}
|
|
115
|
-
if (tolower(trim(enabled)) != "no" && pgdg) print (trim(suites) == "" ? "-" : trim(suites)) "\t" (trim(keyring) == "" ? "-" : trim(keyring)) "\t" (override ? "unsafe" : "safe")
|
|
116
|
-
enabled = ""; pgdg = 0; field = ""; suites = ""; keyring = ""; override = 0; override_count = 0
|
|
117
|
-
}
|
|
118
|
-
/^[[:space:]]*#/ { if (mode == "list" || $0 ~ /^#/) next }
|
|
119
|
-
{
|
|
120
|
-
line = $0
|
|
121
|
-
if (mode == "list") {
|
|
122
|
-
sub(/[[:space:]]*#.*$/, "", line)
|
|
123
|
-
if (line !~ /^[[:space:]]*deb(-src)?[[:space:]]/ || !contains_pgdg(line)) next
|
|
124
|
-
keyring = ""; override = 0
|
|
125
|
-
if (match(line, /\[[^]]*\]/)) {
|
|
126
|
-
options = substr(line, RSTART + 1, RLENGTH - 2)
|
|
127
|
-
count = split(options, tokens, /[[:space:]]+/)
|
|
128
|
-
for (token_index = 1; token_index <= count; token_index++) {
|
|
129
|
-
if (tolower(tokens[token_index]) ~ /^signed-by=/) { keyring = tokens[token_index]; sub(/^[^=]*=/, "", keyring) }
|
|
130
|
-
option_name = tolower(tokens[token_index]); sub(/=.*/, "", option_name)
|
|
131
|
-
option_value = tokens[token_index]; sub(/^[^=]*=/, "", option_value)
|
|
132
|
-
if (unsafe_field(option_name) && !disabled_override(option_value)) override = 1
|
|
133
|
-
}
|
|
134
|
-
sub(/\[[^]]*\]/, "", line)
|
|
135
|
-
}
|
|
136
|
-
count = split(line, tokens, /[[:space:]]+/)
|
|
137
|
-
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")
|
|
138
|
-
next
|
|
139
|
-
}
|
|
140
|
-
if (line ~ /^[[:space:]]*$/) { stanza(); next }
|
|
141
|
-
if (line ~ /^[[:space:]]/) {
|
|
142
|
-
if (field == "uris" && contains_pgdg(line)) pgdg = 1
|
|
143
|
-
if (field == "enabled") enabled = enabled " " line
|
|
144
|
-
if (field == "suites") suites = suites " " line
|
|
145
|
-
if (field == "signed-by") keyring = keyring " " line
|
|
146
|
-
if (unsafe_field(field)) override_values[override_count] = override_values[override_count] " " line
|
|
147
|
-
next
|
|
148
|
-
}
|
|
149
|
-
field = tolower(line)
|
|
150
|
-
sub(/:.*/, "", field)
|
|
151
|
-
value = line
|
|
152
|
-
sub(/^[^:]*:[[:space:]]*/, "", value)
|
|
153
|
-
if (field == "uris" && contains_pgdg(value)) pgdg = 1
|
|
154
|
-
if (field == "enabled") enabled = value
|
|
155
|
-
if (field == "suites") suites = value
|
|
156
|
-
if (field == "signed-by") keyring = value
|
|
157
|
-
if (unsafe_field(field)) override_values[++override_count] = value
|
|
158
|
-
}
|
|
159
|
-
END { if (mode == "sources") stanza() }
|
|
160
|
-
' "$file")" || fail "Cannot inspect APT source: $file"
|
|
161
|
-
if [[ "$file" == "$managed" && -z "$records" ]]; then
|
|
162
|
-
fail "Managed PGDG source has no active PGDG entry: $file"
|
|
163
|
-
fi
|
|
164
|
-
while IFS=$'\t' read -r suite keyring override; do
|
|
165
|
-
[[ -n "$suite" ]] || continue
|
|
166
|
-
[[ "$override" == safe ]] || fail "PGDG source enables an unsafe trust override: $file"
|
|
167
|
-
[[ "$suite" == noble-pgdg && "$keyring" == /* && ! "$keyring" =~ [[:space:],] ]] || fail "PGDG source requires noble-pgdg and one Signed-By keyring: $file"
|
|
168
|
-
[[ -z "$selected_keyring" || "$selected_keyring" == "$keyring" ]] || fail "Conflicting PGDG Signed-By keyrings: $file"
|
|
169
|
-
fingerprint="$(pgdg_key_fingerprint "$keyring")" || fail "Cannot verify PGDG Signed-By keyring: $file"
|
|
170
|
-
[[ "$fingerprint" == @PGDG_FINGERPRINT@ ]] || fail "PGDG Signed-By fingerprint mismatch: $file"
|
|
171
|
-
selected_keyring="$keyring"
|
|
172
|
-
PGDG_SOURCE_STATE=reuse
|
|
173
|
-
done <<< "$records"
|
|
174
|
-
done
|
|
175
|
-
}
|
|
176
|
-
`;
|
|
177
|
-
const postgresInstall = String.raw`
|
|
178
|
-
postgres_ready() {
|
|
179
|
-
local binary
|
|
180
|
-
for binary in postgres initdb pg_ctl psql createdb; do
|
|
181
|
-
[[ -x "/usr/lib/postgresql/@POSTGRES@/bin/$binary" ]] || return 1
|
|
182
|
-
done
|
|
183
|
-
/usr/lib/postgresql/@POSTGRES@/bin/postgres --version | grep -Eq '^postgres \(PostgreSQL\) @POSTGRES@\.'
|
|
184
|
-
}
|
|
185
|
-
if ! postgres_ready; then
|
|
186
|
-
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
|
|
187
|
-
if [[ "$PGDG_SOURCE_STATE" == missing ]]; then
|
|
188
|
-
local key key_fingerprint
|
|
189
|
-
key="$(mktemp)"
|
|
190
|
-
curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' --tlsv1.2 https://www.postgresql.org/media/keys/ACCC4CF8.asc -o "$key"
|
|
191
|
-
key_fingerprint="$(pgdg_key_fingerprint "$key")"
|
|
192
|
-
[[ "$key_fingerprint" == @PGDG_FINGERPRINT@ ]] || fail 'PostgreSQL signing key mismatch'
|
|
193
|
-
install -d -m 755 /usr/share/keyrings
|
|
194
|
-
install -m 644 "$key" /usr/share/keyrings/stll-pgdg.asc
|
|
195
|
-
rm -- "$key"
|
|
196
|
-
printf '%s\n' '@PGDG_SOURCE@' > /etc/apt/sources.list.d/stll-pgdg.list
|
|
197
|
-
fi
|
|
198
|
-
apt-get update
|
|
199
|
-
apt_install postgresql-@POSTGRES@ postgresql-client-@POSTGRES@
|
|
200
|
-
fi
|
|
201
|
-
postgres_ready || fail 'PostgreSQL toolchain is incomplete or differs from the policy major'
|
|
202
|
-
`;
|
|
203
|
-
const valkeyInstall = String.raw`
|
|
204
|
-
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
|
|
205
|
-
local scratch server_url server_sha tools_url tools_sha package
|
|
206
|
-
scratch="$(mktemp -d)"
|
|
207
|
-
chmod 755 "$scratch"
|
|
208
|
-
case "$ARCH" in
|
|
209
|
-
amd64) server_url='@AMD64_SERVER_URL@'; server_sha='@AMD64_SERVER_SHA@'; tools_url='@AMD64_TOOLS_URL@'; tools_sha='@AMD64_TOOLS_SHA@' ;;
|
|
210
|
-
arm64) server_url='@ARM64_SERVER_URL@'; server_sha='@ARM64_SERVER_SHA@'; tools_url='@ARM64_TOOLS_URL@'; tools_sha='@ARM64_TOOLS_SHA@' ;;
|
|
211
|
-
*) fail 'Unsupported Valkey architecture' ;;
|
|
212
|
-
esac
|
|
213
|
-
curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' --tlsv1.2 "$server_url" -o "$scratch/valkey-server.deb"
|
|
214
|
-
curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' --tlsv1.2 "$tools_url" -o "$scratch/valkey-tools.deb"
|
|
215
|
-
(cd "$scratch"; printf '%s %s\n' "$server_sha" valkey-server.deb "$tools_sha" valkey-tools.deb | sha256sum --strict -c -)
|
|
216
|
-
for package in valkey-server valkey-tools; do
|
|
217
|
-
[[ "$(dpkg-deb -f "$scratch/$package.deb" Package)" == "$package" ]] || fail 'Valkey artifact package mismatch'
|
|
218
|
-
[[ "$(dpkg-deb -f "$scratch/$package.deb" Version)" == '@VALKEY@-1.noble' ]] || fail 'Valkey artifact version mismatch'
|
|
219
|
-
[[ "$(dpkg-deb -f "$scratch/$package.deb" Architecture)" == "$ARCH" ]] || fail 'Valkey artifact architecture mismatch'
|
|
220
|
-
done
|
|
221
|
-
apt-get update
|
|
222
|
-
apt_install "$scratch/valkey-server.deb" "$scratch/valkey-tools.deb"
|
|
223
|
-
rm -rf -- "$scratch"
|
|
224
|
-
fi
|
|
225
|
-
[[ "$(valkey-server --version | sed -nE 's/.*v=([0-9.]+).*/\1/p')" == '@VALKEY@' ]] || fail 'Valkey version differs from policy'
|
|
226
|
-
[[ "$(valkey-cli --version)" == 'valkey-cli @VALKEY@' ]] || fail 'Valkey client version differs from policy'
|
|
227
|
-
`;
|
|
228
|
-
const state = String.raw`
|
|
229
|
-
service_account_ready() {
|
|
230
|
-
local uid gid shell
|
|
231
|
-
uid="$(id -u stll-cloud)" || fail 'Service user is unavailable; run install'
|
|
232
|
-
gid="$(id -g stll-cloud)"
|
|
233
|
-
shell="$(getent passwd stll-cloud | cut -d: -f7)"
|
|
234
|
-
[[ "$uid" =~ ^[1-9][0-9]*$ && "$gid" =~ ^[1-9][0-9]*$ && "$shell" == /usr/sbin/nologin ]] || fail 'Service user must be unprivileged with no login shell'
|
|
235
|
-
}
|
|
236
|
-
prepare_state() {
|
|
237
|
-
service_account_ready
|
|
238
|
-
[[ ! -L /var/lib/stll-cloud ]] || fail 'Service state root must not be a symlink'
|
|
239
|
-
if [[ -e /var/lib/stll-cloud ]]; then
|
|
240
|
-
[[ -d /var/lib/stll-cloud && "$(stat -c '%u:%a' /var/lib/stll-cloud)" == 0:755 ]] || fail 'Invalid service state root'
|
|
241
|
-
else
|
|
242
|
-
install -d -m 755 -o root -g root /var/lib/stll-cloud
|
|
243
|
-
fi
|
|
244
|
-
STATE="/var/lib/stll-cloud/$(printf '%s' "$REPO_ROOT" | sha256sum | cut -d' ' -f1)"
|
|
245
|
-
[[ ! -L "$STATE" ]] || fail 'Service state must not be a symlink'
|
|
246
|
-
if [[ -e "$STATE" ]]; then
|
|
247
|
-
[[ -d "$STATE" && "$(stat -c '%u:%a' "$STATE")" == 0:710 && "$(stat -c '%g' "$STATE")" == "$(id -g stll-cloud)" ]] || fail 'Invalid service state ownership'
|
|
248
|
-
else
|
|
249
|
-
install -d -m 710 -o root -g stll-cloud "$STATE"
|
|
250
|
-
fi
|
|
251
|
-
[[ ! -L "$STATE/lock" ]] || fail 'Service lock must not be a symlink'
|
|
252
|
-
exec 9>"$STATE/lock"
|
|
253
|
-
flock -x -w 10 9 || fail 'Another setup process owns the service state'
|
|
254
|
-
}
|
|
255
|
-
`;
|
|
256
|
-
const renderCloudServiceInstall = ({ services, postgres, valkey, valkeyArtifacts }) => {
|
|
257
|
-
const parts = services.map((service) => {
|
|
258
|
-
switch (service) {
|
|
259
|
-
case "postgres": return postgresInstall.replaceAll("@POSTGRES@", postgres).replaceAll("@PGDG_SOURCE@", postgresSourceLine).replaceAll("@PGDG_FINGERPRINT@", postgresSigningFingerprint);
|
|
260
|
-
case "valkey": {
|
|
261
|
-
const replacements = /* @__PURE__ */ new Map([
|
|
262
|
-
["@VALKEY@", valkey],
|
|
263
|
-
["@AMD64_SERVER_URL@", valkeyArtifacts.amd64.server.url],
|
|
264
|
-
["@AMD64_SERVER_SHA@", valkeyArtifacts.amd64.server.sha256],
|
|
265
|
-
["@AMD64_TOOLS_URL@", valkeyArtifacts.amd64.tools.url],
|
|
266
|
-
["@AMD64_TOOLS_SHA@", valkeyArtifacts.amd64.tools.sha256],
|
|
267
|
-
["@ARM64_SERVER_URL@", valkeyArtifacts.arm64.server.url],
|
|
268
|
-
["@ARM64_SERVER_SHA@", valkeyArtifacts.arm64.server.sha256],
|
|
269
|
-
["@ARM64_TOOLS_URL@", valkeyArtifacts.arm64.tools.url],
|
|
270
|
-
["@ARM64_TOOLS_SHA@", valkeyArtifacts.arm64.tools.sha256]
|
|
271
|
-
]);
|
|
272
|
-
return valkeyInstall.replace(/@[A-Z0-9_]+@/g, (token) => {
|
|
273
|
-
const value = replacements.get(token);
|
|
274
|
-
if (value === void 0) throw new Error(`Missing Valkey artifact field: ${token}`);
|
|
275
|
-
return value;
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
default: throw new Error(`Unsupported service: ${String(service)}`);
|
|
279
|
-
}
|
|
280
|
-
});
|
|
281
|
-
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";
|
|
282
|
-
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`;
|
|
283
|
-
};
|
|
284
13
|
//#endregion
|
|
285
|
-
//#region src/
|
|
286
|
-
const
|
|
287
|
-
const
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
14
|
+
//#region src/toolchain-consumer-checks.ts
|
|
15
|
+
const consumerRunnerVersion = "0.14.1";
|
|
16
|
+
const consumerCheckWorkflow = "stella/.github/.github/workflows/package-consumer-compat.yml";
|
|
17
|
+
const record$6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
|
+
const repositoryDirectory = (value) => typeof value === "string" && value !== "" && !value.includes("\\") && !value.includes("${{") && !value.split("").some((character) => {
|
|
19
|
+
const code = character.charCodeAt(0);
|
|
20
|
+
return code < 32 || code === 127;
|
|
21
|
+
}) && !/^[A-Za-z]:/.test(value) && !node_path.default.posix.isAbsolute(value) && (value === "." || value.split("/").every((part) => part !== "" && part !== "." && part !== ".."));
|
|
22
|
+
const packageDirectories = (value) => Array.isArray(value) && value.length > 0 && value.every(repositoryDirectory) && new Set(value).size === value.length;
|
|
23
|
+
const cronFields = [
|
|
24
|
+
{
|
|
25
|
+
minimum: 0,
|
|
26
|
+
maximum: 59
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
minimum: 0,
|
|
30
|
+
maximum: 23
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
minimum: 1,
|
|
34
|
+
maximum: 31
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
minimum: 1,
|
|
38
|
+
maximum: 12,
|
|
39
|
+
names: Object.fromEntries([
|
|
40
|
+
"JAN",
|
|
41
|
+
"FEB",
|
|
42
|
+
"MAR",
|
|
43
|
+
"APR",
|
|
44
|
+
"MAY",
|
|
45
|
+
"JUN",
|
|
46
|
+
"JUL",
|
|
47
|
+
"AUG",
|
|
48
|
+
"SEP",
|
|
49
|
+
"OCT",
|
|
50
|
+
"NOV",
|
|
51
|
+
"DEC"
|
|
52
|
+
].map((name, index) => [name, index + 1]))
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
minimum: 0,
|
|
56
|
+
maximum: 6,
|
|
57
|
+
names: Object.fromEntries([
|
|
58
|
+
"SUN",
|
|
59
|
+
"MON",
|
|
60
|
+
"TUE",
|
|
61
|
+
"WED",
|
|
62
|
+
"THU",
|
|
63
|
+
"FRI",
|
|
64
|
+
"SAT"
|
|
65
|
+
].map((name, index) => [name, index]))
|
|
310
66
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
install: cloudInstallCommand,
|
|
317
|
-
envFile
|
|
67
|
+
];
|
|
68
|
+
const cronFieldValues = (text, field) => {
|
|
69
|
+
const value = (token) => {
|
|
70
|
+
const number = /^\d+$/.test(token) ? Number(token) : field.names?.[token.toUpperCase()];
|
|
71
|
+
return number !== void 0 && Number.isSafeInteger(number) && number >= field.minimum && number <= field.maximum ? number : void 0;
|
|
318
72
|
};
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
service_password() {
|
|
340
|
-
local file="$1"
|
|
341
|
-
service_file "$file" 0
|
|
342
|
-
if [[ ! -e "$file" ]]; then
|
|
343
|
-
(umask 077; openssl rand -hex 32 > "$file")
|
|
344
|
-
root chown root:root "$file"
|
|
345
|
-
fi
|
|
346
|
-
[[ "$(cat "$file")" =~ ^[0-9a-f]{64}$ ]] || fail "Invalid service credential state"
|
|
347
|
-
}
|
|
348
|
-
service_process_matches() {
|
|
349
|
-
local pid="$1" executable="$2" owner actual expected
|
|
350
|
-
[[ "$pid" =~ ^[1-9][0-9]*$ && -d "/proc/$pid" ]] || return 1
|
|
351
|
-
owner="$(stat -c '%u' "/proc/$pid" 2>/dev/null)" || return 2
|
|
352
|
-
[[ "$owner" =~ ^[0-9]+$ ]] || return 2
|
|
353
|
-
[[ "$owner" == "$SERVICE_UID" ]] || return 1
|
|
354
|
-
actual="$(readlink -f "/proc/$pid/exe" 2>/dev/null)" || return 2
|
|
355
|
-
expected="$(readlink -f "$executable")" || return 2
|
|
356
|
-
[[ -n "$actual" && -n "$expected" && "$actual" != *' (deleted)' ]] || return 2
|
|
357
|
-
[[ "$actual" == "$expected" ]]
|
|
358
|
-
}
|
|
359
|
-
service_process() {
|
|
360
|
-
service_process_matches "$1" "$2" || fail "Managed service process identity differs from the recorded state"
|
|
361
|
-
}
|
|
362
|
-
service_prune_pid() {
|
|
363
|
-
local file="$1" executable="$2" service="$3" pid identity
|
|
364
|
-
[[ -e "$file" ]] || return 0
|
|
365
|
-
pid="$(head -n 1 "$file")"
|
|
366
|
-
# PostgreSQL encodes a standalone backend with a negative PID.
|
|
367
|
-
if [[ "$service" == postgres ]]; then pid="$(printf '%s' "$pid" | sed 's/^-//')"; fi
|
|
368
|
-
if service_process_matches "$pid" "$executable"; then return 0; else identity=$?; fi
|
|
369
|
-
[[ "$identity" != 2 ]] || fail "Cannot verify recorded live service process; retaining its PID file"
|
|
370
|
-
# Let PostgreSQL check orphan shared memory before removing a dead-PID lock.
|
|
371
|
-
if [[ "$service" == postgres && "$pid" =~ ^[1-9][0-9]*$ && ! -d "/proc/$pid" ]]; then return 0; fi
|
|
372
|
-
if [[ "$service" == postgres ]]; then
|
|
373
|
-
[[ "$pid" =~ ^[1-9][0-9]*$ && "$SERVICE_UID" != 0 ]] || fail "Cannot safely recover PostgreSQL PID state"
|
|
374
|
-
# The root controller is not our nonroot postgres. Preserve the remaining
|
|
375
|
-
# lock rows so PostgreSQL's EPERM/ancestor path still checks orphan memory.
|
|
376
|
-
root sed -i "1s/.*/$$/" "$file"
|
|
377
|
-
return 0
|
|
378
|
-
fi
|
|
379
|
-
# Restored state can name an unrelated live process; never signal that PID.
|
|
380
|
-
root rm -- "$file"
|
|
381
|
-
}
|
|
382
|
-
`;
|
|
383
|
-
const postgresStart = String.raw`
|
|
384
|
-
local pg_bin='/usr/lib/postgresql/@POSTGRES@/bin' pg_data="$STATE/postgres" pg_password pg_details pg_pid pg_version
|
|
385
|
-
service_password "$STATE/postgres.password"
|
|
386
|
-
pg_password="$(cat "$STATE/postgres.password")"
|
|
387
|
-
service_directory "$pg_data"
|
|
388
|
-
for file in PG_VERSION postmaster.pid postgresql.conf pg_hba.conf pg_ident.conf postgresql.auto.conf; do
|
|
389
|
-
service_file "$pg_data/$file" "$SERVICE_UID"
|
|
390
|
-
done
|
|
391
|
-
if [[ ! -e "$pg_data/PG_VERSION" ]]; then
|
|
392
|
-
[[ -z "$(ls -A "$pg_data")" ]] || fail "PostgreSQL data directory is not initialized"
|
|
393
|
-
service_file "$STATE/postgres.init-password" "$SERVICE_UID"
|
|
394
|
-
(umask 077; printf '%s\n' "$pg_password" > "$STATE/postgres.init-password")
|
|
395
|
-
root chown stll-cloud:stll-cloud "$STATE/postgres.init-password"
|
|
396
|
-
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"
|
|
397
|
-
root rm "$STATE/postgres.init-password"
|
|
398
|
-
fi
|
|
399
|
-
[[ -f "$pg_data/PG_VERSION" && "$(cat "$pg_data/PG_VERSION")" == '@POSTGRES@' ]] || fail "PostgreSQL data has a different major version"
|
|
400
|
-
service_prune_pid "$pg_data/postmaster.pid" "$pg_bin/postgres" postgres
|
|
401
|
-
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
|
|
402
|
-
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"
|
|
403
|
-
fi
|
|
404
|
-
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"
|
|
405
|
-
pg_version="$(printf '%s\n' "$pg_details" | cut -d '|' -f4)"
|
|
406
|
-
[[ "$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"
|
|
407
|
-
(( pg_version / 10000 == @POSTGRES@ )) || fail "PostgreSQL endpoint has a different major version"
|
|
408
|
-
pg_pid="$(head -n 1 "$pg_data/postmaster.pid")"
|
|
409
|
-
service_process "$pg_pid" "$pg_bin/postgres"
|
|
410
|
-
[[ "$(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"
|
|
411
|
-
DATABASE_URL="postgresql://stll_cloud:$pg_password@127.0.0.1:55432/cloud_test"
|
|
412
|
-
`;
|
|
413
|
-
const valkeyStart = String.raw`
|
|
414
|
-
local vk_data="$STATE/valkey" vk_password vk_config vk_info vk_pid vk_expected ready=0
|
|
415
|
-
service_password "$STATE/valkey.password"
|
|
416
|
-
vk_password="$(cat "$STATE/valkey.password")"
|
|
417
|
-
service_directory "$vk_data"
|
|
418
|
-
vk_config="$vk_data/valkey.conf"
|
|
419
|
-
service_file "$vk_config" "$SERVICE_UID"
|
|
420
|
-
for file in valkey.pid valkey.log dump.rdb; do
|
|
421
|
-
[[ ! -L "$vk_data/$file" ]] || fail "Valkey state must not contain symbolic links"
|
|
422
|
-
if [[ -e "$vk_data/$file" ]]; then
|
|
423
|
-
[[ -f "$vk_data/$file" && "$(stat -c '%u' "$vk_data/$file")" == "$SERVICE_UID" ]] || fail "Invalid Valkey state file"
|
|
424
|
-
fi
|
|
425
|
-
done
|
|
426
|
-
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')"
|
|
427
|
-
if [[ ! -e "$vk_config" ]]; then
|
|
428
|
-
(umask 077; printf '%s\n' "$vk_expected" > "$vk_config")
|
|
429
|
-
root chown stll-cloud:stll-cloud "$vk_config"
|
|
430
|
-
fi
|
|
431
|
-
[[ "$(cat "$vk_config")" == "$vk_expected" ]] || fail "Valkey configuration differs from managed settings"
|
|
432
|
-
service_prune_pid "$vk_data/valkey.pid" "$(command -v valkey-server)" valkey
|
|
433
|
-
if [[ ! -e "$vk_data/valkey.pid" ]]; then
|
|
434
|
-
service_run valkey-server "$vk_config" || fail "Valkey start failed; port 56379 must be available"
|
|
435
|
-
fi
|
|
436
|
-
for ((attempt=0; attempt<20; attempt++)); do
|
|
437
|
-
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
|
|
438
|
-
if [[ "$vk_info" == *$'valkey_version:@VALKEY@\r\n'* && "$vk_info" == *"config_file:$vk_config"$'\r\n'* ]]; then ready=1; break; fi
|
|
439
|
-
fi
|
|
440
|
-
sleep 0.25
|
|
441
|
-
done
|
|
442
|
-
[[ "$ready" == 1 ]] || fail "Valkey authenticated readiness or managed version/configuration check failed"
|
|
443
|
-
[[ -f "$vk_data/valkey.pid" ]] || fail "Valkey managed process identifier is unavailable"
|
|
444
|
-
vk_pid="$(cat "$vk_data/valkey.pid")"
|
|
445
|
-
service_process "$vk_pid" "$(command -v valkey-server)"
|
|
446
|
-
[[ "$vk_info" == *"process_id:$vk_pid"$'\r\n'* ]] || fail "Valkey endpoint has a different process identifier"
|
|
447
|
-
[[ "$(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"
|
|
448
|
-
[[ "$(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"
|
|
449
|
-
REDIS_URL="redis://:$vk_password@127.0.0.1:56379/0"
|
|
450
|
-
`;
|
|
451
|
-
/** Render only declared services; installation and environment-file ownership stay with the caller. */
|
|
452
|
-
const renderCloudServiceStart = ({ services, postgres, valkey }) => {
|
|
453
|
-
if (!/^[1-9][0-9]*$/.test(postgres) || !/^\d+\.\d+\.\d+$/.test(valkey)) throw new Error("Invalid cloud service runtime policy");
|
|
454
|
-
if (services.length === 0) return "start_services() { :; }\n";
|
|
455
|
-
const bodies = services.map((service) => {
|
|
456
|
-
switch (service) {
|
|
457
|
-
case "postgres": return postgresStart.replaceAll("@POSTGRES@", postgres);
|
|
458
|
-
case "valkey": return valkeyStart.replaceAll("@VALKEY@", valkey);
|
|
459
|
-
default: throw new Error(`Unsupported cloud service: ${String(service)}`);
|
|
460
|
-
}
|
|
461
|
-
});
|
|
462
|
-
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`;
|
|
463
|
-
};
|
|
464
|
-
//#endregion
|
|
465
|
-
//#region src/toolchain-node.ts
|
|
466
|
-
const numericComponent = "(?:0|[1-9]\\d*)";
|
|
467
|
-
const policyPattern = new RegExp(`^${numericComponent}\\.x$`);
|
|
468
|
-
const selectorPattern = new RegExp(`^(${numericComponent})(?:\\.(?:x|${numericComponent}(?:\\.${numericComponent})?))?$`);
|
|
469
|
-
/** A Node policy selects the complete stable release series of one major. */
|
|
470
|
-
const nodePolicyValid = (value) => policyPattern.test(value) && (0, semver.validRange)(value) !== null;
|
|
471
|
-
/** Static selectors may narrow that series, but cannot float across majors. */
|
|
472
|
-
const nodeSelectorMatches = (value, policy) => {
|
|
473
|
-
if (!nodePolicyValid(policy) || typeof value !== "string") return false;
|
|
474
|
-
const major = selectorPattern.exec(value)?.[1];
|
|
475
|
-
return major !== void 0 && major === policy.slice(0, -2) && (0, semver.validRange)(value) !== null;
|
|
476
|
-
};
|
|
477
|
-
/** Engines must support every stable release in the approved major series. */
|
|
478
|
-
const nodeSupportRangeMatches = (policy, range) => {
|
|
479
|
-
if (!nodePolicyValid(policy)) return false;
|
|
480
|
-
try {
|
|
481
|
-
return (0, semver.subset)(policy, range);
|
|
482
|
-
} catch {
|
|
483
|
-
return false;
|
|
73
|
+
const values = /* @__PURE__ */ new Set();
|
|
74
|
+
for (const part of text.split(",")) {
|
|
75
|
+
const split = part.split("/");
|
|
76
|
+
if (split.length > 2) return void 0;
|
|
77
|
+
const [base, stepText] = split;
|
|
78
|
+
const step = stepText === void 0 ? 1 : Number(stepText);
|
|
79
|
+
if (stepText !== void 0 && !/^\d+$/.test(stepText)) return void 0;
|
|
80
|
+
if (!Number.isSafeInteger(step) || step <= 0 || base === void 0) return void 0;
|
|
81
|
+
const bounds = base.split("-");
|
|
82
|
+
if (bounds.length > 2) return void 0;
|
|
83
|
+
const lower = bounds.at(0);
|
|
84
|
+
if (lower === void 0) return void 0;
|
|
85
|
+
const start = base === "*" ? field.minimum : value(lower);
|
|
86
|
+
const upper = bounds.at(1);
|
|
87
|
+
let end = start;
|
|
88
|
+
if (base === "*" || bounds.length === 1 && stepText !== void 0) end = field.maximum;
|
|
89
|
+
else if (upper !== void 0) end = value(upper);
|
|
90
|
+
if (start === void 0 || end === void 0 || start > end) return void 0;
|
|
91
|
+
for (let selected = start; selected <= end; selected += step) values.add(selected);
|
|
484
92
|
}
|
|
93
|
+
return values;
|
|
485
94
|
};
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
# Generated by stll-cloud-setup. Regenerate with stll-cloud-setup.
|
|
492
|
-
# Networked installation and offline service startup are separate lifecycle steps.
|
|
493
|
-
# Cached filesystems may not preserve processes; run start at every session start.
|
|
494
|
-
# Supported platform: Ubuntu 24.04 (noble), amd64/arm64, root or passwordless sudo.
|
|
495
|
-
set -euo pipefail
|
|
496
|
-
|
|
497
|
-
usage() { echo 'Usage: cloud-setup.sh install|start' >&2; exit 2; }
|
|
498
|
-
[[ "$#" == 1 ]] || usage
|
|
499
|
-
case "$1" in install|start) MODE="$1" ;; *) usage ;; esac
|
|
500
|
-
fail() { echo "cloud-setup: $*" >&2; exit 1; }
|
|
501
|
-
trap 'echo "cloud-setup: command failed at line $LINENO" >&2' ERR
|
|
502
|
-
[[ -f /etc/os-release ]] || fail 'Ubuntu 24.04 is required'
|
|
503
|
-
. /etc/os-release
|
|
504
|
-
[[ "$ID" == ubuntu && "$VERSION_ID" == 24.04 && "$VERSION_CODENAME" == noble ]] || fail 'Only Ubuntu 24.04 (noble) is supported'
|
|
505
|
-
SCRIPT="$(realpath -- "$0")"
|
|
506
|
-
REPO_ROOT="$(cd -- "$(dirname -- "$SCRIPT")/.." && pwd -P)"
|
|
507
|
-
[[ "$SCRIPT" == "$REPO_ROOT/.agents/cloud-setup.sh" ]] || fail 'Run the generated repository script'
|
|
508
|
-
if [[ "$(id -u)" != 0 ]]; then
|
|
509
|
-
command -v sudo >/dev/null && sudo -n true || fail 'Root or passwordless sudo is required'
|
|
510
|
-
exec sudo -n -- bash "$SCRIPT" "$MODE"
|
|
511
|
-
fi
|
|
512
|
-
OUTPUT_UID="$(printenv SUDO_UID || id -u)"
|
|
513
|
-
[[ "$OUTPUT_UID" =~ ^[0-9]+$ ]] || fail 'Invalid invoking user'
|
|
514
|
-
OUTPUT_GID="$(getent passwd "$OUTPUT_UID" | cut -d: -f4)"
|
|
515
|
-
[[ "$OUTPUT_GID" =~ ^[0-9]+$ ]] || fail 'Invoking user is unavailable'
|
|
516
|
-
OUTPUT_USER="$(getent passwd "$OUTPUT_UID" | cut -d: -f1)"
|
|
517
|
-
OUTPUT_HOME="$(getent passwd "$OUTPUT_UID" | cut -d: -f6)"
|
|
518
|
-
[[ -n "$OUTPUT_USER" && "$OUTPUT_HOME" == /* && -d "$OUTPUT_HOME" ]] || fail 'Invoking user requires an existing home directory'
|
|
519
|
-
root() { "$@"; }
|
|
520
|
-
service_run() { runuser -u stll-cloud -- "$@" 9>&-; }
|
|
521
|
-
BUN_VERSION='@BUN@'
|
|
522
|
-
NODE_VERSION='@NODE@'
|
|
523
|
-
NODE_DIR="/opt/stll-cloud/node/$NODE_VERSION"
|
|
524
|
-
BUN_DIR="/opt/stll-cloud/bun/$BUN_VERSION"
|
|
525
|
-
export PATH="$NODE_DIR/bin:$BUN_DIR:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
|
526
|
-
BUN_CACHE="/var/cache/stll-cloud/bun/$OUTPUT_UID"
|
|
527
|
-
@ENV_ASSIGNMENTS@
|
|
528
|
-
[[ -f "$REPO_ROOT/.node-version" && ! -L "$REPO_ROOT/.node-version" ]] || fail 'A regular root .node-version is required'
|
|
529
|
-
[[ "$(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'
|
|
530
|
-
cd -- "$REPO_ROOT"
|
|
531
|
-
case "$(uname -m)" in
|
|
532
|
-
x86_64) ARCH=amd64; NODE_ARCH=x64; BUN_ARCH=x64 ;;
|
|
533
|
-
aarch64|arm64) ARCH=arm64; NODE_ARCH=arm64; BUN_ARCH=aarch64 ;;
|
|
534
|
-
*) fail 'Only amd64 and arm64 are supported' ;;
|
|
535
|
-
esac
|
|
536
|
-
|
|
537
|
-
runtime_ready() {
|
|
538
|
-
[[ -x "$NODE_DIR/bin/node" && "$($NODE_DIR/bin/node --version)" == "v$NODE_VERSION" ]] || fail 'Pinned Node is unavailable; run install'
|
|
539
|
-
[[ -x "$BUN_DIR/bun" && "$($BUN_DIR/bun --version)" == "$BUN_VERSION" ]] || fail 'Pinned Bun is unavailable; run install'
|
|
540
|
-
}
|
|
541
|
-
install_runtimes() {
|
|
542
|
-
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
|
|
543
|
-
apt-get update
|
|
544
|
-
DEBIAN_FRONTEND=noninteractive apt_install ca-certificates curl unzip xz-utils openssl gnupg util-linux
|
|
545
|
-
fi
|
|
546
|
-
local scratch node_archive bun_archive bun_folder
|
|
547
|
-
scratch="$(mktemp -d)"
|
|
548
|
-
trap 'rm -rf -- "$scratch"' RETURN
|
|
549
|
-
if [[ ! -x "$NODE_DIR/bin/node" ]] || [[ "$($NODE_DIR/bin/node --version)" != "v$NODE_VERSION" ]]; then
|
|
550
|
-
node_archive="node-v$NODE_VERSION-linux-$NODE_ARCH.tar.xz"
|
|
551
|
-
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"
|
|
552
|
-
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"
|
|
553
|
-
(cd "$scratch"; awk -v name="$node_archive" '$2 == name { print; count++ } END { if (count != 1) exit 1 }' node-sums | sha256sum --strict -c -)
|
|
554
|
-
install -d -m 755 "$NODE_DIR"
|
|
555
|
-
tar -xJf "$scratch/$node_archive" --strip-components=1 -C "$NODE_DIR"
|
|
556
|
-
fi
|
|
557
|
-
if [[ ! -x "$BUN_DIR/bun" ]] || [[ "$($BUN_DIR/bun --version)" != "$BUN_VERSION" ]]; then
|
|
558
|
-
bun_folder="bun-linux-$BUN_ARCH"
|
|
559
|
-
bun_archive="$bun_folder.zip"
|
|
560
|
-
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"
|
|
561
|
-
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"
|
|
562
|
-
(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 -)
|
|
563
|
-
unzip -q "$scratch/$bun_archive" -d "$scratch/bun"
|
|
564
|
-
install -d -m 755 "$BUN_DIR"
|
|
565
|
-
install -m 755 "$scratch/bun/$bun_folder/bun" "$BUN_DIR/bun"
|
|
566
|
-
fi
|
|
567
|
-
runtime_ready
|
|
568
|
-
ln -sfnT -- "$NODE_DIR/bin/node" /usr/local/bin/node
|
|
569
|
-
ln -sfnT -- "$NODE_DIR/bin/npm" /usr/local/bin/npm
|
|
570
|
-
ln -sfnT -- "$NODE_DIR/bin/npx" /usr/local/bin/npx
|
|
571
|
-
ln -sfnT -- "$BUN_DIR/bun" /usr/local/bin/bun
|
|
572
|
-
rm -rf -- "$scratch"
|
|
573
|
-
trap - RETURN
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
dependency_install() {
|
|
577
|
-
local directory
|
|
578
|
-
for directory in /var/cache/stll-cloud /var/cache/stll-cloud/bun; do
|
|
579
|
-
[[ ! -L "$directory" ]] || fail 'Dependency cache parent must not be a symlink'
|
|
580
|
-
if [[ -e "$directory" ]]; then
|
|
581
|
-
[[ -d "$directory" && "$(stat -c '%u' "$directory")" == 0 ]] || fail 'Dependency cache parent must be root-owned'
|
|
582
|
-
fi
|
|
583
|
-
install -d -m 755 -o root -g root "$directory"
|
|
584
|
-
done
|
|
585
|
-
[[ ! -L "$BUN_CACHE" ]] || fail 'Dependency cache must not be a symlink'
|
|
586
|
-
if [[ -e "$BUN_CACHE" ]]; then
|
|
587
|
-
[[ -d "$BUN_CACHE" && "$(stat -c '%u' "$BUN_CACHE")" == "$OUTPUT_UID" ]] || fail 'Dependency cache ownership differs from the invoking user'
|
|
588
|
-
fi
|
|
589
|
-
install -d -m 700 -o "$OUTPUT_UID" -g "$OUTPUT_GID" "$BUN_CACHE"
|
|
590
|
-
runuser -u "$OUTPUT_USER" -- env HOME="$OUTPUT_HOME" BUN_INSTALL_CACHE_DIR="$BUN_CACHE" PATH="$PATH" "$BUN_DIR/bun" install --frozen-lockfile
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
@SERVICE_INSTALL@
|
|
594
|
-
@SERVICE_START@
|
|
595
|
-
|
|
596
|
-
@ENV_FUNCTIONS@
|
|
597
|
-
|
|
598
|
-
if [[ "$MODE" == install ]]; then
|
|
599
|
-
install_runtimes
|
|
600
|
-
install_services
|
|
601
|
-
dependency_install
|
|
602
|
-
exit 0
|
|
603
|
-
fi
|
|
604
|
-
runtime_ready
|
|
605
|
-
@ENV_VALIDATE@
|
|
606
|
-
prepare_state
|
|
607
|
-
start_services
|
|
608
|
-
@ENV_WRITE@
|
|
609
|
-
`;
|
|
610
|
-
const environmentFunctions = String.raw`
|
|
611
|
-
validate_environment() {
|
|
612
|
-
local part target="$REPO_ROOT" env_path="$REPO_ROOT/$ENV_FILE" first_line temporary
|
|
613
|
-
while IFS= read -r part; do
|
|
614
|
-
target="$target/$part"
|
|
615
|
-
[[ ! -L "$target" ]] || fail 'Environment path must not contain symbolic links'
|
|
616
|
-
done < <(printf '%s\n' "$ENV_FILE" | tr '/' '\n')
|
|
617
|
-
[[ -d "$(dirname -- "$env_path")" ]] || fail 'Environment parent directory is missing'
|
|
618
|
-
if [[ -e "$env_path" ]]; then
|
|
619
|
-
[[ -f "$env_path" ]] || fail 'Environment target must be a regular file'
|
|
620
|
-
IFS= read -r first_line < "$env_path" || fail 'Refusing to overwrite an unowned environment file'
|
|
621
|
-
[[ "$first_line" == "$ENV_MARKER" ]] || fail 'Refusing to overwrite an unowned environment file'
|
|
622
|
-
fi
|
|
623
|
-
}
|
|
624
|
-
write_environment() {
|
|
625
|
-
validate_environment
|
|
626
|
-
local env_path="$REPO_ROOT/$ENV_FILE" temporary
|
|
627
|
-
temporary="$(mktemp "$(dirname -- "$env_path")/.stll-cloud-env.XXXXXX")"
|
|
628
|
-
chmod 600 "$temporary"
|
|
629
|
-
{
|
|
630
|
-
printf '%s\n' "$ENV_MARKER" 'NODE_ENV=test'
|
|
631
|
-
if [[ -n "$DATABASE_URL" ]]; then printf 'DATABASE_URL=%s\n' "$DATABASE_URL"; fi
|
|
632
|
-
if [[ -n "$REDIS_URL" ]]; then printf 'REDIS_URL=%s\n' "$REDIS_URL"; fi
|
|
633
|
-
} > "$temporary"
|
|
634
|
-
chown "$OUTPUT_UID:$OUTPUT_GID" "$temporary"
|
|
635
|
-
mv -f -- "$temporary" "$env_path"
|
|
636
|
-
}
|
|
637
|
-
`;
|
|
638
|
-
const shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
639
|
-
const generateCloudSetup = ({ policy, cloud, nodeVersion }) => {
|
|
640
|
-
const selected = parseCloudSetup(cloud);
|
|
641
|
-
if (selected === void 0) throw new Error("cloud declaration is required");
|
|
642
|
-
const exactNode = nodeVersion.trim();
|
|
643
|
-
if ((0, semver.valid)(exactNode) !== exactNode || !nodeSelectorMatches(exactNode, policy.node)) throw new Error("cloud requires a tracked exact .node-version within the shared Node series");
|
|
644
|
-
const services = cloudServices.filter((service) => selected.services.includes(service));
|
|
645
|
-
const replacements = /* @__PURE__ */ new Map([
|
|
646
|
-
["@BUN@", policy.bun],
|
|
647
|
-
["@NODE@", exactNode],
|
|
648
|
-
["@ENV_ASSIGNMENTS@", selected.envFile === void 0 ? "" : `ENV_FILE=${shellQuote(selected.envFile)}\nENV_MARKER=${shellQuote(cloudEnvironmentMarker)}`],
|
|
649
|
-
["@ENV_FUNCTIONS@", selected.envFile === void 0 ? "" : environmentFunctions],
|
|
650
|
-
["@ENV_VALIDATE@", selected.envFile === void 0 ? "" : "DATABASE_URL=''\nREDIS_URL=''\nvalidate_environment"],
|
|
651
|
-
["@ENV_WRITE@", selected.envFile === void 0 ? "" : "write_environment"],
|
|
652
|
-
["@SERVICE_INSTALL@", renderCloudServiceInstall({
|
|
653
|
-
services,
|
|
654
|
-
postgres: policy.postgres,
|
|
655
|
-
valkey: policy.valkey,
|
|
656
|
-
valkeyArtifacts: policy.valkeyArtifacts
|
|
657
|
-
})],
|
|
658
|
-
["@SERVICE_START@", renderCloudServiceStart({
|
|
659
|
-
services,
|
|
660
|
-
postgres: policy.postgres,
|
|
661
|
-
valkey: policy.valkey
|
|
662
|
-
})]
|
|
663
|
-
]);
|
|
664
|
-
return script.replace(/@[A-Z0-9_]+@/g, (token) => {
|
|
665
|
-
const value = replacements.get(token);
|
|
666
|
-
if (value === void 0) throw new Error(`Missing cloud setup field: ${token}`);
|
|
667
|
-
return value;
|
|
95
|
+
const validConsumerCron = (text) => {
|
|
96
|
+
const fields = text.trim().split(/\s+/);
|
|
97
|
+
return fields.length === cronFields.length && cronFields.every((field, index) => {
|
|
98
|
+
const fieldText = fields.at(index);
|
|
99
|
+
return fieldText !== void 0 && cronFieldValues(fieldText, field) !== void 0;
|
|
668
100
|
});
|
|
669
101
|
};
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
const
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
"template",
|
|
682
|
-
"spec"
|
|
683
|
-
],
|
|
684
|
-
DaemonSet: [
|
|
685
|
-
"spec",
|
|
686
|
-
"template",
|
|
687
|
-
"spec"
|
|
688
|
-
],
|
|
689
|
-
StatefulSet: [
|
|
690
|
-
"spec",
|
|
691
|
-
"template",
|
|
692
|
-
"spec"
|
|
693
|
-
],
|
|
694
|
-
ReplicaSet: [
|
|
695
|
-
"spec",
|
|
696
|
-
"template",
|
|
697
|
-
"spec"
|
|
698
|
-
],
|
|
699
|
-
ReplicationController: [
|
|
700
|
-
"spec",
|
|
701
|
-
"template",
|
|
702
|
-
"spec"
|
|
703
|
-
],
|
|
704
|
-
Job: [
|
|
705
|
-
"spec",
|
|
706
|
-
"template",
|
|
707
|
-
"spec"
|
|
708
|
-
],
|
|
709
|
-
CronJob: [
|
|
710
|
-
"spec",
|
|
711
|
-
"jobTemplate",
|
|
712
|
-
"spec",
|
|
713
|
-
"template",
|
|
714
|
-
"spec"
|
|
715
|
-
]
|
|
716
|
-
};
|
|
717
|
-
/** Shared semantic image enumeration for runtime checks and update-root generation. */
|
|
718
|
-
const containerDocumentImages = (document, file) => {
|
|
719
|
-
if (!record$6(document)) return void 0;
|
|
720
|
-
const services = document["services"];
|
|
721
|
-
if (isComposeDefinitionPath(file) && record$6(services)) {
|
|
722
|
-
const images = [];
|
|
723
|
-
for (const [name, service] of Object.entries(services)) if (record$6(service) && service["image"] !== void 0) images.push({
|
|
724
|
-
image: service["image"],
|
|
725
|
-
path: [
|
|
726
|
-
"services",
|
|
727
|
-
name,
|
|
728
|
-
"image"
|
|
729
|
-
]
|
|
102
|
+
const nightlySchedule = (value) => {
|
|
103
|
+
if (!record$6(value)) return false;
|
|
104
|
+
const schedule = value["schedule"];
|
|
105
|
+
if (!Array.isArray(schedule) || schedule.length === 0) return false;
|
|
106
|
+
const coveredDays = /* @__PURE__ */ new Set();
|
|
107
|
+
for (const entry of schedule) {
|
|
108
|
+
if (!record$6(entry) || typeof entry["cron"] !== "string" || !validConsumerCron(entry["cron"])) return false;
|
|
109
|
+
const fields = entry["cron"].trim().split(/\s+/);
|
|
110
|
+
const selections = cronFields.map((field, index) => {
|
|
111
|
+
const text = fields.at(index);
|
|
112
|
+
return text === void 0 ? void 0 : cronFieldValues(text, field);
|
|
730
113
|
});
|
|
114
|
+
if (selections.at(2)?.size !== 31 || selections.at(3)?.size !== 12) continue;
|
|
115
|
+
for (const day of selections.at(4) ?? []) coveredDays.add(day);
|
|
116
|
+
}
|
|
117
|
+
return coveredDays.size === 7;
|
|
118
|
+
};
|
|
119
|
+
const parseConsumerChecks = (input) => {
|
|
120
|
+
if (input === void 0) return [];
|
|
121
|
+
if (!Array.isArray(input)) throw new Error("consumerChecks must be an array");
|
|
122
|
+
const targets = /* @__PURE__ */ new Set();
|
|
123
|
+
return input.map((entry) => {
|
|
124
|
+
if (!record$6(entry) || Object.keys(entry).some((key) => ![
|
|
125
|
+
"workflow",
|
|
126
|
+
"job",
|
|
127
|
+
"packages",
|
|
128
|
+
"toolingVersion",
|
|
129
|
+
"fixturePath"
|
|
130
|
+
].includes(key)) || typeof entry["workflow"] !== "string" || require_toolchain_inputs.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");
|
|
131
|
+
const target = `${entry["workflow"]}:${entry["job"]}`;
|
|
132
|
+
if (targets.has(target)) throw new Error("consumerChecks targets must be unique");
|
|
133
|
+
targets.add(target);
|
|
731
134
|
return {
|
|
732
|
-
|
|
733
|
-
|
|
135
|
+
workflow: entry["workflow"],
|
|
136
|
+
job: entry["job"],
|
|
137
|
+
packages: entry["packages"],
|
|
138
|
+
toolingVersion: entry["toolingVersion"],
|
|
139
|
+
fixturePath: entry["fixturePath"]
|
|
734
140
|
};
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
let spec = resource;
|
|
754
|
-
for (const key of specPath) spec = record$6(spec) ? spec[key] : void 0;
|
|
755
|
-
if (!record$6(spec)) return;
|
|
756
|
-
for (const key of [
|
|
757
|
-
"containers",
|
|
758
|
-
"initContainers",
|
|
759
|
-
"ephemeralContainers"
|
|
760
|
-
]) {
|
|
761
|
-
const containers = spec[key];
|
|
762
|
-
if (!Array.isArray(containers)) continue;
|
|
763
|
-
for (const [index, container] of containers.entries()) if (record$6(container) && container["image"] !== void 0) images.push({
|
|
764
|
-
image: container["image"],
|
|
765
|
-
path: [
|
|
766
|
-
...prefix,
|
|
767
|
-
...specPath,
|
|
768
|
-
key,
|
|
769
|
-
index,
|
|
770
|
-
"image"
|
|
771
|
-
]
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
/** Consumer checks are scoped declarations, never general runtime exceptions. */
|
|
144
|
+
const checkConsumerChecks = ({ declarations, files, policy }) => {
|
|
145
|
+
const diagnostics = [];
|
|
146
|
+
const exercised = /* @__PURE__ */ new Set();
|
|
147
|
+
const add = (message) => diagnostics.push({
|
|
148
|
+
rule: "configuration",
|
|
149
|
+
path: "stll-toolchain.json",
|
|
150
|
+
line: 1,
|
|
151
|
+
message
|
|
152
|
+
});
|
|
153
|
+
for (const [workflow, text] of Object.entries(files)) {
|
|
154
|
+
if (require_toolchain_inputs.githubAutomationFileKind(workflow) !== "workflow") continue;
|
|
155
|
+
try {
|
|
156
|
+
const document = (0, yaml.parseDocument)(text, {
|
|
157
|
+
uniqueKeys: true,
|
|
158
|
+
merge: true
|
|
772
159
|
});
|
|
160
|
+
if (document.errors.length > 0) throw new Error("invalid workflow YAML");
|
|
161
|
+
const source = document.toJS({ maxAliasCount: 100 });
|
|
162
|
+
if (!record$6(source) || !record$6(source["jobs"])) continue;
|
|
163
|
+
for (const [jobName, job] of Object.entries(source["jobs"])) {
|
|
164
|
+
if (!record$6(job) || typeof job["uses"] !== "string") continue;
|
|
165
|
+
const [workflowPath, ref] = job["uses"].split("@");
|
|
166
|
+
if (workflowPath?.toLowerCase() !== "stella/.github/.github/workflows/package-consumer-compat.yml") continue;
|
|
167
|
+
const declaration = declarations.find((entry) => entry.workflow === workflow && entry.job === jobName);
|
|
168
|
+
if (declaration === void 0) {
|
|
169
|
+
add(`${workflow}:${jobName} requires a consumerChecks declaration`);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (!nightlySchedule(source["on"])) throw new Error(`${workflow} consumer checks require a nonempty valid on.schedule covering every day`);
|
|
173
|
+
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`);
|
|
174
|
+
exercised.add(declaration);
|
|
175
|
+
const approved = Object.entries(policy.actions).find(([name]) => name.toLowerCase() === consumerCheckWorkflow)?.[1];
|
|
176
|
+
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`);
|
|
177
|
+
const inputs = job["with"];
|
|
178
|
+
if (!record$6(inputs)) throw new Error(`${workflow}:${jobName} requires consumer inputs`);
|
|
179
|
+
const names = /* @__PURE__ */ new Set();
|
|
180
|
+
for (const key of Object.keys(inputs)) {
|
|
181
|
+
const normalized = key.toLowerCase();
|
|
182
|
+
if (names.has(normalized) || ![
|
|
183
|
+
"packages",
|
|
184
|
+
"consumer-node",
|
|
185
|
+
"tooling-version",
|
|
186
|
+
"fixture-path"
|
|
187
|
+
].includes(normalized) || normalized !== key) throw new Error(`${workflow}:${jobName} requires unique canonical consumer input names`);
|
|
188
|
+
names.add(normalized);
|
|
189
|
+
}
|
|
190
|
+
if (inputs["consumer-node"] !== policy.consumerNode) throw new Error(`${workflow}:${jobName} consumer-node must be ${policy.consumerNode}`);
|
|
191
|
+
if (declaration.toolingVersion !== consumerRunnerVersion) throw new Error(`${workflow}:${jobName} toolingVersion must match installed @stll/oxlint-config ${consumerRunnerVersion}`);
|
|
192
|
+
if (inputs["tooling-version"] !== declaration.toolingVersion) throw new Error(`${workflow}:${jobName} tooling-version must match consumerChecks.toolingVersion`);
|
|
193
|
+
if (inputs["fixture-path"] !== declaration.fixturePath) throw new Error(`${workflow}:${jobName} fixture-path must match consumerChecks.fixturePath`);
|
|
194
|
+
const fixtureManifest = node_path.default.posix.join(declaration.fixturePath, "consumer-compat.json");
|
|
195
|
+
const fixtureSource = files[fixtureManifest];
|
|
196
|
+
if (fixtureSource === void 0) throw new Error(`consumerChecks fixture configuration must be tracked: ${fixtureManifest}`);
|
|
197
|
+
const fixtures = require_toolchain_schema.parseConsumerFixtures(JSON.parse(fixtureSource));
|
|
198
|
+
require_toolchain_schema.assertConsumerFixtureSelection({
|
|
199
|
+
selected: declaration.packages,
|
|
200
|
+
fixtures
|
|
201
|
+
});
|
|
202
|
+
const rawPackages = inputs["packages"];
|
|
203
|
+
if (typeof rawPackages !== "string") throw new Error(`${workflow}:${jobName} packages must be a static JSON array`);
|
|
204
|
+
const selected = JSON.parse(rawPackages);
|
|
205
|
+
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`);
|
|
206
|
+
const workspacePackages = require_toolchain_schema.discoverConsumerPackages(files);
|
|
207
|
+
const packagesByDirectory = new Map([...workspacePackages.values()].map((entry) => [entry.directory, entry]));
|
|
208
|
+
for (const directory of declaration.packages) {
|
|
209
|
+
const manifestPath = node_path.default.posix.join(directory, "package.json");
|
|
210
|
+
if (files[manifestPath] === void 0) throw new Error(`consumerChecks package manifest must be tracked: ${manifestPath}`);
|
|
211
|
+
const selectedPackage = packagesByDirectory.get(directory);
|
|
212
|
+
if (selectedPackage === void 0) throw new Error(`consumerChecks package must be a discovered workspace member: ${manifestPath}`);
|
|
213
|
+
const fixture = fixtures.find((candidate) => candidate.package === directory);
|
|
214
|
+
if (fixture === void 0) throw new Error(`missing declared consumer fixture: ${directory}`);
|
|
215
|
+
require_toolchain_schema.assertConsumerFixtureKind({
|
|
216
|
+
fixture,
|
|
217
|
+
pkg: selectedPackage
|
|
218
|
+
});
|
|
219
|
+
const manifest = selectedPackage.manifest;
|
|
220
|
+
if (manifest["private"] === true || typeof manifest["name"] !== "string" || manifest["name"].trim() === "") throw new Error(`consumerChecks requires a named published package: ${manifestPath}`);
|
|
221
|
+
const closure = require_toolchain_schema.consumerPackageClosure({
|
|
222
|
+
selected: selectedPackage,
|
|
223
|
+
packages: workspacePackages,
|
|
224
|
+
files
|
|
225
|
+
});
|
|
226
|
+
for (const member of closure.values()) require_toolchain_schema.assertConsumerNodeSupport({
|
|
227
|
+
manifest: member.manifest,
|
|
228
|
+
node: policy.consumerNode,
|
|
229
|
+
label: node_path.default.posix.join(member.directory, "package.json")
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} catch (error) {
|
|
234
|
+
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));
|
|
773
235
|
}
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
return
|
|
777
|
-
ecosystem: "docker",
|
|
778
|
-
images
|
|
779
|
-
};
|
|
236
|
+
}
|
|
237
|
+
for (const declaration of declarations) if (!exercised.has(declaration)) add(`${declaration.workflow}:${declaration.job} consumerChecks declaration has no matching reusable invocation`);
|
|
238
|
+
return diagnostics;
|
|
780
239
|
};
|
|
781
240
|
//#endregion
|
|
782
241
|
//#region src/toolchain-images.ts
|
|
@@ -801,66 +260,6 @@ const canonicalDockerRuntime = (image) => {
|
|
|
801
260
|
return (registry === void 0 ? image : image.slice(registry.length + 1)).replace(/^library\//i, "");
|
|
802
261
|
};
|
|
803
262
|
//#endregion
|
|
804
|
-
//#region src/toolchain-inputs.ts
|
|
805
|
-
const excludedInputPath = (file) => file === "" || file.split("/").some((part) => part === "node_modules" || part === "vendor");
|
|
806
|
-
const javascriptDependencyLockfiles = [
|
|
807
|
-
"bun.lock",
|
|
808
|
-
"package-lock.json",
|
|
809
|
-
"yarn.lock",
|
|
810
|
-
"pnpm-lock.yaml"
|
|
811
|
-
];
|
|
812
|
-
/** Match project, environment, local, and fragment filenames recognized by mise. */
|
|
813
|
-
const isMiseConfigPath = (file) => !excludedInputPath(file) && (/(?:^|\/)\.?mise(?:\.[^./][^/]*)?\.toml$/.test(file) || /(?:^|\/)(?:\.?mise|\.config\/mise)\/config(?:\.[^./][^/]*)?\.toml$/.test(file) || /(?:^|\/)(?:\.?mise|\.config\/mise)\/conf\.d\/[^./][^/]*\.toml$/.test(file));
|
|
814
|
-
/** GitHub action metadata may live at the repository root or in any directory. */
|
|
815
|
-
const githubAutomationFileKind = (file) => {
|
|
816
|
-
if (file === "") return void 0;
|
|
817
|
-
if (/^\.github\/workflows\/[^/]+\.ya?ml$/.test(file)) return "workflow";
|
|
818
|
-
if (/(?:^|\/)action\.ya?ml$/.test(file)) return "action";
|
|
819
|
-
};
|
|
820
|
-
/** Dependabot updates workflows and repository-root action metadata. */
|
|
821
|
-
const isDependabotGithubActionsPath = (file) => {
|
|
822
|
-
const kind = githubAutomationFileKind(file);
|
|
823
|
-
return kind === "workflow" || kind === "action" && !file.includes("/");
|
|
824
|
-
};
|
|
825
|
-
/** Use one filename discriminator for reader selection and Python ecosystem decisions. */
|
|
826
|
-
const pythonDependencyManifestKind = (file) => {
|
|
827
|
-
if (excludedInputPath(file)) return void 0;
|
|
828
|
-
const name = file.split("/").at(-1);
|
|
829
|
-
if (name === "pyproject.toml") return "project";
|
|
830
|
-
if (name === "Pipfile") return "pipfile";
|
|
831
|
-
if (name === "Pipfile.lock") return "pipfile-lock";
|
|
832
|
-
if (name === "setup.py" || name === "setup.cfg") return "setup";
|
|
833
|
-
if (name !== void 0 && /^requirements[^/]*\.(?:txt|in)$/.test(name)) return "requirements";
|
|
834
|
-
};
|
|
835
|
-
const isPythonDependencyManifest = (file) => pythonDependencyManifestKind(file) !== void 0;
|
|
836
|
-
/** Docker definition names use dot-delimited variants, excluding source and documentation. */
|
|
837
|
-
const isDockerDefinitionPath = (file) => {
|
|
838
|
-
if (excludedInputPath(file)) return false;
|
|
839
|
-
const name = file.split("/").at(-1) ?? "";
|
|
840
|
-
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;
|
|
841
|
-
return /^(?:[^./]+\.)*(?:dockerfile|containerfile)(?:\.[^./]+)*$/i.test(name);
|
|
842
|
-
};
|
|
843
|
-
/** Lockfiles establish ecosystem presence without reading their dependency graphs. */
|
|
844
|
-
const toolchainInputKind = (file) => {
|
|
845
|
-
if (file === ".agents/cloud-setup.sh") return "config";
|
|
846
|
-
if (githubAutomationFileKind(file) !== void 0) return "config";
|
|
847
|
-
if (excludedInputPath(file)) return void 0;
|
|
848
|
-
if (pythonDependencyManifestKind(file) === "pipfile-lock" || javascriptDependencyLockfiles.some((name) => file.split("/").at(-1) === name) || /(?:^|\/)uv\.lock$/.test(file)) return "presence";
|
|
849
|
-
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";
|
|
850
|
-
};
|
|
851
|
-
//#endregion
|
|
852
|
-
//#region src/toolchain-workspaces.ts
|
|
853
|
-
/** Match manifest directories with the same positive and negative workspace patterns. */
|
|
854
|
-
const workspaceContains = ({ directory, file, patterns, rootMembership }) => {
|
|
855
|
-
const relative = node_path.default.posix.relative(directory, node_path.default.posix.dirname(file));
|
|
856
|
-
if (relative === "" && rootMembership === "implicit") return true;
|
|
857
|
-
if (relative === ".." || relative.startsWith("../")) return false;
|
|
858
|
-
if (!Array.isArray(patterns)) return false;
|
|
859
|
-
const normalize = (pattern) => node_path.default.posix.normalize(pattern);
|
|
860
|
-
const selected = patterns.filter((value) => typeof value === "string");
|
|
861
|
-
return (0, picomatch.default)(selected.filter((pattern) => !pattern.startsWith("!")).map(normalize))(relative) && !(0, picomatch.default)(selected.filter((pattern) => pattern.startsWith("!")).map((pattern) => normalize(pattern.slice(1))))(relative);
|
|
862
|
-
};
|
|
863
|
-
//#endregion
|
|
864
263
|
//#region src/toolchain-dependabot.ts
|
|
865
264
|
const dependabotRules = ["dependabot-policy"];
|
|
866
265
|
const record$5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -933,7 +332,7 @@ const workspaceRoots = (files, ecosystem) => {
|
|
|
933
332
|
const declaredMember = (root, patterns, directory) => {
|
|
934
333
|
const relative = node_path.default.posix.relative(root, directory);
|
|
935
334
|
if (relative === "" || relative === ".." || relative.startsWith("../")) return false;
|
|
936
|
-
return workspaceContains({
|
|
335
|
+
return require_toolchain_inputs.workspaceContains({
|
|
937
336
|
directory: root,
|
|
938
337
|
file: node_path.default.posix.join(directory, "package.json"),
|
|
939
338
|
patterns,
|
|
@@ -944,7 +343,7 @@ const workspaceRoots = (files, ecosystem) => {
|
|
|
944
343
|
if (ecosystem === "npm") {
|
|
945
344
|
for (const file of manifests) {
|
|
946
345
|
const directory = node_path.default.posix.dirname(file);
|
|
947
|
-
if (javascriptDependencyLockfiles.some((name) => files[node_path.default.posix.join(directory, name)] !== void 0)) installable.add(directory);
|
|
346
|
+
if (require_toolchain_inputs.javascriptDependencyLockfiles.some((name) => files[node_path.default.posix.join(directory, name)] !== void 0)) installable.add(directory);
|
|
948
347
|
}
|
|
949
348
|
let changed = true;
|
|
950
349
|
while (changed) {
|
|
@@ -1025,7 +424,7 @@ const javascriptEcosystem = (directory, files) => {
|
|
|
1025
424
|
return "npm";
|
|
1026
425
|
};
|
|
1027
426
|
const pythonEcosystem = (file, files) => {
|
|
1028
|
-
const manifestKind = pythonDependencyManifestKind(file);
|
|
427
|
+
const manifestKind = require_toolchain_inputs.pythonDependencyManifestKind(file);
|
|
1029
428
|
if (manifestKind === "pipfile" || manifestKind === "pipfile-lock" || manifestKind === "setup") return "pip";
|
|
1030
429
|
const root = node_path.default.posix.dirname(file);
|
|
1031
430
|
if (files[node_path.default.posix.join(root, "uv.lock")] !== void 0 || files[node_path.default.posix.join(root, "uv.toml")] !== void 0) return "uv";
|
|
@@ -1050,17 +449,17 @@ const ecosystemRoots = (files) => {
|
|
|
1050
449
|
requireRoot(pythonEcosystem(node_path.default.posix.join(root, "pyproject.toml"), files), directory);
|
|
1051
450
|
}
|
|
1052
451
|
for (const file of Object.keys(files)) {
|
|
1053
|
-
if (isDependabotGithubActionsPath(file)) requireRoot("github-actions", "/");
|
|
452
|
+
if (require_toolchain_inputs.isDependabotGithubActionsPath(file)) requireRoot("github-actions", "/");
|
|
1054
453
|
const name = node_path.default.posix.basename(file);
|
|
1055
|
-
if (isDockerDefinitionPath(file)) requireRoot("docker", directoryOf(file));
|
|
1056
|
-
if (isComposeDefinitionPath(file) || isKubernetesDefinitionPath(file)) for (const document of (0, yaml.parseAllDocuments)(files[file] ?? "", { merge: true })) {
|
|
454
|
+
if (require_toolchain_inputs.isDockerDefinitionPath(file)) requireRoot("docker", directoryOf(file));
|
|
455
|
+
if (require_toolchain_inputs.isComposeDefinitionPath(file) || require_toolchain_inputs.isKubernetesDefinitionPath(file)) for (const document of (0, yaml.parseAllDocuments)(files[file] ?? "", { merge: true })) {
|
|
1057
456
|
if (document.errors.length > 0) continue;
|
|
1058
457
|
try {
|
|
1059
|
-
const classified = containerDocumentImages(document.toJS({ maxAliasCount: 100 }), file);
|
|
458
|
+
const classified = require_toolchain_inputs.containerDocumentImages(document.toJS({ maxAliasCount: 100 }), file);
|
|
1060
459
|
if (classified !== void 0) requireRoot(classified.ecosystem, directoryOf(file));
|
|
1061
460
|
} catch {}
|
|
1062
461
|
}
|
|
1063
|
-
if (name === "uv.lock" || name === "uv.toml" || name !== "pyproject.toml" && isPythonDependencyManifest(file)) requireRoot(pythonEcosystem(file, files), directoryOf(file));
|
|
462
|
+
if (name === "uv.lock" || name === "uv.toml" || name !== "pyproject.toml" && require_toolchain_inputs.isPythonDependencyManifest(file)) requireRoot(pythonEcosystem(file, files), directoryOf(file));
|
|
1064
463
|
}
|
|
1065
464
|
return roots;
|
|
1066
465
|
};
|
|
@@ -1184,8 +583,57 @@ const checkDependabot = ({ files, policy }) => {
|
|
|
1184
583
|
return diagnostics;
|
|
1185
584
|
};
|
|
1186
585
|
//#endregion
|
|
1187
|
-
//#region src/toolchain-
|
|
586
|
+
//#region src/toolchain-dynamic-selectors.ts
|
|
587
|
+
const locatorValidators = {
|
|
588
|
+
image: /^(?:jobs\.[\w-]+\.(?:container|services\.[\w-]+)|services\.[\w.-]+\.image)$/,
|
|
589
|
+
"bun-source": /^(?:jobs\.[\w-]+|runs)\.steps\.[\w-]+$/
|
|
590
|
+
};
|
|
591
|
+
const knownKind = (value) => typeof value === "string" && Object.hasOwn(locatorValidators, value);
|
|
1188
592
|
const record$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
593
|
+
/** One reviewed declaration identifies one unresolved selector, never a rule. */
|
|
594
|
+
const parseDynamicSelectors = (input) => {
|
|
595
|
+
if (input === void 0) return [];
|
|
596
|
+
if (!Array.isArray(input)) throw new Error("dynamicSelectors must be an array");
|
|
597
|
+
const result = [];
|
|
598
|
+
const keys = /* @__PURE__ */ new Set();
|
|
599
|
+
for (const entry of input) {
|
|
600
|
+
if (!record$4(entry) || Object.keys(entry).some((key) => ![
|
|
601
|
+
"path",
|
|
602
|
+
"kind",
|
|
603
|
+
"reason",
|
|
604
|
+
"at",
|
|
605
|
+
"line"
|
|
606
|
+
].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");
|
|
607
|
+
const kind = entry["kind"];
|
|
608
|
+
if (!knownKind(kind)) throw new Error("unknown dynamic selector kind");
|
|
609
|
+
let selector;
|
|
610
|
+
if (Object.hasOwn(entry, "line") && !Object.hasOwn(entry, "at") && typeof entry["line"] === "number" && Number.isSafeInteger(entry["line"]) && entry["line"] > 0) selector = {
|
|
611
|
+
path: entry["path"],
|
|
612
|
+
kind,
|
|
613
|
+
reason: entry["reason"],
|
|
614
|
+
line: entry["line"]
|
|
615
|
+
};
|
|
616
|
+
else if (!Object.hasOwn(entry, "line") && typeof entry["at"] === "string" && locatorValidators[kind].test(entry["at"])) selector = {
|
|
617
|
+
path: entry["path"],
|
|
618
|
+
kind,
|
|
619
|
+
reason: entry["reason"],
|
|
620
|
+
at: entry["at"]
|
|
621
|
+
};
|
|
622
|
+
else throw new Error("dynamic selector requires one positive line or a kind-specific job/service/step ID locator");
|
|
623
|
+
const key = JSON.stringify([
|
|
624
|
+
selector.path,
|
|
625
|
+
selector.kind,
|
|
626
|
+
selector.at ?? selector.line
|
|
627
|
+
]);
|
|
628
|
+
if (keys.has(key)) throw new Error("dynamic selector locations must be unique");
|
|
629
|
+
keys.add(key);
|
|
630
|
+
result.push(selector);
|
|
631
|
+
}
|
|
632
|
+
return result;
|
|
633
|
+
};
|
|
634
|
+
//#endregion
|
|
635
|
+
//#region src/toolchain-engine-floors.ts
|
|
636
|
+
const record$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1189
637
|
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 !== "..");
|
|
1190
638
|
const parseEngineFloors = (input) => {
|
|
1191
639
|
if (input === void 0) return [];
|
|
@@ -1193,7 +641,7 @@ const parseEngineFloors = (input) => {
|
|
|
1193
641
|
const result = [];
|
|
1194
642
|
const jobs = /* @__PURE__ */ new Set();
|
|
1195
643
|
for (const entry of input) {
|
|
1196
|
-
if (!record$
|
|
644
|
+
if (!record$3(entry) || Object.keys(entry).some((key) => ![
|
|
1197
645
|
"package",
|
|
1198
646
|
"workflow",
|
|
1199
647
|
"job"
|
|
@@ -1214,16 +662,16 @@ const resolveEngineFloor = (entry, files) => {
|
|
|
1214
662
|
const workflowText = files[entry.workflow];
|
|
1215
663
|
if (manifestText === void 0 || workflowText === void 0) throw new Error("engine floor package and workflow must be tracked");
|
|
1216
664
|
const manifest = JSON.parse(manifestText);
|
|
1217
|
-
if (!record$
|
|
665
|
+
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");
|
|
1218
666
|
const range = manifest["engines"]["node"];
|
|
1219
667
|
const minimum = (0, semver.validRange)(range) === null ? null : (0, semver.minVersion)(range);
|
|
1220
668
|
if (minimum === null || minimum.prerelease.length !== 0) throw new Error("engine floor engines.node must declare a stable minimum");
|
|
1221
669
|
const document = (0, yaml.parseDocument)(workflowText, { merge: true });
|
|
1222
670
|
if (document.errors.length !== 0) throw new Error("engine floor workflow must be valid YAML");
|
|
1223
671
|
const workflow = document.toJS({ maxAliasCount: 100 });
|
|
1224
|
-
if (!record$
|
|
672
|
+
if (!record$3(workflow) || !record$3(workflow["jobs"])) throw new Error("engine floor job must exist in its tracked workflow");
|
|
1225
673
|
const job = workflow["jobs"][entry.job];
|
|
1226
|
-
if (!record$
|
|
674
|
+
if (!record$3(job)) throw new Error("engine floor job must exist in its tracked workflow");
|
|
1227
675
|
if (!Array.isArray(job["steps"]) || Object.hasOwn(job, "uses")) throw new Error("engine floor job must declare steps and cannot reuse a workflow");
|
|
1228
676
|
return {
|
|
1229
677
|
...entry,
|
|
@@ -1243,7 +691,7 @@ const packageRules = [
|
|
|
1243
691
|
"typescript-layout",
|
|
1244
692
|
"node-engine"
|
|
1245
693
|
];
|
|
1246
|
-
const record$
|
|
694
|
+
const record$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1247
695
|
const shellSubstitutions = (command) => {
|
|
1248
696
|
const frames = [{
|
|
1249
697
|
kind: "root",
|
|
@@ -1308,7 +756,7 @@ const compilerCommandSegments = (command) => {
|
|
|
1308
756
|
const segments = [[]];
|
|
1309
757
|
for (const source of [command, ...shellSubstitutions(command)]) {
|
|
1310
758
|
segments.push([]);
|
|
1311
|
-
const shellWords = source.match(/(?:[^\s"'\\;&|()]|\\[
|
|
759
|
+
const shellWords = source.match(/(?:[^\s"'\\;&|()]|\\[\s\S]|"(?:[^"\\]|\\[\s\S])*"|'[^']*')+|&&|\|\||[();&|\n]/g);
|
|
1312
760
|
for (const word of shellWords ?? []) if (/^(?:&&|\|\||[;&|\n])$/.test(word)) segments.push([]);
|
|
1313
761
|
else segments.at(-1)?.push(word);
|
|
1314
762
|
}
|
|
@@ -1395,29 +843,29 @@ const launcherValueOptions = new Map(Object.entries({
|
|
|
1395
843
|
"--input-type"
|
|
1396
844
|
]
|
|
1397
845
|
}).map(([launcher, options]) => [launcher, new Set(options)]));
|
|
1398
|
-
const
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
continue;
|
|
1407
|
-
}
|
|
1408
|
-
if (quote === void 0 && (character === "'" || character === "\"")) {
|
|
1409
|
-
quote = character;
|
|
1410
|
-
continue;
|
|
1411
|
-
}
|
|
1412
|
-
const next = word.at(index + 1);
|
|
1413
|
-
if (character === "\\" && next !== void 0 && quote !== "'" && (quote === void 0 || /[$`"\\\n]/.test(next))) {
|
|
1414
|
-
result += next;
|
|
1415
|
-
index += 1;
|
|
1416
|
-
} else result += character;
|
|
846
|
+
const decodeShellWord = (word) => {
|
|
847
|
+
let result = "";
|
|
848
|
+
let quote;
|
|
849
|
+
for (let index = 0; index < word.length; index += 1) {
|
|
850
|
+
const character = word.at(index);
|
|
851
|
+
if (character === quote) {
|
|
852
|
+
quote = void 0;
|
|
853
|
+
continue;
|
|
1417
854
|
}
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
855
|
+
if (quote === void 0 && (character === "'" || character === "\"")) {
|
|
856
|
+
quote = character;
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
const next = word.at(index + 1);
|
|
860
|
+
if (character === "\\" && next !== void 0 && quote !== "'" && (quote === void 0 || /[$`"\\\n]/.test(next))) {
|
|
861
|
+
if (next !== "\n") result += next;
|
|
862
|
+
index += 1;
|
|
863
|
+
} else result += character;
|
|
864
|
+
}
|
|
865
|
+
return result;
|
|
866
|
+
};
|
|
867
|
+
const invokesCompiler = (words) => {
|
|
868
|
+
const tokens = words.map(decodeShellWord);
|
|
1421
869
|
const compiler = (word) => /(?:^|\/)(?:tsc|tsgo)(?:\.js)?$/.test(word);
|
|
1422
870
|
let start = 0;
|
|
1423
871
|
const skipOptions = (index, launcher) => {
|
|
@@ -1474,10 +922,10 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1474
922
|
const document = pnpm ? (0, yaml.parseDocument)(text, { lineCounter }) : void 0;
|
|
1475
923
|
if (document !== void 0 && document.errors.length > 0) throw new Error("invalid YAML");
|
|
1476
924
|
const json = document === void 0 ? JSON.parse(text) : document.toJS();
|
|
1477
|
-
if (!record$
|
|
925
|
+
if (!record$2(json)) throw new Error("manifest must be an object");
|
|
1478
926
|
if (pnpm) {
|
|
1479
|
-
if (json["catalog"] !== void 0 && !record$
|
|
1480
|
-
if (json["catalogs"] !== void 0 && (!record$
|
|
927
|
+
if (json["catalog"] !== void 0 && !record$2(json["catalog"])) throw new Error("catalog must be a mapping");
|
|
928
|
+
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");
|
|
1481
929
|
if (json["packages"] !== void 0 && (!Array.isArray(json["packages"]) || !json["packages"].every((pattern) => typeof pattern === "string"))) throw new Error("workspace packages must be patterns");
|
|
1482
930
|
}
|
|
1483
931
|
manifests.set(file, json);
|
|
@@ -1491,13 +939,13 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1491
939
|
catalog: json["overrides"],
|
|
1492
940
|
prefix: ["overrides"]
|
|
1493
941
|
},
|
|
1494
|
-
...record$
|
|
942
|
+
...record$2(json["catalogs"]) ? Object.entries(json["catalogs"]).map(([name, catalog]) => ({
|
|
1495
943
|
catalog,
|
|
1496
944
|
prefix: ["catalogs", name]
|
|
1497
945
|
})) : []
|
|
1498
946
|
];
|
|
1499
947
|
for (const { catalog, prefix } of catalogEntries) {
|
|
1500
|
-
if (!record$
|
|
948
|
+
if (!record$2(catalog)) continue;
|
|
1501
949
|
for (const [name, value] of Object.entries(catalog)) {
|
|
1502
950
|
const node = document.getIn([...prefix, name], true);
|
|
1503
951
|
if (!(0, yaml.isNode)(node) || node.range === void 0 || node.range === null) continue;
|
|
@@ -1516,7 +964,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1516
964
|
});
|
|
1517
965
|
}
|
|
1518
966
|
}
|
|
1519
|
-
const catalogSources = (json) => [json, ...record$
|
|
967
|
+
const catalogSources = (json) => [json, ...record$2(json["workspaces"]) ? [json["workspaces"]] : []];
|
|
1520
968
|
const pnpmWorkspaceFor = (file) => {
|
|
1521
969
|
let directory = node_path.default.posix.dirname(file);
|
|
1522
970
|
while (true) {
|
|
@@ -1533,14 +981,14 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1533
981
|
const owner = pnpmWorkspaceFor(file);
|
|
1534
982
|
if (owner !== void 0) {
|
|
1535
983
|
const { directory, workspace } = owner;
|
|
1536
|
-
if (node_path.default.posix.basename(file) === "package.json" && !workspaceContains({
|
|
984
|
+
if (node_path.default.posix.basename(file) === "package.json" && !require_toolchain_inputs.workspaceContains({
|
|
1537
985
|
directory,
|
|
1538
986
|
file,
|
|
1539
987
|
patterns: workspace["packages"],
|
|
1540
988
|
rootMembership: "implicit"
|
|
1541
989
|
})) return void 0;
|
|
1542
|
-
if (name === "" || name === "default") return workspace["catalog"] ?? (record$
|
|
1543
|
-
return record$
|
|
990
|
+
if (name === "" || name === "default") return workspace["catalog"] ?? (record$2(workspace["catalogs"]) ? workspace["catalogs"]["default"] : void 0);
|
|
991
|
+
return record$2(workspace["catalogs"]) ? workspace["catalogs"][name] : void 0;
|
|
1544
992
|
}
|
|
1545
993
|
let directory = node_path.default.posix.dirname(file);
|
|
1546
994
|
while (true) {
|
|
@@ -1548,7 +996,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1548
996
|
if (manifest !== void 0) {
|
|
1549
997
|
for (const source of catalogSources(manifest)) {
|
|
1550
998
|
if (name === "" && source["catalog"] !== void 0) return source["catalog"];
|
|
1551
|
-
if (record$
|
|
999
|
+
if (record$2(source["catalogs"]) && source["catalogs"][name] !== void 0) return source["catalogs"][name];
|
|
1552
1000
|
}
|
|
1553
1001
|
if (manifest["workspaces"] !== void 0) return void 0;
|
|
1554
1002
|
}
|
|
@@ -1562,7 +1010,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1562
1010
|
if (visited.has(value)) return void 0;
|
|
1563
1011
|
visited.add(value);
|
|
1564
1012
|
const catalog = catalogFor(file, value.slice(8));
|
|
1565
|
-
value = record$
|
|
1013
|
+
value = record$2(catalog) ? catalog[name] : void 0;
|
|
1566
1014
|
}
|
|
1567
1015
|
return value;
|
|
1568
1016
|
};
|
|
@@ -1578,7 +1026,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1578
1026
|
const workspaces = manifests.get(node_path.default.posix.join(directory, "package.json"))?.["workspaces"];
|
|
1579
1027
|
if (workspaces !== void 0) return {
|
|
1580
1028
|
directory,
|
|
1581
|
-
patterns: record$
|
|
1029
|
+
patterns: record$2(workspaces) ? workspaces["packages"] : workspaces,
|
|
1582
1030
|
source: "json"
|
|
1583
1031
|
};
|
|
1584
1032
|
if (directory === ".") return void 0;
|
|
@@ -1590,13 +1038,13 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1590
1038
|
if (owner === void 0) return "nonmember";
|
|
1591
1039
|
const { directory, patterns, source } = owner;
|
|
1592
1040
|
if (source === "json" && !Array.isArray(patterns)) return "mismatch";
|
|
1593
|
-
if (!workspaceContains({
|
|
1041
|
+
if (!require_toolchain_inputs.workspaceContains({
|
|
1594
1042
|
directory,
|
|
1595
1043
|
file,
|
|
1596
1044
|
patterns,
|
|
1597
1045
|
rootMembership: "implicit"
|
|
1598
1046
|
})) return "nonmember";
|
|
1599
|
-
const candidates = [...manifests.entries()].filter(([candidate, json]) => node_path.default.posix.basename(candidate) === "package.json" && json["name"] === name && workspaceContains({
|
|
1047
|
+
const candidates = [...manifests.entries()].filter(([candidate, json]) => node_path.default.posix.basename(candidate) === "package.json" && json["name"] === name && require_toolchain_inputs.workspaceContains({
|
|
1600
1048
|
directory,
|
|
1601
1049
|
file: candidate,
|
|
1602
1050
|
patterns,
|
|
@@ -1634,7 +1082,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1634
1082
|
"peerDependencies"
|
|
1635
1083
|
]) {
|
|
1636
1084
|
const dependencies = json[section];
|
|
1637
|
-
if (!record$
|
|
1085
|
+
if (!record$2(dependencies)) continue;
|
|
1638
1086
|
for (const [name, value] of Object.entries(dependencies)) {
|
|
1639
1087
|
if (!tsSpecifiers.has(name)) continue;
|
|
1640
1088
|
const resolved = resolve(file, name, value);
|
|
@@ -1660,7 +1108,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1660
1108
|
const workspaceLayouts = /* @__PURE__ */ new Map();
|
|
1661
1109
|
const workspaceLayoutFor = (file) => {
|
|
1662
1110
|
const owner = workspaceOwnerFor(file);
|
|
1663
|
-
if (owner === void 0 || !workspaceContains({
|
|
1111
|
+
if (owner === void 0 || !require_toolchain_inputs.workspaceContains({
|
|
1664
1112
|
...owner,
|
|
1665
1113
|
file,
|
|
1666
1114
|
rootMembership: "implicit"
|
|
@@ -1672,7 +1120,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1672
1120
|
for (const [candidate, member] of manifests) {
|
|
1673
1121
|
if (node_path.default.posix.basename(candidate) !== "package.json") continue;
|
|
1674
1122
|
const memberOwner = workspaceOwnerFor(candidate);
|
|
1675
|
-
if (memberOwner?.directory !== owner.directory || memberOwner.source !== owner.source || !workspaceContains({
|
|
1123
|
+
if (memberOwner?.directory !== owner.directory || memberOwner.source !== owner.source || !require_toolchain_inputs.workspaceContains({
|
|
1676
1124
|
...owner,
|
|
1677
1125
|
file: candidate,
|
|
1678
1126
|
rootMembership: "implicit"
|
|
@@ -1696,7 +1144,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1696
1144
|
message: `packageManager Bun version must be ${policy.bun}, found ${JSON.stringify(manager)}`
|
|
1697
1145
|
});
|
|
1698
1146
|
const checkEntries = (entries, location) => {
|
|
1699
|
-
if (!record$
|
|
1147
|
+
if (!record$2(entries)) return;
|
|
1700
1148
|
for (const [name, raw] of Object.entries(entries)) {
|
|
1701
1149
|
const value = resolve(file, name, raw);
|
|
1702
1150
|
const tsAllowed = tsSpecifiers.get(name);
|
|
@@ -1733,12 +1181,12 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1733
1181
|
"devDependencies",
|
|
1734
1182
|
"optionalDependencies"
|
|
1735
1183
|
]) checkEntries(json[key]);
|
|
1736
|
-
if (record$
|
|
1184
|
+
if (record$2(json["peerDependencies"])) {
|
|
1737
1185
|
for (const [name, value] of Object.entries(json["peerDependencies"])) if (tsSpecifiers.has(name) && !peerRange(resolve(file, name, value))) checkEntries({ [name]: value });
|
|
1738
1186
|
}
|
|
1739
1187
|
for (const source of catalogSources(json)) {
|
|
1740
1188
|
checkEntries(source["catalog"]);
|
|
1741
|
-
if (!record$
|
|
1189
|
+
if (!record$2(source["catalogs"])) continue;
|
|
1742
1190
|
for (const catalog of Object.values(source["catalogs"])) checkEntries(catalog);
|
|
1743
1191
|
}
|
|
1744
1192
|
const ownedNames = /* @__PURE__ */ new Set([...pins.keys(), ...tsSpecifiers.keys()]);
|
|
@@ -1750,7 +1198,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1750
1198
|
"optionalDependencies"
|
|
1751
1199
|
]) {
|
|
1752
1200
|
const entries = resolutionRoot[section];
|
|
1753
|
-
if (!record$
|
|
1201
|
+
if (!record$2(entries)) continue;
|
|
1754
1202
|
for (const [name, value] of Object.entries(entries)) resolutionDependencies[name] = resolve(file, name, value);
|
|
1755
1203
|
}
|
|
1756
1204
|
const affectedSpecifiers = /* @__PURE__ */ new Map();
|
|
@@ -1758,7 +1206,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1758
1206
|
for (const [candidate, member] of manifests) {
|
|
1759
1207
|
if (node_path.default.posix.basename(candidate) !== "package.json") continue;
|
|
1760
1208
|
const memberOwner = workspaceOwnerFor(candidate);
|
|
1761
|
-
if (!(candidate === node_path.default.posix.join(node_path.default.posix.dirname(file), "package.json")) && !(owner !== void 0 && owner.directory === node_path.default.posix.dirname(file) && memberOwner?.directory === owner.directory && memberOwner.source === owner.source && workspaceContains({
|
|
1209
|
+
if (!(candidate === node_path.default.posix.join(node_path.default.posix.dirname(file), "package.json")) && !(owner !== void 0 && owner.directory === node_path.default.posix.dirname(file) && memberOwner?.directory === owner.directory && memberOwner.source === owner.source && require_toolchain_inputs.workspaceContains({
|
|
1762
1210
|
directory: owner.directory,
|
|
1763
1211
|
file: candidate,
|
|
1764
1212
|
patterns: owner.patterns,
|
|
@@ -1770,7 +1218,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1770
1218
|
"optionalDependencies"
|
|
1771
1219
|
]) {
|
|
1772
1220
|
const entries = member[section];
|
|
1773
|
-
if (!record$
|
|
1221
|
+
if (!record$2(entries)) continue;
|
|
1774
1222
|
for (const [name, value] of Object.entries(entries)) {
|
|
1775
1223
|
if (!tsSpecifiers.has(name)) continue;
|
|
1776
1224
|
const values = affectedSpecifiers.get(name) ?? /* @__PURE__ */ new Set();
|
|
@@ -1780,7 +1228,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1780
1228
|
}
|
|
1781
1229
|
}
|
|
1782
1230
|
const checkResolutionMap = (entries, parent) => {
|
|
1783
|
-
if (!record$
|
|
1231
|
+
if (!record$2(entries)) return;
|
|
1784
1232
|
for (const [selector, replacement] of Object.entries(entries)) {
|
|
1785
1233
|
const target = selector === "." ? parent : selector;
|
|
1786
1234
|
if (target === void 0) continue;
|
|
@@ -1803,7 +1251,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1803
1251
|
});
|
|
1804
1252
|
continue;
|
|
1805
1253
|
}
|
|
1806
|
-
if (record$
|
|
1254
|
+
if (record$2(replacement)) {
|
|
1807
1255
|
checkResolutionMap(replacement, target);
|
|
1808
1256
|
continue;
|
|
1809
1257
|
}
|
|
@@ -1837,7 +1285,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1837
1285
|
};
|
|
1838
1286
|
checkResolutionMap(json["overrides"]);
|
|
1839
1287
|
checkResolutionMap(json["resolutions"]);
|
|
1840
|
-
if (record$
|
|
1288
|
+
if (record$2(json["pnpm"])) checkResolutionMap(json["pnpm"]["overrides"]);
|
|
1841
1289
|
const localEntries = toolchainEntries(file, json);
|
|
1842
1290
|
const usesCompiler = policy.typescriptInstallLayouts.some((layout) => localEntries.some(({ name }) => name === layout.compilerPackage));
|
|
1843
1291
|
let selectedLayout = layoutFor(specifierMap(localEntries));
|
|
@@ -1861,7 +1309,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1861
1309
|
key: "dependencies",
|
|
1862
1310
|
message: "TypeScript compiler and compatibility packages must match one complete declared install layout"
|
|
1863
1311
|
});
|
|
1864
|
-
const command = record$
|
|
1312
|
+
const command = record$2(json["scripts"]) ? json["scripts"]["typecheck"] : void 0;
|
|
1865
1313
|
if (selectedLayout !== void 0 && typeof command === "string") {
|
|
1866
1314
|
const normalizedExpected = selectedLayout.typecheckCommand.replace(/\s+/g, " ").trim();
|
|
1867
1315
|
for (const words of compilerCommandSegments(command)) {
|
|
@@ -1879,7 +1327,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1879
1327
|
}
|
|
1880
1328
|
}
|
|
1881
1329
|
}
|
|
1882
|
-
if (record$
|
|
1330
|
+
if (record$2(json["peerDependencies"])) {
|
|
1883
1331
|
const peerLayout = selectedLayout ?? workspaceLayoutFor(file)?.layout;
|
|
1884
1332
|
for (const [name, raw] of Object.entries(json["peerDependencies"])) {
|
|
1885
1333
|
if (!tsSpecifiers.has(name)) continue;
|
|
@@ -1903,7 +1351,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1903
1351
|
});
|
|
1904
1352
|
}
|
|
1905
1353
|
}
|
|
1906
|
-
if (record$
|
|
1354
|
+
if (record$2(json["engines"]) && json["engines"]["node"] !== void 0 && (typeof json["engines"]["node"] !== "string" || !require_toolchain_inputs.nodeSupportRangeMatches(policy.node, json["engines"]["node"]))) add({
|
|
1907
1355
|
file,
|
|
1908
1356
|
rule: "node-engine",
|
|
1909
1357
|
key: "node",
|
|
@@ -1925,7 +1373,7 @@ const runtimeRules = [
|
|
|
1925
1373
|
"runtime-workflow",
|
|
1926
1374
|
"action-pins"
|
|
1927
1375
|
];
|
|
1928
|
-
const record$
|
|
1376
|
+
const record$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1929
1377
|
const pythonSelectorMatches = (value, expected) => value === expected || typeof value === "string" && /^\d+\.\d+$/.test(expected) && value.startsWith(`${expected}.`) && /^\d+\.\d+\.\d+$/.test(value);
|
|
1930
1378
|
const compareRelease = (left, right) => {
|
|
1931
1379
|
for (let index = 0; index < Math.max(left.length, right.length); index++) {
|
|
@@ -1995,7 +1443,7 @@ const pythonRequirementMatches = (requirement, version) => {
|
|
|
1995
1443
|
});
|
|
1996
1444
|
};
|
|
1997
1445
|
/** Check runtime declarations only in the files that declare or configure them. */
|
|
1998
|
-
const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, repository, onDelegated, engineFloors = [], onEngineFloor }) => {
|
|
1446
|
+
const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, repository, onDelegated, dynamicSelectors = [], onDynamicSelector, engineFloors = [], onEngineFloor }) => {
|
|
1999
1447
|
const diagnostics = [];
|
|
2000
1448
|
const name = node_path.default.posix.basename(file);
|
|
2001
1449
|
const lines = text.split(/\r?\n/);
|
|
@@ -2007,10 +1455,20 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2007
1455
|
message
|
|
2008
1456
|
});
|
|
2009
1457
|
};
|
|
1458
|
+
let currentLocation;
|
|
1459
|
+
const dynamic = (value) => typeof value === "string" && value.includes("$");
|
|
1460
|
+
const authorizeDynamic = (kind, line, at = currentLocation) => {
|
|
1461
|
+
const matches = dynamicSelectors.filter((entry) => entry.path === file && entry.kind === kind && (entry.at !== void 0 ? entry.at === at : entry.line === line));
|
|
1462
|
+
if (matches.length !== 1) return false;
|
|
1463
|
+
const entry = matches.at(0);
|
|
1464
|
+
if (entry === void 0) return false;
|
|
1465
|
+
onDynamicSelector?.(entry);
|
|
1466
|
+
return true;
|
|
1467
|
+
};
|
|
2010
1468
|
const lineOf = (key) => Math.max(1, lines.findIndex((line) => line.includes(key)) + 1);
|
|
2011
1469
|
const pin = ({ rule, value, expected, label, line = 1, selector }) => {
|
|
2012
1470
|
let matches = value === expected;
|
|
2013
|
-
if (selector === "node") matches = nodeSelectorMatches(value, expected);
|
|
1471
|
+
if (selector === "node") matches = require_toolchain_inputs.nodeSelectorMatches(value, expected);
|
|
2014
1472
|
else if (label.includes("python")) matches = pythonSelectorMatches(value, expected);
|
|
2015
1473
|
if (!matches) add({
|
|
2016
1474
|
rule,
|
|
@@ -2079,11 +1537,11 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2079
1537
|
const parsed = toml("rust-version");
|
|
2080
1538
|
if (parsed !== void 0) {
|
|
2081
1539
|
const toolchain = parsed["toolchain"];
|
|
2082
|
-
const components = record$
|
|
1540
|
+
const components = record$1(toolchain) ? toolchain["components"] : void 0;
|
|
2083
1541
|
const expected = Array.isArray(components) && components.includes("rustc-dev") ? policy.rustCompilerDevelopment : policy.rust;
|
|
2084
1542
|
pin({
|
|
2085
1543
|
rule: "rust-version",
|
|
2086
|
-
value: record$
|
|
1544
|
+
value: record$1(toolchain) ? toolchain["channel"] : void 0,
|
|
2087
1545
|
expected,
|
|
2088
1546
|
label: "Rust channel",
|
|
2089
1547
|
line: lineOf("channel")
|
|
@@ -2111,14 +1569,14 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2111
1569
|
};
|
|
2112
1570
|
if (parsed !== void 0) {
|
|
2113
1571
|
const workspace = parsed["workspace"];
|
|
2114
|
-
const defaults = record$
|
|
2115
|
-
if (record$
|
|
1572
|
+
const defaults = record$1(workspace) ? workspace["package"] : void 0;
|
|
1573
|
+
if (record$1(defaults) && defaults["rust-version"] !== void 0) rustFloor(defaults["rust-version"]);
|
|
2116
1574
|
const pkg = parsed["package"];
|
|
2117
|
-
const version = record$
|
|
2118
|
-
if (version !== void 0) if (!record$
|
|
1575
|
+
const version = record$1(pkg) ? pkg["rust-version"] : void 0;
|
|
1576
|
+
if (version !== void 0) if (!record$1(version)) rustFloor(version);
|
|
2119
1577
|
else {
|
|
2120
1578
|
let inherited;
|
|
2121
|
-
if (version["workspace"] === true && record$
|
|
1579
|
+
if (version["workspace"] === true && record$1(pkg)) {
|
|
2122
1580
|
const explicit = pkg["workspace"];
|
|
2123
1581
|
let directory = node_path.default.posix.dirname(file);
|
|
2124
1582
|
const explicitPath = typeof explicit === "string" && !explicit.includes("\\") && !explicit.includes(":") && !explicit.includes("${{") && !node_path.default.posix.isAbsolute(explicit) ? node_path.default.posix.normalize(node_path.default.posix.join(directory, explicit)) : void 0;
|
|
@@ -2126,9 +1584,9 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2126
1584
|
if (explicitPath !== void 0) directory = explicitPath;
|
|
2127
1585
|
while (true) {
|
|
2128
1586
|
const table = readManifest(node_path.default.posix.join(directory, "Cargo.toml"))?.["workspace"];
|
|
2129
|
-
if (record$
|
|
1587
|
+
if (record$1(table)) {
|
|
2130
1588
|
const fields = table["package"];
|
|
2131
|
-
inherited = record$
|
|
1589
|
+
inherited = record$1(fields) ? fields["rust-version"] : void 0;
|
|
2132
1590
|
break;
|
|
2133
1591
|
}
|
|
2134
1592
|
if (explicit !== void 0 || directory === ".") break;
|
|
@@ -2142,7 +1600,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2142
1600
|
}
|
|
2143
1601
|
if (name === "Pipfile") {
|
|
2144
1602
|
const requires = toml("python-version")?.["requires"];
|
|
2145
|
-
if (record$
|
|
1603
|
+
if (record$1(requires)) {
|
|
2146
1604
|
const minor = requires["python_version"];
|
|
2147
1605
|
if (minor !== void 0 && minor !== policy.python.split(".").slice(0, 2).join(".")) add({
|
|
2148
1606
|
rule: "python-version",
|
|
@@ -2161,7 +1619,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2161
1619
|
const parsed = toml("python-version");
|
|
2162
1620
|
if (parsed !== void 0) {
|
|
2163
1621
|
const project = parsed["project"];
|
|
2164
|
-
if (record$
|
|
1622
|
+
if (record$1(project) && project["requires-python"] !== void 0) {
|
|
2165
1623
|
if (!pythonRequirementMatches(project["requires-python"], policy.python)) add({
|
|
2166
1624
|
rule: "python-version",
|
|
2167
1625
|
line: lineOf("requires-python"),
|
|
@@ -2169,9 +1627,9 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2169
1627
|
});
|
|
2170
1628
|
}
|
|
2171
1629
|
const tool = parsed["tool"];
|
|
2172
|
-
const configuredUv = record$
|
|
1630
|
+
const configuredUv = record$1(tool) ? tool["uv"] : void 0;
|
|
2173
1631
|
const uv = name === "uv.toml" ? parsed : configuredUv;
|
|
2174
|
-
if (record$
|
|
1632
|
+
if (record$1(uv) && uv["python"] !== void 0) pin({
|
|
2175
1633
|
rule: "python-version",
|
|
2176
1634
|
value: uv["python"],
|
|
2177
1635
|
expected: policy.python,
|
|
@@ -2180,13 +1638,13 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2180
1638
|
});
|
|
2181
1639
|
}
|
|
2182
1640
|
}
|
|
2183
|
-
if (isMiseConfigPath(file)) {
|
|
1641
|
+
if (require_toolchain_inputs.isMiseConfigPath(file)) {
|
|
2184
1642
|
const tools = toml("runtime-manager")?.["tools"];
|
|
2185
|
-
if (record$
|
|
1643
|
+
if (record$1(tools)) for (const [tool, expected] of Object.entries(versions)) {
|
|
2186
1644
|
const configured = tools[tool];
|
|
2187
1645
|
if (configured !== void 0) pin({
|
|
2188
1646
|
rule: "runtime-manager",
|
|
2189
|
-
value: record$
|
|
1647
|
+
value: record$1(configured) ? configured["version"] : configured,
|
|
2190
1648
|
expected,
|
|
2191
1649
|
selector: nodeTools.has(tool) ? "node" : void 0,
|
|
2192
1650
|
label: `mise ${tool}`,
|
|
@@ -2208,12 +1666,13 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2208
1666
|
line: index + 1
|
|
2209
1667
|
});
|
|
2210
1668
|
});
|
|
2211
|
-
const checkRuntimeImage = ({ image, line, label }) => {
|
|
1669
|
+
const checkRuntimeImage = ({ image, line, label, at }) => {
|
|
2212
1670
|
const unknownRuntime = () => {
|
|
1671
|
+
if (dynamic(image) && authorizeDynamic("image", line, at)) return;
|
|
2213
1672
|
add({
|
|
2214
1673
|
rule: "runtime-docker",
|
|
2215
1674
|
line,
|
|
2216
|
-
message: `cannot determine ${label} runtime; use a static image or a
|
|
1675
|
+
message: `cannot determine ${label} runtime; use a static image or a scoped dynamicSelectors declaration`
|
|
2217
1676
|
});
|
|
2218
1677
|
};
|
|
2219
1678
|
if (typeof image !== "string") {
|
|
@@ -2283,7 +1742,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2283
1742
|
line
|
|
2284
1743
|
});
|
|
2285
1744
|
};
|
|
2286
|
-
if (isDockerDefinitionPath(file)) {
|
|
1745
|
+
if (require_toolchain_inputs.isDockerDefinitionPath(file)) {
|
|
2287
1746
|
const variables = /* @__PURE__ */ new Map();
|
|
2288
1747
|
const escape = /^\s*#\s*escape\s*=\s*([\\`])\s*$/m.exec(text)?.[1] ?? "\\";
|
|
2289
1748
|
const continuation = new RegExp(`${escape === "`" ? "`" : "\\\\"}\\s*$`);
|
|
@@ -2416,7 +1875,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2416
1875
|
message: `unterminated Docker heredoc ${heredoc.delimiter}`
|
|
2417
1876
|
});
|
|
2418
1877
|
}
|
|
2419
|
-
if (isComposeDefinitionPath(file) || isKubernetesDefinitionPath(file)) {
|
|
1878
|
+
if (require_toolchain_inputs.isComposeDefinitionPath(file) || require_toolchain_inputs.isKubernetesDefinitionPath(file)) {
|
|
2420
1879
|
const lineCounter = new yaml.LineCounter();
|
|
2421
1880
|
for (const containerDocument of (0, yaml.parseAllDocuments)(text, {
|
|
2422
1881
|
lineCounter,
|
|
@@ -2425,7 +1884,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2425
1884
|
})) {
|
|
2426
1885
|
const contents = containerDocument.contents;
|
|
2427
1886
|
const declaredResource = (0, yaml.isMap)(contents) && (0, yaml.isScalar)(contents.get("apiVersion", true)) && (0, yaml.isScalar)(contents.get("kind", true));
|
|
2428
|
-
const recognizedContainer = isComposeDefinitionPath(file) || declaredResource;
|
|
1887
|
+
const recognizedContainer = require_toolchain_inputs.isComposeDefinitionPath(file) || declaredResource;
|
|
2429
1888
|
if (containerDocument.errors.length > 0) {
|
|
2430
1889
|
if (recognizedContainer) add({
|
|
2431
1890
|
rule: "runtime-docker",
|
|
@@ -2435,13 +1894,14 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2435
1894
|
continue;
|
|
2436
1895
|
}
|
|
2437
1896
|
try {
|
|
2438
|
-
const classified = containerDocumentImages(containerDocument.toJS({ maxAliasCount: 100 }), file);
|
|
1897
|
+
const classified = require_toolchain_inputs.containerDocumentImages(containerDocument.toJS({ maxAliasCount: 100 }), file);
|
|
2439
1898
|
for (const entry of classified?.images ?? []) {
|
|
2440
1899
|
const node = containerDocument.getIn(entry.path, true);
|
|
2441
1900
|
const line = ((0, yaml.isMap)(node) || (0, yaml.isScalar)(node) || (0, yaml.isSeq)(node) || (0, yaml.isAlias)(node)) && node.range !== void 0 && node.range !== null ? lineCounter.linePos(node.range[0]).line : 1;
|
|
2442
1901
|
checkRuntimeImage({
|
|
2443
1902
|
image: entry.image,
|
|
2444
1903
|
line,
|
|
1904
|
+
at: entry.path.join("."),
|
|
2445
1905
|
label: classified?.ecosystem === "docker-compose" ? "Compose image" : "Kubernetes image"
|
|
2446
1906
|
});
|
|
2447
1907
|
}
|
|
@@ -2454,7 +1914,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2454
1914
|
}
|
|
2455
1915
|
}
|
|
2456
1916
|
}
|
|
2457
|
-
const automationKind = githubAutomationFileKind(file);
|
|
1917
|
+
const automationKind = require_toolchain_inputs.githubAutomationFileKind(file);
|
|
2458
1918
|
if (automationKind === void 0) return diagnostics;
|
|
2459
1919
|
const document = (0, yaml.parseDocument)(text, {
|
|
2460
1920
|
uniqueKeys: true,
|
|
@@ -2469,15 +1929,29 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2469
1929
|
return diagnostics;
|
|
2470
1930
|
}
|
|
2471
1931
|
const nodeLine = (node) => {
|
|
2472
|
-
if (!record$
|
|
1932
|
+
if (!record$1(node) || !Array.isArray(node["range"])) return 1;
|
|
2473
1933
|
const offset = node["range"].at(0);
|
|
2474
1934
|
return typeof offset === "number" ? text.slice(0, offset).split("\n").length : 1;
|
|
2475
1935
|
};
|
|
2476
1936
|
let checkoutBindings = [];
|
|
2477
1937
|
let checkoutDestinations = /* @__PURE__ */ new Map();
|
|
2478
1938
|
let unknownCheckoutDestination = false;
|
|
1939
|
+
let unreviewableCheckoutSource = false;
|
|
2479
1940
|
const staticRepositoryPath = (value) => typeof value === "string" && value !== "" && !value.includes("${{") && !value.includes("\\") && !value.includes(":") && !node_path.default.posix.isAbsolute(value) && !value.split("/").includes("..");
|
|
2480
1941
|
const normalizeCheckoutPath = (prefix) => node_path.default.posix.normalize(prefix).replace(/\/$/, "") || ".";
|
|
1942
|
+
const checkoutProvidersFor = (selector) => {
|
|
1943
|
+
if (selector === void 0) return checkoutBindings;
|
|
1944
|
+
let deepest;
|
|
1945
|
+
for (const entry of checkoutBindings) {
|
|
1946
|
+
if (entry.path !== "." && selector !== entry.path && !selector.startsWith(`${entry.path}/`)) continue;
|
|
1947
|
+
if (deepest === void 0 || entry.path !== "." && (deepest.path === "." || entry.path.length > deepest.path.length)) deepest = entry;
|
|
1948
|
+
}
|
|
1949
|
+
return checkoutBindings.filter((entry) => entry.path === deepest?.path);
|
|
1950
|
+
};
|
|
1951
|
+
const hasAmbiguousCheckoutWriters = (selector, providers) => {
|
|
1952
|
+
if (providers.length > 0) return providers.some((entry) => (checkoutDestinations.get(entry.path) ?? 0) > 1);
|
|
1953
|
+
return [...checkoutDestinations].some(([prefix, count]) => count > 1 && (selector === void 0 || prefix === "." || selector === prefix || selector.startsWith(`${prefix}/`)));
|
|
1954
|
+
};
|
|
2481
1955
|
const checkReference = ({ value, tool, line }) => {
|
|
2482
1956
|
const allowed = {
|
|
2483
1957
|
node: [".node-version", ".nvmrc"],
|
|
@@ -2486,8 +1960,28 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2486
1960
|
}[tool];
|
|
2487
1961
|
const rule = tool === "bun" ? "bun-pins" : "runtime-workflow";
|
|
2488
1962
|
const selector = staticRepositoryPath(value) ? node_path.default.posix.normalize(value) : void 0;
|
|
2489
|
-
const
|
|
1963
|
+
const providers = checkoutProvidersFor(selector);
|
|
1964
|
+
const binding = selector === void 0 ? void 0 : providers.at(-1);
|
|
1965
|
+
const dynamicSource = providers.some((entry) => entry.dynamicSource);
|
|
1966
|
+
const sparseFailure = providers.find((entry) => {
|
|
1967
|
+
if (entry.sparse.mode === "invalid") return true;
|
|
1968
|
+
if (entry.sparse.mode !== "files" && entry.sparse.mode !== "mixed") return false;
|
|
1969
|
+
if (selector === void 0) return true;
|
|
1970
|
+
const selectedFile = entry.path === "." ? selector : node_path.default.posix.relative(entry.path, selector);
|
|
1971
|
+
return !entry.sparse.paths.has(selectedFile);
|
|
1972
|
+
});
|
|
1973
|
+
if (sparseFailure !== void 0) {
|
|
1974
|
+
let missingFile = "the selected version file";
|
|
1975
|
+
if (selector !== void 0) missingFile = sparseFailure.path === "." ? selector : node_path.default.posix.relative(sparseFailure.path, selector);
|
|
1976
|
+
add({
|
|
1977
|
+
rule,
|
|
1978
|
+
line,
|
|
1979
|
+
message: `checkout sparse-checkout must explicitly list ${missingFile} without invalid literal patterns`
|
|
1980
|
+
});
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
2490
1983
|
if (selector === void 0) {
|
|
1984
|
+
if (tool === "bun" && dynamic(value) && !unreviewableCheckoutSource && authorizeDynamic("bun-source", line)) return;
|
|
2491
1985
|
add({
|
|
2492
1986
|
rule,
|
|
2493
1987
|
line,
|
|
@@ -2495,7 +1989,8 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2495
1989
|
});
|
|
2496
1990
|
return;
|
|
2497
1991
|
}
|
|
2498
|
-
if (unknownCheckoutDestination ||
|
|
1992
|
+
if (unknownCheckoutDestination || hasAmbiguousCheckoutWriters(selector, providers)) {
|
|
1993
|
+
if (tool === "bun" && dynamicSource && !unreviewableCheckoutSource && authorizeDynamic("bun-source", line)) return;
|
|
2499
1994
|
add({
|
|
2500
1995
|
rule,
|
|
2501
1996
|
line,
|
|
@@ -2504,6 +1999,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2504
1999
|
return;
|
|
2505
2000
|
}
|
|
2506
2001
|
if (binding?.source === "untrusted") {
|
|
2002
|
+
if (tool === "bun" && dynamicSource && !unreviewableCheckoutSource && authorizeDynamic("bun-source", line)) return;
|
|
2507
2003
|
add({
|
|
2508
2004
|
rule,
|
|
2509
2005
|
line: binding.line,
|
|
@@ -2520,13 +2016,15 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2520
2016
|
});
|
|
2521
2017
|
return;
|
|
2522
2018
|
}
|
|
2523
|
-
if (binding !== void 0 && (binding.sparse.mode === "
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2019
|
+
if (binding !== void 0 && (binding.sparse.mode === "dynamic" || binding.sparse.mode === "mixed")) {
|
|
2020
|
+
if (tool !== "bun" || unreviewableCheckoutSource || !authorizeDynamic("bun-source", line)) {
|
|
2021
|
+
add({
|
|
2022
|
+
rule,
|
|
2023
|
+
line,
|
|
2024
|
+
message: `checkout sparse-checkout must explicitly list ${target} without dynamic, glob, or negation patterns`
|
|
2025
|
+
});
|
|
2026
|
+
return;
|
|
2027
|
+
}
|
|
2530
2028
|
}
|
|
2531
2029
|
if (binding?.source === "delegated") {
|
|
2532
2030
|
onDelegated?.({
|
|
@@ -2545,7 +2043,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2545
2043
|
const manifest = JSON.parse(content ?? "");
|
|
2546
2044
|
pin({
|
|
2547
2045
|
rule,
|
|
2548
|
-
value: record$
|
|
2046
|
+
value: record$1(manifest) ? manifest["packageManager"] : void 0,
|
|
2549
2047
|
expected: `bun@${policy.bun}`,
|
|
2550
2048
|
label: "setup-bun version file packageManager",
|
|
2551
2049
|
line
|
|
@@ -2648,10 +2146,10 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2648
2146
|
return inputs;
|
|
2649
2147
|
};
|
|
2650
2148
|
const getInput = (node, key) => inputsOf(node).get(key.toUpperCase());
|
|
2651
|
-
const
|
|
2652
|
-
if (
|
|
2149
|
+
const expressionAt = (value, start) => {
|
|
2150
|
+
if (!value.startsWith("${{", start)) return void 0;
|
|
2653
2151
|
let quote;
|
|
2654
|
-
for (let index = 3; index < value.length; index++) {
|
|
2152
|
+
for (let index = start + 3; index < value.length; index++) {
|
|
2655
2153
|
const character = value.at(index);
|
|
2656
2154
|
if (quote !== void 0) {
|
|
2657
2155
|
if (character === quote) if (quote === "'" && value.at(index + 1) === "'") index++;
|
|
@@ -2661,9 +2159,33 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2661
2159
|
}
|
|
2662
2160
|
if (character === "'" || character === "\"") quote = character;
|
|
2663
2161
|
else if (value.startsWith("${{", index)) return void 0;
|
|
2664
|
-
else if (character === "}" && value.at(index + 1) === "}") return
|
|
2162
|
+
else if (character === "}" && value.at(index + 1) === "}") return {
|
|
2163
|
+
body: value.slice(start + 3, index).trim(),
|
|
2164
|
+
end: index + 2
|
|
2165
|
+
};
|
|
2665
2166
|
}
|
|
2666
2167
|
};
|
|
2168
|
+
const dynamicRefBody = (value) => {
|
|
2169
|
+
if (typeof value !== "string") return void 0;
|
|
2170
|
+
const expression = expressionAt(value, 0);
|
|
2171
|
+
return expression?.end === value.length ? expression.body : void 0;
|
|
2172
|
+
};
|
|
2173
|
+
const sparseEntryIsValid = (entry) => {
|
|
2174
|
+
let literalPath = "";
|
|
2175
|
+
let position = 0;
|
|
2176
|
+
while (position < entry.length) {
|
|
2177
|
+
const start = entry.indexOf("${{", position);
|
|
2178
|
+
const literal = entry.slice(position, start < 0 ? entry.length : start);
|
|
2179
|
+
if (/[!$*?[\]\\:]/.test(literal) || literal.split("/").includes("..")) return false;
|
|
2180
|
+
literalPath += literal;
|
|
2181
|
+
if (start < 0) break;
|
|
2182
|
+
const expression = expressionAt(entry, start);
|
|
2183
|
+
if (expression === void 0 || expression.body === "") return false;
|
|
2184
|
+
literalPath += "expression";
|
|
2185
|
+
position = expression.end;
|
|
2186
|
+
}
|
|
2187
|
+
return staticRepositoryPath(literalPath);
|
|
2188
|
+
};
|
|
2667
2189
|
const checkoutPrefixOf = (node) => {
|
|
2668
2190
|
const destination = getInput(node, "path");
|
|
2669
2191
|
if (destination === void 0) return ".";
|
|
@@ -2676,11 +2198,19 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2676
2198
|
const value = getInput(node, "sparse-checkout");
|
|
2677
2199
|
const cone = getInput(node, "sparse-checkout-cone-mode");
|
|
2678
2200
|
if (!(0, yaml.isScalar)(value) || typeof value.value !== "string" || cone !== void 0 && (!(0, yaml.isScalar)(cone) || cone.value !== true && cone.value !== false && cone.value !== "true" && cone.value !== "false")) return { mode: "invalid" };
|
|
2679
|
-
const paths = value.value.split(/\r?\n/).map((entry) => entry.trim()).filter((entry) => entry !== "");
|
|
2680
|
-
if (paths.length === 0 || paths.some((entry) => !
|
|
2201
|
+
const paths = value.value.split(/\r?\n/).map((entry) => entry.trim().replace(/^\//, "")).filter((entry) => entry !== "");
|
|
2202
|
+
if (paths.length === 0 || paths.some((entry) => !sparseEntryIsValid(entry))) return { mode: "invalid" };
|
|
2203
|
+
const literalPaths = new Set(paths.filter((entry) => !dynamic(entry)));
|
|
2204
|
+
if (paths.some(dynamic)) {
|
|
2205
|
+
if (literalPaths.size === 0) return { mode: "dynamic" };
|
|
2206
|
+
return {
|
|
2207
|
+
mode: "mixed",
|
|
2208
|
+
paths: literalPaths
|
|
2209
|
+
};
|
|
2210
|
+
}
|
|
2681
2211
|
return {
|
|
2682
2212
|
mode: "files",
|
|
2683
|
-
paths:
|
|
2213
|
+
paths: literalPaths
|
|
2684
2214
|
};
|
|
2685
2215
|
};
|
|
2686
2216
|
let currentEngineFloor;
|
|
@@ -2710,8 +2240,9 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2710
2240
|
}
|
|
2711
2241
|
if (value.startsWith("./")) {
|
|
2712
2242
|
const selector = staticRepositoryPath(value) ? node_path.default.posix.normalize(value) : void 0;
|
|
2713
|
-
const
|
|
2714
|
-
|
|
2243
|
+
const providers = checkoutProvidersFor(selector);
|
|
2244
|
+
const binding = selector === void 0 ? void 0 : providers.at(-1);
|
|
2245
|
+
if (selector === void 0 || unknownCheckoutDestination || binding !== void 0 && binding.source !== "tracked" || hasAmbiguousCheckoutWriters(selector, providers)) add({
|
|
2715
2246
|
rule: "action-pins",
|
|
2716
2247
|
line,
|
|
2717
2248
|
message: "local actions require an unambiguous checkout of the current source snapshot"
|
|
@@ -2767,24 +2298,55 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2767
2298
|
const checkoutRepository = getInput(node, "repository");
|
|
2768
2299
|
const checkoutRef = getInput(node, "ref");
|
|
2769
2300
|
const prefix = checkoutPrefixOf(node);
|
|
2301
|
+
const clean = getInput(node, "clean");
|
|
2302
|
+
const cleanIsResolved = clean === void 0 || (0, yaml.isScalar)(clean) && !dynamic(clean.value);
|
|
2770
2303
|
let repo;
|
|
2771
2304
|
if (checkoutRepository !== void 0) repo = (0, yaml.isScalar)(checkoutRepository) ? checkoutRepository.value : null;
|
|
2772
2305
|
const self = repo === void 0 || repo === "${{ github.repository }}" || typeof repo === "string" && repository !== void 0 && repo.toLowerCase() === repository.toLowerCase();
|
|
2773
2306
|
const workflowSource = repo === "${{ job.workflow_repository }}" && (0, yaml.isScalar)(checkoutRef) && checkoutRef.value === "${{ job.workflow_sha }}";
|
|
2774
|
-
const
|
|
2775
|
-
const
|
|
2307
|
+
const triggers = getNode(document.contents, "on");
|
|
2308
|
+
const callerContext = (() => {
|
|
2309
|
+
if ((0, yaml.isMap)(triggers)) return [...keysOf(triggers)];
|
|
2310
|
+
if ((0, yaml.isSeq)(triggers)) return triggers.items.map((event) => (0, yaml.isScalar)(event) ? event.value : void 0);
|
|
2311
|
+
if ((0, yaml.isScalar)(triggers)) return [triggers.value];
|
|
2312
|
+
return [];
|
|
2313
|
+
})().includes("workflow_call");
|
|
2314
|
+
let refValue = (0, yaml.isScalar)(checkoutRef) ? checkoutRef.value : void 0;
|
|
2315
|
+
if (callerContext && (repo === void 0 || repo === "${{ github.repository }}") && checkoutRef === void 0) refValue = "${{ github.sha }}";
|
|
2316
|
+
const currentRef = dynamicRefBody(refValue);
|
|
2317
|
+
const currentSource = self && !callerContext && (checkoutRef === void 0 || currentRef === "github.sha");
|
|
2776
2318
|
const dynamicRef = dynamicRefBody(refValue);
|
|
2777
|
-
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);
|
|
2319
|
+
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");
|
|
2320
|
+
const validSource = cleanIsResolved && ref === approved?.sha && sourceMap({
|
|
2321
|
+
node,
|
|
2322
|
+
key: "if"
|
|
2323
|
+
}) === void 0 && sourceMap({
|
|
2324
|
+
node,
|
|
2325
|
+
key: "continue-on-error"
|
|
2326
|
+
}) === void 0 && (currentSource || workflowSource || delegatedSource);
|
|
2327
|
+
if (!validSource) unreviewableCheckoutSource = true;
|
|
2778
2328
|
if (!staticRepositoryPath(prefix)) checkoutBindings.push({
|
|
2779
2329
|
path: ".",
|
|
2330
|
+
dynamicSource: validSource && dynamic(prefix),
|
|
2780
2331
|
source: "untrusted",
|
|
2781
2332
|
reason: "checkout destination must be a static repository path",
|
|
2782
2333
|
line,
|
|
2783
|
-
sparse:
|
|
2334
|
+
sparse: sparseCheckoutOf(node)
|
|
2784
2335
|
});
|
|
2785
2336
|
else {
|
|
2786
2337
|
const destination = normalizeCheckoutPath(prefix);
|
|
2787
|
-
|
|
2338
|
+
checkoutBindings = checkoutBindings.map((entry) => {
|
|
2339
|
+
if (entry.path === destination || destination !== "." && !entry.path.startsWith(`${destination}/`)) return entry;
|
|
2340
|
+
return {
|
|
2341
|
+
path: entry.path,
|
|
2342
|
+
sparse: { mode: "all" },
|
|
2343
|
+
dynamicSource: false,
|
|
2344
|
+
source: "untrusted",
|
|
2345
|
+
reason: "a later ancestor checkout invalidates this descendant destination",
|
|
2346
|
+
line
|
|
2347
|
+
};
|
|
2348
|
+
});
|
|
2349
|
+
const trustedWriter = cleanIsResolved && ref === approved?.sha && sourceMap({
|
|
2788
2350
|
node,
|
|
2789
2351
|
key: "if"
|
|
2790
2352
|
}) === void 0 && sourceMap({
|
|
@@ -2793,18 +2355,21 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2793
2355
|
}) === void 0 && checkoutDestinations.get(destination) === 1 && !unknownCheckoutDestination;
|
|
2794
2356
|
if (trustedWriter && delegatedSource && typeof refValue === "string") checkoutBindings.push({
|
|
2795
2357
|
path: destination,
|
|
2358
|
+
dynamicSource: validSource && delegatedSource,
|
|
2796
2359
|
sparse: sparseCheckoutOf(node),
|
|
2797
2360
|
source: "delegated",
|
|
2798
2361
|
ref: refValue
|
|
2799
2362
|
});
|
|
2800
2363
|
else if (trustedWriter && (workflowSource || currentSource)) checkoutBindings.push({
|
|
2801
2364
|
path: destination,
|
|
2365
|
+
dynamicSource: validSource && delegatedSource,
|
|
2802
2366
|
sparse: sparseCheckoutOf(node),
|
|
2803
2367
|
source: "tracked"
|
|
2804
2368
|
});
|
|
2805
2369
|
else {
|
|
2806
2370
|
let reason = "checkout does not select a trusted repository source snapshot";
|
|
2807
|
-
if (
|
|
2371
|
+
if (!cleanIsResolved) reason = "checkout clean must be statically resolved";
|
|
2372
|
+
else if (ref !== approved?.sha) reason = "actions/checkout must use the approved action SHA";
|
|
2808
2373
|
else if (sourceMap({
|
|
2809
2374
|
node,
|
|
2810
2375
|
key: "if"
|
|
@@ -2815,6 +2380,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2815
2380
|
else if (checkoutDestinations.get(destination) !== 1 || unknownCheckoutDestination) reason = "checkout destination must have exactly one static writer";
|
|
2816
2381
|
checkoutBindings.push({
|
|
2817
2382
|
path: destination,
|
|
2383
|
+
dynamicSource: validSource && delegatedSource,
|
|
2818
2384
|
sparse: sparseCheckoutOf(node),
|
|
2819
2385
|
source: "untrusted",
|
|
2820
2386
|
reason,
|
|
@@ -2863,21 +2429,23 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2863
2429
|
line: reference === void 0 ? line : nodeLine(reference)
|
|
2864
2430
|
});
|
|
2865
2431
|
};
|
|
2866
|
-
const checkContainer = (node) => {
|
|
2432
|
+
const checkContainer = (node, at) => {
|
|
2867
2433
|
node = resolveNode(node);
|
|
2868
2434
|
if (node === void 0) return;
|
|
2869
2435
|
const image = (0, yaml.isMap)(node) ? getNode(node, "image") : node;
|
|
2870
2436
|
checkRuntimeImage({
|
|
2871
2437
|
image: (0, yaml.isScalar)(image) ? image.value : void 0,
|
|
2872
2438
|
line: nodeLine(image ?? node),
|
|
2873
|
-
label: "container image"
|
|
2439
|
+
label: "container image",
|
|
2440
|
+
at
|
|
2874
2441
|
});
|
|
2875
2442
|
};
|
|
2876
|
-
const checkSteps = (node, floor) => {
|
|
2443
|
+
const checkSteps = (node, locationPrefix, floor) => {
|
|
2877
2444
|
currentEngineFloor = void 0;
|
|
2878
2445
|
checkoutBindings = [];
|
|
2879
2446
|
checkoutDestinations = /* @__PURE__ */ new Map();
|
|
2880
2447
|
unknownCheckoutDestination = false;
|
|
2448
|
+
unreviewableCheckoutSource = false;
|
|
2881
2449
|
node = resolveNode(node);
|
|
2882
2450
|
if (!(0, yaml.isSeq)(node)) return;
|
|
2883
2451
|
for (const step of node.items) {
|
|
@@ -2893,9 +2461,14 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2893
2461
|
}
|
|
2894
2462
|
currentEngineFloor = floor;
|
|
2895
2463
|
try {
|
|
2896
|
-
for (const step of node.items)
|
|
2464
|
+
for (const step of node.items) {
|
|
2465
|
+
const id = getNode(step, "id");
|
|
2466
|
+
currentLocation = (0, yaml.isScalar)(id) && typeof id.value === "string" ? `${locationPrefix}.steps.${id.value}` : void 0;
|
|
2467
|
+
checkAction(step);
|
|
2468
|
+
}
|
|
2897
2469
|
} finally {
|
|
2898
2470
|
currentEngineFloor = void 0;
|
|
2471
|
+
currentLocation = void 0;
|
|
2899
2472
|
}
|
|
2900
2473
|
};
|
|
2901
2474
|
try {
|
|
@@ -2909,15 +2482,15 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2909
2482
|
currentEngineFloor = void 0;
|
|
2910
2483
|
const job = getNode(jobs, key);
|
|
2911
2484
|
checkAction(job);
|
|
2912
|
-
checkContainer(getNode(job, "container"));
|
|
2485
|
+
checkContainer(getNode(job, "container"), `jobs.${key}.container`);
|
|
2913
2486
|
const services = getNode(job, "services");
|
|
2914
|
-
if ((0, yaml.isMap)(services)) for (const service of keysOf(services)) checkContainer(getNode(services, service));
|
|
2915
|
-
checkSteps(getNode(job, "steps"), engineFloors.find((entry) => entry.job === key));
|
|
2487
|
+
if ((0, yaml.isMap)(services)) for (const service of keysOf(services)) checkContainer(getNode(services, service), `jobs.${key}.services.${service}`);
|
|
2488
|
+
checkSteps(getNode(job, "steps"), `jobs.${key}`, engineFloors.find((entry) => entry.job === key));
|
|
2916
2489
|
}
|
|
2917
2490
|
} else {
|
|
2918
2491
|
const runs = getNode(document.contents, "runs");
|
|
2919
2492
|
const using = getNode(runs, "using");
|
|
2920
|
-
if ((0, yaml.isScalar)(using) && using.value === "composite") checkSteps(getNode(runs, "steps"));
|
|
2493
|
+
if ((0, yaml.isScalar)(using) && using.value === "composite") checkSteps(getNode(runs, "steps"), "runs");
|
|
2921
2494
|
if ((0, yaml.isScalar)(using) && using.value === "docker") {
|
|
2922
2495
|
const image = getNode(runs, "image");
|
|
2923
2496
|
const value = (0, yaml.isScalar)(image) ? image.value : void 0;
|
|
@@ -2949,192 +2522,6 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2949
2522
|
return diagnostics;
|
|
2950
2523
|
};
|
|
2951
2524
|
//#endregion
|
|
2952
|
-
//#region src/toolchain-schema.ts
|
|
2953
|
-
const packagePinKeys = [
|
|
2954
|
-
"oxlint",
|
|
2955
|
-
"oxlint-tsgolint",
|
|
2956
|
-
"@oxlint/plugins",
|
|
2957
|
-
"@stll/oxlint-plugin",
|
|
2958
|
-
"oxfmt",
|
|
2959
|
-
"lefthook"
|
|
2960
|
-
];
|
|
2961
|
-
const valkeyArchitectures = ["amd64", "arm64"];
|
|
2962
|
-
const valkeyArtifactPackages = {
|
|
2963
|
-
server: "valkey-server",
|
|
2964
|
-
tools: "valkey-tools"
|
|
2965
|
-
};
|
|
2966
|
-
const record$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2967
|
-
const artifactRecord = (value, keys) => {
|
|
2968
|
-
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");
|
|
2969
|
-
return value;
|
|
2970
|
-
};
|
|
2971
|
-
const parseValkeyArtifacts = (input, version) => {
|
|
2972
|
-
const architectures = artifactRecord(input, valkeyArchitectures);
|
|
2973
|
-
const line = version.split(".").slice(0, 2).join(".");
|
|
2974
|
-
const parseArchitecture = (architecture) => {
|
|
2975
|
-
const packages = artifactRecord(architectures[architecture], Object.keys(valkeyArtifactPackages));
|
|
2976
|
-
const parseArtifact = (kind) => {
|
|
2977
|
-
const artifact = artifactRecord(packages[kind], ["url", "sha256"]);
|
|
2978
|
-
const url = artifact["url"];
|
|
2979
|
-
const sha256 = artifact["sha256"];
|
|
2980
|
-
const expected = `https://download.valkey.io/packaging/valkey-${line}/deb/ubuntu2404/${architecture}/${valkeyArtifactPackages[kind]}_${version}-1.noble_${architecture}.deb`;
|
|
2981
|
-
if (typeof url !== "string" || url !== expected) throw new Error("valkeyArtifacts URL must select the exact official versioned Ubuntu artifact");
|
|
2982
|
-
if (typeof sha256 !== "string" || !/^[a-f0-9]{64}$/.test(sha256)) throw new Error("valkeyArtifacts requires a lowercase SHA256 pin");
|
|
2983
|
-
return {
|
|
2984
|
-
url,
|
|
2985
|
-
sha256
|
|
2986
|
-
};
|
|
2987
|
-
};
|
|
2988
|
-
return {
|
|
2989
|
-
server: parseArtifact("server"),
|
|
2990
|
-
tools: parseArtifact("tools")
|
|
2991
|
-
};
|
|
2992
|
-
};
|
|
2993
|
-
return {
|
|
2994
|
-
amd64: parseArchitecture("amd64"),
|
|
2995
|
-
arm64: parseArchitecture("arm64")
|
|
2996
|
-
};
|
|
2997
|
-
};
|
|
2998
|
-
/** Reject unsupported or incomplete installed policies before inspecting a consumer. */
|
|
2999
|
-
const parseToolchainPolicy = (input) => {
|
|
3000
|
-
if (!record$1(input) || input["schemaVersion"] !== 1) throw new Error("toolchain.json must use schemaVersion 1");
|
|
3001
|
-
const string = (object, key) => {
|
|
3002
|
-
const value = object[key];
|
|
3003
|
-
if (typeof value !== "string" || value.trim() === "") throw new Error(`toolchain.json requires a nonempty ${key}`);
|
|
3004
|
-
return value;
|
|
3005
|
-
};
|
|
3006
|
-
const packages = {};
|
|
3007
|
-
for (const key of packagePinKeys) packages[key] = string(input, key);
|
|
3008
|
-
const bun = string(input, "bun");
|
|
3009
|
-
const typescript = string(input, "typescript");
|
|
3010
|
-
const node = string(input, "node");
|
|
3011
|
-
const python = string(input, "python");
|
|
3012
|
-
const postgres = string(input, "postgres");
|
|
3013
|
-
const valkey = string(input, "valkey");
|
|
3014
|
-
const rust = string(input, "rust");
|
|
3015
|
-
const rustCompilerDevelopment = string(input, "rustCompilerDevelopment");
|
|
3016
|
-
for (const [name, value] of Object.entries({
|
|
3017
|
-
...packages,
|
|
3018
|
-
bun,
|
|
3019
|
-
typescript
|
|
3020
|
-
})) if ((0, semver.valid)(value) !== value) throw new Error(`toolchain.json ${name} must be an exact release`);
|
|
3021
|
-
if (!/^[1-9]\d*$/.test(postgres) || !Number.isSafeInteger(Number(postgres))) throw new Error("PostgreSQL must be a canonical positive major");
|
|
3022
|
-
if ((0, semver.valid)(valkey) !== valkey || !/^\d+\.\d+\.\d+$/.test(valkey)) throw new Error("Valkey must be an exact stable release");
|
|
3023
|
-
const valkeyArtifacts = parseValkeyArtifacts(input["valkeyArtifacts"], valkey);
|
|
3024
|
-
if (!nodePolicyValid(node)) throw new Error("Node must be a canonical major N.x series");
|
|
3025
|
-
if (!/^\d+\.\d+(?:\.\d+)?$/.test(python)) throw new Error("Python must be an exact minor or patch release");
|
|
3026
|
-
if (!/^\d+\.\d+\.\d+$/.test(rust)) throw new Error("Rust must be an exact stable release");
|
|
3027
|
-
if (!/^nightly-\d{4}-\d{2}-\d{2}$/.test(rustCompilerDevelopment)) throw new Error("Rust compiler development must be a dated nightly");
|
|
3028
|
-
const compatibility = input["typescript6Compatibility"];
|
|
3029
|
-
if (!record$1(compatibility)) throw new Error("missing TypeScript compatibility policy");
|
|
3030
|
-
const version = string(compatibility, "version");
|
|
3031
|
-
const packageAlias = string(compatibility, "packageAlias");
|
|
3032
|
-
if ((0, semver.valid)(version) !== version) throw new Error("TypeScript compatibility must be an exact release");
|
|
3033
|
-
const layouts = input["typescriptInstallLayouts"];
|
|
3034
|
-
if (!Array.isArray(layouts) || layouts.length === 0) throw new Error("missing TypeScript install layouts");
|
|
3035
|
-
const typescriptInstallLayouts = layouts.map((layout) => {
|
|
3036
|
-
if (!record$1(layout)) throw new Error("invalid TypeScript install layout");
|
|
3037
|
-
const compilerPackage = string(layout, "compilerPackage");
|
|
3038
|
-
const compilerSpecifier = string(layout, "compilerSpecifier");
|
|
3039
|
-
const typecheckCommand = string(layout, "typecheckCommand");
|
|
3040
|
-
switch (layout["type"]) {
|
|
3041
|
-
case "direct":
|
|
3042
|
-
if (compilerSpecifier !== typescript || compilerPackage !== "typescript") throw new Error("direct TypeScript layout differs from shared version");
|
|
3043
|
-
return {
|
|
3044
|
-
type: "direct",
|
|
3045
|
-
compilerPackage,
|
|
3046
|
-
compilerSpecifier,
|
|
3047
|
-
typecheckCommand
|
|
3048
|
-
};
|
|
3049
|
-
case "split-compatibility": {
|
|
3050
|
-
const compatibilityPackage = string(layout, "compatibilityPackage");
|
|
3051
|
-
const compatibilitySpecifier = string(layout, "compatibilitySpecifier");
|
|
3052
|
-
if (compilerSpecifier !== `npm:typescript@${typescript}` || compatibilitySpecifier !== version) throw new Error("split TypeScript layout differs from shared versions");
|
|
3053
|
-
return {
|
|
3054
|
-
type: "split-compatibility",
|
|
3055
|
-
compilerPackage,
|
|
3056
|
-
compilerSpecifier,
|
|
3057
|
-
compatibilityPackage,
|
|
3058
|
-
compatibilitySpecifier,
|
|
3059
|
-
typecheckCommand
|
|
3060
|
-
};
|
|
3061
|
-
}
|
|
3062
|
-
default: throw new Error("unsupported TypeScript install layout");
|
|
3063
|
-
}
|
|
3064
|
-
});
|
|
3065
|
-
const actionsInput = input["actions"];
|
|
3066
|
-
if (!record$1(actionsInput) || Object.keys(actionsInput).length === 0) throw new Error("missing approved actions");
|
|
3067
|
-
const actions = {};
|
|
3068
|
-
for (const [name, action] of Object.entries(actionsInput)) {
|
|
3069
|
-
if (!record$1(action)) throw new Error(`invalid action policy: ${name}`);
|
|
3070
|
-
const sha = string(action, "sha");
|
|
3071
|
-
const actionVersion = string(action, "version");
|
|
3072
|
-
if (!/^[a-f0-9]{40}$/.test(sha) || !/^v\d+(?:\.\d+)*$/.test(actionVersion)) throw new Error(`invalid action SHA/version: ${name}`);
|
|
3073
|
-
actions[name] = {
|
|
3074
|
-
sha,
|
|
3075
|
-
version: actionVersion
|
|
3076
|
-
};
|
|
3077
|
-
}
|
|
3078
|
-
const dependabotInput = input["dependabot"];
|
|
3079
|
-
if (!record$1(dependabotInput) || !record$1(dependabotInput["schedule"]) || !record$1(dependabotInput["cooldown"]) || !record$1(dependabotInput["groups"])) throw new Error("missing Dependabot policy");
|
|
3080
|
-
const scheduleInput = dependabotInput["schedule"];
|
|
3081
|
-
const schedule = {
|
|
3082
|
-
interval: string(scheduleInput, "interval"),
|
|
3083
|
-
day: string(scheduleInput, "day"),
|
|
3084
|
-
time: string(scheduleInput, "time"),
|
|
3085
|
-
timezone: string(scheduleInput, "timezone")
|
|
3086
|
-
};
|
|
3087
|
-
const defaultDays = dependabotInput["cooldown"]["defaultDays"];
|
|
3088
|
-
if (typeof defaultDays !== "number" || !Number.isInteger(defaultDays) || defaultDays < 5) throw new Error("Dependabot cooldown must be at least five days");
|
|
3089
|
-
const strings = (value) => {
|
|
3090
|
-
if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) throw new Error("Dependabot group values must be string arrays");
|
|
3091
|
-
return value;
|
|
3092
|
-
};
|
|
3093
|
-
const groups = {};
|
|
3094
|
-
for (const [name, group] of Object.entries(dependabotInput["groups"])) {
|
|
3095
|
-
if (!record$1(group)) throw new Error(`invalid Dependabot group: ${name}`);
|
|
3096
|
-
groups[name] = {
|
|
3097
|
-
patterns: strings(group["patterns"]),
|
|
3098
|
-
updateTypes: strings(group["updateTypes"])
|
|
3099
|
-
};
|
|
3100
|
-
}
|
|
3101
|
-
return {
|
|
3102
|
-
bun,
|
|
3103
|
-
typescript,
|
|
3104
|
-
node,
|
|
3105
|
-
postgres,
|
|
3106
|
-
valkey,
|
|
3107
|
-
valkeyArtifacts,
|
|
3108
|
-
python,
|
|
3109
|
-
rust,
|
|
3110
|
-
rustCompilerDevelopment,
|
|
3111
|
-
packages,
|
|
3112
|
-
actions,
|
|
3113
|
-
typescriptInstallLayouts,
|
|
3114
|
-
typescript6Compatibility: {
|
|
3115
|
-
version,
|
|
3116
|
-
packageAlias
|
|
3117
|
-
},
|
|
3118
|
-
dependabot: {
|
|
3119
|
-
schedule,
|
|
3120
|
-
cooldown: { defaultDays },
|
|
3121
|
-
groups,
|
|
3122
|
-
ignoredPackages: [.../* @__PURE__ */ new Set([
|
|
3123
|
-
...Object.keys(packages),
|
|
3124
|
-
...typescriptInstallLayouts.flatMap((layout) => layout.type === "direct" ? [layout.compilerPackage] : [layout.compilerPackage, layout.compatibilityPackage]),
|
|
3125
|
-
packageAlias,
|
|
3126
|
-
"bun-types"
|
|
3127
|
-
])],
|
|
3128
|
-
ignoredActions: Object.keys(actions),
|
|
3129
|
-
ignoredImages: [
|
|
3130
|
-
"node",
|
|
3131
|
-
"python",
|
|
3132
|
-
"oven/bun"
|
|
3133
|
-
]
|
|
3134
|
-
}
|
|
3135
|
-
};
|
|
3136
|
-
};
|
|
3137
|
-
//#endregion
|
|
3138
2525
|
//#region src/toolchain-guard.ts
|
|
3139
2526
|
const toolchainRules = [.../* @__PURE__ */ new Set([
|
|
3140
2527
|
...packageRules,
|
|
@@ -3146,7 +2533,7 @@ const record = (value) => typeof value === "object" && value !== null && !Array.
|
|
|
3146
2533
|
const array = (value) => Array.isArray(value);
|
|
3147
2534
|
/** Repository-owned declarations are explicit; malformed decisions fail closed. */
|
|
3148
2535
|
const parseToolchainConfiguration = (input) => {
|
|
3149
|
-
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
|
|
2536
|
+
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");
|
|
3150
2537
|
const disabled = /* @__PURE__ */ new Set();
|
|
3151
2538
|
const optOuts = input["optOuts"] ?? [];
|
|
3152
2539
|
if (!array(optOuts)) throw new Error("optOuts must be an array");
|
|
@@ -3158,8 +2545,10 @@ const parseToolchainConfiguration = (input) => {
|
|
|
3158
2545
|
}
|
|
3159
2546
|
return {
|
|
3160
2547
|
disabled,
|
|
3161
|
-
cloud: parseCloudSetup(input["cloud"]),
|
|
3162
|
-
engineFloors: parseEngineFloors(input["engineFloors"])
|
|
2548
|
+
cloud: require_toolchain_inputs.parseCloudSetup(input["cloud"]),
|
|
2549
|
+
engineFloors: parseEngineFloors(input["engineFloors"]),
|
|
2550
|
+
dynamicSelectors: parseDynamicSelectors(input["dynamicSelectors"]),
|
|
2551
|
+
consumerChecks: parseConsumerChecks(input["consumerChecks"])
|
|
3163
2552
|
};
|
|
3164
2553
|
};
|
|
3165
2554
|
/** Read one tracked configuration snapshot for guards and policy generation. */
|
|
@@ -3177,7 +2566,7 @@ const readToolchainInputs = (root) => {
|
|
|
3177
2566
|
const trackedFiles = new Set(tracked);
|
|
3178
2567
|
const resolvedRoot = (0, node_fs.realpathSync)(root);
|
|
3179
2568
|
for (const file of tracked) {
|
|
3180
|
-
const kind = toolchainInputKind(file);
|
|
2569
|
+
const kind = require_toolchain_inputs.toolchainInputKind(file);
|
|
3181
2570
|
if (kind === void 0) continue;
|
|
3182
2571
|
if (kind === "presence") {
|
|
3183
2572
|
files[file] = "";
|
|
@@ -3222,13 +2611,12 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
|
|
|
3222
2611
|
}).trim();
|
|
3223
2612
|
repository = /^(?:https?:\/\/github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)([^/]+\/[^/]+?)(?:\.git)?\/?$/.exec(origin)?.[1];
|
|
3224
2613
|
} catch {}
|
|
3225
|
-
let
|
|
3226
|
-
let cloud;
|
|
2614
|
+
let configuration = parseToolchainConfiguration({});
|
|
3227
2615
|
const engineFloors = [];
|
|
2616
|
+
let dynamicSelectors = [];
|
|
3228
2617
|
if (files["stll-toolchain.json"] !== void 0) try {
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
cloud = configuration.cloud;
|
|
2618
|
+
configuration = parseToolchainConfiguration(JSON.parse(files["stll-toolchain.json"]));
|
|
2619
|
+
dynamicSelectors = configuration.dynamicSelectors;
|
|
3232
2620
|
for (const entry of configuration.engineFloors) engineFloors.push(resolveEngineFloor(entry, files));
|
|
3233
2621
|
} catch (error) {
|
|
3234
2622
|
diagnostics.push({
|
|
@@ -3238,36 +2626,43 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
|
|
|
3238
2626
|
message: error instanceof Error ? error.message : String(error)
|
|
3239
2627
|
});
|
|
3240
2628
|
}
|
|
3241
|
-
const
|
|
2629
|
+
const cloud = configuration.cloud;
|
|
2630
|
+
const script = files[require_toolchain_inputs.cloudSetupPath];
|
|
3242
2631
|
if (cloud === void 0 && trackedFiles.has(".agents/cloud-setup.sh")) diagnostics.push({
|
|
3243
2632
|
rule: "cloud-setup-drift",
|
|
3244
|
-
path: cloudSetupPath,
|
|
2633
|
+
path: require_toolchain_inputs.cloudSetupPath,
|
|
3245
2634
|
line: 1,
|
|
3246
2635
|
message: "cloud setup script requires an explicit cloud declaration in stll-toolchain.json"
|
|
3247
2636
|
});
|
|
3248
2637
|
if (cloud !== void 0) try {
|
|
3249
2638
|
const nodeVersion = files[".node-version"];
|
|
3250
2639
|
if (nodeVersion === void 0) throw new Error("cloud setup requires a tracked root .node-version with an exact stable release");
|
|
3251
|
-
if (script !== generateCloudSetup({
|
|
2640
|
+
if (script !== require_toolchain_inputs.generateCloudSetup({
|
|
3252
2641
|
policy,
|
|
3253
2642
|
cloud,
|
|
3254
2643
|
nodeVersion: nodeVersion.trim()
|
|
3255
2644
|
})) diagnostics.push({
|
|
3256
2645
|
rule: "cloud-setup-drift",
|
|
3257
|
-
path: cloudSetupPath,
|
|
2646
|
+
path: require_toolchain_inputs.cloudSetupPath,
|
|
3258
2647
|
line: 1,
|
|
3259
2648
|
message: "cloud setup script is missing or differs from shared policy; run stll-cloud-setup"
|
|
3260
2649
|
});
|
|
3261
2650
|
} catch (error) {
|
|
3262
2651
|
diagnostics.push({
|
|
3263
2652
|
rule: "cloud-setup-drift",
|
|
3264
|
-
path: cloudSetupPath,
|
|
2653
|
+
path: require_toolchain_inputs.cloudSetupPath,
|
|
3265
2654
|
line: 1,
|
|
3266
2655
|
message: error instanceof Error ? error.message : String(error)
|
|
3267
2656
|
});
|
|
3268
2657
|
}
|
|
3269
2658
|
const exercisedFloors = /* @__PURE__ */ new Set();
|
|
3270
2659
|
const mismatchedFloors = /* @__PURE__ */ new Set();
|
|
2660
|
+
const selectorMatches = /* @__PURE__ */ new Map();
|
|
2661
|
+
diagnostics.push(...checkConsumerChecks({
|
|
2662
|
+
declarations: configuration.consumerChecks,
|
|
2663
|
+
files,
|
|
2664
|
+
policy
|
|
2665
|
+
}));
|
|
3271
2666
|
diagnostics.push(...checkPackageFiles({
|
|
3272
2667
|
files,
|
|
3273
2668
|
policy
|
|
@@ -3283,6 +2678,8 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
|
|
|
3283
2678
|
}
|
|
3284
2679
|
},
|
|
3285
2680
|
trackedFiles,
|
|
2681
|
+
dynamicSelectors: dynamicSelectors.filter((entry) => entry.path === file),
|
|
2682
|
+
onDynamicSelector: (entry) => selectorMatches.set(entry, (selectorMatches.get(entry) ?? 0) + 1),
|
|
3286
2683
|
repository,
|
|
3287
2684
|
onDelegated,
|
|
3288
2685
|
engineFloors: engineFloors.filter((entry) => entry.workflow === file),
|
|
@@ -3298,11 +2695,17 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
|
|
|
3298
2695
|
line: 1,
|
|
3299
2696
|
message: `engine floor ${entry.workflow}:${entry.job} requires a setup-node exact patch satisfying ${entry.package} engines.node minimum major ${entry.major}`
|
|
3300
2697
|
});
|
|
2698
|
+
for (const entry of dynamicSelectors) if (selectorMatches.get(entry) !== 1) diagnostics.push({
|
|
2699
|
+
rule: "configuration",
|
|
2700
|
+
path: "stll-toolchain.json",
|
|
2701
|
+
line: 1,
|
|
2702
|
+
message: `dynamic selector ${entry.path}:${entry.at ?? entry.line} must match exactly one unresolved ${entry.kind} selector`
|
|
2703
|
+
});
|
|
3301
2704
|
diagnostics.push(...checkDependabot({
|
|
3302
2705
|
files,
|
|
3303
2706
|
policy: policy.dependabot
|
|
3304
2707
|
}));
|
|
3305
|
-
return diagnostics.filter((diagnostic) => !disabled.has(diagnostic.rule));
|
|
2708
|
+
return diagnostics.filter((diagnostic) => !configuration.disabled.has(diagnostic.rule));
|
|
3306
2709
|
};
|
|
3307
2710
|
//#endregion
|
|
3308
2711
|
Object.defineProperty(exports, "checkToolchain", {
|
|
@@ -3311,30 +2714,12 @@ Object.defineProperty(exports, "checkToolchain", {
|
|
|
3311
2714
|
return checkToolchain;
|
|
3312
2715
|
}
|
|
3313
2716
|
});
|
|
3314
|
-
Object.defineProperty(exports, "cloudSetupPath", {
|
|
3315
|
-
enumerable: true,
|
|
3316
|
-
get: function() {
|
|
3317
|
-
return cloudSetupPath;
|
|
3318
|
-
}
|
|
3319
|
-
});
|
|
3320
|
-
Object.defineProperty(exports, "generateCloudSetup", {
|
|
3321
|
-
enumerable: true,
|
|
3322
|
-
get: function() {
|
|
3323
|
-
return generateCloudSetup;
|
|
3324
|
-
}
|
|
3325
|
-
});
|
|
3326
2717
|
Object.defineProperty(exports, "parseToolchainConfiguration", {
|
|
3327
2718
|
enumerable: true,
|
|
3328
2719
|
get: function() {
|
|
3329
2720
|
return parseToolchainConfiguration;
|
|
3330
2721
|
}
|
|
3331
2722
|
});
|
|
3332
|
-
Object.defineProperty(exports, "parseToolchainPolicy", {
|
|
3333
|
-
enumerable: true,
|
|
3334
|
-
get: function() {
|
|
3335
|
-
return parseToolchainPolicy;
|
|
3336
|
-
}
|
|
3337
|
-
});
|
|
3338
2723
|
Object.defineProperty(exports, "readToolchainInputs", {
|
|
3339
2724
|
enumerable: true,
|
|
3340
2725
|
get: function() {
|