roots / roots/trellis-cli

Kinsta support

Open
#13 4 comments 6 reactions 1 assignee View on GitHub

@retlehs is already working on this.

Since Sep 9, 2025.

enhancement
Dominant language
Go
Stars
169
Forks
30
Avg merge
13h 43m
Merged PRs (30d)
7

Description

Background

Trellis deployments to Kinsta require significant manual configuration documented in Kinsta's Bedrock/Trellis guide. Two recent Kinsta developments make automation practical:

  1. Custom webroot API (changelog) — programmatically set webroot to /current/web via POST /v2/sites/environments/{env_id}/change-webroot-subfolder, eliminating the old support-ticket workflow.

  2. Site Environments API (docs) — retrieve environment details (SSH host/port, paths), create/clone environments, manage SFTP access. Note: DB credentials are not available via the API — they must be entered manually or sourced from MyKinsta.

Scope

v1 does:

  • trellis kinsta setup command that configures a single Trellis environment for Kinsta deployment
  • Kinsta API client for site/environment discovery, SFTP config, and webroot changes
  • Host inventory generation, group_vars configuration, deploy hook patching
  • Bedrock config updates (DB defines, CDN/mu-plugin constants)
  • kinsta-mu-plugins installation via Composer
  • Webroot configuration via API
  • Persisted site/company mappings in .trellis/kinsta.yml

