Skip to content

Commit d18dc04

Browse files
vorporealoz-agent
andcommitted
Rewrite bootstrap: support existing repos, stop before push
- Detects whether the public repo is empty or has existing content. For empty repos, initializes a new git repo. For existing repos, clones and replaces the tree. - Stops after preparing the commit, prints instructions for the user to review, push, and set watermark tags. - No longer pushes or sets watermarks automatically -- the user retains full control to inspect the change before it goes live. Co-Authored-By: Oz <oz-agent@warp.dev>
1 parent 3b2c539 commit d18dc04

1 file changed

Lines changed: 92 additions & 119 deletions

File tree

scripts/bootstrap.sh

Lines changed: 92 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
#!/usr/bin/env bash
22
# Bootstrap script for repo-sync.
33
#
4-
# Generates a clean snapshot of the private repo at HEAD, pushes it as the
5-
# initial commit to the public repo, and sets watermark tags in both repos.
4+
# Generates a clean snapshot of the private repo at HEAD, prepares a commit
5+
# in the public repo that replaces its entire tree with the clean snapshot,
6+
# and prints instructions for the user to review, push, and set watermarks.
67
#
7-
# This is a one-time setup step. After bootstrap, the regular sync workflows
8-
# take over.
8+
# The script stops before pushing so you can review the change.
9+
#
10+
# This is a one-time setup step. After bootstrap + watermark setup, the
11+
# regular sync workflows take over.
912
#
1013
# Usage:
1114
# ./scripts/bootstrap.sh \
@@ -22,7 +25,7 @@
2225
#
2326
# Prerequisites:
2427
# - The private repo must be checked out locally (script runs from its root).
25-
# - The public repo must exist and be empty (no commits).
28+
# - The public repo must exist on GitHub (can be empty or have existing content).
2629
# - The stripping tool must be installed (pip install -e . from the repo-sync repo).
2730
# - gh CLI must be installed.
2831
# - git must be configured with user.name and user.email.
@@ -89,143 +92,113 @@ python -m repo_sync.strip.cli "${SNAPSHOT_DIR}"
8992
echo "Clean snapshot generated at ${SNAPSHOT_DIR}."
9093

9194
# ---------------------------------------------------------------------------
92-
# Step 2: Push the snapshot as the initial commit to the public repo.
95+
# Step 2: Prepare the bootstrap commit in the public repo.
9396
# ---------------------------------------------------------------------------
94-
echo "Step 2: Pushing initial commit to public repo..."
97+
echo "Step 2: Preparing bootstrap commit in public repo..."
9598
WORK_DIR=$(mktemp -d)
9699

97-
pushd "$WORK_DIR" > /dev/null
100+
# Check if the public repo is empty or has existing content.
101+
IS_EMPTY="false"
102+
EXISTING_COMMITS_RAW=$(gh api "/repos/${PUBLIC_REPO}/commits?per_page=1" 2>/dev/null || true)
103+
if ! echo "$EXISTING_COMMITS_RAW" | jq -e 'type == "array" and length > 0' > /dev/null 2>&1; then
104+
IS_EMPTY="true"
105+
fi
106+
107+
if [ "$IS_EMPTY" = "true" ]; then
108+
echo "Public repo is empty. Creating initial commit."
109+
pushd "$WORK_DIR" > /dev/null
110+
git init -b "${DEFAULT_BRANCH}"
111+
git remote add origin "https://x-access-token:${TOKEN}@github.com/${PUBLIC_REPO}.git"
112+
else
113+
echo "Public repo has existing content. Cloning and replacing tree."
114+
git clone "https://x-access-token:${TOKEN}@github.com/${PUBLIC_REPO}.git" "$WORK_DIR"
115+
pushd "$WORK_DIR" > /dev/null
116+
git checkout "${DEFAULT_BRANCH}"
117+
# Remove all existing content (but keep .git/).
118+
git rm -rf --quiet . 2>/dev/null || true
119+
fi
98120

