diff --git a/.github/wiki/plugins/Forgejo.md b/.github/wiki/plugins/Forgejo.md index 1db7f4e1c..f22b4b727 100644 --- a/.github/wiki/plugins/Forgejo.md +++ b/.github/wiki/plugins/Forgejo.md @@ -13,7 +13,7 @@ -> Self-hosted Forgejo git forge plus a Forgejo Actions runner for zero-quota offline CI. +> Self-hosted Forgejo git forge with Forgejo Actions enabled for external runners. ## Install @@ -25,28 +25,23 @@ No license key required. ## Description -Self-hosted Forgejo git forge + Forgejo Actions runner. Provides offline CI that executes .github/workflows/*.yml YAML on self-hosted compute, consuming zero GitHub Actions quota. Designed for the ops profile (ops server on staging/prod). +Self-hosted Forgejo git forge with Forgejo Actions enabled for external runners. There is no in-stack runner and no Docker socket mount: register runners on a CI node that is not a deploy host. Designed for the ops profile (ops server on staging/prod). See the [plugin README](https://github.com/nself-org/plugins/blob/main/free/forgejo/README.md) for isolation and trust, runner registration and migration from the removed runner. This plugin runs as its own container in your nSelf stack (rebuild with `nself build && nself start` after install). -Category: `development`. Current version: `1.0.0`. +Category: `development`. ## Configuration | Env Var | Default | Description | |---------|---------|-------------| -| `NSELF_FORGEJO_ADMIN_USER` | *(required)* | Required. | -| `NSELF_FORGEJO_ADMIN_PASSWORD` | — | Required. | -| `NSELF_FORGEJO_ADMIN_EMAIL` | *(required)* | Required. | -| `FORGEJO_RUNNER_REGISTRATION_TOKEN` | — | Required. | -| `FORGEJO_HTTP_PORT` | *(see plugin.json)* | Optional. | -| `FORGEJO_SSH_PORT` | *(see plugin.json)* | Optional. | -| `FORGEJO_DOMAIN` | *(see plugin.json)* | Optional. | -| `FORGEJO_RUNNER_NAME` | *(see plugin.json)* | Optional. | -| `FORGEJO_RUNNER_LABELS` | *(see plugin.json)* | Optional. | -| `FORGEJO_RUNNER_CAPACITY` | *(see plugin.json)* | Optional. | -| `FORGEJO_RUNNER_MEMORY` | *(see plugin.json)* | Optional. | -| `FORGEJO_RUNNER_CPU` | *(see plugin.json)* | Optional. | +| `NSELF_FORGEJO_ADMIN_USER` | *(required)* | Admin account name, created on first boot. | +| `NSELF_FORGEJO_ADMIN_PASSWORD` | *(required)* | Admin account password. | +| `NSELF_FORGEJO_ADMIN_EMAIL` | *(required)* | Admin account email. | +| `FORGEJO_HTTP_PORT` | `3844` | Optional. HTTP port, published on 127.0.0.1 only. | +| `FORGEJO_SSH_PORT` | `3222` | Optional. SSH port for git over SSH. | +| `FORGEJO_DOMAIN` | `localhost` | Optional. Public hostname used in clone URLs and webhook callbacks. | +| `FORGEJO_DISABLE_REGISTRATION` | `true` | Optional. Self-service sign-up is closed by default; set `false` to open it. | ## Ports diff --git a/.github/workflows/forgejo-smoke.yml b/.github/workflows/forgejo-smoke.yml index 92e78906b..9173b4f2f 100644 --- a/.github/workflows/forgejo-smoke.yml +++ b/.github/workflows/forgejo-smoke.yml @@ -44,3 +44,8 @@ jobs: run: | nself version bash free/forgejo/tests/upgrade-smoke.sh + + - name: Wiki env docs consistency + run: | + sudo apt-get update -qq && sudo apt-get install -y -qq jq >/dev/null + ./free/forgejo/tests/wiki-env-docs.sh diff --git a/free/ci/README.md b/free/ci/README.md index 774911094..575db5941 100644 --- a/free/ci/README.md +++ b/free/ci/README.md @@ -100,15 +100,15 @@ The `nself-ci` binary takes the exact argv of core `nself ci` (P7-CANON-09), so |---|---| | `nself-ci [flags] [repo-root]` | The gate. Flags: `--check`, `--no-status`, `--no-gitleaks`, `--sha`, `--owner`, `--repo`, `-v`/`--verbose`, `--filesystem` (plus the existing `--env`, `--gateway`). Flags and the repo-root may be mixed. | | `nself-ci build [flags] [dir]` | Local signed Android artifact build (see "Artifact builds"). `--artifact` defaults to `android`. | -| `nself-ci forgejo [--url U] [--runner R]` | Forgejo server and runner health for the ops profile (default URL `http://localhost:3844`). Always exits 0, as in core. | +| `nself-ci forgejo [--url U]` | Forgejo server health for the ops profile (default URL `http://localhost:3844`). Always exits 0, as in core. | | `nself-ci serve [flags]` | Webhook listener daemon on port 3845 (HMAC-verified, Docker-sandboxed jobs, fail-closed defaults). Each job runs the gate by exec of this same executable. | | `nself-ci run [flags] [search-root]` | Pipeline over `.ci.yaml` manifests (unchanged). | -`--help` of `ci`, `build`, `forgejo` and `serve` prints the text core prints; it is captured from a built core binary by `scripts/capture-core-surface.sh` into `testdata/core-surface/` and embedded (`internal/surface`). `ci eval` is not part of this binary (P7-CI-44/45). +`--help` of `ci`, `build`, `forgejo` and `serve` uses the core text captured by `scripts/capture-core-surface.sh` into `testdata/core-surface/` and embedded (`internal/surface`). The capture normalizes Forgejo help to remove the in-stack runner retired by P7-TRUST-21. `ci eval` is not part of this binary (P7-CI-44/45). Core framing: with `--check` the binary prints core's section header first and `Error: gate failed` on a failing gate, so `nself-ci --no-status --check REPO` matches `nself ci --no-status --check REPO` byte for byte. The argv that deployed 1.4.x CLIs send (`--no-status` and friends, never `--check`) keeps its exact output, because those CLIs print the header themselves. -Parity proof: `bash scripts/parity-with-core.sh` (needs `$NSELF`, `$TMPDIR`, `gitleaks`) builds core `nself` from cli origin/main and the plugins origin/main and head `nself-ci`, then diffs stdout, stderr and exit codes for core-vs-head and for deployed-1.4.x (base cli with each binary). `--self-test` proves the comparator flags a planted difference. +Parity proof: `bash scripts/parity-with-core.sh` (needs `$NSELF`, `$TMPDIR`, `gitleaks`) builds core `nself` from cli origin/main and the plugins origin/main and head `nself-ci`, then diffs stdout, stderr and exit codes for core-vs-head and for deployed-1.4.x (base cli with each binary). `--self-test` proves the comparator flags a planted difference. Forgejo is excluded from frozen-core parity: its server-only health check and removal of `--runner` are covered by the `internal/forgejo` and `cmd` tests. ## Experimental placement-plan v1 cost totals diff --git a/free/ci/cmd/forgejo.go b/free/ci/cmd/forgejo.go index 01eaf59b4..fa6175567 100644 --- a/free/ci/cmd/forgejo.go +++ b/free/ci/cmd/forgejo.go @@ -1,11 +1,11 @@ package main -// Purpose: the "forgejo" subcommand: Forgejo server and runner health, ported +// Purpose: the "forgejo" subcommand: Forgejo server health, ported // from core `nself ci forgejo` (P7-CANON-09). The implementation lives in // internal/forgejo; this file parses the flags only. -// Inputs: --url (default http://localhost:3844), --runner. +// Inputs: --url (default http://localhost:3844). // Outputs: status table on stdout; exit 0 (core never failed this command). -// Constraints: output and exit codes identical to core on fixtures. +// Constraints: retain the legacy exit-zero contract; no in-stack runner probe. import ( "flag" @@ -22,7 +22,6 @@ func forgejoCmd(args []string, eh flag.ErrorHandling) int { var cfg forgejo.Config fs := flag.NewFlagSet("forgejo", eh) fs.StringVar(&cfg.URL, "url", forgejo.DefaultURL, "Forgejo base URL") - fs.StringVar(&cfg.Runner, "runner", "", "Runner container name (default: _forgejo_runner)") if _, err := parseInterspersed(fs, args); err != nil { return 2 } diff --git a/free/ci/cmd/forgejo_v15.go b/free/ci/cmd/forgejo_v15.go index 55b980f30..0576ccd45 100644 --- a/free/ci/cmd/forgejo_v15.go +++ b/free/ci/cmd/forgejo_v15.go @@ -1,6 +1,6 @@ package main -// Purpose: `nself ci forgejo` in v1.5 (P7-HOST-12, Epic D14): the V15-only entry beside the frozen v1.4 +// Purpose: `nself ci forgejo` in v1.5 (P7-HOST-12, Epic D14): the V15-only entry beside the v1.4 // one (cmd/forgejo.go). It prints one deprecation line per process, then runs the Forgejo check of // internal/forgejo (server health and the optional admin API; no runner container, P7-TRUST-21 // removed that runner). Removal is v1.6 (D-0119); `nself ci doctor --providers` is the way forward. diff --git a/free/ci/cmd/forgejo_v15_test.go b/free/ci/cmd/forgejo_v15_test.go index d2e68e9cb..ed02a2e84 100644 --- a/free/ci/cmd/forgejo_v15_test.go +++ b/free/ci/cmd/forgejo_v15_test.go @@ -1,7 +1,8 @@ package main -// Purpose: tests of `nself ci forgejo` in v1.4 and v1.5 mode (P7-HOST-12): the v1.4 output equals the -// CANON-09 golden, the V15 alias prints one deprecation line per process and then the server and API +// Purpose: tests of `nself ci forgejo` in v1.4 and v1.5 mode (P7-HOST-12): both +// modes check the server without a runner. The V15 alias prints one deprecation +// line per process and then the server and API // check (no runner line), and the URL comes from --url, then the policy key, then the default. // Inputs: httptest servers on loopback, a stub docker on PATH for the v1.4 half. // Outputs: pass or fail; no network beyond loopback. @@ -11,6 +12,7 @@ import ( "bytes" "context" "encoding/json" + "flag" "io" "net/http" "net/http/httptest" @@ -52,7 +54,7 @@ func captureStd(t *testing.T, fn func()) (string, string) { return string(bo), string(be) } -// TestForgejoAlias proves acceptance line 4: v1.4 `nself-ci forgejo` output equals the CANON-09 golden; +// TestForgejoAlias proves acceptance line 4: v1.4 `nself-ci forgejo` checks only the server; // the V15 `nself ci forgejo` prints one deprecation line naming `nself ci doctor --providers` and removal // v1.6, then the server and API check result. func TestForgejoAlias(t *testing.T) { @@ -66,15 +68,14 @@ func TestForgejoAlias(t *testing.T) { t.Setenv("NSELF_FORGEJO_ADMIN_USER", "") t.Setenv("NSELF_FORGEJO_ADMIN_PASSWORD", "") - // v1.4: the frozen table entry, byte for byte as the CANON-09 port prints it. + // v1.4: retain the table and exit contract without probing a removed runner. t.Setenv("NSELF_V15", "") golden := "\n→ Forgejo CI stack (ops profile)\n" + " ✓ Forgejo server " + srv.URL + " healthy\n" + - " ✓ Forgejo runner container=my_runner state=running\n" + " API auth n/a (set NSELF_FORGEJO_ADMIN_USER + NSELF_FORGEJO_ADMIN_PASSWORD)\n\n" + "✓ Forgejo CI stack is healthy.\n" out, errOut := captureStd(t, func() { - if code := run([]string{"forgejo", "--url", srv.URL, "--runner", "my_runner"}); code != 0 { + if code := run([]string{"forgejo", "--url", srv.URL}); code != 0 { t.Errorf("v1.4 exit %d", code) } }) @@ -132,3 +133,14 @@ func TestForgejoAlias(t *testing.T) { t.Fatalf("a stray argument is a usage error, got %d", code) } } + +// TestForgejoRejectsRemovedRunner keeps the parser and advertised help aligned. +func TestForgejoRejectsRemovedRunner(t *testing.T) { + if code := forgejoCmd([]string{"--runner", "old-runner"}, flag.ContinueOnError); code != 2 { + t.Fatalf("removed flag returned %d, want usage error", code) + } + out, _ := captureStd(t, func() { run([]string{"forgejo", "--help"}) }) + if strings.Contains(out, "--runner") || !strings.Contains(out, "external") { + t.Fatalf("stale help: %s", out) + } +} diff --git a/free/ci/cmd/main.go b/free/ci/cmd/main.go index 86f597042..5b7fd8203 100644 --- a/free/ci/cmd/main.go +++ b/free/ci/cmd/main.go @@ -13,7 +13,7 @@ // nself-ci [flags] [repo-root] — single-repo gate // nself-ci run [flags] [search-root] — pipeline: discover .ci.yaml + run stages // nself-ci build --artifact android [dir] — local signed-APK build + gh release upload -// nself-ci forgejo [--url U] [--runner R] — Forgejo server + runner health +// nself-ci forgejo [--url U] — Forgejo server health // nself-ci serve [flags] — webhook listener daemon (port 3845) // nself ci run --env local — via nself CLI proxy // diff --git a/free/ci/internal/forgejo/check_v15.go b/free/ci/internal/forgejo/check_v15.go index 4b7fba41c..408df9aba 100644 --- a/free/ci/internal/forgejo/check_v15.go +++ b/free/ci/internal/forgejo/check_v15.go @@ -2,7 +2,7 @@ package forgejo // Purpose: the v1.5 Forgejo check behind `nself ci forgejo` (P7-HOST-12, Epic D14): the server health // endpoint and the optional admin-authenticated API probe, with the base URL taken from the operator -// policy key providers.forgejo.url. Unlike the v1.4 command (forgejo.go, frozen) it has no runner +// policy key providers.forgejo.url. Both command versions omit the runner // container check: P7-TRUST-21 removed that runner. // Inputs: CheckConfig (URL, optional admin user and password, HTTP client); the policy key. // Outputs: Result, and the status lines on a writer; Deprecation, the one line the alias prints. diff --git a/free/ci/internal/forgejo/forgejo.go b/free/ci/internal/forgejo/forgejo.go index 4fb698885..b0a554345 100644 --- a/free/ci/internal/forgejo/forgejo.go +++ b/free/ci/internal/forgejo/forgejo.go @@ -1,16 +1,6 @@ -// Package forgejo implements `nself ci forgejo`: a Forgejo server and runner -// health check for the ops profile. -// -// Purpose: behaviour-preserving port of cli cmd/commands/ci_forgejo.go -// (P7-CANON-09). Queries the Forgejo /-/health endpoint and the -// forgejo-runner container state (docker inspect) and prints a status table. -// Inputs: Config (URL, runner container name); optional -// NSELF_FORGEJO_ADMIN_USER and NSELF_FORGEJO_ADMIN_PASSWORD for the API probe. -// Outputs: the status table on stdout, warnings on stderr; never an error -// (core returned nil for every outcome, so the exit code is always 0). -// Constraints: no Forgejo token; docker found by PATH lookup as in core. -// Temporary duplicate of the core command, deleted with it at P7-SHIP-09. -// SPORT: F08-SERVICE-INVENTORY (forgejo, ops profile, port 3844) +// Package forgejo implements the Forgejo server health check for the ops profile. +// The plugin has no in-stack runner (P7-TRUST-21); external runner health is +// managed on its CI node. The legacy gate's exit-zero contract is retained. package forgejo import ( @@ -18,7 +8,6 @@ import ( "fmt" "net/http" "os" - "os/exec" "strings" "time" @@ -30,8 +19,7 @@ const DefaultURL = "http://localhost:3844" // Config holds the parsed flags. type Config struct { - URL string // Forgejo base URL - Runner string // runner container name; empty probes the common names + URL string // Forgejo base URL } // healthResponse is the subset of /-/health we care about. @@ -42,37 +30,26 @@ type healthResponse struct { // Run prints the Forgejo stack status. func Run(cfg Config) error { baseURL := strings.TrimRight(cfg.URL, "/") - runnerContainer := cfg.Runner client := &http.Client{Timeout: 10 * time.Second} clui.Section("Forgejo CI stack (ops profile)") serverOK, serverMsg := probeServer(client, baseURL) - runnerContainer, runnerStatus := probeRunner(runnerContainer) jobsInfo := probeAPI(client, baseURL, serverOK) serverIcon := "✗" if serverOK { serverIcon = "✓" } - runnerIcon := "✗" - if runnerStatus == "running" { - runnerIcon = "✓" - } - fmt.Printf(" %s Forgejo server %s %s\n", serverIcon, baseURL, serverMsg) - fmt.Printf(" %s Forgejo runner container=%s state=%s\n", runnerIcon, runnerContainer, runnerStatus) fmt.Printf(" API auth %s\n", jobsInfo) fmt.Println() if !serverOK { clui.Warn("Forgejo server unreachable. Is the ops profile running? Try: nself start --profile ops") } - if runnerStatus != "running" { - clui.Warn("Forgejo runner is not running. Check: docker logs " + runnerContainer) - } - if serverOK && runnerStatus == "running" { + if serverOK { clui.Success("Forgejo CI stack is healthy.") } return nil @@ -92,28 +69,6 @@ func probeServer(client *http.Client, baseURL string) (bool, string) { return false, fmt.Sprintf("HTTP %d", resp.StatusCode) } -// probeRunner returns the container name used and its state (docker inspect). -func probeRunner(runner string) (string, string) { - state := "unknown" - if runner == "" { - // Try common project prefix patterns. - for _, candidate := range []string{"nself_forgejo_runner", "app_forgejo_runner"} { - out, err := exec.Command("docker", "inspect", "--format", "{{.State.Status}}", candidate).Output() - if err == nil { - runner = candidate - state = strings.TrimSpace(string(out)) - break - } - } - if runner == "" { - state = "not found (docker inspect failed — is Docker running?)" - } - } else if out, err := exec.Command("docker", "inspect", "--format", "{{.State.Status}}", runner).Output(); err == nil { - state = strings.TrimSpace(string(out)) - } - return runner, state -} - // probeAPI reports the optional admin-authenticated API probe. func probeAPI(client *http.Client, baseURL string, serverOK bool) string { info := "n/a (set NSELF_FORGEJO_ADMIN_USER + NSELF_FORGEJO_ADMIN_PASSWORD)" diff --git a/free/ci/internal/forgejo/forgejo_test.go b/free/ci/internal/forgejo/forgejo_test.go index c885784e7..d7b9322c0 100644 --- a/free/ci/internal/forgejo/forgejo_test.go +++ b/free/ci/internal/forgejo/forgejo_test.go @@ -50,47 +50,30 @@ func stubDocker(t *testing.T, state string, ok bool) string { return log } -// TestHealthyStack: server healthy and runner running prints the success line. +// TestHealthyStack needs only the server; Docker must never be invoked. func TestHealthyStack(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/-/health" { - _, _ = w.Write([]byte(`{"healthy": true}`)) - return - } - http.NotFound(w, r) + _, _ = w.Write([]byte(`{"healthy": true}`)) })) defer srv.Close() - log := stubDocker(t, "running", true) - out, errOut := run(t, Config{URL: srv.URL + "/", Runner: "my_runner"}) - for _, want := range []string{"Forgejo CI stack (ops profile)", "✓ Forgejo server " + srv.URL + " healthy", "container=my_runner state=running", "Forgejo CI stack is healthy."} { - if !strings.Contains(out, want) { - t.Errorf("stdout lacks %q:\n%s", want, out) - } - } - if errOut != "" { - t.Errorf("stderr = %q", errOut) + log := stubDocker(t, "", false) + out, errOut := run(t, Config{URL: srv.URL + "/"}) + if !strings.Contains(out, "Forgejo CI stack is healthy.") || strings.Contains(out, "runner") || errOut != "" { + t.Fatalf("healthy server without runner: stdout=%q stderr=%q", out, errOut) } - b, _ := os.ReadFile(log) - if !strings.Contains(string(b), "inspect --format {{.State.Status}} my_runner") { - t.Errorf("docker argv = %q", b) + if _, err := os.Stat(log); !os.IsNotExist(err) { + t.Fatalf("Docker was invoked: %v", err) } } -// TestUnreachableAndCandidates: no server, docker finds none; both warnings print. -func TestUnreachableAndCandidates(t *testing.T) { +func TestUnreachable(t *testing.T) { log := stubDocker(t, "", false) out, errOut := run(t, Config{URL: "http://127.0.0.1:1"}) - if !strings.Contains(out, "✗ Forgejo server http://127.0.0.1:1 unreachable") || - !strings.Contains(out, "state=not found (docker inspect failed — is Docker running?)") { - t.Errorf("stdout:\n%s", out) - } - if !strings.Contains(errOut, "Forgejo server unreachable. Is the ops profile running?") || - !strings.Contains(errOut, "Forgejo runner is not running. Check: docker logs") { - t.Errorf("stderr:\n%s", errOut) + if !strings.Contains(out, "unreachable") || !strings.Contains(errOut, "Forgejo server unreachable") || strings.Contains(out+errOut, "runner") { + t.Fatalf("unreachable server: stdout=%q stderr=%q", out, errOut) } - b, _ := os.ReadFile(log) - if !strings.Contains(string(b), "nself_forgejo_runner") || !strings.Contains(string(b), "app_forgejo_runner") { - t.Errorf("both candidates must be probed, got %q", b) + if _, err := os.Stat(log); !os.IsNotExist(err) { + t.Fatalf("Docker was invoked: %v", err) } } @@ -111,12 +94,12 @@ func TestAPIProbe(t *testing.T) { stubDocker(t, "running", true) t.Setenv("NSELF_FORGEJO_ADMIN_USER", "adm") t.Setenv("NSELF_FORGEJO_ADMIN_PASSWORD", "pw") - out, _ := run(t, Config{URL: srv.URL, Runner: "r"}) + out, _ := run(t, Config{URL: srv.URL}) if !strings.Contains(out, "API reachable (admin authenticated)") { t.Errorf("stdout:\n%s", out) } t.Setenv("NSELF_FORGEJO_ADMIN_PASSWORD", "wrong") - out, _ = run(t, Config{URL: srv.URL, Runner: "r"}) + out, _ = run(t, Config{URL: srv.URL}) if !strings.Contains(out, "API returned HTTP 401") { t.Errorf("stdout:\n%s", out) } diff --git a/free/ci/internal/surface/help/ci-forgejo.txt b/free/ci/internal/surface/help/ci-forgejo.txt index caff9681a..c2690ca6d 100644 --- a/free/ci/internal/surface/help/ci-forgejo.txt +++ b/free/ci/internal/surface/help/ci-forgejo.txt @@ -1,7 +1,6 @@ -Query the Forgejo server health endpoint and the forgejo-runner -container status. Designed for the ops profile where Forgejo provides -offline CI that runs .github/workflows/*.yml with zero GitHub Actions -quota consumption. +Query the Forgejo server health endpoint. Workflow jobs run on external +runners registered on a CI node that is not a deploy host. This plugin +ships no runner container. Env vars used (optional — for API queue depth): NSELF_FORGEJO_ADMIN_USER Forgejo admin username @@ -10,14 +9,12 @@ Env vars used (optional — for API queue depth): Examples: nself ci forgejo nself ci forgejo --url http://ci.example.com:3844 - nself ci forgejo --runner nself_forgejo_runner Usage: nself ci forgejo [flags] Flags: -h, --help help for forgejo - --runner string Runner container name (default: _forgejo_runner) --url string Forgejo base URL (default "http://localhost:3844") Global Flags: diff --git a/free/ci/internal/surface/help/ci.txt b/free/ci/internal/surface/help/ci.txt index b3da68445..efa630225 100644 --- a/free/ci/internal/surface/help/ci.txt +++ b/free/ci/internal/surface/help/ci.txt @@ -20,7 +20,7 @@ Usage: Available Commands: build Build a signed release artifact locally (Android only) eval Run eval-gate suite or validate an eval-set YAML - forgejo Show Forgejo server + runner health (ops profile) + forgejo Show Forgejo server health (ops profile) serve Start the nself-ci webhook listener daemon (port 3845) Flags: diff --git a/free/ci/internal/surface/surface.go b/free/ci/internal/surface/surface.go index 4c6767de7..912f61ccc 100644 --- a/free/ci/internal/surface/surface.go +++ b/free/ci/internal/surface/surface.go @@ -3,7 +3,8 @@ // Purpose: `nself-ci --help` (and the subcommand helps) print exactly what the // core `nself ci --help` prints, so the plugin binary is a drop-in for the core // command (P7-CANON-09). The text is captured from a built core binary by -// scripts/capture-core-surface.sh, never typed by hand. +// scripts/capture-core-surface.sh. Forgejo is normalized by that script to +// omit the runner removed in P7-TRUST-21. // Inputs: a command key: "" for ci, or "build", "forgejo", "serve". // Outputs: the help text bytes. // Constraints: the embedded copies must equal testdata/core-surface (unit test). diff --git a/free/ci/plugin.json b/free/ci/plugin.json index 146013012..0868279e8 100644 --- a/free/ci/plugin.json +++ b/free/ci/plugin.json @@ -78,11 +78,6 @@ }, { "flags": [ - { - "name": "runner", - "type": "string", - "usage": "Runner container name (default: _forgejo_runner)" - }, { "default": "http://localhost:3844", "name": "url", @@ -94,7 +89,7 @@ "name": "forgejo", "output": "document", "side_effect": "read", - "summary": "Show Forgejo server + runner health (ops profile)" + "summary": "Show Forgejo server health (ops profile)" }, { "flags": [ diff --git a/free/ci/scripts/capture-core-surface.sh b/free/ci/scripts/capture-core-surface.sh index 0c07816cd..25b64dfae 100755 --- a/free/ci/scripts/capture-core-surface.sh +++ b/free/ci/scripts/capture-core-surface.sh @@ -7,6 +7,7 @@ # Outputs: testdata/core-surface/{ci,ci-build,ci-forgejo,ci-serve}.help.txt, help-json-ci.json (the # `help --json ci` subtree), and byte copies of the four help files in internal/surface/help/ # (embedded by the binary; a unit test fails if the two copies drift). +# Forgejo is normalized after capture: P7-TRUST-21 removed its in-stack runner. # Exit: 0 captured, 1 a capture was empty or the tool failed, 2 usage. set -euo pipefail bin=${1:-} @@ -34,4 +35,5 @@ if not keep: json.dump({"schema_version": d["schema_version"], "commands": keep}, sys.stdout, indent=2, sort_keys=True) print() ' > "$td/help-json-ci.json" +python3 "$here/scripts/normalize-forgejo-surface.py" echo "captured: $(ls "$td" | tr '\n' ' ')" diff --git a/free/ci/scripts/normalize-forgejo-surface.py b/free/ci/scripts/normalize-forgejo-surface.py new file mode 100644 index 000000000..cf666e532 --- /dev/null +++ b/free/ci/scripts/normalize-forgejo-surface.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +"""Apply the post-P7-TRUST-21 Forgejo surface to a frozen core capture.""" +import json +from pathlib import Path + +root = Path(__file__).resolve().parent.parent +capture = root / "testdata/core-surface" +path = capture / "ci-forgejo.help.txt" +text = path.read_text() +text = ("Query the Forgejo server health endpoint. Workflow jobs run on external\n" + "runners registered on a CI node that is not a deploy host. This plugin\n" + "ships no runner container.\n" + text[text.index("\nEnv vars"):]) +text = "\n".join(line for line in text.split("\n") if "--runner" not in line) +path.write_text(text) +(root / "internal/surface/help/ci-forgejo.txt").write_text(text) +path = capture / "help-json-ci.json" +data = json.loads(path.read_text()) +for command in data["commands"]: + if command["path"] == "nself ci forgejo": + command["flags"] = [f for f in command["flags"] if f["name"] != "runner"] + command["summary"] = "Show Forgejo server health (ops profile)" +path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n") + +path = capture / "ci.help.txt" +text = path.read_text().replace("Show Forgejo server + runner health", "Show Forgejo server health") +path.write_text(text) +(root / "internal/surface/help/ci.txt").write_text(text) diff --git a/free/ci/scripts/parity-with-core.sh b/free/ci/scripts/parity-with-core.sh index ea1b09034..57ccfb63c 100755 --- a/free/ci/scripts/parity-with-core.sh +++ b/free/ci/scripts/parity-with-core.sh @@ -3,9 +3,8 @@ # Purpose: two legs, both diffing stdout, stderr and exit code (binary and temp paths normalised): # leg 1 core `nself ci ARGS` (cli origin/main build; its proxy runs the plugins origin/main binary # found first on PATH) versus the head `nself-ci ARGS` with the same core argv: --help of ci, -# build, forgejo, serve; the gate on a clean and a planted-secret fixture with --no-status -# --check (and --no-gitleaks, --verbose); forgejo with a recording stub docker (unreachable and -# healthy server); serve refusing to start without a secret. +# build, serve; the gate on a clean and a planted-secret fixture with --no-status +# --check (and --no-gitleaks, --verbose); serve refusing to start without a secret. # leg 2 deployed 1.4.x: the base cli with the plugins origin/main nself-ci first on PATH versus the # same base cli with the head nself-ci first on PATH, across the buildCIArgs flag matrix (each # of --check, --no-gitleaks, --no-status, --sha, --owner, --repo, -v, --filesystem, a repo-root @@ -40,9 +39,7 @@ PLUG_LOCK=${PARITY_PLUGINS_LOCK:-/tmp/nself-plugins-compile.lock} WORK=$(mktemp -d "$TMPDIR/parity.XXXXXX") WORK=$(cd "$WORK" && pwd) -SRV_PID= cleanup() { - [ -n "$SRV_PID" ] && kill "$SRV_PID" 2>/dev/null || true git -C "$ROOT" worktree remove --force "$WORK/plugins-base" >/dev/null 2>&1 || true if [ -n "${NSELF:-}" ] && [ -d "$NSELF/cli" ]; then git -C "$NSELF/cli" worktree remove --force "$WORK/cli" >/dev/null 2>&1 || true; fi rm -rf "$WORK" @@ -88,32 +85,13 @@ printf 'package main\n\nvar tok = "%s%s"\n' "ghp_" "R8sT1uV2wX3yZ4aB5cD6eF7gH8iJ for r in clean secret; do ( cd "$WORK/fx/$r" && git init -q . && git add -A && git -c user.email=p@x -c user.name=p commit -qm init ); done command -v gitleaks >/dev/null 2>&1 || die "gitleaks is required (the fixtures exercise the secret scan)" -# stub docker: records argv, answers inspect with "running" (a missing stub would hide the docker path) -mkdir -p "$WORK/stub" -cat > "$WORK/stub/docker" <> "$WORK/docker.log" -case " \${STUB_FAIL:-} " in *" \$4 "*) exit 1;; esac -[ "\$1" = inspect ] && echo running -exit 0 -STUB -chmod +x "$WORK/stub/docker" -# healthy Forgejo: a static /-/health document -mkdir -p "$WORK/srv/-"; printf '{"healthy": true}\n' > "$WORK/srv/-/health" -PORT=$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()') -( cd "$WORK/srv" && exec python3 -m http.server "$PORT" --bind 127.0.0.1 >/dev/null 2>&1 ) & SRV_PID=$! -for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do - python3 -c "import urllib.request,sys; urllib.request.urlopen('http://127.0.0.1:$PORT/-/health', timeout=1)" 2>/dev/null && break; sleep 0.25 -done - # ---- comparator --------------------------------------------------------------------------------- norm() { # normalise volatile text; reads stdin sed -E \ -e "s#$WORK##g" \ -e 's/\([0-9][0-9a-zµ.]*\)/(T)/g' \ -e 's/[0-9]{1,2}:[0-9]{2}(AM|PM)/TIME/g' \ - -e 's/in [0-9.]+(µs|ms|s)/in DUR/g' \ - -e "s#127\\.0\\.0\\.1:$PORT#127.0.0.1:PORT#g" + -e 's/in [0-9.]+(µs|ms|s)/in DUR/g' } CASES=0; FAILS=0 # capture cmd args...: stdout, stderr, rc into dir @@ -176,7 +154,6 @@ c1() { # c1