add code & testing files for continue-on-error flag

- add continue-on-error flag as a command line option
    - if set, doesnt exit on error and continues
        processing other files that were passed in
- add fatalErrorHandler to handle fatal errors
    - if continue-on-error flag is set, does not exit
- add temporary tests for continue-on-error flag
    - add tests in batch-tests subdirectory
This commit is contained in:
iyz 2025-02-19 14:50:10 -05:00 committed by Manuel Rüger
parent ff015e2c24
commit 024259e480
6 changed files with 200 additions and 46 deletions

35
error_handler.go Normal file
View File

@ -0,0 +1,35 @@
package main
import (
"fmt"
"github.com/reconquest/pkg/log"
)
type FatalErrorHandler struct {
ContinueOnError bool
}
func NewErrorHandler(continueOnError bool) *FatalErrorHandler {
return &FatalErrorHandler{
ContinueOnError: continueOnError,
}
}
func (h *FatalErrorHandler) Handle(err error, format string, args ...interface{}) {
errorMesage := fmt.Sprintf(format, args...)
if err == nil {
if h.ContinueOnError {
log.Error(errorMesage)
return
}
log.Fatal(errorMesage)
}
if h.ContinueOnError {
log.Errorf(err, errorMesage)
return
}
log.Fatalf(err, errorMesage)
}

152
main.go
View File