99-
# Initialize a new git repo using the same default branch as the private repo.
100-
git init -b "${DEFAULT_BRANCH}"
121+
# Copy the clean snapshot into the working tree.
101122
cp -a "${SNAPSHOT_DIR}/." .
102123
git add -A
103124

104-
# Check if there's anything to commit.
125+
# Check if there are any changes to commit.
105126
if git diff --cached --quiet; then
106-
echo "Error: clean snapshot is empty. Nothing to push." >&2
127+
echo "No changes -- the public repo already matches the clean snapshot."
107128
popd > /dev/null
108129
rm -rf "$SNAPSHOT_DIR" "$WORK_DIR"
109-
exit 1
130+
exit 0
110131
fi
111132

112133
git commit -m "repo-sync: initial sync from private repo
113134
114135
Repo-Sync-Origin: ${PRIVATE_REPO}@${HEAD_SHA}"
115136

116-
INITIAL_COMMIT_SHA=$(git rev-parse HEAD)
117-
echo "Initial commit: ${INITIAL_COMMIT_SHA}"
118-
119-
# Push to the public repo.
120-
git remote add origin "https://x-access-token:${TOKEN}@github.com/${PUBLIC_REPO}.git"
121-
122-
# Safety check: refuse to push if the public repo already has commits.
123-
# The GitHub API returns a 409 for empty repos, so we treat any non-array
124-
# response (including errors) as "empty repo, proceed."
125-
EXISTING_COMMITS_RAW=$(gh api "/repos/${PUBLIC_REPO}/commits?per_page=1" 2>/dev/null || true)
126-
if echo "$EXISTING_COMMITS_RAW" | jq -e 'type == "array" and length > 0' > /dev/null 2>&1; then
127-
echo "Error: public repo ${PUBLIC_REPO} already has commits. Bootstrap is only for empty repos." >&2
128-
echo "If you want to overwrite, delete the repo's content first." >&2
129-
popd > /dev/null
130-
rm -rf "$SNAPSHOT_DIR" "$WORK_DIR"
131-
exit 1
132-
fi
133-
134-
git push -u origin "${DEFAULT_BRANCH}"
135-
echo "Pushed to ${PUBLIC_REPO}/${DEFAULT_BRANCH}."
137+
BOOTSTRAP_COMMIT_SHA=$(git rev-parse HEAD)
138+
echo "Bootstrap commit: ${BOOTSTRAP_COMMIT_SHA}"
136139

137140
popd > /dev/null
138141

