Narrow Gitea Actions deploy guardrail
Homelab Main / validate-and-deploy (push) Failing after 5s
Details
Homelab Main / validate-and-deploy (push) Failing after 5s
Details
This commit is contained in:
parent
c470e64070
commit
e59e3258fc
|
|
@ -147,7 +147,7 @@ jobs:
|
||||||
tofu -chdir="${stack}" validate
|
tofu -chdir="${stack}" validate
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Block automatic deploy for high-impact changes
|
- name: Block automatic deploy for Raspberry Pi Gitea changes
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
|
@ -156,14 +156,92 @@ jobs:
|
||||||
)"
|
)"
|
||||||
|
|
||||||
if [[ -n "${event_before}" && ! "${event_before}" =~ ^0+$ ]]; then
|
if [[ -n "${event_before}" && ! "${event_before}" =~ ^0+$ ]]; then
|
||||||
changed_files="$(git diff --name-only "${event_before}" HEAD)"
|
base_ref="${event_before}"
|
||||||
else
|
else
|
||||||
changed_files="$(git diff-tree --no-commit-id --name-only -r HEAD)"
|
base_ref="$(git rev-parse HEAD^ 2>/dev/null || git hash-object -t tree /dev/null)"
|
||||||
fi
|
fi
|
||||||
|
changed_files="$(git diff --name-only "${base_ref}" HEAD)"
|
||||||
printf '%s\n' "${changed_files}"
|
printf '%s\n' "${changed_files}"
|
||||||
|
|
||||||
if printf '%s\n' "${changed_files}" | grep -Eq '^(bootstrap/(provisioning|cluster|platform|edge)/|infra/gitea/|lab[.]sh|[.]gitea/workflows/)'; then
|
blocked_files="$(printf '%s\n' "${changed_files}" | grep -E '^infra/gitea/' || true)"
|
||||||
echo "High-impact bootstrap, runner, or workflow changes require a manual Debian run."
|
if printf '%s\n' "${changed_files}" | grep -qx 'lab.sh' &&
|
||||||
|
python3 - "${base_ref}" <<'PY'
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
base_ref = sys.argv[1]
|
||||||
|
path = "lab.sh"
|
||||||
|
target_functions = {
|
||||||
|
"deploy_gitea",
|
||||||
|
"install_gitea_backup_timer",
|
||||||
|
"backup_gitea",
|
||||||
|
"drill_gitea_restore",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def function_ranges(content):
|
||||||
|
starts = []
|
||||||
|
for index, line in enumerate(content.splitlines(), 1):
|
||||||
|
match = re.match(r"^([A-Za-z_][A-Za-z0-9_]*)\(\) \{", line)
|
||||||
|
if match:
|
||||||
|
starts.append((index, match.group(1)))
|
||||||
|
|
||||||
|
ranges = []
|
||||||
|
for offset, (start, name) in enumerate(starts):
|
||||||
|
end = starts[offset + 1][0] - 1 if offset + 1 < len(starts) else len(content.splitlines())
|
||||||
|
if name in target_functions:
|
||||||
|
ranges.append((start, end))
|
||||||
|
return ranges
|
||||||
|
|
||||||
|
|
||||||
|
current_ranges = function_ranges(open(path, encoding="utf-8").read())
|
||||||
|
try:
|
||||||
|
base_content = subprocess.check_output(
|
||||||
|
["git", "show", f"{base_ref}:{path}"],
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
base_ranges = []
|
||||||
|
else:
|
||||||
|
base_ranges = function_ranges(base_content)
|
||||||
|
|
||||||
|
diff = subprocess.check_output(
|
||||||
|
["git", "diff", "--unified=0", base_ref, "HEAD", "--", path],
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def overlaps(start, length, ranges):
|
||||||
|
if length == 0:
|
||||||
|
end = start
|
||||||
|
else:
|
||||||
|
end = start + length - 1
|
||||||
|
return any(start <= range_end and end >= range_start for range_start, range_end in ranges)
|
||||||
|
|
||||||
|
|
||||||
|
for line in diff.splitlines():
|
||||||
|
match = re.match(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@", line)
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
old_start = int(match.group(1))
|
||||||
|
old_length = int(match.group(2) or "1")
|
||||||
|
new_start = int(match.group(3))
|
||||||
|
new_length = int(match.group(4) or "1")
|
||||||
|
if overlaps(old_start, old_length, base_ranges) or overlaps(new_start, new_length, current_ranges):
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
sys.exit(1)
|
||||||
|
PY
|
||||||
|
then
|
||||||
|
blocked_files="${blocked_files}"$'\n'"lab.sh"
|
||||||
|
fi
|
||||||
|
blocked_files="$(printf '%s\n' "${blocked_files}" | sed '/^$/d' | sort -u)"
|
||||||
|
|
||||||
|
if [[ -n "${blocked_files}" ]]; then
|
||||||
|
printf '%s\n' "${blocked_files}"
|
||||||
|
echo "Raspberry Pi Gitea service changes require a manual Debian run."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
||||||
11
README.md
11
README.md
|
|
@ -407,12 +407,11 @@ This repo includes a Gitea Actions workflow at
|
||||||
a repository-scoped Debian host runner with the label `homelab-debian`.
|
a repository-scoped Debian host runner with the label `homelab-debian`.
|
||||||
|
|
||||||
The workflow validates shell syntax, Kubernetes manifests, and all OpenTofu
|
The workflow validates shell syntax, Kubernetes manifests, and all OpenTofu
|
||||||
stacks before deployment. It automatically stops when high-impact files under
|
stacks before deployment. Automatic deploy is blocked only for Raspberry Pi
|
||||||
`bootstrap/provisioning`, `bootstrap/cluster`, `bootstrap/platform`,
|
Gitea service changes: files under `infra/gitea/`, or edits inside the
|
||||||
`bootstrap/edge`, `infra/gitea`, `lab.sh`, or `.gitea/workflows` change; those
|
`deploy_gitea`, `install_gitea_backup_timer`, `backup_gitea`, or
|
||||||
changes still require a manual Debian run. Lower-risk app changes proceed to
|
`drill_gitea_restore` functions in `lab.sh`. Other changes proceed to
|
||||||
`./lab.sh apps` after validation passes, which skips Gitea, Pimox, cluster,
|
`./lab.sh apps` after validation passes.
|
||||||
platform, and edge changes.
|
|
||||||
|
|
||||||
`./lab.sh bootstrap-gitea-repo` also registers the Debian host SSH public key
|
`./lab.sh bootstrap-gitea-repo` also registers the Debian host SSH public key
|
||||||
with the Gitea repository and switches the Debian working copy's `gitea` remote
|
with the Gitea repository and switches the Debian working copy's `gitea` remote
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue