Clone or refresh a repository fleet
Every repository your build needs, present on disk and current, cloned if missing, fast-forwarded if already there, driven from a list you maintain rather than a script you edit.
When you finish you will have one flow that takes a machine with nothing checked out to a machine ready to build, and that is safe to re-run any time.
Why bother
Section titled “Why bother”A fleet of repositories is the point where “just clone it” stops scaling.
| Without this | With this |
|---|---|
A README with thirty git clone lines that drifts the week it is written | One list, read by the flow, reviewed like code |
| A new machine takes an hour of copy-paste | One command, and you get coffee |
| Nobody is sure whether their checkout is current | Re-running is the answer, and it is cheap |
| A new repository is added and half the team never gets it | Add a row; everyone picks it up on their next run |
| One missing repository aborts the whole script | The run continues and tells you which one |
The list is the deliverable as much as the flow is. It becomes the answer to “what makes up this product”, in a file, with a history.
Before you start
Section titled “Before you start”| You need | Why |
|---|---|
| Read access to the repositories | Cloning |
| A working directory with room | Everything lands under it |
| A list of repositories | A CSV with a name and a URL |
Step 1: put the fleet in a list, not the flow
Section titled “Step 1: put the fleet in a list, not the flow”The flow should not change when the fleet does.
name,url,buildservice-alpha,https://git.example.com/org/service-alpha.git,TRUEservice-beta,https://git.example.com/org/service-beta.git,TRUEtool-gamma,https://git.example.com/org/tool-gamma.git,FALSEThe build column is not used here, but it will be by
Build every service from trunk: one list, several flows.
Step 2: declare the list as a table
Section titled “Step 2: declare the list as a table”name: refresh-fleetworkingdirectory: ~/workspacecontinueonerror: truevars: branch: maintables: repos: type: csv file: ./fleet.csvcontinueonerror: true matters. One unreachable repository should not stop the other twenty-nine
from updating; you want the run to finish and tell you what it could not do.
Step 3: clone what is missing
Section titled “Step 3: clone what is missing”A task with table: runs once per row, with each column available as a variable.
tasks: - name: clone table: repos check: | "{{name}}" !== "" shell: | if [ ! -d "{{name}}/.git" ]; then echo "cloning {{name}}" git clone --branch {{branch}} {{url}} {{name}} else echo "present: {{name}}" fiStep 4: fast-forward what exists
Section titled “Step 4: fast-forward what exists”Separate from cloning, because the failure modes differ: a clone fails on access, an update fails on local changes. Keeping them apart tells you which happened.
- name: update table: repos shell: | cd {{name}} 2>/dev/null || exit 0 git fetch --prune --quiet git checkout {{branch}} --quiet git pull --ff-only --quiet && echo "updated: {{name}}" \ || echo "SKIPPED {{name}} — local changes or diverged"--ff-only is the safety. It refuses to merge, so a repository with local work is reported and left
alone rather than silently altered.
Step 5: report
Section titled “Step 5: report” - name: summary shell: | echo "fleet under $(pwd):" ls -1d */ | wc -lThe finished thing
Section titled “The finished thing”name: refresh-fleetlist: trueworkingdirectory: ~/workspacecontinueonerror: truevars: branch: maintables: repos: type: csv file: ./fleet.csv
tasks: - name: clone table: repos check: | "{{name}}" !== "" shell: | if [ ! -d "{{name}}/.git" ]; then echo "cloning {{name}}" git clone --branch {{branch}} {{url}} {{name}} else echo "present: {{name}}" fi
- name: update table: repos shell: | cd {{name}} 2>/dev/null || exit 0 git fetch --prune --quiet git checkout {{branch}} --quiet git pull --ff-only --quiet && echo "updated: {{name}}" \ || echo "SKIPPED {{name}} — local changes or diverged"
- name: summary shell: | echo "fleet under $(pwd):" ls -1d */ | wc -lfunction main() { const repos = csv.read({ path: './fleet.csv' }).records; const cloned = [], updated = [], dirty = [];
for (const repo of repos) { const dir = `~/workspace/${repo.name}`;
if (!file.exists({ path: `${dir}/.git` }).exists) { const r = git.clone({ url: repo.url, directory: dir, depth: 1 }); if (!r.success) throw new Error(`clone ${repo.name}: ${r.error}`); cloned.push(repo.name); continue; }
// never discard someone's work to make a build succeed const st = git.status({ directory: dir }); if (!st.clean) { dirty.push(repo.name); log.error(`skipping ${repo.name}: working tree dirty`); continue; }
const r = git.pull({ directory: dir, remote: 'origin' }); if (!r.success) throw new Error(`pull ${repo.name}: ${r.error}`); updated.push(repo.name); }
log.info(`cloned ${cloned.length}, updated ${updated.length}, skipped ${dirty.length} dirty`); return { cloned, updated, dirty };}kis script run refresh-fleet.js --vars branch=maingit.status is what makes this safe, and it is the call with no natural flow equivalent, deciding
whether to pull is a branch, and branches are what scripts are for.
Either works here, and the script is arguably clearer: the clone-or-pull decision is a
conditional, which reads better in code than as a choice: node. Use the flow when this runs as
one stage of a larger build.
kis flow -f refresh-fleet.yamlTrack a release branch instead of the mainline:
kis flow -f refresh-fleet.yaml -v branch=release-1.1Verify
Section titled “Verify”The run log names every repository as cloned, present, updated or skipped. Anything marked
SKIPPED has local work, deal with those by hand, which is the point of not merging for you.
Adapt it
Section titled “Adapt it”| Change | Where |
|---|---|
| A different branch per repository | Add a branch column and use {{branch}} from the row |
| Shallow clones for speed | git clone --depth 1: fine for building, not for tagging |
| Run it much faster | See Build a fleet in parallel for the map pattern |
Related
Section titled “Related”- Build every service from trunk: the next step
- Data & Templating tasks: tables and CSV sources