139142
# ---------------------------------------------------------------------------
140-
# Step 3: Set watermark tag in the public repo.
141-
# The watermark points to the initial commit in the public repo.
143+
# Step 3: Print review and next-steps instructions.
142144
# ---------------------------------------------------------------------------
143-
echo "Step 3: Setting watermark in public repo..."
144-
WATERMARK_TAG="repo-sync/watermark/private-to-public"
145-
146-
# Get the actual commit SHA on the public repo (after push).
147-
PUBLIC_HEAD_SHA=$(gh api "/repos/${PUBLIC_REPO}/git/ref/heads/${DEFAULT_BRANCH}" \
148-
--jq '.object.sha')
149-
150-
# Create the watermark tag. Try POST (create) first; if the tag already
151-
# exists, fall back to PATCH (update).
152-
if ! gh api -X POST "/repos/${PUBLIC_REPO}/git/refs" \
153-
-f ref="refs/tags/${WATERMARK_TAG}" \
154-
-f sha="${PUBLIC_HEAD_SHA}" > /dev/null 2>&1; then
155-
gh api -X PATCH "/repos/${PUBLIC_REPO}/git/refs/tags/${WATERMARK_TAG}" \
156-
-f sha="${PUBLIC_HEAD_SHA}" \
157-
-F force=true > /dev/null
145+
echo ""
146+
echo "=========================================================================="
147+
echo " Bootstrap commit is ready for review."
148+
echo "=========================================================================="
149+
echo ""
150+
echo "The commit has been prepared locally in:"
151+
echo " ${WORK_DIR}"
152+
echo ""
153+
echo "--- Review the changes: ---"
154+
echo ""
155+
if [ "$IS_EMPTY" = "true" ]; then
156+
echo " cd ${WORK_DIR} && git log -1"
157+
echo " cd ${WORK_DIR} && git diff --stat HEAD"
158+
else
159+
echo " cd ${WORK_DIR} && git log -1"
160+
echo " cd ${WORK_DIR} && git diff HEAD~1 --stat"
161+
echo " cd ${WORK_DIR} && git diff HEAD~1"
158162
fi
159-
160-
echo "Watermark '${WATERMARK_TAG}' set in ${PUBLIC_REPO} -> ${PUBLIC_HEAD_SHA}."
161-
162-
# ---------------------------------------------------------------------------
163-
# Step 4: Set watermark tag in the private repo.
164-
# Use git's empty tree SHA as a sentinel — there are no public commits to
165-
# sync back yet. The public-to-private workflow will see this and know that
166-
# the initial public commit (which has a Repo-Sync-Origin trailer) should be
167-
# skipped.
168-
# ---------------------------------------------------------------------------
169-
echo "Step 4: Setting watermark in private repo..."
170-
PRIVATE_WATERMARK_TAG="repo-sync/watermark/public-to-private"
171-
172-
# The sentinel: we create a lightweight tag pointing to the public repo's
173-
# initial commit. Since that commit has a Repo-Sync-Origin trailer, the
174-
# public-to-private workflow will recognize it and skip it.
175-
#
176-
# We need to set this in the private repo. The watermark points to the
177-
# public repo's initial commit, and the Repo-Sync-Origin trailer on that
178-
# commit tells the workflow that everything up to HEAD_SHA has been synced.
179-
#
180-
# Since the watermark tag for public-to-private lives in the private repo
181-
# (the target repo for that direction), we need to create a commit object
182-
# in the private repo that has the right trailer. We'll create a tag
183-
# pointing to a specially-crafted commit.
184-
#
185-
# Simplest approach: create a lightweight tag pointing to the current HEAD
186-
# of the private repo. The initial public commit already has
187-
# Repo-Sync-Origin: <private-repo>@<HEAD>, so the public-to-private
188-
# workflow will look at the public repo's HEAD and see that the last commit
189-
# is sync-originated. But the watermark is read differently for the reverse
190-
# direction — it needs to point to the last synced PUBLIC commit.
191-
#
192-
# For the initial bootstrap, we tag the public repo's initial commit in the
193-
# private repo's watermark. We use the GitHub API to create a commit object
194-
# in the private repo that carries the right trailer.
195-
BOOTSTRAP_MSG="repo-sync: bootstrap sentinel
196-
197-
Repo-Sync-Origin: ${PUBLIC_REPO}@${PUBLIC_HEAD_SHA}"
198-
199-
# Create a commit object in the private repo using the tree of the current HEAD.
200-
PRIVATE_HEAD_SHA=$(gh api "/repos/${PRIVATE_REPO}/git/ref/heads/$(gh api "/repos/${PRIVATE_REPO}" --jq '.default_branch')" \
201-
--jq '.object.sha')
202-
PRIVATE_TREE=$(gh api "/repos/${PRIVATE_REPO}/git/commits/${PRIVATE_HEAD_SHA}" \
203-
--jq '.tree.sha')
204-
205-
SENTINEL_COMMIT=$(gh api -X POST "/repos/${PRIVATE_REPO}/git/commits" \
206-
-f message="${BOOTSTRAP_MSG}" \
207-
-f tree="${PRIVATE_TREE}" \
208-
-f "parents[]=${PRIVATE_HEAD_SHA}" \
209-
--jq '.sha')
210-
211-
# Create the watermark tag. Try POST first; fall back to PATCH.
212-
if ! gh api -X POST "/repos/${PRIVATE_REPO}/git/refs" \
213-
-f ref="refs/tags/${PRIVATE_WATERMARK_TAG}" \
214-
-f sha="${SENTINEL_COMMIT}" > /dev/null 2>&1; then
215-
gh api -X PATCH "/repos/${PRIVATE_REPO}/git/refs/tags/${PRIVATE_WATERMARK_TAG}" \
216-
-f sha="${SENTINEL_COMMIT}" \
217-
-F force=true > /dev/null
163+
echo ""
164+
echo "--- Push (after reviewing): ---"
165+
echo ""
166+
if [ "$IS_EMPTY" = "true" ]; then
167+
echo " cd ${WORK_DIR} && git push -u origin ${DEFAULT_BRANCH}"
168+
else
169+
echo " cd ${WORK_DIR} && git push origin ${DEFAULT_BRANCH}"
218170
fi
171+
echo ""
172+
echo "--- Set watermark tags (after pushing): ---"
173+
echo ""
174+
echo "Run these commands to set the watermark tags that the sync workflows need."
175+
echo "You can use the same token you used for bootstrap."
176+
echo ""
219177

