Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions .github/workflows/demo.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: demo

# Distinct title so it isn't confused with the ci workflow on the same commit.
run-name: demo — ${{ github.head_ref || github.ref_name }}

# The tutorial (#84) is the first artifact whose correctness depends on three
# repositories at once: this one, forgejo-warden and the published reference.
# This job runs its six steps against forgejo-warden's own Docker Compose
# sandbox, with the policy cut out of the tutorial page by scripts/demo.sh,
# so the page and the job cannot drift apart. Like corpus.yml it has no
# pull_request or push trigger: it pulls a container image and a second
# repository, and either being unavailable would fail the main gate on a
# change that has nothing to do with it.
on:
workflow_dispatch:
schedule:
# Mondays, early UTC, after the corpus run.
- cron: '41 6 * * 1'

concurrency:
group: demo-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

env:
# forgejo-warden at the commit the tutorial was last run against. Move it
# when the tutorial changes or warden's loader does.
WARDEN_REF: 3e20374d79ea7780245acfe49b053d76ad6b516f
WARDEN_DIR: .forgejo-warden

jobs:
demo:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v6

- name: Check out forgejo-warden
uses: actions/checkout@v6
with:
repository: INTENTIUS/forgejo-warden
ref: ${{ env.WARDEN_REF }}
path: ${{ env.WARDEN_DIR }}

- uses: actions/setup-node@v6
with:
node-version: '24'

# warden's dependencies include the published @intentius/tsad-reference,
# which is the evaluator the tutorial runs on.
- name: Install and build forgejo-warden
working-directory: ${{ env.WARDEN_DIR }}
run: |
npm ci
npm run build

# bootstrap.sh appends FORGEJO_E2E_URL and FORGEJO_E2E_TOKEN to
# $GITHUB_ENV when it runs under Actions, so later steps inherit them.
- name: Stand up the sandbox
working-directory: ${{ env.WARDEN_DIR }}
run: bash e2e/bootstrap.sh

- name: The six steps
env:
TUTORIAL: docs/content/try-it/_index.md
run: WARDEN_DIR="${{ github.workspace }}/${{ env.WARDEN_DIR }}" bash scripts/demo.sh

- name: Tear down
if: always()
working-directory: ${{ env.WARDEN_DIR }}
run: docker compose -f e2e/docker-compose.yml down -v
2 changes: 1 addition & 1 deletion docs/assets/css/site.css
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ th { background: var(--surface-2); font-weight: 600; }

/* typescript-as-data additions: consumer status dots and captioned fences. */
.status-dot.status-reference { background: var(--pass); }
.status-dot.status-pr-open { background: var(--warn-line); }
.status-dot.status-merged { background: var(--warn-line); }
figure.code { margin: 1rem 0; }
figure.code figcaption { font-size: 0.85rem; color: var(--muted); padding: 0.25rem 0.75rem; border: 1px solid var(--line); border-bottom: 0; border-radius: 8px 8px 0 0; background: var(--surface); }
figure.code figcaption + .highlight pre, figure.code figcaption + pre { margin-top: 0; border-top-left-radius: 0; border-top-right-radius: 0; }
2 changes: 1 addition & 1 deletion docs/content/for-your-platform/forgejo-warden.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ weight: 2
diataxis: how-to
---

