From 8a44f4380bc2dcefc7ba6f7e3c56795277de0fcb Mon Sep 17 00:00:00 2001 From: lex00 <121451605+lex00@users.noreply.github.com> Date: Sat, 12 Sep 2026 13:16:06 -0600 Subject: [PATCH] demo: the tutorial's six steps as a weekly job against forgejo-warden's sandbox (#84) scripts/demo.sh cuts governance.ts out of docs/content/try-it/_index.md and runs the six steps against the e2e stack: org and empty repo by API, dry run in --config-mode check, apply, drift by PATCH, dry run, apply, with the repo's wiki flag read back after each apply. The page and the job cannot say different things. demo.yml runs it on Mondays and on dispatch, forgejo-warden pinned by SHA (3e20374, the build that folds through the published reference), the same shape as corpus.yml. The tutorial now creates the org and the repo first: warden keeps what exists in a declared state and never creates an org, and repo-settings runs before repo-baseline, so the page as written would have PATCHed a repo that did not exist. The consumer roster says warden's loader is on main. --- .github/workflows/demo.yml | 73 +++++++++++++++++++ docs/assets/css/site.css | 2 +- .../for-your-platform/forgejo-warden.md | 2 +- docs/content/try-it/_index.md | 26 ++++--- docs/data/consumers.yaml | 4 +- docs/layouts/_partials/header.html | 2 +- docs/layouts/home.html | 2 +- scripts/demo.sh | 62 ++++++++++++++++ 8 files changed, 157 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/demo.yml create mode 100755 scripts/demo.sh diff --git a/.github/workflows/demo.yml b/.github/workflows/demo.yml new file mode 100644 index 0000000..e5402ed --- /dev/null +++ b/.github/workflows/demo.yml @@ -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 diff --git a/docs/assets/css/site.css b/docs/assets/css/site.css index 6d93bd4..84cedf0 100644 --- a/docs/assets/css/site.css +++ b/docs/assets/css/site.css @@ -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; } diff --git a/docs/content/for-your-platform/forgejo-warden.md b/docs/content/for-your-platform/forgejo-warden.md index 4071a2b..1d5844c 100644 --- a/docs/content/for-your-platform/forgejo-warden.md +++ b/docs/content/for-your-platform/forgejo-warden.md @@ -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 diff --git a/docs/content/try-it/_index.md b/docs/content/try-it/_index.md index 181619e..db42f1b 100644 --- a/docs/content/try-it/_index.md +++ b/docs/content/try-it/_index.md @@ -14,9 +14,10 @@ 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`. @@ -24,12 +25,15 @@ 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)). @@ -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, @@ -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. @@ -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. diff --git a/docs/data/consumers.yaml b/docs/data/consumers.yaml index 98f5f7a..26ad22d 100644 --- a/docs/data/consumers.yaml +++ b/docs/data/consumers.yaml @@ -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 diff --git a/docs/layouts/_partials/header.html b/docs/layouts/_partials/header.html index 8c666a8..f624392 100644 --- a/docs/layouts/_partials/header.html +++ b/docs/layouts/_partials/header.html @@ -16,7 +16,7 @@ {{ $page := site.GetPage (printf "/for-your-platform/%s" .id) }} {{ with $page }}{{ end }}{{ .name }}{{ if $page }}{{ end }} - {{ if eq .status "reference" }}reference{{ else if eq .status "pr-open" }}PR open{{ end }} + {{ if eq .status "reference" }}reference{{ else if eq .status "merged" }}on main{{ end }} {{ end }} diff --git a/docs/layouts/home.html b/docs/layouts/home.html index 584d5fb..c4fa48a 100644 --- a/docs/layouts/home.html +++ b/docs/layouts/home.html @@ -32,7 +32,7 @@

Pick your platform

{{ $page := site.GetPage (printf "/for-your-platform/%s" .id) }}
  • {{ with $page }}{{ end }}{{ .name }}{{ if $page }}{{ end }} - {{ if eq .status "reference" }}reference platform{{ else if eq .status "pr-open" }}PR open{{ end }} + {{ if eq .status "reference" }}reference platform{{ else if eq .status "merged" }}on main{{ end }} {{ .status_label | markdownify }}
  • {{ end }} diff --git a/scripts/demo.sh b/scripts/demo.sh new file mode 100755 index 0000000..0096bcb --- /dev/null +++ b/scripts/demo.sh @@ -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= \ +# TUTORIAL= 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"