mirror of
https://gitea.com/gitea/runner.git
synced 2026-08-29 07:17:46 +00:00
feat: gate set-env/add-path and render annotation locations (#1109)
`::set-env::` and `::add-path::` let a step rewrite the environment of every later step from its own output, which the runner honoured silently. They are now refused, as GitHub has done since 2020, and `ACTIONS_ALLOW_UNSECURE_COMMANDS` opts back in per step or job. Support for that variable is new here too, and is the only opt-in, matching GitHub rather than adding a runner config key on top. Annotations keep their source location: Gitea has no annotation store and its web UI strips command properties, so `::error file=main.go,line=12::msg` is rendered as `::error::main.go:12: msg`. `DEVELOPMENT.md` writes down the log line encoding rules this relies on. --------- Co-authored-by: silverwind <me@silverwind.io> Reviewed-on: https://gitea.com/gitea/runner/pulls/1109 Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com> Co-authored-by: bircni <bircni@icloud.com>
This commit is contained in:
@@ -72,9 +72,12 @@ func TestReporter_parseLogRow(t *testing.T) {
|
||||
"Debug enabled", true,
|
||||
[]string{
|
||||
"::debug::GitHub Actions runtime token access controls",
|
||||
// Left escaped: the web UI decodes it, and a real newline would not survive storage.
|
||||
"::debug::first%0Asecond",
|
||||
},
|
||||
[]string{
|
||||
"GitHub Actions runtime token access controls",
|
||||
"::debug::GitHub Actions runtime token access controls",
|
||||
"::debug::first%0Asecond",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -86,31 +89,46 @@ func TestReporter_parseLogRow(t *testing.T) {
|
||||
"<nil>",
|
||||
},
|
||||
},
|
||||
// The three annotation levels share one code path, so the property shapes are only
|
||||
// exercised under "error"; notice and warning just prove the level token round-trips.
|
||||
{
|
||||
"notice", false,
|
||||
[]string{
|
||||
"::notice file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
|
||||
"::notice::Gosh, that's not going to work",
|
||||
},
|
||||
[]string{
|
||||
"::notice file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
|
||||
"::notice::Gosh, that's not going to work",
|
||||
},
|
||||
},
|
||||
{
|
||||
"warning", false,
|
||||
[]string{
|
||||
"::warning file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
|
||||
"::warning::Gosh, that's not going to work",
|
||||
},
|
||||
[]string{
|
||||
"::warning file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
|
||||
"::warning::Gosh, that's not going to work",
|
||||
},
|
||||
},
|
||||
{
|
||||
"error", false,
|
||||
[]string{
|
||||
"::error file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
|
||||
"::error::Gosh, that's not going to work",
|
||||
"::error file=file.name,line=42,col=7::Gosh, that's not going to work",
|
||||
// The message keeps its own '::', the property list ends at the first one.
|
||||
"::error file=main.cpp,line=12::no member named 'foo' in 'std::vector<int>'",
|
||||
// GitHub matches property names case-insensitively.
|
||||
"::error File=file.name,Line=42,Col=7::Gosh, that's not going to work",
|
||||
// Only the property separators are decoded here, %25/%0A are left for the web UI.
|
||||
"::error file=a%3Ab.go,title=100%252C::still %25 escaped%0Aand multi-line",
|
||||
},
|
||||
[]string{
|
||||
"::error file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
|
||||
"::error::file.name:42: Cool Title: Gosh, that's not going to work",
|
||||
"::error::Gosh, that's not going to work",
|
||||
"::error::file.name:42:7: Gosh, that's not going to work",
|
||||
"::error::main.cpp:12: no member named 'foo' in 'std::vector<int>'",
|
||||
"::error::file.name:42:7: Gosh, that's not going to work",
|
||||
"::error::a:b.go: 100%252C: still %25 escaped%0Aand multi-line",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -149,6 +167,24 @@ func TestReporter_parseLogRow(t *testing.T) {
|
||||
"*** bar baz ***",
|
||||
},
|
||||
},
|
||||
{
|
||||
// a token naming a real command must still resume
|
||||
"stop-commands with a command-named token", false,
|
||||
[]string{
|
||||
"::stop-commands::add-mask",
|
||||
"::set-output name=x::suppressed",
|
||||
"::add-mask::",
|
||||
"::add-mask::masked",
|
||||
"masked",
|
||||
},
|
||||
[]string{
|
||||
"<nil>",
|
||||
"::set-output name=x::suppressed",
|
||||
"<nil>",
|
||||
"<nil>",
|
||||
"***",
|
||||
},
|
||||
},
|
||||
{
|
||||
"unknown command", false,
|
||||
[]string{
|
||||
@@ -179,6 +215,19 @@ func TestReporter_parseLogRow(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Both add-mask forms must register the secret and drop their own row: the runner forwards
|
||||
// the raw line, so failing to consume it writes the secret straight to the job log.
|
||||
func TestReporter_parseLogRowAddMask(t *testing.T) {
|
||||
for _, line := range []string{"::add-mask::supersecret", "##[add-mask]supersecret"} {
|
||||
r := &Reporter{logReplacer: strings.NewReplacer()}
|
||||
|
||||
assert.Nil(t, r.parseLogRow(&log.Entry{Message: line}), line)
|
||||
|
||||
row := r.parseLogRow(&log.Entry{Message: "using supersecret now"})
|
||||
assert.Equal(t, "using *** now", row.Content, line)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReporter_Fire(t *testing.T) {
|
||||
t.Run("ignore command lines", func(t *testing.T) {
|
||||
client := mocks.NewClient(t)
|
||||
@@ -1013,7 +1062,7 @@ func TestReporter_Result(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestReporter_SetOutputs(t *testing.T) {
|
||||
r := &Reporter{state: &runnerv1.TaskState{}}
|
||||
r := &Reporter{state: &runnerv1.TaskState{}, logReplacer: strings.NewReplacer()}
|
||||
|
||||
r.SetOutputs(map[string]string{"foo": "bar"})
|
||||
got, ok := r.outputs["foo"]
|
||||
|
||||
Reference in New Issue
Block a user