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

# Delivering Artifacts to Git

Most artifacts are delivered to a place your application reads from: a file in an object store, an image in a container registry. Some destinations cannot be read from at all. A hosted build service that builds your application from a git repository has to be **pushed to**, and there is nobody inside your customer's account to do the push.

The `# @artifact_commit()` annotation closes that gap. It declares that the contents of one of your artifacts become a commit on a branch of a git repository in your customer's account. The appliance controller performs the push itself, using its own identity in that account, so no git credential, access token, or console authorization is involved anywhere.

The artifact is still an ordinary object-store file. What is new is where it lands and who moves it.

## When you need this

You need this when something inside your customer's account builds your application from a git repository rather than running a bundle you built yourself.

That happens when two things hold at once. The build has to run **per install**, because some of what goes into it is created by the install's own apply. A framework that compiles public environment values into its browser bundle cannot be served by one bundle you built in advance. And the thing that builds has to be **pushed to** rather than read from. If it accepts an artifact you hand it, [ordinary artifact delivery](/fundamentals/artifacts) already covers you.

AWS Amplify is an example that shows both halves at once:

* **Build-time values are compiled into the bundle.** A framework like Next.js bakes its public environment values into the browser bundle during the build, and some of those values (an identity provider client ID, for example) are created by the install's own apply. One prebuilt bundle therefore cannot serve every install.
* **Amplify has no manual deploy path for server-rendered apps.** For an SSR app, a git repository is the only input it accepts.
* **Of the git sources Amplify accepts, only CodeCommit authenticates without a person.** GitHub, GitLab, and Bitbucket each require somebody to complete an interactive authorization flow and hand over a token. Nobody is present inside your customer's account to do that.

Nothing in the mechanism is specific to Amplify. Any service inside your customer's account that builds from a branch it watches has the same shape.

## How a delivery works

<Steps>
  <Step title="You publish the artifact">
    Your build packages the tree you want committed as a tar archive and uploads it to a bucket in your own account.
  </Step>

  <Step title="Your origin stack declares the delivery">
    A data source for the object, an `aws_codecommit_repository` resource for the destination, and a local that names both, annotated with `# @artifact_commit()`.
  </Step>

  <Step title="Tensor9 moves the artifact to the appliance controller">
    During deployment, Tensor9 fetches the object from your bucket and streams it to the appliance controller.
  </Step>

  <Step title="The appliance controller pushes it">
    The appliance controller unpacks the archive and pushes the tree onto the branch as a single commit, authenticating with its own identity in the customer's account.
  </Step>

  <Step title="The build service builds">
    Whatever watches that branch sees the push and builds from it.
  </Step>
</Steps>

Deliveries are idempotent. Before pushing, the appliance controller compares the tree it is about to write against what is already at the branch tip and writes nothing when they match, so redeploying unchanged contents does not trigger a rebuild. When it does write, the delivery replaces the branch with a single commit. The branch belongs to the delivery, and hand edits to it are overwritten.

### Packaging the artifact

* The archive is `tar`, optionally gzipped. The format is detected from the file's contents, so the object key can be named anything.
* Entry paths are preserved exactly and no leading directory is stripped. Create the archive from inside the directory you want at the repository root.
* A `.git` directory inside the archive is ignored, so an archive made from a working tree does not carry its history in.

## Declaring a delivery

```hcl theme={null}
data "aws_s3_object" "webapp_src" {
  count  = local.webapp_seeded ? 1 : 0
  bucket = var.webapp_artifact_bucket
  key    = var.webapp_artifact_key
}

locals {
  # @artifact_commit()
  webapp_delivery = {
    src    = one(data.aws_s3_object.webapp_src)
    dest   = one(aws_codecommit_repository.webapp)
    branch = var.webapp_branch
    after  = [aws_amplify_branch.web]
  }
}
```

All four keys are required.

| Key      | Value                                                                                                             |
| -------- | ----------------------------------------------------------------------------------------------------------------- |
| `src`    | A reference to the `data "aws_s3_object"` block holding the artifact.                                             |
| `dest`   | A reference to the `aws_codecommit_repository` resource the commit lands on.                                      |
| `branch` | The branch the commit lands on.                                                                                   |
| `after`  | The resources that watch the repository for pushes and must therefore exist before the artifact lands. See below. |

The annotation itself takes no arguments. Every value is a real Terraform reference inside the object, so Terraform validates it: a rename breaks loudly, your editor completes it, and your linter sees it.

### `after` decides whether anything builds

`after` names the resources that must exist before the artifact lands, written as addresses, the way `depends_on` entries are written.

Tensor9 already orders the delivery after every resource it can see **consuming** the destination repository. `after` is for a resource that **watches** the repository without consuming it, which no reading of your Terraform can find. `aws_amplify_branch` is the example: it references the Amplify app's ID and names the repository nowhere, so nothing connects it to the delivery.

<Warning>
  **Getting `after` wrong fails silently.** A build service that watches a branch discards a push notification for a branch it holds no record of, and it never re-scans. If the artifact lands before the watcher exists, the apply succeeds, no build runs, no build fails, and the install looks healthy while serving nothing. Creating the watcher afterwards does not back-fill a build for the commit already sitting at the branch tip.