[forgejo-warden](https://github.com/INTENTIUS/forgejo-warden) keeps a Forgejo org and its repos in a declared state. It diffs against live and applies guardrails. Runs are dry by default, and deletes happen only where the policy says `owned`. Its policy is one file, and the `.ts` form of that file is [forgejo-warden#33](https://github.com/INTENTIUS/forgejo-warden/pull/33).
[forgejo-warden](https://github.com/INTENTIUS/forgejo-warden) keeps a Forgejo org and its repos in a declared state. It diffs against live and applies guardrails. Runs are dry by default, and deletes happen only where the policy says `owned`. Its policy is one file, and the `.ts` form of that file landed in [forgejo-warden#33](https://github.com/INTENTIUS/forgejo-warden/pull/33).

## Author

Expand Down
26 changes: 16 additions & 10 deletions docs/content/try-it/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,26 @@ forgejo-warden keeps a Forgejo org in a declared state. Its e2e stack stands up
Clone https://github.com/INTENTIUS/forgejo-warden and run `npm ci`. Confirm
Docker is running (`docker info`). Start the sandbox with
`eval "$(npm run --silent e2e:up)"`, which exports FORGEJO_E2E_URL and
FORGEJO_E2E_TOKEN. Write a governance policy, as governance.ts, for one org with one repo that
disables the wiki, allows squash merges, and protects `main` with one
required approval and a `ci` status check. Run a dry-run reconcile against the sandbox and show me
FORGEJO_E2E_TOKEN. Create an org `my-org` with an empty repo `api` through the API. Write a
governance policy, as governance.ts, for that org and repo that disables
the wiki, allows squash merges, and protects `main` with one required
approval and a `ci` status check. Run a dry-run reconcile against the sandbox and show me
the plan. Do not apply until I say so. When I do, apply, then tell me to
change the repo's wiki setting in the web UI at http://localhost:3000, run
another dry-run, and show me the drift. Finish with `npm run e2e:down`.
```

## By hand

1. Stand up the sandbox.
1. Stand up the sandbox, and give it an org with an empty repo. warden keeps what exists in a declared state; it does not create the org, and a repo comes from a `repoBaselines` entry or, as here, from one API call.

```bash
git clone https://github.com/INTENTIUS/forgejo-warden && cd forgejo-warden
npm ci
npm ci && npm run build
eval "$(npm run --silent e2e:up)"
auth=(-H "Authorization: token $FORGEJO_E2E_TOKEN" -H "Content-Type: application/json")
curl -fsS "${auth[@]}" -X POST "$FORGEJO_E2E_URL/api/v1/orgs" -d '{"username":"my-org"}'
curl -fsS "${auth[@]}" -X POST "$FORGEJO_E2E_URL/api/v1/orgs/my-org/repos" -d '{"name":"api"}'
```

2. Write the policy, in YAML or in TypeScript. The loader reads either; the `.ts` form is folded to its value without being run ([forgejo-warden#33](https://github.com/INTENTIUS/forgejo-warden/pull/33)).
Expand All @@ -39,7 +43,7 @@ another dry-run, and show me the drift. Finish with `npm run e2e:down`.

export const policy = {
orgs: {
"warden-admin": {
"my-org": {
repos: {
api: {
hasWiki: false,
Expand All @@ -55,18 +59,18 @@ another dry-run, and show me the drift. Finish with `npm run e2e:down`.
3. See the plan. Reads only; changes nothing.

```bash
forgejo-warden reconcile --config governance.ts \
node bin/forgejo-warden.js reconcile --config governance.ts \
--base-url "$FORGEJO_E2E_URL" --token-env FORGEJO_E2E_TOKEN --mode dry-run
```

4. Apply it, then open http://localhost:3000 and look at the repo.
4. Apply it, then open http://localhost:3000/my-org/api and look at the repo.

```bash
forgejo-warden reconcile --config governance.ts \
node bin/forgejo-warden.js reconcile --config governance.ts \
--base-url "$FORGEJO_E2E_URL" --token-env FORGEJO_E2E_TOKEN --mode apply
```

5. Drift. In the web UI, turn the wiki back on. Run step 3 again: the plan shows the one change, read from the live instance. There is no state file to be stale.
5. Drift. In the web UI, turn the wiki back on (or `PATCH` the repo with `{"has_wiki":true}`). Run step 3 again: the plan shows the one change, read from the live instance. There is no state file to be stale.

6. Reconcile with step 4, then tear down.

Expand All @@ -78,4 +82,6 @@ another dry-run, and show me the drift. Finish with `npm run e2e:down`.

The policy is data. Nothing in it ran, and nothing needed to; the plan is computed from the declared values and the live org. `--config-mode check` folds the file and also runs it, and refuses if the two disagree, which is the guarantee made visible. `--config-mode run` skips the fold for anyone who only wants typed JSON. Deletes were never proposed, because nothing was marked `owned`. The same file works against Codeberg or any self-hosted Forgejo by changing `--base-url`.

[A workflow](https://github.com/INTENTIUS/typescript-as-data/blob/main/.github/workflows/demo.yml) runs these six steps every week against the same sandbox, with the policy cut out of this page by `scripts/demo.sh`, so the page cannot rot.

The second chapter, importing an existing artifact and rebuilding it byte for byte through generated source, is chant's, and lands here with its measurement when [the round-trip property](https://github.com/INTENTIUS/typescript-as-data/issues/80) does.
4 changes: 2 additions & 2 deletions docs/data/consumers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ consumers:
- id: forgejo-warden
name: forgejo-warden
kind: org governance
status: pr-open
status_label: "Reads `governance.ts` through the published reference; the first consumer that is neither this repository nor chant. Its `.ts` policy is forgejo-warden#33, merged the day the packages are on npm."
status: merged
status_label: "Reads `governance.ts` through the published reference; the first consumer that is neither this repository nor chant. Its `.ts` loader is on main (forgejo-warden#33), and the tutorial's six steps run against it weekly."
repo: https://github.com/INTENTIUS/forgejo-warden
- id: chant
name: chant
Expand Down
2 changes: 1 addition & 1 deletion docs/layouts/_partials/header.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
{{ $page := site.GetPage (printf "/for-your-platform/%s" .id) }}
<li{{ if eq $page $cur }} class="current"{{ end }}>
{{ with $page }}<a href="{{ .RelPermalink }}">{{ end }}{{ .name }}{{ if $page }}</a>{{ end }}
{{ if eq .status "reference" }}<span class="badge badge-proven">reference</span>{{ else if eq .status "pr-open" }}<span class="badge">PR open</span>{{ end }}
{{ if eq .status "reference" }}<span class="badge badge-proven">reference</span>{{ else if eq .status "merged" }}<span class="badge">on main</span>{{ end }}
</li>
{{ end }}
</ul>
Expand Down
2 changes: 1 addition & 1 deletion docs/layouts/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ <h2>Pick your platform</h2>
{{ $page := site.GetPage (printf "/for-your-platform/%s" .id) }}
<li class="picker-row status-{{ .status }}" data-name="{{ .name | lower }}">
{{ with $page }}<a href="{{ .RelPermalink }}">{{ end }}<span class="picker-name">{{ .name }}</span>{{ if $page }}</a>{{ end }}
{{ if eq .status "reference" }}<span class="badge badge-proven">reference platform</span>{{ else if eq .status "pr-open" }}<span class="badge">PR open</span>{{ end }}
{{ if eq .status "reference" }}<span class="badge badge-proven">reference platform</span>{{ else if eq .status "merged" }}<span class="badge">on main</span>{{ end }}
<span class="picker-status">{{ .status_label | markdownify }}</span>
</li>
{{ end }}
Expand Down
62 changes: 62 additions & 0 deletions scripts/demo.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
# The six steps of docs/content/try-it/_index.md, run against forgejo-warden's
# e2e sandbox (#84). The policy is cut out of the tutorial page itself, so the
# job and the page cannot say different things: if the fence changes, this
# runs the new one.
#
# WARDEN_DIR=<forgejo-warden checkout, npm ci and npm run build done> \
# TUTORIAL=<path to try-it/_index.md> bash scripts/demo.sh
#
# The sandbox must already be up (npm run e2e:up in WARDEN_DIR exported
# FORGEJO_E2E_URL and FORGEJO_E2E_TOKEN); tearing it down is the caller's.
set -euo pipefail

: "${WARDEN_DIR:?a forgejo-warden checkout}"
: "${TUTORIAL:?the tutorial page}"
: "${FORGEJO_E2E_URL:?run e2e:up first}"
: "${FORGEJO_E2E_TOKEN:?run e2e:up first}"

ORG=my-org
REPO=api
WORK="$(mktemp -d)"
WARDEN="node $WARDEN_DIR/bin/forgejo-warden.js"
API="$FORGEJO_E2E_URL/api/v1"
auth=(-H "Authorization: token $FORGEJO_E2E_TOKEN" -H "Content-Type: application/json")

step() { echo; echo "== $*"; }

step "1. the sandbox gets an org and an empty repo, which is what warden manages"
curl -fsS "${auth[@]}" -X POST "$API/orgs" -d "{\"username\":\"$ORG\"}" >/dev/null
curl -fsS "${auth[@]}" -X POST "$API/orgs/$ORG/repos" -d "{\"name\":\"$REPO\"}" >/dev/null

step "2. the policy, cut from the tutorial's governance.ts fence"
awk '/^ *```ts \{title="governance.ts"\}$/{f=1; next} f && /^ *```$/{exit} f' "$TUTORIAL" | sed 's/^ //' > "$WORK/governance.ts"
test -s "$WORK/governance.ts"
# The type import resolves against the checkout, the way a user's node_modules would.
mkdir -p "$WORK/node_modules/@intentius" && ln -s "$WARDEN_DIR" "$WORK/node_modules/@intentius/forgejo-warden"
cat "$WORK/governance.ts"

step "3. dry run, folding the file and also running it, refusing on disagreement"
$WARDEN reconcile --config "$WORK/governance.ts" --config-mode check \
--base-url "$FORGEJO_E2E_URL" --token-env FORGEJO_E2E_TOKEN --mode dry-run | tee "$WORK/plan1.txt"
grep -q "hasWiki" "$WORK/plan1.txt"

step "4. apply"
$WARDEN reconcile --config "$WORK/governance.ts" --config-mode check \
--base-url "$FORGEJO_E2E_URL" --token-env FORGEJO_E2E_TOKEN --mode apply | tee "$WORK/apply1.txt"
! grep -q "FAILED" "$WORK/apply1.txt"
test "$(curl -fsS "${auth[@]}" "$API/repos/$ORG/$REPO" | node -pe 'JSON.parse(require("fs").readFileSync(0)).has_wiki')" = false

step "5. drift: the wiki is turned back on out of band, and the next dry run reads it from live"
curl -fsS "${auth[@]}" -X PATCH "$API/repos/$ORG/$REPO" -d '{"has_wiki":true}' >/dev/null
$WARDEN reconcile --config "$WORK/governance.ts" --config-mode check \
--base-url "$FORGEJO_E2E_URL" --token-env FORGEJO_E2E_TOKEN --mode dry-run | tee "$WORK/plan2.txt"
grep -q "hasWiki" "$WORK/plan2.txt"

step "6. reconcile"
$WARDEN reconcile --config "$WORK/governance.ts" --config-mode check \
--base-url "$FORGEJO_E2E_URL" --token-env FORGEJO_E2E_TOKEN --mode apply | tee "$WORK/apply2.txt"
! grep -q "FAILED" "$WORK/apply2.txt"
test "$(curl -fsS "${auth[@]}" "$API/repos/$ORG/$REPO" | node -pe 'JSON.parse(require("fs").readFileSync(0)).has_wiki')" = false

echo; echo "demo: all six steps held"