Add managed namespace wiki reset deploy

This commit is contained in:
2026-05-23 17:51:27 +02:00
parent 1221156084
commit 22d7fa5e6f
5 changed files with 228 additions and 17 deletions
+79 -15
View File
@@ -33,21 +33,22 @@ const (
)
type DeployWikiOptions struct {
SourceDir string
Endpoint string
Token string
Version string
Namespaces []string
CategoryIDs map[string]int
ManifestPath string
DryRun bool
Force bool
AllowCreates bool
StalePolicy string
Username string
Password string
NotesDelimiter string
TitlePrefixMinLength int
SourceDir string
Endpoint string
Token string
Version string
Namespaces []string
CategoryIDs map[string]int
ManifestPath string
DryRun bool
Force bool
AllowCreates bool
ResetManagedNamespaces bool
StalePolicy string
Username string
Password string
NotesDelimiter string
TitlePrefixMinLength int
}
type deployResult struct {
@@ -456,6 +457,23 @@ func planNodeBBDeploy(pages map[string]wikiDeployPage, manifest wikiDeployManife
remotePagesByCID := map[int][]nodeBBWikiPage{}
var plans []wikiDeployPlan
var result deployResult
if opts.ResetManagedNamespaces {
if !opts.AllowCreates && len(pageIDs) > 0 {
return nil, result, next, errors.New("wiki managed namespace reset requires --create so current generated pages can be recreated")
}
resetPlans, purged, err := planManagedNamespaceReset(opts, client)
if err != nil {
return nil, result, next, err
}
plans = append(plans, resetPlans...)
result.Purged += purged
manifest = wikiDeployManifest{Version: "nodebb-v1", Pages: map[string]wikiDeployManifestPage{}}
for _, cid := range opts.CategoryIDs {
if cid != 0 {
remotePagesByCID[cid] = []nodeBBWikiPage{}
}
}
}
for _, pageID := range pageIDs {
page := pages[pageID]
@@ -615,6 +633,52 @@ func planNodeBBDeploy(pages map[string]wikiDeployPage, manifest wikiDeployManife
return plans, result, next, nil
}
func planManagedNamespaceReset(opts DeployWikiOptions, client *nodeBBClient) ([]wikiDeployPlan, int, error) {
namespaces := slices.Clone(opts.Namespaces)
if len(namespaces) == 0 {
for namespace := range opts.CategoryIDs {
namespaces = append(namespaces, namespace)
}
}
slices.Sort(namespaces)
seenTIDs := map[int]struct{}{}
var plans []wikiDeployPlan
for _, namespace := range namespaces {
cid := opts.CategoryIDs[namespace]
if cid == 0 {
continue
}
remotePages, err := client.listNamespacePages(cid)
if err != nil {
return nil, 0, err
}
slices.SortFunc(remotePages, func(a, b nodeBBWikiPage) int {
return a.TID - b.TID
})
for _, remotePage := range remotePages {
if remotePage.TID == 0 {
continue
}
if _, ok := seenTIDs[remotePage.TID]; ok {
continue
}
seenTIDs[remotePage.TID] = struct{}{}
title := strings.TrimSpace(remotePage.Title)
if title == "" {
title = strings.TrimSpace(remotePage.TitleLeaf)
}
pageID := fmt.Sprintf("%s:reset-topic-%d", namespace, remotePage.TID)
plans = append(plans, wikiDeployPlan{
Page: wikiDeployPage{PageID: pageID, Title: title, Namespace: namespace},
Entry: wikiDeployManifestPage{TID: remotePage.TID, CID: cid, Namespace: namespace, Title: title},
Action: "purge",
})
}
}
return plans, len(plans), nil
}
func findMappedRemotePage(entry wikiDeployManifestPage, remotePagesByCID map[int][]nodeBBWikiPage, client *nodeBBClient) (nodeBBWikiPage, bool, error) {
remotePages, ok := remotePagesByCID[entry.CID]
if !ok {
+128
View File
@@ -1575,6 +1575,134 @@ func TestDeployWikiPurgesTrackedStaleTopicsBeforeCreatingReplacementPages(t *tes
}
}
func TestDeployWikiResetManagedNamespacesPurgesRemotePagesBeforeCreatingFreshManifest(t *testing.T) {
root := t.TempDir()
sourceDir := filepath.Join(root, "pages")
if err := os.MkdirAll(filepath.Join(sourceDir, "skills"), 0755); err != nil {
t.Fatalf("create source dir: %v", err)
}
generated := `<!-- sow-topdata-wiki:page=skills:replacement -->
<!-- sow-topdata-wiki:managed:start hash="sha256:local" -->
<h1>Replacement</h1>
<p>Generated replacement page</p>
<!-- sow-topdata-wiki:managed:end -->
`
if err := os.WriteFile(filepath.Join(sourceDir, "skills", "replacement.html"), []byte(generated), 0644); err != nil {
t.Fatalf("write source page: %v", err)
}
manifestPath := filepath.Join(root, "deploy-manifest.json")
if err := saveDeployManifest(manifestPath, wikiDeployManifest{
Version: "nodebb-v1",
Pages: map[string]wikiDeployManifestPage{
"skills:stale": {Hash: "old-hash", Title: "Replacement", Namespace: "skills", TID: 7, PID: 42, CID: 3},
},
}); err != nil {
t.Fatalf("write deploy manifest: %v", err)
}
var calls []string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/plugins/westgate-wiki/namespace/3/pages":
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"response": map[string]any{
"pages": []map[string]any{
{"tid": 7, "title": "Old Replacement", "titleLeaf": "Old Replacement", "slug": "7/replacement", "wikiPath": "/wiki/skills/replacement"},
{"tid": 8, "title": "Manual Note", "titleLeaf": "Manual Note", "slug": "8/manual-note", "wikiPath": "/wiki/skills/manual-note"},
},
"hasMore": false,
},
})
case r.Method == http.MethodDelete && r.URL.Path == "/api/v3/topics/7":
calls = append(calls, "purge:7")
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"status": map[string]any{"code": "ok"}})
case r.Method == http.MethodDelete && r.URL.Path == "/api/v3/topics/8":
calls = append(calls, "purge:8")
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"status": map[string]any{"code": "ok"}})
case r.Method == http.MethodPost && r.URL.Path == "/api/v3/topics":
calls = append(calls, "create")
if strings.Join(calls, ",") != "purge:7,purge:8,create" {
t.Fatalf("expected namespace reset purges before create, got %#v", calls)
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"response": map[string]any{
"tid": 11,
"mainPost": map[string]any{
"pid": 99,
"tid": 11,
},
},
})
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,
CategoryIDs: map[string]int{"skills": 3},
AllowCreates: true,
ResetManagedNamespaces: true,
}, nil)
if err != nil {
t.Fatalf("DeployWikiWithOptions reset-managed-namespaces failed: %v", err)
}
if result.Created != 1 || result.Purged != 2 || result.Stale != 0 {
t.Fatalf("expected one create and two namespace purges, got %#v", result)
}
if strings.Join(calls, ",") != "purge:7,purge:8,create" {
t.Fatalf("expected reset purges before create, got %#v", calls)
}
manifest := loadDeployManifest(manifestPath)
if _, ok := manifest.Pages["skills:stale"]; ok {
t.Fatalf("expected reset to remove previous manifest mapping")
}
entry := manifest.Pages["skills:replacement"]
if entry.TID != 11 || entry.PID != 99 || entry.CID != 3 {
t.Fatalf("expected fresh created mapping in manifest, got %#v", entry)
}
}
func TestDeployWikiResetManagedNamespacesRequiresCreateForLocalPages(t *testing.T) {
root := t.TempDir()
sourceDir := filepath.Join(root, "pages")
if err := os.MkdirAll(filepath.Join(sourceDir, "skills"), 0755); err != nil {
t.Fatalf("create source dir: %v", err)
}
generated := `<!-- sow-topdata-wiki:page=skills:replacement -->
<!-- sow-topdata-wiki:managed:start hash="sha256:local" -->
<h1>Replacement</h1>
<!-- sow-topdata-wiki:managed:end -->
`
if err := os.WriteFile(filepath.Join(sourceDir, "skills", "replacement.html"), []byte(generated), 0644); err != nil {
t.Fatalf("write source page: %v", err)
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("reset without --create must fail before remote calls, got %s %s", r.Method, r.URL.String())
}))
defer server.Close()
_, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
SourceDir: sourceDir,
Endpoint: server.URL,
Token: "nodebb-token",
CategoryIDs: map[string]int{"skills": 3},
ResetManagedNamespaces: true,
}, nil)
if err == nil || !strings.Contains(err.Error(), "requires --create") {
t.Fatalf("expected reset without create to fail clearly, got %v", err)
}
}
func TestDeployWikiPurgeTreatsAlreadyMissingTrackedTopicAsSuccess(t *testing.T) {
root := t.TempDir()
sourceDir := filepath.Join(root, "pages")