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.commit

View on GitHub (pinned to 4f41128141)

Solutions

  1. 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 }}'.
  2. 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.
  3. Check single-quote balance inside the expression: every ' literal must be closed before the '}}', e.g. "${{ contains(github.ref, 'refs/heads') }}".
  4. 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).
  5. 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

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


AI-assisted analysis of nektos/act@4f41128141 (2026-08-15). Data as JSON: /api/errors/6ddce567d0520985. Report an issue: GitHub.