# Deploy from a shared world

Get what your app wrote to the vendor, with a receipt for every change, and a check in the way.

This page is executed as written by `packages/cli/src/journeys/tutorials.test.ts`; the recording
is made from the same run.

![The page, recorded](../media/deploy-from-a-shared-world/deploy-from-a-shared-world.gif)

Nothing your app does in a world reaches a vendor until an entry lands on a world whose twin has
a **root**: the vendor's real account, with a credential sealed beside it. That world is the
team's shared world, and this page sets its GitHub twin's root, pushes to it, and reads the
receipts. GitHub itself is a third world here, so the whole chain runs on one machine; the
commands are the same when the root is `https://api.github.com`.

## The three worlds

```json file=package.json
{ "name": "acme-web", "private": true, "type": "module", "dependencies": { "@octokit/rest": "^21" } }
```

```js file=file-issue.mjs
import { Octokit } from '@octokit/rest';
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
const { data: issue } = await octokit.issues.create({ owner: 'acme', repo: 'web', title: process.argv[2] ?? 'Launch checklist' });
console.log(`filed #${issue.number}`);
```

```bash
npm install
npm install -g @volter/world
npm install -D @volter/twin-github
mkdir ../reality && cd ../reality && volter world init --bare acme/reality --twins github
volter world serve --port 4400 &
mkdir ../team && cd ../team && volter world init --bare acme/team --twins github
volter world serve --port 4300 &
cd ../acme-web
volter world init
volter world up
volter remote add origin http://127.0.0.1:4300/acme/team --token "$(cat ../team/.volter/token)"
```

```text
serving  acme/reality  http://127.0.0.1:4400/acme/reality
serving  acme/team  http://127.0.0.1:4300/acme/team
acme-web  1 twin up, story loaded
```

## Set the root

On the shared world, tell the GitHub twin where the vendor is, and which repository the account
is, and seal the credential. The
credential is read from stdin, encrypted at once under a key in your config directory, and never
readable back. Here the vendor is a world, so its credential is that world's own token. The deploy policy says what a landed entry needs: `gated` waits for a verified and
approved changeset; `auto` would deploy on arrival; `hold` waits to be run.

```bash
cd ../team
volter twin github root http://127.0.0.1:4400/acme/reality/github --scope repos/acme/web --deploy gated
cat ../reality/.volter/token | volter twin github credential
volter twin github
cd ../acme-web
```

```text
github  root http://127.0.0.1:4400/acme/reality/github/repos/acme/web  deploy gated  credential sealed
```

## Write, cut a changeset, push

```bash
volter world run -- node file-issue.mjs
volter world changeset -m "The launch checklist"
volter world push
volter world log --receipts
```

```text
filed #1
changeset  the-launch-checklist  1 change
pushed  the-launch-checklist  1 change → origin
github   issue.create   acme/web#1    landed
```

The entry landed on the shared world and waits: the policy is `gated`, and the changeset has not
been verified or approved.

## Verify, approve, deploy

The shared world runs the changeset's checks and records the result, a reviewer signs the current
hash, and deploy performs it against the root.

```bash
cd ../team
volter world verify the-launch-checklist
volter world approve the-launch-checklist --as ada
volter world deploy
volter world log --receipts
cd ../acme-web
```

```text
verified  the-launch-checklist  checks passed
approved  the-launch-checklist  by ada
deployed  the-launch-checklist  1 change
github   issue.create   acme/web#1    deployed
```

The shared world's dashboard does the same on its **Changesets** page, as a pull request is reviewed: each
changeset with what it changes at each vendor, its checks and its approvals, and **Verify**,
**Approve** and **Deploy** buttons. Opened from the platform, an approval is signed as the person you
are; a read-only link sees it all and changes nothing. Deploy asks you to type the changeset's name,
and afterwards the page lists its receipts.

Reality has the issue:

```bash
curl -s http://127.0.0.1:4400/acme/reality/github/repos/acme/web/issues -H "authorization: Bearer $(cat ../reality/.volter/token)"
```

```text
"title":"Launch checklist"
```

And your world sees the receipt on its next fetch:

```bash
volter world pull
volter world log --receipts
```

```text
github   issue.create   acme/web#1    deployed
```

## The check that refuses

A check is a file under `.volter/checks/` in the world that deploys. The shipped one refuses any
entry carrying a credential-shaped string. A rebased or unverified changeset is refused the same
way, with the reason on the entry.

```bash
volter world run -- node file-issue.mjs "Use sk-live-4e2c9a1b7f3d8e6a5c4b3a2f1e0d9c8b for now"
volter world changeset -m "Oops"
volter world push
cd ../team
volter world verify oops
cd ../acme-web
```

```text
refused  oops  no-secrets: a credential-shaped string in title
```

## When the vendor moved

If the account changed under a changeset since it was cut, a push refuses rather than overwrite,
and `rebase` names the conflict by record and field. [Read the vendor through a shared
world](./read-the-vendor-through-a-shared-world.md) walks it.

## When the vendor is down

A deploy is a transaction. Stop the vendor and deploy: the entry's receipt says `failed` with the
reason, and nothing after it is attempted. Bring the vendor back and deploy again: what already
crossed is not sent twice, the failed entry is retried, the rest run.

```bash
volter world run -- node file-issue.mjs "Second checklist"
volter world changeset -m "The second checklist"
volter world push
cd ../team
volter world verify the-second-checklist
volter world approve the-second-checklist --as ada
kill %1
volter world deploy
volter world log --receipts
```

```text
github   issue.create   acme/web#3    failed
```

```bash
cd ../reality
volter world serve --port 4400 &
cd ../team
for i in $(seq 1 60); do curl -s http://127.0.0.1:4400/-/ping >/dev/null && break; sleep 1; done; sleep 2
volter world deploy
volter world log --receipts
cd ../acme-web
```

```text
deployed  the-second-checklist  1 change
github   issue.create   acme/web#3    deployed
```

## Clean up

```bash
volter world down
kill $(jobs -p)
```

<!-- playback:BEGIN — GENERATED by `bun scripts/docs-media.ts`; do not edit between markers -->

## Playback

Each command above, as the recording shows it.

<details><summary><code>npm install</code></summary>

![step 1](../media/deploy-from-a-shared-world/step-01.png)

</details>
<details><summary><code>npm install -g @volter/world</code></summary>

![step 2](../media/deploy-from-a-shared-world/step-02.png)

</details>
<details><summary><code>npm install -D @volter/twin-github</code></summary>

![step 3](../media/deploy-from-a-shared-world/step-03.png)

</details>
<details><summary><code>mkdir ../reality &amp;&amp; cd ../reality &amp;&amp; volter world init --bare acme/reality --twins github</code></summary>

![step 4](../media/deploy-from-a-shared-world/step-04.png)

</details>
<details><summary><code>volter world serve --port 4400 &amp;</code></summary>

![step 5](../media/deploy-from-a-shared-world/step-05.png)

</details>
<details><summary><code>mkdir ../team &amp;&amp; cd ../team &amp;&amp; volter world init --bare acme/team --twins github</code></summary>

![step 6](../media/deploy-from-a-shared-world/step-06.png)

</details>
<details><summary><code>volter world serve --port 4300 &amp;</code></summary>

![step 7](../media/deploy-from-a-shared-world/step-07.png)

</details>
<details><summary><code>cd ../acme-web</code></summary>

![step 8](../media/deploy-from-a-shared-world/step-08.png)

</details>
<details><summary><code>volter world init</code></summary>

![step 9](../media/deploy-from-a-shared-world/step-09.png)

</details>
<details><summary><code>volter world up</code></summary>

![step 10](../media/deploy-from-a-shared-world/step-10.png)

</details>
<details><summary><code>volter remote add origin http://127.0.0.1:4300/acme/team --token "$(cat ../team/.volter/token)"</code></summary>

![step 11](../media/deploy-from-a-shared-world/step-11.png)

</details>
<details><summary><code>cd ../team</code></summary>

![step 12](../media/deploy-from-a-shared-world/step-12.png)

</details>
<details><summary><code>volter twin github root http://127.0.0.1:4400/acme/reality/github --scope repos/acme/web --deploy gated</code></summary>

![step 13](../media/deploy-from-a-shared-world/step-13.png)

</details>
<details><summary><code>cat ../reality/.volter/token | volter twin github credential</code></summary>

![step 14](../media/deploy-from-a-shared-world/step-14.png)

</details>
<details><summary><code>volter twin github</code></summary>

![step 15](../media/deploy-from-a-shared-world/step-15.png)

</details>
<details><summary><code>cd ../acme-web</code></summary>

![step 16](../media/deploy-from-a-shared-world/step-16.png)

</details>
<details><summary><code>volter world run -- node file-issue.mjs</code></summary>

![step 17](../media/deploy-from-a-shared-world/step-17.png)

</details>
<details><summary><code>volter world changeset -m "The launch checklist"</code></summary>

![step 18](../media/deploy-from-a-shared-world/step-18.png)

</details>
<details><summary><code>volter world push</code></summary>

![step 19](../media/deploy-from-a-shared-world/step-19.png)

</details>
<details><summary><code>volter world log --receipts</code></summary>

![step 20](../media/deploy-from-a-shared-world/step-20.png)

</details>
<details><summary><code>cd ../team</code></summary>

![step 21](../media/deploy-from-a-shared-world/step-21.png)

</details>
<details><summary><code>volter world verify the-launch-checklist</code></summary>

![step 22](../media/deploy-from-a-shared-world/step-22.png)

</details>
<details><summary><code>volter world approve the-launch-checklist --as ada</code></summary>

![step 23](../media/deploy-from-a-shared-world/step-23.png)

</details>
<details><summary><code>volter world deploy</code></summary>

![step 24](../media/deploy-from-a-shared-world/step-24.png)

</details>
<details><summary><code>volter world log --receipts</code></summary>

![step 25](../media/deploy-from-a-shared-world/step-25.png)

</details>
<details><summary><code>cd ../acme-web</code></summary>

![step 26](../media/deploy-from-a-shared-world/step-26.png)

</details>
<details><summary><code>curl -s http://127.0.0.1:4400/acme/reality/github/repos/acme/web/issues -H "authorization: Bearer $(cat ../reality/.volter/token)"</code></summary>

![step 27](../media/deploy-from-a-shared-world/step-27.png)

</details>
<details><summary><code>volter world pull</code></summary>

![step 28](../media/deploy-from-a-shared-world/step-28.png)

</details>
<details><summary><code>volter world log --receipts</code></summary>

![step 29](../media/deploy-from-a-shared-world/step-29.png)

</details>
<details><summary><code>volter world run -- node file-issue.mjs "Use sk-live-4e2c9a1b7f3d8e6a5c4b3a2f1e0d9c8b for now"</code></summary>

![step 30](../media/deploy-from-a-shared-world/step-30.png)

</details>
<details><summary><code>volter world changeset -m "Oops"</code></summary>

![step 31](../media/deploy-from-a-shared-world/step-31.png)

</details>
<details><summary><code>volter world push</code></summary>

![step 32](../media/deploy-from-a-shared-world/step-32.png)

</details>
<details><summary><code>cd ../team</code></summary>

![step 33](../media/deploy-from-a-shared-world/step-33.png)

</details>
<details><summary><code>volter world verify oops</code></summary>

![step 34](../media/deploy-from-a-shared-world/step-34.png)

</details>
<details><summary><code>cd ../acme-web</code></summary>

![step 35](../media/deploy-from-a-shared-world/step-35.png)

</details>
<details><summary><code>volter world run -- node file-issue.mjs "Second checklist"</code></summary>

![step 36](../media/deploy-from-a-shared-world/step-36.png)

</details>
<details><summary><code>volter world changeset -m "The second checklist"</code></summary>

![step 37](../media/deploy-from-a-shared-world/step-37.png)

</details>
<details><summary><code>volter world push</code></summary>

![step 38](../media/deploy-from-a-shared-world/step-38.png)

</details>
<details><summary><code>cd ../team</code></summary>

![step 39](../media/deploy-from-a-shared-world/step-39.png)

</details>
<details><summary><code>volter world verify the-second-checklist</code></summary>

![step 40](../media/deploy-from-a-shared-world/step-40.png)

</details>
<details><summary><code>volter world approve the-second-checklist --as ada</code></summary>

![step 41](../media/deploy-from-a-shared-world/step-41.png)

</details>
<details><summary><code>kill %1</code></summary>

![step 42](../media/deploy-from-a-shared-world/step-42.png)

</details>
<details><summary><code>volter world deploy</code></summary>

![step 43](../media/deploy-from-a-shared-world/step-43.png)

</details>
<details><summary><code>volter world log --receipts</code></summary>

![step 44](../media/deploy-from-a-shared-world/step-44.png)

</details>
<details><summary><code>cd ../reality</code></summary>

![step 45](../media/deploy-from-a-shared-world/step-45.png)

</details>
<details><summary><code>volter world serve --port 4400 &amp;</code></summary>

![step 46](../media/deploy-from-a-shared-world/step-46.png)

</details>
<details><summary><code>cd ../team</code></summary>

![step 47](../media/deploy-from-a-shared-world/step-47.png)

</details>
<details><summary><code>for i in $(seq 1 60); do curl -s http://127.0.0.1:4400/-/ping &gt;/dev/null &amp;&amp; break; sleep 1; done; sleep 2</code></summary>

![step 48](../media/deploy-from-a-shared-world/step-48.png)

</details>
<details><summary><code>volter world deploy</code></summary>

![step 49](../media/deploy-from-a-shared-world/step-49.png)

</details>
<details><summary><code>volter world log --receipts</code></summary>

![step 50](../media/deploy-from-a-shared-world/step-50.png)

</details>
<details><summary><code>cd ../acme-web</code></summary>

![step 51](../media/deploy-from-a-shared-world/step-51.png)

</details>
<details><summary><code>volter world down</code></summary>

![step 52](../media/deploy-from-a-shared-world/step-52.png)

</details>
<details><summary><code>kill $(jobs -p)</code></summary>

![step 53](../media/deploy-from-a-shared-world/step-53.png)

</details>

<!-- playback:END -->
