Skip to content

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

Open
renovate[bot] wants to merge 1 commit intomasterfrom
renovate/actions-checkout-6-x
Open

chore(deps): update actions/checkout action to v6.0.2#2993
renovate[bot] wants to merge 1 commit intomasterfrom
renovate/actions-checkout-6-x

Conversation

@renovate
Copy link
Contributor

@renovate renovate bot commented Feb 4, 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: 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.

@renovate renovate bot requested a review from clD11 as a code owner February 4, 2026 09:06
@renovate renovate bot force-pushed the renovate/actions-checkout-6-x branch from 7178559 to 37f88c8 Compare February 21, 2026 14:22
@github-actions
Copy link

[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

This PR introduces several improvements to the actions/checkout GitHub Action for the v6 release:

  1. Worktree credential support: Adds includeIf.gitdir patterns with /worktrees/* glob so that git credentials persist into worktrees created after checkout.
  2. Tag fetching refactored: Moves tag fetching logic from the fetch() method's --no-tags flag toggling into the refspec itself. --no-tags is now always passed, and tags are fetched explicitly via +refs/tags/*:refs/tags/* in the refspec when fetchTags is true.
  3. Ref integrity verification: After fetching, testRef is called to verify the fetched ref still points to the expected commit, guarding against tags/refs that were moved after the workflow was triggered.
  4. Annotated tag dereferencing fix: testRef now uses ^{commit} to dereference annotated tags when comparing commits, fixing false mismatches for annotated tags.
  5. Orchestration ID in user-agent: Appends ACTIONS_ORCHESTRATION_ID (sanitized) to the git HTTP user-agent string for telemetry.
  6. Version bumps: All internal workflow references updated from v4/v4.1.6 to v6, README examples updated, and CHANGELOG casing normalized.

Possible Issues

  • Ref verification may be too strict in shallow clones: After a shallow fetch with fetchDepth > 0, testRef calls git.revParse(ref + "^{commit}"). If the ref or its dereferenced commit isn't in the shallow history (e.g., for annotated tags whose tag object was fetched but the commit is beyond the depth), this could fail unexpectedly. The error message suggests the ref "may have been updated" which would be misleading in this case.
  • Tag refspec change for refs/tags/ + commit: When a specific tag ref is provided with a commit SHA, the old code used +${commit}:${ref} (fetching the commit and writing it as the tag ref), while the new code uses +${ref}:${ref} (fetching the tag by name). This is a semantic change — it fetches whatever the remote tag currently points to rather than the specific commit. The post-fetch testRef check should catch mismatches, but it means the fetch itself may bring in a different commit than intended before the verification step.
  • Empty refspec when fetchTags is true and ref is a tag with commit: When fetchTags=true and the ref is refs/tags/X with a commit, the specific tag refspec is skipped (since fetchTags already adds +refs/tags/*:refs/tags/*). This is correct but relies on the wildcard fetch succeeding — if the tag doesn't exist on the remote, the fetch won't fail (it just won't match), and the subsequent testRef will catch it.

Security Hotspots

  1. Ref integrity after fetch (src/git-source-provider.ts): The new testRef verification is a security improvement — it prevents silently checking out a moved tag. However, there is a TOCTOU (time-of-check-time-of-use) gap between testRef and the actual git checkout that follows. In practice this is minimal since the data is already local, but worth noting.
  2. Orchestration ID sanitization (src/git-command-manager.ts): The sanitization regex [^a-z0-9_.-] is applied to the env var before embedding it in the user-agent string. This looks adequate — it replaces any unexpected characters with underscores. However, extremely long orchestration IDs are not truncated, which could theoretically cause issues with HTTP header length limits.
Changes

Changes

  • .github/workflows/*.yml: All self-references updated from actions/checkout@v4 / @v4.1.6 to @v6.
  • CHANGELOG.md: Version heading casing normalized from V to v.
  • README.md: All example references updated to @v6; "What's new" section rewritten for v6.
  • __test__/verify-fetch-tags.sh (new): Shell script to verify tags are present after fetch-tags: true checkout.
  • __test__/verify-worktree.sh (new): Shell script that creates a worktree and verifies credentials propagate to it.
  • __test__/git-command-manager.test.ts: Tests updated to remove fetchTags option from fetch; tags now appear in refSpec. New test suite for orchestration ID user-agent behavior.
  • __test__/ref-helper.test.ts: New tests for getRefSpec with fetchTags=true.
  • src/git-auth-helper.ts: Adds worktree-aware includeIf.gitdir patterns for both host and container git directories.
  • src/git-command-manager.ts: Removes fetchTags from fetch options, always passes --no-tags. Adds orchestration ID to git user-agent.
  • src/git-source-provider.ts: Passes fetchTags to getRefSpec instead of fetch. Adds post-fetch ref verification.
  • src/ref-helper.ts: getRefSpec accepts optional fetchTags parameter; tags fetched via refspec. testRef uses ^{commit} for annotated tag dereferencing.
  • src/misc/generate-docs.ts: Updated version string to v6.
  • dist/index.js: Compiled bundle reflecting all source changes.
sequenceDiagram
    participant Runner as GitHub Runner
    participant Action as checkout action
    participant Git as git CLI
    participant Remote as GitHub Remote

    Runner->>Action: Start checkout (ref, commit, fetchTags)
    Action->>Git: createCommandManager()
    Git-->>Action: git version (+ set user-agent with orchId)
    
    Action->>Git: init()
    Action->>Git: remoteAdd(origin, url)
    
    alt persist-credentials
        Action->>Git: config(includeIf.gitdir:...path)
        Action->>Git: config(includeIf.gitdir:.../worktrees/*.path)
        Action->>Git: config(includeIf.gitdir:container...path)
        Action->>Git: config(includeIf.gitdir:container.../worktrees/*.path)
    end

    Action->>Action: getRefSpec(ref, commit, fetchTags)
    Note over Action: If fetchTags, prepend +refs/tags/*:refs/tags/*
    Note over Action: Always pass --no-tags to fetch

    Action->>Git: fetch(refSpec, {fetchDepth, ...})
    Git->>Remote: git fetch --no-tags --prune ... refSpecs
    Remote-->>Git: objects + refs

    Action->>Git: testRef(ref, commit)
    Note over Git: For tags: revParse(ref^{commit})
    Git-->>Action: match? true/false

    alt ref mismatch
        Action-->>Runner: Error: ref moved after trigger
    else ref matches
        Action->>Git: checkout(ref, startPoint)
        Git-->>Action: working tree updated
        Action-->>Runner: Checkout complete
    end
Loading

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.

0 participants