M
Writing

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.

July 12, 2026 11 min read By Mahfuzur Rahman
xCloud WordPressGitHubPush-to-deploySecure automation

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.

01 xCloud control planeValidates the request, coordinates progress, and talks to GitHub.
02 GitHubCreates the private repository, deploy key, and webhook.
03 Live site serverTurns the existing files into a first commit and performs the initial push.
04 Deployment pipelineTakes over when later commits arrive on the selected branch.

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?

Automation is not finished when the happy path works. It is finished when an interrupted run can explain what survived and continue safely.

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.

01 ValidateConfirm the site, connected account, repository name, and ignore rules.
02 ReserveLock the site so two setup attempts cannot run at the same time.
03 CreateCreate—or carefully reuse—the private repository.
04 SnapshotInitialize the existing site and capture its first Git commit.
05 PushUse a temporary credential for the initial branch, then remove it.
06 ConnectInstall the deploy key and webhook used by normal deployments.
07 CompleteMove the site into its standard Git-managed state.

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.

The important result was never “the job completed.” It was “the repository, branch, deploy key, webhook, and site configuration now agree.”

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.

Good automation is measured less by how many options it offers and more by how confidently it can complete—or safely refuse—the operation behind them.

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.