Add CI and deploy summaries
ci / Compose (push) Skipped
ci / Workflows (push) Skipped
ci / Shell (push) Skipped
ci / Formatting (push) Skipped
ci / Python and tests (push) Skipped
ci / YAML (push) Skipped
ci / Kubernetes (push) Skipped
ci / Dockerfiles (push) Skipped
ci / Compose (pull_request) Successful in 14s
ci / Workflows (pull_request) Successful in 7s
ci / Shell (pull_request) Successful in 23s
ci / Formatting (pull_request) Successful in 26s
ci / Python and tests (pull_request) Successful in 8s
ci / YAML (pull_request) Successful in 14s
ci / Dockerfiles (pull_request) Successful in 7s
ci / Kubernetes (pull_request) Successful in 10s
ci / build (pull_request) Skipped
ci / Compose (push) Skipped
ci / Workflows (push) Skipped
ci / Shell (push) Skipped
ci / Formatting (push) Skipped
ci / Python and tests (push) Skipped
ci / YAML (push) Skipped
ci / Kubernetes (push) Skipped
ci / Dockerfiles (push) Skipped
ci / Compose (pull_request) Successful in 14s
ci / Workflows (pull_request) Successful in 7s
ci / Shell (pull_request) Successful in 23s
ci / Formatting (pull_request) Successful in 26s
ci / Python and tests (pull_request) Successful in 8s
ci / YAML (pull_request) Successful in 14s
ci / Dockerfiles (pull_request) Successful in 7s
ci / Kubernetes (pull_request) Successful in 10s
ci / build (pull_request) Skipped
This commit is contained in:
1 parent
b677d553b4
commit
d74822cd27
4 files changed
+89
-3
No files matched your search
@@ -3,7 +3,9 @@
|
||||
The native Gitea runner runs on **vps**; production runs on **workstation**.
|
||||
Compose, workflow, shell, Python, formatting, YAML, Dockerfile and Kubernetes
|
||||
checks appear as separate jobs. Jobs run on `homelab:host`, one at a time; the
|
||||
build waits for every check to pass. No job images or Kubernetes credentials
|
||||
build waits for every check to pass. CI and deploy runs also show a summary with
|
||||
the release SHA, image build or reuse results, deploy mode, selected services,
|
||||
and image digests. No job images or Kubernetes credentials
|
||||
are needed on the VPS. Builds use one pinned BuildKit helper container. CI and deploy are separate workflows.
|
||||
|
||||
## Runner installation
|
||||
|
||||
@@ -294,10 +294,47 @@ def follow(run_id, phase):
|
||||
time.sleep(3)
|
||||
|
||||
|
||||
def summary(run_id):
|
||||
directory = run_directory(run_id)
|
||||
request = json.loads((directory / 'request.json').read_text())
|
||||
release = request['release']
|
||||
plan_file = directory / 'plan.json'
|
||||
lines = [
|
||||
f'## Deploy `{release["sha"]}`',
|
||||
'',
|
||||
f'- Mode: `{request["mode"]}`',
|
||||
f'- Refresh third-party images: `{request["refresh_images"]}`',
|
||||
]
|
||||
if not plan_file.exists():
|
||||
lines.extend(['', 'Plan was not created. Check the controller log.'])
|
||||
print('\n'.join(lines))
|
||||
return
|
||||
plan = json.loads(plan_file.read_text())
|
||||
lines.extend(['', '### Selected services'])
|
||||
count = 0
|
||||
for kind, services in plan['selected'].items():
|
||||
for service in services:
|
||||
lines.append(f'- `{kind}`: `{service}`')
|
||||
count += 1
|
||||
if not count:
|
||||
lines.append('- None')
|
||||
lines.extend(['', '### Selected Helm releases'])
|
||||
lines.extend(f'- `{release}`' for release in plan.get('helm', []))
|
||||
if not plan.get('helm'):
|
||||
lines.append('- None')
|
||||
lines.extend(['', '### Images pinned in the checked release'])
|
||||
lines.extend(f'- `{image}@{digest}`' for image, digest in sorted(release['images'].items()))
|
||||
lines.extend(['', '### Removed resources requiring manual review'])
|
||||
lines.extend(f'- `{item}`' for item in plan.get('removed', []))
|
||||
if not plan.get('removed'):
|
||||
lines.append('- None')
|
||||
print('\n'.join(lines))
|
||||
|
||||
|
||||
def main():
|
||||
os.umask(0o077)
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('action', choices=('start', 'execute', 'recover', 'status', 'follow'))
|
||||
parser.add_argument('action', choices=('start', 'execute', 'recover', 'status', 'follow', 'summary'))
|
||||
parser.add_argument('run_id')
|
||||
parser.add_argument('phase', nargs='?', choices=('apply', 'verify', 'smoke'))
|
||||
parser.add_argument('--retry', action='store_true', help='Retry failed recovery checks; never repeat apply')
|
||||
@@ -315,6 +352,8 @@ def main():
|
||||
if (directory / 'plan.json').exists():
|
||||
plan = json.loads((directory / 'plan.json').read_text())
|
||||
print(json.dumps({k: plan[k] for k in ('sha', 'selected', 'helm', 'removed')}, indent=2))
|
||||
elif args.action == 'summary':
|
||||
summary(args.run_id)
|
||||
elif not follow(args.run_id, args.phase):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -231,6 +231,8 @@ def build(output):
|
||||
)
|
||||
signature.write_text(image + '\n')
|
||||
release = {'version': 1, 'sha': sha, 'images': {}, 'inputs': {}}
|
||||
built = []
|
||||
reused = []
|
||||
for name, (context, dockerfile) in IMAGES.items():
|
||||
image = f'gcr.forust.xyz/forust/{name}'
|
||||
inputs = fingerprint(context, dockerfile)
|
||||
@@ -255,8 +257,10 @@ def build(output):
|
||||
if exists:
|
||||
print(f'Reuse {name}: inputs unchanged')
|
||||
digest = old_digest
|
||||
reused.append((name, image, digest))
|
||||
else:
|
||||
print(f'Build {name}', flush=True)
|
||||
built.append((name, image))
|
||||
metadata = Path(docker_config) / 'metadata.json'
|
||||
command(
|
||||
'docker',
|
||||
@@ -286,6 +290,23 @@ def build(output):
|
||||
release['inputs'][image] = inputs
|
||||
validate_release(release, sha)
|
||||
output.write_text(json.dumps(release, indent=2) + '\n')
|
||||
summary = os.environ.get('GITHUB_STEP_SUMMARY')
|
||||
if summary:
|
||||
lines = [f'## Image release for `{sha}`', '', '### Built']
|
||||
lines.extend(f'- `{name}` — `{image}`' for name, image in built)
|
||||
if not built:
|
||||
lines.append('- None')
|
||||
lines.extend(['', '### Reused from successful CI'])
|
||||
lines.extend(f'- `{name}` — `{image}@{digest}`' for name, image, digest in reused)
|
||||
if not reused:
|
||||
lines.append('- None')
|
||||
lines.extend(['', '### Release digests'])
|
||||
lines.extend(
|
||||
f'- `{name}` — `{image}@{release["images"][image]}`'
|
||||
for name in IMAGES
|
||||
for image in [f'gcr.forust.xyz/forust/{name}']
|
||||
)
|
||||
Path(summary).write_text('\n'.join(lines) + '\n')
|
||||
finally:
|
||||
# Cleanup errors must neither leak credentials nor mask the original build error.
|
||||
try:
|
||||
|
||||
@@ -40,7 +40,31 @@ PY
|
||||
done
|
||||
exit "$rc"
|
||||
;;
|
||||
apply|verify|smoke)
|
||||
apply)
|
||||
result=0
|
||||
for attempt in 1 2 3; do
|
||||
rc=0
|
||||
# shellcheck disable=SC2029 # The run ID and operation are validated local arguments, not remote variables.
|
||||
ssh "${ssh_opts[@]}" "$DEPLOY_USER@$DEPLOY_HOST" python3 "$controller" follow "$DEPLOY_RUN_ID" apply || rc=$?
|
||||
[ "$rc" -eq 0 ] && break
|
||||
[ "$rc" -eq 255 ] || { result="$rc"; break; }
|
||||
echo "SSH disconnected; reconnecting to the existing deploy ($attempt/3)"
|
||||
if [ "$attempt" -eq 3 ]; then result=255; break; fi
|
||||
sleep 5
|
||||
done
|
||||
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
|
||||
rc=0
|
||||
# shellcheck disable=SC2029 # The run ID is validated above.
|
||||
ssh "${ssh_opts[@]}" "$DEPLOY_USER@$DEPLOY_HOST" python3 "$controller" summary "$DEPLOY_RUN_ID" >"$key_dir/deploy-summary.md" || rc=$?
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
cat "$key_dir/deploy-summary.md" >>"$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo 'Deploy summary is unavailable. Check the controller log.' >>"$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
fi
|
||||
exit "$result"
|
||||
;;
|
||||
verify|smoke)
|
||||
for attempt in 1 2 3; do
|
||||
rc=0
|
||||
# shellcheck disable=SC2029 # The run ID and operation are validated local arguments, not remote variables.
|
||||
|
||||
Reference in new issue
Block a user