Skip to content

Latest commit

 

History

History
217 lines (170 loc) · 11.3 KB

File metadata and controls

217 lines (170 loc) · 11.3 KB

Releasing and deploying

Prerequisite: Continuous integration — which suites run where.

Everything is published under https://www.danielhobi.ch/dreamrefactory/, and a release is a tag. Nothing deploys from an ordinary push to master except the documentation.

Seven things in one directory

Six builds and a doc set share that one hosting directory, and each goes out on its own:

tag build lands at
site-v0.1.1 npm run build -w site /dreamrefactory/ — the front door and the eight format editors
taoot-v0.9.53 npm run build -w taoot /dreamrefactory/taoot/ — Titanic's four pages (the front page, /play/, /collection/, the unlisted /speedrun/)
dust-v0.3.6 npm run build -w dust /dreamrefactory/dust/ — Dust's three pages (the game, /collection/, the unlisted /speedrun/)
timelapse-v0.1.0 npm run build -w timelapse /dreamrefactory/timelapse/ — Timelapse's one page
skullcracker-v0.1.0 npm run build -w skullcracker /dreamrefactory/skullcracker/ — Skull Cracker's two pages (the films and its menu, walk.html)
redjack-v0.1.1 npm run build -w redjack /dreamrefactory/redjack/ — RedJack's one page
lunicus-v0.1.0 npm run build -w lunicus /dreamrefactory/lunicus/ — Lunicus's one page
jumpraven-v0.1.0 npm run build -w jumpraven /dreamrefactory/jumpraven/ — Jump Raven's one page
(no tag) npm run docs:build /dreamrefactory/docs/ — on any push that touches docs/
# in the package that is releasing:
npm version 0.9.1 --no-git-tag-version -w @dreamfactory/taoot
# commit and merge, then from master:
npm run release -- taoot            # or several: taoot dust timelapse skullcracker redjack lunicus jumpraven
npm run release                     # everything whose version has no tag yet
npm run release -- --dry-run        # what it would do, and nothing else

Do not let npm version cut the tag. It writes a bare v0.9.1, which no pattern here listens for — the tag would push and deploy nothing at all, silently. --no-git-tag-version keeps it to the files and leaves the tag to the hand that knows which of the six it is.

Do not git push --tags a multi-game release. GitHub creates no workflow run at all when more than three tags arrive in a single push — not merely the excess ones. Nothing warns you: the push succeeds, every tag is on the remote, and the Actions tab is empty. A four-game release hits it exactly; recovery is one workflow_dispatch run per tag.

npm run release (tools/release.mts) pushes one tag per push, and after each one waits for the deploy to appear in the Actions tab — dispatching it by hand and saying so if it does not. It refuses to tag anything but a clean master that matches its remote, and it spells each tag from the package's own version, which is the pairing deploy.yml re-checks before it uploads.

Every namespace carries its target's name, and a tag naming none of them fails the run rather than defaulting to one, since a default would let a mis-typed tag ship the wrong build. deploy.yml can also be run from the Actions tab, where a workflow_dispatch input picks the target.

Adding a game to the lane

Four lines in deploy.yml and nothing else, because everything after the target is resolved is driven by the name: npm run build:<target> writes dist/<target>, and that directory is mirrored to <target>/. The four are the push.tags trigger, the workflow_dispatch choices, the concurrency group and the shell that turns a tag into a target — and site/tests/deploy-lanes.ts fails when a game in the registry is missing from any of them, because three of the four ways to get it wrong are silent.

The manifest is written by npm run manifest -w <game> — or by npm run manifest, which fans out over every package that has one — or by mkmanifest.ts run inside the game's directory on the host. All of them produce the same keys; a run from the root must not prefix every key with the game's own directory, or the page indexes nothing (site/tests/manifest-keys.ts).

What the workflow cannot do is put the RIP there. A runner has no game data, so the manifest the build writes describes almost nothing and is deleted before the upload; a freshly deployed game shows its "no game data" page until the host's own copy and a manifest generated beside it (tools/mkmanifest.ts) are in place, so deploying a new game is two steps.

Why sharing a directory is safe

Because the mirror only adds and overwrites — see below. The site's build writes the root of the tree and the four games write directories inside it, so none of the five can remove another's files. Asset names are content-hashed, so a superseded bundle is dead weight rather than a stale page.

Each package holds its own version

taoot/package.json, dust/package.json, timelapse/package.json, skullcracker/package.json, redjack/package.json, lunicus/package.json, jumpraven/package.json, site/package.json the sources of truth — semver
each package's vite.config.ts substitutes its own for __APP_VERSION__ at build time
site/src/version.ts exports VERSION, and draws it in the top bar beside the wordmark
site/src/bug-report.ts puts it in the issue body, so a report names the build it came from

