fix: refresh Canvas image packages (#17633)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Graham Neubig
2026-09-22 16:36:56 -04:00
committed by GitHub
co-authored by openhands
parent ffdc65e10c
commit 361126fa97
3 changed files with 192 additions and 0 deletions
+85
View File
@@ -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
+19
View File
@@ -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