nektos/act · error
ErrShortRef
ErrShortRef
Error message
short SHA references are not supported
What it means
This panic comes from act's expression pre-processor, rewriteSubExpression (pkg/runner/expression.go:416). Before evaluating, act rewrites every string that contains both '${{' and '}}' into a format(...) call by scanning for '${{' ... '}}' pairs while tracking single-quoted string literals. The panic at line 456 fires when the scanner is inside an expression (it has consumed a '${{') but can find neither a closing '}}' nor a single-quote string start ahead of the current position, so the expression can never be terminated. It is raised via panic(), not returned as an error, so it crashes the whole act run with a stack trace instead of producing a per-step failure.
Source
Thrown at pkg/common/git/git.go:33
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/storer"
"github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/mattn/go-isatty"
log "github.com/sirupsen/logrus"
"github.com/nektos/act/pkg/common"
)
var (
codeCommitHTTPRegex = regexp.MustCompile(`^https?://git-codecommit\.(.+)\.amazonaws.com/v1/repos/(.+)$`)
codeCommitSSHRegex = regexp.MustCompile(`ssh://git-codecommit\.(.+)\.amazonaws.com/v1/repos/(.+)$`)
githubHTTPRegex = regexp.MustCompile(`^https?://.*github.com.*/(.+)/(.+?)(?:.git)?$`)
githubSSHRegex = regexp.MustCompile(`github.com[:/](.+)/(.+?)(?:.git)?$`)
cloneLock sync.Mutex
ErrShortRef = errors.New("short SHA references are not supported")
ErrNoRepo = errors.New("unable to find git repo")
)
type Error struct {
err error
commit string
}
func (e *Error) Error() string {
return e.err.Error()
}
func (e *Error) Unwrap() error {
return e.err
}
func (e *Error) Commit() string {
return e.commitView on GitHub (pinned to 4f41128141)
Solutions
- Inspect the exact string in the panic's stack context (the run/if/env/with value act was rewriting) and add the missing '}}' to close the '${{' expression, e.g. '${{ github.ref' → '${{ github.ref }}'.
- If the '}}' the scanner saw was a literal one from shell text (awk '{{ ... }}', heredoc), quote or rewrite that literal text so it does not appear before an unclosed '${{', or close the '${{' properly so pairing is unambiguous.
- Check single-quote balance inside the expression: every ' literal must be closed before the '}}', e.g. "${{ contains(github.ref, 'refs/heads') }}".
- Run 'act' with the workflow after fixing and confirm the step proceeds; as a guard, grep your workflows for '${{' occurrences lacking a following '}}' on the same string (yq '.jobs[].steps[] | .run, .if, .env' style extraction helps).
- If you are embedding '${{' as literal text (not meant to be evaluated), you cannot leave it unclosed in the same string with a stray '}}' — restructure the value or generate the brace at runtime from shell (e.g. printing '${' twice) so act never sees '${{{' patterns.
Example fix
# before (workflow YAML)
steps:
- name: broken
run: |
awk '{{ print $1 }}' file.txt # stray }} from awk braces
echo "${{ env.BRANCH" # unclosed ${{ → panic("unclosed expression.")
# after
steps:
- name: fixed
run: |
awk '{{ print $1 }}' file.txt
echo "${{ env.BRANCH }}" # expression closed, pairs match Defensive patterns
Strategy: validation
Validate before calling
// Validate a workflow string before act evaluates it: every '${{' must be
// followed (after balanced single quotes) by a '}}'.
func hasUnclosedExpression(s string) bool {
if !strings.Contains(s, "${{") {
return false
}
pos := 0
for pos < len(s) {
start := strings.Index(s[pos:], "${{")
if start == -1 {
return false // no more expressions
}
pos += start + 3
inString := false
for pos < len(s) {
if inString {
q := strings.IndexByte(s[pos:], '\'')
if q == -1 {
return true // unclosed string inside expression
}
pos += q + 1
inString = false
continue
}
if strings.HasPrefix(s[pos:], "}}") {
pos += 2
break
}
if s[pos] == '\'' {
inString = true
pos++
continue
}
pos++
}
if inString || (pos >= len(s) && !strings.HasSuffix(s[:pos], "}}")) {
// loop ran off the end without finding }}
if !strings.Contains(s[pos:], "}}") {
return true
}
}
}
return false
}
// usage: reject the step value before running act
if hasUnclosedExpression(step.Run) {
return fmt.Errorf("step %q has an unclosed ${{ expression", step.Name)
} Type guard
// Simplest sufficient guard for this panic: the closer '}}' must appear
// AFTER the last '${{' opener (quote handling aside, this catches the
// scanner-panic shape where the only '}}' precedes the '${{').
func expressionLooksClosed(s string) bool {
lastOpen := strings.LastIndex(s, "${{")
if lastOpen == -1 {
return true
}
return strings.Contains(s[lastOpen:], "}}")
} Try / catch
// rewriteSubExpression panics instead of returning an error, so Go callers
// who feed strings into act's evaluation path (EvalBool/EvalString/evaluators)
// can only contain it with recover:
func safeEvalBool(ctx context.Context, ev ExpressionEvaluator, expr string, d exprparser.DefaultStatusCheck) (b bool, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("invalid expression %q: %v", expr, r)
}
}()
return EvalBool(ctx, ev, expr, d)
}
// Prefer pre-validating the string (see validationCode) over relying on recover;
// treat a panic here as a workflow-authoring bug to fix in the YAML, not a
// runtime condition to retry. Prevention
- Lint workflows for balanced delimiters: every '${{' needs a matching '}}' in the same string value; add this check to CI (actionlint also catches malformed expressions).
- When embedding shell text that legitimately contains '}}' (awk, brace expansion), make sure any '${{' in the same string is fully closed before it or restructure the script so the two never coexist in one value.
- Keep single quotes inside expressions balanced ('${{ contains(x, 'y') }}', never a dangling quote).
- Never paste '${{' into run/if/env/with as literal text expecting it to pass through; act always tries to interpolate it.
- If you maintain code that passes user input into act's expression evaluation, run the closer-after-opener guard (typeGuard) before evaluation and reject with a clear error instead of letting act panic.
When it happens
Trigger: Specifically: (1) the input contains a '${{' whose matching '}}' appears BEFORE it, e.g. 'echo }} ${{ github.ref' — the cheap Contains pre-check at line 417 passes because '}}' exists somewhere, but after the '${{' there is no closer and no quote → panic. (2) The expression opens a single-quoted literal but never closes the expression, e.g. "${{ contains(github.ref, 'refs/" — after the string literal is consumed, no '}}' and no further quote remain → panic. Note a plain '${{ failure()' with NO '}}' anywhere does NOT panic here; it slips past the pre-check and fails later in the exprparser with a normal error.
Common situations: Hand-edited workflow YAML in .github/workflows/ with a missing '}}' (often after copy-paste or a brace-eating template tool); shell steps whose script body contains a literal '}}' (awk blocks, heredocs, 'cp foo{,}}'-style text) plus a '{{' that never got closed; composite action inputs or env values interpolating '${{' inside single quotes that are themselves unbalanced; regressions when migrating workflows where a linter stripped one brace of a pair; any place act evaluates user-supplied strings (run, if, env, with, name) through the interpolation path.
Related errors
- unclosed string.
- failed to insert node %v into mapping %v unexpected type %v
- invalid Step %v: missing run or uses key
- Invalid run/uses syntax for job:%s step:%+v
- expressions are not allowed here
AI-assisted analysis of nektos/act@4f41128141 (2026-08-15).
Data as JSON: /api/errors/6ddce567d0520985.
Report an issue: GitHub.