M
Writing

Product engineering · WordPress · Git automation

From an existing WordPress site to push-to-deploy

How a secure Git bootstrap creates a private repository, pushes an existing WordPress site, and enables deploys without exposing OAuth tokens.

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

01 · The starting point

The site already existed. Its delivery workflow did not.

A one-click WordPress installation can be perfectly healthy and still have no repository, no deployment key, no webhook, and no safe route from a developer’s laptop to production.

Adding Git manually meant opening another browser tab, creating a repository, connecting to the server, initializing the working tree, choosing what not to commit, arranging credentials, and finally wiring push-to-deploy. For experienced engineers, this is familiar work. For many site owners and agencies, it is a small expedition.

The product goal was simple to describe: take an existing WordPress or PHP site and make it Git-managed with one guided action. The engineering goal was less forgiving: complete a multi-system workflow without leaking a credential, creating duplicate resources, or leaving the site trapped halfway through setup.

Before

Repository setup required Git fluency

Users moved between a hosting panel, GitHub, and SSH while manually carrying configuration from one system to another.

Target experience

Choose, confirm, and let the system coordinate

The user selects an account and repository name, reviews ignore rules, then receives a normal push-to-deploy site.

02 · The workflow

“Create repository” was only one frame in the film.

The feature crossed three trust boundaries: the application, a remote site server, and GitHub. Each system could succeed while the next failed. Treating the workflow as one controller request would hide progress, make timeouts likely, and leave recovery to guesswork.

I modelled setup as a queued orchestration with explicit stages. The stage was persisted separately from verbose logs so the interface could say what was happening—and, on failure, where it stopped.

01 ValidateConfirm the site, user, repository name, and requested ignore rules.
02 ReserveAcquire a site-level lock and record that setup has begun.
03 CreateCreate or safely reuse a private repository owned by the connected account.
04 SnapshotInitialize the existing site, apply ignore rules, and create the first commit.
05 PushUse a short-lived credential to deliver the initial branch, then remove it.
06 ConnectInstall a deploy key and webhook so future pushes use the normal deployment path.
07 CompleteSwitch the site into its standard Git-managed state.
The important state was not “the job ran.” It was “the repository, initial branch, deploy key, webhook, and site configuration now agree.”

03 · The credential boundary

The initial push needed an OAuth token. The server did not get to keep it.

Creating a repository through an API is straightforward. Pushing the existing site into that repository is where credential handling becomes interesting. The remote server needs temporary permission to perform the first authenticated push, but it should not become a permanent home for the user’s OAuth token.

The safe design treated the token as a single-use delivery credential. It was transferred in a restrictive temporary file, consumed by one Git operation, and removed by cleanup logic regardless of the outcome. The repository remote itself remained token-free.

Credential rule

Never place the token in the remote URL

URLs are copied into Git configuration, command histories, diagnostics, and error messages far too easily.

Credential rule

Never persist it in job state

Queued payloads, database metadata, and retry records live much longer than the initial push requires.

Credential rule

Use a narrow temporary credential file

Transfer it with restrictive permissions, use it for one operation, and remove it on success or failure.

Credential rule

Keep an outer cleanup path

If execution fails before the in-script cleanup is registered, the orchestrator must still remove the credential.

receive secret → write mode 0600 → authenticate one push
      ↓ success                         ↓ failure
remove immediately               remove immediately
      ↓                                 ↓
continue with deploy key         retain resumable setup state

There were smaller shell boundaries too. User-edited ignore rules could not be interpolated directly into a command, so the content travelled as encoded data and was decoded into a file. Site identifiers were validated again at the job boundary before becoming filesystem paths. Commit identity was scoped to the single commit rather than written into persistent Git configuration.

04 · Retries and recovery

Retrying safely meant understanding what had already become real.

A remote repository might be created successfully just before a network failure. A webhook might exist while the first push did not. A worker might disappear while the interface still says setup is running. “Start again” cannot mean “blindly repeat every call.”

The workflow used a lock scoped to the site to prevent double submission. On retry, each external resource was discovered and validated before the system decided whether to reuse, reconcile, or reject it.

Retry decision

Repository already exists and is ours

Reuse it after validating that its state is compatible with resuming the setup.

Retry decision

Repository exists but is not safe to reuse

Stop with a precise error. Automation should never adopt or overwrite an unrelated repository.

Retry decision

Webhook or deploy key already exists

Reconcile it to the desired state instead of creating duplicates.

Retry decision

A previous worker disappeared

Expire stale in-progress state and allow a deliberate retry rather than leaving the site locked forever.

The job deliberately avoided automatic retries for steps that were not universally idempotent. Instead, the user received a failure state with the last completed stage and an explicit retry action. It is less theatrical than pretending every failure heals itself, but considerably kinder to repositories.

06 · Deliberate boundaries

A narrower first version produced a more reliable system.

Repository automation becomes combinatorial quickly: multiple providers, organizations, public and private visibility, arbitrary default branches, existing histories, monorepos, and different site types. Supporting all of it at once would make recovery policy nearly impossible to explain.

The first version chose one provider, private repositories, one default branch, and site types whose filesystem shape was understood. Unsupported cases failed before remote state was changed. These were not missing flourishes; they were boundaries that made the security and retry model auditable.

Good automation is not measured by how many switches appear in its form. It is measured by how confidently it can complete—or safely refuse—the operation behind them.

07 · Lessons carried forward

One click should remove work, not conceal risk.

The final experience was intentionally small: choose a connected account, confirm a repository name, review ignore rules, and begin. The complexity stayed behind the interface, but its state did not disappear.

  • Persist business progress, not secrets. Durable state should explain what happened without retaining credentials used along the way.
  • Design the retry before the happy path ships. External systems will produce partial success; the product needs a policy for every surviving artifact.
  • Separate bootstrap credentials from runtime credentials. Broad account access should not become permanent server configuration.
  • Validate again at every trust boundary. A value accepted by the web request is still untrusted when it becomes a path or shell input later.
  • Make failure visible and actionable. “Failed at repository push” is useful; “something went wrong” is merely decorative.

The feature was never really about running git init. It was about turning a manual sequence—with credentials, remote state, and awkward recovery—into a product workflow that a user could trust without first becoming its operator.