mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-10 04:36:40 +00:00
Merge remote-tracking branch 'upstream/main' into feature/self-host-runners
This commit is contained in:
26 files changed
+306
-79
No files matched your search
@@ -14,6 +14,7 @@ source "${REPO_ROOT}/scripts/meta/env/load.sh"
|
||||
package_prefix="${PACKAGE_PREFIX:-core}"
|
||||
max_age_days="${MAX_AGE_DAYS:-30}"
|
||||
tag_prefix="${TAG_PREFIX:-ci-}"
|
||||
dry_run="${DRY_RUN:-false}"
|
||||
|
||||
if [[ ! "${max_age_days}" =~ ^[0-9]+$ ]]; then
|
||||
echo "ERROR: MAX_AGE_DAYS must be a non-negative integer." >&2
|
||||
@@ -25,15 +26,12 @@ if [[ -z "${tag_prefix}" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v gh >/dev/null 2>&1; then
|
||||
echo "ERROR: gh CLI not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
echo "ERROR: jq not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
for tool in gh jq; do
|
||||
if ! command -v "${tool}" >/dev/null 2>&1; then
|
||||
echo "ERROR: ${tool} not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
encode_package_name() {
|
||||
local package="${1}"
|
||||
@@ -53,24 +51,43 @@ resolve_packages() {
|
||||
done
|
||||
}
|
||||
|
||||
is_deletable_ci_version() {
|
||||
local version="${1}"
|
||||
local tags
|
||||
classify_version() {
|
||||
local version="${1}" cutoff_ts="${2}"
|
||||
local tags updated_ts
|
||||
|
||||
tags="$(jq -r '.tags[]?' <<<"${version}")"
|
||||
[[ -n "${tags}" ]] || {
|
||||
echo "keep:untagged"
|
||||
return 0
|
||||
}
|
||||
grep -Eq "^(latest|v[0-9].*)$" <<<"${tags}" && {
|
||||
echo "keep:release-tag"
|
||||
return 0
|
||||
}
|
||||
grep -q "^${tag_prefix}" <<<"${tags}" || {
|
||||
echo "keep:no-ci-tag"
|
||||
return 0
|
||||
}
|
||||
|
||||
[[ -n "${tags}" ]] || return 1
|
||||
updated_ts="$(date -u -d "$(jq -r '.updated_at' <<<"${version}")" +%s)"
|
||||
[[ "${updated_ts}" -lt "${cutoff_ts}" ]] || {
|
||||
echo "keep:too-new"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Keep release tags even when the version also has ci-* tags.
|
||||
grep -q "^${tag_prefix}" <<<"${tags}" || return 1
|
||||
grep -Eq "^(latest|v.*)$" <<<"${tags}" && return 1
|
||||
grep -vq "^${tag_prefix}" <<<"${tags}" && return 1
|
||||
|
||||
return 0
|
||||
echo "delete"
|
||||
}
|
||||
|
||||
cutoff="$(date -u -d "${max_age_days} days ago" +%s)"
|
||||
|
||||
echo "GHCR CI image cleanup"
|
||||
echo " org=${ORG} tag-prefix=${tag_prefix} max-age=${max_age_days}d dry-run=${dry_run}"
|
||||
echo " cutoff=$(date -u -d "@${cutoff}" +%Y-%m-%dT%H:%M:%SZ) (delete ${tag_prefix}* versions without a semver/latest tag, older than this)"
|
||||
|
||||
total_scanned=0
|
||||
total_deleted=0
|
||||
declare -A kept_reasons=()
|
||||
|
||||
while IFS= read -r package; do
|
||||
package="${package#"${package%%[![:space:]]*}"}"
|
||||
package="${package%"${package##*[![:space:]]}"}"
|
||||
@@ -78,24 +95,51 @@ while IFS= read -r package; do
|
||||
|
||||
encoded_package="$(encode_package_name "${package}")"
|
||||
|
||||
gh api --paginate \
|
||||
"/orgs/${ORG}/packages/container/${encoded_package}/versions" \
|
||||
--jq '.[] | {id: .id, updated_at: .updated_at, tags: .metadata.container.tags}' |
|
||||
jq -c '.' |
|
||||
while IFS= read -r version; do
|
||||
id="$(jq -r '.id' <<<"${version}")"
|
||||
updated_at="$(jq -r '.updated_at' <<<"${version}")"
|
||||
updated_ts="$(date -u -d "${updated_at}" +%s)"
|
||||
pkg_scanned=0
|
||||
pkg_deleted=0
|
||||
|
||||
if [[ "${updated_ts}" -ge "${cutoff}" ]]; then
|
||||
continue
|
||||
fi
|
||||
echo "== ${package} =="
|
||||
|
||||
is_deletable_ci_version "${version}" || continue
|
||||
while IFS= read -r version; do
|
||||
[[ -n "${version}" ]] || continue
|
||||
pkg_scanned=$((pkg_scanned + 1))
|
||||
|
||||
echo "Deleting ${package} version ${id} updated at ${updated_at}"
|
||||
decision="$(classify_version "${version}" "${cutoff}")"
|
||||
|
||||
if [[ "${decision}" != "delete" ]]; then
|
||||
kept_reasons["${decision}"]=$((${kept_reasons["${decision}"]:-0} + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
id="$(jq -r '.id' <<<"${version}")"
|
||||
updated_at="$(jq -r '.updated_at' <<<"${version}")"
|
||||
tags_csv="$(jq -r '[.tags[]?] | join(",")' <<<"${version}")"
|
||||
|
||||
if [[ "${dry_run}" == "true" ]]; then
|
||||
echo " would delete id=${id} tags=${tags_csv} updated=${updated_at}"
|
||||
else
|
||||
echo " deleting id=${id} tags=${tags_csv} updated=${updated_at}"
|
||||
gh api \
|
||||
--method DELETE \
|
||||
"/orgs/${ORG}/packages/container/${encoded_package}/versions/${id}"
|
||||
done
|
||||
fi
|
||||
pkg_deleted=$((pkg_deleted + 1))
|
||||
done < <(gh api --paginate \
|
||||
"/orgs/${ORG}/packages/container/${encoded_package}/versions" \
|
||||
--jq '.[] | {id: .id, updated_at: .updated_at, tags: .metadata.container.tags}' |
|
||||
jq -c '.')
|
||||
|
||||
echo " -> scanned ${pkg_scanned}, deleted ${pkg_deleted}"
|
||||
total_scanned=$((total_scanned + pkg_scanned))
|
||||
total_deleted=$((total_deleted + pkg_deleted))
|
||||
done < <(resolve_packages)
|
||||
|
||||
echo "== Summary =="
|
||||
echo " scanned ${total_scanned} versions, deleted ${total_deleted}"
|
||||
for reason in "${!kept_reasons[@]}"; do
|
||||
echo " kept ${kept_reasons[${reason}]} (${reason})"
|
||||
done
|
||||
|
||||
if [[ "${total_deleted}" -eq 0 ]]; then
|
||||
echo " no versions matched the deletion criteria"
|
||||
fi
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
image="${1:?Usage: pull_with_retry.sh <image>}"
|
||||
max_attempts="${MAX_ATTEMPTS:-7}"
|
||||
retry_delay_seconds="${RETRY_DELAY_SECONDS:-20}"
|
||||
attempt=1
|
||||
|
||||
while true; do
|
||||
echo "=== docker pull ${image} attempt ${attempt}/${max_attempts} ==="
|
||||
if docker pull "${image}"; then
|
||||
echo "Pulled ${image} on attempt ${attempt}/${max_attempts}."
|
||||
break
|
||||
fi
|
||||
|
||||
if [[ "${attempt}" -ge "${max_attempts}" ]]; then
|
||||
echo "Failed to pull ${image} after ${max_attempts} attempts." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Attempt ${attempt} failed. Retrying in ${retry_delay_seconds}s..."
|
||||
sleep "${retry_delay_seconds}"
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
@@ -9,6 +9,7 @@ set -euo pipefail
|
||||
|
||||
UPDATE_BASE_BRANCH="${UPDATE_BASE_BRANCH:-master}"
|
||||
UPDATE_BRANCH_SUFFIX="${UPDATE_BRANCH_SUFFIX:-$(date +%Y%m%d)}"
|
||||
UPDATE_DEDUPE_PREFIX="${UPDATE_DEDUPE_PREFIX:-${UPDATE_BRANCH_PREFIX}-}"
|
||||
|
||||
if ! command -v gh >/dev/null 2>&1; then
|
||||
echo "ERROR: gh CLI not found." >&2
|
||||
@@ -70,7 +71,7 @@ mapfile -t OPEN_PRS < <(
|
||||
--base "${UPDATE_BASE_BRANCH}" \
|
||||
--limit 100 \
|
||||
--json number,headRefName \
|
||||
--jq ".[] | select(.headRefName | startswith(\"${UPDATE_BRANCH_PREFIX}-\")) | \"\(.number)\t\(.headRefName)\""
|
||||
--jq ".[] | select(.headRefName | startswith(\"${UPDATE_DEDUPE_PREFIX}\")) | \"\(.number)\t\(.headRefName)\""
|
||||
)
|
||||
|
||||
DUPLICATE_PR=""
|
||||
|
||||
Executable
+60
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Opens one update PR per top-level entry that changed under the given root
|
||||
# (e.g. one PR per role under roles/), instead of a single combined PR.
|
||||
# The actual dedup/push/PR logic is reused from open_pr.sh; this wrapper only
|
||||
# isolates one entry's changes per call.
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
ROOT="${1:?Usage: open_role_prs.sh <root-dir> (e.g. roles)}"
|
||||
|
||||
: "${GH_TOKEN:?Missing GH_TOKEN}"
|
||||
: "${UPDATE_BRANCH_PREFIX:?Missing UPDATE_BRANCH_PREFIX}"
|
||||
: "${UPDATE_COMMIT_MESSAGE:?Missing UPDATE_COMMIT_MESSAGE}"
|
||||
: "${UPDATE_PR_TITLE:?Missing UPDATE_PR_TITLE}"
|
||||
: "${UPDATE_PR_BODY:?Missing UPDATE_PR_BODY}"
|
||||
|
||||
date_suffix="$(date +%Y%m%d)"
|
||||
|
||||
# A throwaway identity is enough for the scratch commit below; open_pr.sh sets
|
||||
# the real bot identity for each published commit.
|
||||
git config user.name >/dev/null 2>&1 || git config user.name "github-actions[bot]"
|
||||
git config user.email >/dev/null 2>&1 || git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
base="$(git rev-parse HEAD)"
|
||||
|
||||
git add -A -- "${ROOT}"
|
||||
if git diff --cached --quiet; then
|
||||
echo "No ${ROOT} changes to split into per-entry PRs."
|
||||
exit 0
|
||||
fi
|
||||
git commit --quiet -m "scratch: ${ROOT} updates"
|
||||
scratch="$(git rev-parse HEAD)"
|
||||
git reset --quiet --hard "${base}"
|
||||
|
||||
mapfile -t entries < <(
|
||||
git diff --name-only "${base}" "${scratch}" -- "${ROOT}/" |
|
||||
sed -E "s#^${ROOT}/([^/]+)/.*#\1#;t;d" |
|
||||
sort -u
|
||||
)
|
||||
|
||||
echo "Changed ${ROOT} entries: ${entries[*]:-(none)}"
|
||||
|
||||
for entry in "${entries[@]}"; do
|
||||
[[ -z "${entry}" ]] && continue
|
||||
echo "=== Opening PR for ${ROOT}/${entry} ==="
|
||||
|
||||
git checkout --quiet --force --detach "${base}"
|
||||
git checkout --quiet "${scratch}" -- "${ROOT}/${entry}/"
|
||||
|
||||
UPDATE_BRANCH_SUFFIX="${entry}-${date_suffix}" \
|
||||
UPDATE_DEDUPE_PREFIX="${UPDATE_BRANCH_PREFIX}-${entry}-" \
|
||||
UPDATE_COMMIT_MESSAGE="${UPDATE_COMMIT_MESSAGE} (${entry})" \
|
||||
UPDATE_PR_TITLE="${UPDATE_PR_TITLE} (${entry})" \
|
||||
UPDATE_PR_BODY="${UPDATE_PR_BODY}"$'\n\n'"Scope: \`${ROOT}/${entry}\`." \
|
||||
bash "${SCRIPT_DIR}/open_pr.sh" "${ROOT}/${entry}"
|
||||
done
|
||||
|
||||
git checkout --quiet --force --detach "${base}" || true
|
||||
Reference in new issue
Block a user