v1 does not:

  • Write DB credentials automatically (API doesn't expose them; user runs trellis vault edit)
  • Manage DNS or SSL
  • Provide a custom deploy command (standard trellis deploy works after setup)
  • Modify cli_config.Config struct or trellis.cli.yml (feature-local config only)

Command

trellis kinsta setup [--company=<id>] [--environment=<environment>] [--site=<site>] [--kinsta-site=<id>] [--force]

Flags:

  • --company — Kinsta company ID (also via KINSTA_COMPANY_ID env var or company in .trellis/kinsta.yml). Prompted interactively if not provided.
  • --environment — Trellis environment name (staging or production).
  • --site — Trellis site key as defined in wordpress_sites.yml (e.g. example.com). This is the local Trellis site name, not the Kinsta site ID.
  • --kinsta-site — explicit Kinsta site ID, bypassing name matching entirely. Useful when Trellis and Kinsta naming conventions differ.
  • --force — overwrite existing hosts file even if it contains non-Kinsta content.

Flow

  1. Authenticate — resolve Kinsta API token from KINSTA_API_TOKEN env var or prompt.
    Resolve company ID using this precedence:

    1. --company flag (highest)
    2. KINSTA_COMPANY_ID env var
    3. company in .trellis/kinsta.yml
    4. Interactive prompt (lowest)
  2. Select Kinsta site/environment — map Trellis --environment to Kinsta environment type: production maps to live, staging maps to staging, anything else errors with a message listing valid values. Then resolve Kinsta site ID using this precedence:

    1. --kinsta-site flag (highest)
    2. Persisted mapping in .trellis/kinsta.yml (sites.<site>.<env>)
    3. --site name matched against Kinsta site names via API
    4. Interactive selection from API site list (lowest)

    For name matching (3): zero matches errors, multiple matches prompts. On first successful mapping, offer to persist the Kinsta site ID to .trellis/kinsta.yml for future runs. Persistence requires both --site (or single-site project auto-detect) and --environment to be known — if either is missing, the mapping is not persisted (the resolved ID is still used for the current run).

  3. Fetch environment details — call ListEnvironments for environment metadata,
    then GetSFTPConfig for SSH host, port, and username.

  4. Generate host inventory — write/update hosts/<env>:

    kinsta_<env> ansible_host=<ip> ansible_ssh_port=<port> ansible_ssh_extra_args='-o StrictHostKeyChecking=no'
    
    [web]
    kinsta_<env>
    
    [<env>]
    kinsta_<env>
    
  5. Update group_vars/<env>/main.yml — create if needed, append Kinsta-specific vars (this file isn't standard in all Trellis projects but is the correct place for environment-level overrides that aren't site-specific):

    project_root: /www/<site_path>/public
    www_root: /www/<site_path>/public
    web_user: <kinsta_user>
    web_group: www-data
    
  6. Update ansible.cfg — set forks = 3 (host key checking is scoped to the Kinsta host via inventory vars in step 4, not set globally)

  7. Patch deploy hooks — modify roles/deploy/hooks/finalize-after.yml:

    • Remove Reload php-fpm task (Kinsta manages PHP)
    • Add Clear Kinsta cache task via URI module
  8. Update Bedrock config (config/application.php):

    • Add DB credential defines for MyKinsta compatibility
    • Add KINSTA_CDN_USERDIRS and KINSTAMU_CUSTOM_MUPLUGIN_URL constants
  9. Add kinsta-mu-plugins via
    retlehs/kinsta-mu-plugins:

    • Add VCS repository entry to composer.json repositories. Detect whether repositories is an array (common) or object (named keys) and merge accordingly:
      { "type": "vcs", "url": "https://github.com/retlehs/kinsta-mu-plugins.git" }
      
      Deduplicate by URL to ensure idempotency.
    • Check if kinsta/kinsta-mu-plugins already exists in require with a compatible constraint — if so, skip composer require. Otherwise run composer require kinsta/kinsta-mu-plugins:^2 to pin to current major (this handles adding the require entry and updating the lock file)
  10. Set webroot via APIPOST /v2/sites/environments/{env_id}/change-webroot-subfolder with web_root_subfolder: "/current/web" (leading slash per API convention)

  11. Print next steps — direct user to MyKinsta for DB credentials and to run trellis vault edit <environment> to configure them.

Each step checks current state before modifying. Re-running on an already-configured environment is safe (update values if changed, skip if current).

Config: .trellis/kinsta.yml

company: "company-id"
sites:
  example.com:          # Trellis site key
    staging: "kinsta-site-id"
    production: "kinsta-site-id"
  • Created on first trellis kinsta setup when user opts to persist.
  • Git-tracked by default — contains non-sensitive IDs/mappings useful to the whole team. No API tokens are stored here (those stay in env vars). Teams that prefer local-only can add it to .gitignore; the setup command does not manage .gitignore entries for this file.
  • Malformed file: parse error aborts with message showing the file path and YAML error. Never silently ignore or overwrite a malformed file.
  • Loaded by kinsta commands only. Feature-local file avoids modifying the shared cli_config.Config struct and its validators.

Implementation

Additive changes only — minimal edits to existing files (main.go command registration).
All Kinsta logic lives in new files.

New files
pkg/kinsta/
  client.go          # API client — auth, HTTP, error handling
  types.go           # API request/response types (Site, Environment, etc.)
  client_test.go

cmd/
  kinsta.go           # Namespace command
  kinsta_setup.go     # Setup command implementation
  kinsta_setup_test.go
Kinsta API client
type Client struct {
    Token   string
    BaseURL string // https://api.kinsta.com/v2
    HTTP    *http.Client
}

func (c *Client) ListSites(ctx context.Context, company string) ([]Site, error)              // GET /sites?company=<id>
func (c *Client) ListEnvironments(ctx context.Context, siteID string) ([]Environment, error) // GET /sites/{site_id}/environments
func (c *Client) GetSFTPConfig(ctx context.Context, siteID, envID string) (*SFTPConfig, error) // GET /sites/{site_id}/environments/{env_id}/ssh/config
func (c *Client) ChangeWebroot(ctx context.Context, envID string, subfolder string) error    // POST /sites/environments/{env_id}/change-webroot-subfolder
Command registration (main.go)
"kinsta": func() (cli.Command, error) {
    return &cmd.NamespaceCommand{
        HelpText:     "Usage: trellis kinsta <subcommand> [<args>]",
        SynopsisText: "Commands for Kinsta hosting integration",
    }, nil
},
"kinsta setup": func() (cli.Command, error) {
    return cmd.NewKinstaSetupCommand(ui, trellis), nil
},

File mutation strategy

Managed blocks — for file types that support comments (hosts/INI, YAML, PHP), wrap auto-generated sections with markers (# BEGIN trellis-kinsta / # END trellis-kinsta, or // BEGIN trellis-kinsta for PHP) so that reruns can find and update existing blocks deterministically.

JSON files (composer.json) cannot carry comments — use structural merge only.

Parse strategies:

  • Hosts (INI) — template overwrites file. If it already exists with non-Kinsta content, require --force to overwrite (default: abort with error). Back up original to hosts/<env>.bak before overwriting.
  • YAML (group_vars) — merge keys rather than append blindly.
  • PHP (application.php) — string match for existing defines before inserting. Use // BEGIN/END trellis-kinsta markers.
  • JSON (composer.json) — detect repositories shape (array or object), append/merge VCS entry, deduplicate by URL. Then composer require owns the require key and lock.
  • YAML (deploy hooks) — parse task list, remove/add by task name field.

No rollback — if a step fails mid-way, leave partial changes in the working tree. User can review with git diff and restore selectively or via VCS as needed.

PR Execution Plan

Single PR, built and reviewed piece by piece:

  • pkg/kinsta/ — API client + types + tests (mockable HTTP)
  • cmd/kinsta.go — namespace command
  • cmd/kinsta_setup.go — command skeleton + flag parsing + config loading
  • Site/company resolution logic with precedence rules
  • .trellis/kinsta.yml read/write/persist
  • File mutations: hosts, group_vars, ansible.cfg, deploy hooks
  • File mutations: application.php, composer.json
  • Webroot API call + next-steps output
  • main.go — command registration
  • Tests: golden files, precedence, integration, edge cases

Acceptance Criteria

  • trellis kinsta setup configures all automatable Kinsta integration steps end-to-end (DB creds remain manual via trellis vault edit)
  • Idempotent: rerunning on an already-configured environment produces no unnecessary changes
  • Company precedence honored: --company > KINSTA_COMPANY_ID env var > .trellis/kinsta.yml > prompt
  • Site precedence honored: --kinsta-site > .trellis/kinsta.yml mapping > --site name match > interactive
  • No global host_key_checking = False — scoped to Kinsta host via inventory vars only
  • Existing hosts file with non-Kinsta content is not overwritten without --force
  • Malformed .trellis/kinsta.yml aborts with clear parse error
  • DB credentials: user directed to MyKinsta + trellis vault edit, not auto-written
  • composer require skipped when package already present with compatible constraint
  • composer.json repositories array/object shape detected and handled correctly
  • --force creates hosts/<env>.bak before overwriting existing hosts file

Test Matrix

  • API client — mock HTTP responses for each endpoint, error cases
  • Golden files — hosts, group_vars, deploy hooks, composer.json, application.php (fresh setup + idempotent rerun)
  • Precedence — company (--company > env var > config > prompt), site (--kinsta-site > config mapping > --site name match > interactive)
  • Environment mappingproduction -> live, staging -> staging, invalid value -> error with valid options listed
  • Edge cases:
    • --site with zero Kinsta matches (error)
    • --site with multiple matches (prompt)
    • Existing non-Kinsta hosts file without --force (abort)
    • Existing non-Kinsta hosts file with --force (backup + overwrite)
    • Pre-existing Kinsta blocks in files (rerun/update)
    • Missing optional files (e.g. no finalize-after.yml)
    • Composer require skipped when already present
    • Malformed .trellis/kinsta.yml (abort with parse error)

Future Commands

Once the foundation exists, these become straightforward additions:

  • trellis kinsta cache clear — clear Kinsta cache via API
  • trellis kinsta environments — list environments for a site
  • trellis kinsta deploy — deploy wrapper with Kinsta-specific pre/post hooks

Ref https://kinsta.com/blog/bedrock-trellis/
Ref https://kinsta.com/changelog/custom-webroot/
Ref https://api-docs.kinsta.com/tag/WordPress-Site-Environments
Ref https://api-docs.kinsta.com/api-reference/wordpress-sites/get-list-of-company-sites
Ref https://api-docs.kinsta.com/api-reference/wordpress-site-environments/get-sftpssh-connections-config
Ref https://api-docs.kinsta.com/api-reference/wordpress-site-environments/change-webroot-subfolder-of-environment
Ref https://api-docs.kinsta.com/api-reference/wordpress-site-environments/get-site-environments
Ref https://github.com/retlehs/kinsta-mu-plugins

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.