Check-Before-Act¶
The most intuitive idempotency pattern: look before you leap.
The Pattern¶
Check if the desired state exists. If yes, skip. If no, act.
flowchart LR
A[Start] --> B{State exists?}
B -->|Yes| C[Skip]
B -->|No| D[Create]
C --> E[Done]
D --> E
%% Ghostty Hardcore Theme
style A fill:#5e7175,color:#f8f8f3
style B fill:#fd971e,color:#1b1d1e
style C fill:#a7e22e,color:#1b1d1e
style D fill:#65d9ef,color:#1b1d1e
style E fill:#5e7175,color:#f8f8f3
When to Use¶
Good Fit
- Creating resources that shouldn't be duplicated (PRs, branches, issues)
- Operations where existence is easy to verify
- Workflows where partial completion is visible
- APIs that don't support upsert semantics
Poor Fit
- High-concurrency scenarios (race conditions)
- Operations where state changes rapidly
- When atomic upsert is available (use that instead)
Examples¶
Branch Management¶
# Check if branch exists on remote
if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then
# Branch exists: reset to remote state
git checkout -B "$BRANCH" "origin/$BRANCH"
echo "Checked out existing branch"
else
# Branch doesn't exist: create it
git checkout -b "$BRANCH"
echo "Created new branch"
fi
The -B flag is critical here. It force-resets an existing local branch to match the remote, handling diverged state from previous failed runs.
Pull Request Creation¶
# Check if PR already exists for this branch
EXISTING=$(gh pr list --head "$BRANCH" --base "$DEFAULT_BRANCH" --json number --jq 'length')
if [ "$EXISTING" -eq 0 ]; then
gh pr create --title "Automated update" --body "From central repo"
echo "Created new PR"
else
echo "PR already exists, skipping creation"
fi
GitHub Issue Creation¶
# Check by title (approximate matching)
EXISTING=$(gh issue list --search "in:title $ISSUE_TITLE" --json number --jq 'length')
if [ "$EXISTING" -eq 0 ]; then
gh issue create --title "$ISSUE_TITLE" --body "$ISSUE_BODY"
else
echo "Similar issue already exists"
fi
File Creation¶
# Only create if file doesn't exist
if [ ! -f "$TARGET_FILE" ]; then
cp "$SOURCE_FILE" "$TARGET_FILE"
echo "Created $TARGET_FILE"
else
echo "$TARGET_FILE already exists"
fi
Directory Creation¶
Built-in Idempotency
mkdir -p is a built-in example of check-before-act. It succeeds whether the directory exists or not. Look for similar flags in other tools (cp -n, ln -sf).
GitHub Actions Examples¶
Conditional Step Execution¶
- name: Check if release exists
id: check
run: |
if gh release view "${{ env.VERSION }}" &>/dev/null; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi
- name: Create release
if: steps.check.outputs.exists == 'false'
run: gh release create "${{ env.VERSION }}" --generate-notes
PR Creation with Existence Check¶
- name: Create PR if not exists
run: |
if ! gh pr list --head "${{ env.BRANCH }}" --json number | jq -e 'length > 0'; then
gh pr create \
--title "Automated update" \
--body "Weekly sync from upstream"
fi
Edge Cases and Gotchas¶
Race Conditions¶
The TOCTOU Problem
Time-of-check to time-of-use (TOCTOU) is the fundamental weakness of check-before-act. The state can change between checking and acting.
The time between "check" and "act" creates a window for race conditions:
sequenceDiagram
%% Ghostty Hardcore Theme
participant W1 as Worker 1
participant S as Shared State
participant W2 as Worker 2
W1->>S: Check: exists?
S-->>W1: No
W2->>S: Check: exists?
S-->>W2: No
W1->>S: Create resource
W2->>S: Create resource
Note over S: Duplicate created!
rect rgb(27, 29, 30)
Note over W1,W2: Both workers pass the check<br/>before either acts
end
# Thread 1 # Thread 2
if [ ! -f lock ]; then if [ ! -f lock ]; then
# Both threads pass check
touch lock touch lock
do_work do_work # Duplicate!
fi fi
Mitigation: Use atomic operations where possible, or accept that occasional duplicates may occur.
Stale Checks¶
Remote state can change between check and act:
# Check passes: PR doesn't exist
EXISTING=$(gh pr list --head "$BRANCH" --json number --jq 'length')
# Meanwhile, another process creates the PR...
# Act fails: PR now exists
gh pr create ... # Error: PR already exists
Mitigation: Handle the "already exists" error gracefully:
Incorrect Check Conditions¶
The most common bug: checking the wrong thing.
# Wrong: checks local branch, not remote
if git branch --list "$BRANCH" | grep -q "$BRANCH"; then
# Right: checks remote branch
if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then
Partial State¶
What if the previous run created the resource but didn't configure it?
# PR exists but labels weren't added
if [ "$EXISTING" -eq 0 ]; then
gh pr create --title "Update" --label "automated"
else
# PR exists, but does it have the label?
# This check-before-act missed the incomplete state
fi
Mitigation: Either make the check more comprehensive, or make configuration idempotent too:
# Always ensure labels (idempotent operation)
gh pr edit "$PR_NUMBER" --add-label "automated" 2>/dev/null || true
Anti-Patterns¶
Silent Skipping Without Logging¶
# Bad: no visibility into what happened
if [ -f "$FILE" ]; then
: # Do nothing, silently
else
create_file
fi
# Good: explicit logging
if [ -f "$FILE" ]; then
echo "Skipping: $FILE already exists"
else
create_file
echo "Created: $FILE"
fi
Checking the Wrong Layer¶
# Bad: checks local state, acts on remote
if [ -d ".git" ]; then
git push origin main # Remote might not have this branch!
fi
Over-Broad Checks¶
# Bad: any PR counts as "exists"
if gh pr list --json number | jq -e 'length > 0'; then
echo "PR exists"
fi
# Good: check for specific branch
if gh pr list --head "$BRANCH" --json number | jq -e 'length > 0'; then
echo "PR for $BRANCH exists"
fi
Comparison with Other Patterns¶
| Aspect | Check-Before-Act | Upsert | Force Overwrite |
|---|---|---|---|
| Race condition safe | No | Yes | Yes |
| Requires API support | No | Yes | Depends |
| Visibility into state | High | Low | Low |
| Complexity | Low | Low | Low |
Summary¶
Check-Before-Act is the workhorse of idempotency patterns. It's explicit, debuggable, and works everywhere.
Key Takeaways
- Check the right state: remote vs local, specific vs broad
- Log your decisions: visibility beats silent skipping
- Handle race conditions: either accept them or use atomic alternatives
- Consider partial state: a resource existing isn't always enough