Skip to content

Conversation

@Yermanaco
Copy link

@Yermanaco Yermanaco commented Nov 13, 2025

Use case: This change fixes an idempotency problem in workflows that automate version bumps and commits. Previously, after the action created a branch and later runs produced new commits, the action did nothing when the branch already existed and the branch could remain behind the latest commits. With this PR the action attempts a fast‑forward update to move the existing branch to the requested SHA when possible, so repeated runs will advance the branch to include new commits.

Note: non‑fast‑forward updates are rejected by the API (no force), and branch protection or insufficient token scopes can still prevent the update

@Yermanaco Yermanaco marked this pull request as ready for review November 13, 2025 16:05
@Yermanaco
Copy link
Author

Yermanaco commented Nov 13, 2025

@peterjgrainger can you review it please? thx!

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR enhances the branch creation action to support idempotent workflows by implementing fast-forward updates for existing branches. When a branch already exists, the action now updates it to the requested SHA instead of failing, allowing repeated workflow runs to advance the branch with new commits.

Key Changes:

  • Switched from repos.getBranch to git.getRef for checking branch existence, followed by git.updateRef for fast-forward updates when the branch exists
  • Added comprehensive debug logging for troubleshooting branch operations
  • Updated documentation and action metadata to reflect the new update behavior

Reviewed Changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/create-branch.ts Implements the new branch update logic using getRef/updateRef APIs with fallback to createRef for new branches
dist/index.js Compiled JavaScript output reflecting the TypeScript source changes
action.yml Updated input and output descriptions to clarify the update behavior
tests/create-branch.test.ts Added test case for the new fast-forward update scenario and updated existing test mocks
README.md Enhanced documentation explaining the fast-forward update behavior and corrected spelling errors

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@peterjgrainger
Copy link
Owner

@Yermanaco. Thanks for the contribution. I'll have to take a look to make sure it doesn't break existing functionality, but I think it's a good idea!

@peterjgrainger
Copy link
Owner

@Yermanaco I've been through this again and it changes the functionality instead of creating a branch it now creates or updates which might be confusing as the name is create branch.

Could you show me a workflow where you get the idempotency problem please?

@Yermanaco
Copy link
Author

Yermanaco commented Nov 20, 2025

@Yermanaco I've been through this again and it changes the functionality instead of creating a branch it now creates or updates which might be confusing as the name is create branch.

Could you show me a workflow where you get the idempotency problem please?

Thanks for the feedback! I cannot share any workflow due to is an internal private workflow but let me clarify the use case.

The Problem

Current behavior blocks idempotent workflows:

  • If branch exists → action does nothing
  • Problem: The branch SHA becomes stale if the source ref was updated

Real scenario in our CI/CD:

We have workflows that:

  1. Bump version tags/commits in release-XY branch
  2. Create certification branches from those commits: ver-BrandA-24.29, ver-BrandB-24.29, etc.
  3. Run tests, deployments, and other validations

What happens when a workflow fails mid-execution:

- name: Bump version
  run: npm version patch  # Creates new commit + tag on release-XY

- name: Create cert branch
  uses: peterjgrainger/[email protected]
  with:
    branch: ver-BrandA-24.29
    sha: ${{ steps.get-sha.outputs.sha }}  # Points to new bump commit

- name: Deploy  # ❌ This step fails

# Re-run the workflow:
# - release-XY has the new bumped commit
# - But ver-BrandA-24.29 still points to the OLD commit (before bump)
# - Action skips because branch exists → STALE SHA ❌

Another use case

  1. release-XY gets a hotfix or another bump
  2. We need cert branches to point to the latest commit
  3. Currently, we must manually delete branches before re-running de workflow again → not idempotent
  4. This PR makes the action idempotent: it ensures the branch always points to the specified SHA, whether it exists or not

Behavior after this change:

  • Branch doesn't exist → create it ✅
  • Branch exists, same SHA → do nothing
  • Branch exists, different SHA → update ref to new SHA ✅
  • Backward Compatibility

This is 100% backward compatible:

  • Existing workflows that create branches once → still work (no change)
  • Workflows that re-run or need to update branches → now work
  • FMPOV the sha input already implies "make the branch point here", so updating the ref when it exists is the expected behavior.

Name Still Makes Sense

The action "creates" the branch state: ensures it exists at the given SHA. Similar to how actions/checkout creates/updates the local branch, or Terraform "creates" resources (even if they exist, it ensures the desired state).

If you prefer, I can:

  • Add an output updated: true/false to distinguish create vs. update
  • Update README to document better the idempotent behavior
  • (Optional) Add a flag like allow_update: true (though idempotency should be default IMO)

@Yermanaco
Copy link
Author

@Yermanaco I've been through this again and it changes the functionality instead of creating a branch it now creates or updates which might be confusing as the name is create branch.
Could you show me a workflow where you get the idempotency problem please?

Thanks for the feedback! I cannot share any workflow due to is an internal private workflow but let me clarify the use case.

The Problem

Current behavior blocks idempotent workflows:

  • If branch exists → action does nothing
  • Problem: The branch SHA becomes stale if the source ref was updated

Real scenario in our CI/CD:

We have workflows that:

  1. Bump version tags/commits in release-XY branch
  2. Create certification branches from those commits: ver-BrandA-24.29, ver-BrandB-24.29, etc.
  3. Run tests, deployments, and other validations

What happens when a workflow fails mid-execution:

- name: Bump version
  run: npm version patch  # Creates new commit + tag on release-XY