220-
echo "Watermark '${PRIVATE_WATERMARK_TAG}' set in ${PRIVATE_REPO} -> ${SENTINEL_COMMIT}."
221-
222-
# ---------------------------------------------------------------------------
223-
# Cleanup.
224-
# ---------------------------------------------------------------------------
225-
rm -rf "$SNAPSHOT_DIR" "$WORK_DIR"
178+
# Public repo watermark: points to the bootstrap commit.
179+
echo " # Set watermark in public repo (private-to-public direction)."
180+
echo " PUBLIC_HEAD=\$(gh api /repos/${PUBLIC_REPO}/git/ref/heads/${DEFAULT_BRANCH} --jq '.object.sha')"
181+
echo " gh api -X POST /repos/${PUBLIC_REPO}/git/refs -f ref=refs/tags/repo-sync/watermark/private-to-public -f sha=\${PUBLIC_HEAD} 2>/dev/null \\"
182+
echo " || gh api -X PATCH /repos/${PUBLIC_REPO}/git/refs/tags/repo-sync/watermark/private-to-public -f sha=\${PUBLIC_HEAD} -F force=true"
183+
echo ""
226184

185+
# Private repo watermark: a sentinel commit with the Repo-Sync-Origin trailer
186+
# so the public-to-private workflow knows the bootstrap commit is already synced.
187+
echo " # Set watermark in private repo (public-to-private direction)."
188+
echo " PRIVATE_HEAD=\$(gh api /repos/${PRIVATE_REPO}/git/ref/heads/${DEFAULT_BRANCH} --jq '.object.sha')"
189+
echo " PRIVATE_TREE=\$(gh api /repos/${PRIVATE_REPO}/git/commits/\${PRIVATE_HEAD} --jq '.tree.sha')"
190+
echo " SENTINEL=\$(gh api -X POST /repos/${PRIVATE_REPO}/git/commits \\"
191+
echo " -f message='repo-sync: bootstrap sentinel' \\"
192+
echo " -f tree=\${PRIVATE_TREE} \\"
193+
echo " -f 'parents[]=\${PRIVATE_HEAD}' \\"
194+
echo " --jq '.sha')"
195+
echo " # Note: the sentinel commit message should include the trailer."
196+
echo " # The above is simplified; see docs/TECH-DESIGN.md for the full trailer format."
197+
echo " gh api -X POST /repos/${PRIVATE_REPO}/git/refs -f ref=refs/tags/repo-sync/watermark/public-to-private -f sha=\${SENTINEL} 2>/dev/null \\"
198+
echo " || gh api -X PATCH /repos/${PRIVATE_REPO}/git/refs/tags/repo-sync/watermark/public-to-private -f sha=\${SENTINEL} -F force=true"
199+
echo ""
200+
echo "--- Cleanup (after setting watermarks): ---"
201+
echo ""
202+
echo " rm -rf ${WORK_DIR} ${SNAPSHOT_DIR}"
227203
echo ""
228-
echo "=== Bootstrap complete ==="
229-
echo "Public repo ${PUBLIC_REPO} has been initialized with a clean snapshot."
230-
echo "Watermarks have been set in both repos."
231-
echo "The regular sync workflows can now take over."
204+
echo "NOTE: Do not delete the directories above until you have pushed and set watermarks."

0 commit comments

Comments
 (0)