vouchington-tooling 0.0.16 → 0.0.17
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 +12 -0
- package/dist/cli/commands/nuget-central-version.d.mts +1 -0
- package/dist/cli/commands/nuget-central-version.mjs +11 -0
- package/dist/cli/commands/swift-semantic-equal.d.mts +1 -0
- package/dist/cli/commands/swift-semantic-equal.mjs +4 -0
- package/dist/cli/index.mjs +15 -0
- package/dist/cli/parse.d.mts +7 -1
- package/dist/cli/parse.mjs +10 -0
- package/dist/cli/usage.d.mts +1 -1
- package/dist/cli/usage.mjs +17 -1
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +4 -0
- package/dist/nuget-central-version/cli.d.mts +1 -0
- package/dist/nuget-central-version/cli.mjs +11 -0
- package/dist/nuget-central-version/index.d.mts +1 -0
- package/dist/nuget-central-version/index.mjs +74 -0
- package/dist/swift-resolved-pin-delta/index.d.mts +18 -0
- package/dist/swift-resolved-pin-delta/index.mjs +63 -0
- package/dist/swift-semantic-equal/cli.d.mts +2 -0
- package/dist/swift-semantic-equal/cli.mjs +26 -0
- package/dist/swift-semantic-equal/index.d.mts +1 -0
- package/dist/swift-semantic-equal/index.mjs +1 -0
- package/dist/swift-semantic-equal/normalize.d.mts +1 -0
- package/dist/swift-semantic-equal/normalize.mjs +108 -0
- package/dist/swift-source-offset/index.d.mts +2 -0
- package/dist/swift-source-offset/index.mjs +68 -0
- package/package.json +21 -1
- package/scripts/gha/ghcr-package-retention.sh +65 -0
- package/scripts/gha/install-github-release.sh +57 -13
- package/scripts/gha/install-playwright-chromium-arm64.sh +84 -0
- package/scripts/gha/lint-links.sh +92 -0
- package/scripts/gha/materialize-pr-context.sh +123 -0
- package/scripts/gha/run-with-timeout.sh +119 -0
- package/scripts/gha/wait-for-apt-locks.sh +60 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Keep the most recent KEEP_MIN non-protected GHCR package versions, plus any
|
|
3
|
+
# version whose tags match the protected prefix/contains rules.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
KEEP_MIN="${KEEP_MIN:-10}"
|
|
7
|
+
OWNER_TYPE="${GHCR_OWNER_TYPE:-user}"
|
|
8
|
+
OWNER="${GHCR_OWNER:-}"
|
|
9
|
+
PROTECTED_PREFIX="${PROTECTED_TAG_PREFIX:-latest}"
|
|
10
|
+
PROTECTED_CONTAINS="${PROTECTED_TAG_CONTAINS:-buildcache}"
|
|
11
|
+
|
|
12
|
+
case "$KEEP_MIN" in
|
|
13
|
+
*[!0-9]* | '')
|
|
14
|
+
echo "ghcr-package-retention: KEEP_MIN must be a non-negative integer" >&2
|
|
15
|
+
exit 2
|
|
16
|
+
;;
|
|
17
|
+
esac
|
|
18
|
+
|
|
19
|
+
case "$OWNER_TYPE" in
|
|
20
|
+
user) package_root="user/packages/container" ;;
|
|
21
|
+
org)
|
|
22
|
+
[ -n "$OWNER" ] || {
|
|
23
|
+
echo "ghcr-package-retention: GHCR_OWNER is required when GHCR_OWNER_TYPE=org" >&2
|
|
24
|
+
exit 2
|
|
25
|
+
}
|
|
26
|
+
package_root="orgs/${OWNER}/packages/container"
|
|
27
|
+
;;
|
|
28
|
+
*)
|
|
29
|
+
echo "ghcr-package-retention: GHCR_OWNER_TYPE must be user or org" >&2
|
|
30
|
+
exit 2
|
|
31
|
+
;;
|
|
32
|
+
esac
|
|
33
|
+
|
|
34
|
+
if [ "$#" -eq 0 ]; then
|
|
35
|
+
echo "usage: ghcr-package-retention.sh <url-encoded-package>..." >&2
|
|
36
|
+
exit 2
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
for pkg in "$@"; do
|
|
40
|
+
echo "=== Processing $pkg ==="
|
|
41
|
+
count=0
|
|
42
|
+
while IFS= read -r version_json; do
|
|
43
|
+
version_id=$(printf '%s' "$version_json" | jq -r '.id')
|
|
44
|
+
tags=$(printf '%s' "$version_json" | jq -r '[.metadata.container.tags[]?]')
|
|
45
|
+
|
|
46
|
+
protected=$(printf '%s' "$tags" |
|
|
47
|
+
jq --arg prefix "$PROTECTED_PREFIX" --arg contains "$PROTECTED_CONTAINS" \
|
|
48
|
+
'map(select(startswith($prefix) or contains($contains))) | length')
|
|
49
|
+
if [ "$protected" -gt 0 ]; then
|
|
50
|
+
printf ' keep (protected tag): version %s\n' "$version_id"
|
|
51
|
+
continue
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
count=$((count + 1))
|
|
55
|
+
if [ "$count" -le "$KEEP_MIN" ]; then
|
|
56
|
+
printf ' keep (recent %d/%d): version %s\n' "$count" "$KEEP_MIN" "$version_id"
|
|
57
|
+
continue
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
printf ' delete: version %s\n' "$version_id"
|
|
61
|
+
gh api -X DELETE "${package_root}/$pkg/versions/$version_id" || true
|
|
62
|
+
done < <(gh api \
|
|
63
|
+
"${package_root}/$pkg/versions?per_page=100" \
|
|
64
|
+
--jq '.[] | @json')
|
|
65
|
+
done
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
4
|
usage() {
|
|
5
|
-
echo "usage: install-github-release.sh --repo owner/name --version VERSION --asset TEMPLATE --bin NAME [--tag-prefix PREFIX] [--strip-components N]" >&2
|
|
5
|
+
echo "usage: install-github-release.sh --repo owner/name --version VERSION --asset TEMPLATE --bin NAME [--tag-prefix PREFIX] [--strip-components N] [--bin-dir DIR] [--version-flag FLAG] [--checksums-asset NAME] [--no-checksum]" >&2
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
REPO="${REPO:-}"
|
|
@@ -11,6 +11,10 @@ ASSET_TEMPLATE="${ASSET_TEMPLATE:-}"
|
|
|
11
11
|
BIN_NAME="${BIN_NAME:-}"
|
|
12
12
|
TAG_PREFIX="${TAG_PREFIX:-}"
|
|
13
13
|
STRIP_COMPONENTS="${STRIP_COMPONENTS:-1}"
|
|
14
|
+
BIN_DIR="${INSTALL_BIN_DIR:-}"
|
|
15
|
+
VERSION_FLAG="${VERSION_FLAG:---version}"
|
|
16
|
+
CHECKSUMS_ASSET="${CHECKSUMS_ASSET:-}"
|
|
17
|
+
NO_CHECKSUM="${NO_CHECKSUM:-0}"
|
|
14
18
|
|
|
15
19
|
if [ -n "${RELEASE_OWNER:-}" ] && [ -n "${RELEASE_REPO:-}" ]; then
|
|
16
20
|
REPO="${RELEASE_OWNER}/${RELEASE_REPO}"
|
|
@@ -48,6 +52,25 @@ while [ "${#}" -gt 0 ]; do
|
|
|
48
52
|
STRIP_COMPONENTS="$2"
|
|
49
53
|
shift 2
|
|
50
54
|
;;
|
|
55
|
+
--bin-dir)
|
|
56
|
+
[ "${#}" -ge 2 ] || { usage; exit 2; }
|
|
57
|
+
BIN_DIR="$2"
|
|
58
|
+
shift 2
|
|
59
|
+
;;
|
|
60
|
+
--version-flag)
|
|
61
|
+
[ "${#}" -ge 2 ] || { usage; exit 2; }
|
|
62
|
+
VERSION_FLAG="$2"
|
|
63
|
+
shift 2
|
|
64
|
+
;;
|
|
65
|
+
--checksums-asset)
|
|
66
|
+
[ "${#}" -ge 2 ] || { usage; exit 2; }
|
|
67
|
+
CHECKSUMS_ASSET="$2"
|
|
68
|
+
shift 2
|
|
69
|
+
;;
|
|
70
|
+
--no-checksum)
|
|
71
|
+
NO_CHECKSUM=1
|
|
72
|
+
shift
|
|
73
|
+
;;
|
|
51
74
|
-h | --help)
|
|
52
75
|
usage
|
|
53
76
|
exit 2
|
|
@@ -74,7 +97,7 @@ if ! [[ "$STRIP_COMPONENTS" =~ ^[0-9]+$ ]]; then
|
|
|
74
97
|
exit 2
|
|
75
98
|
fi
|
|
76
99
|
|
|
77
|
-
BIN_DIR="${RUNNER_TEMP:-${HOME}/.local}/bin"
|
|
100
|
+
BIN_DIR="${BIN_DIR:-${RUNNER_TEMP:-${HOME}/.local}/bin}"
|
|
78
101
|
mkdir -p "$BIN_DIR"
|
|
79
102
|
if [ -n "${GITHUB_PATH:-}" ]; then
|
|
80
103
|
echo "$BIN_DIR" >> "$GITHUB_PATH"
|
|
@@ -82,7 +105,8 @@ fi
|
|
|
82
105
|
|
|
83
106
|
already_installed() {
|
|
84
107
|
local output
|
|
85
|
-
|
|
108
|
+
# shellcheck disable=SC2086
|
|
109
|
+
output="$("$BIN_DIR/$BIN_NAME" $VERSION_FLAG 2>/dev/null || true)"
|
|
86
110
|
printf '%s\n' "$output" | awk -v v="$VERSION" '
|
|
87
111
|
{
|
|
88
112
|
n = split($0, parts, /[^0-9A-Za-z.-]+/)
|
|
@@ -115,22 +139,42 @@ fi
|
|
|
115
139
|
asset_name="$ASSET_TEMPLATE"
|
|
116
140
|
asset_name="${asset_name//\{version\}/${VERSION}}"
|
|
117
141
|
asset_name="${asset_name//\{platform\}/${RELEASE_PLATFORM}}"
|
|
142
|
+
checksums_name="$CHECKSUMS_ASSET"
|
|
143
|
+
checksums_name="${checksums_name//\{version\}/${VERSION}}"
|
|
144
|
+
checksums_name="${checksums_name//\{platform\}/${RELEASE_PLATFORM}}"
|
|
118
145
|
base_url="https://github.com/${REPO}/releases/download/${TAG_PREFIX}${VERSION}"
|
|
119
|
-
|
|
120
|
-
|
|
146
|
+
work="${RUNNER_TEMP:-/tmp}/${BIN_NAME}-${VERSION}-${RELEASE_PLATFORM}"
|
|
147
|
+
archive="${work}-archive"
|
|
148
|
+
extract_dir="${work}-extract"
|
|
121
149
|
|
|
122
150
|
rm -rf "$extract_dir"
|
|
123
151
|
mkdir -p "$extract_dir"
|
|
124
152
|
curl -fsSL -o "$archive" "${base_url}/${asset_name}"
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
153
|
+
if [ "$NO_CHECKSUM" != 1 ]; then
|
|
154
|
+
if [ -n "$checksums_name" ]; then
|
|
155
|
+
curl -fsSL -o "${archive}.checksums" "${base_url}/${checksums_name}"
|
|
156
|
+
expected=$(grep -F "$asset_name" "${archive}.checksums" | awk '{print $1}')
|
|
157
|
+
rm "${archive}.checksums"
|
|
158
|
+
else
|
|
159
|
+
curl -fsSL -o "${archive}.sha256" "${base_url}/${asset_name}.sha256"
|
|
160
|
+
expected=$(awk '{print $1}' "${archive}.sha256")
|
|
161
|
+
rm "${archive}.sha256"
|
|
162
|
+
fi
|
|
163
|
+
if [ -z "$expected" ]; then
|
|
164
|
+
echo "install-github-release.sh: checksum not found for ${asset_name}" >&2
|
|
165
|
+
rm -f "$archive"
|
|
166
|
+
exit 1
|
|
167
|
+
fi
|
|
168
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
169
|
+
echo "${expected} ${archive}" | sha256sum -c -
|
|
170
|
+
else
|
|
171
|
+
echo "${expected} ${archive}" | shasum -a 256 -c -
|
|
172
|
+
fi
|
|
131
173
|
fi
|
|
132
|
-
|
|
133
|
-
|
|
174
|
+
case "$asset_name" in
|
|
175
|
+
*.zip) unzip -q "$archive" -d "$extract_dir" ;;
|
|
176
|
+
*) tar -xzf "$archive" -C "$extract_dir" --strip-components="$STRIP_COMPONENTS" ;;
|
|
177
|
+
esac
|
|
134
178
|
mv "$extract_dir/$BIN_NAME" "$BIN_DIR/$BIN_NAME"
|
|
135
179
|
chmod +x "$BIN_DIR/$BIN_NAME"
|
|
136
180
|
rm -rf "$archive" "$extract_dir"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
# shellcheck source=scripts/gha/download-with-diagnostics.sh
|
|
6
|
+
source "${script_dir}/download-with-diagnostics.sh"
|
|
7
|
+
|
|
8
|
+
browsers_json="${PLAYWRIGHT_BROWSERS_JSON:-}"
|
|
9
|
+
if [ -z "$browsers_json" ] || [ ! -f "$browsers_json" ]; then
|
|
10
|
+
browsers_json="node_modules/playwright/node_modules/playwright-core/browsers.json"
|
|
11
|
+
fi
|
|
12
|
+
if [ ! -f "$browsers_json" ]; then
|
|
13
|
+
browsers_json=$(find node_modules -path '*/playwright-core/browsers.json' -print -quit 2>/dev/null || true)
|
|
14
|
+
fi
|
|
15
|
+
if [ -z "$browsers_json" ] || [ ! -f "$browsers_json" ]; then
|
|
16
|
+
echo "::error::playwright-core/browsers.json not found; set PLAYWRIGHT_BROWSERS_JSON or install Playwright first"
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
browsers_path="${PLAYWRIGHT_BROWSERS_PATH:-${HOME}/.cache/ms-playwright}"
|
|
21
|
+
mirror="${PLAYWRIGHT_CHROMIUM_MIRROR:-https://cdn.playwright.dev/dbazure/download/playwright/builds/chromium}"
|
|
22
|
+
mkdir -p "$browsers_path"
|
|
23
|
+
|
|
24
|
+
browser_revision() {
|
|
25
|
+
node -e "const fs=require('node:fs'); const file=process.argv[1]; const name=process.argv[2]; const browser=JSON.parse(fs.readFileSync(file,'utf8')).browsers.find(b => b.name === name); if (!browser) { process.stderr.write('unknown browser '+name); process.exit(1) } process.stdout.write(String(browser.revision))" "$browsers_json" "$1"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
install_browser() {
|
|
29
|
+
local name="$1" archive="$2"
|
|
30
|
+
local rev
|
|
31
|
+
rev=$(browser_revision "$name")
|
|
32
|
+
local dir="${browsers_path}/${name//-/_}-${rev}"
|
|
33
|
+
local marker="${dir}/INSTALLATION_COMPLETE"
|
|
34
|
+
|
|
35
|
+
if [ -f "$marker" ]; then
|
|
36
|
+
echo "Already installed: ${name}-${rev} (cache hit)"
|
|
37
|
+
return
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
echo "Installing ${name} v${rev} via curl + system unzip"
|
|
41
|
+
local tmp="${RUNNER_TEMP:-/tmp}/${name}-${rev}-${archive}"
|
|
42
|
+
rm -f "$tmp"
|
|
43
|
+
rm -rf "$dir"
|
|
44
|
+
mkdir -p "$dir"
|
|
45
|
+
# integrity-check: skip reason=playwright-cdn-provides-no-published-checksums
|
|
46
|
+
ci_download_to "${mirror}/${rev}/${archive}" "$tmp" --retry 3 --retry-all-errors --max-time 300 --retry-max-time 300
|
|
47
|
+
unzip -q "$tmp" -d "$dir"
|
|
48
|
+
rm -f "$tmp"
|
|
49
|
+
|
|
50
|
+
local exe
|
|
51
|
+
case "$name" in
|
|
52
|
+
chromium) exe="$dir/chrome-linux/chrome" ;;
|
|
53
|
+
chromium-headless-shell) exe="$dir/chrome-linux/headless_shell" ;;
|
|
54
|
+
*)
|
|
55
|
+
echo "::error::install_browser: unknown browser name '$name'"
|
|
56
|
+
exit 1
|
|
57
|
+
;;
|
|
58
|
+
esac
|
|
59
|
+
if [ ! -x "$exe" ]; then
|
|
60
|
+
echo "::error::Expected ${name} binary missing or not executable after extract: $exe"
|
|
61
|
+
exit 1
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
touch "$marker"
|
|
65
|
+
echo "Installed ${name} at ${dir}"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if [ "$#" -eq 0 ]; then
|
|
69
|
+
install_browser chromium chromium-linux-arm64.zip
|
|
70
|
+
install_browser chromium-headless-shell chromium-headless-shell-linux-arm64.zip
|
|
71
|
+
exit 0
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
while [ "$#" -gt 0 ]; do
|
|
75
|
+
spec="$1"
|
|
76
|
+
shift
|
|
77
|
+
name="${spec%%:*}"
|
|
78
|
+
archive="${spec#*:}"
|
|
79
|
+
if [ -z "$name" ] || [ "$archive" = "$spec" ]; then
|
|
80
|
+
echo "::error::browser spec must be name:archive, got '$spec'"
|
|
81
|
+
exit 2
|
|
82
|
+
fi
|
|
83
|
+
install_browser "$name" "$archive"
|
|
84
|
+
done
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Two-pass Markdown link check. Pass 1: internal relative links and #anchors
|
|
3
|
+
# fail the job. Pass 2: external http/https links warn only.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
explicit_offline=false
|
|
7
|
+
options=()
|
|
8
|
+
files=()
|
|
9
|
+
globs=()
|
|
10
|
+
config="lychee.toml"
|
|
11
|
+
|
|
12
|
+
while [ "$#" -gt 0 ]; do
|
|
13
|
+
case "$1" in
|
|
14
|
+
--offline=true | --offline)
|
|
15
|
+
explicit_offline=true
|
|
16
|
+
shift
|
|
17
|
+
;;
|
|
18
|
+
--offline=false)
|
|
19
|
+
shift
|
|
20
|
+
;;
|
|
21
|
+
--config)
|
|
22
|
+
[ "$#" -ge 2 ] || {
|
|
23
|
+
echo "lint-links: --config requires a path" >&2
|
|
24
|
+
exit 2
|
|
25
|
+
}
|
|
26
|
+
config="$2"
|
|
27
|
+
shift 2
|
|
28
|
+
;;
|
|
29
|
+
--glob)
|
|
30
|
+
[ "$#" -ge 2 ] || {
|
|
31
|
+
echo "lint-links: --glob requires a pattern" >&2
|
|
32
|
+
exit 2
|
|
33
|
+
}
|
|
34
|
+
globs+=("$2")
|
|
35
|
+
shift 2
|
|
36
|
+
;;
|
|
37
|
+
--)
|
|
38
|
+
shift
|
|
39
|
+
files+=("$@")
|
|
40
|
+
break
|
|
41
|
+
;;
|
|
42
|
+
-*)
|
|
43
|
+
options+=("$1")
|
|
44
|
+
shift
|
|
45
|
+
;;
|
|
46
|
+
*)
|
|
47
|
+
if [[ "$1" == *.md && -f "$1" ]]; then
|
|
48
|
+
files+=("$1")
|
|
49
|
+
else
|
|
50
|
+
options+=("$1")
|
|
51
|
+
fi
|
|
52
|
+
shift
|
|
53
|
+
;;
|
|
54
|
+
esac
|
|
55
|
+
done
|
|
56
|
+
|
|
57
|
+
if [ "${#files[@]}" -eq 0 ]; then
|
|
58
|
+
if [ "${#globs[@]}" -eq 0 ]; then
|
|
59
|
+
globs=('*.md')
|
|
60
|
+
fi
|
|
61
|
+
while IFS= read -r -d '' file; do
|
|
62
|
+
if [ -f "$file" ]; then
|
|
63
|
+
files+=("$file")
|
|
64
|
+
fi
|
|
65
|
+
done < <(git ls-files -z -- "${globs[@]}")
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
if [ "${#files[@]}" -eq 0 ]; then
|
|
69
|
+
echo "No tracked Markdown files found"
|
|
70
|
+
exit 0
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
base=(lychee --config "$config")
|
|
74
|
+
if [ "${#options[@]}" -gt 0 ]; then
|
|
75
|
+
base+=("${options[@]+"${options[@]}"}")
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
echo "::group::Internal link check (relative paths + anchors)"
|
|
79
|
+
"${base[@]}" --offline --include-fragments -- "${files[@]}"
|
|
80
|
+
echo "::endgroup::"
|
|
81
|
+
|
|
82
|
+
if [ "$explicit_offline" = "true" ]; then
|
|
83
|
+
exit 0
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
echo "::group::External link check (warn-only)"
|
|
87
|
+
if ! "${base[@]}" --scheme http --scheme https -- "${files[@]}"; then
|
|
88
|
+
echo "::endgroup::"
|
|
89
|
+
echo "::warning title=External link check::External link(s) failed (non-blocking) — see the 'External link check' group above."
|
|
90
|
+
exit 0
|
|
91
|
+
fi
|
|
92
|
+
echo "::endgroup::"
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Materialize PR title, body, files, diff, comments, and linked issue/PR
|
|
3
|
+
# threads so a review agent does not need gh or bash.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
PR_NUMBER="${PR_NUMBER:?PR_NUMBER is required}"
|
|
7
|
+
GITHUB_REPOSITORY="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}"
|
|
8
|
+
GITHUB_WORKSPACE="${GITHUB_WORKSPACE:?GITHUB_WORKSPACE is required}"
|
|
9
|
+
MAX_MATERIALIZED="${MAX_MATERIALIZED:-20}"
|
|
10
|
+
MAX_SEEN="${MAX_SEEN:-40}"
|
|
11
|
+
|
|
12
|
+
case "$MAX_MATERIALIZED" in
|
|
13
|
+
*[!0-9]* | '')
|
|
14
|
+
echo "materialize-pr-context: MAX_MATERIALIZED must be a non-negative integer" >&2
|
|
15
|
+
exit 2
|
|
16
|
+
;;
|
|
17
|
+
esac
|
|
18
|
+
case "$MAX_SEEN" in
|
|
19
|
+
*[!0-9]* | '')
|
|
20
|
+
echo "materialize-pr-context: MAX_SEEN must be a non-negative integer" >&2
|
|
21
|
+
exit 2
|
|
22
|
+
;;
|
|
23
|
+
esac
|
|
24
|
+
|
|
25
|
+
review_context="${REVIEW_CONTEXT_DIR:-${GITHUB_WORKSPACE}/.review-context}"
|
|
26
|
+
rm -rf "$review_context"
|
|
27
|
+
mkdir -p "$review_context/issues"
|
|
28
|
+
|
|
29
|
+
extract_issue_numbers() {
|
|
30
|
+
grep -oE "(https://github.com/${GITHUB_REPOSITORY}/(issues|pull)/[0-9]+|#[0-9]+)" |
|
|
31
|
+
grep -oE '[0-9]+' ||
|
|
32
|
+
true
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
enqueue_from_text() {
|
|
36
|
+
local seen_file="$1"
|
|
37
|
+
local queue_file="$2"
|
|
38
|
+
local materialized_file="$3"
|
|
39
|
+
local number
|
|
40
|
+
while IFS= read -r number; do
|
|
41
|
+
[ -n "$number" ] || continue
|
|
42
|
+
[ "$number" = "$PR_NUMBER" ] && continue
|
|
43
|
+
grep -qxF "$number" "$seen_file" && continue
|
|
44
|
+
if [ "$(wc -l <"$materialized_file" | tr -d ' ')" -ge "$MAX_MATERIALIZED" ]; then
|
|
45
|
+
return 0
|
|
46
|
+
fi
|
|
47
|
+
if [ "$(wc -l <"$seen_file" | tr -d ' ')" -ge "$MAX_SEEN" ]; then
|
|
48
|
+
return 0
|
|
49
|
+
fi
|
|
50
|
+
printf '%s\n' "$number" >>"$seen_file"
|
|
51
|
+
printf '%s\n' "$number" >>"$queue_file"
|
|
52
|
+
done
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" \
|
|
56
|
+
--json title,body,url,author,baseRefName,headRefName,headRefOid \
|
|
57
|
+
>"$review_context/pr.json"
|
|
58
|
+
gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files?per_page=100" |
|
|
59
|
+
jq 'add // []' >"$review_context/files.json"
|
|
60
|
+
if ! gh pr diff "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" >"$review_context/pr.diff"; then
|
|
61
|
+
printf 'PR diff unavailable (GitHub limits patch responses to 300 files). Use files.json.\n' \
|
|
62
|
+
>"$review_context/pr.diff"
|
|
63
|
+
fi
|
|
64
|
+
gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments?per_page=100" |
|
|
65
|
+
jq 'add // []' >"$review_context/pr-comments.json"
|
|
66
|
+
gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/comments?per_page=100" |
|
|
67
|
+
jq 'add // []' >"$review_context/pr-review-comments.json"
|
|
68
|
+
gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews?per_page=100" |
|
|
69
|
+
jq 'add // []' >"$review_context/pr-reviews.json"
|
|
70
|
+
|
|
71
|
+
seen_file="${review_context}/.seen"
|
|
72
|
+
queue_file="${review_context}/.queue"
|
|
73
|
+
materialized_file="${review_context}/.materialized"
|
|
74
|
+
: >"$seen_file"
|
|
75
|
+
: >"$queue_file"
|
|
76
|
+
: >"$materialized_file"
|
|
77
|
+
|
|
78
|
+
{
|
|
79
|
+
jq -r '.body // ""' "$review_context/pr.json"
|
|
80
|
+
jq -r '.[].body // ""' "$review_context/pr-comments.json"
|
|
81
|
+
jq -r '.[].body // ""' "$review_context/pr-review-comments.json"
|
|
82
|
+
jq -r '.[].body // ""' "$review_context/pr-reviews.json"
|
|
83
|
+
} | extract_issue_numbers | enqueue_from_text "$seen_file" "$queue_file" "$materialized_file"
|
|
84
|
+
|
|
85
|
+
index=1
|
|
86
|
+
while true; do
|
|
87
|
+
if [ "$(wc -l <"$materialized_file" | tr -d ' ')" -ge "$MAX_MATERIALIZED" ]; then
|
|
88
|
+
break
|
|
89
|
+
fi
|
|
90
|
+
issue_number="$(sed -n "${index}p" "$queue_file")"
|
|
91
|
+
[ -n "$issue_number" ] || break
|
|
92
|
+
index=$((index + 1))
|
|
93
|
+
if ! gh api "repos/${GITHUB_REPOSITORY}/issues/${issue_number}" \
|
|
94
|
+
--jq '{number,title,body,url,pull_request}' \
|
|
95
|
+
>"$review_context/issues/${issue_number}.json"; then
|
|
96
|
+
rm -f "$review_context/issues/${issue_number}.json"
|
|
97
|
+
continue
|
|
98
|
+
fi
|
|
99
|
+
printf '%s\n' "$issue_number" >>"$materialized_file"
|
|
100
|
+
gh api --paginate --slurp \
|
|
101
|
+
"repos/${GITHUB_REPOSITORY}/issues/${issue_number}/comments?per_page=100" |
|
|
102
|
+
jq 'add // []' >"$review_context/issues/${issue_number}-comments.json" ||
|
|
103
|
+
printf '[]\n' >"$review_context/issues/${issue_number}-comments.json"
|
|
104
|
+
{
|
|
105
|
+
jq -r '.body // ""' "$review_context/issues/${issue_number}.json"
|
|
106
|
+
jq -r '.[].body // ""' "$review_context/issues/${issue_number}-comments.json"
|
|
107
|
+
if jq -e '.pull_request != null' "$review_context/issues/${issue_number}.json" >/dev/null; then
|
|
108
|
+
gh api --paginate --slurp \
|
|
109
|
+
"repos/${GITHUB_REPOSITORY}/pulls/${issue_number}/comments?per_page=100" |
|
|
110
|
+
jq 'add // []' >"$review_context/issues/${issue_number}-review-comments.json" ||
|
|
111
|
+
printf '[]\n' >"$review_context/issues/${issue_number}-review-comments.json"
|
|
112
|
+
gh api --paginate --slurp \
|
|
113
|
+
"repos/${GITHUB_REPOSITORY}/pulls/${issue_number}/reviews?per_page=100" |
|
|
114
|
+
jq 'add // []' >"$review_context/issues/${issue_number}-reviews.json" ||
|
|
115
|
+
printf '[]\n' >"$review_context/issues/${issue_number}-reviews.json"
|
|
116
|
+
jq -r '.[].body // ""' "$review_context/issues/${issue_number}-review-comments.json"
|
|
117
|
+
jq -r '.[].body // ""' "$review_context/issues/${issue_number}-reviews.json"
|
|
118
|
+
fi
|
|
119
|
+
} | extract_issue_numbers | enqueue_from_text "$seen_file" "$queue_file" "$materialized_file"
|
|
120
|
+
done
|
|
121
|
+
|
|
122
|
+
rm -f "$seen_file" "$queue_file" "$materialized_file"
|
|
123
|
+
printf 'Materialized PR #%s context in %s\n' "$PR_NUMBER" "$review_context"
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
if [ "$#" -lt 3 ]; then
|
|
5
|
+
echo "::error::run-with-timeout: timeout runner requires timeout seconds, kill-after seconds, and a command"
|
|
6
|
+
exit 2
|
|
7
|
+
fi
|
|
8
|
+
|
|
9
|
+
timeout_seconds="$1"
|
|
10
|
+
kill_after_seconds="$2"
|
|
11
|
+
shift 2
|
|
12
|
+
|
|
13
|
+
case "$timeout_seconds" in
|
|
14
|
+
*[!0-9]* | '0' | '')
|
|
15
|
+
echo "::error::run-with-timeout: timeout seconds must be a positive integer"
|
|
16
|
+
exit 2
|
|
17
|
+
;;
|
|
18
|
+
esac
|
|
19
|
+
case "$kill_after_seconds" in
|
|
20
|
+
*[!0-9]* | '0' | '')
|
|
21
|
+
echo "::error::run-with-timeout: kill-after seconds must be a positive integer"
|
|
22
|
+
exit 2
|
|
23
|
+
;;
|
|
24
|
+
esac
|
|
25
|
+
|
|
26
|
+
if command -v timeout >/dev/null 2>&1 &&
|
|
27
|
+
timeout --version 2>/dev/null | grep -q 'GNU coreutils'; then
|
|
28
|
+
timeout_pid=""
|
|
29
|
+
pending_signal_number=""
|
|
30
|
+
|
|
31
|
+
cleanup_timeout_group() {
|
|
32
|
+
[ -n "$timeout_pid" ] || return
|
|
33
|
+
kill -KILL -- "-$timeout_pid" 2>/dev/null || true
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Invoked through signal traps.
|
|
37
|
+
# shellcheck disable=SC2329
|
|
38
|
+
handle_timeout_signal() {
|
|
39
|
+
signal_number="$1"
|
|
40
|
+
if [ -z "$timeout_pid" ]; then
|
|
41
|
+
pending_signal_number="$signal_number"
|
|
42
|
+
return
|
|
43
|
+
fi
|
|
44
|
+
kill -TERM "$timeout_pid" 2>/dev/null || true
|
|
45
|
+
wait "$timeout_pid" 2>/dev/null || true
|
|
46
|
+
cleanup_timeout_group
|
|
47
|
+
exit $((128 + signal_number))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
trap 'handle_timeout_signal 2' INT
|
|
51
|
+
trap 'handle_timeout_signal 15' TERM
|
|
52
|
+
trap 'handle_timeout_signal 1' HUP
|
|
53
|
+
|
|
54
|
+
timeout --kill-after="${kill_after_seconds}s" "${timeout_seconds}s" "$@" &
|
|
55
|
+
timeout_pid=$!
|
|
56
|
+
if [ -n "$pending_signal_number" ]; then
|
|
57
|
+
handle_timeout_signal "$pending_signal_number"
|
|
58
|
+
fi
|
|
59
|
+
if wait "$timeout_pid"; then
|
|
60
|
+
status=0
|
|
61
|
+
else
|
|
62
|
+
status=$?
|
|
63
|
+
fi
|
|
64
|
+
cleanup_timeout_group
|
|
65
|
+
exit "$status"
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
if command -v perl >/dev/null 2>&1; then
|
|
69
|
+
exec perl -e '
|
|
70
|
+
my $timeout = shift @ARGV;
|
|
71
|
+
my $kill_after = shift @ARGV;
|
|
72
|
+
sub terminate_group {
|
|
73
|
+
my ($pid) = @_;
|
|
74
|
+
kill(15, -$pid);
|
|
75
|
+
for (1 .. ($kill_after * 10)) {
|
|
76
|
+
waitpid($pid, 1);
|
|
77
|
+
return if kill(0, -$pid) == 0;
|
|
78
|
+
select undef, undef, undef, 0.1;
|
|
79
|
+
}
|
|
80
|
+
kill(9, -$pid);
|
|
81
|
+
waitpid($pid, 0);
|
|
82
|
+
}
|
|
83
|
+
my $pid = fork();
|
|
84
|
+
if (!defined $pid) {
|
|
85
|
+
exit 125;
|
|
86
|
+
}
|
|
87
|
+
if ($pid == 0) {
|
|
88
|
+
setpgrp(0, 0);
|
|
89
|
+
exec @ARGV;
|
|
90
|
+
exit 127;
|
|
91
|
+
}
|
|
92
|
+
my $timed_out = 0;
|
|
93
|
+
local $SIG{INT} = sub {
|
|
94
|
+
terminate_group($pid);
|
|
95
|
+
exit 130;
|
|
96
|
+
};
|
|
97
|
+
local $SIG{TERM} = sub {
|
|
98
|
+
terminate_group($pid);
|
|
99
|
+
exit 143;
|
|
100
|
+
};
|
|
101
|
+
local $SIG{HUP} = sub {
|
|
102
|
+
terminate_group($pid);
|
|
103
|
+
exit 129;
|
|
104
|
+
};
|
|
105
|
+
local $SIG{ALRM} = sub {
|
|
106
|
+
$timed_out = 1;
|
|
107
|
+
terminate_group($pid);
|
|
108
|
+
};
|
|
109
|
+
alarm $timeout;
|
|
110
|
+
waitpid($pid, 0);
|
|
111
|
+
my $status = $?;
|
|
112
|
+
alarm 0;
|
|
113
|
+
exit 124 if $timed_out;
|
|
114
|
+
exit($status & 127 ? 128 + ($status & 127) : $status >> 8);
|
|
115
|
+
' "$timeout_seconds" "$kill_after_seconds" "$@"
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
echo "::error::run-with-timeout: neither GNU timeout nor Perl is available to enforce the deadline"
|
|
119
|
+
exit 125
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
if [ "$(uname -s)" != "Linux" ]; then
|
|
5
|
+
exit 0
|
|
6
|
+
fi
|
|
7
|
+
|
|
8
|
+
timeout_seconds="${APT_LOCK_TIMEOUT_SECONDS:-120}"
|
|
9
|
+
case "$timeout_seconds" in
|
|
10
|
+
*[!0-9]* | '')
|
|
11
|
+
echo "::error::wait-for-apt-locks: APT_LOCK_TIMEOUT_SECONDS must be a non-negative integer"
|
|
12
|
+
exit 2
|
|
13
|
+
;;
|
|
14
|
+
esac
|
|
15
|
+
|
|
16
|
+
lock_probe_cmd=()
|
|
17
|
+
lock_probe_detail_cmd=()
|
|
18
|
+
if command -v fuser >/dev/null 2>&1; then
|
|
19
|
+
lock_probe_cmd=(fuser)
|
|
20
|
+
lock_probe_detail_cmd=(fuser -v)
|
|
21
|
+
elif command -v lsof >/dev/null 2>&1; then
|
|
22
|
+
lock_probe_cmd=(lsof)
|
|
23
|
+
lock_probe_detail_cmd=(lsof -w)
|
|
24
|
+
else
|
|
25
|
+
echo "::warning::fuser and lsof are unavailable; skipping apt/dpkg lock probe"
|
|
26
|
+
exit 0
|
|
27
|
+
fi
|
|
28
|
+
if command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1; then
|
|
29
|
+
lock_probe_cmd=(sudo -n "${lock_probe_cmd[@]}")
|
|
30
|
+
lock_probe_detail_cmd=(sudo -n "${lock_probe_detail_cmd[@]}")
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
deadline=$((SECONDS + timeout_seconds))
|
|
34
|
+
lock_paths=(
|
|
35
|
+
/var/lib/apt/lists/lock
|
|
36
|
+
/var/lib/dpkg/lock
|
|
37
|
+
/var/lib/dpkg/lock-frontend
|
|
38
|
+
/var/cache/apt/archives/lock
|
|
39
|
+
)
|
|
40
|
+
busy_paths=()
|
|
41
|
+
|
|
42
|
+
while true; do
|
|
43
|
+
busy_paths=()
|
|
44
|
+
for lock_path in "${lock_paths[@]}"; do
|
|
45
|
+
if [ -e "$lock_path" ] && "${lock_probe_cmd[@]}" "$lock_path" >/dev/null 2>&1; then
|
|
46
|
+
busy_paths+=("$lock_path")
|
|
47
|
+
fi
|
|
48
|
+
done
|
|
49
|
+
|
|
50
|
+
if [ "${#busy_paths[@]}" -eq 0 ]; then
|
|
51
|
+
exit 0
|
|
52
|
+
fi
|
|
53
|
+
if [ "$SECONDS" -ge "$deadline" ]; then
|
|
54
|
+
echo "::error::Timed out waiting for apt/dpkg locks after ${timeout_seconds}s: ${busy_paths[*]}"
|
|
55
|
+
"${lock_probe_detail_cmd[@]}" "${busy_paths[@]}" || true
|
|
56
|
+
exit 1
|
|
57
|
+
fi
|
|
58
|
+
echo "::warning::Waiting for apt/dpkg locks: ${busy_paths[*]}"
|
|
59
|
+
sleep 5
|
|
60
|
+
done
|