Skip to content

Design: actors, render, and update

Who acts on a project made from this template, what each actor can change, and which workflows hold publishing tokens. The security assurance case follows.

Actors

Actor Actions Scope
Copier user Runs copier copy or copier update locally All files in their project via git push
Template repo CI Tests answer combinations and executes generated project checks Runner container only; publishes only template releases
Generated project CI Runs stack workflows on pull requests, pushes to main, and scheduled jobs Read-only by default (contents: read or {}); elevated privileges scoped to specific jobs
Dependency bot (Renovate or Dependabot) Opens PRs for dependencies, lockfiles, and GitHub Actions Branch and PR. Renovate auto-merges minor and patch updates when required checks pass. Dependabot never auto-merges
copier update workflow Renders latest template release weekly and opens an update PR Pushes only to chore/copier-update. Opens an issue if workflow updates require manual approval
Maintainer Reviews and merges PRs, tags releases (vX.Y.Z) Project main branch and published releases

Render

copier copy reads copier.yml from the newest tagged release, collects answers, and writes the files in template/. It executes no commands or migration scripts. Answers and the template commit are saved to .copier-answers.yml. Copier only touches the network to fetch the template.

Update

copier update performs a three-way merge between the recorded commit in .copier-answers.yml, the latest template release, and the local project tree. Conflicting changes leave standard git conflict markers. Files listed in _skip_if_exists are never modified by updates (which files). Running with --defaults accepts default values for newly added questions.

The weekly CI update workflow runs Copier in a read-only job, generates a binary patch, and applies it in a fresh job that holds the push token. This push job runs no template code or git hooks. The split has to be across jobs, not steps: steps in one job share the runner's state, GITHUB_PATH included, so a template step could change what a later step runs. Only the patch crosses between the jobs, never configuration or scripts.

Workflows that can publish

These rendered workflows publish. Most request id-token: write to exchange OpenID Connect tokens with external registries without long-lived secrets; the Cloudflare jobs use a stored API token:

Workflow Condition Trigger Token scope Protections
release.yml Not publish_to_pypi Pushed vX.Y.Z tag contents: write, to create the GitHub release from the tag's CHANGELOG section Publishes only a tag signed with a key in .github/allowed_signers on the default branch. Holds no OIDC token, builds nothing and runs only verify-tag.sh and gh release create
python-release.yml publish_to_pypi Pushed vX.Y.Z tag PyPI trusted publishing, attestation signing, GitHub release OIDC token scoped to publish job; release job has only contents: write. Neither runs project code. Limit pypi environment to v* tags
ts-release.yml publish_to_npm Pushed vX.Y.Z tag Publish to npm publish job uploads the tarball generated by build. Limit npm environment to v* tags
docker.yml (push job) publish_to_ghcr Pushes to main, tags, manual dispatch Sign image provenance; packages: write to push to GHCR Pull requests run only the unprivileged build job. Require review on ghcr environment
benchmarks.yml codspeed extra PRs that touch benchmarked code, pushes to main, manual dispatch Upload benchmark results to CodSpeed Fork PRs receive no OIDC token. Only in-repo branches can upload results
ts.yml (preview job) ts_kind=web-app Ready, non-bot pull requests from this repo, not the Copier update branch CLOUDFLARE_API_TOKEN in the preview environment: deploys a <slug>-pr-<n> Worker through deploy-worker.sh Fork and bot pull requests get no token. Runs the branch's own code with the token: see the threat table
ts.yml (production job) ts_kind=web-app Pushes to main CLOUDFLARE_API_TOKEN in the production environment: deploys the Worker through deploy-worker.sh Deploys the build the checks ran on; only main can deploy to the environment
preview-retire.yml ts_kind=web-app or site Pull request closed, merged or turned draft; daily; manual dispatch CLOUDFLARE_API_TOKEN in the preview environment: deletes preview Workers Checks out the base branch, not the pull request; skips fork pull requests
site.yml (deploy job) site, site_host=pages Pushes to main, manual dispatch on main pages: write and id-token: write, on this job only: publishes to GitHub Pages Deploys the build job's artifact and runs no repository code
site.yml (deploy job) site, site_host=cloudflare Pushes to main, manual dispatch on main CLOUDFLARE_API_TOKEN in the production environment: deploys the <slug>-site Worker through deploy-worker.sh, with --domain for a custom domain Deploys the build job's artifact; only main can deploy to the environment
site.yml (preview job) site Ready, non-bot pull requests from this repo, not the Copier update branch CLOUDFLARE_API_TOKEN in the preview environment: deploys a <slug>-site-pr-<n> Worker through deploy-worker.sh Fork and bot pull requests get no token. Deploys the build job's artifact, but runs the branch's own deploy-worker.sh with the token, as the web app's preview does
copier-update.yml (push job) dependency_bot=dependabot Weekly, manual dispatch With the update App: an App token with Contents, Pull requests and Workflows write, minted from the copier-update environment's key. Without it, GITHUB_TOKEN with contents: write and pull-requests: write. The job also holds issues: write, to open the issue when a push is not possible Applies the patch the read-only job made and runs no template code or hooks (Update). Limit the copier-update environment to main
docs.yml docs_site extra Pushes to main, manual dispatch on main pages: write and id-token: write, on the deploy job only: publishes to GitHub Pages Deploys the build job's artifact from main and runs no repository code
scorecard.yml public Pushes to main, weekly, branch rule changes Publish OpenSSF Scorecard results Runs approved actions on main; never triggered by pull requests

