Distribution and updates
The server comes as an npm package that you do not edit, your website is yours to edit, and updates arrive as Dependabot pull requests.
This page shows which files you edit, how updates reach you, and which pull requests are safe to merge. The contact API and the admin screen come from an npm package that you do not edit. The Astro website is yours to edit.
Two parts, split by who edits them
@goodnight-web/server (npm package) | Your site (the starter) | |
|---|---|---|
| Who edits it | goodnight-web maintainers. You do not edit it. | You, or an AI tool working for you |
| What it contains | The Worker factory, the contact API, Better Auth with passkeys, the admin API, the prebuilt admin screen with Japanese and English strings, security headers, and the D1 migrations | Astro pages, Markdown content (ja and en), site.config.ts, styles, wrangler.jsonc, and a src/worker.ts of about three lines |
| How it changes | New versions, following semver | Your own commits |
The starter's src/worker.ts only calls the factory, the function that builds the Worker. The server code stays inside the package.
The starter's AGENTS.md and CLAUDE.md tell AI tools:
- What to edit:
src/pages,src/content,site.config.ts,src/styles - What to leave alone: the factory call, the binding names, and
@goodnight-web/*
Colors
The site and the admin screen have separate colors. Changing your site's colors does not change the admin screen, and a package update does not change your site's colors.
- Your site: its colors are in the starter. The three main colors are in
themeinsite.config.ts, and the shades mixed from them are insrc/styles/global.css. The sample site is green. - The admin screen (
/admin): a fixed theme inside the package, with a violet primary color on neutral backgrounds and a light and a dark mode.
How you get a copy
The Deploy to Cloudflare button copies the templates/starter folder of the public goodnight-web repository into a new repository in your own GitHub account. It is a copy, not a fork.
Your copy does not follow the goodnight-web repository. The server code reaches you through the npm package instead. See Getting started.
The lockfile
The starter includes package-lock.json, which records the exact version and checksum of every package the site installs. Workers Builds installs with npm ci from it, so the first deploy installs exactly the committed packages, and so does every later build until a pull request changes the file.
- Dependabot updates
package.jsonandpackage-lock.jsontogether. - Do not edit versions in
package.jsonby hand without updating the lockfile. Runnpm install(ornpm install <package>@<version>) and commit both files. If they do not match,npm cistops and the build fails. - Do not edit
package-lock.jsonby hand.
Your repository must be private
Keep your copy of the site in a private repository. On GitHub, open Settings > General > Danger Zone > Change visibility and choose private.
A public repository shows anyone your full site source, your configuration, and the history of every change, including anything committed by mistake. It also lets strangers open pull requests, which is exactly the situation the update check workflow warns about.
The starter's .github/workflows/visibility-check.yml runs on every push, and you can also run it manually. If the repository is public, the job fails with an error in Japanese and English that tells you to make it private.
The workflow needs no token and no permissions. The workflow lives in the starter folder, so it runs only in your copy.
How updates reach you
-
goodnight-web publishes a new version of
@goodnight-web/server. If it fixes a vulnerability, a GitHub security advisory comes with it. -
Dependabot proposes the version once it has been public for 5 days. The settings are in the starter's
.github/dependabot.yml:@goodnight-web/*updates are grouped into one pull request.- Security updates skip this 5-day wait (the cooldown).
- Minor and patch updates of the site's other packages (such as Astro) come in a second group.
templates/starter/.github/dependabot.yml cooldown: default-days: 5 groups: goodnight-web: patterns: ['@goodnight-web/*'] site-dependencies: patterns: ['*'] exclude-patterns: ['@goodnight-web/*'] update-types: [minor, patch] -
Dependabot opens one pull request in your repository. It changes
package.jsonandpackage-lock.json. -
If Workers Builds builds non-production branches (a setting of the Worker's build configuration in the Cloudflare dashboard), it builds the pull request and shows whether the build passed. The build gets no public address: the starter turns preview URLs off (
"preview_urls": falseinwrangler.jsonc). -
You review the pull request: the Update check comment and status, the build result if there is one, and the changes. To try the update before merging, check out the pull request's branch and run
npm run previewon your computer. Then merge. The deploy script runswrangler d1 migrations apply DB --remote && wrangler deploy, so the database is migrated before the new code runs.
Preview URLs stay off because each preview version would run with your production D1 database and secrets at a public address, before you merge it. If you want them, set "preview_urls": true knowing that.
There is no one-click update in the admin screen. It would need a token that can write to your GitHub repository, stored in the Worker. goodnight-web does not give the Worker that access.
The update check workflow
Merge only pull requests that have the official update comment and a passing goodnight-web/update-check check. A pull request that says "update" is not always an update from goodnight-web.
The starter's .github/workflows/update-check.yml comments on every pull request. The comment is in Japanese and English. The workflow updates the same comment instead of adding new ones. It also sets a commit status named goodnight-web/update-check on the pull request's latest commit: it passes only for an official update. Anyone can paste the text of the comment into a pull request, but setting the status takes write access to the repository (this workflow's token has it), and the status belongs to one commit. The status has not been tested on GitHub yet.
| Result | Comment |
|---|---|
| Official update | "Official goodnight-web update" / "Safe to merge. If a build result is shown, check it first." |
| Anything else | "This is not an official update" / "If you don't understand the changes, don't merge." |
What counts as official
A pull request counts as an official update only if all of these are true:
- The author is
dependabot[bot]and the account type isBot. - The branch is in your own repository, not in a fork.
- Every commit in the pull request was authored and committed by
dependabot[bot], and GitHub shows its signature as verified. A commit that someone else pushed to the Dependabot branch, or a commit made in the GitHub web interface, makes it not official. - The only changed files are
package.jsonand lockfiles (bun.lock,package-lock.json, orpnpm-lock.yaml). Each of them is modified, not added or removed. - In
package.json, every changed line is the version of a@goodnight-web/*dependency.
The lockfile may change in other ways. A new @goodnight-web/server version can bring new versions of the packages it uses (for example hono), and those changes appear only in the lockfile. But every package added to package-lock.json must come from https://registry.npmjs.org/ with a sha512 checksum. A Git URL, a local path, a link, plain http://, or another host makes the pull request not official.
The packages your site depends on directly are listed in package.json. That diff is checked line by line.
If the check cannot see every change, the pull request is not counted as official. For example, GitHub may omit or truncate the package.json or package-lock.json diff, or the list of changed files or commits may be incomplete.
Inside the script
The script .github/scripts/update-check.cjs collects a reason for every rule that fails. The pull request is official only when the list stays empty. The first two rules:
const user = pullRequest.user;
if (!user || user.login !== OFFICIAL_AUTHOR || user.type !== 'Bot') {
reasons.push(
`author is not ${OFFICIAL_AUTHOR}: ${user ? clean(user.login) : 'unknown'}`,
);
}
const headRepo = pullRequest.head.repo
? pullRequest.head.repo.full_name
: null;
if (headRepo !== pullRequest.base.repo.full_name) {
reasons.push(
`branch is not in this repository: ${headRepo === null ? 'deleted repository' : clean(headRepo)}`,
);
}Then every changed line in package.json must be an @goodnight-web/* entry set to a version number:
for (const change of packageJsonChanges(hunks)) {
if (change.name === null) {
reasons.push(
`package.json: change outside a dependency entry: ${clean(change.line)}`,
);
} else if (!change.name.startsWith(OFFICIAL_SCOPE)) {
reasons.push(
`package.json: entry other than an ${OFFICIAL_SCOPE}* dependency changed: ${clean(change.name)}`,
);
} else if (change.invalidSpec) {
reasons.push(
`package.json: ${clean(change.name)} is not set to a version number: ${clean(change.line)}`,
);
} else {
officialChanges += 1;
}
}Code from the pull request never runs
The workflow runs on pull_request_target, so it has the permission to comment. It never checks out or runs code from the pull request.
It checks out only .github/scripts from the base branch, so the script that runs is the one already in your repository. It reads the pull request's metadata and its list of changed files through the GitHub API:
steps:
- name: Check out the script from the base branch
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.base.sha }}
persist-credentials: false
sparse-checkout: .github/scripts
- name: Classify the pull request and comment
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { run } = require('./.github/scripts/update-check.cjs');
await run({ github, context, core });