Skip to content

chore(deps): update actions/checkout action to v6.0.2#411

Merged
mschfh merged 1 commit intomasterfrom
renovate/actions-checkout-6-x
Apr 18, 2026
Merged

chore(deps): update actions/checkout action to v6.0.2#411
mschfh merged 1 commit intomasterfrom
renovate/actions-checkout-6-x

Conversation

@renovate
Copy link
Copy Markdown
Contributor

@renovate renovate Bot commented Apr 17, 2026

This PR contains the following updates:

Package Type Update Change
actions/checkout action patch v6.0.0v6.0.2

Release Notes

actions/checkout (actions/checkout)

v6.0.2

Compare Source

v6.0.1

Compare Source


Configuration

📅 Schedule: (UTC)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@github-actions
Copy link
Copy Markdown

[puLL-Merge] - actions/checkout@v6.0.0..v6.0.2

Diff
diff --git .github/workflows/check-dist.yml .github/workflows/check-dist.yml
index db3e37f2b..c7d49620f 100644
--- .github/workflows/check-dist.yml
+++ .github/workflows/check-dist.yml
@@ -22,7 +22,7 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - uses: actions/checkout@v4.1.6
+      - uses: actions/checkout@v6
 
       - name: Set Node.js 24.x
         uses: actions/setup-node@v4
diff --git .github/workflows/codeql-analysis.yml .github/workflows/codeql-analysis.yml
index 778d474d8..377fae951 100644
--- .github/workflows/codeql-analysis.yml
+++ .github/workflows/codeql-analysis.yml
@@ -39,7 +39,7 @@ jobs:
 
     steps:
     - name: Checkout repository
-      uses: actions/checkout@v4.1.6
+      uses: actions/checkout@v6
 
     - name: Initialize CodeQL
       uses: github/codeql-action/init@v3
diff --git .github/workflows/licensed.yml .github/workflows/licensed.yml
index 1f71aa749..36e70e2c1 100644
--- .github/workflows/licensed.yml
+++ .github/workflows/licensed.yml
@@ -9,6 +9,6 @@ jobs:
     runs-on: ubuntu-latest
     name: Check licenses
     steps:
-      - uses: actions/checkout@v4.1.6
+      - uses: actions/checkout@v6
       - run: npm ci
       - run: npm run licensed-check
\ No newline at end of file
diff --git .github/workflows/publish-immutable-actions.yml .github/workflows/publish-immutable-actions.yml
index 87c020728..44d571ba9 100644
--- .github/workflows/publish-immutable-actions.yml
+++ .github/workflows/publish-immutable-actions.yml
@@ -14,7 +14,7 @@ jobs:
 
     steps:
       - name: Checking out
-        uses: actions/checkout@v4
+        uses: actions/checkout@v6
       - name: Publish
         id: publish
         uses: actions/publish-immutable-action@0.0.3
diff --git .github/workflows/test.yml .github/workflows/test.yml
index 7c47d7b6a..0383c88d7 100644
--- .github/workflows/test.yml
+++ .github/workflows/test.yml
@@ -19,7 +19,7 @@ jobs:
       - uses: actions/setup-node@v4
         with:
           node-version: 24.x
-      - uses: actions/checkout@v4.1.6
+      - uses: actions/checkout@v6
       - run: npm ci
       - run: npm run build
       - run: npm run format-check
@@ -37,7 +37,7 @@ jobs:
     steps:
       # Clone this repo
       - name: Checkout
-        uses: actions/checkout@v4.1.6
+        uses: actions/checkout@v6
 
       # Basic checkout
       - name: Checkout basic
@@ -87,6 +87,17 @@ jobs:
       - name: Verify fetch filter
         run: __test__/verify-fetch-filter.sh
 
+      # Fetch tags
+      - name: Checkout with fetch-tags
+        uses: ./
+        with:
+          ref: test-data/v2/basic
+          path: fetch-tags-test
+          fetch-tags: true
+      - name: Verify fetch-tags
+        shell: bash
+        run: __test__/verify-fetch-tags.sh
+
       # Sparse checkout
       - name: Sparse checkout
         uses: ./
@@ -165,6 +176,22 @@ jobs:
       - name: Verify submodules recursive
         run: __test__/verify-submodules-recursive.sh
 
+      # Worktree credentials
+      - name: Checkout for worktree test
+        uses: ./
+        with:
+          path: worktree-test
+      - name: Verify worktree credentials
+        shell: bash
+        run: __test__/verify-worktree.sh worktree-test worktree-branch
+
+      # Worktree credentials in container step
+      - name: Verify worktree credentials in container step
+        if: runner.os == 'Linux'
+        uses: docker://bitnami/git:latest
+        with:
+          args: bash __test__/verify-worktree.sh worktree-test container-worktree-branch
+
       # Basic checkout using REST API
       - name: Remove basic
         if: runner.os != 'windows'
@@ -202,7 +229,7 @@ jobs:
     steps:
       # Clone this repo
       - name: Checkout
-        uses: actions/checkout@v4.1.6
+        uses: actions/checkout@v6
 
       # Basic checkout using git
       - name: Checkout basic
@@ -234,7 +261,7 @@ jobs:
     steps:
       # Clone this repo
       - name: Checkout
-        uses: actions/checkout@v4.1.6
+        uses: actions/checkout@v6
 
       # Basic checkout using git
       - name: Checkout basic
@@ -264,7 +291,7 @@ jobs:
     steps:
       # Clone this repo
       - name: Checkout
-        uses: actions/checkout@v4.1.6
+        uses: actions/checkout@v6
         with:
           path: localClone
 
@@ -291,8 +318,8 @@ jobs:
           git fetch --no-tags --depth=1 origin +refs/heads/main:refs/remotes/origin/main
 
       # needed to make checkout post cleanup succeed
-      - name: Fix Checkout v4
-        uses: actions/checkout@v4.1.6
+      - name: Fix Checkout v6
+        uses: actions/checkout@v6
         with:
           path: localClone
 
@@ -301,7 +328,7 @@ jobs:
     steps:
       # Clone this repo
       - name: Checkout
-        uses: actions/checkout@v4.1.6
+        uses: actions/checkout@v6
         with:
           path: actions-checkout
 
diff --git .github/workflows/update-main-version.yml .github/workflows/update-main-version.yml
index 643b954e4..b3b23fe4e 100644
--- .github/workflows/update-main-version.yml
+++ .github/workflows/update-main-version.yml
@@ -23,7 +23,7 @@ jobs:
     # Note this update workflow can also be used as a rollback tool.
     # For that reason, it's best to pin `actions/checkout` to a known, stable version
     # (typically, about two releases back).
-    - uses: actions/checkout@v4.1.6
+    - uses: actions/checkout@v6
       with:
         fetch-depth: 0
     - name: Git config