A page belongs to exactly one package and reads exactly one number. Node does no substitution, so a test or a tool that imports version.ts reads 0.0.0-dev rather than throwing.

The deploy fails if the tag and the package disagree: the pages read their number from the manifest, and a site announcing a version nobody tagged is worse than a failed deploy.

What a deploy never touches

Each game's directory holds things that are in no build and never in this repository:

*/gamefiles/ the CD rips 7.4 GB (Titanic) and 645 MB (Dust), gitignored forever
*.zip the offline DBGL archives the collection page links to ~1 GB apiece
*/gamefiles.json the listing of a rip see below

nightdive.mov is not one of them: since #171 it is generated and deployed, and reaches the host like lang.stg does. The film is not in git — taoot/assets/nightdive.gif is, and a Vite plugin compiles the MOV into taoot/public/ at build time.

So the mirror only adds and overwrites. There is no --delete and no option to turn one on: re-uploading a wrong file costs a minute, and a deleted 7 GB rip does not. The files that could in principle be written over are excluded from the transfer as well (.github/actions/ftp-mirror, which every deploy in the repository goes through).

Why the manifest is not uploaded

gamefiles.json is the listing every page reads to find out what game data exists (the manifest). A build writes it by walking that game's gamefiles/ — and a GitHub runner has no rip, so the file it produces holds a handful of entries against the 4,172 (Titanic) or 460 (Dust) the host serves. Uploading it would leave the page offering nothing, so the workflow deletes it before the transfer and excludes it besides.

The host's copy has to be regenerated whenever the game data there changes, or when this repository adds an authored DF file to a package's public/. Do it where the tree is, once per game:

cd …/dreamrefactory/taoot && npx tsx …/tools/mkmanifest.ts . ./gamefiles .
cd …/dreamrefactory/dust  && npx tsx …/tools/mkmanifest.ts . ./gamefiles .

The third argument is where the authored files (lang.stg, nightdive.mov) are: public/ in a checkout, but the game's directory in a deployment, because that is where public/ is served from.

Each game's tree has its own listing, so Dust's page downloads a 20 KB listing of its own disc rather than a slice of Titanic's 212 KB index.

Secrets

The account details live on the dreamrefactory environment; FTP_HOST is a repository secret and falls through to it, because it is the same server for everything.

Secret where
FTP_HOST repository the host to connect to — a hostname, no scheme and no path
FTP_USER environment
FTP_PASSWORD environment
FTP_PATH environment . — the account lands in the site directory already
FTP_PORT either optional, 21 if unset
FTP_INSECURE_TLS either optional escape hatch, see below
FTP_ALLOW_PLAINTEXT either optional escape hatch, see below
gh secret set FTP_USER --env dreamrefactory   # and FTP_PASSWORD, FTP_PATH

The first four are required; the run fails with a named error before it connects if one is missing. The environment exists so that the account scoped to this directory cannot be reached by anything else.

The connection

Plain FTP sends the password as text on the wire, so the upload demands TLS: AUTH TLS on the ordinary FTP port, which is explicit FTPS — what a shared host usually means by "FTP over SSL". Passive mode, because the runner is behind NAT. One connection rather than several, because shared FTP accounts cap concurrent logins.

FTP_HOST is s067.cyon.net, not www.danielhobi.ch: the FTP server's certificate is a real one but it names the provider (*.cyon.net), so connecting by the domain fails verification while connecting by the server's own name passes it.

If cyon ever moves the account to another machine the connection will fail outright — check dig -x on the site's address for the new server name and update the secret, or fall back to www.danielhobi.ch with FTP_INSECURE_TLS=1.

Two escape hatches for a host that cannot do TLS properly. Both are off by default and both print a warning into the run when used:

FTP_INSECURE_TLS=1 still encrypted, but the certificate is not checked. The usual shared-hosting case: the cert names the server rather than the domain
FTP_ALLOW_PLAINTEXT=1 no TLS at all. The password crosses the wire in the clear — a last resort, and worth asking the host about first

The password never reaches a command line: the action writes the lftp script to $RUNNER_TEMP at mode 600 and deletes it on exit.

The documentation is not a release

docs.yml publishes docs/ on any push that touches it. Docs are not versioned against a game — a correction to a format page should be readable the day it is written.

Unlike the five builds, the docs site cannot be path-independent: VitePress needs an absolute base for its router, so docs/.vitepress/config.ts names /dreamrefactory/docs/ outright. It is the one place in the repository that knows the deployment's URL. It sits under docs/ rather than at the root because a VitePress site owns its whole route namespace — and this doc set has an editors/ section that would land exactly on top of the editors application.

Back to Reference.