BLOG/DEVELOPER DAILY
THEME
DEVELOPER DAILY

The git function: clone, commit, tag without shelling out

Cloning and reading a repo in a workflow usually means parsing git stdout with awk. func: git does the common operations as first-class steps and hands back structured outputs instead.

Jun 27, 2026 OrchStep Team 6 minROLE: Backend DeveloperSCALE: Any

Every workflow that touches a repo starts the same way: git clone, then a little forest of git tag --list | sort -V | tail -1 and git rev-parse HEAD piped through awk, each parsing human-readable output that was never meant to be parsed. It works until a tag has an unusual name or git changes its phrasing by a hair.

func: git does the common operations as first-class steps and hands back structured outputsresult.commit_sha, result.tags, result.commit_info.sha — so you read a field instead of scraping stdout. It runs with the workflow's merged environment, so tokens from env:, dotenv:, or secrets: are already available.

Clone, list tags, read HEAD — no parsing

orchstep.yml
name: repo-ops
tasks:
  inspect:
    steps:
      - name: get
        func: git
        do: "clone https://github.com/org/repo.git work/repo"
      - name: tags
        func: git
        args:
          operation: list_tags
          repo_dir: work/repo
          pattern: "v*"
      - name: head
        func: git
        do: "commit-info work/repo"
      - name: show
        func: shell
        do: |
          echo "latest tag is {{ last (sortAlpha steps.tags.result.tags) }}"
          echo "head sha is {{ steps.head.result.commit_info.sha }}"
          echo "clone sha is {{ steps.get.result.commit_sha }}"

Run it against a repo and you get clean values, not stdout to slice:

  Step: get
Successfully cloned repo to work/repo
  [ok] get
  Step: tags
  [ok] tags
  Step: head
  [ok] head
  Step: show
latest tag is v1.1.0
head sha is 0664eb82913afe1624b338ac2c2713cfd39e2cdb
clone sha is 0664eb82913afe1624b338ac2c2713cfd39e2cdb
  [ok] show
Result: success

Note the access path: git steps nest their structured data under result, so it's steps.tags.result.tags and steps.head.result.commit_info.sha. Read those rather than regex-parsing — that's the whole point of the function.

Two forms: shortcut or args

Quick operations take a do: shortcut; anything with options uses args:. They're equivalent:

# do: shortcut
- { name: get, func: git, do: "clone https://github.com/org/repo.git work/repo" }

# args: form — same clone, with options
- name: get
  func: git
  args:
    operation: clone
    url: https://github.com/org/repo.git
    dest: work/repo
    branch: main
    depth: 1

The operations cover the everyday set: clone, checkout, fetch, list_tags, list_branches, commit_info, and push (which stages, commits, and pushes in one step).

Auth that stays out of your logs

Pass auth: with a type, and use {{ secrets.X }} for the token so it's masked in output and kept out of the run history:

- name: clone
  func: git
  args:
    operation: clone
    url: https://github.com/org/private-repo.git
    dest: work/repo
    auth:
      type: token
      token: "{{ secrets.GITHUB_TOKEN }}"
  # result.commit_sha, result.dest

SSH works the same way with type: ssh and a key_path.

GitOps: commit and push state back

The push operation is the one that earns its keep — record what you deployed in a state repo, structured output and all:

- name: track_deploy
  func: git
  args:
    operation: push
    repo_dir: state-repo
    branch: "state/{{ vars.target }}"
    message: "Deploy {{ vars.version }} to {{ vars.target }}"
    files: ["deployed/{{ vars.target }}.yml"]
    create_branch: true
    author_name: "OrchStep Bot"
    author_email: "bot@orchstep.dev"
    auth:
      type: token
      token: "{{ secrets.GITHUB_TOKEN }}"
  # result.pushed, result.files_changed, result.commit_sha

Because it's a normal step, you can wrap a flaky network clone in a retry that keys off the structured output:

- name: clone
  func: git
  do: "clone https://github.com/org/repo.git work/repo"
  retry:
    max_attempts: 3
    interval: 2s
    backoff_rate: 1.5
    when: '!result.success'

What you gained

Hand-rolled git in shellfunc: git
tag --list | sort -V | tail -1steps.tags.result.tags, already a list
rev-parse HEAD then trimresult.commit_info.sha
token in the command line / logsauth.token: "{{ secrets.GITHUB_TOKEN }}", masked
parse exit codes by handresult.success for retry.when
clone defaults you re-typeshallow + single-branch by default

Where this is not the answer

func: git deliberately covers the common operations, not every flag. Rebases, cherry-picks, submodule gymnastics, an obscure option — those still belong in func: shell running git directly, and that's a supported, normal escape hatch:

- name: rebase
  func: shell
  do: "cd work/repo && git rebase origin/main"

For reproducibility, check out a tag or SHA (ref: v1.2.3) rather than a moving branch, and keep clone shallow unless you actually need history.

Where to go next

  • git — every operation, parameter, and return field
  • shell — the escape hatch for anything not covered
  • Error Handling — retry, catch, finally

Find the git clone at the top of your CI script and the awk two lines below it. That pair is exactly what func: git replaces.

#GIT#AUTOMATION#OUTPUTS#GITOPS
Try it in two minutes — one binary, no signup.
curl -fsSL https://orchstep.dev/install.sh | sh

RELATED — DEVELOPER DAILY