Environments and deploy
How a merged change travels from GitHub to a QA site and then to a client's production server. The mechanism is the same for every client instance; the last step differs per client.
How this page was verified
Read from the deploy folders of master and the five client forks on 2026-09-05 and 2026-09-06, from the encrypted twins' consumer READMEs, and from the release script on the Rumah box.
The three boxes
Binari runs three machines with a self-hosted GitHub Actions runner. Every deploy is a workflow job on one of them; GitHub's own minutes are never used.
| Box | Where | Runner label | What it does |
|---|---|---|---|
| Rumah box | Rumah, public IP | rumah | producer for every client instance; consumer and QA site for Jadestone, JOTRE and Timas; master QA on MySQL and PostgreSQL; the KTP QA checkout |
| Lab box | office LAN | lab | consumer and QA sites for Medco (its SQL Server is reachable only from the LAN); master QA on SQL Server |
| Windows Server box | office LAN, Windows Server 2022, IIS | winserver2022 | consumer and QA site for Tomori |
The QA databases for the Rumah-box sites run on the box itself; Medco's two QA databases run on the shared dev database host. See each instance page.
Producer and consumer
A client ships as two repositories: the plain one developers work in, and the encrypted twin that holds the ionCube build. Nothing serves the plain repository on a client site.
push to development / main
→ workflow on the Rumah box runs deploy/dev.sh or deploy/prod.sh (PRODUCER)
→ lib/sync.sh copies the git-tracked changes plain → encrypted
→ push encrypted to GitHub and to the INARTS GitLab
→ the encrypted repo's own workflow fires (CONSUMER)
→ consumer pulls the served checkout and runs phinx migrateThe producer never touches a web root; it works in fixed deployer workspaces ~/deployer/<env>/<repo>{,-encrypted} on the Rumah box. The push to the encrypted repo is made with the runner's SSH key, not GITHUB_TOKEN, because a token push would not trigger the consumer's workflow. There is no loop: the consumer never pushes back.
lib/sync.sh has two lists:
SYNC_PROTECT: files the encrypted repo keeps its own version of.main.phpeverywhere (it is the encoded file), plussetting.phpin Medco. If the source changes a protected file, the sync commits everything else, writes.sync-blockedand stops before pushing, so a human re-encodes by hand.SYNC_IGNORE:deploy/and.github/. Each repository keeps its own deploy tooling, and none of it reaches the client. A push that changes only ignored paths updates the plain repository but makes no encrypted commit.
The sync also refuses a reverse or divergent diff, and the workflows use a concurrency group plus a flock so two deploys of one branch never overlap.
Per branch, per instance
| Instance | development | staging | main |
|---|---|---|---|
| Jadestone | consumer on the Rumah box pulls ~/www/inact-jadestone-encrypted and migrates | push staging to INARTS, then ssh to jadestone.inactsoft.com and pull the plain repository plus migrate | encrypted main pushed to GitHub and INARTS; no consumer, the client pulls from INARTS |
| Medco | producer on the Rumah box, consumer on the Lab box pulls two served roots and migrates each | none | encrypted main pushed; no consumer; a release zip is applied by hand on a box with no internet |
| JOTRE | consumer on the Rumah box | none, although the branch exists | encrypted main pushed; no consumer; Eris pulls onto the client's machine over VPN |
| Timas | consumer on the Rumah box | no branch | the push to GitHub fires deploy/prod.sh in the encrypted twin, which ssh's to edms.timas.com, pulls from INARTS and migrates |
| Tomori | producer on the Rumah box, consumer on the Windows Server box pulls the IIS site and migrates | no branch | encrypted main pushed; a release zip built from a tag is applied by hand |
| Master | main only: deploy/rumah.sh pulls and migrates the MySQL and PostgreSQL QA checkouts, deploy/lab.sh the SQL Server one, and both mirror main to INARTS |
The master setup is QA fan-out only: one branch, no encrypted twin, no sync. Its deploy/README.md warns in capitals that a new client fork must replace the whole deploy/ folder and the workflow with the producer/consumer pattern copied from a migrated instance.
What the QA sites are
| Site | Box |
|---|---|
inact-mysql.bdt.dev, inact-psql.bdt.dev | Rumah box |
inact-sqlsrv.bdt.dev | Lab box |
inact-jadestone-encrypted.bdt.dev, inact-jotre-encrypted.bdt.dev, inact-timas-encrypted.bdt.dev | Rumah box |
inact-medco-v3-encrypted.bdt.dev, inact-medco-v3-encrypted-testable.bdt.dev | Lab box |
inact-tomori-encrypted.bdt.dev | Windows Server box |
inact-ktp.bdt.dev | Rumah box, updated by hand |
The rule: an instance that ships encrypted gets -encrypted in its QA hostname. These are also where an encoded build is tested, because the encoded files cannot run on an Apple-silicon Mac.
Migrations
- Dev and staging migrate themselves. Every consumer runs
phinx migrateafter the pull. A migration merged todevelopmentreaches the shared dev database on its own. Never list "run the migration on dev" as an open task. - Production is manual except Timas. Timas's prod consumer migrates. Jadestone and JOTRE are pulled by hand and migrated by hand. Medco and Tomori receive the migration files inside the release zip and run phinx on site: Tomori with
C:\php\php.exe .\vendor\bin\phinx migrateunder IIS, per the team's notes. - A local run touches only your snapshot. It is not evidence that shared dev is migrated; the deployer covers that anyway.
TODO: the team notes mention a
run_sql.phpfor running SQL on production boxes that have no SQL client. It is not in any repository read on 2026-09-06; where it lives and when it is used needs a line here.
Cutting a production release
For the zip-delivered clients (Medco, Tomori) the release is cut on the Rumah box with ~/apps/deployer/lib/inact-release.sh <encrypted-repo> <slug>:
- It tags the encrypted repository's release branch (
mainby default). It does not advance the branch; the producer already did that from the plainmain, so the branch model stays the only gate on what reaches production. - It packages the diff against the previous tag as
releases/updates-<slug>-<TAG>.zip, plus<TAG>-DELETED.txtlisting removed paths. - Options:
--dry-run,--no-push,--prev <tag>,--branch <name>.
The zip is then taken to the client site and applied by hand, migrations included.
Notifications and logs
Every workflow ends with a Slack step that posts success or failure to the deploy channel when the SLACK_WEBHOOK_URL repository secret is set; without it the step no-ops. Each run keeps its log in the repository's Actions tab and can be re-run: the scripts are idempotent, and a run with nothing new to sync finds .sync-state current and does nothing.
The INARTS mirror
Every plain and encrypted push also goes to the INARTS GitLab, under the names INARTS uses (inact-timas-rekind for JOTRE, inact-kso-timas for KTP). For Timas and JOTRE the INARTS copy is the one production pulls from. Medco pushes only the encrypted twin there. The master QA jobs on both boxes mirror main to INARTS and treat a "cannot lock ref" race between them as success.
Gotchas
stagingon Medco and JOTRE deploys nowhere. The branch exists; the workflow ignores it.- The encrypted twin is not a mirror. Protected files differ by design, and ignored paths never arrive. Do not diff the two repositories and "fix" the differences.
- A blocked sync is silent to the developer. Look for
.sync-blockedin the deployer workspace when a push did not reach QA. - Tomori's runner must run as an account with the INARTS SSH key. Set up on 2026-09-01; if the service is reverted to NETWORK SERVICE, the pull fails.