Scheduled workflows report a failure through report-failure.yml. Its one job holds issues: write and runs no repository code; the caller passes only an issue title.

None of these workflows use pull_request_target or workflow_run, preventing untrusted fork code from running with write permissions. Actions pin full commit SHAs, and checkouts set persist-credentials: false.

just repo-settings creates the pypi, npm, ghcr, preview, production and copier-update environments a project uses. It limits the release environments to v* tags (ghcr also to main), adds you as their reviewer, and limits production and copier-update to main. preview takes no reviewer or branch limit, because previews run on pull request refs and start unattended.

Assurance case

Security claims and controls for this template and generated projects. Vulnerability disclosures follow SECURITY.md.

Threat model

Defended threats:

  • Malicious code, branch names, or titles submitted via pull requests.
  • Compromised third-party actions, packages, or tools (including Copier).
  • Unauthorized attempts to acquire publishing tokens within CI jobs.
  • Crafted Copier answers designed to break out of templated files.
  • Accidental secret commits.

Out of scope: maintainer account compromise, deliberate weakening of workflows by maintainers, and a compromise of the platforms themselves: GitHub, Cloudflare, and the PyPI, npm and GHCR registries.

Trust boundaries

Boundary Cross-boundary inputs Controls
User machine Remote template and user answers No Copier tasks or migrations in copier.yml; validator rules check answers (Render)
Template repo CI PR code and metadata pull_request trigger only; contents: read in ci.yml, dco.yml, and pr-title.yml; ci ok required check
Template releases Pushed vX.Y.Z tag release.yml has only contents: write, holds no OIDC token, requires a tag signed with a key in .github/allowed_signers on main (a check that holds only with the v* tag ruleset: a tag runs its own commit's workflow and script), runs only release script and gh release create
Template docs site PR code and pushes to main docs.yml builds with contents: read. Only its deploy job holds pages: write and id-token: write, and it runs on main alone. It checks out no code and deploys the build job's artifact
Generated project CI PR code and metadata Workflows use contents: read or {}; no pull_request_target or workflow_run; persist-credentials: false on checkouts
Release jobs with OIDC Built artifacts OIDC tokens isolated to upload jobs without project code execution (Workflows that can publish); ruleset limits v* tags to admins
Weekly update workflow New template releases copier-update.yml runs Copier read-only and applies patches in a separate job holding push credentials (Update)
Dependency updates New package and action versions Renovate and Dependabot enforce a 7-day cooldown on new releases; uv (exclude-newer) and pnpm (minimumReleaseAge) keep the same week on install; Cargo and Go do not

Secure design principles

  • Least privilege: Workflows default to contents: read or permissions: {}. Jobs request write permissions only where required.
  • Separation of privilege: Build and publish steps run in separate jobs. The update workflow isolates Copier rendering from the git push job.
  • Fail-safe defaults: publish_to_* default to off, and extras starts empty except for cold_install, which a public licensed project above minimal gets. user_data has no default, preventing unintentional data collection claims.
  • Complete mediation: Changes to main require PRs and passing checks via branch rulesets (main.json, plus main-checks.json in a public repo).
  • Economy of mechanism: Renders run no executable tasks. Updates apply plain git apply patches without executing hooks.
  • Open design: Templates, workflows, and assurance cases are public and auditable.

Countering common weaknesses

  • Workflow injection: No run: step expands event expressions inline; expressions pass via env: variables. CI validates workflows with zizmor and jactionlint.
  • Token theft: Registry tokens are restricted to dedicated jobs that run no repository code. The Cloudflare preview jobs are the exception: they deploy the pull request's code, and the web app's also installs the branch's packages and runs its wrangler with the token. Checkouts disable credential persistence.
  • Dependency compromise: Actions pin full commit SHAs. Generated projects commit lockfiles, and CI installs with strict lockfile flags. A 7-day cooldown delays non-urgent updates.
  • Answer injection: validators in copier.yml constrain identifiers, URLs, versions, and descriptions. Descriptions are restricted to single lines without quotes or backslashes. Values pass into structured data via to_json.
  • Untested templates: check-render.sh ensures every file in template/ is rendered and validated across test combinations.

Security assessment

Reviewed 2026-11-12 for release 0.15.0:

Threat Impact Controls
Event metadata injected into shell steps Arbitrary command execution with workflow token Event fields pass through env: instead of inline expansion; zizmor and jactionlint audit workflows
Release job attempts token theft Malicious package or container published OIDC tokens scoped strictly to upload jobs without project code; registries restrict uploads to v* tags
Copier compromises update workflow Unauthorized commit pushed to repository Copier runs read-only; patch applied in separate push job without hook execution
Compromised package or action release Malicious code in CI or runtime Pinned action SHAs, committed lockfiles, and 7-day update cooldown
Crafted answer escapes template syntax Malformed or insecure project structure Validators in copier.yml validate types, formats, and escape sequences
Branch code reads the Cloudflare token in a preview job Anyone who can push a branch to a pull request a person opened can replace or delete the production Worker, or copy the token until it is rotated Fork pull requests, pull requests a bot opened (Renovate, Dependabot, the update App) and the Copier update branch get no token, and a bot's push does not start a preview. A person who later marks the pull request ready or reopens it deploys its head as it is, bot commits included. Accepted while the Worker serves only static files: the harm is a replaced site, as with a bad dependency merged to main. Give previews their own Cloudflare account before a Worker gets bindings (KV, D1, secrets), since those reach every preview
Update App key read from another branch Anyone who can push a branch can mint a token with Contents, Pull requests and Workflows write The copier-update environment is limited to main. On a private repo on GitHub Free there are no environment branch policies, so the key is readable from any branch: use one App per repo there, or the GITHUB_TOKEN path (The update App). just repo-settings says so when the plan refuses its rulesets
Release tag signed by the wrong person A package published from a tag no maintainer signed verify-tag.sh checks the SSH signature against allowed_signers on main. It holds only with the v* tag ruleset, since a tag runs its own commit's workflow. Template updates follow the newest v* tag whether or not release.yml accepted it
Accidental secret commit Leaked credentials betterleaks hook blocks local commits; on public repos, GitHub secret scanning with push protection covers remotes