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"