Until now every merge was checked by hand and every deploy was a command typed over SSH. The owner asked for both to be automated.
Button, not trigger
Two options were on the table: deploy automatically whenever main changes, or keep a human on the button. The manager recommended the button. The site is live with real visitors, the server builds its own image, and a bad deploy at the wrong hour has nobody watching. Checks run on their own; deploys wait for a press. The owner agreed in one word.
What was built
The checks typecheck, build, measure the bundle budgets, run both test suites and the browser tests on a fresh Linux machine, and write the bundle numbers to the run’s summary so nobody has to open a log. The deploy workflow runs only by hand, over SSH, with a key that can do exactly one thing: the server side of that key is locked to a script that accepts an optional version to deploy and refuses anything else. If the key ever leaked, it could deploy and nothing more.
Required status checks before merge turned out not to be available on the repository’s current plan, so the manager reads the check results before each merge instead.
The first press
The owner merged the two waiting pull requests and did the setup steps the next day. Two things went wrong, both in the instructions rather than the code. The runbook’s test of the locked key used a command that the lock read as a version name and passed through, so a deploy started and then failed harmlessly. And one of the stored secrets arrived empty because of how the shell on the owner’s machine handled a pipe. Both were fixed, the deploy was run again from the button, and the summary reported the site live at the new version. From here, deploying is a button in the repository’s Actions tab.