Dev #72

Merged
Latte merged 71 commits from dev into main 2026-10-14 17:52:23 +00:00
3 changed files with 67 additions and 21 deletions
Showing only changes of commit 4e108771f8 - Show all commits

No files matched your search

+25 -11
View File
@@ -7,7 +7,9 @@ name: ci
# test-unit pytest (coverage gate from pyproject.toml: 90 percent)
# test-integration pytest against a throwaway Gitea in Docker
# test-mcp MCP smoke tests, including against the freshly built image
# build-and-push build and scan the image; push it on push events (needs lint + test-unit)
# build-and-push build and scan the image; push it on push events (needs lint, test-unit,
# test-integration, test-mcp and security, so :dev and :latest are never
# pushed while any of them is red)
# docs-check docs drift check
# security security test suite, pip-audit, ruff bandit rules
#
@@ -21,7 +23,10 @@ name: ci
#
# Docker hygiene on the shared runner: every resource this workflow creates has
# a per-run name (aegis-ci-<run id>-...) and is removed by an `if: always()`
# step that deletes exactly those names. Nothing here ever prunes.
# step that deletes exactly those names. Registry tags (:sha-<12>, :dev, :latest, :main)
# are shared with other users of the daemon: they are applied only immediately before the
# push and only untagged afterwards, never used to remove containers. Nothing here ever
# prunes.
on:
push:
@@ -153,6 +158,8 @@ jobs:
env:
AEGIS_INTEGRATION: "1"
AEGIS_IT_NETWORK_MODE: container
# A runner that cannot start must fail the job, not xfail the Actions scenario.
AEGIS_IT_REQUIRE_RUNNER: "1"
run: uv run pytest tests/integration -m integration --no-cov
- name: Cleanup
if: always()
@@ -204,8 +211,8 @@ jobs:
run: bash scripts/ci/cleanup_job_resources.sh "${MCP_IMAGE}"
# ---------------------------------------------------------------------------
# 6. Build the image from the checked-out commit. On push events it is also
# pushed:
# 6. Build the image from the checked-out commit under a per-run local tag
# (aegis-ci-<run id>-build:<sha>). On push events it is also tagged and pushed:
# :sha-<12> every branch (immutable, traceable to the commit; lets any
# feature branch image be pulled and tested)
# :dev additionally on dev
@@ -214,7 +221,7 @@ jobs:
# outputs and the job summary.
# ---------------------------------------------------------------------------
build-and-push:
needs: [lint, test-unit]
needs: [lint, test-unit, test-integration, test-mcp, security]
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
timeout-minutes: 30
@@ -233,6 +240,8 @@ jobs:
run: |
IMAGE="git.hiddenden.cafe/${GITHUB_REPOSITORY,,}"
SHA_TAG="${IMAGE}:sha-${GITHUB_SHA::12}"
# Local build tag: unique per run, never a registry tag.
BUILD_TAG="aegis-ci-${GITHUB_RUN_ID}-build:${GITHUB_SHA}"
EXTRA_TAGS=""
if [ "${GITHUB_EVENT_NAME}" = "push" ]; then
if [ "${GITHUB_REF_NAME}" = "main" ]; then
@@ -244,6 +253,7 @@ jobs:
{
echo "image=${IMAGE}"
echo "sha_tag=${SHA_TAG}"
echo "build_tag=${BUILD_TAG}"
echo "extra_tags=${EXTRA_TAGS}"
} >> "$GITHUB_OUTPUT"
@@ -253,11 +263,11 @@ jobs:
docker build -f docker/Dockerfile \
--label "org.opencontainers.image.revision=${GITHUB_SHA}" \
--label "org.opencontainers.image.source=https://git.hiddenden.cafe/${GITHUB_REPOSITORY}" \
-t "${{ steps.meta.outputs.sha_tag }}" .
-t "${{ steps.meta.outputs.build_tag }}" .
- name: Smoke-test image
shell: bash
run: docker run --rm "${{ steps.meta.outputs.sha_tag }}" version
run: docker run --rm "${{ steps.meta.outputs.build_tag }}" version
# Vulnerability scan of the image that is about to be pushed: pinned Trivy
# (sha256 verified), fails on fixable HIGH and CRITICAL findings. Waivers live
@@ -271,7 +281,7 @@ jobs:
- name: Scan image
shell: bash
run: bash scripts/ci/scan_image.sh "${{ steps.meta.outputs.sha_tag }}"
run: bash scripts/ci/scan_image.sh "${{ steps.meta.outputs.build_tag }}"
# No third-party action sees the registry token: the CLI reads it from stdin. The
# credentials go to a per-job config directory (DOCKER_CONFIG), never into the shared
@@ -290,12 +300,14 @@ jobs:
printf '%s' "${REGISTRY_TOKEN}" | DOCKER_CONFIG="${config_dir}" \
docker login git.hiddenden.cafe --username "${REGISTRY_USER}" --password-stdin
# The registry tags are applied here, right before the push, and removed again by the
# cleanup step (untag only).
- name: Tag and push
if: github.event_name == 'push'
shell: bash
run: |
for tag in ${{ steps.meta.outputs.extra_tags }}; do
docker tag "${{ steps.meta.outputs.sha_tag }}" "${tag}"
for tag in ${{ steps.meta.outputs.sha_tag }} ${{ steps.meta.outputs.extra_tags }}; do
docker tag "${{ steps.meta.outputs.build_tag }}" "${tag}"
done
for tag in ${{ steps.meta.outputs.sha_tag }} ${{ steps.meta.outputs.extra_tags }}; do
docker push "${tag}"
@@ -331,8 +343,10 @@ jobs:
if [ -n "${DOCKER_CONFIG:-}" ]; then
rm -rf "${DOCKER_CONFIG}"
fi
# Registry tags first (untag only), then the per-run build tag.
bash scripts/ci/cleanup_job_resources.sh \
"${{ steps.meta.outputs.sha_tag }}" ${{ steps.meta.outputs.extra_tags }}
"${{ steps.meta.outputs.sha_tag }}" ${{ steps.meta.outputs.extra_tags }} \
"${{ steps.meta.outputs.build_tag }}"
# ---------------------------------------------------------------------------
# 7. Docs drift check
+7 -5
View File
@@ -10,10 +10,12 @@
# network only), plus the containers still attached to those networks;
# * containers started from the per-run image refs given as arguments, and
# those refs. Per-run refs are names containing "aegis-ci-" (named after the
# run id) and immutable ":sha-<commit>" tags;
# * for any other ref (shared tags such as :latest, :main, :dev, which a
# deployment on the same daemon may use) only the tag is removed. Containers
# are never touched, and an image still used by a container simply stays.
# run id), such as the local build tag aegis-ci-<run id>-build:<sha>;
# * for any other ref (registry tags such as :sha-<12>, :latest, :main, :dev,
# which a deployment on the same daemon may use) only the tag is removed.
# Containers are never touched, and an image still used by a container simply
# stays. Pass registry tags before the per-run ref: the image itself goes away
# with its last tag.
# Everything is best effort (a failing removal never fails the job).
set -uo pipefail
@@ -46,7 +48,7 @@ fi
is_per_run_ref() {
case "$1" in
*aegis-ci-* | *:sha-*) return 0 ;;
*aegis-ci-*) return 0 ;;
*) return 1 ;;
esac
}
+35 -5
View File
@@ -275,19 +275,25 @@ def run_cleanup(tmp_path: Path, *refs: str) -> list[str]:
def test_cleanup_removes_containers_only_for_per_run_refs(tmp_path: Path) -> None:
registry = "git.hiddenden.cafe/hiddenden/aegisgitea-mcp"
per_run = f"{registry}:sha-0123456789ab"
shared = [f"{registry}:latest", f"{registry}:main", f"{registry}:dev"]
per_run = "aegis-ci-4242-build:0123456789abcdef"
shared = [
f"{registry}:sha-0123456789ab",
f"{registry}:latest",
f"{registry}:main",
f"{registry}:dev",
]
calls = run_cleanup(tmp_path, per_run, *shared)
calls = run_cleanup(tmp_path, *shared, per_run)
removed = [call for call in calls if call.startswith("rm -f")]
assert removed == [f"rm -f -v cid-of-{per_run}"]
queried = [call for call in calls if call.startswith("ps ")]
assert all("sha-0123456789ab" in call for call in queried) and len(queried) == 1
# Shared tags are only untagged.
assert len(queried) == 1 and per_run in queried[0]
# Registry tags (the immutable sha tag included) are only untagged.
for ref in shared:
assert f"image rm {ref}" in calls
assert f"rmi {ref}" not in calls
assert f"rmi {per_run}" in calls
def test_cleanup_treats_names_with_the_run_prefix_as_per_run(tmp_path: Path) -> None:
@@ -324,3 +330,27 @@ def test_scripts_are_executable_and_parse() -> None:
for script in (ROOT / "scripts" / "ci").glob("*.sh"):
assert os.access(script, os.X_OK), script.name
subprocess.run(["bash", "-n", str(script)], check=True, timeout=30) # noqa: S603, S607
def test_build_and_push_builds_under_a_per_run_tag_and_gates_on_every_test_job() -> None:
jobs = {job.name: job for job in jobs_of(ROOT / ".gitea" / "workflows" / "ci.yml")}
build = jobs["build-and-push"]
assert {"lint", "test-unit", "test-integration", "test-mcp", "security"} <= set(build.needs)
text = build.text
assert 'BUILD_TAG="aegis-ci-${GITHUB_RUN_ID}-build:${GITHUB_SHA}"' in text
# Nothing is built, scanned or smoke-tested under a registry tag.
assert '-t "${{ steps.meta.outputs.build_tag }}"' in text
assert 'scan_image.sh "${{ steps.meta.outputs.build_tag }}"' in text
assert '-t "${{ steps.meta.outputs.sha_tag }}"' not in text
# Registry tags are applied only in the push step, and cleanup removes the build tag last.
assert text.index("docker build") < text.index("docker tag") < text.index("docker push")
assert text.count("docker tag") == 1
cleanup = text[text.index("scripts/ci/cleanup_job_resources.sh") :]
assert cleanup.index("sha_tag") < cleanup.index("build_tag")
def test_the_integration_job_requires_the_actions_runner() -> None:
jobs = {job.name: job for job in jobs_of(ROOT / ".gitea" / "workflows" / "ci.yml")}
assert 'AEGIS_IT_REQUIRE_RUNNER: "1"' in jobs["test-integration"].text