An internal platform that a whole site logs into is production software, and production software has to be changed while people are using it. There is no quiet hour on a floor that runs shifts. So the release has to be routine enough to do often, and safe enough that a bad one doesn't cost anybody their afternoon.
The second problem is quieter and takes longer to notice. When users can't see what changed, every release is a rumour — a screen moved, a button behaves differently, and nobody can tell whether that was a fix, a fault, or their imagination. The same goes in the other direction: a request made in passing is a request that gets lost, and once people learn that, the operators who actually know what's wrong stop telling you.
- The system is in use during every shift; there is no window to take it down
- A release that fails must leave the site no worse off than not releasing at all
- A change nobody can see is indistinguishable from a bug
- A request with no visible state is a request that gets asked for twice, then abandoned
Two pipelines, one per branch, running on self-hosted runners on the machines they deploy to. A push to the test branch deploys to the test server; a push to the main branch deploys to production. They run the same script — the only meaningful differences are which branch is pulled and which machine the job is pinned to — so the test slot exercises the real deployment path rather than a rehearsal of it.
The deploy itself is deliberately blunt. The branch is downloaded as an archive over the API, the target directory is emptied — everything except the version- control and workflow directories — and the files are laid down fresh. That means a file deleted in the repository actually disappears from the server, instead of lingering as a stale module that still imports.
- The downloaded archive is checked for a ZIP signature before anything is extracted — a response that isn't an archive never reaches the extract step
- The deployment path must be explicitly configured; a blank one fails the job rather than writing somewhere arbitrary
- Running process IDs are captured before a single file is touched
- A page of the deployed branch's commit history is written into the deployed directory as a step of the job
Restarting is where an internal platform gets hurt, so that step carries most of the logic. The new process is started through the machine's own boot task; the job then watches for a process that isn't one of the ones it recorded at the start — ten checks, two seconds apart.
If a new process appears, the old ones are stopped and the deploy passes. If none appears inside that window and an old process is still serving, the job fails on purpose, says exactly why, and explicitly leaves the old process running. The failure mode becomes "the release didn't take", which somebody can look at in the morning, rather than "the site is down", which somebody has to fix now.
The check is on process identity rather than on a status message, because the thing that most often goes wrong in a deploy is that the new process exits during startup. A process that never came up cannot be mistaken for one that did. The tables behind this work are created and extended in place on every access — added columns and guarded indexes, never a drop or a rename — so an older process and a newer one can share the database during the handover without either of them tripping over the schema.
The version-history page inside the application reads the file the deploy wrote, not the hosted repository. That inversion is the point: the page describes the code that is actually on that machine, it keeps working when the network or the credential doesn't, and each server's changelog reflects its own branch. If the file is missing it falls back to a live query, so a box set up by hand still shows something.
Small things separate a page people trust from one they stop opening. The file is produced by a PowerShell step, so it is read tolerantly of a byte-order mark; anything that isn't a list of records is discarded rather than half-rendered; entries before a fixed cutoff are filtered out, so the changelog starts at a deliberate point rather than at the first commit; and commit text is escaped before it reaches the page, because commit messages are text a human typed.
The other half is intake. Anyone on the floor can file a short request — seven request types, four priorities, what they do today, how many people it affects — which lands in a triage queue with four states, where it picks up an owner, a status and notes. Larger work goes through a full intake record routed to three named approval roles: all three must approve, any single denial settles it immediately, and one recorded vote per role is enforced by a database constraint rather than by the form. An intake record becomes an issue and a card on the development board, and the two sides reconcile every fifteen minutes under a database-level lock so only one process in the fleet ever polls.
- The card shows the branch linked to the issue, read off the board — so a request in flight points at the code meant to satisfy it
- Reconciliation refuses to overwrite a status set by the approval vote, so a card dragged on the board can't quietly revert a decision three people made
- Priority resolves asymmetrically — the board wins where it has a value, the record is pushed up where it doesn't — so the two sides converge instead of flapping
- Imported comments skip on the external identifier regardless of which side wrote them, so a comment pushed out is never re-imported as a copy; a filtered unique index on source and external identifier backs it in the schema
- Issue creation reads the stored issue number first and searches the board before adding, so a retried sync can't produce a second issue
- The manual sync button reports failure as failure — the sweep swallows its own errors internally, so the endpoint inspects the result rather than returning a success the board never actually reflected
Releases became ordinary. The same pipeline runs to the test slot and to production, a failed release leaves the previous version serving, and each machine can say what it is running without anyone opening a terminal.
The honest measure of this work isn't deployment speed. It's that a request submitted from the floor has a visible state and an owner — and, for the work that becomes a project, a branch and eventually a line in a changelog the submitter can read in the same application they filed it from. That is the part that keeps people reporting problems, and the reports are worth more than the pipeline.