diff --git .github/workflows/update-test-ubuntu-git.yml .github/workflows/update-test-ubuntu-git.yml
index 5c252b98d..10e4dac93 100644
--- .github/workflows/update-test-ubuntu-git.yml
+++ .github/workflows/update-test-ubuntu-git.yml
@@ -26,7 +26,7 @@ jobs:
  
     steps:
       - name: Checkout repository
-        uses: actions/checkout@v4
+        uses: actions/checkout@v6
 
       # Use `docker/login-action` to log in to GHCR.io. 
       # Once published, the packages are scoped to the account defined here.
diff --git CHANGELOG.md CHANGELOG.md
index 25befb782..6d5a6f302 100644
--- CHANGELOG.md
+++ CHANGELOG.md
@@ -1,19 +1,19 @@
 # Changelog
 
-## V6.0.0
+## v6.0.0
 * Persist creds to a separate file by @ericsciple in https://github.com/actions/checkout/pull/2286
 * Update README to include Node.js 24 support details and requirements by @salmanmkc in https://github.com/actions/checkout/pull/2248
 
-## V5.0.1
+## v5.0.1
 * Port v6 cleanup to v5 by @ericsciple in https://github.com/actions/checkout/pull/2301
 
-## V5.0.0
+## v5.0.0
 * Update actions checkout to use node 24 by @salmanmkc in https://github.com/actions/checkout/pull/2226
 
-## V4.3.1
+## v4.3.1
 * Port v6 cleanup to v4 by @ericsciple in https://github.com/actions/checkout/pull/2305
 
-## V4.3.0
+## v4.3.0
 * docs: update README.md by @motss in https://github.com/actions/checkout/pull/1971
 * Add internal repos for checking out multiple repositories by @mouismail in https://github.com/actions/checkout/pull/1977
 * Documentation update - add recommended permissions to Readme by @benwells in https://github.com/actions/checkout/pull/2043
diff --git README.md README.md
index 5ad476f49..f0f65f9f6 100644
--- README.md
+++ README.md
@@ -4,8 +4,9 @@
 
 ## What's new
 
