Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,23 +76,28 @@ jobs:
run: ./scripts/check-go-coverage.sh

- name: Slophammer static check
run: go run github.com/dutifuldev/slophammer/go/cmd/slophammer@latest check .
working-directory: backend
run: go run github.com/dutifuldev/slophammer/go/cmd/slophammer-go@v0.3.0 check ..

- name: Slophammer DRY gate
run: go run github.com/dutifuldev/slophammer/go/cmd/slophammer@latest go dry .
working-directory: backend
run: go run github.com/dutifuldev/slophammer/go/cmd/slophammer-go@v0.3.0 dry ..

- name: Slophammer CRAP gate
env:
DATABASE_URL: postgres://news:testpass@localhost:5432/news_pipeline?sslmode=disable
run: go run github.com/dutifuldev/slophammer/go/cmd/slophammer@latest go crap .
working-directory: backend
run: go run github.com/dutifuldev/slophammer/go/cmd/slophammer-go@v0.3.0 crap ..

- name: Slophammer mutate4go mutation gate
run: go run github.com/dutifuldev/slophammer/go/cmd/slophammer@latest go mutate . --scan
working-directory: backend
run: go run github.com/dutifuldev/slophammer/go/cmd/slophammer-go@v0.3.0 mutate .. --scan

- name: Slophammer executable check
env:
DATABASE_URL: postgres://news:testpass@localhost:5432/news_pipeline?sslmode=disable
run: go run github.com/dutifuldev/slophammer/go/cmd/slophammer@latest check . --execute
working-directory: backend
run: go run github.com/dutifuldev/slophammer/go/cmd/slophammer-go@v0.3.0 check .. --execute

frontend:
name: Frontend (TypeScript)
Expand Down
15 changes: 10 additions & 5 deletions .github/workflows/go-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,25 @@ jobs:
cache-dependency-path: backend/go.sum

- name: Static Slophammer check
run: go run github.com/dutifuldev/slophammer/go/cmd/slophammer@latest check .
working-directory: backend
run: go run github.com/dutifuldev/slophammer/go/cmd/slophammer-go@v0.3.0 check ..

- name: DRY check
run: go run github.com/dutifuldev/slophammer/go/cmd/slophammer@latest go dry .
working-directory: backend
run: go run github.com/dutifuldev/slophammer/go/cmd/slophammer-go@v0.3.0 dry ..

- name: CRAP check
env:
DATABASE_URL: postgres://news:testpass@localhost:5432/news_pipeline?sslmode=disable
run: go run github.com/dutifuldev/slophammer/go/cmd/slophammer@latest go crap .
working-directory: backend
run: go run github.com/dutifuldev/slophammer/go/cmd/slophammer-go@v0.3.0 crap ..

- name: mutate4go mutation smoke check
run: go run github.com/dutifuldev/slophammer/go/cmd/slophammer@latest go mutate . --scan
working-directory: backend
run: go run github.com/dutifuldev/slophammer/go/cmd/slophammer-go@v0.3.0 mutate .. --scan

- name: Executable Slophammer check
env:
DATABASE_URL: postgres://news:testpass@localhost:5432/news_pipeline?sslmode=disable
run: go run github.com/dutifuldev/slophammer/go/cmd/slophammer@latest check . --execute
working-directory: backend
run: go run github.com/dutifuldev/slophammer/go/cmd/slophammer-go@v0.3.0 check .. --execute
64 changes: 33 additions & 31 deletions backend/internal/app/person_identities.go
Original file line number Diff line number Diff line change
Expand Up @@ -462,41 +462,43 @@ func executePersonIdentitiesListCommand(cfg personIdentitiesListCommandConfig) i
}

func runPersonIdentitiesShow(args []string) int {
if len(args) < 1 {
fmt.Fprintln(os.Stderr, "usage: scoop person-identities show <identity_ref-or-person_identity_uuid>")
return 2
}
fs := newAppFlagSet("person-identities show")
envLoader := cli.AddEnvFlag(fs, ".env", "Path to the .env file")
timeout := fs.Duration("timeout", 30*time.Second, "Command timeout")
format := fs.String("format", outputFormatTable, "Output format: table or json")
if exitCode, ok := parseAppFlagSet(fs, args[1:]); !ok {
return exitCode
}
if fs.NArg() != 0 {
fmt.Fprintln(os.Stderr, "too many positional arguments")
return 2
}
return runWithReadPool(*timeout, envLoader, func(ctx context.Context, pool *db.Pool) int {
identity, err := pool.GetPersonIdentity(ctx, args[0])
if err != nil {
if errors.Is(err, db.ErrNoRows) {
fmt.Fprintln(os.Stderr, "Person identity not found")
return 1
}
fmt.Fprintf(os.Stderr, "Failed to show person identity: %v\n", err)
return 1
}
return printPersonIdentityResult(identity, *format)
})
return runPersonIdentityCommand(
"person-identities show",
"usage: scoop person-identities show <identity_ref-or-person_identity_uuid>",
args,
"Failed to show person identity",
func(ctx context.Context, pool *db.Pool, ref string) (*db.PersonIdentityRecord, error) {
return pool.GetPersonIdentity(ctx, ref)
},
)
}

func runPersonIdentitiesArchive(args []string, archived bool) int {
return runPersonIdentityCommand(
"person-identities archive",
"usage: scoop person-identities archive <identity_ref-or-person_identity_uuid>",
args,
"Failed to update person identity",
func(ctx context.Context, pool *db.Pool, ref string) (*db.PersonIdentityRecord, error) {
return pool.SetPersonIdentityArchived(ctx, ref, archived, globaltime.UTC())
},
)
}

// runPersonIdentityCommand runs a single-identity command: shared flag
// parsing, the identity operation, and result printing.
func runPersonIdentityCommand(
name string,
usage string,
args []string,
failure string,
operation func(context.Context, *db.Pool, string) (*db.PersonIdentityRecord, error),
) int {
if len(args) < 1 {
fmt.Fprintln(os.Stderr, "usage: scoop person-identities archive <identity_ref-or-person_identity_uuid>")
fmt.Fprintln(os.Stderr, usage)
return 2
}
fs := newAppFlagSet("person-identities archive")
fs := newAppFlagSet(name)
envLoader := cli.AddEnvFlag(fs, ".env", "Path to the .env file")
timeout := fs.Duration("timeout", 30*time.Second, "Command timeout")
format := fs.String("format", outputFormatTable, "Output format: table or json")
Expand All @@ -508,13 +510,13 @@ func runPersonIdentitiesArchive(args []string, archived bool) int {
return 2
}
return runWithReadPool(*timeout, envLoader, func(ctx context.Context, pool *db.Pool) int {
identity, err := pool.SetPersonIdentityArchived(ctx, args[0], archived, globaltime.UTC())
identity, err := operation(ctx, pool, args[0])
if err != nil {
if errors.Is(err, db.ErrNoRows) {
fmt.Fprintln(os.Stderr, "Person identity not found")
return 1
}
fmt.Fprintf(os.Stderr, "Failed to update person identity: %v\n", err)
fmt.Fprintf(os.Stderr, "%s: %v\n", failure, err)
return 1
}
return printPersonIdentityResult(identity, *format)
Expand Down
63 changes: 38 additions & 25 deletions backend/internal/app/tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,41 +129,60 @@ func parseTagsCreateCommand(args []string) (tagsCreateCommandConfig, int, bool)
}
slug := args[0]

fs := newAppFlagSet("tags create")
envLoader := cli.AddEnvFlag(fs, ".env", "Path to the .env file")
timeout := fs.Duration("timeout", 30*time.Second, "Command timeout")
format := fs.String("format", outputFormatTable, "Output format: table or json")
description := fs.String("description", "", "Description")
color := fs.String("color", "", "Tag color as #RRGGBB")
highlightColor := fs.String("highlight-color", "", "Article/story highlight color as #RRGGBB")
if err := fs.Parse(args[1:]); err != nil {
flags := newTagAttributeFlagSet("tags create")
if err := flags.fs.Parse(args[1:]); err != nil {
if errors.Is(err, flag.ErrHelp) {
return tagsCreateCommandConfig{}, 0, false
}
return tagsCreateCommandConfig{}, 2, false
}
if fs.NArg() != 0 {
if flags.fs.NArg() != 0 {
fmt.Fprintln(os.Stderr, "tags create accepts only one tag")
return tagsCreateCommandConfig{}, 2, false
}
outputFormat, err := parseOutputFormat(*format, outputFormatTable)
outputFormat, err := parseOutputFormat(*flags.format, outputFormatTable)
if err != nil {
fmt.Fprintf(os.Stderr, "Invalid format: %v\n", err)
return tagsCreateCommandConfig{}, 2, false
}
return tagsCreateCommandConfig{
envLoader: envLoader,
timeout: *timeout,
envLoader: flags.envLoader,
timeout: *flags.timeout,
format: outputFormat,
opts: db.UpsertTagOptions{
Slug: slug,
Description: stringPtrFromFlag(description),
Color: stringPtrFromFlag(color),
HighlightColor: stringPtrFromFlag(highlightColor),
Description: stringPtrFromFlag(flags.description),
Color: stringPtrFromFlag(flags.color),
HighlightColor: stringPtrFromFlag(flags.highlightColor),
},
}, 0, true
}

// tagAttributeFlags bundles the flag set shared by the tag commands that
// accept the tag attribute flags alongside the common app flags.
type tagAttributeFlags struct {
fs *flag.FlagSet
envLoader *cli.EnvLoader
timeout *time.Duration
format *string
description *string
color *string
highlightColor *string
}

func newTagAttributeFlagSet(name string) tagAttributeFlags {
fs := newAppFlagSet(name)
return tagAttributeFlags{
fs: fs,
envLoader: cli.AddEnvFlag(fs, ".env", "Path to the .env file"),
timeout: fs.Duration("timeout", 30*time.Second, "Command timeout"),
format: fs.String("format", outputFormatTable, "Output format: table or json"),
description: fs.String("description", "", "Description"),
color: fs.String("color", "", "Tag color as #RRGGBB"),
highlightColor: fs.String("highlight-color", "", "Article/story highlight color as #RRGGBB"),
}
}

func executeTagsCreateCommand(cfg tagsCreateCommandConfig) int {
ctx, cancel, pool, err := connectReadPool(cfg.timeout, cfg.envLoader)
if err != nil {
Expand Down Expand Up @@ -215,24 +234,18 @@ func runTagsUpdate(args []string) int {
if !ok {
return exitCode
}
fs := newAppFlagSet("tags update")
envLoader := cli.AddEnvFlag(fs, ".env", "Path to the .env file")
timeout := fs.Duration("timeout", 30*time.Second, "Command timeout")
format := fs.String("format", outputFormatTable, "Output format: table or json")
description := fs.String("description", "", "Description")
color := fs.String("color", "", "Tag color as #RRGGBB")
highlightColor := fs.String("highlight-color", "", "Article/story highlight color as #RRGGBB")
if err := fs.Parse(args[1:]); err != nil {
flags := newTagAttributeFlagSet("tags update")
if err := flags.fs.Parse(args[1:]); err != nil {
if errors.Is(err, flag.ErrHelp) {
return 0
}
return 2
}
if fs.NArg() != 0 {
if flags.fs.NArg() != 0 {
fmt.Fprintln(os.Stderr, "too many positional arguments")
return 2
}
return updateTag(slug, tagUpdateOptionsFromFlags(description, color, highlightColor), *timeout, envLoader, *format)
return updateTag(slug, tagUpdateOptionsFromFlags(flags.description, flags.color, flags.highlightColor), *flags.timeout, flags.envLoader, *flags.format)
}

func parseRequiredTagSlugArg(args []string) (string, int, bool) {
Expand Down
29 changes: 8 additions & 21 deletions backend/internal/translation/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,12 @@ func (m *Manager) TranslateArticleByUUID(ctx context.Context, articleUUID string
return RunStats{}, err
}

tasks := make([]translationTask, 0, 2)
tasks := appendArticleTranslationTasks(make([]translationTask, 0, 2), article)

return m.runTasks(ctx, tasks, opts)
}

func appendArticleTranslationTasks(tasks []translationTask, article articleTranslationTarget) []translationTask {
if strings.TrimSpace(article.Title) != "" {
tasks = append(tasks, translationTask{
SourceType: SourceTypeArticleTitle,
Expand All @@ -176,8 +181,7 @@ func (m *Manager) TranslateArticleByUUID(ctx context.Context, articleUUID string
ContentOrigin: article.TextOrigin,
})
}

return m.runTasks(ctx, tasks, opts)
return tasks
}

func (m *Manager) TranslateCollection(ctx context.Context, collection string, opts CollectionRunOptions) (RunStats, error) {
Expand Down Expand Up @@ -299,24 +303,7 @@ func (m *Manager) translateStory(ctx context.Context, story storyTranslationTarg
}

for _, article := range articles {
if strings.TrimSpace(article.Title) != "" {
tasks = append(tasks, translationTask{
SourceType: SourceTypeArticleTitle,
SourceID: article.ArticleID,
SourceLang: article.SourceLang,
OriginalText: article.Title,
ContentOrigin: ContentOriginNormalized,
})
}
if strings.TrimSpace(article.Text) != "" {
tasks = append(tasks, translationTask{
SourceType: SourceTypeArticleText,
SourceID: article.ArticleID,
SourceLang: article.SourceLang,
OriginalText: article.Text,
ContentOrigin: article.TextOrigin,
})
}
tasks = appendArticleTranslationTasks(tasks, article)
}

return m.runTasks(ctx, tasks, opts)
Expand Down
36 changes: 20 additions & 16 deletions slophammer.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
go:
coverage_threshold: 85
dry_max_candidates: 0
dry_paths:
- backend/cmd
- backend/internal
- backend/schema
dry_exclude:
- "**/*_test.go"
- "**/testdata/**"
- "**/fixtures/**"
crap_max_score: 8
mutation_targets:
- backend/internal/language/normalize.go
- backend/internal/reader/preview.go
- backend/internal/httpapi/tag_handlers.go
- backend/internal/httpapi/person_identity_handlers.go
coverage:
threshold: 85
dry:
max_findings: 0
paths:
- backend/cmd
- backend/internal
- backend/schema
exclude:
- "**/*_test.go"
- "**/testdata/**"
- "**/fixtures/**"
crap:
max_score: 8
mutation:
targets:
- backend/internal/language/normalize.go
- backend/internal/reader/preview.go
- backend/internal/httpapi/tag_handlers.go
- backend/internal/httpapi/person_identity_handlers.go
dependency_boundaries:
- from: backend/cmd/scoop
allow:
Expand Down