|
1 | 1 | #!/usr/bin/env bash |
2 | 2 | # Bootstrap script for repo-sync. |
3 | 3 | # |
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. |
6 | 7 | # |
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. |
9 | 12 | # |
10 | 13 | # Usage: |
11 | 14 | # ./scripts/bootstrap.sh \ |
|
22 | 25 | # |
23 | 26 | # Prerequisites: |
24 | 27 | # - 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). |
26 | 29 | # - The stripping tool must be installed (pip install -e . from the repo-sync repo). |
27 | 30 | # - gh CLI must be installed. |
28 | 31 | # - git must be configured with user.name and user.email. |
@@ -89,143 +92,113 @@ python -m repo_sync.strip.cli "${SNAPSHOT_DIR}" |
89 | 92 | echo "Clean snapshot generated at ${SNAPSHOT_DIR}." |
90 | 93 |
|
91 | 94 | # --------------------------------------------------------------------------- |
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. |
93 | 96 | # --------------------------------------------------------------------------- |
94 | | -echo "Step 2: Pushing initial commit to public repo..." |
| 97 | +echo "Step 2: Preparing bootstrap commit in public repo..." |
95 | 98 | WORK_DIR=$(mktemp -d) |
96 | 99 |
|
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 |
98 | 120 |
|
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. |
101 | 122 | cp -a "${SNAPSHOT_DIR}/." . |
102 | 123 | git add -A |
103 | 124 |
|
104 | | -# Check if there's anything to commit. |
| 125 | +# Check if there are any changes to commit. |
105 | 126 | 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." |
107 | 128 | popd > /dev/null |
108 | 129 | rm -rf "$SNAPSHOT_DIR" "$WORK_DIR" |
109 | | - exit 1 |
| 130 | + exit 0 |
110 | 131 | fi |
111 | 132 |
|
112 | 133 | git commit -m "repo-sync: initial sync from private repo |
113 | 134 |
|
114 | 135 | Repo-Sync-Origin: ${PRIVATE_REPO}@${HEAD_SHA}" |
115 | 136 |
|
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}" |
136 | 139 |
|
137 | 140 | popd > /dev/null |
138 | 141 |
|
139 | 142 | # --------------------------------------------------------------------------- |
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. |
142 | 144 | # --------------------------------------------------------------------------- |
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" |
158 | 162 | 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}" |
218 | 170 | 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 "" |
219 | 177 |
|
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 "" |
226 | 184 |
|
| 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}" |
227 | 203 | 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