</Warning>

One caution, and it applies only when a resource you name in `after` is itself gated by `count` or `for_each`.

Terraform accepts a `depends_on` entry naming a resource whose count is zero, and orders the delivery against nothing at all. Tensor9 does not compare that gate against the delivery's own. So if the watcher is switched off on an apply where the delivery still runs, the delivery lands unordered, and you are back in the silent case above: the apply succeeds and nothing builds.

Gate the delivery so it can never be on while the watcher is off. The usual way is to build the delivery's gate out of the watcher's own:

```hcl theme={null}
locals {
  # var.enable_web_app is also what gates aws_amplify_branch.web
  webapp_seeded = var.enable_web_app && var.webapp_artifact_key != ""
}
```

A delivery gated this way always has a branch to be ordered against.

Write `after = []` only if nothing watches the repository. `after = null` is an error, to prevent silent misconfigurations.

### The version and the etag are derived, not declared

Tensor9 reads four values off `src`: the source object's bucket, key, `version_id`, and `etag`. Declaring a `version` key or an `etag` key is refused. Both are functions of `src`, so stating either separately could only ever disagree with it.

The two do different jobs, and they are not interchangeable:

* The **version is the pin**. It is what Tensor9 hands S3 when it fetches the object, so it decides which bytes are delivered.
* The **etag is only a change token**. It is never read. It exists so that a republished artifact plans a difference.

This is also why `src` has to be a data source rather than a bucket and key you write out yourself. Both `version_id` and `etag` come from an S3 `HeadObject` performed at plan time, and your own AWS provider is the only thing positioned to make that call. The bucket and the key are just the block's own arguments read back.

### How Tensor9 notices a republished artifact

A delivery re-runs when any of those four values changes, so republishing your artifact plans a difference however you choose to publish it. The etag is what covers the awkward case: an unversioned bucket reports an empty version, so an object overwritten at a stable key moves nothing else Terraform could compare.

Versioning the bucket, or writing a new object key per build, is still worth doing, for a different reason. It gives the delivery a pin:

* A **versioned bucket** produces a new version per publish, and Tensor9 fetches that exact version.
* **Immutable keys** (a build ID or commit SHA in the key, never overwritten) need no pin, because the current version of that key is the only version there will ever be.
* An **unversioned bucket written at a stable key** has neither. Tensor9 fetches whatever the key holds at the moment the appliance controller reads it, which is not necessarily the object the plan compared.

<Note>
  On an unversioned bucket with stable keys, republishing between plan and apply delivers the newer bytes rather than the ones the plan saw. The delivery still lands and the build still runs; it is just not the artifact you were looking at. Either of the other two arrangements closes that window.
</Note>

## What ships today

| Requirement    | Detail                                                                                                                                                                 |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Source         | `src` must reference a `data "aws_s3_object"` block.                                                                                                                   |
| Destination    | `dest` must reference an `aws_codecommit_repository` resource.                                                                                                         |
| Module         | Both blocks must be declared in the same module as the annotated local.                                                                                                |
| Appliance      | The appliance must be on AWS, with a [connected](/fundamentals/connectivity) link.                                                                                     |
| Counted blocks | If `src` and `dest` are `count`-gated, both counts must agree, and each reference must select a single instance with `one(...)` or `[0]`. `for_each` is not supported. |
| Source use     | Tensor9 evaluates the `data "aws_s3_object"` block with your own credentials against your own bucket, so no resource that runs inside the appliance may also read it.  |

Declaring a delivery that Tensor9 cannot perform is a **compile error, not a silent skip**. A declaration compiled for an appliance that has nowhere to put the commit fails the build rather than quietly producing a deployment that reads as healthy and serves nothing.

## It stays inert in your own deployment

The annotation is a comment, and the declaration is an ordinary Terraform local. Your own `terraform plan` and `terraform apply` see a local that nobody reads, and behave exactly as they did before you added it. That is deliberate: an integration that stopped your ordinary deployments from planning would not be one you could adopt.

Gate the delivery the same way you gate the rest of the feature. In the example above, `count = local.webapp_seeded ? 1 : 0` on the source leaves the whole declaration inert when the feature is off.

One consequence is worth knowing. During a Tensor9 deployment, the annotated local is replaced by the delivery itself, so the keys you wrote are no longer readable from it, and referencing `local.webapp_delivery.src` elsewhere in your stack is refused at compile time. The delivery does expose `repository_name`, `branch_name`, `commit_id`, and `tree_digest`, but none of those exist in a deployment without Tensor9, so a stack you also deploy yourself should leave the local unread.

A resource that reads one of those values must not also consume the destination repository, and must not appear in `after`. Either arrangement asks for the artifact to land both before and after the same resource, so Tensor9 refuses the declaration at compile time rather than emitting a graph Terraform would report as a cycle far from its cause.

## Related Topics

* [**Artifacts**](/fundamentals/artifacts): How artifacts move from your environment into an appliance
* [**Origin Stacks**](/fundamentals/origin-stacks): How to define portable infrastructure code
* [**Deployments**](/fundamentals/deployments): How compilation and deployment works
