Skip to content

Commit 9feb6b1

Browse files
committed
harden pgext catalog workflows
1 parent dafb227 commit 9feb6b1

56 files changed

Lines changed: 3633 additions & 659 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cli/cc_cate.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -474,12 +474,17 @@ func (g *CCCateGenerator) GenerateAllCategoryPages(ctx context.Context) error {
474474
return fmt.Errorf("failed to generate category index: %w", err)
475475
}
476476

477+
var failed []string
477478
for _, cat := range g.Cache.Categories {
478479
if err := g.GenerateCategoryPage(ctx, cat); err != nil {
479480
logrus.Errorf("Failed to generate category page for %s: %v", cat.Name, err)
481+
failed = append(failed, cat.Name)
480482
}
481483
}
482484

483-
logrus.Infof("Generated %d category pages", len(g.Cache.Categories))
485+
logrus.Infof("Generated %d/%d category pages", len(g.Cache.Categories)-len(failed), len(g.Cache.Categories))
486+
if len(failed) > 0 {
487+
return fmt.Errorf("failed to generate %d category pages: %s", len(failed), strings.Join(failed, ", "))
488+
}
484489
return nil
485490
}

cli/cc_list.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ icon: fas fa-code
354354
anchor = "cpp"
355355
}
356356

357-
b.WriteString(fmt.Sprintf("\n--------\n\n"))
357+
b.WriteString("\n--------\n\n")
358358
b.WriteString(fmt.Sprintf("## %s {#%s}\n\n", lang, anchor))
359359
b.WriteString(fmt.Sprintf("%s 语言编写的扩展(%d 个)\n\n", lang, len(exts)))
360360

cli/cc_os.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,6 @@ func (g *CCOSGenerator) generateOSMatrixCell(ospkg *OSPackageInfo, pg int, osNam
276276
return fmt.Sprintf("%s %s %s %d", state, org, version, count)
277277
}
278278

279-
280279
// GenerateAllOSPages generates OS pages for all active OS distributions
281280
func (g *CCOSGenerator) GenerateAllOSPages(ctx context.Context) error {
282281
osList, err := GetActiveOS(ctx)
@@ -337,10 +336,7 @@ func (g *CCOSGenerator) GenerateAllOSPages(ctx context.Context) error {
337336

338337
if len(failedOS) > 0 {
339338
logrus.Warnf("Failed to generate pages for: %v", failedOS)
340-
}
341-
342-
if successCount == 0 {
343-
return fmt.Errorf("failed to generate any OS pages")
339+
return fmt.Errorf("failed to generate %d/%d OS pages: %s", len(failedOS), len(osList), strings.Join(failedOS, ", "))
344340
}
345341
return nil
346342
}

cli/cc_page.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ func NewCCPageGenerator(cache *ExtensionCache, outputDir, stubDir string) *CCPag
3333

3434
// GenerateExtensionPage generates a single extension page (Chinese only)
3535
func (g *CCPageGenerator) GenerateExtensionPage(ctx context.Context, ext *Extension) error {
36-
content := g.generateExtensionContent(ctx, ext)
36+
content, err := g.generateExtensionContent(ctx, ext)
37+
if err != nil {
38+
return err
39+
}
3740

3841
// Append stub content if exists
3942
stubPath := filepath.Join(g.StubDir, ext.Name+".md")
@@ -45,11 +48,17 @@ func (g *CCPageGenerator) GenerateExtensionPage(ctx context.Context, ext *Extens
4548
}
4649

4750
// generateExtensionContent generates the markdown content
48-
func (g *CCPageGenerator) generateExtensionContent(ctx context.Context, ext *Extension) string {
51+
func (g *CCPageGenerator) generateExtensionContent(ctx context.Context, ext *Extension) (string, error) {
4952
var b strings.Builder
5053

51-
packages, _ := LoadPackages(ctx, ext.Pkg)
52-
binaries, _ := LoadBinaries(ctx, ext.Name)
54+
packages, err := LoadPackages(ctx, ext.Pkg)
55+
if err != nil {
56+
return "", fmt.Errorf("load packages for %s: %w", ext.Name, err)
57+
}
58+
binaries, err := LoadBinaries(ctx, ext.Name)
59+
if err != nil {
60+
return "", fmt.Errorf("load binaries for %s: %w", ext.Name, err)
61+
}
5362
siblings := g.Cache.GetSiblingExtensions(ext.Pkg, ext.Name)
5463
allExts := g.getAllPackageExtensions(ext, siblings)
5564

@@ -71,7 +80,7 @@ func (g *CCPageGenerator) generateExtensionContent(ctx context.Context, ext *Ext
7180

7281
b.WriteString(g.generateInstall(ext))
7382

74-
return b.String()
83+
return b.String(), nil
7584
}
7685

7786
// getAllPackageExtensions returns all extensions for the package (self + siblings), sorted by ID

cli/confgen.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ func GeneratePigstyConfig(osName string, outputDir string, dryRun bool, verbose
3535
if err != nil {
3636
return fmt.Errorf("failed to get SQL DB: %w", err)
3737
}
38+
defer sqlDB.Close()
3839

3940
// Create generator
4041
generator := NewPigstyConfigGenerator(sqlDB, osName, verbose)
@@ -886,6 +887,7 @@ func GetConfigConstants() *ConfigConstants {
886887
{"pg_exporter", "pg_exporter", "pg-exporter"},
887888
{"pgbackrest_exporter", "pgbackrest_exporter", "pgbackrest-exporter"},
888889
{"vip-manager", "vip-manager", "vip-manager"},
890+
{"pg-hardstorage", "pg-hardstorage", "pg-hardstorage"},
889891
{"pg_hardstorage", "pg-hardstorage", "pg-hardstorage"},
890892
{"pgbadger", "pgbadger", "pgbadger"},
891893
{"pg_activity", "pg_activity", "pg-activity"},
@@ -1132,7 +1134,7 @@ repo_upstream_default:
11321134
- { name: docker-ce ,description: 'Docker' ,module: infra ,releases: [11,12,13,22,24,26] ,arch: [x86_64, aarch64] ,baseurl: { default: 'https://download.docker.com/linux/${distro_name} ${distro_codename} stable' ,china: 'https://mirrors.aliyun.com/docker-ce/linux/${distro_name} ${distro_codename} stable' }}
11331135
- { name: base ,description: 'Debian Basic' ,module: node ,releases: [11,12,13 ] ,arch: [x86_64, aarch64] ,baseurl: { default: 'http://deb.debian.org/debian/ ${distro_codename} main non-free-firmware' ,china: 'https://mirrors.aliyun.com/debian/ ${distro_codename} main non-free-firmware' }}
11341136
- { name: updates ,description: 'Debian Updates' ,module: node ,releases: [11,12,13 ] ,arch: [x86_64, aarch64] ,baseurl: { default: 'http://deb.debian.org/debian/ ${distro_codename}-updates main non-free-firmware' ,china: 'https://mirrors.aliyun.com/debian/ ${distro_codename}-updates main non-free-firmware' }}
1135-
- { name: security ,description: 'Debian Securitcy' ,module: node ,releases: [11,12,13 ] ,arch: [x86_64, aarch64] ,baseurl: { default: 'http://security.debian.org/debian-security ${distro_codename}-security main non-free-firmware' ,china: 'https://mirrors.aliyun.com/debian-security/ ${distro_codename}-security main non-free-firmware' }}
1137+
- { name: security ,description: 'Debian Security' ,module: node ,releases: [11,12,13 ] ,arch: [x86_64, aarch64] ,baseurl: { default: 'http://security.debian.org/debian-security ${distro_codename}-security main non-free-firmware' ,china: 'https://mirrors.aliyun.com/debian-security/ ${distro_codename}-security main non-free-firmware' }}
11361138
- { name: base ,description: 'Ubuntu Basic' ,module: node ,releases: [ 22,24,26] ,arch: [x86_64 ] ,baseurl: { default: 'https://mirrors.edge.kernel.org/ubuntu/ ${distro_codename} main universe multiverse restricted' ,china: 'https://mirrors.aliyun.com/ubuntu/ ${distro_codename} main restricted universe multiverse' }}
11371139
- { name: updates ,description: 'Ubuntu Updates' ,module: node ,releases: [ 22,24,26] ,arch: [x86_64 ] ,baseurl: { default: 'https://mirrors.edge.kernel.org/ubuntu/ ${distro_codename}-updates main restricted universe multiverse' ,china: 'https://mirrors.aliyun.com/ubuntu/ ${distro_codename}-updates main restricted universe multiverse' }}
11381140
- { name: backports ,description: 'Ubuntu Backports' ,module: node ,releases: [ 22,24,26] ,arch: [x86_64 ] ,baseurl: { default: 'https://mirrors.edge.kernel.org/ubuntu/ ${distro_codename}-backports main restricted universe multiverse' ,china: 'https://mirrors.aliyun.com/ubuntu/ ${distro_codename}-backports main restricted universe multiverse' }}

cli/confgen_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1038,8 +1038,7 @@ func TestDebTemplateRepoReleaseCompatibility(t *testing.T) {
10381038

10391039
func TestRPMTemplateEL10RepoReleaseCompatibility(t *testing.T) {
10401040
required := []string{
1041-
"name: timescaledb ,description: 'TimescaleDB' ,module: extra ,releases: [8,9 ] ,arch: [x86_64, aarch64]",
1042-
"name: timescaledb ,description: 'TimescaleDB' ,module: extra ,releases: [ 10] ,arch: [x86_64 ]",
1041+
"name: timescaledb ,description: 'TimescaleDB' ,module: extra ,releases: [8,9,10] ,arch: [x86_64, aarch64]",
10431042
"name: mysql ,description: 'MySQL' ,module: mysql ,releases: [8,9,10] ,arch: [x86_64, aarch64]",
10441043
"name: mongo ,description: 'MongoDB' ,module: mongo ,releases: [8,9,10] ,arch: [x86_64, aarch64]",
10451044
"name: gitlab-ee ,description: 'Gitlab EE' ,module: gitlab ,releases: [8,9,10] ,arch: [x86_64, aarch64]",
@@ -1052,7 +1051,8 @@ func TestRPMTemplateEL10RepoReleaseCompatibility(t *testing.T) {
10521051
}
10531052

10541053
forbidden := []string{
1055-
"name: timescaledb ,description: 'TimescaleDB' ,module: extra ,releases: [8,9,10] ,arch: [x86_64, aarch64]",
1054+
"name: timescaledb ,description: 'TimescaleDB' ,module: extra ,releases: [8,9 ] ,arch: [x86_64, aarch64]",
1055+
"name: timescaledb ,description: 'TimescaleDB' ,module: extra ,releases: [ 10] ,arch: [x86_64 ]",
10561056
}
10571057
for _, fragment := range forbidden {
10581058
if strings.Contains(rpmTemplate, fragment) {

cli/fetcher.go

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,11 @@ type FetchResult struct {
4949

5050
// FetchOptions contains options for fetch operation
5151
type FetchOptions struct {
52-
Force bool // Force re-download all repos
53-
Region string // Region for mirror selection (default|china)
54-
Parallel int // Number of parallel workers
55-
Retry int // Number of retry attempts
52+
Force bool // Force re-download all repos
53+
Region string // Region for mirror selection (default|china)
54+
Parallel int // Number of parallel workers
55+
Retry int // Number of retry attempts
56+
BestEffort bool // Allow a partial result when at least one repository succeeds
5657
}
5758

5859
// Fetcher handles repository metadata fetching
@@ -62,6 +63,7 @@ type Fetcher struct {
6263
force bool
6364
maxWorkers int
6465
retries int
66+
bestEffort bool
6567
}
6668

6769
// NewFetcher creates a new repository fetcher
@@ -93,6 +95,7 @@ func NewFetcher(opts FetchOptions) *Fetcher {
9395
force: opts.Force,
9496
maxWorkers: opts.Parallel,
9597
retries: opts.Retry,
98+
bestEffort: opts.BestEffort,
9699
}
97100
}
98101

@@ -116,48 +119,49 @@ func (f *Fetcher) FetchAll(ctx context.Context) error {
116119
results := f.fetchConcurrent(ctx, repos)
117120

118121
// Process results
119-
var updated, skipped, failed int
120-
var errors []string
122+
report := RunReport{Operation: "fetch", Total: len(repos)}
121123

122124
for _, result := range results {
123125
if result.Error != nil {
124-
failed++
125-
errors = append(errors, fmt.Sprintf("%s: %v", result.Repository.ID, result.Error))
126+
report.AddFailure(result.Repository.ID, result.Error)
126127
continue
127128
}
128129

129130
if result.Updated {
130131
if err := f.saveMetadata(ctx, result); err != nil {
131-
failed++
132-
errors = append(errors, fmt.Sprintf("%s: save failed: %v", result.Repository.ID, err))
132+
report.AddFailure(result.Repository.ID, fmt.Errorf("save metadata: %w", err))
133133
} else {
134-
updated++
134+
report.Succeeded++
135135
logrus.Debugf("Updated %s (%s)", result.Repository.ID, formatBytes(int64(len(result.Data))))
136136
}
137137
} else {
138-
skipped++
138+
report.Skipped++
139139
logrus.Debugf("Skipped %s (not modified)", result.Repository.ID)
140140
}
141141
}
142142

143-
// Update status timestamp
144-
if updated > 0 {
145-
if err := f.updateFetchTime(ctx); err != nil {
146-
logrus.Warnf("Failed to update fetch time: %v", err)
147-
}
148-
}
149-
150143
// Log summary
151-
logrus.Infof("Fetch complete: %d updated, %d skipped, %d failed", updated, skipped, failed)
144+
logrus.Infof("Fetch complete: %d updated, %d skipped, %d failed",
145+
report.Succeeded, report.Skipped, report.Failed())
152146

153147
// Report errors
154-
for _, errMsg := range errors {
155-
logrus.Error(errMsg)
148+
for _, failure := range report.Failures {
149+
logrus.Errorf("%s: %v", failure.Item, failure.Err)
156150
}
157151

158-
// Fail if all repositories failed
159-
if failed == len(repos) {
160-
return fmt.Errorf("all repositories failed to fetch")
152+
if err := report.Err(f.bestEffort); err != nil {
153+
return err
154+
}
155+
if report.Failed() > 0 {
156+
logrus.Warnf("best-effort fetch accepted %d failed repositories", report.Failed())
157+
}
158+
159+
// Advance the accepted-run timestamp only after strict/best-effort policy
160+
// has approved the result.
161+
if report.Succeeded > 0 {
162+
if err := f.updateFetchTime(ctx); err != nil {
163+
logrus.Warnf("Failed to update fetch time: %v", err)
164+
}
161165
}
162166

163167
return nil
@@ -195,8 +199,7 @@ func (f *Fetcher) loadRepositories(ctx context.Context) ([]*RepoMetadata, error)
195199
&repo.CachedSize,
196200
)
197201
if err != nil {
198-
logrus.Warnf("Failed to scan repository: %v", err)
199-
continue
202+
return nil, fmt.Errorf("scan repository metadata: %w", err)
200203
}
201204
repos = append(repos, repo)
202205
}

cli/gen_list.go

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -787,12 +787,7 @@ func (g *ListGenerator) GenerateLicenseList(locale, outputPath string) error {
787787
licenses = append(licenses, licenseItem{name, exts, order})
788788
}
789789

790-
sort.Slice(licenses, func(i, j int) bool {
791-
if len(licenses[i].exts) != len(licenses[j].exts) {
792-
return len(licenses[i].exts) > len(licenses[j].exts)
793-
}
794-
return licenses[i].order < licenses[j].order
795-
})
790+
sortLicenseItems(licenses)
796791

797792
var b strings.Builder
798793

@@ -841,6 +836,24 @@ func (g *ListGenerator) GenerateLicenseList(locale, outputPath string) error {
841836
return os.WriteFile(outputPath, []byte(b.String()), 0644)
842837
}
843838

839+
func sortLicenseItems(licenses []licenseItem) {
840+
sort.Slice(licenses, func(i, j int) bool {
841+
if len(licenses[i].exts) != len(licenses[j].exts) {
842+
return len(licenses[i].exts) > len(licenses[j].exts)
843+
}
844+
if licenses[i].order != licenses[j].order {
845+
return licenses[i].order < licenses[j].order
846+
}
847+
848+
left := strings.ToLower(licenses[i].name)
849+
right := strings.ToLower(licenses[j].name)
850+
if left != right {
851+
return left < right
852+
}
853+
return licenses[i].name < licenses[j].name
854+
})
855+
}
856+
844857
func (g *ListGenerator) generateLicenseSection(license string, extensions []*Extension, isZh bool) string {
845858
var b strings.Builder
846859

cli/gen_list_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package cli
2+
3+
import (
4+
"bytes"
5+
"database/sql"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
"testing"
10+
)
11+
12+
func TestSortLicenseItemsUsesStableNameTieBreak(t *testing.T) {
13+
oneExtension := []*Extension{{}}
14+
items := []licenseItem{
15+
{name: "Zulu", exts: oneExtension, order: 999},
16+
{name: "beta", exts: oneExtension, order: 999},
17+
{name: "Alpha", exts: oneExtension, order: 999},
18+
{name: "alpha", exts: oneExtension, order: 999},
19+
}
20+
21+
sortLicenseItems(items)
22+
23+
got := make([]string, len(items))
24+
for i, item := range items {
25+
got[i] = item.name
26+
}
27+
if want := "Alpha,alpha,beta,Zulu"; strings.Join(got, ",") != want {
28+
t.Fatalf("license order = %q, want %q", strings.Join(got, ","), want)
29+
}
30+
}
31+
32+
func TestGenerateLicenseListIsDeterministicForEqualRankLicenses(t *testing.T) {
33+
extensions := []*Extension{
34+
{ID: 4, Name: "zulu_ext", Pkg: "zulu_ext", License: sql.NullString{String: "Zulu", Valid: true}},
35+
{ID: 3, Name: "beta_ext", Pkg: "beta_ext", License: sql.NullString{String: "beta", Valid: true}},
36+
{ID: 2, Name: "alpha_upper_ext", Pkg: "alpha_upper_ext", License: sql.NullString{String: "Alpha", Valid: true}},
37+
{ID: 1, Name: "alpha_lower_ext", Pkg: "alpha_lower_ext", License: sql.NullString{String: "alpha", Valid: true}},
38+
}
39+
40+
generate := func(name string, exts []*Extension) []byte {
41+
t.Helper()
42+
path := filepath.Join(t.TempDir(), name+".md")
43+
generator := NewListGenerator(&ExtensionCache{Extensions: exts}, filepath.Dir(path))
44+
if err := generator.GenerateLicenseList("en", path); err != nil {
45+
t.Fatalf("generate %s: %v", name, err)
46+
}
47+
content, err := os.ReadFile(path)
48+
if err != nil {
49+
t.Fatalf("read %s: %v", name, err)
50+
}
51+
return content
52+
}
53+
54+
forward := generate("forward", extensions)
55+
reverse := generate("reverse", []*Extension{extensions[3], extensions[2], extensions[1], extensions[0]})
56+
if !bytes.Equal(forward, reverse) {
57+
t.Fatal("license output changed when extension input order changed")
58+
}
59+
60+
previous := -1
61+
for _, section := range []string{"Alpha", "alpha", "beta", "Zulu"} {
62+
index := bytes.Index(forward, []byte("\n## "+section+"\n"))
63+
if index < 0 {
64+
t.Fatalf("generated output is missing %s section", section)
65+
}
66+
if index <= previous {
67+
t.Fatalf("%s section appears out of deterministic name order", section)
68+
}
69+
previous = index
70+
}
71+
}

cli/gen_page.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,8 @@ func (g *ExtensionGenerator) generateExtensionContent(ext *Extension, packages [
108108
func (g *ExtensionGenerator) generateFrontmatter(ext *Extension) string {
109109
categoryTitle := "Unknown"
110110
if ext.Category.Valid {
111-
categoryTitle = strings.Title(ext.Category.String)
111+
// Category codes are canonical display labels (for example RAG, GIS).
112+
categoryTitle = ext.Category.String
112113
}
113114

114115
desc := ext.EnDesc.String

0 commit comments

Comments
 (0)