- name: Create cert branch
  uses: peterjgrainger/[email protected]
  with:
    branch: ver-BrandA-24.29
    sha: ${{ steps.get-sha.outputs.sha }}  # Points to new bump commit

- name: Deploy  # ❌ This step fails

# Re-run the workflow:
# - release-XY has the new bumped commit
# - But ver-BrandA-24.29 still points to the OLD commit (before bump)
# - Action skips because branch exists → STALE SHA ❌

Another use case

  1. release-XY gets a hotfix or another bump
  2. We need cert branches to point to the latest commit
  3. Currently, we must manually delete branches before re-running de workflow again → not idempotent
  4. This PR makes the action idempotent: it ensures the branch always points to the specified SHA, whether it exists or not

Behavior after this change:

  • Branch doesn't exist → create it ✅
  • Branch exists, same SHA → do nothing
  • Branch exists, different SHA → update ref to new SHA ✅
  • Backward Compatibility

This is 100% backward compatible:

  • Existing workflows that create branches once → still work (no change)
  • Workflows that re-run or need to update branches → now work
  • FMPOV the sha input already implies "make the branch point here", so updating the ref when it exists is the expected behavior.

Name Still Makes Sense

The action "creates" the branch state: ensures it exists at the given SHA. Similar to how actions/checkout creates/updates the local branch, or Terraform "creates" resources (even if they exist, it ensures the desired state).

If you prefer, I can:

  • Add an output updated: true/false to distinguish create vs. update
  • Update README to document better the idempotent behavior
  • (Optional) Add a flag like allow_update: true (though idempotency should be default IMO)

@peterjgrainger WDYT? Does that sound good to you?

@peterjgrainger
Copy link
Owner

I agree this is an issue. I'm trying to figure out the best way to implement without it having unintended side effects. I'm thinking more of failing the action if a branch exists which would be clearer.

My biggest concern is someone making a mistake and fast-forwarding a branch they didn't intend to.

e.g.

  • Run action to create branch main
  • Branch is already created (obviously)
  • main branch is fast forwarded to the given SHA
  • chaos!

What are your thoughts on having a new action for updating a branch

e.g. From your example

- name: Create cert branch
  id: create-branch
  uses: peterjgrainger/[email protected]
  with:
    branch: ver-BrandA-24.29
    sha: ${{ steps.get-sha.outputs.sha }}  
    
 - name: Update branch if already created.
    if: {{ steps.create-branch.outputs.created == false }}
    uses: peterjgrainger/[email protected]
    with:
        branch: ver-BrandA-24.29
        sha: ${{ steps.get-sha.outputs.sha }}

Either that or put all the logic in the workflow rather than the action:

  • Check branch exists
  • If yes -> update
  • if no -> create

@Yermanaco
Copy link
Author

I agree this is an issue. I'm trying to figure out the best way to implement without it having unintended side effects. I'm thinking more of failing the action if a branch exists which would be clearer.

My biggest concern is someone making a mistake and fast-forwarding a branch they didn't intend to.

e.g.

  • Run action to create branch main
  • Branch is already created (obviously)
  • main branch is fast forwarded to the given SHA
  • chaos!

What are your thoughts on having a new action for updating a branch

e.g. From your example

- name: Create cert branch
  id: create-branch
  uses: peterjgrainger/[email protected]
  with:
    branch: ver-BrandA-24.29
    sha: ${{ steps.get-sha.outputs.sha }}  
    
 - name: Update branch if already created.
    if: {{ steps.create-branch.outputs.created == false }}
    uses: peterjgrainger/[email protected]
    with:
        branch: ver-BrandA-24.29
        sha: ${{ steps.get-sha.outputs.sha }}

Either that or put all the logic in the workflow rather than the action:

  • Check branch exists
  • If yes -> update
  • if no -> create

First of all, Happy new year! Thanks for considering this! I understand your concern about unintended updates, but I think creating a separate action adds unnecessary complexity.

Why a Separate Action Isn't Needed

The two-action workflow you proposed requires:

  • Maintaining two separate actions with similar code
  • Users need to know about both actions
  • More boilerplate and conditional logic in every workflow
  • Logic that should be internal to the action

Better Solution: Optional Flag

Add an allow_update input to control the behavior:

- name: Create or update cert branch
  uses: peterjgrainger/[email protected]
  with:
    branch: ver-BrandA-24.29
    sha: ${{ steps.get-sha.outputs.sha }}
    allow_update: true  # Optional flag

Safety Is Already Built-In

Your concern about accidentally updating main is valid, but these safeguards already exist:

  1. API-Level Protection: Non-fast-forward updates are rejected by the GitHub API (no force by default). If someone tries to update main to an older commit, the API returns 422 Update is not a fast forward.

  2. Branch Protection Rules: Protected branches can't be updated without proper permissions, preventing accidental updates.

  3. Token Scopes: Insufficient permissions will fail the update. Most workflows use GITHUB_TOKEN which respects repository settings.

  4. Explicit SHA Input: Users must explicitly provide the sha input to trigger an update, preventing accidental changes.

My Recommendation

I'd suggest making allow_update default to true because:

  • The sha input implies "make branch point here" (declarative/idempotent)
  • Fast-forward safety is built into the API
  • Branch protection rules provide additional safety
  • Most users expect idempotent behavior (like Terraform, Kubernetes, etc.)

But if you prefer conservative defaults, we can set allow_update: false by default and require explicit opt-in.

What I Can Do

I can update the PR to add the allow_update input. Which default would you prefer?

  • Option A (my preference): allow_update: true by default (idempotent, safe, simple)
  • Option B (more conservative): allow_update: false by default (requires explicit opt-in)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants