Skip to main content

Set up a repository

Generate a repository from a starter and complete the one-time settings that files cannot hold.

Files set up most of the standard. The settings below live in GitHub, npm and Vercel, so a person does them once per repository. Agents can run the gh commands; the npm and Vercel steps need the maintainer's own login.

Replace OWNER/REPO with the repository, for example lupinum-dev/nuxt-tour.

Generate the repository

Pick a starter in starters/ of lupinum-dev/lupinum-oss: nuxt-module (a Nuxt module, the usual case), library (one framework-free package) or library-monorepo (several packages that release together in one Changesets fixed group, for example a Vue package and its Nuxt module). From a lupinum-oss checkout:

bash
node starters/nuxt-module/setup.mjs --output ../nuxt-example --name nuxt-example \
  --title "Nuxt Example" --description "One sentence." \
  --repository lupinum-dev/nuxt-example --domain nuxt-example.lupinum.com
cd ../nuxt-example
pnpm install
pnpm verify

--package defaults to @lupinum/<name>; --plausible SCRIPT_ID adds Plausible analytics to the site. The monorepo starter takes --package once per package and --primary for the package the docs lead with. --help prints each starter's options.

Commit pnpm-lock.yaml. Fill in the library-specific parts of AGENTS.md and check D1 in internals/decisions.md. For a library, create the empty GitHub repository (gh repo create OWNER/REPO --public) and the npm environment (below) before you push the first commit to main: that push already packs the starting version and waits for approval in npm (see First publication). If the environment does not exist yet, GitHub creates it without a reviewer. Add the ruleset after the first push.

Adopt the standard in an existing repository

Follow Migrate a repository. It uses the settings on this page in a fixed order.

GitHub settings

Merge settings: squash only, delete branches after merge, auto-merge off. The ruleset asks for no approvals, so auto-merge would let any token that can open a pull request merge it once ci is green.

bash
gh repo edit OWNER/REPO --enable-squash-merge --enable-merge-commit=false \
  --enable-rebase-merge=false --enable-auto-merge=false --delete-branch-on-merge

Actions token: read-only by default. A library also lets Actions create pull requests, so release.yml can open the Version packages pull request. A repository that publishes nothing leaves that off (can_approve_pull_request_reviews=false).

bash
gh api -X PUT repos/OWNER/REPO/actions/permissions/workflow \
  -f default_workflow_permissions=read -F can_approve_pull_request_reviews=true

Dependabot security updates off: Renovate opens every update pull request. Dependabot alerts stay on (see below).

bash
gh api -X DELETE repos/OWNER/REPO/automated-security-fixes

Ruleset for main: pull request required, required check ci (only from GitHub Actions, app ID 15368, so no other app can fake it), no force push, no deletion, linear history, squash only, review threads resolved before merge. "Require branches to be up to date" stays off (strict_required_status_checks_policy: false): ci runs again on main after every merge, and the Version packages pull request would otherwise need a manual update after each merge.

bash
gh api -X POST repos/OWNER/REPO/rulesets --input - <<'EOF'
{
  "name": "main",
  "target": "branch",
  "enforcement": "active",
  "conditions": { "ref_name": { "include": ["~DEFAULT_BRANCH"], "exclude": [] } },
  "rules": [
    { "type": "deletion" },
    { "type": "non_fast_forward" },
    { "type": "required_linear_history" },
    { "type": "pull_request", "parameters": {
        "required_approving_review_count": 0,
        "dismiss_stale_reviews_on_push": false,
        "require_code_owner_review": false,
        "require_last_push_approval": false,
        "required_review_thread_resolution": true,
        "allowed_merge_methods": ["squash"] } },
    { "type": "required_status_checks", "parameters": {
        "strict_required_status_checks_policy": false,
        "required_status_checks": [{ "context": "ci", "integration_id": 15368 }] } }
  ]
}
EOF

For a library, a second ruleset protects every tag: once release.yml has created a tag, nobody can move or delete it. It covers all tags, so it fits every tag format (v1.2.0, @scope/name@1.2.0) without a pattern to get wrong. The audit fails a library without it or with bypass actors on it.

bash
gh api -X POST repos/OWNER/REPO/rulesets --input - <<'EOF'
{
  "name": "tags",
  "target": "tag",
  "enforcement": "active",
  "conditions": { "ref_name": { "include": ["~ALL"], "exclude": [] } },
  "rules": [{ "type": "update" }, { "type": "deletion" }]
}
EOF

A library also needs the npm environment: the maintainer must approve every deployment, administrators cannot bypass that, and only main can deploy. Self-review stays allowed because the maintainer both merges the Version packages pull request and approves the publish.

bash
gh api -X PUT repos/OWNER/REPO/environments/npm --input - <<EOF
{
  "reviewers": [{ "type": "User", "id": $(gh api user --jq .id) }],
  "prevent_self_review": false,
  "can_admins_bypass": false,
  "deployment_branch_policy": { "protected_branches": false, "custom_branch_policies": true }
}
EOF
gh api -X POST repos/OWNER/REPO/environments/npm/deployment-branch-policies \
  -f name=main -f type=branch
gh api repos/OWNER/REPO/environments/npm --jq .can_admins_bypass   # must print false

If it prints true, turn off Allow administrators to bypass configured protection rules under Settings → Environments → npm.

Security features: secret scanning with push protection, private vulnerability reporting, Dependabot alerts (alerts only; Renovate opens the update pull requests) and CodeQL default setup. Do not add a CodeQL workflow file. Add no Actions secrets: the standard needs none, and the audit warns about any that exist.

bash
gh api -X PATCH repos/OWNER/REPO --input - <<'EOF'
{ "security_and_analysis": {
    "secret_scanning": { "status": "enabled" },
    "secret_scanning_push_protection": { "status": "enabled" } } }
EOF
gh api -X PUT repos/OWNER/REPO/private-vulnerability-reporting
gh api -X PUT repos/OWNER/REPO/vulnerability-alerts
gh api -X PATCH repos/OWNER/REPO/code-scanning/default-setup -f state=configured

Install the Renovate GitHub App on the repository, and for libraries the pkg.pr.new app, which preview.yml needs. Renovate installed for all repositories of an organization starts in Silent mode: it opens no pull requests and no Dependency Dashboard. Turn Silent mode off for the organization at developer.mend.io (Settings → Dependency Updates). The audit fails GH-08 until the Dependency Dashboard issue exists.

A repository that publishes nothing, such as lupinum-oss, needs the GitHub settings above except the library-only steps: leave pull request creation by Actions off (can_approve_pull_request_reviews=false), and do not create the tag ruleset or the npm environment. Skip the npm settings below.

npm settings

For a library, do this for every package, after its first version exists on npm. For a brand-new package, first follow First publication of a new package.

  1. On npmjs.com, open the package, then Settings.
  2. Under Trusted publishing, add a GitHub Actions publisher: organization or user OWNER, repository REPO, workflow release.yml, environment npm, allowed to publish directly (not only to stage a version).
  3. Under Publishing access, select Require two-factor authentication and disallow tokens. Trusted publishing keeps working; stolen tokens do not.
  4. Record the result as NPM-01 evidence in fleet/libraries.json in lupinum-oss, with the date.

The CLI equivalent for step 2 needs an npm version that has npm trust (check with npm trust --help; Node.js 24 ships one):

bash
npm trust github @lupinum/PACKAGE --repo OWNER/REPO --file release.yml --env npm --allow-publish

Never create an NPM_TOKEN secret.

Vercel

Only for a library with a docs site (see Write library docs). Import the repository in Vercel through its Git integration:

  • Root Directory: docs.
  • Include files outside the Root Directory in the Build Step: on, because the docs build uses the repository's pnpm workspace.
  • Build command, output and the ignore command come from docs/vercel.json. The ignore command builds only when the docs or the workspace manifests changed since the branch's last deployment, so most pull requests do not use build minutes.
  • Add the production domain.

There is no Vercel token in GitHub and no custom preview workflow.

A second maintainer

The standard assumes one maintainer: nobody else can review, so the release summary before the npm approval replaces a second reviewer. When a second person becomes a maintainer, add for every library:

  • .github/CODEOWNERS with both maintainers for /.github/ and /scripts/release.mjs, and "require_code_owner_review": true in the main ruleset. A change to the release machinery then needs the other person's approval.
  • The second maintainer as a required reviewer of the npm environment.
  • The second maintainer as an owner of every npm package (with 2FA) and of the GitHub organization, so one lost account no longer stops releases.

Check the result

From a lupinum-oss checkout:

bash
pnpm audit:repos OWNER/REPO

Fix every FAIL. Read every WARN: it is either excess tooling to remove, or a setting the audit could not read with your access. Add the repository to fleet/libraries.json in lupinum-oss.

The audit cannot see the npm Publishing access setting. Check it by hand once, after you set it.