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.
Files changed (34) hide show
  1. package/README.md +12 -0
  2. package/dist/cli/commands/nuget-central-version.d.mts +1 -0
  3. package/dist/cli/commands/nuget-central-version.mjs +11 -0
  4. package/dist/cli/commands/swift-semantic-equal.d.mts +1 -0
  5. package/dist/cli/commands/swift-semantic-equal.mjs +4 -0
  6. package/dist/cli/index.mjs +15 -0
  7. package/dist/cli/parse.d.mts +7 -1
  8. package/dist/cli/parse.mjs +10 -0
  9. package/dist/cli/usage.d.mts +1 -1
  10. package/dist/cli/usage.mjs +17 -1
  11. package/dist/index.d.mts +5 -0
  12. package/dist/index.mjs +4 -0
  13. package/dist/nuget-central-version/cli.d.mts +1 -0
  14. package/dist/nuget-central-version/cli.mjs +11 -0
  15. package/dist/nuget-central-version/index.d.mts +1 -0
  16. package/dist/nuget-central-version/index.mjs +74 -0
  17. package/dist/swift-resolved-pin-delta/index.d.mts +18 -0
  18. package/dist/swift-resolved-pin-delta/index.mjs +63 -0
  19. package/dist/swift-semantic-equal/cli.d.mts +2 -0
  20. package/dist/swift-semantic-equal/cli.mjs +26 -0
  21. package/dist/swift-semantic-equal/index.d.mts +1 -0
  22. package/dist/swift-semantic-equal/index.mjs +1 -0
  23. package/dist/swift-semantic-equal/normalize.d.mts +1 -0
  24. package/dist/swift-semantic-equal/normalize.mjs +108 -0
  25. package/dist/swift-source-offset/index.d.mts +2 -0
  26. package/dist/swift-source-offset/index.mjs +68 -0
  27. package/package.json +21 -1
  28. package/scripts/gha/ghcr-package-retention.sh +65 -0
  29. package/scripts/gha/install-github-release.sh +57 -13
  30. package/scripts/gha/install-playwright-chromium-arm64.sh +84 -0
  31. package/scripts/gha/lint-links.sh +92 -0
  32. package/scripts/gha/materialize-pr-context.sh +123 -0
  33. package/scripts/gha/run-with-timeout.sh +119 -0
  34. 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
- output="$("$BIN_DIR/$BIN_NAME" --version 2>/dev/null || true)"
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
- archive="${RUNNER_TEMP:-/tmp}/${BIN_NAME}-${VERSION}-${RELEASE_PLATFORM}.tar.gz"
120
- extract_dir="${RUNNER_TEMP:-/tmp}/${BIN_NAME}-${VERSION}-${RELEASE_PLATFORM}"
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
- curl -fsSL -o "${archive}.sha256" "${base_url}/${asset_name}.sha256"
126
- expected=$(awk '{print $1}' "${archive}.sha256")
127
- if command -v sha256sum >/dev/null 2>&1; then
128
- echo "${expected} ${archive}" | sha256sum -c -
129
- else
130
- echo "${expected} ${archive}" | shasum -a 256 -c -
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
- rm "${archive}.sha256"
133
- tar -xzf "$archive" -C "$extract_dir" --strip-components="$STRIP_COMPONENTS"
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