xCloud engineering story · WordPress · Git automation
From an existing WordPress site to push-to-deploy
The story of turning a live WordPress site into a secure GitHub push-to-deploy workflow—without leaking OAuth tokens or losing control of partial failures.
01 · The innocent-looking button
Some of the hardest engineering problems arrive disguised as a button.
Recently, while working on xCloud, I worked on a feature with a wonderfully simple promise: take an existing WordPress site, create a private GitHub repository for it, and make future pushes deploy automatically.
From the user’s perspective, the journey should be almost uneventful. Connect GitHub. Choose a repository name. Review a few ignore rules. Click one button.
That is exactly how good product automation should feel. Unfortunately, the machinery behind that button had other plans.
existing WordPress site
↓
connect GitHub
↓
private repository + push-to-deploy The site already existed. It had files, uploads, plugins, themes, configuration, and production traffic. What it did not have was Git history, a remote repository, a deployment key, a webhook, or a safe path from a developer’s laptop back to the server.
The task was not really “run git init.” It was to create a trustworthy bridge between three separate systems without dropping credentials—or the site—into the water below.
02 · What one click really meant
The workflow crossed three systems, and every one could fail independently.
The application had to create and configure resources through GitHub. The live server had to inspect an existing filesystem, initialise a repository, create the first commit, and push it. Then the deployment layer had to connect the webhook and deploy key that future pushes would use.
This is where the feature stopped looking like a form submission and started looking like a small distributed system. GitHub might accept the repository creation while the server became unreachable. The server might push successfully while webhook configuration timed out. A worker might disappear after changing real external state but before recording that it had done so.
A single success flag could not describe any of that. We needed to know which parts of the world had already changed.
03 · The tempting shortcut
The obvious implementation was also the one most likely to age badly.
The first mental model was pleasantly direct:
create repository
git init
git add .
git commit
git push
configure deployment It looks reasonable because every individual command is reasonable. The problem appears in the space between them.
What happens if the user clicks twice? What if the repository name already exists? What if git push succeeds but the job times out before saving progress? What if a retry produces a second webhook? And most importantly, how does the live server authenticate that first push without permanently storing the user’s GitHub access token?
04 · The credential problem
The first push needed a powerful credential. The server was not allowed to keep it.
The connected GitHub account already had an OAuth token available to the application. That token could create the repository and authenticate the initial push. The easiest implementation would have placed it directly inside the Git remote URL.
https://[email protected]/account/repository.git It would also have left that token exposed to Git configuration, diagnostics, process arguments, copied commands, and possibly logs. Convenient, certainly. Secure, rather less so.
The better model was to treat the OAuth token as a disposable bootstrap credential. It was transferred in a temporary file with restrictive permissions, used for one authenticated Git operation, and removed whether the push succeeded or failed. The remote URL itself remained clean.
receive token → temporary file (0600) → authenticate one push
↓
always remove it
↓
continue with a deploy key We added cleanup at two levels. The remote script removed the file after use, while the outer job also attempted removal if execution failed before the script reached its own cleanup. Secrets have an unfortunate habit of surviving precisely the failure path nobody expected.
The same caution applied to other inputs. User-edited ignore rules travelled as encoded content instead of being interpolated into shell commands. Site identifiers were validated again before becoming filesystem paths. Commit identity was scoped to the one commit rather than written into permanent server configuration.
05 · When failure leaves things behind
The hardest failures were not total failures. They were partial successes.
A clean failure is almost polite. Nothing changed, so the operation can report the error and try again later. External APIs are rarely that courteous.
A request can time out after GitHub has already created the repository. A push can reach the remote before the worker records success. A webhook can exist even though the final setup screen says the operation failed.
Surviving state
The repository exists, but the push failed
A retry must recognise the repository as its own work instead of trying to create it again.
Surviving state
The push succeeded, but the webhook failed
The code is already real on GitHub. Recovery should continue from that point, not rebuild history.
Surviving state
A deploy key already exists
The system should reconcile the expected key instead of quietly producing duplicates.
Surviving state
A worker disappeared halfway through
Stale progress must eventually expire so the site is not locked behind an eternal “in progress” state.
This changed how I thought about retries. A retry was not “run the same job again.” It was “inspect the current world, prove which resources belong to this setup, and resume from the first incomplete stage.”
If an existing repository matched the connected account and the workflow’s expected state, it could be reused. If it did not, setup stopped with a precise error. Automation should be helpful, but it should never become confident enough to adopt or overwrite an unrelated repository.
06 · Turning steps into state
Once every stage was explicit, recovery stopped being guesswork.
We modelled the setup as a queued workflow and persisted its business stage separately from verbose execution logs. That gave both the system and the interface a shared language for progress.
A site-level lock prevented two setup attempts from racing each other. If a worker vanished, stale in-progress state could expire deliberately instead of blocking the site forever. We also avoided blind automatic retries for steps that were not guaranteed to be idempotent.
Instead, the user saw the last meaningful stage and received an explicit retry action. “Repository created; initial push failed” is useful information. “Something went wrong” is merely a sentence wearing a warning colour.
07 · After the first push
The OAuth token opened the door once. It did not become the house key.
Bootstrap and everyday deployment have different security needs. The initial setup required account-level permission to create and configure GitHub resources. Normal deployments did not.
After the first push, a repository-scoped deploy key became the server’s long-term credential. A webhook notified the platform when the chosen branch changed. The broad OAuth token returned to the control plane and never became permanent server configuration.
developer pushes a commit
↓
GitHub sends a webhook
↓
deployment pipeline starts
↓
server reads its repository with a deploy key From that moment forward, the bootstrapped site behaved like any other Git-connected site on xCloud. The unusual credential existed only long enough to create the ordinary workflow.
08 · The value of saying no
A reliable first version needed fewer promises, not more switches.
Repository automation becomes combinatorial very quickly: multiple Git providers, organisations, public and private visibility, arbitrary branches, existing histories, monorepos, and different application layouts.
Trying to support all of those combinations immediately would have made ownership checks, recovery rules, and credential policy much harder to reason about. So the first version chose deliberate boundaries: one provider, private repositories, one expected branch, and site types whose filesystem shape we understood.
Unsupported cases failed before remote state changed. That was not a lack of ambition. It was how the security and retry model remained explainable.
09 · What the user finally sees
After all that engineering, the finished experience became almost boring.
The user connects a GitHub account, confirms a repository name, reviews ignore rules, and starts the setup. The interface shows useful progress. If something fails, it says where. If the user retries, the system continues without duplicating or stealing resources.
Before
A manual journey through three tools
Create the repository, connect through SSH, initialise Git, arrange credentials, push the site, then configure deployment.
After
One guided action with visible progress
The platform coordinates the same work while keeping credentials temporary and partial failures recoverable.
That quiet result is the point. The complexity did not disappear; it became organised enough that the user no longer had to carry it.
10 · What this changed for me
One-click features should remove work without hiding consequences.
This project changed the way I think about automation that crosses system boundaries. The visible action may be small, but its design must account for every resource that can outlive the request.
- Persist progress, not secrets. Durable state should explain what happened without retaining the credentials used along the way.
- Design recovery before celebrating the happy path. Partial success is normal when several external systems are involved.
- Separate bootstrap access from everyday access. A powerful temporary credential should not become permanent infrastructure.
- Validate again at every trust boundary. A safe web input can become an unsafe path, command argument, or remote resource later.
- Make state visible in product language. Users need to know what happened and what they can do next.
In the end, the most satisfying part was not the repository creation or even the first successful deployment. It was watching a complicated chain of credentials, API calls, server commands, and recovery decisions collapse into an experience that felt obvious.
That is often the strange reward of platform engineering: when the system is finally designed well, the user barely notices it was difficult at all.