Back to Dyad

Git Workflow

rules/git-workflow.md

0.44.012.6 KB
Original Source

Git Workflow

When pushing changes and creating PRs:

  1. If the branch already has an associated PR, push to whichever remote the branch is tracking.
  2. If the branch hasn't been pushed before, default to pushing to origin (the fork wwwillchen/dyad), then create a PR from the fork to the upstream repo (dyad-sh/dyad).
  3. If you cannot push to the fork due to permissions, push directly to upstream (dyad-sh/dyad) as a last resort.

Bot account push permissions: The keppo-bot account does NOT have write access to upstream (dyad-sh/dyad). If a branch tracks upstream (e.g., upstream/claude/...), pushing will fail with a permission error. In this case, push to origin (the bot's fork at keppo-bot/dyad) instead:

bash
git push --force-with-lease -u origin HEAD

This overrides the branch's tracking remote. Always check which remote origin points to (git remote -v) — for bot workspaces, origin is typically the bot's fork, not the upstream repo.

gh pr create branch detection

If gh pr create says you must first push the current branch to a remote even though git push -u succeeded, create the PR with an explicit head ref:

bash
gh pr create --head <owner>:<branch> ...

This can happen when remotes are configured in a non-fork layout and gh fails to infer the branch mapping.

GH auth allowlist and git push

If gh auth status succeeds but git push fails with Repo <owner>/<repo> is not allowlisted followed by fatal: could not read Username for 'https://github.com/...': Device not configured, run gh auth setup-git first and then push to an allowlisted remote. In some bot workspaces, fork remotes are not allowlisted even when upstream is, so retry the push against upstream if project policy permits it.

Empty branches cannot produce PRs

Before creating a PR for a freshly pushed branch, check whether it is actually ahead of the base branch:

bash
git rev-list --left-right --count upstream/main...HEAD

If this returns 0 0, the branch has no commits ahead of upstream/main. GitHub cannot open a PR for an empty branch, so do not fabricate an empty commit just to satisfy gh pr create; report the branch as pushed but PR-blocked instead.

gh pr create fork-collab permission error

If gh pr create from a fork fails with GraphQL: Fork collab Fork collab can't be granted by someone without permission (createPullRequest), add --no-maintainer-edit. gh defaults to enabling maintainer edits, which requires a permission the fork account does not have for the upstream repo.

bash
gh pr create --repo dyad-sh/dyad --head <owner>:<branch> --no-maintainer-edit --title "..." --body "..."

gh pr create body quoting

When passing a PR body inline via gh pr create --body "...", unescaped backticks are evaluated by zsh before gh runs. Avoid backticks in inline bodies, or use a body file / heredoc so literal code identifiers do not turn into command not found errors.

Skipping automated review

Add #skip-bugbot to the PR description for trivial PRs that won't affect end-users, such as:

  • Claude settings, commands, or agent configuration
  • Linting or test setup changes
  • Documentation-only changes
  • CI/build configuration updates

Cross-repo PR workflows (forks)

When running GitHub Actions with pull_request_target on cross-repo PRs (from forks):

  • The checkout action sets origin to the fork (head repo), not the base repo
  • To rebase onto the base repo's main, you must add an upstream remote: git remote add upstream https://github.com/<base-repo>.git
  • Remote setup for cross-repo PRs: origin → fork (push here), upstream → base repo (rebase from here)
  • The GITHUB_TOKEN can push to the fork if the PR author enabled "Allow edits from maintainers"
  • claude-code-action overwrites origin's fetch URL to point to the base repo (using GITHUB_REPOSITORY). Any workflow that needs to push to the fork must set pushurl separately via git remote set-url --push origin <fork-url>, because git uses pushurl over url when both are configured. See pr-review-responder.yml and claude-rebase.yml for examples.

GITHUB_TOKEN and workflow chaining

Actions performed using the default GITHUB_TOKEN (including labels added by github-actions[bot] via actions/github-script) do not trigger pull_request_target or other workflow events. This is a GitHub limitation to prevent infinite loops. If one workflow adds a label that should trigger another workflow (e.g., label-rebase-prs.yml adds cc:rebase to trigger claude-rebase.yml), the label-adding step must use a PAT or GitHub App token (e.g., PR_RW_GITHUB_TOKEN) instead of GITHUB_TOKEN.

Bash case allowlists in workflows

When matching GitHub bot logins in Bash case patterns, escape literal square brackets. For example, keppo-bot[bot] is parsed as a character class and does not match the login; use keppo-bot\[bot\].

GitHub API calls with special characters

When using gh api to post comments or replies containing backticks, $(), or other shell metacharacters, the security hook will block the command. Instead of passing the body inline with -f body="...", write a JSON file and use --input:

bash
# Write JSON body to a file (use the Write tool, not echo/cat)
# File: .claude/tmp/reply_body.json
# {"body": "Your comment with `backticks` and special chars"}

gh api repos/dyad-sh/dyad/pulls/123/comments/456/replies --input .claude/tmp/reply_body.json

Similarly for GraphQL mutations, write the full query + variables as JSON and use --input:

bash
# {"query": "mutation($threadId: ID!) { ... }", "variables": {"threadId": "PRRT_abc123"}}
gh api graphql --input .claude/tmp/resolve_thread.json

Adding labels to PRs

gh pr edit --add-label can fail for two reasons:

  1. GraphQL "Projects (classic)" deprecation error on repos that had classic projects. Use the REST API instead:
bash
gh api repos/dyad-sh/dyad/issues/{PR_NUMBER}/labels -f "labels[]=label-name"
  1. Bot account permission errors: The keppo-bot account (and similar bot/fork accounts) may not have permission to add labels on the upstream repo (dyad-sh/dyad). Both gh pr edit --add-label and the REST API will fail with 403/permission errors. In this case, skip label addition and note it in the PR summary rather than failing the workflow. Labels can be added later by a maintainer with appropriate permissions.

CI file access (claude-code-action)

In CI, claude-code-action restricts file access to the repo working directory (e.g., /home/runner/work/dyad/dyad). Skills that save intermediate files (like PR diffs) must use ./filename (current working directory), never /tmp/. Using /tmp/ causes errors like: cat in '/tmp/pr_*_diff.patch' was blocked. For security, Claude Code may only concatenate files from the allowed working directories.

Force-pushing after rebase with split-remote origin

When origin has separate fetch and push URLs (e.g., fetch → dyad-sh/dyad, push → keppo-bot/dyad), git push --force-with-lease fails with "stale info" after a rebase because the local tracking ref was refreshed from the fetch URL but does not reflect the push URL's state. In this specific split-remote configuration, use git push --force origin HEAD:

bash
git push --force origin HEAD

Note: Plain --force can overwrite others' remote commits. Only use this in the split-remote scenario described above, where --force-with-lease cannot work. In normal setups, always prefer --force-with-lease.

Repo allowlist push fallback

In some Codex shells, pushing to fork remotes can fail immediately with Repo <owner>/<repo> is not allowlisted even when gh auth status shows a valid token. If both fork remotes are blocked this way but upstream is allowed, push the branch directly to upstream (for example git push --force-with-lease upstream HEAD:<branch>) and then repoint the local branch to track upstream/<branch> so later status and push commands reflect the real remote.

Rebase workflow and conflict resolution

Handling unstaged changes during rebase

If git rebase fails with "You have unstaged changes" (common with spurious package-lock.json changes):

bash
git stash push -m "Stashing changes before rebase"
git rebase upstream/main
git stash pop

The stashed changes will be automatically merged back after the rebase completes.

Conflict resolution tips

  • Modify/delete conflicts: When a rebase shows CONFLICT (modify/delete): <file> deleted in <commit> and modified in HEAD, use git rm <file> (not git add) to resolve by confirming the deletion. Use git add <file> only when you want to keep the modified version instead.
  • Non-interactive rebase continue: After resolving conflicts, prefer GIT_EDITOR=true git rebase --continue in agent shells. Plain git rebase --continue can open vi for COMMIT_EDITMSG and fail with error: vi died of signal 15 when stdin is not interactive.
  • Before rebasing: If npm install modified package-lock.json (common in CI/local), discard changes with git restore package-lock.json to avoid "unstaged changes" errors
  • When resolving import conflicts (e.g., <<<<<<< HEAD with different imports), keep both imports if both are valid and needed by the component
  • When resolving conflicts in i18n-related commits, watch for duplicate constant definitions that conflict with imports from @/lib/schemas (e.g., DEFAULT_ZOOM_LEVEL)
  • If both sides of a conflict have valid imports/hooks, keep both and remove any duplicate constant redefinitions
  • When rebasing documentation/table conflicts (e.g., workflow README tables), prefer keeping both additions from HEAD and upstream - merge new rows/content from both branches rather than choosing one side
  • Complementary additions: When both sides added new sections at the end of a file (e.g., both added different documentation tips), keep both sections rather than choosing one — they're not truly conflicting, just different additions
  • Preserve variable declarations used in common code: When one side of a conflict declares a variable (e.g., const iframe = po.previewPanel.getPreviewIframeElement()) that is referenced in non-conflicting code between or after conflict markers, keep the declaration even when adopting the other side's verification approach — the variable is needed regardless of which style you choose
  • React component wrapper conflicts: When rebasing UI changes that conflict on wrapper div classes (e.g., flex items-start space-x-2 vs flex items-end gap-1), keep the newer styling from the incoming commit but preserve any functional components (like dialogs or modals) that exist in HEAD but not in the incoming change
  • Refactoring conflicts: When incoming commits refactor code (e.g., extracting inline logic into helper functions), and HEAD has new features in the same area, integrate HEAD's features into the new structure. Example: if incoming code moves streaming logic to runSingleStreamPass() and HEAD adds mid-turn compaction to the inline code, add compaction support to the new function rather than keeping the old inline version

Rebasing with uncommitted changes

If you need to rebase but have uncommitted changes (e.g., package-lock.json from startup npm install):

  1. Stash changes: git stash push -m "Stash changes before rebase"
  2. Rebase: git rebase upstream/main (resolve conflicts if needed)
  3. After rebase completes, review stashed changes: git stash show -p
  4. If stashed changes are spurious (e.g., package-lock.json peer markers when package.json conflicts were resolved during rebase), drop the stash: git stash drop
  5. Otherwise, pop stash: git stash pop and discard spurious changes: git restore package-lock.json (if package.json unchanged)

This prevents rebase conflicts from uncommitted changes while preserving any work in progress.

Resolving documentation rebase conflicts

When rebasing a PR branch that conflicts with upstream documentation changes (e.g., AGENTS.md):

  • If upstream has reorganized content (e.g., moved sections to separate rules/*.md files), keep upstream's version
  • Discard the PR's inline content that conflicts with the new organization
  • The PR's documentation changes may need to be re-applied to the new file locations after the rebase

Resolving package.json engine conflicts

When rebasing causes conflicts in the engines field of package.json (e.g., node version requirements), accept the incoming change from upstream/main to maintain consistency with the base branch requirements. The same resolution should be applied to the corresponding section in package-lock.json.