1
0
mirror of https://github.com/actions/checkout.git synced 2026-03-07 08:51:46 +08:00

Compare commits

...

7 Commits

Author SHA1 Message Date
Theomegas1
704c543acf
Merge 8ec026fcc5 into 0c366fd6a8 2026-01-10 01:41:19 +05:30
eric sciple
0c366fd6a8
Update changelog (#2357) 2026-01-09 14:09:42 -06:00
eric sciple
de0fac2e45
Fix tag handling: preserve annotations and explicit fetch-tags (#2356)
This PR fixes several issues with tag handling in the checkout action:

1. fetch-tags: true now works (fixes #1471)
   - Tags refspec is now included in getRefSpec() when fetchTags=true
   - Previously tags were only fetched during a separate fetch that was
     overwritten by the main fetch

2. Tag checkout preserves annotations (fixes #290)
   - Tags are fetched via refspec (+refs/tags/*:refs/tags/*) instead of
     --tags flag
   - This fetches the actual tag objects, preserving annotations

3. Tag checkout with fetch-tags: true no longer fails (fixes #1467)
   - When checking out a tag with fetchTags=true, only the wildcard
     refspec is used (specific tag refspec is redundant)

Changes:
- src/ref-helper.ts: getRefSpec() now accepts fetchTags parameter and
  prepends tags refspec when true
- src/git-command-manager.ts: fetch() simplified to always use --no-tags,
  tags are fetched explicitly via refspec
- src/git-source-provider.ts: passes fetchTags to getRefSpec()
- Added E2E test for fetch-tags option

Related #1471, #1467, #290
2026-01-09 13:42:23 -06:00
Copilot
064fe7f331
Add orchestration_id to git user-agent when ACTIONS_ORCHESTRATION_ID is set (#2355)
* Initial plan

* Add orchestration ID support to git user-agent

Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>

* Apply suggestion from @Copilot

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

* Improve tests to verify user-agent content and handle empty sanitized IDs

Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>

* Simplify orchestration ID validation to accept any non-empty sanitized value

Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>

* Remove test for orchestration ID with only invalid characters

Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>
Co-authored-by: Tingluo Huang <tingluohuang@github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-08 15:07:38 -05:00
Theomegas1
8ec026fcc5
Createmikes codeql.yml 2025-09-24 09:43:27 -04:00
Theomegas1
524dcd0a11
Create release,tagset
.net,react
2025-09-04 21:33:51 -04:00
Theomegas1
3ee49d2c6b
Create generator-generic-ossf-slsa3-publish.yml
tech sav
2025-09-04 17:48:46 -04:00
12 changed files with 820 additions and 97 deletions

234
.github/workflows/codeql.yml vendored Normal file
View File

@ -0,0 +1,234 @@
- name: Setup Java JDK
uses: actions/setup-java@v5.0.0
with:
# The Java version to set up. Takes a whole or semver Java version. See examples of supported syntax in README file
java-version: # optional
# The path to the `.java-version` file. See examples of supported syntax in README file
java-version-file: # optional
# Java distribution. See the list of supported distributions in README file
distribution:
# The package type (jdk, jre, jdk+fx, jre+fx)
java-package: # optional, default is jdk
# The architecture of the package (defaults to the action runner's architecture)
architecture: # optional
# Path to where the compressed JDK is located
jdkFile: # optional
# Set this option if you want the action to check for the latest available version that satisfies the version spec
check-latest: # optional
# ID of the distributionManagement repository in the pom.xml file. Default is `github`
server-id: # optional, default is github
# Environment variable name for the username for authentication to the Apache Maven repository. Default is $GITHUB_ACTOR
server-username: # optional, default is GITHUB_ACTOR
# Environment variable name for password or token for authentication to the Apache Maven repository. Default is $GITHUB_TOKEN
server-password: # optional, default is GITHUB_TOKEN
# Path to where the settings.xml file will be written. Default is ~/.m2.
settings-path: # optional
# Overwrite the settings.xml file if it exists. Default is "true".
overwrite-settings: # optional, default is true
# GPG private key to import. Default is empty string.
gpg-private-key: # optional
# Environment variable name for the GPG private key passphrase. Default is $GPG_PASSPHRASE.
gpg-passphrase: # optional
# Name of the build platform to cache dependencies. It can be "maven", "gradle" or "sbt".
cache: # optional
# The path to a dependency file: pom.xml, build.gradle, build.sbt, etc. This option can be used with the `cache` option. If this option is omitted, the action searches for the dependency file in the entire repository. This option supports wildcards and a list of file names for caching multiple dependencies.
cache-dependency-path: # optional
# Workaround to pass job status to post job step. This variable is not intended for manual setting
job-status: # optional, default is ${{ job.status }}
# The token used to authenticate when fetching version manifests hosted on github.com, such as for the Microsoft Build of OpenJDK. When running this action on github.com, the default value is sufficient. When running on GHES, you can pass a personal access token for github.com if you are experiencing rate limiting.
token: # optional, default is ${{ github.server_url == 'https://github.com' && github.token || '' }}
# Name of Maven Toolchain ID if the default name of "${distribution}_${java-version}" is not wanted. See examples of supported syntax in Advanced Usage file
mvn-toolchain-id: # optional
# Name of Maven Toolchain Vendor if the default name of "${distribution}" is not wanted. See examples of supported syntax in Advanced Usage file
mvn-toolchain-vendor: # optional
- name: Setup Java JDK
uses: actions/setup-java@v5.0.0
with:
# The Java version to set up. Takes a whole or semver Java version. See examples of supported syntax in README file
java-version: # optional
# The path to the `.java-version` file. See examples of supported syntax in README file
java-version-file: # optional
# Java distribution. See the list of supported distributions in README file
distribution:
# The package type (jdk, jre, jdk+fx, jre+fx)
java-package: # optional, default is jdk
# The architecture of the package (defaults to the action runner's architecture)
architecture: # optional
# Path to where the compressed JDK is located
jdkFile: # optional
# Set this option if you want the action to check for the latest available version that satisfies the version spec
check-latest: # optional
# ID of the distributionManagement repository in the pom.xml file. Default is `github`
server-id: # optional, default is github
# Environment variable name for the username for authentication to the Apache Maven repository. Default is $GITHUB_ACTOR
server-username: # optional, default is GITHUB_ACTOR
# Environment variable name for password or token for authentication to the Apache Maven repository. Default is $GITHUB_TOKEN
server-password: # optional, default is GITHUB_TOKEN
# Path to where the settings.xml file will be written. Default is ~/.m2.
settings-path: # optional
# Overwrite the settings.xml file if it exists. Default is "true".
overwrite-settings: # optional, default is true
# GPG private key to import. Default is empty string.
gpg-private-key: # optional
# Environment variable name for the GPG private key passphrase. Default is $GPG_PASSPHRASE.
gpg-passphrase: # optional
# Name of the build platform to cache dependencies. It can be "maven", "gradle" or "sbt".
cache: # optional
# The path to a dependency file: pom.xml, build.gradle, build.sbt, etc. This option can be used with the `cache` option. If this option is omitted, the action searches for the dependency file in the entire repository. This option supports wildcards and a list of file names for caching multiple dependencies.
cache-dependency-path: # optional
# Workaround to pass job status to post job step. This variable is not intended for manual setting
job-status: # optional, default is ${{ job.status }}
# The token used to authenticate when fetching version manifests hosted on github.com, such as for the Microsoft Build of OpenJDK. When running this action on github.com, the default value is sufficient. When running on GHES, you can pass a personal access token for github.com if you are experiencing rate limiting.
token: # optional, default is ${{ github.server_url == 'https://github.com' && github.token || '' }}
# Name of Maven Toolchain ID if the default name of "${distribution}_${java-version}" is not wanted. See examples of supported syntax in Advanced Usage file
mvn-toolchain-id: # optional
# Name of Maven Toolchain Vendor if the default name of "${distribution}" is not wanted. See examples of supported syntax in Advanced Usage file
mvn-toolchain-vendor: # optional
- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v5.0.0
with:
# Optional SDK version(s) to use. If not provided, will install global.json version when available. Examples: 2.2.104, 3.1, 3.1.x, 3.x, 6.0.2xx
dotnet-version: # optional
# Optional quality of the build. The possible values are: daily, signed, validated, preview, ga.
dotnet-quality: # optional
# Optional global.json location, if your global.json isn't located in the root of the repo.
global-json-file: # optional
# Optional package source for which to set up authentication. Will consult any existing NuGet.config in the root of the repo and provide a temporary NuGet.config using the NUGET_AUTH_TOKEN environment variable as a ClearTextPassword
source-url: # optional
# Optional OWNER for using packages from GitHub Package Registry organizations/users other than the current repository's owner. Only used if a GPR URL is also provided in source-url
owner: # optional
# Optional NuGet.config location, if your NuGet.config isn't located in the root of the repo.
config-file: # optional
# Optional input to enable caching of the NuGet global-packages folder
cache: # optional
# Used to specify the path to a dependency file: packages.lock.json. Supports wildcards or a list of file names for caching multiple dependencies.
cache-dependency-path: # optional
- name: Custom Models Action
# You may pin to the exact commit or the version.
# uses: datarobot-oss/custom-models-action@0a5d55659824973261d6b49389569dd4e28795bf
uses: datarobot-oss/custom-models-action@v1.6.0
with:
# DataRobot authentication API token.
api-token:
# DataRobot frontend web server.
webserver:
# The branch for which pull request and push events will trigger the action.
branch:
# Determines the namespace under which models and deployments will be created, updated and
deleted.
namespace: # optional
# Whether to detected local deleted model definitions and consequently delete them
in DataRobot.
allow-model-deletion: # optional, default is false
# Whether to detect local deleted deployment definitions and consequently delete them
in DataRobot.
allow-deployment-deletion: # optional, default is false
# Whether to handle custom inference models only, without deployments..
models-only: # optional, default is false
# Whether a request to an HTTPS URL will be made without a certificate verification.
skip-cert-verification: # optional, default is false
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
schedule:
- cron: '24 19 * * 1'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
- language: javascript-typescript
build-mode: none
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node`
# or others). This is typically only required for manual builds.
# - name: Setup runtime (example)
# uses: actions/setup-example@v1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

View File

@ -0,0 +1,105 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow lets you generate SLSA provenance file for your project.
# The generation satisfies level 3 for the provenance requirements - see https://slsa.dev/spec/v0.1/requirements
# The project is an initiative of the OpenSSF (openssf.org) and is developed at
# https://github.com/slsa-framework/slsa-github-generator.
# The provenance file can be verified using https://github.com/slsa-framework/slsa-verifier.
# For more information about SLSA and how it improves the supply-chain, visit slsa.dev.
name: SLSA generic generator
on:
workflow_dispatch:
release:
types: [created]
jobs:
build:
runs-on: ubuntu-latest
outputs:
digests: ${{ steps.hash.outputs.digests }}
steps:
- uses: actions/checkout@v4
# ========================================================
#
# Step 1: Build your artifacts.
#
# ========================================================
- name: Build artifacts
run: |
# These are some amazing artifacts.
echo "artifact1" > artifact1
echo "artifact2" > artifact2
# ========================================================
#
# Step 2: Add a step to generate the provenance subjects
# as shown below. Update the sha256 sum arguments
# to include all binaries that you generate
# provenance for.
#
# ========================================================
- name: Generate subject for provenance
id: hash
run: |
set -euo pipefail
# List the artifacts the provenance will refer to.
files=$(ls artifact*)
# Generate the subjects (base64 encoded).
echo "hashes=$(sha256sum $files | base64 -w0)" >> "${GITHUB_OUTPUT}"
provenance:
needs: [build]
permissions: - name: Deploy HelmFile
# You may pin to the exact commit or the version.
# uses: cloudposse/github-action-deploy-helmfile@fcc0ea83519505047bd34a4e017f1d0c3516a5cc
uses: cloudposse/github-action-deploy-helmfile@0.7.0
with:
# Cluster name
cluster:
# AWS region
aws-region: # optional, default is us-east-1
# The path where lives the helmfile.
helmfile-path: # optional, default is deploy
# Helmfile name
helmfile: # optional, default is helmfile.yaml
# Operation with helmfiles. (valid options - `deploy`, `destroy`)
operation: # default is deploy
# Helmfile environment
environment: # optional, default is preview
# Git SHA
gitref-sha: # optional, default is
# Kubernetes namespace
namespace:
# Docker image
image:
# Docker image tag
image-tag:
# Debug mode
debug: # optional, default is false
# The name of the label used to describe the helm release
release_label_name: # optional, default is release
# YAML string with extra values to use in a helmfile deploy
values_yaml: # optional
# Helm version
helm_version: # optional, default is 3.11.1
# Helmfile version
helmfile_version: # optional, default is 0.143.5
# Kubectl version
kubectl_version: # optional, default is 1.26.3
# Kubectl version
chamber_version: # optional, default is 2.11.1
actions: read # To read the workflow path.
id-token: write # To sign the provenance.
contents: write # To add assets to a release.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.4.0
with:
base64-subjects: "${{ needs.build.outputs.digests }}"
upload-assets: true # Optional: Upload to a new release

View File

@ -87,6 +87,17 @@ jobs:
- name: Verify fetch filter - name: Verify fetch filter
run: __test__/verify-fetch-filter.sh 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 # Sparse checkout
- name: Sparse checkout - name: Sparse checkout
uses: ./ uses: ./

View File

@ -1,5 +1,11 @@
# Changelog # Changelog
## v6.0.2
* Fix tag handling: preserve annotations and explicit fetch-tags by @ericsciple in https://github.com/actions/checkout/pull/2356
## v6.0.1
* Add worktree support for persist-credentials includeIf by @ericsciple in https://github.com/actions/checkout/pull/2327
## v6.0.0 ## v6.0.0
* Persist creds to a separate file by @ericsciple in https://github.com/actions/checkout/pull/2286 * 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 * Update README to include Node.js 24 support details and requirements by @salmanmkc in https://github.com/actions/checkout/pull/2248

View File

@ -108,7 +108,7 @@ describe('Test fetchDepth and fetchTags options', () => {
jest.restoreAllMocks() 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) jest.spyOn(exec, 'exec').mockImplementation(mockExec)
const workingDirectory = 'test' const workingDirectory = 'test'
const lfs = false const lfs = false
@ -122,45 +122,7 @@ describe('Test fetchDepth and fetchTags options', () => {
const refSpec = ['refspec1', 'refspec2'] const refSpec = ['refspec1', 'refspec2']
const options = { const options = {
filter: 'filterValue', filter: 'filterValue',
fetchDepth: 0, fetchDepth: 0
fetchTags: true
}
await git.fetch(refSpec, options)
expect(mockExec).toHaveBeenCalledWith(
expect.any(String),
[
'-c',
'protocol.version=2',
'fetch',
'--prune',
'--no-recurse-submodules',
'--filter=filterValue',
'origin',
'refspec1',
'refspec2'
],
expect.any(Object)
)
})
it('should call execGit with the correct arguments when fetchDepth is 0 and fetchTags is false', async () => {
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
const workingDirectory = 'test'
const lfs = false
const doSparseCheckout = false
git = await commandManager.createCommandManager(
workingDirectory,
lfs,
doSparseCheckout
)
const refSpec = ['refspec1', 'refspec2']
const options = {
filter: 'filterValue',
fetchDepth: 0,
fetchTags: false
} }
await git.fetch(refSpec, options) await git.fetch(refSpec, options)
@ -183,7 +145,45 @@ describe('Test fetchDepth and fetchTags options', () => {
) )
}) })
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 0 and refSpec includes tags', async () => {
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
const workingDirectory = 'test'
const lfs = false
const doSparseCheckout = false
git = await commandManager.createCommandManager(
workingDirectory,
lfs,
doSparseCheckout
)
const refSpec = ['refspec1', 'refspec2', '+refs/tags/*:refs/tags/*']
const options = {
filter: 'filterValue',
fetchDepth: 0
}
await git.fetch(refSpec, options)
expect(mockExec).toHaveBeenCalledWith(
expect.any(String),
[
'-c',
'protocol.version=2',
'fetch',
'--no-tags',
'--prune',
'--no-recurse-submodules',
'--filter=filterValue',
'origin',
'refspec1',
'refspec2',
'+refs/tags/*:refs/tags/*'
],
expect.any(Object)
)
})
it('should call execGit with the correct arguments when fetchDepth is 1', async () => {
jest.spyOn(exec, 'exec').mockImplementation(mockExec) jest.spyOn(exec, 'exec').mockImplementation(mockExec)
const workingDirectory = 'test' const workingDirectory = 'test'
@ -197,8 +197,7 @@ describe('Test fetchDepth and fetchTags options', () => {
const refSpec = ['refspec1', 'refspec2'] const refSpec = ['refspec1', 'refspec2']
const options = { const options = {
filter: 'filterValue', filter: 'filterValue',
fetchDepth: 1, fetchDepth: 1
fetchTags: false
} }
await git.fetch(refSpec, options) 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) jest.spyOn(exec, 'exec').mockImplementation(mockExec)
const workingDirectory = 'test' const workingDirectory = 'test'
@ -233,11 +232,10 @@ describe('Test fetchDepth and fetchTags options', () => {
lfs, lfs,
doSparseCheckout doSparseCheckout
) )
const refSpec = ['refspec1', 'refspec2'] const refSpec = ['refspec1', 'refspec2', '+refs/tags/*:refs/tags/*']
const options = { const options = {
filter: 'filterValue', filter: 'filterValue',
fetchDepth: 1, fetchDepth: 1
fetchTags: true
} }
await git.fetch(refSpec, options) await git.fetch(refSpec, options)
@ -248,13 +246,15 @@ describe('Test fetchDepth and fetchTags options', () => {
'-c', '-c',
'protocol.version=2', 'protocol.version=2',
'fetch', 'fetch',
'--no-tags',
'--prune', '--prune',
'--no-recurse-submodules', '--no-recurse-submodules',
'--filter=filterValue', '--filter=filterValue',
'--depth=1', '--depth=1',
'origin', 'origin',
'refspec1', 'refspec1',
'refspec2' 'refspec2',
'+refs/tags/*:refs/tags/*'
], ],
expect.any(Object) 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) jest.spyOn(exec, 'exec').mockImplementation(mockExec)
const workingDirectory = 'test' const workingDirectory = 'test'
@ -349,10 +349,9 @@ describe('Test fetchDepth and fetchTags options', () => {
lfs, lfs,
doSparseCheckout doSparseCheckout
) )
const refSpec = ['refspec1', 'refspec2'] const refSpec = ['refspec1', 'refspec2', '+refs/tags/*:refs/tags/*']
const options = { const options = {
filter: 'filterValue', filter: 'filterValue',
fetchTags: true,
showProgress: true showProgress: true
} }
@ -364,15 +363,134 @@ describe('Test fetchDepth and fetchTags options', () => {
'-c', '-c',
'protocol.version=2', 'protocol.version=2',
'fetch', 'fetch',
'--no-tags',
'--prune', '--prune',
'--no-recurse-submodules', '--no-recurse-submodules',
'--progress', '--progress',
'--filter=filterValue', '--filter=filterValue',
'origin', 'origin',
'refspec1', 'refspec1',
'refspec2' 'refspec2',
'+refs/tags/*:refs/tags/*'
], ],
expect.any(Object) 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)'
)
})
})

View File

@ -152,7 +152,22 @@ describe('ref-helper tests', () => {
it('getRefSpec sha + refs/tags/', async () => { it('getRefSpec sha + refs/tags/', async () => {
const refSpec = refHelper.getRefSpec('refs/tags/my-tag', commit) const refSpec = refHelper.getRefSpec('refs/tags/my-tag', commit)
expect(refSpec.length).toBe(1) 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 () => { 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*') 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 () => { it('getRefSpec refs/heads/ only', async () => {
const refSpec = refHelper.getRefSpec('refs/heads/my/branch', '') const refSpec = refHelper.getRefSpec('refs/heads/my/branch', '')
expect(refSpec.length).toBe(1) expect(refSpec.length).toBe(1)
@ -187,4 +210,21 @@ describe('ref-helper tests', () => {
expect(refSpec.length).toBe(1) expect(refSpec.length).toBe(1)
expect(refSpec[0]).toBe('+refs/tags/my-tag:refs/tags/my-tag') 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'
)
})
}) })

9
__test__/verify-fetch-tags.sh Executable file
View File

@ -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"

80
dist/index.js vendored
View File

@ -653,7 +653,6 @@ const fs = __importStar(__nccwpck_require__(7147));
const fshelper = __importStar(__nccwpck_require__(7219)); const fshelper = __importStar(__nccwpck_require__(7219));
const io = __importStar(__nccwpck_require__(7436)); const io = __importStar(__nccwpck_require__(7436));
const path = __importStar(__nccwpck_require__(1017)); const path = __importStar(__nccwpck_require__(1017));
const refHelper = __importStar(__nccwpck_require__(8601));
const regexpHelper = __importStar(__nccwpck_require__(3120)); const regexpHelper = __importStar(__nccwpck_require__(3120));
const retryHelper = __importStar(__nccwpck_require__(2155)); const retryHelper = __importStar(__nccwpck_require__(2155));
const git_version_1 = __nccwpck_require__(3142); const git_version_1 = __nccwpck_require__(3142);
@ -831,9 +830,9 @@ class GitCommandManager {
fetch(refSpec, options) { fetch(refSpec, options) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const args = ['-c', 'protocol.version=2', 'fetch']; const args = ['-c', 'protocol.version=2', 'fetch'];
if (!refSpec.some(x => x === refHelper.tagsRefSpec) && !options.fetchTags) { // Always use --no-tags for explicit control over tag fetching
args.push('--no-tags'); // Tags are fetched explicitly via refspec when needed
} args.push('--no-tags');
args.push('--prune', '--no-recurse-submodules'); args.push('--prune', '--no-recurse-submodules');
if (options.showProgress) { if (options.showProgress) {
args.push('--progress'); args.push('--progress');
@ -1206,7 +1205,17 @@ class GitCommandManager {
} }
} }
// Set the user agent // 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}`); core.debug(`Set git useragent to: ${gitHttpUserAgent}`);
this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent; this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent;
}); });
@ -1529,13 +1538,26 @@ function getSource(settings) {
if (!(yield refHelper.testRef(git, settings.ref, settings.commit))) { if (!(yield refHelper.testRef(git, settings.ref, settings.commit))) {
refSpec = refHelper.getRefSpec(settings.ref, settings.commit); refSpec = refHelper.getRefSpec(settings.ref, settings.commit);
yield git.fetch(refSpec, fetchOptions); 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 { else {
fetchOptions.fetchDepth = settings.fetchDepth; fetchOptions.fetchDepth = settings.fetchDepth;
fetchOptions.fetchTags = settings.fetchTags; const refSpec = refHelper.getRefSpec(settings.ref, settings.commit, settings.fetchTags);
const refSpec = refHelper.getRefSpec(settings.ref, settings.commit);
yield git.fetch(refSpec, fetchOptions); 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(); core.endGroup();
// Checkout info // Checkout info
@ -2274,53 +2296,67 @@ function getRefSpecForAllHistory(ref, commit) {
} }
return result; return result;
} }
function getRefSpec(ref, commit) { function getRefSpec(ref, commit, fetchTags) {
if (!ref && !commit) { if (!ref && !commit) {
throw new Error('Args ref and commit cannot both be empty'); throw new Error('Args ref and commit cannot both be empty');
} }
const upperRef = (ref || '').toUpperCase(); const upperRef = (ref || '').toUpperCase();
const result = [];
// When fetchTags is true, always include the tags refspec
if (fetchTags) {
result.push(exports.tagsRefSpec);
}
// SHA // SHA
if (commit) { if (commit) {
// refs/heads // refs/heads
if (upperRef.startsWith('REFS/HEADS/')) { if (upperRef.startsWith('REFS/HEADS/')) {
const branch = ref.substring('refs/heads/'.length); const branch = ref.substring('refs/heads/'.length);
return [`+${commit}:refs/remotes/origin/${branch}`]; result.push(`+${commit}:refs/remotes/origin/${branch}`);
} }
// refs/pull/ // refs/pull/
else if (upperRef.startsWith('REFS/PULL/')) { else if (upperRef.startsWith('REFS/PULL/')) {
const branch = ref.substring('refs/pull/'.length); const branch = ref.substring('refs/pull/'.length);
return [`+${commit}:refs/remotes/pull/${branch}`]; result.push(`+${commit}:refs/remotes/pull/${branch}`);
} }
// refs/tags/ // refs/tags/
else if (upperRef.startsWith('REFS/TAGS/')) { else if (upperRef.startsWith('REFS/TAGS/')) {
return [`+${commit}:${ref}`]; if (!fetchTags) {
result.push(`+${ref}:${ref}`);
}
} }
// Otherwise no destination ref // Otherwise no destination ref
else { else {
return [commit]; result.push(commit);
} }
} }
// Unqualified ref, check for a matching branch or tag // Unqualified ref, check for a matching branch or tag
else if (!upperRef.startsWith('REFS/')) { else if (!upperRef.startsWith('REFS/')) {
return [ result.push(`+refs/heads/${ref}*:refs/remotes/origin/${ref}*`);
`+refs/heads/${ref}*:refs/remotes/origin/${ref}*`, if (!fetchTags) {
`+refs/tags/${ref}*:refs/tags/${ref}*` result.push(`+refs/tags/${ref}*:refs/tags/${ref}*`);
]; }
} }
// refs/heads/ // refs/heads/
else if (upperRef.startsWith('REFS/HEADS/')) { else if (upperRef.startsWith('REFS/HEADS/')) {
const branch = ref.substring('refs/heads/'.length); const branch = ref.substring('refs/heads/'.length);
return [`+${ref}:refs/remotes/origin/${branch}`]; result.push(`+${ref}:refs/remotes/origin/${branch}`);
} }
// refs/pull/ // refs/pull/
else if (upperRef.startsWith('REFS/PULL/')) { else if (upperRef.startsWith('REFS/PULL/')) {
const branch = ref.substring('refs/pull/'.length); const branch = ref.substring('refs/pull/'.length);
return [`+${ref}:refs/remotes/pull/${branch}`]; result.push(`+${ref}:refs/remotes/pull/${branch}`);
} }
// refs/tags/ // refs/tags/
else { else if (upperRef.startsWith('REFS/TAGS/')) {
return [`+${ref}:${ref}`]; if (!fetchTags) {
result.push(`+${ref}:${ref}`);
}
} }
// Other refs
else {
result.push(`+${ref}:${ref}`);
}
return result;
} }
/** /**
* Tests whether the initial fetch created the ref at the expected commit * Tests whether the initial fetch created the ref at the expected commit
@ -2356,7 +2392,9 @@ function testRef(git, ref, commit) {
// refs/tags/ // refs/tags/
else if (upperRef.startsWith('REFS/TAGS/')) { else if (upperRef.startsWith('REFS/TAGS/')) {
const tagName = ref.substring('refs/tags/'.length); 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 // Unexpected
else { else {

108
release,tagset Normal file
View File

@ -0,0 +1,108 @@
- uses: actions/checkout@v5
with:
# Repository name with owner. For example, actions/checkout
# Default: ${{ github.repository }}
repository: ''
# The branch, tag or SHA to checkout. When checking out the repository that
# triggered a workflow, this defaults to the reference or SHA for that event.
# Otherwise, uses the default branch.
ref: ''
# Personal access token (PAT) used to fetch the repository. The PAT is configured
# with the local git config, which enables your scripts to run authenticated git
# commands. The post-job step removes the PAT.
#
# We recommend using a service account with the least permissions necessary. Also
# when generating a new PAT, select the least scopes necessary.
#
# [Learn more about creating and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
#
# Default: ${{ github.token }}
token: ''
# SSH key used to fetch the repository. The SSH key is configured with the local
# git config, which enables your scripts to run authenticated git commands. The
# post-job step removes the SSH key.
#
# We recommend using a service account with the least permissions necessary.
#
# [Learn more about creating and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
ssh-key: ''
# Known hosts in addition to the user and global host key database. The public SSH
# keys for a host may be obtained using the utility `ssh-keyscan`. For example,
# `ssh-keyscan github.com`. The public key for github.com is always implicitly
# added.
ssh-known-hosts: ''
# Whether to perform strict host key checking. When true, adds the options
# `StrictHostKeyChecking=yes` and `CheckHostIP=no` to the SSH command line. Use
# the input `ssh-known-hosts` to configure additional hosts.
# Default: true
ssh-strict: ''
# The user to use when connecting to the remote SSH host. By default 'git' is
# used.
# Default: git
ssh-user: ''
# Whether to configure the token or SSH key with the local git config
# Default: true
persist-credentials: ''
# Relative path under $GITHUB_WORKSPACE to place the repository
path: ''
# Whether to execute `git clean -ffdx && git reset --hard HEAD` before fetching
# Default: true
clean: ''
# Partially clone against a given filter. Overrides sparse-checkout if set.
# Default: null
filter: ''
# Do a sparse checkout on given patterns. Each pattern should be separated with
# new lines.
# Default: null
sparse-checkout: ''
# Specifies whether to use cone-mode when doing a sparse checkout.
# Default: true
sparse-checkout-cone-mode: ''
# Number of commits to fetch. 0 indicates all history for all branches and tags.
# Default: 1
fetch-depth: ''
# Whether to fetch tags, even if fetch-depth > 0.
# Default: false
fetch-tags: ''
# Whether to show progress status output when fetching.
# Default: true
show-progress: ''
# Whether to download Git-LFS files
# Default: false
lfs: ''
# Whether to checkout submodules: `true` to checkout submodules or `recursive` to
# recursively checkout submodules.
#
# When the `ssh-key` input is not provided, SSH URLs beginning with
# `git@github.com:` are converted to HTTPS.
#
# Default: false
submodules: ''
# Add repository path as safe.directory for Git global config by running `git
# config --global --add safe.directory <path>`
# Default: true
set-safe-directory: ''
# The base URL for the GitHub instance that you are trying to clone from, will use
# environment defaults to fetch from the same instance that the workflow is
# running from unless specified. Example URLs are https://github.com or
# https://my-ghes-server.example.com
github-server-url: ''

View File

@ -37,7 +37,6 @@ export interface IGitCommandManager {
options: { options: {
filter?: string filter?: string
fetchDepth?: number fetchDepth?: number
fetchTags?: boolean
showProgress?: boolean showProgress?: boolean
} }
): Promise<void> ): Promise<void>
@ -280,14 +279,13 @@ class GitCommandManager {
options: { options: {
filter?: string filter?: string
fetchDepth?: number fetchDepth?: number
fetchTags?: boolean
showProgress?: boolean showProgress?: boolean
} }
): Promise<void> { ): Promise<void> {
const args = ['-c', 'protocol.version=2', 'fetch'] const args = ['-c', 'protocol.version=2', 'fetch']
if (!refSpec.some(x => x === refHelper.tagsRefSpec) && !options.fetchTags) { // Always use --no-tags for explicit control over tag fetching
args.push('--no-tags') // Tags are fetched explicitly via refspec when needed
} args.push('--no-tags')
args.push('--prune', '--no-recurse-submodules') args.push('--prune', '--no-recurse-submodules')
if (options.showProgress) { if (options.showProgress) {
@ -730,7 +728,19 @@ class GitCommandManager {
} }
} }
// Set the user agent // 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}`) core.debug(`Set git useragent to: ${gitHttpUserAgent}`)
this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent
} }

View File

@ -159,7 +159,6 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
const fetchOptions: { const fetchOptions: {
filter?: string filter?: string
fetchDepth?: number fetchDepth?: number
fetchTags?: boolean
showProgress?: boolean showProgress?: boolean
} = {} } = {}
@ -182,12 +181,35 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
if (!(await refHelper.testRef(git, settings.ref, settings.commit))) { if (!(await refHelper.testRef(git, settings.ref, settings.commit))) {
refSpec = refHelper.getRefSpec(settings.ref, settings.commit) refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
await git.fetch(refSpec, fetchOptions) 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 { } else {
fetchOptions.fetchDepth = settings.fetchDepth fetchOptions.fetchDepth = settings.fetchDepth
fetchOptions.fetchTags = settings.fetchTags const refSpec = refHelper.getRefSpec(
const refSpec = refHelper.getRefSpec(settings.ref, settings.commit) settings.ref,
settings.commit,
settings.fetchTags
)
await git.fetch(refSpec, fetchOptions) 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() core.endGroup()

View File

@ -76,55 +76,75 @@ export function getRefSpecForAllHistory(ref: string, commit: string): string[] {
return result return result
} }
export function getRefSpec(ref: string, commit: string): string[] { export function getRefSpec(
ref: string,
commit: string,
fetchTags?: boolean
): string[] {
if (!ref && !commit) { if (!ref && !commit) {
throw new Error('Args ref and commit cannot both be empty') throw new Error('Args ref and commit cannot both be empty')
} }
const upperRef = (ref || '').toUpperCase() const upperRef = (ref || '').toUpperCase()
const result: string[] = []
// When fetchTags is true, always include the tags refspec
if (fetchTags) {
result.push(tagsRefSpec)
}
// SHA // SHA
if (commit) { if (commit) {
// refs/heads // refs/heads
if (upperRef.startsWith('REFS/HEADS/')) { if (upperRef.startsWith('REFS/HEADS/')) {
const branch = ref.substring('refs/heads/'.length) const branch = ref.substring('refs/heads/'.length)
return [`+${commit}:refs/remotes/origin/${branch}`] result.push(`+${commit}:refs/remotes/origin/${branch}`)
} }
// refs/pull/ // refs/pull/
else if (upperRef.startsWith('REFS/PULL/')) { else if (upperRef.startsWith('REFS/PULL/')) {
const branch = ref.substring('refs/pull/'.length) const branch = ref.substring('refs/pull/'.length)
return [`+${commit}:refs/remotes/pull/${branch}`] result.push(`+${commit}:refs/remotes/pull/${branch}`)
} }
// refs/tags/ // refs/tags/
else if (upperRef.startsWith('REFS/TAGS/')) { else if (upperRef.startsWith('REFS/TAGS/')) {
return [`+${commit}:${ref}`] if (!fetchTags) {
result.push(`+${ref}:${ref}`)
}
} }
// Otherwise no destination ref // Otherwise no destination ref
else { else {
return [commit] result.push(commit)
} }
} }
// Unqualified ref, check for a matching branch or tag // Unqualified ref, check for a matching branch or tag
else if (!upperRef.startsWith('REFS/')) { else if (!upperRef.startsWith('REFS/')) {
return [ result.push(`+refs/heads/${ref}*:refs/remotes/origin/${ref}*`)
`+refs/heads/${ref}*:refs/remotes/origin/${ref}*`, if (!fetchTags) {
`+refs/tags/${ref}*:refs/tags/${ref}*` result.push(`+refs/tags/${ref}*:refs/tags/${ref}*`)
] }
} }
// refs/heads/ // refs/heads/
else if (upperRef.startsWith('REFS/HEADS/')) { else if (upperRef.startsWith('REFS/HEADS/')) {
const branch = ref.substring('refs/heads/'.length) const branch = ref.substring('refs/heads/'.length)
return [`+${ref}:refs/remotes/origin/${branch}`] result.push(`+${ref}:refs/remotes/origin/${branch}`)
} }
// refs/pull/ // refs/pull/
else if (upperRef.startsWith('REFS/PULL/')) { else if (upperRef.startsWith('REFS/PULL/')) {
const branch = ref.substring('refs/pull/'.length) const branch = ref.substring('refs/pull/'.length)
return [`+${ref}:refs/remotes/pull/${branch}`] result.push(`+${ref}:refs/remotes/pull/${branch}`)
} }
// refs/tags/ // refs/tags/
else { else if (upperRef.startsWith('REFS/TAGS/')) {
return [`+${ref}:${ref}`] if (!fetchTags) {
result.push(`+${ref}:${ref}`)
}
} }
// Other refs
else {
result.push(`+${ref}:${ref}`)
}
return result
} }
/** /**
@ -170,8 +190,10 @@ export async function testRef(
// refs/tags/ // refs/tags/
else if (upperRef.startsWith('REFS/TAGS/')) { else if (upperRef.startsWith('REFS/TAGS/')) {
const tagName = ref.substring('refs/tags/'.length) const tagName = ref.substring('refs/tags/'.length)
// Use ^{commit} to dereference annotated tags to their underlying commit
return ( return (
(await git.tagExists(tagName)) && commit === (await git.revParse(ref)) (await git.tagExists(tagName)) &&
commit === (await git.revParse(`${ref}^{commit}`))
) )
} }
// Unexpected // Unexpected