@ -44,6 +44,12 @@ var flags = []cli.Flag{
TakesFile: true, TakesFile: true,
EnvVars: []string{"MARK_FILES"}, EnvVars: []string{"MARK_FILES"},
}), }),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "continue-on-error",
Value: false,
Usage: "dont exit if an error occurs while processing a file, continue processing remaining files.",
EnvVars: []string{"MARK_CONTINUE_ON_ERROR"},
}),
altsrc.NewBoolFlag(&cli.BoolFlag{ altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "compile-only", Name: "compile-only",
Value: false, Value: false,
@ -276,6 +282,11 @@ func RunMark(cCtx *cli.Context) error {
} }
} }
fatalErrorHandler := NewErrorHandler(cCtx.Bool("continue-on-error"))
fmt.Printf("Processing %d files\n", len(files))
fmt.Printf("continue-on-error: %t\n", cCtx.Bool("continue-on-error"))
// Loop through files matched by glob pattern // Loop through files matched by glob pattern
for _, file := range files { for _, file := range files {
log.Infof( log.Infof(
@ -284,9 +295,9 @@ func RunMark(cCtx *cli.Context) error {
file, file,
) )
target := processFile(file, api, cCtx, creds.PageID, creds.Username) target := processFile(file, api, cCtx, creds.PageID, creds.Username, fatalErrorHandler)
if target != nil && !(cCtx.Bool("dry-run") || cCtx.Bool("compile-only")) { // on dry-run or compile-only, the target is nil if target != nil { // on dry-run or compile-only, the target is nil
log.Infof( log.Infof(
nil, nil,
"page successfully updated: %s", "page successfully updated: %s",
@ -304,10 +315,13 @@ func processFile(
cCtx *cli.Context, cCtx *cli.Context,
pageID string, pageID string,
username string, username string,
fatalErrorHandler *FatalErrorHandler,
) *confluence.PageInfo { ) *confluence.PageInfo {
markdown, err := os.ReadFile(file) markdown, err := os.ReadFile(file)
if err != nil { if err != nil {
log.Fatal(err) fatalErrorHandler.Handle(err, "unable to read file %q", file)
return nil
// log.Fatal(err)
} }
markdown = bytes.ReplaceAll(markdown, []byte("\r\n"), []byte("\n")) markdown = bytes.ReplaceAll(markdown, []byte("\r\n"), []byte("\n"))
@ -316,7 +330,9 @@ func processFile(
meta, markdown, err := metadata.ExtractMeta(markdown, cCtx.String("space"), cCtx.Bool("title-from-h1"), parents, cCtx.Bool("title-append-generated-hash")) meta, markdown, err := metadata.ExtractMeta(markdown, cCtx.String("space"), cCtx.Bool("title-from-h1"), parents, cCtx.Bool("title-append-generated-hash"))
if err != nil { if err != nil {
log.Fatal(err) fatalErrorHandler.Handle(err, "unable to extract metadata from file %q", file)
return nil
// log.Fatal(err)
} }
if pageID != "" && meta != nil { if pageID != "" && meta != nil {
@ -329,29 +345,37 @@ func processFile(
} }
if pageID == "" && meta == nil { if pageID == "" && meta == nil {
log.Fatal( fatalErrorHandler.Handle(nil, "specified file doesn't contain metadata and URL is not specified via command line or doesn't contain pageId GET-parameter")
`specified file doesn't contain metadata ` + return nil
`and URL is not specified via command line ` + // log.Fatal(
`or doesn't contain pageId GET-parameter`, // `specified file doesn't contain metadata ` +
) // `and URL is not specified via command line ` +
// `or doesn't contain pageId GET-parameter`,
// )
} }
if meta.Space == "" { if meta.Space == "" {
log.Fatal( fatalErrorHandler.Handle(nil, "space is not set ('Space' header is not set and '--space' option is not set)")
"space is not set ('Space' header is not set and '--space' option is not set)", return nil
) // log.Fatal(
// "space is not set ('Space' header is not set and '--space' option is not set)",
// )
} }
if meta.Title == "" { if meta.Title == "" {
log.Fatal( fatalErrorHandler.Handle(nil, "page title is not set ('Title' header is not set and '--title-from-h1' option and 'h1_title' config is not set or there is no H1 in the file)")
`page title is not set ('Title' header is not set ` + return nil
`and '--title-from-h1' option and 'h1_title' config is not set or there is no H1 in the file)`, // log.Fatal(
) // `page title is not set ('Title' header is not set ` +
// `and '--title-from-h1' option and 'h1_title' config is not set or there is no H1 in the file)`,
// )
} }
stdlib, err := stdlib.New(api) stdlib, err := stdlib.New(api)
if err != nil { if err != nil {
log.Fatal(err) fatalErrorHandler.Handle(err, "unable to retrieve standard library")
return nil
// log.Fatal(err)
} }
templates := stdlib.Templates templates := stdlib.Templates
@ -366,7 +390,9 @@ func processFile(
templates, templates,
) )
if err != nil { if err != nil {
log.Fatal(err) fatalErrorHandler.Handle(err, "unable to process includes")
return nil
// log.Fatal(err)
} }
if !recurse { if !recurse {
@ -381,7 +407,9 @@ func processFile(
templates, templates,
) )
if err != nil { if err != nil {
log.Fatal(err) fatalErrorHandler.Handle(err, "unable to extract macros")
return nil
// log.Fatal(err)
} }
macros = append(macros, stdlib.Macros...) macros = append(macros, stdlib.Macros...)
@ -389,13 +417,17 @@ func processFile(
for _, macro := range macros { for _, macro := range macros {
markdown, err = macro.Apply(markdown) markdown, err = macro.Apply(markdown)
if err != nil { if err != nil {
log.Fatal(err) fatalErrorHandler.Handle(err, "unable to apply macro")
return nil
// log.Fatal(err)
} }
} }
links, err := page.ResolveRelativeLinks(api, meta, markdown, filepath.Dir(file), cCtx.String("space"), cCtx.Bool("title-from-h1"), parents, cCtx.Bool("title-append-generated-hash")) links, err := page.ResolveRelativeLinks(api, meta, markdown, filepath.Dir(file), cCtx.String("space"), cCtx.Bool("title-from-h1"), parents, cCtx.Bool("title-append-generated-hash"))
if err != nil { if err != nil {
log.Fatalf(err, "unable to resolve relative links") fatalErrorHandler.Handle(err, "unable to resolve relative links")
return nil
// log.Fatalf(err, "unable to resolve relative links")
} }
markdown = page.SubstituteLinks(markdown, links) markdown = page.SubstituteLinks(markdown, links)
@ -403,7 +435,9 @@ func processFile(
if cCtx.Bool("dry-run") { if cCtx.Bool("dry-run") {
_, _, err := page.ResolvePage(cCtx.Bool("dry-run"), api, meta) _, _, err := page.ResolvePage(cCtx.Bool("dry-run"), api, meta)
if err != nil { if err != nil {
log.Fatalf(err, "unable to resolve page location") fatalErrorHandler.Handle(err, "unable to resolve page location")
return nil
// log.Fatalf(err, "unable to resolve page location")
} }
} }
@ -413,7 +447,6 @@ func processFile(
"the leading H1 heading will be excluded from the Confluence output", "the leading H1 heading will be excluded from the Confluence output",
) )
} }
html, _ := mark.CompileMarkdown(markdown, stdlib, file, cCtx.String("mermaid-provider"), cCtx.Float64("mermaid-scale"), cCtx.Bool("drop-h1"), cCtx.Bool("strip-linebreaks")) html, _ := mark.CompileMarkdown(markdown, stdlib, file, cCtx.String("mermaid-provider"), cCtx.Float64("mermaid-scale"), cCtx.Bool("drop-h1"), cCtx.Bool("strip-linebreaks"))
fmt.Println(html) fmt.Println(html)
return nil return nil
@ -424,11 +457,13 @@ func processFile(
if meta != nil { if meta != nil {
parent, page, err := page.ResolvePage(cCtx.Bool("dry-run"), api, meta) parent, page, err := page.ResolvePage(cCtx.Bool("dry-run"), api, meta)
if err != nil { if err != nil {
log.Fatalf( // log.Fatalf(
karma.Describe("title", meta.Title).Reason(err), // karma.Describe("title", meta.Title).Reason(err),
"unable to resolve %s", // "unable to resolve %s",
meta.Type, // meta.Type,
) // )
fatalErrorHandler.Handle(karma.Describe("title", meta.Title).Reason(err), "unable to resolve %s", meta.Type)
return nil
} }
if page == nil { if page == nil {
@ -440,12 +475,14 @@ func processFile(
``, ``,
) )
if err != nil { if err != nil {
log.Fatalf( fatalErrorHandler.Handle(err, "can't create %s %q", meta.Type, meta.Title)
err, return nil
"can't create %s %q", // log.Fatalf(
meta.Type, // err,
meta.Title, // "can't create %s %q",
) // meta.Type,
// meta.Title,
// )
} }
// (issues/139): A delay between the create and update call // (issues/139): A delay between the create and update call
// helps mitigate a 409 conflict that can occur when attempting // helps mitigate a 409 conflict that can occur when attempting
@ -456,12 +493,16 @@ func processFile(
target = page target = page
} else { } else {
if pageID == "" { if pageID == "" {
log.Fatalf(nil, "URL should provide 'pageId' GET-parameter") fatalErrorHandler.Handle(nil, "URL should provide 'pageId' GET-parameter")
return nil
// log.Fatalf(nil, "URL should provide 'pageId' GET-parameter")
} }
page, err := api.GetPageByID(pageID) page, err := api.GetPageByID(pageID)
if err != nil { if err != nil {
log.Fatalf(err, "unable to retrieve page by id") fatalErrorHandler.Handle(err, "unable to retrieve page by id")
return nil
// log.Fatalf(err, "unable to retrieve page by id")
} }
target = page target = page
@ -470,7 +511,9 @@ func processFile(
// Resolve attachments created from <!-- Attachment: --> directive // Resolve attachments created from <!-- Attachment: --> directive
localAttachments, err := attachment.ResolveLocalAttachments(vfs.LocalOS, filepath.Dir(file), meta.Attachments) localAttachments, err := attachment.ResolveLocalAttachments(vfs.LocalOS, filepath.Dir(file), meta.Attachments)
if err != nil { if err != nil {
log.Fatalf(err, "unable to locate attachments") fatalErrorHandler.Handle(err, "unable to locate attachments")
return nil
// log.Fatalf(err, "unable to locate attachments")
} }
attaches, err := attachment.ResolveAttachments( attaches, err := attachment.ResolveAttachments(
@ -479,7 +522,9 @@ func processFile(
localAttachments, localAttachments,
) )
if err != nil { if err != nil {
log.Fatalf(err, "unable to create/update attachments") fatalErrorHandler.Handle(err, "unable to create/update attachments")
return nil
// log.Fatalf(err, "unable to create/update attachments")
} }
markdown = attachment.CompileAttachmentLinks(markdown, attaches) markdown = attachment.CompileAttachmentLinks(markdown, attaches)
@ -499,7 +544,9 @@ func processFile(
inlineAttachments, inlineAttachments,
) )
if err != nil { if err != nil {
log.Fatalf(err, "unable to create/update attachments") fatalErrorHandler.Handle(err, "unable to create/update attachments")
return nil
// log.Fatalf(err, "unable to create/update attachments")
} }
{ {
@ -519,7 +566,9 @@ func processFile(
}, },
) )
if err != nil { if err != nil {
log.Fatal(err) fatalErrorHandler.Handle(err, "unable to execute layout template")
return nil
// log.Fatal(err)
} }
html = buffer.String() html = buffer.String()
@ -567,11 +616,15 @@ func processFile(
if shouldUpdatePage { if shouldUpdatePage {
err = api.UpdatePage(target, html, cCtx.Bool("minor-edit"), finalVersionMessage, meta.Labels, meta.ContentAppearance, meta.Emoji) err = api.UpdatePage(target, html, cCtx.Bool("minor-edit"), finalVersionMessage, meta.Labels, meta.ContentAppearance, meta.Emoji)
if err != nil { if err != nil {
log.Fatal(err) fatalErrorHandler.Handle(err, "unable to update page")
return nil
// log.Fatal(err)
} }
} }
updateLabels(api, target, meta) if !updateLabels(api, target, meta, fatalErrorHandler) { // on error updating labels, return nil
return nil
}
if cCtx.Bool("edit-lock") { if cCtx.Bool("edit-lock") {
log.Infof( log.Infof(
@ -583,14 +636,16 @@ func processFile(
err := api.RestrictPageUpdates(target, username) err := api.RestrictPageUpdates(target, username)
if err != nil { if err != nil {
log.Fatal(err) fatalErrorHandler.Handle(err, "unable to restrict page updates")
return nil
// log.Fatal(err)
} }
} }
return target return target
} }
func updateLabels(api *confluence.API, target *confluence.PageInfo, meta *metadata.Meta) { func updateLabels(api *confluence.API, target *confluence.PageInfo, meta *metadata.Meta, fatalErrorHandler *FatalErrorHandler) bool {
labelInfo, err := api.GetPageLabels(target, "global") labelInfo, err := api.GetPageLabels(target, "global")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -613,16 +668,21 @@ func updateLabels(api *confluence.API, target *confluence.PageInfo, meta *metada
if len(addLabels) > 0 { if len(addLabels) > 0 {
_, err = api.AddPageLabels(target, addLabels) _, err = api.AddPageLabels(target, addLabels)
if err != nil { if err != nil {
log.Fatal(err) fatalErrorHandler.Handle(err, "error adding labels")
return false
// log.Fatal(err)
} }
} }
for _, label := range delLabels { for _, label := range delLabels {
_, err = api.DeletePageLabel(target, label) _, err = api.DeletePageLabel(target, label)
if err != nil { if err != nil {
log.Fatal(err) fatalErrorHandler.Handle(err, "error deleting labels")
return false
// log.Fatal(err)
} }
} }
return true
} }
// Page has label but label not in Metadata // Page has label but label not in Metadata

15
testdata/batch-tests/broken-test.md vendored Normal file
View File

@ -0,0 +1,15 @@
# a
## b
### c
#### d
##### e
# f
## g
# This/is some_Heading.yml

10
testdata/batch-tests/irfan-test.md vendored Normal file
View File

@ -0,0 +1,10 @@
<!-- Space: MySpace -->
<!-- Parent: Parent -->
<!-- Title: whatnot -->
## Foo
> **TL;DR:** Thingy!
> More stuff
Foo

15
testdata/batch-tests/mark-test.md vendored Normal file
View File

@ -0,0 +1,15 @@
# a
## b
### c
#### d
##### e
# f
## g
# This/is some_Heading.yml

19
testdata/batch-tests/rich-test.md vendored Normal file
View File

@ -0,0 +1,19 @@
<!-- Space: TEST2 -->
<!-- Title: H2 -->
<!-- Title: whatnot -->
# a
## b
### c
#### d
##### e
# f
## g
# This/is some_Heading.yml