Skip to content

emit-annotations: workflow-command message and property values are not escaped #81

Description

@dacharyc

Summary

formatAnnotation in report/annotations.go emits GitHub Actions workflow commands without escaping the message or the property values (file, title). GitHub's workflow-command spec requires certain characters to be percent-encoded; without it, annotations with those characters are truncated, mis-rendered, or split into the wrong fields.

Details

The command is built as:

return fmt.Sprintf("::%s%s::%s", cmd, params, res.Message)

with params containing file=<path>, ,line=N, and ,title=<category>, all interpolated raw.

GitHub requires:

  • Message data%%25, \r%0D, \n%0A
  • Property values (e.g. file, title) — the above plus :%3A and ,%2C

Impact

  • A res.Message containing a newline is cut off at the first \n (the runner treats the command as ending there), so multi-line messages lose everything after the first line.
  • A literal % in a message can be misinterpreted.
  • A title or file containing , prematurely ends the property and corrupts the parsed parameters; a : can break the key=value parsing.

These are most likely to surface as messages grow longer or include punctuation, paths on Windows (C:\...), or categories with commas.

Repro

A result with Message: "line one\nline two" produces:

::error file=skill/SKILL.md,line=1,title=Cat::line one
line two

GitHub parses only line one as the annotation; line two is dropped from the annotation (it appears as a normal log line).

Suggested fix

Add the two escaping helpers and apply escapeData to the message and escapeProperty to each property value:

func escapeData(s string) string {
    s = strings.ReplaceAll(s, "%", "%25")
    s = strings.ReplaceAll(s, "\r", "%0D")
    s = strings.ReplaceAll(s, "\n", "%0A")
    return s
}

func escapeProperty(s string) string {
    s = escapeData(s)
    s = strings.ReplaceAll(s, ":", "%3A")
    s = strings.ReplaceAll(s, ",", "%2C")
    return s
}

(The % replacement must run first so the other substitutions aren't double-encoded.)

Happy to send a PR if that's useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions