mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-02 10:16:20 +08:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Paperclip Cloud needs a verified image for each merged source commit. > - Fresh builders restore compiled native dependencies from registry caches. > - The current workflow imports up to eleven historical cache manifests at once. > - Live builds missed native layers that a fresh builder reused from one manifest. > - This PR selects the nearest available cache and tests reuse across fresh builders. ## Linked Issues or Issue Description Refs #13329 and #13330. A search of open cache PRs found no duplicate of this change. **What existing behavior does this improve?** Remote Docker cache reuse on fresh Cloud image builders. **Current behavior** [Cloud run 34714483272](https://github.com/paperclipai/paperclip/actions/runs/34714483272/job/103609096836) imported the previous cache manifest successfully but rebuilt `cargo-chef` and Rust dependencies. The dependency compile took 3m43s. The preceding image build had already exported those layers. A controlled [fresh-builder diagnostic](https://github.com/paperclipai/paperclip/actions/runs/34715336530) used the same source and registry cache. The single-manifest job reused both layers immediately. The multiple-manifest job rebuilt them and failed the cache assertion. Both jobs used GitHub-hosted runners with read-only access. **Proposed behavior** Inspect cache manifests in first-parent order and import only the nearest available one. Keep full-SHA cache exports, the ten-commit search bound, and the legacy fallback. If caches cannot be read, permit a cold build. **Reason and benefit** Avoid the observed cache misses without changing image contents or builder sizes. Expected savings include about four minutes of native tool/dependency compilation when those inputs are unchanged. The final merge-to-deployable gain still needs a post-merge measurement. **Breaking changes** No image, artifact, deployment, or runner-routing contract changes. ## What Changed - Select one available ancestor cache after Docker login and Buildx setup. - Preserve separate writable cache tags for each full source SHA. - Test cache ordering, missing caches, registry errors, and workflow integration. - Add the selector tests to the existing release-registry suite. - Export a local test cache, remove the first builder, and verify a source rebuild on a fresh builder. - Document cache selection and the stronger Docker check. ## Verification - Passed 456 focused workflow, routing, readiness, preview-artifact, and cache-selector tests. - Passed shell syntax, ShellCheck for the changed probe, actionlint workflow validation, and `git diff --check`. actionlint's shell checks were disabled for the workflow validation because unchanged migration-label commands trigger existing SC2012 notes. - The fresh-builder registry diagnostic proves the single-cache behavior. The [permanent two-builder probe passed](https://github.com/paperclipai/paperclip/actions/runs/34715771048/job/103612624090), including a changed real binary and dependency-declaration invalidation. - Passed all 35 latest-head checks (green or intentionally skipped), including full typecheck, test, build, and browser suites in [PR CI run 34715771217](https://github.com/paperclipai/paperclip/actions/runs/34715771217). - The real selector CLI inspected registry metadata and chose the nearest available ancestor cache. - Fresh Greptile review is 5/5 with no open findings. The PR title was corrected to meet the source-change naming rule; the review check passed after that correction. - Local full-suite runs and Docker builds are unavailable because the local Docker daemon is unresponsive after disk exhaustion. CI provides the Linux verification. ## Risks - Missing or unreadable caches cause a slower cold build. The selector logs that condition and preserves image publication. - Inspecting several missing ancestors adds lookup time. Each lookup has a ten-second timeout and the search is bounded. - The Docker test now exports a local cache. It removes the first builder before starting the second to release disk space, then cleans up its builders and files. ## Model Used OpenAI GPT-6 through Codex, with reasoning, repository tools, and code execution. The exact serving model ID and context window are not exposed by this environment. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Paperclip <noreply@paperclip.ing>
76 lines
3.6 KiB
Bash
76 lines
3.6 KiB
Bash
#!/usr/bin/env bash
|
|
# Build the real Docker target on two fresh builders using an exported cache.
|
|
# Export only metadata, avoiding a multi-gigabyte test image in the daemon.
|
|
set -euo pipefail
|
|
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
probe_dir="$(mktemp -d "${TMPDIR:-/tmp}/paperclip-runner-cache.XXXXXX")"
|
|
baseline_builder="${probe_dir##*/}-baseline"
|
|
rebuild_builder="${probe_dir##*/}-rebuild"
|
|
cleanup() {
|
|
for builder in "$baseline_builder" "$rebuild_builder"; do
|
|
docker buildx rm "$builder" >/dev/null 2>&1 || true
|
|
done
|
|
rm -rf "$probe_dir"
|
|
}
|
|
trap cleanup EXIT
|
|
mkdir "$probe_dir/context"
|
|
cd "$repo_root"
|
|
git ls-files -z | tar -cf - --null -T - | tar -xf - -C "$probe_dir/context"
|
|
cd "$probe_dir/context"
|
|
export PROBE_DIR="$probe_dir"
|
|
cp Dockerfile "$probe_dir/cache-probe.Dockerfile"
|
|
cat >> "$probe_dir/cache-probe.Dockerfile" <<'DOCKER'
|
|
FROM runner-build AS cache-proof
|
|
RUN ./runner/target/release/paperclip-runnerd --build-metadata > /metadata.json
|
|
FROM scratch AS cache-proof-export
|
|
COPY --from=cache-proof /metadata.json /metadata.json
|
|
COPY --from=runner-plan /tmp/runner-recipe.json /recipe.json
|
|
FROM scratch AS recipe-proof-export
|
|
COPY --from=runner-plan /tmp/runner-recipe.json /recipe.json
|
|
DOCKER
|
|
build_proof() {
|
|
local result="$1" builder="$2"
|
|
shift 2
|
|
docker buildx build --builder "$builder" --file "$probe_dir/cache-probe.Dockerfile" --target cache-proof-export --output "type=local,dest=$probe_dir/$result" --progress plain "$@" . 2>&1 | tee "$probe_dir/$result.log"
|
|
}
|
|
docker buildx create --name "$baseline_builder" --driver docker-container
|
|
build_proof baseline "$baseline_builder" --cache-to "type=local,dest=$probe_dir/cache,mode=max"
|
|
# Removing the first builder proves the second build cannot use daemon-local
|
|
# state, and releases its disk space before importing the exported cache.
|
|
docker buildx rm "$baseline_builder"
|
|
docker buildx create --name "$rebuild_builder" --driver docker-container
|
|
python3 - <<'CHECK'
|
|
from pathlib import Path
|
|
p=Path('packages/paperclip-runner/runner/crates/runner-core/src/bin/paperclip-runnerd.rs')
|
|
s=p.read_text(); needle='paperclip-runner/runnerd-build-metadata/v1'
|
|
assert s.count(needle)==1
|
|
p.write_text(s.replace(needle,needle+'-cache-probe'))
|
|
CHECK
|
|
build_proof source-change "$rebuild_builder" --cache-from "type=local,src=$probe_dir/cache"
|
|
python3 - <<'CHECK'
|
|
import os,json,re
|
|
from pathlib import Path
|
|
root=Path(os.environ['PROBE_DIR'])
|
|
before=json.loads((root/'baseline/metadata.json').read_text())
|
|
after=json.loads((root/'source-change/metadata.json').read_text())
|
|
assert before['schema']=='paperclip-runner/runnerd-build-metadata/v1'
|
|
assert after['schema']==before['schema']+'-cache-probe'
|
|
assert (root/'baseline/recipe.json').read_bytes()==(root/'source-change/recipe.json').read_bytes()
|
|
log=(root/'source-change.log').read_text()
|
|
step=re.search(r'#(\d+) \[runner-deps[^\n]+ RUN cargo chef cook',log)[1]
|
|
assert f'#{step} CACHED' in log
|
|
assert 'Compiling paperclip-runner-core' in log
|
|
print('PASS: fresh builder imported compiled dependencies; real binary changed.')
|
|
p=Path('packages/paperclip-runner/runner/Cargo.toml')
|
|
s=p.read_text(); assert 'serde_json = "1.0"' in s
|
|
p.write_text(s.replace('serde_json = "1.0"','serde_json = ">=1.0.0, <2.0.0"'))
|
|
CHECK
|
|
docker buildx build --builder "$rebuild_builder" --file "$probe_dir/cache-probe.Dockerfile" --target recipe-proof-export --output "type=local,dest=$probe_dir/manifest-change" --progress plain .
|
|
python3 - <<'CHECK'
|
|
from pathlib import Path
|
|
import os
|
|
root=Path(os.environ['PROBE_DIR'])
|
|
assert (root/'source-change/recipe.json').read_bytes()!=(root/'manifest-change/recipe.json').read_bytes()
|
|
print('PASS: dependency declaration change invalidates the recipe.')
|
|
CHECK
|