mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-09-28 05:54:50 +08:00
fix: refresh Canvas image packages (#17633)
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
co-authored by
openhands
parent
ffdc65e10c
commit
361126fa97
Executable
+85
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Advance the agent-server Debian snapshot after a seven-day observation period."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import urllib.request
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
MINIMUM_AGE = timedelta(days=7)
|
||||
SNAPSHOT_RE = re.compile(r"(?m)^ARG DEBIAN_SNAPSHOT=(\d{8}T\d{6}Z)$")
|
||||
ARCHIVES = ("debian", "debian-security")
|
||||
|
||||
|
||||
def eligible_snapshot(now: datetime) -> datetime:
|
||||
if now.tzinfo is None:
|
||||
raise ValueError("now must be timezone-aware")
|
||||
cutoff = now.astimezone(UTC) - MINIMUM_AGE
|
||||
return cutoff.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
|
||||
def format_snapshot(value: datetime) -> str:
|
||||
return value.astimezone(UTC).strftime("%Y%m%dT%H%M%SZ")
|
||||
|
||||
|
||||
def validate_snapshot_age(snapshot: datetime, now: datetime) -> None:
|
||||
age = now.astimezone(UTC) - snapshot.astimezone(UTC)
|
||||
if age < MINIMUM_AGE:
|
||||
raise ValueError(f"snapshot is only {age} old; minimum age is {MINIMUM_AGE}")
|
||||
|
||||
|
||||
def verify_snapshot(snapshot: str) -> None:
|
||||
for archive in ARCHIVES:
|
||||
url = f"https://snapshot.debian.org/archive/{archive}/{snapshot}/"
|
||||
request = urllib.request.Request(url, method="HEAD")
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=30) as response:
|
||||
if response.status != 200:
|
||||
raise ValueError(f"{url} returned HTTP {response.status}")
|
||||
except OSError as exc:
|
||||
raise ValueError(f"unable to verify {url}: {exc}") from exc
|
||||
|
||||
|
||||
def update_dockerfile(path: Path, snapshot: str) -> bool:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
matches = SNAPSHOT_RE.findall(text)
|
||||
if len(matches) != 1:
|
||||
raise ValueError(f"expected exactly one DEBIAN_SNAPSHOT in {path}")
|
||||
updated = SNAPSHOT_RE.sub(f"ARG DEBIAN_SNAPSHOT={snapshot}", text)
|
||||
if updated == text:
|
||||
return False
|
||||
path.write_text(updated, encoding="utf-8")
|
||||
return True
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--dockerfile", type=Path, required=True)
|
||||
parser.add_argument(
|
||||
"--now",
|
||||
type=lambda value: datetime.fromisoformat(value.replace("Z", "+00:00")),
|
||||
default=datetime.now(UTC),
|
||||
help="UTC reference time for deterministic testing",
|
||||
)
|
||||
parser.add_argument("--skip-network-check", action="store_true")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
snapshot_time = eligible_snapshot(args.now)
|
||||
validate_snapshot_age(snapshot_time, args.now)
|
||||
snapshot = format_snapshot(snapshot_time)
|
||||
if not args.skip_network_check:
|
||||
verify_snapshot(snapshot)
|
||||
changed = update_dockerfile(args.dockerfile, snapshot)
|
||||
print(f"Debian snapshot: {snapshot} ({'updated' if changed else 'unchanged'})")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
name: Update Canvas Debian Snapshot
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: 17 6 * * 1
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: update-canvas-debian-snapshot
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
update-snapshot:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.OPENHANDS_BOT_GITHUB_PAT_PUBLIC }}
|
||||
BRANCH: chore/update-canvas-debian-snapshot
|
||||
DOCKERFILE: docker/Dockerfile
|
||||
IMAGE: openhands/agent-canvas:snapshot-update
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.OPENHANDS_BOT_GITHUB_PAT_PUBLIC }}
|
||||
- name: Select newest snapshot older than seven days
|
||||
run: python .github/scripts/update_debian_snapshot.py --dockerfile "$DOCKERFILE"
|
||||
- name: Read image build defaults
|
||||
id: defaults
|
||||
run: |
|
||||
echo "agent_server_image=$(node -p \"require('./config/defaults.json').images.agentServer + ':' + require('./config/defaults.json').versions.agentServer + '-python'\")" >> "$GITHUB_OUTPUT"
|
||||
echo "agent_server_version=$(node -p \"require('./config/defaults.json').versions.agentServer\")" >> "$GITHUB_OUTPUT"
|
||||
echo "automation_version=$(node -p \"require('./config/defaults.json').versions.automation\")" >> "$GITHUB_OUTPUT"
|
||||
- name: Build image
|
||||
run: |
|
||||
docker build --tag "$IMAGE" --file "$DOCKERFILE" \
|
||||
--build-arg AGENT_SERVER_IMAGE="${{ steps.defaults.outputs.agent_server_image }}" \
|
||||
--build-arg AGENT_SERVER_VERSION="${{ steps.defaults.outputs.agent_server_version }}" \
|
||||
--build-arg AUTOMATION_VERSION="${{ steps.defaults.outputs.automation_version }}" .
|
||||
- name: Scan image
|
||||
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
|
||||
with:
|
||||
image-ref: ${{ env.IMAGE }}
|
||||
format: json
|
||||
output: trivy.json
|
||||
scanners: vuln
|
||||
- name: Open or refresh update PR
|
||||
env:
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if git diff --quiet; then
|
||||
echo "Debian snapshot is already current."
|
||||
exit 0
|
||||
fi
|
||||
SNAPSHOT=$(sed -n 's/^ARG DEBIAN_SNAPSHOT=//p' "$DOCKERFILE")
|
||||
TRIVY_COUNTS=$(jq -r '[.Results[]?.Vulnerabilities[]?] | group_by(.Severity) | map("\(.[0].Severity): \(length)") | join(", ")' trivy.json)
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git fetch origin "$BRANCH" || true
|
||||
git checkout -B "$BRANCH"
|
||||
git add "$DOCKERFILE"
|
||||
git commit -m "chore(canvas): update Debian snapshot to $SNAPSHOT" \
|
||||
-m "Use the newest UTC snapshot that has completed the seven-day observation period." \
|
||||
-m "Co-authored-by: openhands <openhands@all-hands.dev>"
|
||||
git push --force-with-lease -u origin "$BRANCH"
|
||||
BODY=$(cat <<EOF2
|
||||
## Summary
|
||||
Advance the Canvas runtime to Debian snapshot \\`$SNAPSHOT\\`, the newest UTC snapshot that completed the seven-day observation period.
|
||||
|
||||
The workflow built and scanned the image before opening this PR.
|
||||
|
||||
**Trivy findings:** $TRIVY_COUNTS
|
||||
|
||||
_This pull request was created by an automated workflow._
|
||||
EOF2
|
||||
)
|
||||
EXISTING=$(gh pr list --repo "$REPO" --head "$BRANCH" --state open --json number --jq '.[0].number')
|
||||
if [ -n "$EXISTING" ]; then
|
||||
gh pr edit "$EXISTING" --repo "$REPO" --title "chore(canvas): update Debian snapshot to $SNAPSHOT" --body "$BODY"
|
||||
else
|
||||
gh pr create --repo "$REPO" --base main --head "$BRANCH" --title "chore(canvas): update Debian snapshot to $SNAPSHOT" --body "$BODY"
|
||||
fi
|
||||
@@ -97,9 +97,28 @@ USER root
|
||||
|
||||
# Install system deps required by automation's transitive dependencies
|
||||
# (asyncpg needs libpq, which the agent-server base image may not include).
|
||||
# Refresh inherited packages after adding dependencies so the final image
|
||||
# includes available security fixes.
|
||||
ARG DEBIAN_SNAPSHOT=20260913T000000Z
|
||||
RUN if command -v apt-get >/dev/null 2>&1; then \
|
||||
printf '%s\n' \
|
||||
'Types: deb' \
|
||||
"URIs: http://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}/" \
|
||||
'Suites: trixie' \
|
||||
'Components: main' \
|
||||
'Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg' \
|
||||
'Check-Valid-Until: no' \
|
||||
'' \
|
||||
'Types: deb' \
|
||||
"URIs: http://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}/" \
|
||||
'Suites: trixie-security' \
|
||||
'Components: main' \
|
||||
'Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg' \
|
||||
'Check-Valid-Until: no' \
|
||||
> /etc/apt/sources.list.d/debian.sources && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends libpq-dev && \
|
||||
apt-get upgrade -y --no-install-recommends && \
|
||||
rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user