> ## Documentation Index
> Fetch the complete documentation index at: https://docs.entire.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Build it with the Go SDK

> Handle installs, read, clone and create repositories, report check runs and deployments, comment on trails and receive events with the Entire Go SDK.

The SDK needs Go 1.24 or newer and has no dependencies outside the standard library. Its API reference is on [pkg.go.dev](https://pkg.go.dev/go.entire.com/entire-sdk-go/plugin).

```bash theme={null}
go get go.entire.com/entire-sdk-go
```

## Create the app and handle installs

This server sends customers to Entire and accepts their receipts:

```go theme={null}
package main

import (
	"log"
	"net/http"
	"os"

	"go.entire.com/entire-sdk-go/plugin"
)

func main() {
	pemBytes, err := os.ReadFile("plugin-key.pem")
	if err != nil {
		log.Fatal(err)
	}
	key, err := plugin.ParsePrivateKey(pemBytes)
	if err != nil {
		log.Fatal(err)
	}
	app, err := plugin.New(plugin.Config{
		Slug:        "acme-ci",
		Issuer:      "urn:entire:plugin:acme-ci",
		Signer:      key,
		Permissions: plugin.Permissions{Contents: plugin.Read, Checks: plugin.Write},
		Audience:    "urn:entire:plugin:acme-ci",
	})
	if err != nil {
		log.Fatal(err)
	}

	http.HandleFunc("GET /install", func(w http.ResponseWriter, r *http.Request) {
		state := plugin.NewState()
		saveState(w, state) // for example, in a signed, HttpOnly cookie
		link, err := app.InstallURL("https://ci.acme.example/callback", state)
		if err != nil {
			http.Error(w, "install link failed", http.StatusInternalServerError)
			return
		}
		http.Redirect(w, r, link, http.StatusFound)
	})

	http.HandleFunc("GET /callback", func(w http.ResponseWriter, r *http.Request) {
		receipt, err := app.AcceptReceipt(r.Context(), r.URL.Query(), loadState(r))
		if err != nil {
			http.Error(w, "install failed", http.StatusBadRequest)
			return
		}
		saveInstallation(receipt.InstallationID, receipt.CoreURL, receipt.ProjectID)
		w.Write([]byte("Installed."))
	})

	log.Fatal(http.ListenAndServe(":8080", nil))
}
```

`saveState`, `loadState` and `saveInstallation` are your own functions. `AcceptReceipt` checks the state, the receipt's signature, issuer, audience, plugin, expiry and single use, and returns `plugin.ErrAccessDenied` when the customer canceled. Store `receipt.CoreURL` with the installation ID; it is a convenient Core to call later.

Set `Config.Permissions` to every permission you registered, including `Trails` if you registered `trails:read` or `trails:write`. `inst.Token` requests them for every covered repository, and Entire refuses that token with `400 repository_required` unless it names every registered permission. Calls that act on one repository, such as `inst.Checks` and `inst.Deployments`, use a token for that repository with only the permission they need.

Two defaults matter for production. `Config.Cores` defaults to Entire's production Cores, and receipts from any other issuer are refused. Receipt replay protection defaults to memory, so give `Config.ReplayStore` a shared store when you run more than one process.

## Read the customer's repositories

`RepositoryClient` returns an ordinary SDK client whose token reaches one repository, with the permissions you pass. It routes every call to the repository's region and renews the token as it nears expiry:

```go theme={null}
func readRepos(ctx context.Context, app *plugin.App, installationID, coreURL string) error {
	inst, err := app.Installation(installationID, coreURL)
	if err != nil {
		return err
	}
	repos, err := inst.Repositories(ctx)
	if err != nil {
		return err
	}
	for _, repo := range repos {
		client, repoID, err := inst.RepositoryClient(ctx, repo.ID, plugin.Permissions{Contents: plugin.Read})
		if err != nil {
			return err
		}
		readme, err := client.Git.ReadFile(ctx, repoID, "HEAD", "README.md")
		if err != nil {
			return err
		}
		log.Printf("%s: %d bytes of README", repo.FullName, len(readme))
	}
	return nil
}
```

The SDK caches each token until five minutes before it expires, and concurrent callers share one mint. If you need the raw token, call `inst.RepositoryToken` or, for the whole installation, `inst.Token`.

The same client [comments on trails](https://docs.entire.com/guides/api/plugins/trail-comments.md#with-the-go-sdk) when you add `Discussions: plugin.Write` to the permissions you pass.

## Clone with Git

Clone through `git-remote-entire`, the Git helper that ships with the [Entire CLI](https://docs.entire.com/installation.md). A repository's `EntireCloneURL` names it, and `RemoteHelperEnv` returns environment variables that hand the helper the installation's token in `ENTIRE_TOKEN`:

```go theme={null}
env, err := inst.RemoteHelperEnv(ctx, repo)
if err != nil {
	return err
}
cmd := exec.CommandContext(ctx, "git", "clone", repo.EntireCloneURL, dir)
cmd.Env = append(os.Environ(), env...)
```

`RemoteHelperEnv` uses the installation's token, which reaches every repository in the installation, so run Git only where you trust it as much as your plugin. The token lasts about 45 minutes; call `RemoteHelperEnv` again for later fetches.

> [!WARNING]
>
> Don't clone `CloneURL` with `GitEnv` for now. Entire answers the clone host's first request with a redirect to the node that holds the repository, and Git does not send `GitEnv`'s header to that node, so the clone fails.

Code that speaks the Git protocol itself, such as a proxy, calls `GitAuthorization` instead. It returns an `Authorization` header value with a token narrowed to one repository and the permissions you pass, cached and renewed like `RepositoryToken`'s, so call it for every request:

```go theme={null}
auth, err := inst.GitAuthorization(ctx, repo, plugin.Permissions{Contents: plugin.Read})
if err != nil {
	return err
}
req.Header.Set("Authorization", auth)
```

Entire answers a clone URL's first request with a redirect to the node that holds the repository. Send the header to that node too, but only when `Config.HostPolicy` approves its host: it carries a credential.

## Create a repository

Plugins registered with `repositories:create` create repositories in the installation's project. Add `Repositories: plugin.Create` to `Config.Permissions`, and `Contents: plugin.Write` if you push to them. `CreateRepository` mints the create token, creates the repository, and waits until it is ready:

```go theme={null}
repo, err := inst.CreateRepository(ctx, "api", nil)
if err != nil {
	return err
}
saveRepository(installationID, repo.ID)
```

The returned repository is already in the installation and ready, so call `RepositoryClient` or `RepositoryToken` with its ID to act on it. Pass `&plugin.CreateRepositoryOptions{ObjectFormat: "sha256"}` for a SHA-256 repository. A name the project already has returns `plugin.ErrRepositoryExists`, and the daily limit returns `plugin.ErrCreateLimit`; the wrapped `*entire.HTTPError` says when to retry. If the repository is still provisioning when the wait gives up, `CreateRepository` returns `plugin.ErrRepositoryNotReady` with the new ID, so store it rather than create again. See [Create a repository](https://docs.entire.com/guides/api/plugins/act-for-an-installation.md#create-a-repository) for the errors, the limit, and what happens on uninstall.

## Report check runs

Plugins registered with `checks:write` write check runs on a commit in any repository the installation covers. A write is idempotent, so retry it freely. `ExternalUpdatedAt` is when the state changed on your side, and Entire applies writes to a run in that order:

```go theme={null}
func reportLint(ctx context.Context, inst *plugin.Installation, repoID, sha, attemptID string, finishedAt time.Time) error {
	_, err := inst.Checks.Upsert(ctx, repoID, sha, plugin.UpsertRequest{
		Branch: "main",
		Suite:  plugin.Suite{Key: "ci", Name: "Acme CI", ExternalID: attemptID},
		Runs: []plugin.Run{{
			Key: "lint", Name: "lint", ExternalID: attemptID + "/lint",
			Status: plugin.Completed, Conclusion: plugin.Success,
			ExternalUpdatedAt: finishedAt,
		}},
	})
	return err
}
```

The SDK validates a write as Entire does before sending it, sends it to the repository's region with a token for that repository that carries only `checks:write`, and follows a repository that moved to another region.

### Your plugin's gate

Each plugin that can write checks gets its own [gate](https://docs.entire.com/guides/trails/gates.md) on every repository it covers, keyed by its slug. Entire creates the gate the first time it evaluates a repository's gates after the install. The gate is enabled and blocking, so your reports can stop a trail from merging:

* **No required names.** The gate doesn't apply until you report on the trail's head commit. After that, every suite on the head commit must pass.
* **Required names.** The customer lists names, as [Review and require checks](https://docs.entire.com/guides/ci/checks.md) describes, under the repository's **Settings** then **Gates**. A name is a suite key, such as `ci`, or a suite key and a run key, such as `ci / lint`. A listed name with no report on the head commit waits, unless the commit was pushed before the plugin was connected. Checks that aren't listed never block.

The `success`, `neutral`, `skipped` and `action_required` conclusions don't fail a run. Keep your suite and run keys stable, because customers name them in their gate settings.

Deployments never count toward this gate. To show the resources you deployed and links to them on the Change, [report deployments](https://docs.entire.com/guides/api/plugins/deployments.md#with-the-go-sdk) with `inst.Deployments`.

## Receive events

`App.WebhookHandler` verifies each delivery's signature, skips deliveries already handled, and calls your function. Answer within seconds and queue slow work. Webhooks are experimental, and field names may change:

```go theme={null}
func webhooks(app *plugin.App) {
	http.Handle("POST /webhook", app.WebhookHandler(func(ctx context.Context, d *plugin.Delivery) error {
		change, err := d.Event.RefChange()
		if err != nil {
			return nil // not a ref event
		}
		enqueueBuild(d.InstallationID, change.Repository.ID, change.After)
		return nil
	}))
}
```

Set `Config.DeliveryJWKSURL` to the delivery JWKS URL Entire gives you when it registers your webhook.

Entire delivers fifteen events:

| Event | Reference |
| :- | :- |
| `installation.created` | [Installation created](https://docs.entire.com/api-reference/webhooks/installation/installation-created.md) |
| `installation.updated` | [Installation updated](https://docs.entire.com/api-reference/webhooks/installation/installation-updated.md) |
| `installation.repositories_added` | [Installation repositories added](https://docs.entire.com/api-reference/webhooks/installation/installation-repositories-added.md) |
| `installation.repositories_removed` | [Installation repositories removed](https://docs.entire.com/api-reference/webhooks/installation/installation-repositories-removed.md) |
| `installation.suspended` | Not in the reference yet. Decode it with `InstallationChange`; `suspended_by` is `plugin` or `customer`, and only that side can resume it. |
| `installation.resumed` | Not in the reference yet. Decode it with `InstallationChange`. |
| `installation.deleted` | [Installation deleted](https://docs.entire.com/api-reference/webhooks/installation/installation-deleted.md) |
| `ref.created` | [Ref created](https://docs.entire.com/api-reference/webhooks/ref/ref-created.md) |
| `ref.updated` | [Ref updated](https://docs.entire.com/api-reference/webhooks/ref/ref-updated.md) |
| `ref.deleted` | [Ref deleted](https://docs.entire.com/api-reference/webhooks/ref/ref-deleted.md) |
| `trail.opened` | [Trail opened](https://docs.entire.com/api-reference/webhooks/trail/trail-opened.md) |
| `trail.updated` | [Trail updated](https://docs.entire.com/api-reference/webhooks/trail/trail-updated.md) |
| `trail.closed` | [Trail closed](https://docs.entire.com/api-reference/webhooks/trail/trail-closed.md) |
| `trail.reopened` | [Trail reopened](https://docs.entire.com/api-reference/webhooks/trail/trail-reopened.md) |
| `trail.merged` | [Trail merged](https://docs.entire.com/api-reference/webhooks/trail/trail-merged.md) |

Trail events need `contents:read`. See [Receive trail events](https://docs.entire.com/guides/api/plugins/trail-events.md) for when each one fires and how to decode it.

## Try it on your machine

The SDK's `examples/plugin-reader` example runs the whole flow without a public server. It serves the post-install callback on `https://localhost:8443/callback`, opens the install link, verifies the receipt, then reads each repository with a client scoped to it.

1. Make a key and print its public half with `go run ./examples/plugin-reader -key plugin-key.pem pubkey`.
2. Ask Entire to register a test plugin with that key, `contents:read` and `https://localhost:8443/callback` as a post-install URL.
3. Make a certificate your browser trusts, for example with `mkcert localhost`.
4. Run the install, and choose a test project and repository on the install page.

```bash theme={null}
go run ./examples/plugin-reader -slug your-test-slug -key plugin-key.pem \
  -tls-cert localhost.pem -tls-key localhost-key.pem install
```

Later runs read the saved installation with `run`.
