mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-26 06:16:56 +09:00
fix(actions): keep folded block scalars intact across serialization (#39787)
Folded (`>`) scalars in workflows gained blank lines and had spaces turned into newlines every time Gitea re-serialized a job. This bumps go-yaml to its main branch and drops our workarounds. The runner gets the same change in https://gitea.com/gitea/runner/pulls/1321. - go-yaml main fixes the accumulating blank lines (https://github.com/yaml/go-yaml/pull/366) and the block scalar indentation indicators (https://github.com/yaml/go-yaml/pull/414), so `BlockSafeString` and the folded scalar requoting go away - The job encoder no longer wraps lines, because go-yaml still wraps more-indented lines of folded scalars and turns a space into a newline - The fix applies to folded scalars anywhere in a workflow, including workflow-level `env` Fixes https://github.com/go-gitea/gitea/issues/39775 Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
1 parent
078198d38d
commit
c1ae4ec29e
7 files changed
+72
-59
No files matched your search
@@ -4,7 +4,6 @@
|
||||
package jobparser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -65,44 +64,31 @@ func (w *SingleWorkflow) SetJob(id string, job *Job) error {
|
||||
m := map[string]*Job{
|
||||
id: job,
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
encoder := yaml.NewEncoder(&buf)
|
||||
encoder.SetIndent(2)
|
||||
if err := encoder.Encode(m); err != nil {
|
||||
out, err := marshalYAML(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
encoder.Close()
|
||||
|
||||
node := yaml.Node{}
|
||||
if err := yaml.Unmarshal(buf.Bytes(), &node); err != nil {
|
||||
if err := yaml.Unmarshal(out, &node); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(node.Content) != 1 || node.Content[0].Kind != yaml.MappingNode {
|
||||
return fmt.Errorf("can not set job: %s", buf.String())
|
||||
return fmt.Errorf("can not set job: %s", out)
|
||||
}
|
||||
w.RawJobs = *node.Content[0]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *SingleWorkflow) Marshal() ([]byte, error) {
|
||||
// Encode with the same indentation SetJob uses (2). yaml.Marshal's default
|
||||
// indentation (4) makes the encoder emit multi-line block scalars (e.g. a
|
||||
// `run:` step that begins with blank lines) with a wrong explicit indentation
|
||||
// indicator (`run: |4`) that then fails to re-parse, which silently strands
|
||||
// the job during concurrency evaluation. Keeping both encoders at indent 2
|
||||
// makes the serialized single workflow round-trip.
|
||||
var buf bytes.Buffer
|
||||
enc := yaml.NewEncoder(&buf)
|
||||
enc.SetIndent(2)
|
||||
payload := *w
|
||||
payload.RunName = "" // already interpolated into the run title, a runner would parse it as a template again
|
||||
if err := enc.Encode(&payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := enc.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
return marshalYAML(&payload)
|
||||
}
|
||||
|
||||
// marshalYAML never wraps lines, go-yaml wraps more-indented lines of folded scalars until https://github.com/yaml/go-yaml/pull/441 is released.
|
||||
func marshalYAML(v any) ([]byte, error) {
|
||||
return yaml.Dump(v, yaml.WithV3Defaults(), yaml.WithIndent(2), yaml.WithLineWidth(-1))
|
||||
}
|
||||
|
||||
type Job struct {
|
||||
@@ -196,29 +182,18 @@ func (j *Job) DisplayName() string {
|
||||
return util.EllipsisDisplayString(unescapeExpressions(j.Name), 255)
|
||||
}
|
||||
|
||||
// BlockSafeString works around https://github.com/yaml/go-yaml/issues/399, quoting a value whose
|
||||
// leading newline would cost a literal block scalar its indentation indicator.
|
||||
type BlockSafeString string
|
||||
|
||||
func (s BlockSafeString) MarshalYAML() (any, error) {
|
||||
if !strings.HasPrefix(string(s), "\n") {
|
||||
return string(s), nil
|
||||
}
|
||||
return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Style: yaml.DoubleQuotedStyle, Value: string(s)}, nil
|
||||
}
|
||||
|
||||
type Step struct {
|
||||
ID string `yaml:"id,omitempty"`
|
||||
If yaml.Node `yaml:"if,omitempty"`
|
||||
Name BlockSafeString `yaml:"name,omitempty"`
|
||||
Uses string `yaml:"uses,omitempty"`
|
||||
Run BlockSafeString `yaml:"run,omitempty"`
|
||||
WorkingDirectory string `yaml:"working-directory,omitempty"`
|
||||
Shell string `yaml:"shell,omitempty"`
|
||||
Env yaml.Node `yaml:"env,omitempty"`
|
||||
With yaml.Node `yaml:"with,omitempty"`
|
||||
RawContinueOnError yaml.Node `yaml:"continue-on-error,omitempty"` // raw: the runner evaluates it with the steps context
|
||||
TimeoutMinutes string `yaml:"timeout-minutes,omitempty"`
|
||||
ID string `yaml:"id,omitempty"`
|
||||
If yaml.Node `yaml:"if,omitempty"`
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Uses string `yaml:"uses,omitempty"`
|
||||
Run string `yaml:"run,omitempty"`
|
||||
WorkingDirectory string `yaml:"working-directory,omitempty"`
|
||||
Shell string `yaml:"shell,omitempty"`
|
||||
Env yaml.Node `yaml:"env,omitempty"`
|
||||
With yaml.Node `yaml:"with,omitempty"`
|
||||
RawContinueOnError yaml.Node `yaml:"continue-on-error,omitempty"` // raw: the runner evaluates it with the steps context
|
||||
TimeoutMinutes string `yaml:"timeout-minutes,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalYAML canonicalizes booleans like continue-on-error
|
||||
@@ -240,9 +215,9 @@ func (s *Step) String() string {
|
||||
}
|
||||
return (&model.Step{
|
||||
ID: s.ID,
|
||||
Name: string(s.Name),
|
||||
Name: s.Name,
|
||||
Uses: s.Uses,
|
||||
Run: string(s.Run),
|
||||
Run: s.Run,
|
||||
}).String()
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
_, origJob := sws[0].Job()
|
||||
require.Len(t, origJob.Steps, 1)
|
||||
const wantRun = "\n\necho start\necho done\n"
|
||||
require.Equal(t, wantRun, string(origJob.Steps[0].Run))
|
||||
require.Equal(t, wantRun, origJob.Steps[0].Run)
|
||||
|
||||
payload, err := sws[0].Marshal()
|
||||
require.NoError(t, err)
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
// the round-trip must preserve the run block byte-for-byte
|
||||
_, gotJob := roundTripped[0].Job()
|
||||
require.Len(t, gotJob.Steps, 1)
|
||||
require.Equal(t, wantRun, string(gotJob.Steps[0].Run), "round-trip must preserve run content; got payload:\n%s", payload)
|
||||
require.Equal(t, wantRun, gotJob.Steps[0].Run, "round-trip must preserve run content; got payload:\n%s", payload)
|
||||
}
|
||||
|
||||
// Typing a step's continue-on-error as a bool used to reject the whole `jobs:` node.
|
||||
@@ -104,3 +104,37 @@ jobs:
|
||||
require.Equal(t, w, steps[i].RawContinueOnError, "step %d, payload:\n%s", i, payload)
|
||||
}
|
||||
}
|
||||
|
||||
// Folded (`>`) scalars in with/env used to gain blank lines and have spaces turned into newlines
|
||||
// on every Marshal -> Parse cycle.
|
||||
func TestSingleWorkflowRoundTripFoldedScalar(t *testing.T) {
|
||||
const wf = `name: demo
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: ./.github/actions/build
|
||||
with:
|
||||
cfg: >
|
||||
begin
|
||||
first "indented" line with path dir/Installer Scripts/some long path/another long path/some file name.xzy,
|
||||
second "indented" line
|
||||
end
|
||||
`
|
||||
const want = "begin\n first \"indented\" line with path dir/Installer Scripts/some long path/another long path/some file name.xzy,\n second \"indented\" line\nend\n"
|
||||
sws, err := Parse([]byte(wf))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, sws, 1)
|
||||
for range 3 {
|
||||
payload, err := sws[0].Marshal()
|
||||
require.NoError(t, err)
|
||||
sws, err = Parse(payload)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, sws, 1)
|
||||
}
|
||||
_, job := sws[0].Job()
|
||||
var with map[string]string
|
||||
require.NoError(t, job.Steps[0].With.Decode(&with))
|
||||
require.Equal(t, want, with["cfg"])
|
||||
}
|
||||
@@ -7,5 +7,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: extract_tag
|
||||
name: "\nExtract tag for variant"
|
||||
run: "\necho Test\n"
|
||||
name: |-
|
||||
|
||||
Extract tag for variant
|
||||
run: |
|
||||
|
||||
echo Test
|
||||
Reference in new issue
Block a user