mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-08-06 17:04:22 +02:00
enhance: improve config, comment out values in example file (#1145)
`config.example.yaml` now has every value commented out, as gitea's `app.example.ini` does, so it documents each option with its default instead of imposing it. Copying it no longer pins values the runner would otherwise pick, and a changed default reaches configs that never named the option. `config init` writes the file to configure: a header comment and no option, so every option keeps its default. It refuses to overwrite an existing config without `--force`, and writes `config.yaml` in the working directory when `-c` is absent. `config set`, `add` and `remove` keep such a file readable. Comments go back to the indentation they were written at, which the encoder drops for a comment block that has no key below it, and a file of only comments keeps its text instead of being emptied by the first edit. Also rewrote the config docs sections for clarity. Reviewed-on: https://gitea.com/gitea/runner/pulls/1145 Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com> Reviewed-by: bircni <bircni@icloud.com> Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
@@ -160,6 +160,7 @@ type editSession struct {
|
||||
root *yaml.Node
|
||||
field *fieldInfo
|
||||
segments []string
|
||||
preamble []byte // text of a file that holds no YAML node, which the encoder cannot give back
|
||||
}
|
||||
|
||||
// loadForEdit validates the path and parses the file, so every caller fails before anything is written.
|
||||
@@ -176,7 +177,7 @@ func loadForEdit(file, path string) (*editSession, error) {
|
||||
content, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, fmt.Errorf("config file %q does not exist, create one with `config generate`", file)
|
||||
return nil, fmt.Errorf("config file %q does not exist, create one with `config init`", file)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -185,7 +186,9 @@ func loadForEdit(file, path string) (*editSession, error) {
|
||||
if err := yaml.Unmarshal(content, &root); err != nil {
|
||||
return nil, fmt.Errorf("parse config file %q: %w", file, err)
|
||||
}
|
||||
var preamble []byte
|
||||
if root.Kind == 0 || len(root.Content) == 0 {
|
||||
preamble = bytes.TrimSpace(content) // all the file has is comments
|
||||
root = yaml.Node{
|
||||
Kind: yaml.DocumentNode,
|
||||
Content: []*yaml.Node{{Kind: yaml.MappingNode, Tag: "!!map"}},
|
||||
@@ -195,7 +198,7 @@ func loadForEdit(file, path string) (*editSession, error) {
|
||||
return nil, fmt.Errorf("config file %q is not a YAML mapping", file)
|
||||
}
|
||||
|
||||
return &editSession{file: file, path: path, original: content, root: &root, field: field, segments: segments}, nil
|
||||
return &editSession{file: file, path: path, original: content, root: &root, field: field, segments: segments, preamble: preamble}, nil
|
||||
}
|
||||
|
||||
func loadSequenceEdit(file, path string, values []string) (*editSession, error) {
|
||||
@@ -319,16 +322,28 @@ func encodeYAML(node *yaml.Node) ([]byte, error) {
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// restoreBlankLines re-inserts the blank lines between top-level sections that the encoder drops.
|
||||
func restoreBlankLines(original, generated []byte) []byte {
|
||||
// restoreLayout re-applies the spacing the encoder drops: the blank lines between
|
||||
// top-level sections, and the indentation of comments, which the encoder emits at the
|
||||
// indentation of the node it attached them to rather than the one they were written at.
|
||||
func restoreLayout(original, generated []byte) []byte {
|
||||
type comment struct {
|
||||
line string // as written, indentation included
|
||||
trimmed string
|
||||
blankBefore bool
|
||||
}
|
||||
|
||||
var comments []comment
|
||||
spaced := map[string]bool{}
|
||||
blank := false
|
||||
for line := range strings.Lines(string(original)) {
|
||||
line = strings.TrimRight(line, "\r\n")
|
||||
trimmed := strings.TrimSpace(line)
|
||||
switch {
|
||||
case strings.TrimSpace(line) == "":
|
||||
case trimmed == "":
|
||||
blank = true
|
||||
case strings.HasPrefix(line, "#"): // the block belongs to the key below it
|
||||
case strings.HasPrefix(trimmed, "#"):
|
||||
comments = append(comments, comment{line: line, trimmed: trimmed, blankBefore: blank})
|
||||
blank = false
|
||||
default:
|
||||
if key, ok := topLevelKey(line); ok && blank {
|
||||
spaced[key] = true
|
||||
@@ -338,16 +353,26 @@ func restoreBlankLines(original, generated []byte) []byte {
|
||||
}
|
||||
|
||||
var out []string
|
||||
appendBlank := func() {
|
||||
if len(out) > 0 && strings.TrimSpace(out[len(out)-1]) != "" {
|
||||
out = append(out, "")
|
||||
}
|
||||
}
|
||||
for line := range strings.Lines(string(generated)) {
|
||||
line = strings.TrimRight(line, "\r\n")
|
||||
if key, ok := topLevelKey(line); ok && spaced[key] {
|
||||
start := len(out)
|
||||
for start > 0 && strings.HasPrefix(out[start-1], "#") {
|
||||
start--
|
||||
}
|
||||
if start > 0 && strings.TrimSpace(out[start-1]) != "" {
|
||||
out = slices.Insert(out, start, "")
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "#") {
|
||||
if i := slices.IndexFunc(comments, func(c comment) bool { return c.trimmed == trimmed }); i >= 0 {
|
||||
if comments[i].blankBefore {
|
||||
appendBlank()
|
||||
} else if len(out) > 0 && strings.TrimSpace(out[len(out)-1]) == "" {
|
||||
out = out[:len(out)-1] // the encoder separates a comment block it moved
|
||||
}
|
||||
line = comments[i].line
|
||||
comments = comments[i+1:] // the encoder keeps their order, so earlier ones cannot match again
|
||||
}
|
||||
} else if key, ok := topLevelKey(line); ok && spaced[key] {
|
||||
appendBlank()
|
||||
}
|
||||
out = append(out, line)
|
||||
}
|
||||
@@ -377,12 +402,21 @@ func (s *editSession) write() error {
|
||||
return fmt.Errorf("the edit would produce a config the runner cannot load: %w", err)
|
||||
}
|
||||
|
||||
content := restoreBlankLines(s.original, generated)
|
||||
if len(s.preamble) > 0 { // before restoreLayout, so that it spaces the preamble too
|
||||
generated = slices.Concat(s.preamble, []byte("\n"), generated)
|
||||
}
|
||||
|
||||
content := restoreLayout(s.original, generated)
|
||||
if bytes.Contains(s.original, []byte("\r\n")) { // the encoder only ever emits LF
|
||||
content = bytes.ReplaceAll(content, []byte("\n"), []byte("\r\n"))
|
||||
}
|
||||
|
||||
file := s.file
|
||||
return WriteFile(s.file, content)
|
||||
}
|
||||
|
||||
// WriteFile replaces the config file in one step, keeping the mode and owner of the
|
||||
// file it replaces, so a half-written config never reaches a runner reading it.
|
||||
func WriteFile(file string, content []byte) error {
|
||||
if resolved, err := filepath.EvalSymlinks(file); err == nil {
|
||||
file = resolved // keeps a config linked in from elsewhere intact
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user