diff --git a/README.md b/README.md index 2d8425b..19a706b 100644 --- a/README.md +++ b/README.md @@ -428,8 +428,9 @@ is only used where it cannot cross a canonical parent path. `--create` is still required for genuinely new pages that have no existing remote topic. If NodeBB rejects a create because the clean wiki URL already exists, `deploy-wiki` re-queries the namespace by page leaf, adopts the matching topic, and updates it -instead. Updates and stale-page archives acquire a Westgate Wiki edit lock -before saving, then send the returned +instead. Pass `--debug` when deploying large wiki changes to emit planning and +live mutation progress. Updates and stale-page archives acquire a Westgate Wiki +edit lock before saving, then send the returned `wikiEditLockToken` with the NodeBB post edit request. Stale generated pages are reported by default. `--stale-policy archive` rewrites stale generated pages in place as archived diff --git a/internal/app/app.go b/internal/app/app.go index c525de2..2448759 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -1030,6 +1030,10 @@ func (c *topdataConsole) phaseLabel(message string) string { return "planning wiki deploy" case strings.HasPrefix(message, "Planning NodeBB wiki page "): return "planning wiki pages" + case strings.HasPrefix(message, "Executing NodeBB wiki actions"): + return "executing wiki deploy" + case strings.HasPrefix(message, "Executing NodeBB wiki action "): + return "executing wiki actions" case strings.HasPrefix(message, "Listing NodeBB wiki namespace category "): return "listing remote wiki pages" case strings.HasPrefix(message, "Collecting "): diff --git a/internal/topdata/wiki_deploy.go b/internal/topdata/wiki_deploy.go index 72099a4..42ec4a6 100644 --- a/internal/topdata/wiki_deploy.go +++ b/internal/topdata/wiki_deploy.go @@ -200,7 +200,12 @@ func DeployWikiWithOptions(p *project.Project, opts DeployWikiOptions, progress if opts.Version != "" { summary = fmt.Sprintf("%s (%s)", summary, opts.Version) } - for _, plan := range orderNodeBBDeployPlans(plans) { + orderedPlans := orderNodeBBDeployPlans(plans) + progress(fmt.Sprintf("Executing NodeBB wiki actions: total %d, create %d, update %d, rename %d, archive %d, purge %d", len(orderedPlans), result.Created, result.Updated, result.Renamed, result.Archived, result.Purged)) + for i, plan := range orderedPlans { + if shouldReportNodeBBDeployActionProgress(i, len(orderedPlans)) { + progress(fmt.Sprintf("Executing NodeBB wiki action %d/%d: %s %s", i+1, len(orderedPlans), plan.Action, plan.Page.PageID)) + } switch plan.Action { case "create": created, err := client.createTopic(plan.Entry.CID, plan.Page.Title, plan.Content, summary) @@ -246,6 +251,14 @@ func DeployWikiWithOptions(p *project.Project, opts DeployWikiOptions, progress return result, nil } +func shouldReportNodeBBDeployActionProgress(index, total int) bool { + if total <= 0 { + return false + } + position := index + 1 + return position == 1 || position == total || position%100 == 0 +} + func orderNodeBBDeployPlans(plans []wikiDeployPlan) []wikiDeployPlan { if len(plans) <= 1 { return plans diff --git a/internal/topdata/wiki_deploy_test.go b/internal/topdata/wiki_deploy_test.go index faa27ca..fd6386a 100644 --- a/internal/topdata/wiki_deploy_test.go +++ b/internal/topdata/wiki_deploy_test.go @@ -132,6 +132,100 @@ func TestDeployWikiReportsPlanningProgressBeforeRemoteWork(t *testing.T) { } } +func TestDeployWikiReportsLiveExecutionProgress(t *testing.T) { + root := t.TempDir() + sourceDir := filepath.Join(root, "pages") + if err := os.MkdirAll(filepath.Join(sourceDir, "feat"), 0755); err != nil { + t.Fatalf("create source dir: %v", err) + } + updatePage := ` + +

Updated

+

New generated content

+ +` + renamePage := ` + +

Renamed

+

Unchanged generated content

+ +` + if err := os.WriteFile(filepath.Join(sourceDir, "feat", "updated.html"), []byte(updatePage), 0644); err != nil { + t.Fatalf("write update page: %v", err) + } + if err := os.WriteFile(filepath.Join(sourceDir, "feat", "renamed.html"), []byte(renamePage), 0644); err != nil { + t.Fatalf("write rename page: %v", err) + } + oldUpdate := ` + +

Updated

+

Old generated content

+ +` + manifestPath := filepath.Join(root, "deploy-manifest.json") + if err := saveDeployManifest(manifestPath, wikiDeployManifest{ + Version: "nodebb-v1", + Pages: map[string]wikiDeployManifestPage{ + "feat:renamed": {Hash: computeManagedHash(renamePage), Title: "Old Renamed", TopicTitle: "Old Renamed", Namespace: "feat", TID: 7, PID: 70, CID: 5}, + "feat:updated": {Hash: computeManagedHash(oldUpdate), Title: "Updated", TopicTitle: "Updated", Namespace: "feat", TID: 8, PID: 80, CID: 5}, + }, + }); err != nil { + t.Fatalf("write deploy manifest: %v", err) + } + + progress := []string{} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && r.URL.Path == "/api/v3/topics/7": + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{"response": map[string]any{"tid": 7, "title": "Old Renamed", "mainPid": 70}}) + case r.Method == http.MethodGet && r.URL.Path == "/api/v3/posts/80": + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{"response": map[string]any{"pid": 80, "tid": 8, "content": oldUpdate}}) + case r.Method == http.MethodPut && r.URL.Path == "/api/v3/plugins/westgate-wiki/edit-lock": + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{"response": map[string]any{"token": "lock-token"}}) + case r.Method == http.MethodPut && r.URL.Path == "/api/v3/posts/80": + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{"response": map[string]any{"pid": 80, "tid": 8}}) + case r.Method == http.MethodPut && r.URL.Path == "/api/v3/plugins/westgate-wiki/page/move": + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{"response": map[string]any{"tid": 7}}) + default: + t.Fatalf("unexpected request %s %s", r.Method, r.URL.String()) + } + })) + defer server.Close() + + result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{ + SourceDir: sourceDir, + Endpoint: server.URL, + Token: "nodebb-token", + ManifestPath: manifestPath, + }, func(message string) { + progress = append(progress, message) + }) + if err != nil { + t.Fatalf("DeployWikiWithOptions live deploy failed: %v", err) + } + if result.Updated != 1 || result.Renamed != 1 { + t.Fatalf("expected one update and one rename, got %#v", result) + } + + joined := strings.Join(progress, "\n") + for _, expected := range []string{ + "Executing NodeBB wiki actions: total 2, create 0, update 1, rename 1, archive 0, purge 0", + "Executing NodeBB wiki action 1/2:", + "Executing NodeBB wiki action 2/2:", + "rename feat:renamed", + "update feat:updated", + } { + if !strings.Contains(joined, expected) { + t.Fatalf("expected execution progress %q, got:\n%s", expected, joined) + } + } +} + func TestNodeBBNamespacePaginationRejectsRepeatedCursor(t *testing.T) { requests := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {