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.
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 outputs — result.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
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: successNote 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: 1The 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.destSSH 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_shaBecause 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 shell | func: git |
|---|---|
tag --list | sort -V | tail -1 | steps.tags.result.tags, already a list |
rev-parse HEAD then trim | result.commit_info.sha |
| token in the command line / logs | auth.token: "{{ secrets.GITHUB_TOKEN }}", masked |
| parse exit codes by hand | result.success for retry.when |
| clone defaults you re-type | shallow + 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.
curl -fsSL https://orchstep.dev/install.sh | sh