Skip to content

feat(import): add resumable deferred file recovery #315

feat(import): add resumable deferred file recovery

feat(import): add resumable deferred file recovery #315

Workflow file for this run

---
# Pullbox Docker Validation Pipeline
# PR/manual Docker checks prove that image changes build, scan, and smoke-test
# without publishing anything to GHCR or Docker Hub.
#
# Trusted pull requests run the full production Docker Hardened Images build on
# the dedicated Docker runner. Untrusted pull requests fall back to a reduced
# public sanity check that does not require secrets or self-hosted runners.
# GitHub Actions owns Docker validation for PR checks.
name: Docker Validate
on:
pull_request:
branches:
- develop
- main
types:
- opened
- synchronize
- reopened
- ready_for_review
- labeled
- unlabeled
workflow_dispatch:
permissions:
contents: read
concurrency:
group: docker-validate-${{ github.ref || github.run_id }}
cancel-in-progress: true
env:
PYTHON_DEFAULT: "3.14"
jobs:
release_sync_check:
name: Release Sync Check
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
is_sync: ${{ steps.release-sync.outputs.is_sync }}
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Detect safe post-release sync PR
id: release-sync
env:
RELEASE_SYNC_EVENT_NAME: ${{ github.event_name }}
RELEASE_SYNC_BASE_REF: ${{ github.base_ref || '' }}
RELEASE_SYNC_HEAD_REF: ${{ github.head_ref || '' }}
RELEASE_SYNC_REPOSITORY: ${{ github.repository }}
RELEASE_SYNC_HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name || '' }}
RELEASE_SYNC_ACTOR: ${{ github.actor }}
run: |
git fetch --no-tags origin main:refs/remotes/origin/main
if ! git cat-file -e origin/main:.github/scripts/validate-release-sync-pr.py 2>/dev/null; then
echo "is_sync=false" >> "$GITHUB_OUTPUT"
echo "reason=trusted release sync validator is not available on origin/main yet" >> "$GITHUB_OUTPUT"
echo "release_sync=false"
echo "reason=trusted release sync validator is not available on origin/main yet"
exit 0
fi
git show origin/main:.github/scripts/validate-release-sync-pr.py > /tmp/pullbox-release-sync-validator.py
python /tmp/pullbox-release-sync-validator.py
full_ci_check:
name: Full Docker Gate
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
run_full: ${{ steps.gate.outputs.run_full }}
trusted_full: ${{ steps.gate.outputs.trusted_full }}
untrusted_full: ${{ steps.gate.outputs.untrusted_full }}
label_required: ${{ steps.gate.outputs.label_required }}
permissions:
contents: read
steps:
- name: Resolve Docker validation gate
id: gate
run: |
echo "run_full=${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && !github.event.pull_request.draft && (github.event.pull_request.head.repo.full_name != github.repository || github.actor == 'dependabot[bot]' || startsWith(github.event.pull_request.head.ref, 'dependabot/') || contains(github.event.pull_request.labels.*.name, 'ci:full'))) }}" >> "$GITHUB_OUTPUT"
echo "trusted_full=${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && !github.event.pull_request.draft && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && !startsWith(github.event.pull_request.head.ref, 'dependabot/') && contains(github.event.pull_request.labels.*.name, 'ci:full')) }}" >> "$GITHUB_OUTPUT"
echo "untrusted_full=${{ github.event_name == 'pull_request' && !github.event.pull_request.draft && (github.event.pull_request.head.repo.full_name != github.repository || github.actor == 'dependabot[bot]' || startsWith(github.event.pull_request.head.ref, 'dependabot/')) }}" >> "$GITHUB_OUTPUT"
echo "label_required=${{ github.event_name == 'pull_request' && !github.event.pull_request.draft && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && !startsWith(github.event.pull_request.head.ref, 'dependabot/') && !contains(github.event.pull_request.labels.*.name, 'ci:full') }}" >> "$GITHUB_OUTPUT"
untrusted-sanity:
name: Docker Sanity (untrusted PR)
needs: [release_sync_check, full_ci_check]
if: needs.release_sync_check.outputs.is_sync != 'true' && needs.full_ci_check.outputs.untrusted_full == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Explain reduced validation
run: |
echo "Repository secrets are unavailable for fork or Dependabot pull requests."
echo "Running a reduced Docker sanity check instead of the trusted Docker Hardened Images build."
- name: Build local development image
run: docker build -f docker/Dockerfile.dev -t pullbox:pr-sanity .
- name: Inspect built development image
run: docker image inspect pullbox:pr-sanity >/dev/null
- name: Cleanup
if: always()
run: docker image rm -f pullbox:pr-sanity || true
trusted-production-validate:
name: Production Docker Validate (trusted)
needs: [release_sync_check, full_ci_check]
if: needs.release_sync_check.outputs.is_sync != 'true' && needs.full_ci_check.outputs.trusted_full == 'true'
runs-on: [self-hosted, Linux, X64, docker]
timeout-minutes: 35
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Runner preflight
run: .github/scripts/preflight-runner.sh docker
- name: Validate DHI authentication
env:
DHI_USERNAME: ${{ secrets.DHI_USERNAME }}
DHI_TOKEN: ${{ secrets.DHI_TOKEN }}
run: |
if [ -z "${DHI_USERNAME}" ] || [ -z "${DHI_TOKEN}" ]; then
echo "::error::DHI_USERNAME and DHI_TOKEN repository secrets are required to build Docker Hardened Images in CI."
exit 1
fi
- name: Log in to DHI
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: dhi.io
username: ${{ secrets.DHI_USERNAME }}
password: ${{ secrets.DHI_TOKEN }}
- name: Build production Docker image
run: |
BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
GIT_SHA="${GITHUB_SHA}"
docker build \
-f docker/Dockerfile \
-t pullbox:validate \
--build-arg BUILD_DATE="${BUILD_DATE}" \
--build-arg GIT_SHA="${GIT_SHA}" \
--build-arg GIT_BRANCH="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}" \
--build-arg VERSION="validate-${GIT_SHA::7}" \
.
- name: Inspect built production image
run: docker image inspect pullbox:validate >/dev/null
- name: Verify container security runtime
run: |
docker run --rm -i --entrypoint python pullbox:validate - \
< scripts/verify_container_security_runtime.py
- name: Run Grype scan
uses: anchore/scan-action@27805bf3b4e84b4a5c980df22ed233c00390a439 # v7.4.2
with:
image: pullbox:validate
grype-version: v0.110.0
fail-build: true
severity-cutoff: high
output-format: table
config: .grype.yaml
- name: Verify packaged static assets
run: |
docker run --rm --entrypoint python pullbox:validate -c '
import pullbox.app
assets = [
pullbox.app.STATIC_DIR / "css" / "tailwind.css",
pullbox.app.STATIC_DIR / "js" / "htmx.min.js",
]
missing = [str(asset) for asset in assets if not asset.is_file()]
if missing:
raise SystemExit("Missing packaged static assets: " + ", ".join(missing))
print("Packaged static assets verified")
'
- name: Start container
run: |
docker run -d \
--name pullbox-validate \
-p 127.0.0.1::8585 \
-e PULLBOX_SECRET_KEY=docker-validate-secret-key-for-ci \
-e PULLBOX_LOG_LEVEL=WARNING \
pullbox:validate
validate_port="$(docker port pullbox-validate 8585/tcp | awk -F: 'NR == 1 { print $NF }')"
if [ -z "${validate_port}" ]; then
echo "::error::Could not determine Pullbox validation port."
docker port pullbox-validate || true
exit 1
fi
echo "PULLBOX_VALIDATE_URL=http://127.0.0.1:${validate_port}" >> "$GITHUB_ENV"
echo "Container listening on http://127.0.0.1:${validate_port}"
- name: Wait for healthy
run: |
for i in $(seq 1 30); do
if curl -sf "${PULLBOX_VALIDATE_URL}/ping" > /dev/null 2>&1; then
echo "Container healthy after ${i}s"
exit 0
fi
sleep 1
done
echo "Container failed to become healthy"
docker logs pullbox-validate
exit 1
- name: Write Docker validation summary
if: always()
run: |
{
echo "## Docker Validation"
echo ""
echo "- Production image build: pullbox:validate"
echo "- Grype scan: high severity gate"
echo "- Smoke test: /ping health check"
echo "- Publish: disabled for validation workflow"
} >> "$GITHUB_STEP_SUMMARY"
- name: Collect container logs on failure
if: failure() || cancelled()
run: |
mkdir -p test-results/docker-validate
docker logs pullbox-validate > test-results/docker-validate/container.log 2>&1 || true
- name: Upload Docker validation failure artifacts
if: failure() || cancelled()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: docker-validate-failure-artifacts
path: test-results/docker-validate/
if-no-files-found: ignore
retention-days: 7
- name: Cleanup
if: always()
run: |
docker rm -f pullbox-validate || true
docker image rm -f pullbox:validate || true
docker-validate-required:
name: Docker Validate Required
runs-on: ubuntu-latest
needs:
- release_sync_check
- full_ci_check
- untrusted-sanity
- trusted-production-validate
if: always()
timeout-minutes: 5
permissions:
contents: read
steps:
- name: Verify Docker validation jobs
env:
NEEDS_CONTEXT: ${{ toJson(needs) }}
RELEASE_SYNC_PR: ${{ needs.release_sync_check.outputs.is_sync }}
RUN_FULL: ${{ needs.full_ci_check.outputs.run_full }}
TRUSTED_FULL: ${{ needs.full_ci_check.outputs.trusted_full }}
UNTRUSTED_FULL: ${{ needs.full_ci_check.outputs.untrusted_full }}
LABEL_REQUIRED: ${{ needs.full_ci_check.outputs.label_required }}
run: |
python - <<'PY'
import json
import os
import sys
needs = json.loads(os.environ["NEEDS_CONTEXT"])
release_sync_pr = os.environ["RELEASE_SYNC_PR"].lower() == "true"
run_full = os.environ["RUN_FULL"].lower() == "true"
trusted_full = os.environ["TRUSTED_FULL"].lower() == "true"
untrusted_full = os.environ["UNTRUSTED_FULL"].lower() == "true"
label_required = os.environ["LABEL_REQUIRED"].lower() == "true"
if release_sync_pr:
release_sync = needs.get("release_sync_check", {}).get("result")
if release_sync != "success":
print(f"Release sync validation failed: {release_sync}", file=sys.stderr)
sys.exit(1)
unexpected = {
name: info.get("result")
for name, info in needs.items()
if name != "release_sync_check" and info.get("result") not in {"skipped", "success"}
}
if unexpected:
print(f"Release sync fast path saw unexpected Docker results: {unexpected}", file=sys.stderr)
sys.exit(1)
print("Release sync fast path validated; Docker validation intentionally skipped.")
sys.exit(0)
if not run_full:
if label_required:
print("Docker validation is waiting for ci:full before running the required Docker gate.", file=sys.stderr)
else:
print("Docker validation was intentionally not requested for this PR state.", file=sys.stderr)
sys.exit(1)
if trusted_full:
result = needs.get("trusted-production-validate", {}).get("result")
if result != "success":
print(f"Trusted Docker validation did not pass: {result}", file=sys.stderr)
sys.exit(1)
print("Trusted Docker validation passed.")
sys.exit(0)
if untrusted_full:
result = needs.get("untrusted-sanity", {}).get("result")
if result != "success":
print(f"Untrusted Docker sanity validation did not pass: {result}", file=sys.stderr)
sys.exit(1)
print("Untrusted Docker sanity validation passed.")
sys.exit(0)
print("Docker validation gate resolved to no runnable validation path.", file=sys.stderr)
sys.exit(1)
PY