# Ephemera — Source-provider connection on AWS (CodeConnections + aws CLI)

> Self-executing Markdown. The **AWS binding** of the *source-connection* intent — a handshake connection
> between AWS and a git source provider (GitHub / GitLab / Bitbucket) that CI/CD (CodeBuild, CodePipeline)
> consumes to pull source. AWS CodeConnections (**formerly** CodeStar Connections; the legacy
> `codestar-connections` CLI/API was retired after Apr 2025 — this plan uses `aws codeconnections`). The
> cloud is the source of truth; this file is intent + write-back ledger + audit.

> **Provides / Requires**: **Provides** `source-connection(CONN_NAME, ConnectionArn)` — a consumer plan
> ([`ci-runner.aws.md`](./ci-runner.aws.md); a future `codepipeline.aws.md`) **Requires** it and passes the ARN as its source, then
> **discovers** it by name at bind time (`list-connections`). **Requires** nothing upstream — but the
> connection is inert until a **human completes the OAuth handshake** (§2), so a consumer must gate on
> `ConnectionStatus == AVAILABLE`.

---

## 🤖 Director prompt

Observe before acting; verify each step before advancing; stop at 🔴/💥 for human go; write realized values
back into Live State. The defining trait of this plan: **`create-connection` mints a `PENDING` connection
that only a human can move to `AVAILABLE`** — by authorizing AWS in the git provider's OAuth flow from the
console. That handshake is §2's 🔴 gate; it is not scriptable. `create-connection` is **non-idempotent**
(a new ARN every call, and AWS *allows duplicate connection names*), so `apply` observes by name **+ our
`ManagedBy` tag** and reuses, minting only when absent (the ACM `request-certificate` lesson).

> **Candor: PARTIALLY DOGFOODED LIVE 2026-07-12** (us-west-2, ~$0, torn down + absence-verified) on the
> handshake-skipped path: §1 `create-connection` → `PENDING` **proven** (contract 2, the negative — created
> with tags, status read back `PENDING`), consumer discovery by name+`ManagedBy` tag **proven** (ci-runner's
> §1 found and correctly refused it), and 💥 teardown **proven** (ownership-checked delete + absence verify;
> ⚠ datum: a `PENDING`-only teardown leaves **no git-side app install** to clean up — that warning applies
> only after §2 has run). **Still unrun:** §2 handshake, `AVAILABLE` (contract 3), same-ARN re-apply
> (contract 4 — the connection was deleted before a re-apply was exercised). CLI shapes now proven live for
> create/get/list/list-tags/delete.

```
Legend  🟢 create · 🟡 config · 🔴 GATE (human go) · 💥 destructive (human go) · ⏳ wait · ✔ verify
```

## What you need, and why  *(read if CodeConnections is new to you)*

A **connection** is a stored, reusable authorization that lets AWS build/deploy services read your git repos
without you pasting a personal access token into each one. You create it once (CLI), authorize it once
(console OAuth handshake — you must be an **admin/owner of the git org/account** to install the app), and
then CodeBuild/CodePipeline reference its ARN. It is the AWS-managed replacement for long-lived source tokens.

- **`aws` CLI v2, authenticated** (`aws sts get-caller-identity` succeeds). No IAM calls here — CodeConnections
  is a regional service; session creds are fine (no `--no-session` dance).
- **A region** (`AWS_REGION`) — connections are regional; a consumer in another region needs its own.
- **Admin access to the git org/account** you're connecting (the §2 handshake installs an app there).
- **AWS console access** — §2 cannot be done from the CLI (AWS's constraint, not ours).

## Intent

Stand up a **reusable, AWS-managed authorization to a git source provider** so CI/CD pulls source over a
maintained connection instead of a hand-rolled token. One connection, authorized once, referenced by many
pipelines. The *intent* is provider-neutral (GitHub / GitLab / Bitbucket via the `PROVIDER` knob); the
realization is AWS CodeConnections. There is **no Cloudflare sibling** — CF folds git-provider auth into the
Workers Builds / Pages dashboard integration (a GitHub-App install tied to the build config), never a
standalone, ARN-bearing connection resource you'd `Provides`/`Requires`. That asymmetry is why this intent is
AWS-shaped (like `lambda-layer.aws.md`), not a missing binding.

**Acceptance contract** (this AWS-shaped intent; no sibling binding — see the portability note):
1. `get-connection` returns the connection for our ARN with the chosen `ProviderType`.
2. **Negative (the human-gate proof).** *Immediately after create, before the handshake,* status is
   `PENDING` — the connection is genuinely inert; nothing AWS-side flips it to usable without the human OAuth
   step. This negative is what makes §2's gate real, not ceremony.
3. **Positive.** *After* the handshake, `get-connection` reports `AVAILABLE` — CodeBuild/CodePipeline can now
   use the ARN.
4. **Determinism.** Re-running `apply` reuses the same ARN (observe by name + `ManagedBy` tag) — it does not
   mint a duplicate connection.

## Provisioning Inputs

| # | Question | Options (closed enum) | Default | Sets | Gates |
|---|----------|-----------------------|---------|------|-------|
| 1 | Git source provider | `github` / `gitlab` / `bitbucket` | `github` | `PROVIDER` | §0 provider-type map + §1 create + §2 handshake flavor |
| 2 | Connection name | text — `[A-Za-z0-9_-]`, ≤ 32 chars | `${PROVIDER}-connection` | `CONN_NAME` | the connection's identity (reuse key) |
| 3 | Environment | `dev` / `stg` / `uat` / `prod` | `dev` | `ENV` | the `Environment` tag (see multi-env note) |

```yaml
# → written into Live State once resolved (the deterministic input to every step below)
resolved_inputs:
  provider:  github         # github | gitlab | bitbucket
  conn_name: github-connection
  env:       dev
  resolved_by: <human who confirmed>
  resolved_at: <timestamp>
```

> **Multi-env note.** A connection is commonly an **account-shared** resource (the source Terragrunt module
> lives in `shared/`), not one-per-env — so `CONN_NAME` is a free-text identity, **not** auto-suffixed with
> `ENV`. Run one shared connection (`github-connection`) or one per env (`github-dev`) by choosing the name;
> the `Environment` tag still records which `ENV` provisioned it. **Self-managed** providers
> (GitHub Enterprise Server, GitLab self-managed) additionally need a **Host** (`create-host`, itself
> console-gated) — a deliberate omission below.

## TAGS — provenance & cost tags

CodeConnections **is** taggable (unlike Lambda layers). Tag **on create** — `create-connection --tags` takes
a **Key/Value list** (`tags_kv`). Renderers inline in §0 (canonical: `scripts/tags.sh`).

## Live State

```yaml
status:        not-created      # published template - run it to realize state
last_action:   partial dogfood 2026-07-12 — §1 create→PENDING proven + tagged, handshake skipped by design, teardown ownership-checked + absence-verified
last_verified: 2026-07-12 (see verify table)
resolved_inputs: { provider: github, conn_name: github-connection, env: dev }
```

> **`pending-handshake`** is a plan-specific intermediate state (EPHEMERA.md permits these): set it after §1
> when the connection exists but is `PENDING`; advance to `live` only when §3 observes `AVAILABLE`.

| key         | value (filled on apply) |
|-------------|-------------------------|
| AWS_REGION  | `—` |
| CONN_NAME   | `${CONN_NAME}` |
| CONN_ARN    | `—` (`arn:aws:codeconnections:<region>:<acct>:connection/<uuid>`) |
| STATUS      | `—` (`PENDING` → `AVAILABLE`) |

| ✔ check                              | expected                                              | observed (2026-07-12 dogfood)                       | result |
|--------------------------------------|-------------------------------------------------------|-----------------------------------------------------|--------|
| connection exists                    | `get-connection` returns our ARN + `ProviderType`     | ARN returned, `ProviderType=GitHub`                 | **PASS (proven live)** |
| inert before handshake (negative)    | status is `PENDING` right after create                | `PENDING` read back immediately after create        | **PASS (proven live)** |
| available after handshake            | status is `AVAILABLE`                                  | not run — handshake skipped by design               | pending-dogfood |
| apply is deterministic               | re-apply reuses the same ARN (no duplicate minted)    | not exercised (deleted before re-apply)             | pending-dogfood |
| tags present                         | connection carries `ManagedBy=ephemera`               | consumer + teardown both read the tag off the ARN   | **PASS (proven live)** |

## 0. Variables

```bash
set -euo pipefail
export AWS_REGION="${AWS_REGION:-us-west-2}"
export ENV="${ENV:-dev}"
export PROVIDER="${PROVIDER:-github}"                       # github | gitlab | bitbucket
export CONN_NAME="${CONN_NAME:-${PROVIDER}-connection}"      # ≤32 chars, [A-Za-z0-9_-]

case "$PROVIDER" in
  github)    PROVIDER_TYPE="GitHub";;
  gitlab)    PROVIDER_TYPE="GitLab";;
  bitbucket) PROVIDER_TYPE="Bitbucket";;
  *) echo "PROVIDER must be github|gitlab|bitbucket (self-managed hosts are a named omission)"; exit 1;;
esac
printf '%s' "$CONN_NAME" | grep -Eq '^[A-Za-z0-9_-]{1,32}$' || { echo "CONN_NAME must be [A-Za-z0-9_-], ≤32 chars"; exit 1; }

# ── TAGS — resolved once (canonical: scripts/tags.sh) ──
PLAN_SOURCE="source-connection.aws.md"
PLAN_VERSION="2026-07-05"
TAG_COST_CENTER="${TAG_COST_CENTER:-}"; TAG_OWNER="${TAG_OWNER:-}"
TAGS="$(printf '%s\n' "ManagedBy=ephemera" "Source=${PLAN_SOURCE}" "PlanVersion=${PLAN_VERSION}" \
  "CostCenter=${TAG_COST_CENTER}" "Owner=${TAG_OWNER}" "Environment=${ENV}")"
_tags_list() { printf '%s\n' "$TAGS" "$@" | awk '
  { eq=index($0,"="); if(eq==0) next; k=substr($0,1,eq-1); v=substr($0,eq+1); if(v=="") next;
    val[k]=v; if(!(k in seen)){ order[++n]=k; seen[k]=1 } }
  END { for(i=1;i<=n;i++) print order[i]"="val[order[i]] }'; }
tags_kv() { _tags_list "$@" | while IFS='=' read -r k v; do printf 'Key=%s,Value=%s ' "$k" "$v"; done; }
```

## Dependency frontier

```
§1 create-connection 🟢 (observe by name+tag; PENDING) ─> §2 🔴 human OAuth handshake (console) ─⏳→ AVAILABLE ─> §3 ✔ acceptance
```
Non-negotiable edges: **the connection must exist (PENDING) before the human can complete it** (§1 before
§2); **a consumer must not bind it until `AVAILABLE`** (§3's gate is a consumer's precondition). The handshake
is the one irreversible-authorization step — everything before it is a cheap, reversible create. Teardown
reverses (delete the connection; the git-side app install is a separate manual step — see teardown).

## 1. Create the connection  🟢  *(observe-by-name+tag; result is PENDING)*

```bash
# observe first — create-connection is non-idempotent (new ARN each call; AWS allows duplicate names).
# Reuse ONLY a connection with our name AND our ManagedBy tag (dup names could be someone else's).
CONN_ARN=""
for arn in $(aws codeconnections list-connections --region "$AWS_REGION" \
    --provider-type-filter "$PROVIDER_TYPE" \
    --query "Connections[?ConnectionName=='${CONN_NAME}'].ConnectionArn" --output text 2>/dev/null); do
  [ -z "$arn" ] && continue
  OWN="$(aws codeconnections list-tags-for-resource --region "$AWS_REGION" --resource-arn "$arn" \
    --query "Tags[?Key=='ManagedBy'].Value | [0]" --output text 2>/dev/null || true)"
  [ "$OWN" = "ephemera" ] && { CONN_ARN="$arn"; echo "reusing our connection: $CONN_ARN"; break; }
done
if [ -z "$CONN_ARN" ]; then
  CONN_ARN="$(aws codeconnections create-connection --region "$AWS_REGION" \
    --provider-type "$PROVIDER_TYPE" --connection-name "$CONN_NAME" --tags $(tags_kv) \
    --query ConnectionArn --output text)"
  echo "created (PENDING): $CONN_ARN"
fi
```
```bash
# ✔ the connection exists and (on a fresh create) is PENDING — the negative that makes §2 real
STATUS="$(aws codeconnections get-connection --region "$AWS_REGION" --connection-arn "$CONN_ARN" \
  --query 'Connection.ConnectionStatus' --output text)"
echo "status: $STATUS"
[ "$STATUS" = "PENDING" ] || [ "$STATUS" = "AVAILABLE" ] || { echo "unexpected status $STATUS"; exit 1; }
```
> → Live State: `CONN_ARN`; `status: pending-handshake` (if `PENDING`), else jump to §3.

## 2. Complete the OAuth handshake  🔴  *(human, in the AWS console — NOT scriptable)*

> 🔴 Human go. This authorizes AWS to access your git organization (installs an app with repo access) — a
> real, consequential grant, and the only way `PENDING → AVAILABLE`. There is **no CLI** for it. Print the
> console URL + the exact click-path, wait for the human to finish, then poll (§3). If the human declines,
> the connection stays `PENDING` (harmless, reversible via teardown).

**Proven click-path** (GitHub; GitLab/Bitbucket differ only in the provider's OAuth screen):
1. Open the Developer Tools **Connections** console (your region):
   `https://<region>.console.aws.amazon.com/codesuite/settings/connections?region=<region>`
2. Select `${CONN_NAME}` (shows **Pending**), click **Update pending connection**.
3. In the popup, **Install a new app** (or pick an existing app install).
4. Under **Repository access**, choose **All repositories** or **Only select repositories**.
5. A number appears in the **App installation** field — select it, then **Connect**.
6. The connection status becomes **Available**.

```bash
# ⏳ after the human finishes, poll get-connection until AVAILABLE (or ERROR)
for i in $(seq 1 30); do
  STATUS="$(aws codeconnections get-connection --region "$AWS_REGION" --connection-arn "$CONN_ARN" \
    --query 'Connection.ConnectionStatus' --output text)"
  echo "poll $i: $STATUS"
  [ "$STATUS" = "AVAILABLE" ] && break
  [ "$STATUS" = "ERROR" ] && { echo "handshake ERROR — re-run 'Update pending connection' in the console"; break; }
  sleep 10
done
```
> → Live State: `STATUS`; `status: live` once `AVAILABLE`.

## 3. Acceptance verify  ✔  *(the contract)*

```bash
CONN_JSON="$(aws codeconnections get-connection --region "$AWS_REGION" --connection-arn "$CONN_ARN" --output json)"
# 1 — exists with the chosen provider
printf '%s' "$CONN_JSON" | grep -q "\"ProviderType\": \"${PROVIDER_TYPE}\"" && echo "1: connection exists (${PROVIDER_TYPE})" || { echo "1 FAILED"; exit 1; }
# 3 — AVAILABLE after the handshake (the positive)
printf '%s' "$CONN_JSON" | grep -q '"ConnectionStatus": "AVAILABLE"' \
  && echo "3: AVAILABLE — usable by CodeBuild/CodePipeline" \
  || { echo "3: still not AVAILABLE — complete §2's handshake in the console"; exit 1; }
# 5 — tags present
aws codeconnections list-tags-for-resource --region "$AWS_REGION" --resource-arn "$CONN_ARN" \
  --query "Tags[?Key=='ManagedBy'].Value | [0]" --output text | grep -q ephemera \
  && echo "5: tags ok" || { echo "5: connection untagged"; exit 1; }
```
> The **negative** (contract 2 — `PENDING` right after create) is asserted in §1 *before* the handshake; a
> re-run of a live connection skips it (already `AVAILABLE`). → Live State: fill verify rows, `status: live`.

## Update (idempotent reconcile)  🟡

- **Re-apply** → §1's observe-by-name+tag reuses the same ARN; a live connection stays live (no re-handshake).
- **A handshake that fell back to `PENDING`/`ERROR`** (revoked app, expired install) → re-run §2 in the
  console (**Update pending connection**); the ARN is unchanged.
- **Change tags** → `aws codeconnections tag-resource --resource-arn "$CONN_ARN" --tags $(tags_kv)`.
- **Change provider or name** → that's a *different* connection (new ARN + a fresh handshake); tear the old
  one down first. There is no in-place provider/name edit.

## Teardown — observe-first, resumable  💥

> 💥 Human go. `delete-connection` is immediate and the ARN is never reused. Ownership-checked by our
> `ManagedBy` tag — name-discovery must not delete a connection this plan didn't create. ⚠ **Deleting the
> connection does NOT uninstall the app on the git side** — the GitHub/GitLab App install lingers in your org
> with its granted repo access until you remove it manually (GitHub → *Settings → Applications → Installed
> apps*). Delete both, or you leave a dangling grant.

```bash
# observe + ownership-check, then delete
CONN_ARN="$(aws codeconnections list-connections --region "$AWS_REGION" --provider-type-filter "$PROVIDER_TYPE" \
  --query "Connections[?ConnectionName=='${CONN_NAME}'].ConnectionArn" --output text 2>/dev/null | tr '\t' '\n' | head -1)"
if [ -n "$CONN_ARN" ] && [ "$CONN_ARN" != "None" ]; then
  OWN="$(aws codeconnections list-tags-for-resource --region "$AWS_REGION" --resource-arn "$CONN_ARN" \
    --query "Tags[?Key=='ManagedBy'].Value | [0]" --output text 2>/dev/null || true)"
  [ "$OWN" = "ephemera" ] || { echo "connection ${CONN_NAME} lacks ManagedBy=ephemera — not ours to delete"; exit 1; }
  aws codeconnections delete-connection --region "$AWS_REGION" --connection-arn "$CONN_ARN"
  echo "deleted $CONN_ARN — NOW remove the app install in your git org (see warning above)"
fi
```
```bash
# ✔ gone
LEFT="$(aws codeconnections list-connections --region "$AWS_REGION" --provider-type-filter "$PROVIDER_TYPE" \
  --query "Connections[?ConnectionName=='${CONN_NAME}'].ConnectionArn" --output text 2>/dev/null || true)"
{ [ -z "$LEFT" ] || [ "$LEFT" = "None" ]; } && echo "connection gone" || echo "still present: $LEFT (dup name? check tags)"
```
> → Live State: `status: gone`, clear realized ids. (The git-side app install is out-of-band — note it done.)

## Composition — how this plugs into the fleet

`source-connection(CONN_NAME, ConnectionArn)` is a producer with no runtime of its own — it exists to be
**referenced**. [`ci-runner.aws.md`](./ci-runner.aws.md) (and a future `codepipeline.aws.md`) **Requires** it: discover the ARN by name
(`list-connections`), assert `ConnectionStatus == AVAILABLE`, and pass it as the pipeline's source
(`--source Type=CODECONNECTIONS,Location=...` / the CodePipeline source action's `ConnectionArn`). The seam is
a **plain ARN string + a status precondition**, discovered from the cloud — no shared state file. The realized
`codebuild-github-runner` from the source Terragrunt is exactly this consumer.

## Deliberately not included

- **Self-managed providers (a Host)** — GitHub Enterprise Server / GitLab self-managed need a **Host**
  (`create-host` → its own console-gated registration) before a connection. That's a second console gate and a
  distinct resource; add a `HOST` branch when the need is real. The three cloud providers cover the common case.
- **The consumer (CodeBuild / CodePipeline)** — this plan Provides the connection; wiring a build/pipeline that
  consumes it is a separate plan (the `Requires` seam above), out of scope here.
- **Automating the OAuth handshake** — there is no API for it (AWS constraint). The plan presents the proven
  click-path and polls; it cannot click for you.
- **The git-side app uninstall** — surfaced as a teardown gotcha, but removing the app from your git org is a
  provider-console action outside AWS's (and this plan's) reach.
