Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 56 additions & 2 deletions .github/workflows/fork-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,64 @@ jobs:
- name: Exercise release-only workflow steps
run: node scripts/release-smoke.ts

deployment_scope:
name: Classify Deployment Scope
if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/fork/integration'
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
actions: read
contents: read
outputs:
deploy: ${{ steps.classify.outputs.deploy }}
steps:
- name: Checkout integration source
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Find previous successful integration CI
id: previous
env:
GH_TOKEN: ${{ github.token }}
run: |
previous_sha="$(
gh api --method GET \
"repos/${GITHUB_REPOSITORY}/actions/workflows/fork-ci.yml/runs" \
-f branch=fork/integration \
-f event=workflow_dispatch \
-f status=success \
-f per_page=20 \
--jq ".workflow_runs | map(select(.head_sha != \"${GITHUB_SHA}\")) | first | .head_sha // \"\""
)"
echo "sha=${previous_sha}" >>"${GITHUB_OUTPUT}"

- name: Classify changes since previous successful integration CI
id: classify
env:
PREVIOUS_SHA: ${{ steps.previous.outputs.sha }}
run: |
deploy=true
if [[ "${PREVIOUS_SHA}" =~ ^[0-9a-f]{40}$ ]] &&
git fetch --quiet origin "${PREVIOUS_SHA}" &&
git cat-file -e "${PREVIOUS_SHA}^{commit}"; then
deploy="$(
scripts/classify-deployment-diff.sh "${PREVIOUS_SHA}" "${GITHUB_SHA}" |
tee /dev/stderr |
sed -n 's/^deploy=//p'
)"
else
echo "No fetchable previous successful integration SHA; deployment remains enabled."
fi
echo "deploy=${deploy}" >>"${GITHUB_OUTPUT}"

dispatch_mobile_ota:
name: Dispatch Mobile OTA
needs: [check, test, mobile_native_static_analysis, release_smoke]
if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/fork/integration'
needs: [check, test, mobile_native_static_analysis, release_smoke, deployment_scope]
if: |
github.event_name == 'workflow_dispatch' &&
github.ref == 'refs/heads/fork/integration' &&
needs.deployment_scope.outputs.deploy == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
Expand Down
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ branches.
deployment. Do not re-enable or target those workflows for fork releases.
- Updating `fork/tim` or merging a PR into `fork/changes` triggers the stack workflow, which rebases
the provenance layers, rebuilds `fork/integration`, and dispatches CI for its exact SHA.
- Successful `fork/integration` CI hands the exact tested SHA to the private operations repository.
- Successful `fork/integration` CI classifies the complete tree diff from the previous approved
integration tree. Runtime-affecting changes hand the exact tested SHA to the private operations
repository; tests, documentation, agent metadata, and GitHub-only metadata do not deploy.
- Machine topology and deployment implementation belong in a separate private operations repository,
not this repository.

Expand Down
9 changes: 8 additions & 1 deletion docs/fork-stack.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,16 @@ feature PR merged into fork/changes
→ rebase-pr-stack workflow
→ fork/integration updated atomically
→ CI dispatched for the exact integration SHA
→ successful CI triggers fleet deployment
→ successful CI classifies the tree diff
→ runtime-affecting changes trigger fleet deployment
→ test, documentation, and automation-only changes stop after CI
```

Deployment classification compares complete tested integration trees rather than only the latest
commit. Unknown paths are runtime-affecting by default. This preserves safe deployment when a PR
contains mixed changes or a new source directory appears, while avoiding fleet rebuilds and mobile
OTA updates for tests, snapshots, documentation, agent instructions, and GitHub-only metadata.

The manifest contains the permanent `fork/tim` PR followed by the permanent `fork/changes` PR. The
synchronizer rebases that provenance chain onto the latest upstream `main` and rebuilds
`fork/integration`. Other open repository PRs are ignored. Temporary state is retained after a
Expand Down
50 changes: 50 additions & 0 deletions scripts/classify-deployment-diff.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env bash

set -euo pipefail

base_sha="${1:-}"
head_sha="${2:-}"

if [[ ! "${base_sha}" =~ ^[0-9a-f]{40}$ ]] || [[ ! "${head_sha}" =~ ^[0-9a-f]{40}$ ]]; then
echo "usage: $0 <base-sha> <head-sha>" >&2
exit 2
fi

is_non_runtime_path() {
case "$1" in
.agents/* | .github/* | docs/* | \
AGENTS.md | CLAUDE.md | README.md | */README.md | \
*.md | *.mdx | *.snap | \
*.test.* | *.spec.* | \
test/* | tests/* | */test/* | */tests/* | \
*/__snapshots__/* | */__tests__/* | */testUtils/* | */fixtures/* | \
apps/server/scripts/acp-mock-agent.ts | scripts/release-smoke.ts)
return 0
;;
*)
return 1
;;
esac
}

runtime_paths=()
non_runtime_paths=()
while IFS= read -r -d '' path; do
if is_non_runtime_path "${path}"; then
non_runtime_paths+=("${path}")
else
runtime_paths+=("${path}")
fi
done < <(git diff --name-only -z "${base_sha}" "${head_sha}")

printf 'Changed paths: %d runtime, %d non-runtime\n' \
"${#runtime_paths[@]}" "${#non_runtime_paths[@]}"

if ((${#runtime_paths[@]} > 0)); then
printf 'Runtime-affecting paths:\n'
printf ' %s\n' "${runtime_paths[@]}"
printf 'deploy=true\n'
else
printf 'Only tests, documentation, agent metadata, or CI metadata changed.\n'
printf 'deploy=false\n'
fi
Loading