Three studios. One build script. A lot of trust issues.
It started as a practical move. Two friends running small indie teams wanted to stop reinventing the wheel every time they shipped a patch. They pooled their build configs into a shared repo, invited a third studio they barely knew, and waited for the magic of collaboration to kick in. Instead, they got late-night Slack messages about broken pipelines, whispered doubts about who owned what, and a surprising discovery: the script itself was the easy part. What followed changed how they hired, how they trusted each other, and why they finally started enjoying their work again.
Who Needs This and What Goes Wrong Without It
The pain of maintaining solo build scripts
Every studio I have visited starts the same way. One engineer writes a build script for Project A. It works. Six months later, Project B appears and that same engineer—or someone new—writes a second script from scratch because the first one is tangled in project-specific paths and undocumented assumptions. Now you have two scripts, two sets of bugs, and zero shared knowledge. The catch is that neither team trusts the other's script, so they keep their distance. That sounds fine until someone quits. Suddenly no one knows why the CI pipeline expects a certain environment variable or which Python version the deploy step actually needs. You lose a day. Then another. Honest—I have watched three-person teams spend an entire sprint reverse-engineering a shell script a former intern left behind. The pain is not technical; it's social. When every project hoards its own tooling, hiring a new person means hours of context transfer just to get a build running. That hurts.
When shared infrastructure magnifies trust issues
Shared scripts don't create trust problems. They expose them. One studio in this story had two teams that refused to touch each other's build files. The mobile team insisted on Docker. The web team hated Docker. Every shared script attempt ended in a passive-aggressive comment thread about which base image to use. The real issue was not the container format—it was the unspoken belief that the other team would break things and not fix them. Most teams skip this: they assume a shared script is a technical artifact. It's not. It's a social contract. If you can't agree on a single `build.sh` that both sides can modify without fear, you probably can't agree on deployment windows either. What usually breaks first is not the code; it's the willingness to say "I broke it, I will fix it." Without that, shared infrastructure becomes a blame magnet. That's why hiring for tooling roles becomes harder—candidates smell the friction during interviews and walk.
Why hiring for tooling roles becomes harder
I talked to a lead who lost two strong DevOps candidates last year. Both asked the same question during the interview: "How many build scripts do you maintain?" When the answer was "One per project, maybe seven total," both declined. The reason? They didn't want to spend their days untangling duplicated work that a shared script could solve. The trade-off is real: solo scripts feel safe because they avoid group politics, but they make the tooling role feel like a janitorial job. The candidate wants to build leverage, not patch six different Makefiles. A shared script, even an imperfect one, signals that the studio values consistency over fiefdoms. It tells the hire: "You will work on one thing that everyone uses, and your changes matter immediately." Without that signal, you attract only the people who don't mind cleaning up after others—and that's a shrinking pool.
We had five build scripts. None of them worked on my machine. I spent day one rewriting one that worked for everyone. Day two we argued about YAML. Day three I updated my resume.
— Studio engineer describing their onboarding week, quoted from a private retrospective
The blunt truth: a shared build script is not the goal. The goal is a team that can trust each other enough to share the sharpest tool in the shed. If you skip the trust part, the script becomes another wall. If you build it together, it becomes a handshake. Most teams pick the wrong starting point—they open a text editor instead of a conversation. That's why this chapter exists. You need to know who needs this, and what goes wrong without it, before you write a single line of Bash.
Prerequisites You Should Settle First
Establishing a shared vocabulary around builds
Before one line of shell script hits a repository, three studios learned the hard way that words like “broken build” mean wildly different things to different teams. Studio A considered a build broken only if the artifact failed to compile; Studio B called it broken the moment a test suite returned a single warning they hadn’t seen before; Studio C’s lead declared a build “dead” if the deployment pipeline took longer than eight minutes. That sounds fine until you share a script—then one team’s “minor failure” becomes another team’s blocked release. The fix? A shared vocabulary written down before the script existed. Define terms like “green build,” “acceptable risk,” and “urgent fix” as concrete thresholds, not feelings. I have seen teams waste two days arguing over whether a linting error counts as a build failure—don’t be that group.
The catch is that vocabulary alone won’t stick unless each studio’s lead person sits in the same room (or Zoom) and agrees to those definitions publicly. Get that buy-in before you commit a single config file. One lead who shrugs and says “we’ll fix it later” will silently override every agreement the moment the script breaks on a Friday night.
Getting buy-in from each studio’s lead
Most teams skip this: they assume a shared tool is a technical problem. Wrong order. The real prerequisite is a conversation where each studio’s technical lead answers one question: “If this script fails and blocks your deploy for a day, do you still want to be part of the shared project?”. That question surfaces unspoken fears—loss of autonomy, blame, extra work for juniors who didn’t ask for this. One studio nearly pulled out because their lead worried the script would become a gatekeeping mechanism controlled by the fastest-moving team. The fix was a simple rule: any studio can override the shared script locally without notifying the others, as long as they document the change within 48 hours. Not perfect—but it kept three proud teams in the room.
What usually breaks first is trust in the failure-handling process. A junior developer on Studio B pushed a change that broke the shared script for everyone. The other two studios wanted a rollback mechanism with an emergency call—Studio B’s lead argued that speed of iteration mattered more. The resolution was a two-tier rule: for production blocking failures, any studio can force a rollback by pinging a shared Slack channel and waiting 15 minutes for objections. No objections, rollback happens. It sounds bureaucratic, but I have seen this exact pattern prevent two weeks of resentment. Get the failure-responsibility plan in writing—who owns the fix, what the timeout looks like, and when a rollback is mandatory, not optional.
Deciding on a source of truth
Git repo? CI service? A shared folder on Dropbox (please no)? The three studios chose a single private GitHub repository with branch protection rules, but only after an awkward debate about ownership. Studio A wanted the repo under their org because “we wrote the first draft.” Studio C refused because that gave Studio A unilateral control over merges. The compromise: a neutral GitHub org created specifically for the shared script, with all three leads as admin-level owners. That move cost thirty minutes of setup time but saved months of “who approved that merge?” hell. The source of truth must feel neutral—if any one studio hosts it, the other two will always feel like tenants.
Odd bit about programming: the dull step fails first.
Odd bit about programming: the dull step fails first.
“We spent more time deciding where to put the repo than writing the script. Stupid, maybe. But that stupid hour saved us three stupid fights later.”
— Studio C’s lead, reflecting on the process six months in
The trick is to also decide what doesn’t live in that shared source of truth. Secrets, per-studio environment variables, and API keys stayed in each studio’s own vault. One shared repo, three local overlays. That boundary—shared logic vs. local context—prevented the script from becoming a blob of conditional statements trying to handle every studio’s weird CI quirks. Start there, and the technical setup in the next section actually has a fighting chance.
The Core Workflow: How They Built and Shared the Script
Step 1: Designing a modular build script
The first studio—the one that had been burned by a monolithic Makefile before—insisted on modules. Not microservices madness, just three discrete files: one for fetching dependencies, one for compiling, one for packaging. Each file could run solo. That decision saved them later. The second studio pushed back: “Why not one script with flags?” They tried that on a Tuesday. By Thursday the flag combinations had exploded, and someone had accidentally passed a production flag to a dev build. Three hours of rollback. The modular layout stuck.
The tricky bit was naming conventions. Studio A used build_linux.sh, Studio B used compile-mac.rb. You see the problem. They settled on a flat scripts/ directory with explicit prefixes: 01-fetch-deps.sh, 02-compile.sh, 03-package.sh. Boring. Reliable. The catch is that boring scales—when Studio C joined, they added 04-test.sh without touching lines 1–3. That was the moment trust started forming.
‘We kept arguing about which language to script in. Then we realized the script wasn’t the product—the output was. Bash won because everyone could read it.’
— Build lead from Studio B, retrospective chat
Step 2: Setting up a shared CI pipeline
They didn’t share the script by email. No Slack copy-paste. They pushed it to a private GitHub repo that all three studios could write to—but only after a second pair of eyes reviewed. That guardrail caught a stupid bug on day one: someone had hardcoded /Users/alice/tools into the path resolver. The pipeline ran on three different CI systems (GitHub Actions, GitLab CI, and a crusty Jenkins box). Each system needed a thin wrapper that called the same 02-compile.sh. We fixed this by declaring an environment contract: the script expects $BUILD_ROOT, $TARGET_ARCH, $OUTPUT_DIR. If those aren’t set, it fails fast with a readable error. No silent fallbacks.
What usually breaks first is the CI agent itself. Studio C’s Jenkins agent ran an older glibc. The compile script used a sort -V flag that didn’t exist on that host. That cost them an afternoon—but because the script was modular, they patched 01-fetch-deps.sh to check tool versions before touching source code. The check isn’t fancy: three if statements and an exit code. Most teams skip this step. Then they wonder why builds fail inconsistently across machines.
Honestly—the CI pipeline felt like over-engineering for the first three weeks. “We’re three small studios, why do we need a review gate?” But the moment someone pushed a commit that silently broke Studio A’s build while Studio B’s still passed, the gate paid for itself. Without it, the blame game would have started. With it, they had a log entry: commit a3f2c, merged at 14:23, broke test suite for ARM targets. No finger-pointing, just a revert.
Step 3: Testing across all three studios’ projects
They ran the script against each studio’s real codebase every night. Not a toy example—production projects. Studio A’s game engine. Studio B’s rendering library. Studio C’s audio middleware. The first week, Studio C’s build failed because their project used a custom CMAKE_TOOLCHAIN_FILE that the shared script didn’t propagate. They added an optional EXTRA_CMAKE_ARGS variable. One line. That line made the script actually shared rather than imposed.
Some tests passed on two studios but failed on the third. That’s where the real learning happened. Studio A had a fast SSD; Studio B’s CI ran on network-attached storage with higher latency. The script’s cp commands timed out. They added a retry loop with exponential backoff—three lines in 01-fetch-deps.sh. Small changes, big trust dividends. The rhythm became: merge a change, wait for three green CI runs, then celebrate with a lazy emoji in the shared channel.
What about the days when all three failed? That happened twice. Once because a remote package registry went down. Once because someone updated the script’s shebang from /bin/bash to /usr/bin/env bash and two agents didn’t have that path. Each failure produced a new guard clause. By month two, the script had more defensive checks than features. That’s not bloat—that’s earned paranoia. Would you rather debug a three-studio build at 11 PM on a Friday? No. The script doesn’t care about your feelings. It checks, fails early, and prints exactly which variable is missing.
Tools, Setup, and Environment Realities
Choosing between GitHub Actions, GitLab CI, and self-hosted runners
The three studios started where most teams do: GitHub Actions. It was fine—until cross-studio secrets needed rotating. Studio A hit the rate limit on public runners during a weekend deploy. Studio B, stuck on GitLab self-hosted, faced a different pain: runner maintenance ate one engineer’s Friday every month. The switch came when Studio C introduced Windows ARM builds for their mobile toolchain. GitHub Actions didn’t support it natively. GitLab CI did—but only with a paid tier. The fix? A hybrid. They kept GitHub Actions for Linux PR checks and moved release pipelines to a single self-hosted runner behind a Tailscale tunnel.
Flag this for game: shortcuts cost a day.
Flag this for game: shortcuts cost a day.
That sounds fine until you account for drift. One studio pinned their runner image to Ubuntu 20.04; another used the latest tag. A build succeeded in one environment and failed in the next. They learned the hard way: pin everything—OS version, Docker image hash, even the shell version. The shared script now starts with a function that checks environment parity and exits with a cryptic error if mismatched. Frustrating? Yes. But it caught the week-long debug session they’d had before.
Managing secrets and environment variables across studios
Secrets were the first thing to break. Studio A stored API keys in GitHub encrypted secrets. Studio B used GitLab CI variables with environment scopes. Studio C stuffed everything into a .env file committed to a private repo—don’t do that. The shared script needed three separate secret injection paths. One engineer joked they’d rather hand-write secrets in a shared Google Doc. Almost tried it. What actually worked: a small wrapper script that reads a local .secrets.yaml (gitignored) and maps variable names per platform. The wrapper is only 47 lines of Bash. It rejects any secret that isn’t defined in the remote CI variables list. A missing key halts the build before it touches the real toolchain.
Most teams skip this step. They assume “the secret exists” and move on. The catch is that secrets expire, or a new junior engineer forgets to add one to their fork. The wrapper script now includes a dry-run mode. Run it, and it prints which secrets resolve and which are missing—without leaking values. Studio C’s lead called it “the boring thing that saved a Friday.”
Handling platform-specific build steps (Windows, macOS, Linux)
The script had to compile a Rust crate, download a Node module, and sign a macOS binary. That’s three platforms, three toolchain installers, three signing workflows. Studio A’s macOS runner used brew; Studio C’s Windows runner used Chocolatey. The shared script swelled with if [[ "$OS" == "Windows" ]] blocks—ugly but functional. Then Studio B added an Apple Silicon runner. The conditionals doubled.
The fix was brutal but honest: extract platform logic into separate YAML include files. The main script calls include: ci/${{ runner.os }}.yml. Each platform file owns its dependencies and signing steps. A developer touching only Linux never sees the macOS notarization code. This split also made debugging easier—when Windows signing failed, they fixed ci/Windows.yml without touching the other two. The cost? Three files to maintain instead of one. Worth it when a cURL flag differs between curl on macOS and curl on Windows (yes, it does).
“We spent two days debugging a PATH issue on Windows that didn’t exist on Linux. Turned out the script sourced .bashrc—Windows doesn’t have one.”— Engineer from Studio B, post-mortem notes
Now the script explicitly checks for Windows and sources the profile.ps1 equivalent. Small change. Huge silence in the chat afterward.
Variations for Different Constraints
Two-person team with no dedicated DevOps
This is the setup I see most often: a tiny studio where everyone wears four hats already. You're the designer, the build manager, and the person who resets the printer. The shared script works here only if you kill every unnecessary abstraction. No Docker, no multi-stage YAML, no secret rotation that requires a PhD. Strip it to a single shell file that calls three commands in order. The catch? One person becomes the accidental gatekeeper. I watched a two-person team burn three days because the only person who understood the script took a long weekend. Their fix was brutal but effective: write a ten-line README that covers the one failure mode they actually see—permission denied on the output directory. That's it. The trade-off is speed against resilience. You gain a repeatable build in under twenty minutes. You lose the ability to onboard a third person without hand-holding. That's fine until you hire.
Three studios with conflicting platform preferences
Now scale the tension. Studio A ships on Steam. Studio B targets consoles. Studio C lives entirely on itch.io with DRM-free ZIPs. The shared script needs to produce three different output shapes from the same source tree. Most teams try to solve this with flags. —if platform console then sign with cert X. That works until a flag interacts with another flag and your Mac build inherits the Windows icon. The trick is to invert the logic: write one script that never checks the platform at all. Instead, let each studio maintain a tiny env file that defines four variables: output format, compression tool, signing key path, and notification webhook. The script reads those, nothing else. A studio that wants to keep some builds private simply leaves the webhook line blank. That sounds fragile—and it's, until you realize the alternative was a 400-line switch statement nobody dared touch. The pitfall here is trust: one studio can accidentally commit their private signing key into the shared repo. The fix is a pre-commit hook that screams if it sees a .p12 or .env file. I have seen this blow up exactly once, and that studio now runs a separate, non-shared script for their confidential builds. Partial trust is still trust—you just draw a line and enforce it with tooling, not hope.
A studio that wants to keep some builds private
This is the hardest variant. One studio in the trio built a companion app that they distribute only to paying beta testers via a private CDN. They could not share the upload credentials with the other two studios—contractual, not petty. The naive solution was to fork the shared script. Don't. Forks rot fast. Instead, they added a single optional step at the end of the pipeline: a hook script that runs only if a file named .upload.sh exists in a special directory. The hook is gitignored by default. The studio with private builds places their upload script there; the other two leave it empty. The shared script never sees the credentials.
We spent four hours arguing about security models. Then we wrote three lines of bash and went home.
— engineer at the private-build studio
The downside is that the hook runs with full environment access—a mistake there could dump tokens into stdout. The studio fixed this by running the hook inside a Docker container that mounts only the build artifact, not the source tree. Overkill? Maybe. But it let them keep the shared script without leaking anything. That's the real lesson: adapt the script to your constraint, not the other way around.
Pitfalls, Debugging, and What to Check When It Fails
The 'it works on my machine' problem across studios
You share a script. Studio A runs it—green builds, happy faces. Studio B runs the same commit—red wall of errors, blocked deploys. The immediate instinct is a blame shuffle: wrong Node version, different shell, missing env var. But the real failure was invisible. The three studios eventually traced it to a .npmrc file that Studio A had committed six months ago and everyone else had ignored. The script assumed registry credentials lived in environment variables. Studio A’s developer had them hardcoded in a local config that never got pushed. That sounds fine until the script hits Studio C’s CI runner, which runs as root and can’t read the user-level config at all. The fix wasn’t a technical one—it was a communication rule: before the script runs anywhere, each team must declare every implicit dependency aloud in a shared doc. Credentials, shell aliases, editor plugins, the works. Embarrassing to write down. Saved them three days of head-scratching.
Field note: game plans crack at handoff.
Field note: game plans crack at handoff.
When a broken script blocks all three teams
The script was supposed to be a shared lever. Then a bad refactor locked every pipeline simultaneously. Studio B’s junior engineer had pushed a change to the build matrix that looked right but silently dropped the Linux arm64 target. Nobody caught it because the PR review was a rubber stamp—everyone assumed the script was “working.” It was not. Every deploy after 4 p.m. that Friday failed. The recovery was painful: roll back the script, run three manual builds, then call a 30-minute huddle where each studio explained what they needed the script to do, not what it should do. That distinction mattered. The script’s author had written it for the happy path. Studio C needed a custom artifact naming convention. Studio A needed a different compression tool. None of that was in the initial spec. The lesson: a shared script isn’t shared until every upstream dependency and downstream consumer has signed off. One team’s optimization is another team’s outage.
“We stopped debugging the code and started debugging the conversation. That’s where the real bug lived.”
— lead engineer, Studio B, on the Friday afternoon call that changed their review process
How they debugged a silent failure in the deployment step
The builds passed. Tests passed. But the deployment step emitted zero errors and zero artifacts. A ghost. The script ran to completion—no crash, no warning—and uploaded nothing to the staging bucket. Studio A’s ops person noticed the empty directory after an hour of monitoring. The usual checklist—check logs, check permissions, re-run—yielded nothing. What broke the logjam was a simple question: “What changed between the last good deploy and this one?” Answer: nothing in the script, but someone had rotated the S3 credentials and forgot to update the shared vault. The script didn’t fail; it succeeded at doing nothing. Silent failures are the worst because they feel like success. The studios fixed this by adding a single line after each deployment step: assert at least 1 file exists. But the deeper fix was cultural—they built a habit of asking “what didn’t break” after every shared change, not just “what broke.” Most teams skip this. That hurts.
The trick is to treat every silent failure as a signal, not a fluke. Check the build size. Check the output hash. Better yet—watch the logs in real time during the first run after a script change. One studio wrote a small wrapper that printed “DANGER: zero artifacts produced” in red block letters. Overkill? One near-miss later, nobody complained. That is debugging as a communication pattern, not a technical autopsy. The script was the symptom. The silence was the disease.
FAQ: What the Studios Wish They Knew
How often should we update the shared script?
Every two weeks felt right — until it didn’t. One studio pushed an update every Monday morning, another waited until a build flat-out broke. The catch is simple: frequency reveals trust. Too fast, and teams stop reading changelogs. Too slow, and the script ossifies around one studio’s forgotten edge case. What the studios finally landed on: a single stable release per sprint, with a second hotfix channel for urgent patches. They reserved the hotfix for security bumps and dependency collapses only — not for “I prefer this linter rule” whims. That distinction saved them from merge fatigue. Most teams skip this: tag each release with the studio that sponsored the change. A month later, when the seam blows out, you know who to talk to — not blame, just talk.
Who pays for CI minutes when one studio causes a spike?
Honestly, nobody had a good answer for the first three months. Studio A ran heavy integration tests every commit; Studio B’s tiny unit suite barely dented the monthly cap. Resentment crept in quietly. One studio lead put it bluntly: “We split the bill equally, but we didn’t split the pain equally.” The fix they found was ugly but fair — a per-commit cost badge in the pull request comment. A bot scrapes the CI runner’s duration, multiplies by the cloud rate, and posts a dollar figure. Not exact, but visible. When people see a $0.42 badge versus a $4.10 badge, they self-regulate. That said, don’t let the bot replace a human conversation. One studio accidentally ran a 45-minute end-to-end suite ten times in an afternoon — the badge showed $37. Instead of a tense Slack thread, they called a five-minute huddle and agreed to tag expensive jobs with a costly label. Transparency beat accounting every time.
“We stopped worrying about who caused the bill and started asking what the bill bought us.”
— lead engineer, Studio C, after the sixth month retrospective
Can we fork the script and still stay aligned?
You can. You probably shouldn’t — at least not without a short leash. One studio forked because they needed a Python 3.8 dependency the others hadn’t adopted. Within six weeks their fork had drifted by 140 lines, and upstream patches no longer applied cleanly. The painful lesson: a fork is a marriage you stop dating. What worked better was a feature-flag approach inside the shared script itself. They inserted a —target-studio argument that conditionally loaded different toolchain versions. Fork avoided, alignment preserved. The trade-off? The script grew conditional branches, and readability suffered. But the alternative — three diverging scripts nobody could merge — hurt worse. If you absolutely must fork, set a four-week reconcile window. Mark your calendar. If the window closes unanswered, archive the fork or kill it. Otherwise you end up with what one team called “the silent drift problem”: everyone thinks they’re aligned until a cross-studio deployment fails at 3 PM on a Friday.
What usually breaks first is the unspoken stuff: whose responsibility to update the README, who decides when a breaking change is worth the disruption, and who absorbs the cost of a failed experiment. The studios learned to write those rules down — not as policy documents, but as a single pinned message in their shared channel. Short. Ugly. Honest. That message saved more rebuilds than any script ever did. Your first move this week: write that message. Then update the script.
What to Do Next: Your First Week With a Shared Script
Audit your current build pipeline for duplication
Monday morning. Coffee in hand—open every CI config file your team touches. I am not kidding: grep for the same compiler flags, the same artifact paths, the same environment variable exports scattered across three repositories. One studio we worked with found seventeen identical lines in two separate YAML files. Seventeen. Nobody knew who owned them, and when a security patch hit, only half got updated. That is where trust starts to crack. Your job this week: find the seams. Map every build step to a source. If you can't point to a single script that defines a given flag, you have already lost control.
Most teams skip this because it feels like archaeology. Honest—it takes an afternoon, tops. Print the configs, spread them on a table, draw circles around repeats. Then ask: who changed this last? If the answer is “I don’t know,” you have found your first candidate for the shared script. Patch that one spot. Not the whole pipeline. One. Prove it works before you propose a grand unification.
Invite one other studio for a trial month
Cold email the lead engineer at a studio you respect—or, better, one you compete with. Scary? Sure. Here is what one studio did: they offered their linter configuration plus a half-day pairing session. No contracts, no NDAs. Just a shared Slack channel and a promise to break things together. The catch? They discovered their counterpart was using a patched version of a library that broke the shared script silently. That hurt. But finding it in week one saved them a production outage six weeks later.
A trial month exposes personality mismatches fast. Does the other team fix issues quickly, or do they blame the script? Do they add hacks instead of opening a pull request? You learn more from one broken Tuesday than from a month of planning. Set a recurring 30-minute sync—not a status meeting, a “show me what you broke” session. Keep it raw. No slides. One concrete rule: if a build fails for more than four hours, the person who broke it buys coffee for the whole channel. That alone cuts blame spirals.
“We spent three months polishing a shared script nobody used. The trial month taught us that trust is faster than perfection.”
— build lead, mid-size game studio (anonymous by request)
Set a regular sync to review pain points
Wednesday, 11 AM. Same Zoom link. Start with the single worst build failure from the past week. Not the dashboard, not the metrics—the one thing that made someone swear. Name it. Fix it during the call if it's small. That's the rhythm. We fixed a broken environment variable once in under four minutes because both studios had their terminals open. That kind of velocity changes how people feel about collaboration.
The tricky bit is keeping the sync from rotting into a status roundtable. Force a rotating chair: each week a different person picks the pain point. No agenda beyond that. If nothing broke, talk about what almost broke. One studio realized their shared script silently swallowed a deprecation warning for two months—nobody noticed because the logs were never checked. A five-minute review would have caught it in week one. So: Wednesday, 11 AM, one problem, fix or bin it. Three strikes on the same issue means the script needs a rewrite. Don't let politeness delay the inevitable. Your first week is about building the habit, not the perfect procedure. Do the audit, send the invite, start the sync—then get out of your own way.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!