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.
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.
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.
05 · Making deploys work
The OAuth token opened the door once. The deploy key handled ordinary traffic.
After the initial push, the site no longer needed the broad OAuth credential. Normal deployments could use a repository-scoped deploy key, while a webhook notified the hosting platform when the selected branch changed.
This separation reduced the long-term credential surface. The account token was used by the control plane to create and configure repository resources. The server received only the key required for its own repository. Future pushes followed the same deployment path as sites that had been Git-managed from the beginning.
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.
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.