name: Internal dependencies # Every internal Git dependency names `branch = "main"`, and Cargo.lock picks # the commit CI builds and Fabro ships. Each night this job moves the lock to # the current main of each internal library with `cargo update -p`, then runs # the Linux test suite from rust.yml against it, so drift is noticed without # anyone asking. A passing lock goes to one pull request from # `bot/internal-deps`, opened or updated here and never merged here. A failure # opens one tracking issue labeled `internal-deps`, or comments on the open # one; the next passing run closes it. Nothing is pushed to main. on: schedule: - cron: "17 6 * * *" workflow_dispatch: # Never cancel a run midway: the report and pull request jobs must see how # the check ended. concurrency: group: ${{ github.workflow }} cancel-in-progress: false permissions: {} env: CARGO_TERM_COLOR: always CARGO_NET_RETRY: "10" jobs: check: name: Test (Linux) runs-on: ubuntu-24.04-x86-32-cores timeout-minutes: 60 permissions: contents: read outputs: changed: ${{ steps.update.outputs.changed }} summary: ${{ steps.update.outputs.summary }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable with: toolchain: 1.97.1 - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 with: cache-on-failure: true - uses: taiki-e/install-action@773334c0e05d7e699e4d78234494308223f3a2cf # nextest # One package per internal repository: updating one package from a Git # repository moves every package from that repository. daytona-sdk comes # through sandbox-driver, but its repository is separate, so it moves on # its own. The summary names each repository whose locked commit moved, # for the job summary, the pull request, and the tracking issue. - name: Update internal dependencies id: update env: INTERNAL_CRATES: sandbox-driver pebble-agent petri-runtime lithos-llm twin-openai daytona-sdk run: | set -euo pipefail cp Cargo.lock "$RUNNER_TEMP/Cargo.lock.before" args=() for crate in $INTERNAL_CRATES; do args+=(-p "$crate") done cargo update "${args[@]}" 2>&1 | tee "$RUNNER_TEMP/cargo-update.log" python3 - "$RUNNER_TEMP/Cargo.lock.before" Cargo.lock > "$RUNNER_TEMP/summary.md" <<'PY' import re import sys import tomllib SOURCE = re.compile(r"git\+https://github\.com/([^?#]+?)(?:\.git)?\?[^#]*#([0-9a-f]+)$") def commits(path): found = {} with open(path, "rb") as lock: for package in tomllib.load(lock).get("package", []): match = SOURCE.match(package.get("source", "")) if match: found.setdefault(match[1], set()).add(match[2]) return found before, after = commits(sys.argv[1]), commits(sys.argv[2]) rows = [] for repo in sorted(set(before) | set(after)): old, new = sorted(before.get(repo, ())), sorted(after.get(repo, ())) if old == new: continue if len(old) == 1 and len(new) == 1: moved = f"[`{old[0][:7]}...{new[0][:7]}`](https://github.com/{repo}/compare/{old[0]}...{new[0]})" else: moved = " ".join(f"`{c[:7]}`" for c in old) + " -> " + " ".join(f"`{c[:7]}`" for c in new) rows.append(f"| `{repo}` | {moved} |") if rows: print("| Repository | Commits |\n|---|---|") print("\n".join(rows)) else: print("No internal commit moved.") PY { echo echo "
cargo update output" echo echo '```' cat "$RUNNER_TEMP/cargo-update.log" echo '```' echo echo "
" } >> "$RUNNER_TEMP/summary.md" cat "$RUNNER_TEMP/summary.md" >> "$GITHUB_STEP_SUMMARY" # Only a moved internal commit counts: `cargo update` can also # rewrite unrelated entries of a lock that `--locked` accepts. if grep -q '^| `' "$RUNNER_TEMP/summary.md"; then echo "changed=true" >> "$GITHUB_OUTPUT" else git checkout -- Cargo.lock echo "changed=false" >> "$GITHUB_OUTPUT" echo "Cargo.lock already locks every internal main; nothing to do." fi delimiter="SUMMARY_$(openssl rand -hex 16)" { echo "summary<<$delimiter" cat "$RUNNER_TEMP/summary.md" echo "$delimiter" } >> "$GITHUB_OUTPUT" # The rest is rust.yml's Test (Linux) job on the updated lock. # Every Petri run takes its scope through a sandbox-driver plugin # executable that Petri finds on PATH: `sandbox-driver-host` for the # `local` provider, `sandbox-driver-docker` for `docker`. Installed # at the commit the updated Cargo.lock resolves sandbox-driver to, so # the plugins and the in-process driver are one build. - name: Read the sandbox-driver commit Cargo.lock resolves if: steps.update.outputs.changed == 'true' id: sandbox-driver run: | rev="$(cargo metadata --locked --format-version 1 | jq -r '.packages[] | select(.name == "sandbox-driver") | .source' | sed 's/.*#//' | sort -u)" [[ "$rev" =~ ^[0-9a-f]{40}$ ]] echo "rev=$rev" >> "$GITHUB_OUTPUT" - name: Restore the sandbox-driver plugin executables if: steps.update.outputs.changed == 'true' id: sandbox-driver-cache uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: | ~/.cargo/bin/sandbox-driver-host ~/.cargo/bin/sandbox-driver-docker key: sandbox-driver-plugins-${{ runner.os }}-${{ steps.sandbox-driver.outputs.rev }} - name: Install the sandbox-driver plugin executables if: steps.update.outputs.changed == 'true' && steps.sandbox-driver-cache.outputs.cache-hit != 'true' env: SANDBOX_DRIVER_REV: ${{ steps.sandbox-driver.outputs.rev }} run: cargo install --locked --git https://github.com/lithoscomputer/sandbox-driver --rev "$SANDBOX_DRIVER_REV" sandbox-driver-host sandbox-driver-docker # The images the suite's Docker tests run; see rust.yml. - name: Pull the images the Docker tests run if: steps.update.outputs.changed == 'true' run: | backend="$(dirname "$(cargo metadata --locked --format-version 1 | jq -r '.packages[] | select(.name == "petri-executor-sandbox") | .manifest_path')")/src/backend.rs" pin="$(sed -n 's/^const RUNNER_PIN: &str = "\([0-9a-f]*\)";$/\1/p' "$backend")" test -n "$pin" docker pull "ghcr.io/lithoscomputer/ubuntu-24.04:slim-$pin" docker pull ghcr.io/lithoscomputer/ubuntu-22.04:slim - name: Test if: steps.update.outputs.changed == 'true' run: cargo nextest run --locked --workspace --status-level slow --profile ci # The pull request job commits exactly the lock that passed. - name: Keep the tested lock if: steps.update.outputs.changed == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: internal-deps-lock path: Cargo.lock if-no-files-found: error retention-days: 7 pull-request: name: Open the update pull request needs: check if: needs.check.outputs.changed == 'true' runs-on: ubuntu-24.04 timeout-minutes: 10 # Repository settings must allow GitHub Actions to create pull requests. # PR CI created or updated with GITHUB_TOKEN waits for a maintainer to # select "Approve workflows to run" on the pull request. permissions: contents: write # push the tested lock to the update branch pull-requests: write # open or update the pull request steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 # the branch merges the tested commit into its history persist-credentials: false - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: internal-deps-lock path: ${{ runner.temp }}/internal-deps-lock # The branch is only ever added to, never rewritten: an open pull # request's branch merges the tested commit and takes the tested lock; # with no open pull request, a leftover branch is deleted and a new one # starts from the tested commit. The push names the branch explicitly, # so it can never reach main. - name: Push the lock and open or update the pull request env: GH_TOKEN: ${{ github.token }} BRANCH: bot/internal-deps SUMMARY: ${{ needs.check.outputs.summary }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | set -euo pipefail lock="$RUNNER_TEMP/internal-deps-lock/Cargo.lock" repo_api="repos/$GITHUB_REPOSITORY" remote="https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" pr="$(gh api "$repo_api/pulls?head=$GITHUB_REPOSITORY_OWNER:$BRANCH&base=main&state=open" --jq '.[0].number // empty')" if [ -n "$pr" ]; then git fetch --no-tags "$remote" "refs/heads/$BRANCH" git checkout -B "$BRANCH" FETCH_HEAD # Cargo.lock is the only file the branch changes, so it is the # only possible conflict, and the tested lock resolves it. git merge --no-ff --no-commit "$GITHUB_SHA" || true cp "$lock" Cargo.lock git add Cargo.lock if [ -n "$(git diff --name-only --diff-filter=U)" ]; then echo "::error::$BRANCH conflicts with main outside Cargo.lock; close its pull request and rerun." exit 1 fi else if git ls-remote --exit-code --heads "$remote" "$BRANCH" > /dev/null; then git push "$remote" --delete "$BRANCH" fi git checkout -B "$BRANCH" "$GITHUB_SHA" cp "$lock" Cargo.lock git add Cargo.lock fi if git rev-parse -q --verify MERGE_HEAD > /dev/null || ! git diff --cached --quiet; then git commit -m "Update internal dependencies to their current main" git push "$remote" "HEAD:refs/heads/$BRANCH" else echo "$BRANCH already carries this lock." fi body="$RUNNER_TEMP/body.md" { echo "Moves Cargo.lock to the current main of each internal library with \`cargo update -p\`. The Linux test suite passed on this lock: $RUN_URL" echo echo "${SUMMARY:-No summary was recorded.}" echo echo "Opened by the Internal dependencies workflow (\`.github/workflows/internal-deps.yml\`), which updates this branch each night the update passes. Select \"Approve workflows to run\" on this pull request to start PR CI, then merge it when CI is green." } > "$body" if [ -n "$pr" ]; then gh api -X PATCH "$repo_api/pulls/$pr" -F body=@"$body" --jq '"Updated \(.html_url)"' else gh api "$repo_api/pulls" -f title="Update internal dependencies" -f head="$BRANCH" -f base=main \ -F body=@"$body" --jq '"Opened \(.html_url)"' fi report: name: report needs: check if: always() && (needs.check.result == 'success' || needs.check.result == 'failure') runs-on: ubuntu-24.04 timeout-minutes: 10 permissions: issues: write # open, comment on, and close the tracking issue; create its label steps: - name: Update the tracking issue env: GH_TOKEN: ${{ github.token }} RESULT: ${{ needs.check.result }} CHANGED: ${{ needs.check.outputs.changed }} SUMMARY: ${{ needs.check.outputs.summary }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | set -euo pipefail label=internal-deps repo_api="repos/$GITHUB_REPOSITORY" issue="$(gh api "$repo_api/issues?labels=$label&state=open&per_page=100" \ --jq '[.[] | select(.pull_request == null)] | sort_by(.number) | .[0].number // empty')" body="$RUNNER_TEMP/body.md" if [ "$RESULT" = "success" ]; then if [ "$CHANGED" = "true" ]; then echo "The updated lock passed the Linux test suite; the pull request job carries it." >> "$GITHUB_STEP_SUMMARY" fi if [ -n "$issue" ]; then { echo "The nightly internal dependency update passed again: $RUN_URL" echo echo "${SUMMARY:-No summary was recorded.}" } > "$body" gh api "$repo_api/issues/$issue/comments" -F body=@"$body" --silent gh api -X PATCH "$repo_api/issues/$issue" -f state=closed -f state_reason=completed --silent echo "Closed #$issue." fi exit 0 fi { echo "The nightly internal dependency update failed: $RUN_URL" echo echo "It moved Cargo.lock to the current main of each internal library with \`cargo update -p\` and ran the Linux test suite against it. Whoever broke an API this repository uses fixes it here promptly." echo echo "${SUMMARY:-No summary was recorded: the run failed before \`cargo update\` finished.}" } > "$body" if [ -n "$issue" ]; then gh api "$repo_api/issues/$issue/comments" -F body=@"$body" --silent echo "Commented on #$issue." else if ! gh api "$repo_api/labels/$label" --silent 2>/dev/null; then gh api "$repo_api/labels" -f name="$label" -f color=d93f0b \ -f description="Nightly internal dependency update" --silent fi gh api "$repo_api/issues" -f title="Nightly internal dependency update failed" \ -F body=@"$body" -f "labels[]=$label" --jq '"Opened #\(.number)."' fi