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 itgoodnight-web maintainers. You do not edit it.You, or an AI tool working for you
What it containsThe 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 migrationsAstro pages, Markdown content (ja and en), site.config.ts, styles, wrangler.jsonc, and a src/worker.ts of about three lines
How it changesNew versions, following semverYour 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 theme in site.config.ts, and the shades mixed from them are in src/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.json and package-lock.json together.
  • Do not edit versions in package.json by hand without updating the lockfile. Run npm install (or npm install <package>@<version>) and commit both files. If they do not match, npm ci stops and the build fails.
  • Do not edit package-lock.json by 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

  1. goodnight-web publishes a new version of @goodnight-web/server. If it fixes a vulnerability, a GitHub security advisory comes with it.

  2. 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]
  3. Dependabot opens one pull request in your repository. It changes package.json and package-lock.json.

  4. 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": false in wrangler.jsonc).

  5. 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 preview on your computer. Then merge. The deploy script runs wrangler 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.

ResultComment
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 is Bot.
  • 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.json and lockfiles (bun.lock, package-lock.json, or pnpm-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:

templates/starter/.github/scripts/update-check.cjs
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:

templates/starter/.github/scripts/update-check.cjs
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:

templates/starter/.github/workflows/update-check.yml
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 });

On this page