-- Updated `persist-credentials` to store the credentials under `$RUNNER_TEMP` instead of directly in the local git config.
-  - This requires a minimum Actions Runner version of [v2.329.0](https://github.com/actions/runner/releases/tag/v2.329.0) to access the persisted credentials for [Docker container action](https://docs.github.com/en/actions/tutorials/use-containerized-services/create-a-docker-container-action) scenarios.
+- Improved credential security: `persist-credentials` now stores credentials in a separate file under `$RUNNER_TEMP` instead of directly in `.git/config`
+- No workflow changes required — `git fetch`, `git push`, etc. continue to work automatically
+- Running authenticated git commands from a [Docker container action](https://docs.github.com/actions/sharing-automations/creating-actions/creating-a-docker-container-action) requires Actions Runner [v2.329.0](https://github.com/actions/runner/releases/tag/v2.329.0) or later
 
 # Checkout v5
 
@@ -51,7 +52,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 
 <!-- start usage -->
 ```yaml
-- uses: actions/checkout@v5
+- uses: actions/checkout@v6
   with:
     # Repository name with owner. For example, actions/checkout
     # Default: ${{ github.repository }}
@@ -190,7 +191,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 ## Fetch only the root files
 
 ```yaml
-- uses: actions/checkout@v5
+- uses: actions/checkout@v6
   with:
     sparse-checkout: .
 ```
@@ -198,7 +199,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 ## Fetch only the root files and `.github` and `src` folder
 
 ```yaml
-- uses: actions/checkout@v5
+- uses: actions/checkout@v6
   with:
     sparse-checkout: |
       .github
@@ -208,7 +209,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 ## Fetch only a single file
 
 ```yaml
-- uses: actions/checkout@v5
+- uses: actions/checkout@v6
   with:
     sparse-checkout: |
       README.md
@@ -218,7 +219,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 ## Fetch all history for all tags and branches
 
 ```yaml
-- uses: actions/checkout@v5
+- uses: actions/checkout@v6
   with:
     fetch-depth: 0
 ```
@@ -226,7 +227,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 ## Checkout a different branch
 
 ```yaml
-- uses: actions/checkout@v5
+- uses: actions/checkout@v6
   with:
     ref: my-branch
 ```
@@ -234,7 +235,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 ## Checkout HEAD^
 
 ```yaml
-- uses: actions/checkout@v5
+- uses: actions/checkout@v6
   with:
     fetch-depth: 2
 - run: git checkout HEAD^
@@ -244,12 +245,12 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 
 ```yaml
 - name: Checkout
-  uses: actions/checkout@v5
+  uses: actions/checkout@v6
   with:
     path: main
 
 - name: Checkout tools repo
-  uses: actions/checkout@v5
+  uses: actions/checkout@v6
   with:
     repository: my-org/my-tools
     path: my-tools
@@ -260,10 +261,10 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 
 ```yaml
 - name: Checkout
-  uses: actions/checkout@v5
+  uses: actions/checkout@v6
 
 - name: Checkout tools repo
-  uses: actions/checkout@v5
+  uses: actions/checkout@v6
   with:
     repository: my-org/my-tools
     path: my-tools
@@ -274,12 +275,12 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 
 ```yaml
 - name: Checkout
-  uses: actions/checkout@v5
+  uses: actions/checkout@v6
   with:
     path: main
 
 - name: Checkout private tools
-  uses: actions/checkout@v5
+  uses: actions/checkout@v6
   with:
     repository: my-org/my-private-tools
     token: ${{ secrets.GH_PAT }} # `GH_PAT` is a secret that contains your PAT
@@ -292,7 +293,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 ## Checkout pull request HEAD commit instead of merge commit
 
 ```yaml
-- uses: actions/checkout@v5
+- uses: actions/checkout@v6
   with:
     ref: ${{ github.event.pull_request.head.sha }}
 ```
@@ -308,7 +309,7 @@ jobs:
   build:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v5
+      - uses: actions/checkout@v6
 ```
 
 ## Push a commit using the built-in token
@@ -319,7 +320,7 @@ jobs:
   build:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v5
+      - uses: actions/checkout@v6
       - run: |
           date > generated.txt
           # Note: the following account information will not work on GHES
@@ -341,7 +342,7 @@ jobs:
   build:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v5
+      - uses: actions/checkout@v6
         with:
           ref: ${{ github.head_ref }}
       - run: |
diff --git __test__/git-command-manager.test.ts __test__/git-command-manager.test.ts
index cea73d4dd..8a97d827a 100644
--- __test__/git-command-manager.test.ts
+++ __test__/git-command-manager.test.ts
@@ -108,7 +108,7 @@ describe('Test fetchDepth and fetchTags options', () => {
     jest.restoreAllMocks()
   })
 
-  it('should call execGit with the correct arguments when fetchDepth is 0 and fetchTags is true', async () => {
+  it('should call execGit with the correct arguments when fetchDepth is 0', async () => {
     jest.spyOn(exec, 'exec').mockImplementation(mockExec)
     const workingDirectory = 'test'
     const lfs = false
@@ -122,8 +122,7 @@ describe('Test fetchDepth and fetchTags options', () => {
     const refSpec = ['refspec1', 'refspec2']
     const options = {
       filter: 'filterValue',
-      fetchDepth: 0,
-      fetchTags: true
+      fetchDepth: 0
     }
 
     await git.fetch(refSpec, options)
@@ -134,6 +133,7 @@ describe('Test fetchDepth and fetchTags options', () => {
         '-c',
         'protocol.version=2',
         'fetch',
+        '--no-tags',
         '--prune',
         '--no-recurse-submodules',
         '--filter=filterValue',
@@ -145,7 +145,7 @@ describe('Test fetchDepth and fetchTags options', () => {
     )
   })
 
-  it('should call execGit with the correct arguments when fetchDepth is 0 and fetchTags is false', async () => {
+  it('should call execGit with the correct arguments when fetchDepth is 0 and refSpec includes tags', async () => {
     jest.spyOn(exec, 'exec').mockImplementation(mockExec)
 
     const workingDirectory = 'test'
@@ -156,11 +156,10 @@ describe('Test fetchDepth and fetchTags options', () => {
       lfs,
       doSparseCheckout
     )
-    const refSpec = ['refspec1', 'refspec2']
+    const refSpec = ['refspec1', 'refspec2', '+refs/tags/*:refs/tags/*']
     const options = {
       filter: 'filterValue',
-      fetchDepth: 0,
-      fetchTags: false
+      fetchDepth: 0
     }
 
     await git.fetch(refSpec, options)
@@ -177,13 +176,14 @@ describe('Test fetchDepth and fetchTags options', () => {
         '--filter=filterValue',
         'origin',
         'refspec1',
-        'refspec2'
+        'refspec2',
+        '+refs/tags/*:refs/tags/*'
       ],
       expect.any(Object)
     )
   })
 
-  it('should call execGit with the correct arguments when fetchDepth is 1 and fetchTags is false', async () => {
+  it('should call execGit with the correct arguments when fetchDepth is 1', async () => {
     jest.spyOn(exec, 'exec').mockImplementation(mockExec)
 
     const workingDirectory = 'test'
@@ -197,8 +197,7 @@ describe('Test fetchDepth and fetchTags options', () => {
     const refSpec = ['refspec1', 'refspec2']
     const options = {
       filter: 'filterValue',
-      fetchDepth: 1,
-      fetchTags: false
+      fetchDepth: 1
     }
 
     await git.fetch(refSpec, options)
@@ -222,7 +221,7 @@ describe('Test fetchDepth and fetchTags options', () => {
     )
   })
 
-  it('should call execGit with the correct arguments when fetchDepth is 1 and fetchTags is true', async () => {
+  it('should call execGit with the correct arguments when fetchDepth is 1 and refSpec includes tags', async () => {
     jest.spyOn(exec, 'exec').mockImplementation(mockExec)
 
     const workingDirectory = 'test'
@@ -233,11 +232,10 @@ describe('Test fetchDepth and fetchTags options', () => {
       lfs,
       doSparseCheckout
     )
-    const refSpec = ['refspec1', 'refspec2']
+    const refSpec = ['refspec1', 'refspec2', '+refs/tags/*:refs/tags/*']
     const options = {
       filter: 'filterValue',
-      fetchDepth: 1,
-      fetchTags: true
+      fetchDepth: 1
     }
 
     await git.fetch(refSpec, options)
@@ -248,13 +246,15 @@ describe('Test fetchDepth and fetchTags options', () => {
         '-c',
         'protocol.version=2',
         'fetch',
+        '--no-tags',
         '--prune',
         '--no-recurse-submodules',
         '--filter=filterValue',
         '--depth=1',
         'origin',
         'refspec1',
-        'refspec2'
+        'refspec2',
+        '+refs/tags/*:refs/tags/*'
       ],
       expect.any(Object)
     )
@@ -338,7 +338,7 @@ describe('Test fetchDepth and fetchTags options', () => {
     )
   })
 
-  it('should call execGit with the correct arguments when fetchTags is true and showProgress is true', async () => {
+  it('should call execGit with the correct arguments when showProgress is true and refSpec includes tags', async () => {
     jest.spyOn(exec, 'exec').mockImplementation(mockExec)
 
     const workingDirectory = 'test'
@@ -349,10 +349,9 @@ describe('Test fetchDepth and fetchTags options', () => {
       lfs,
       doSparseCheckout
     )
-    const refSpec = ['refspec1', 'refspec2']
+    const refSpec = ['refspec1', 'refspec2', '+refs/tags/*:refs/tags/*']
     const options = {
       filter: 'filterValue',
-      fetchTags: true,
       showProgress: true
     }
 
@@ -364,15 +363,134 @@ describe('Test fetchDepth and fetchTags options', () => {
         '-c',
         'protocol.version=2',
         'fetch',
+        '--no-tags',
         '--prune',
         '--no-recurse-submodules',
         '--progress',
         '--filter=filterValue',
         'origin',
         'refspec1',
-        'refspec2'
+        'refspec2',
+        '+refs/tags/*:refs/tags/*'
       ],
       expect.any(Object)
     )
   })
 })
+
+describe('git user-agent with orchestration ID', () => {
+  beforeEach(async () => {
+    jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn())
+    jest.spyOn(fshelper, 'directoryExistsSync').mockImplementation(jest.fn())
+  })
+
+  afterEach(() => {
+    jest.restoreAllMocks()
+    // Clean up environment variable to prevent test pollution
+    delete process.env['ACTIONS_ORCHESTRATION_ID']
+  })
+
+  it('should include orchestration ID in user-agent when ACTIONS_ORCHESTRATION_ID is set', async () => {
+    const orchId = 'test-orch-id-12345'
+    process.env['ACTIONS_ORCHESTRATION_ID'] = orchId
+
+    let capturedEnv: any = null
+    mockExec.mockImplementation((path, args, options) => {
+      if (args.includes('version')) {
+        options.listeners.stdout(Buffer.from('2.18'))
+      }
+      // Capture env on any command
+      capturedEnv = options.env
+      return 0
+    })
+    jest.spyOn(exec, 'exec').mockImplementation(mockExec)
+
+    const workingDirectory = 'test'
+    const lfs = false
+    const doSparseCheckout = false
+    git = await commandManager.createCommandManager(
+      workingDirectory,
+      lfs,
+      doSparseCheckout
+    )
+
+    // Call a git command to trigger env capture after user-agent is set
+    await git.init()
+
+    // Verify the user agent includes the orchestration ID
+    expect(git).toBeDefined()
+    expect(capturedEnv).toBeDefined()
+    expect(capturedEnv['GIT_HTTP_USER_AGENT']).toBe(
+      `git/2.18 (github-actions-checkout) actions_orchestration_id/${orchId}`
+    )
+  })
+
+  it('should sanitize invalid characters in orchestration ID', async () => {
+    const orchId = 'test (with) special/chars'
+    process.env['ACTIONS_ORCHESTRATION_ID'] = orchId
+
+    let capturedEnv: any = null
+    mockExec.mockImplementation((path, args, options) => {
+      if (args.includes('version')) {
+        options.listeners.stdout(Buffer.from('2.18'))
+      }
+      // Capture env on any command
+      capturedEnv = options.env
+      return 0
+    })
+    jest.spyOn(exec, 'exec').mockImplementation(mockExec)
+
+    const workingDirectory = 'test'
+    const lfs = false
+    const doSparseCheckout = false
+    git = await commandManager.createCommandManager(
+      workingDirectory,
+      lfs,
+      doSparseCheckout
+    )
+
+    // Call a git command to trigger env capture after user-agent is set
+    await git.init()
+
+    // Verify the user agent has sanitized orchestration ID (spaces, parentheses, slash replaced)
+    expect(git).toBeDefined()
+    expect(capturedEnv).toBeDefined()
+    expect(capturedEnv['GIT_HTTP_USER_AGENT']).toBe(
+      'git/2.18 (github-actions-checkout) actions_orchestration_id/test__with__special_chars'
+    )
+  })
+
+  it('should not modify user-agent when ACTIONS_ORCHESTRATION_ID is not set', async () => {
+    delete process.env['ACTIONS_ORCHESTRATION_ID']
+
+    let capturedEnv: any = null
+    mockExec.mockImplementation((path, args, options) => {
+      if (args.includes('version')) {
+        options.listeners.stdout(Buffer.from('2.18'))
+      }
+      // Capture env on any command
+      capturedEnv = options.env
+      return 0
+    })
+    jest.spyOn(exec, 'exec').mockImplementation(mockExec)
+
+    const workingDirectory = 'test'
+    const lfs = false
+    const doSparseCheckout = false
+    git = await commandManager.createCommandManager(
+      workingDirectory,
+      lfs,
+      doSparseCheckout
+    )
+
+    // Call a git command to trigger env capture after user-agent is set
+    await git.init()
+
+    // Verify the user agent does NOT contain orchestration ID
+    expect(git).toBeDefined()
+    expect(capturedEnv).toBeDefined()
+    expect(capturedEnv['GIT_HTTP_USER_AGENT']).toBe(
+      'git/2.18 (github-actions-checkout)'
+    )
+  })
+})
diff --git __test__/ref-helper.test.ts __test__/ref-helper.test.ts
index 5c8d76b87..4943abd6d 100644
--- __test__/ref-helper.test.ts
+++ __test__/ref-helper.test.ts
@@ -152,7 +152,22 @@ describe('ref-helper tests', () => {
   it('getRefSpec sha + refs/tags/', async () => {
     const refSpec = refHelper.getRefSpec('refs/tags/my-tag', commit)
     expect(refSpec.length).toBe(1)
-    expect(refSpec[0]).toBe(`+${commit}:refs/tags/my-tag`)
+    expect(refSpec[0]).toBe(`+refs/tags/my-tag:refs/tags/my-tag`)
+  })
+
+  it('getRefSpec sha + refs/tags/ with fetchTags', async () => {
+    // When fetchTags is true, only include tags wildcard (specific tag is redundant)
+    const refSpec = refHelper.getRefSpec('refs/tags/my-tag', commit, true)
+    expect(refSpec.length).toBe(1)
+    expect(refSpec[0]).toBe('+refs/tags/*:refs/tags/*')
+  })
+
+  it('getRefSpec sha + refs/heads/ with fetchTags', async () => {
+    // When fetchTags is true, include both the branch refspec and tags wildcard
+    const refSpec = refHelper.getRefSpec('refs/heads/my/branch', commit, true)
+    expect(refSpec.length).toBe(2)
+    expect(refSpec[0]).toBe('+refs/tags/*:refs/tags/*')
+    expect(refSpec[1]).toBe(`+${commit}:refs/remotes/origin/my/branch`)
   })
 
   it('getRefSpec sha only', async () => {
@@ -168,6 +183,14 @@ describe('ref-helper tests', () => {
     expect(refSpec[1]).toBe('+refs/tags/my-ref*:refs/tags/my-ref*')
   })
 
+  it('getRefSpec unqualified ref only with fetchTags', async () => {
+    // When fetchTags is true, skip specific tag pattern since wildcard covers all
+    const refSpec = refHelper.getRefSpec('my-ref', '', true)
+    expect(refSpec.length).toBe(2)
+    expect(refSpec[0]).toBe('+refs/tags/*:refs/tags/*')
+    expect(refSpec[1]).toBe('+refs/heads/my-ref*:refs/remotes/origin/my-ref*')
+  })
+
   it('getRefSpec refs/heads/ only', async () => {
     const refSpec = refHelper.getRefSpec('refs/heads/my/branch', '')
     expect(refSpec.length).toBe(1)
@@ -187,4 +210,21 @@ describe('ref-helper tests', () => {
     expect(refSpec.length).toBe(1)
     expect(refSpec[0]).toBe('+refs/tags/my-tag:refs/tags/my-tag')
   })
+
+  it('getRefSpec refs/tags/ only with fetchTags', async () => {
+    // When fetchTags is true, only include tags wildcard (specific tag is redundant)
+    const refSpec = refHelper.getRefSpec('refs/tags/my-tag', '', true)
+    expect(refSpec.length).toBe(1)
+    expect(refSpec[0]).toBe('+refs/tags/*:refs/tags/*')
+  })
+
+  it('getRefSpec refs/heads/ only with fetchTags', async () => {
+    // When fetchTags is true, include both the branch refspec and tags wildcard
+    const refSpec = refHelper.getRefSpec('refs/heads/my/branch', '', true)
+    expect(refSpec.length).toBe(2)
+    expect(refSpec[0]).toBe('+refs/tags/*:refs/tags/*')
+    expect(refSpec[1]).toBe(
+      '+refs/heads/my/branch:refs/remotes/origin/my/branch'
+    )
+  })
 })
diff --git a/__test__/verify-fetch-tags.sh b/__test__/verify-fetch-tags.sh
new file mode 100755
index 000000000..74cff1ed6
--- /dev/null
+++ __test__/verify-fetch-tags.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+
+# Verify tags were fetched
+TAG_COUNT=$(git -C ./fetch-tags-test tag | wc -l)
+if [ "$TAG_COUNT" -eq 0 ]; then
+    echo "Expected tags to be fetched, but found none"
+    exit 1
+fi
+echo "Found $TAG_COUNT tags"
diff --git a/__test__/verify-worktree.sh b/__test__/verify-worktree.sh
new file mode 100755
index 000000000..3a4d3e4df
--- /dev/null
+++ __test__/verify-worktree.sh
@@ -0,0 +1,51 @@
+#!/bin/bash
+set -e
+
+# Verify worktree credentials
+# This test verifies that git credentials work in worktrees created after checkout
+# Usage: verify-worktree.sh <checkout-path> <worktree-name>
+
+CHECKOUT_PATH="$1"
+WORKTREE_NAME="$2"
+
+if [ -z "$CHECKOUT_PATH" ] || [ -z "$WORKTREE_NAME" ]; then
+  echo "Usage: verify-worktree.sh <checkout-path> <worktree-name>"
+  exit 1
+fi
+
+cd "$CHECKOUT_PATH"
+
+# Add safe directory for container environments
+git config --global --add safe.directory "*" 2>/dev/null || true
+
+# Show the includeIf configuration
+echo "Git config includeIf entries:"
+git config --list --show-origin | grep -i include || true
+
+# Create the worktree
+echo "Creating worktree..."
+git worktree add "../$WORKTREE_NAME" HEAD --detach
+
+# Change to worktree directory
+cd "../$WORKTREE_NAME"
+
+# Verify we're in a worktree
+echo "Verifying worktree gitdir:"
+cat .git
+
+# Verify credentials are available in worktree by checking extraheader is configured
+echo "Checking credentials in worktree..."
+if git config --list --show-origin | grep -q "extraheader"; then
+  echo "Credentials are configured in worktree"
+else
+  echo "ERROR: Credentials are NOT configured in worktree"
+  echo "Full git config:"
+  git config --list --show-origin
+  exit 1
+fi
+
+# Verify fetch works in the worktree
+echo "Fetching in worktree..."
+git fetch origin
+
+echo "Worktree credentials test passed!"
diff --git dist/index.js dist/index.js
index a251a1966..fe3f3170e 100644
--- dist/index.js
+++ dist/index.js
@@ -412,6 +412,9 @@ class GitAuthHelper {
                 // Configure host includeIf
                 const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`;
                 yield this.git.config(hostIncludeKey, credentialsConfigPath);
+                // Configure host includeIf for worktrees
+                const hostWorktreeIncludeKey = `includeIf.gitdir:${gitDir}/worktrees/*.path`;
+                yield this.git.config(hostWorktreeIncludeKey, credentialsConfigPath);
                 // Container git directory
                 const workingDirectory = this.git.getWorkingDirectory();
                 const githubWorkspace = process.env['GITHUB_WORKSPACE'];
@@ -424,6 +427,9 @@ class GitAuthHelper {
                 // Configure container includeIf
                 const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`;
                 yield this.git.config(containerIncludeKey, containerCredentialsPath);
+                // Configure container includeIf for worktrees
+                const containerWorktreeIncludeKey = `includeIf.gitdir:${containerGitDir}/worktrees/*.path`;
+                yield this.git.config(containerWorktreeIncludeKey, containerCredentialsPath);
             }
         });
     }
@@ -647,7 +653,6 @@ const fs = __importStar(__nccwpck_require__(7147));
 const fshelper = __importStar(__nccwpck_require__(7219));
 const io = __importStar(__nccwpck_require__(7436));
 const path = __importStar(__nccwpck_require__(1017));
-const refHelper = __importStar(__nccwpck_require__(8601));
 const regexpHelper = __importStar(__nccwpck_require__(3120));
 const retryHelper = __importStar(__nccwpck_require__(2155));
 const git_version_1 = __nccwpck_require__(3142);
@@ -825,9 +830,9 @@ class GitCommandManager {
     fetch(refSpec, options) {
         return __awaiter(this, void 0, void 0, function* () {
             const args = ['-c', 'protocol.version=2', 'fetch'];
-            if (!refSpec.some(x => x === refHelper.tagsRefSpec) && !options.fetchTags) {
-                args.push('--no-tags');
-            }
+            // Always use --no-tags for explicit control over tag fetching
+            // Tags are fetched explicitly via refspec when needed
+            args.push('--no-tags');
             args.push('--prune', '--no-recurse-submodules');
             if (options.showProgress) {
                 args.push('--progress');
@@ -1200,7 +1205,17 @@ class GitCommandManager {
                 }
             }
             // Set the user agent
-            const gitHttpUserAgent = `git/${this.gitVersion} (github-actions-checkout)`;
+            let gitHttpUserAgent = `git/${this.gitVersion} (github-actions-checkout)`;
+            // Append orchestration ID if set
+            const orchId = process.env['ACTIONS_ORCHESTRATION_ID'];
+            if (orchId) {
+                // Sanitize the orchestration ID to ensure it contains only valid characters
+                // Valid characters: 0-9, a-z, _, -, .
+                const sanitizedId = orchId.replace(/[^a-z0-9_.-]/gi, '_');
+                if (sanitizedId) {
+                    gitHttpUserAgent = `${gitHttpUserAgent} actions_orchestration_id/${sanitizedId}`;
+                }
+            }
             core.debug(`Set git useragent to: ${gitHttpUserAgent}`);
             this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent;
         });
@@ -1523,13 +1538,26 @@ function getSource(settings) {
                 if (!(yield refHelper.testRef(git, settings.ref, settings.commit))) {
                     refSpec = refHelper.getRefSpec(settings.ref, settings.commit);
                     yield git.fetch(refSpec, fetchOptions);
+                    // Verify the ref now matches. For branches, the targeted fetch above brings
+                    // in the specific commit. For tags (fetched by ref), this will fail if
+                    // the tag was moved after the workflow was triggered.
+                    if (!(yield refHelper.testRef(git, settings.ref, settings.commit))) {
+                        throw new Error(`The ref '${settings.ref}' does not point to the expected commit '${settings.commit}'. ` +
+                            `The ref may have been updated after the workflow was triggered.`);
+                    }
                 }
             }
             else {
                 fetchOptions.fetchDepth = settings.fetchDepth;
-                fetchOptions.fetchTags = settings.fetchTags;
-                const refSpec = refHelper.getRefSpec(settings.ref, settings.commit);
+                const refSpec = refHelper.getRefSpec(settings.ref, settings.commit, settings.fetchTags);
                 yield git.fetch(refSpec, fetchOptions);
+                // For tags, verify the ref still points to the expected commit.
+                // Tags are fetched by ref (not commit), so if a tag was moved after the
+                // workflow was triggered, we would silently check out the wrong commit.
+                if (!(yield refHelper.testRef(git, settings.ref, settings.commit))) {
+                    throw new Error(`The ref '${settings.ref}' does not point to the expected commit '${settings.commit}'. ` +
+                        `The ref may have been updated after the workflow was triggered.`);
+                }
             }
             core.endGroup();
             // Checkout info
@@ -2268,53 +2296,67 @@ function getRefSpecForAllHistory(ref, commit) {
     }
     return result;
 }
-function getRefSpec(ref, commit) {
+function getRefSpec(ref, commit, fetchTags) {
     if (!ref && !commit) {
         throw new Error('Args ref and commit cannot both be empty');
     }
     const upperRef = (ref || '').toUpperCase();
+    const result = [];
+    // When fetchTags is true, always include the tags refspec
+    if (fetchTags) {
+        result.push(exports.tagsRefSpec);
+    }
     // SHA
     if (commit) {
         // refs/heads
         if (upperRef.startsWith('REFS/HEADS/')) {
             const branch = ref.substring('refs/heads/'.length);
-            return [`+${commit}:refs/remotes/origin/${branch}`];
+            result.push(`+${commit}:refs/remotes/origin/${branch}`);
         }
         // refs/pull/
         else if (upperRef.startsWith('REFS/PULL/')) {
             const branch = ref.substring('refs/pull/'.length);
-            return [`+${commit}:refs/remotes/pull/${branch}`];
+            result.push(`+${commit}:refs/remotes/pull/${branch}`);
         }
         // refs/tags/
         else if (upperRef.startsWith('REFS/TAGS/')) {
-            return [`+${commit}:${ref}`];
+            if (!fetchTags) {
+                result.push(`+${ref}:${ref}`);
+            }
         }
         // Otherwise no destination ref
         else {
-            return [commit];
+            result.push(commit);
         }
     }
     // Unqualified ref, check for a matching branch or tag
     else if (!upperRef.startsWith('REFS/')) {
-        return [
-            `+refs/heads/${ref}*:refs/remotes/origin/${ref}*`,
-            `+refs/tags/${ref}*:refs/tags/${ref}*`
-        ];
+        result.push(`+refs/heads/${ref}*:refs/remotes/origin/${ref}*`);
+        if (!fetchTags) {
+            result.push(`+refs/tags/${ref}*:refs/tags/${ref}*`);
+        }
     }
     // refs/heads/
     else if (upperRef.startsWith('REFS/HEADS/')) {
         const branch = ref.substring('refs/heads/'.length);
-        return [`+${ref}:refs/remotes/origin/${branch}`];
+        result.push(`+${ref}:refs/remotes/origin/${branch}`);
     }
     // refs/pull/
     else if (upperRef.startsWith('REFS/PULL/')) {
         const branch = ref.substring('refs/pull/'.length);
-        return [`+${ref}:refs/remotes/pull/${branch}`];
+        result.push(`+${ref}:refs/remotes/pull/${branch}`);
     }
     // refs/tags/
+    else if (upperRef.startsWith('REFS/TAGS/')) {
+        if (!fetchTags) {
+            result.push(`+${ref}:${ref}`);
+        }
+    }
+    // Other refs
     else {
-        return [`+${ref}:${ref}`];
+        result.push(`+${ref}:${ref}`);
     }
+    return result;
 }
 /**
  * Tests whether the initial fetch created the ref at the expected commit
@@ -2350,7 +2392,9 @@ function testRef(git, ref, commit) {
         // refs/tags/
         else if (upperRef.startsWith('REFS/TAGS/')) {
             const tagName = ref.substring('refs/tags/'.length);
-            return ((yield git.tagExists(tagName)) && commit === (yield git.revParse(ref)));
+            // Use ^{commit} to dereference annotated tags to their underlying commit
+            return ((yield git.tagExists(tagName)) &&
+                commit === (yield git.revParse(`${ref}^{commit}`)));
         }
         // Unexpected
         else {
diff --git src/git-auth-helper.ts src/git-auth-helper.ts
index a1950a60c..e67db148a 100644
--- src/git-auth-helper.ts
+++ src/git-auth-helper.ts
@@ -374,6 +374,10 @@ class GitAuthHelper {
       const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`
       await this.git.config(hostIncludeKey, credentialsConfigPath)
 
+      // Configure host includeIf for worktrees
+      const hostWorktreeIncludeKey = `includeIf.gitdir:${gitDir}/worktrees/*.path`
+      await this.git.config(hostWorktreeIncludeKey, credentialsConfigPath)
+
       // Container git directory
       const workingDirectory = this.git.getWorkingDirectory()
       const githubWorkspace = process.env['GITHUB_WORKSPACE']
@@ -395,6 +399,13 @@ class GitAuthHelper {
       // Configure container includeIf
       const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`
       await this.git.config(containerIncludeKey, containerCredentialsPath)
+
+      // Configure container includeIf for worktrees
+      const containerWorktreeIncludeKey = `includeIf.gitdir:${containerGitDir}/worktrees/*.path`
+      await this.git.config(
+        containerWorktreeIncludeKey,
+        containerCredentialsPath
+      )
     }
   }
 
diff --git src/git-command-manager.ts src/git-command-manager.ts
index a45e15a86..f5ba40e9f 100644
--- src/git-command-manager.ts
+++ src/git-command-manager.ts
@@ -37,7 +37,6 @@ export interface IGitCommandManager {
     options: {
       filter?: string
       fetchDepth?: number
-      fetchTags?: boolean
       showProgress?: boolean
     }
   ): Promise<void>
@@ -280,14 +279,13 @@ class GitCommandManager {
     options: {
       filter?: string
       fetchDepth?: number
-      fetchTags?: boolean
       showProgress?: boolean
     }
   ): Promise<void> {
     const args = ['-c', 'protocol.version=2', 'fetch']
-    if (!refSpec.some(x => x === refHelper.tagsRefSpec) && !options.fetchTags) {
-      args.push('--no-tags')
-    }
+    // Always use --no-tags for explicit control over tag fetching
+    // Tags are fetched explicitly via refspec when needed
+    args.push('--no-tags')
 
     args.push('--prune', '--no-recurse-submodules')
     if (options.showProgress) {
@@ -730,7 +728,19 @@ class GitCommandManager {
       }
     }
     // Set the user agent
-    const gitHttpUserAgent = `git/${this.gitVersion} (github-actions-checkout)`
+    let gitHttpUserAgent = `git/${this.gitVersion} (github-actions-checkout)`
+
+    // Append orchestration ID if set
+    const orchId = process.env['ACTIONS_ORCHESTRATION_ID']
+    if (orchId) {
+      // Sanitize the orchestration ID to ensure it contains only valid characters
+      // Valid characters: 0-9, a-z, _, -, .
+      const sanitizedId = orchId.replace(/[^a-z0-9_.-]/gi, '_')
+      if (sanitizedId) {
+        gitHttpUserAgent = `${gitHttpUserAgent} actions_orchestration_id/${sanitizedId}`
+      }
+    }
+
     core.debug(`Set git useragent to: ${gitHttpUserAgent}`)
     this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent
   }
diff --git src/git-source-provider.ts src/git-source-provider.ts
index 2d3513897..ec871784f 100644
--- src/git-source-provider.ts
+++ src/git-source-provider.ts
@@ -159,7 +159,6 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
     const fetchOptions: {
       filter?: string
       fetchDepth?: number
-      fetchTags?: boolean
       showProgress?: boolean
     } = {}
 
@@ -182,12 +181,35 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
       if (!(await refHelper.testRef(git, settings.ref, settings.commit))) {
         refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
         await git.fetch(refSpec, fetchOptions)
+
+        // Verify the ref now matches. For branches, the targeted fetch above brings
+        // in the specific commit. For tags (fetched by ref), this will fail if
+        // the tag was moved after the workflow was triggered.
+        if (!(await refHelper.testRef(git, settings.ref, settings.commit))) {
+          throw new Error(
+            `The ref '${settings.ref}' does not point to the expected commit '${settings.commit}'. ` +
+              `The ref may have been updated after the workflow was triggered.`
+          )
+        }
       }
     } else {
       fetchOptions.fetchDepth = settings.fetchDepth
-      fetchOptions.fetchTags = settings.fetchTags
-      const refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
+      const refSpec = refHelper.getRefSpec(
+        settings.ref,
+        settings.commit,
+        settings.fetchTags
+      )
       await git.fetch(refSpec, fetchOptions)
+
+      // For tags, verify the ref still points to the expected commit.
+      // Tags are fetched by ref (not commit), so if a tag was moved after the
+      // workflow was triggered, we would silently check out the wrong commit.
+      if (!(await refHelper.testRef(git, settings.ref, settings.commit))) {
+        throw new Error(
+          `The ref '${settings.ref}' does not point to the expected commit '${settings.commit}'. ` +
+            `The ref may have been updated after the workflow was triggered.`
+        )
+      }
     }
     core.endGroup()
 
diff --git src/misc/generate-docs.ts src/misc/generate-docs.ts
index 6d4816f15..b78f035c5 100644
--- src/misc/generate-docs.ts
+++ src/misc/generate-docs.ts
@@ -120,7 +120,7 @@ function updateUsage(
 }
 
 updateUsage(
-  'actions/checkout@v5',
+  'actions/checkout@v6',
   path.join(__dirname, '..', '..', 'action.yml'),
   path.join(__dirname, '..', '..', 'README.md')
 )
diff --git src/ref-helper.ts src/ref-helper.ts
index 58f929098..5130f53d7 100644
--- src/ref-helper.ts
+++ src/ref-helper.ts
@@ -76,55 +76,75 @@ export function getRefSpecForAllHistory(ref: string, commit: string): string[] {
   return result
 }
 
-export function getRefSpec(ref: string, commit: string): string[] {
+export function getRefSpec(
+  ref: string,
+  commit: string,
+  fetchTags?: boolean
+): string[] {
   if (!ref && !commit) {
     throw new Error('Args ref and commit cannot both be empty')
   }
 
   const upperRef = (ref || '').toUpperCase()
+  const result: string[] = []
+
+  // When fetchTags is true, always include the tags refspec
+  if (fetchTags) {
+    result.push(tagsRefSpec)
+  }
 
   // SHA
   if (commit) {
     // refs/heads
     if (upperRef.startsWith('REFS/HEADS/')) {
       const branch = ref.substring('refs/heads/'.length)
-      return [`+${commit}:refs/remotes/origin/${branch}`]
+      result.push(`+${commit}:refs/remotes/origin/${branch}`)
     }
     // refs/pull/
     else if (upperRef.startsWith('REFS/PULL/')) {
       const branch = ref.substring('refs/pull/'.length)
-      return [`+${commit}:refs/remotes/pull/${branch}`]
+      result.push(`+${commit}:refs/remotes/pull/${branch}`)
     }
     // refs/tags/
     else if (upperRef.startsWith('REFS/TAGS/')) {
-      return [`+${commit}:${ref}`]
+      if (!fetchTags) {
+        result.push(`+${ref}:${ref}`)
+      }
     }
     // Otherwise no destination ref
     else {
-      return [commit]
+      result.push(commit)
     }
   }
   // Unqualified ref, check for a matching branch or tag
   else if (!upperRef.startsWith('REFS/')) {
-    return [
-      `+refs/heads/${ref}*:refs/remotes/origin/${ref}*`,
-      `+refs/tags/${ref}*:refs/tags/${ref}*`
-    ]
+    result.push(`+refs/heads/${ref}*:refs/remotes/origin/${ref}*`)
+    if (!fetchTags) {
+      result.push(`+refs/tags/${ref}*:refs/tags/${ref}*`)
+    }
   }
   // refs/heads/
   else if (upperRef.startsWith('REFS/HEADS/')) {
     const branch = ref.substring('refs/heads/'.length)
-    return [`+${ref}:refs/remotes/origin/${branch}`]
+    result.push(`+${ref}:refs/remotes/origin/${branch}`)
   }
   // refs/pull/
   else if (upperRef.startsWith('REFS/PULL/')) {
     const branch = ref.substring('refs/pull/'.length)
-    return [`+${ref}:refs/remotes/pull/${branch}`]
+    result.push(`+${ref}:refs/remotes/pull/${branch}`)
   }
   // refs/tags/
+  else if (upperRef.startsWith('REFS/TAGS/')) {
+    if (!fetchTags) {
+      result.push(`+${ref}:${ref}`)
+    }
+  }
+  // Other refs
   else {
-    return [`+${ref}:${ref}`]
+    result.push(`+${ref}:${ref}`)
   }
+
+  return result
 }
 
 /**
@@ -170,8 +190,10 @@ export async function testRef(
   // refs/tags/
   else if (upperRef.startsWith('REFS/TAGS/')) {
     const tagName = ref.substring('refs/tags/'.length)
+    // Use ^{commit} to dereference annotated tags to their underlying commit
     return (
-      (await git.tagExists(tagName)) && commit === (await git.revParse(ref))
+      (await git.tagExists(tagName)) &&
+      commit === (await git.revParse(`${ref}^{commit}`))
     )
   }
   // Unexpected

Description

Multi-faceted update to actions/checkout v6 covering: (1) worktree credential support via includeIf.gitdir glob patterns for /worktrees/*, (2) refactored tag fetching — always passes --no-tags and controls tags via explicit refspec instead of a fetchTags flag on the fetch options, (3) post-fetch ref verification to detect moved tags/refs after workflow trigger, (4) annotated tag dereference fix using ^{commit} in testRef, (5) orchestration ID appended to git HTTP user-agent, (6) workflow/doc bumps from v4/v5 to v6.

Possible Issues

  1. Ref verification may break legitimate workflows. The new testRef check after every fetch throws if the ref doesn't match the expected commit. For shallow fetches (fetchDepth > 0), tags pointing to commits outside the depth window will fail revParse — this could cause spurious failures when ref is a tag and commit is known but the tag's commit object isn't fetched.

  2. Empty refspec when fetchTags=true and ref is refs/tags/* with commit set. In getRefSpec, when commit is truthy and upperRef.startsWith('REFS/TAGS/') and fetchTags is true, the specific tag refspec is skipped (only tags wildcard remains). The fetch brings all tags but doesn't fetch the specific commit SHA — the commit may not be reachable, causing checkout to fail.

  3. getRefSpec for refs/tags/ + commit changed from +${commit}:${ref} to +${ref}:${ref}. Previously the commit SHA was used as the source; now the ref name is used. This is a semantic change — it fetches by ref name rather than pinning to the known commit. For the non-fetchTags path this makes the fetch trust the remote ref rather than the resolved commit, which weakens the security the new post-fetch check tries to add (TOCTOU between fetch and verify).

  4. Worktree includeIf glob pattern. includeIf.gitdir:…/worktrees/* uses a trailing * — git's includeIf gitdir matching treats * as not matching /. If worktree names contain subdirectories this won't match. Likely fine for typical worktree names, but worth noting.

Security Hotspots

  1. TOCTOU on tag fetch + verify (git-source-provider.ts). Tags are now fetched by ref name (+${ref}:${ref}) instead of by commit SHA. A race exists: the remote tag could be moved between the fetch and the testRef verification. The old approach (+${commit}:${ref}) was immune to this race. The new post-fetch check mitigates but doesn't eliminate the window.

  2. Orchestration ID sanitization (git-command-manager.ts). Regex [^a-z0-9_.-] is case-insensitive so uppercase letters pass through. The sanitization looks adequate, but the env var ACTIONS_ORCHESTRATION_ID is runner-controlled — if an attacker can inject arbitrary env vars, they can influence the user-agent header. Low risk given runner trust model.

Changes

Changes

src/ref-helper.ts / dist/index.js

  • getRefSpec gains optional fetchTags param; when true, prepends +refs/tags/*:refs/tags/* and omits redundant specific-tag refspecs.
  • Tag fetch with commit changed from +${commit}:${ref}+${ref}:${ref}.
  • testRef uses ${ref}^{commit} to dereference annotated tags.

src/git-command-manager.ts

  • Removed fetchTags from fetch options interface; --no-tags always passed.
  • Appends ACTIONS_ORCHESTRATION_ID (sanitized) to GIT_HTTP_USER_AGENT.

src/git-source-provider.ts

  • fetchTags moved from fetch options to getRefSpec call.
  • Post-fetch testRef verification added in both full-history and shallow paths.

src/git-auth-helper.ts

  • Adds includeIf.gitdir:…/worktrees/* entries for both host and container paths.

Tests & CI

  • Updated unit tests to match new fetch arg expectations and added orchestration ID tests.
  • New verify-fetch-tags.sh and verify-worktree.sh integration tests.
  • All workflow files bumped to actions/checkout@v6.

Docs

  • README.md examples updated to @v6; "What's new" rewritten.
  • CHANGELOG.md version headers lowercased.
sequenceDiagram
    participant WF as Workflow
    participant GSP as git-source-provider
    participant RH as ref-helper
    participant GCM as git-command-manager
    participant GAH as git-auth-helper
    participant Git as git CLI

    WF->>GSP: getSource(settings)
    GSP->>GAH: configureAuth()
    GAH->>Git: git config includeIf.gitdir:...path
    GAH->>Git: git config includeIf.gitdir:.../worktrees/*.path
    GSP->>RH: getRefSpec(ref, commit, fetchTags)
    RH-->>GSP: refSpec[] (includes tags refspec if fetchTags)
    GSP->>GCM: fetch(refSpec, {fetchDepth, ...})
    GCM->>Git: git fetch --no-tags --prune ... origin <refSpecs>
    Git-->>GCM: fetch result
    GCM-->>GSP: done
    GSP->>RH: testRef(git, ref, commit)
    RH->>Git: git rev-parse ref^{commit}
    Git-->>RH: resolved SHA
    RH-->>GSP: matches?
    alt ref mismatch
        GSP->>WF: throw Error("ref does not point to expected commit")
    else
        GSP->>Git: git checkout
        GSP-->>WF: success
    end
Loading

@renovate renovate Bot force-pushed the renovate/actions-checkout-6-x branch from 8cf7da2 to e1114e7 Compare April 17, 2026 23:44
@mschfh mschfh enabled auto-merge (squash) April 18, 2026 00:30
@mschfh mschfh merged commit 5260c76 into master Apr 18, 2026
9 checks passed
@mschfh mschfh deleted the renovate/actions-checkout-6-x branch April 18, 2026 00:30
DJAndries added a commit that referenced this pull request Apr 21, 2026
* chore(deps): update dependency go to 1.26 (#402)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update public.ecr.aws/docker/library/golang docker tag to v1.26 (#403)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update actions/setup-go action to v6.4.0 (#400)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module github.com/go-chi/chi/v5 to v5.2.5 (#399)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update actions/checkout action to v6.0.2 (#411)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update golangci/golangci-lint-action action to v9.2.0 (#413)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update aws-sdk-go-v2 monorepo (#404)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Add warning logs for items that exceed size limit (#414)

* Add warning logs for items that exceed size limit

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
DJAndries added a commit that referenced this pull request Apr 29, 2026
* chore(deps): update dependency go to 1.26 (#402)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update public.ecr.aws/docker/library/golang docker tag to v1.26 (#403)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update actions/setup-go action to v6.4.0 (#400)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update module github.com/go-chi/chi/v5 to v5.2.5 (#399)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update actions/checkout action to v6.0.2 (#411)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update golangci/golangci-lint-action action to v9.2.0 (#413)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update aws-sdk-go-v2 monorepo (#404)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Add warning logs for items that exceed size limit (#414)

* Add warning logs for items that exceed size limit

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* chore(deps): update alpine docker tag to v3.23 (#401)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* perf: optimize DynamoDB client timeouts and retry config (#408)

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: mschfh <37435502+mschfh@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant