You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
gh run download doesn't work as expected with name #11610
When I try to download an artifact with gh run download -n my-artifact, command hangs.
I ran with GH_DEBUG=api and the I see in first batch of https://api.github.com/repositories/XXXXXXX/actions/artifacts?per_page=100&page=1 my artifact the good name BUT cli doesn't return it. It continues to iterate.
My artifact is the latest and not expired, if I specify run-id it works but that's not my use case.
We have 259634 artifacts currently but mainly expired (retention days: 1)
As you've already figured, this is due to the large number of artifacts and gh iterating over all of them. Note that the API endpoint that gh uses returns all artifacts regardless of their expiry.
I looked into the code and have added notes about potential fix at the end. However, I don't think this is a bug, but rather this can be considered as an enhancement to run download. So, I'll relabel this and keep the issue open for internal team discussion. I'll update you here.
A workaround for your situation is to directly call the API endpoint like this:
This will print the download URL for unexpired artifacts.
Notes on potential implementation
The API endpoint to fetch the list of artifacts supports a name query parameter to narrow down the lookup. One idea could be gh populating the name parameter if --name is provided. However, the gotcha is, unlike the --name option, the name query parameter does not support multiple values. So, if there are multiple names, gh has to repeat the API call per --name entries.
I cannot say this is a good approach, but it's going to help with such situations.
Thank you for your issue! We have categorized it as a feature request, and it has been added to our backlog. In doing so, we are not committing to implementing this feature at this time, but, we will consider it for future releases based on community feedback and our own product roadmap.
Unless you see the
help wantedContributions welcome
label, we are not currently looking for external contributions for this feature.
If you come across this issue and would like to see it implemented, please add a thumbs up! This will help us prioritize the feature. Please only comment if you have additional information or viewpoints to contribute.
@babakks thanks for the workaround! might be a dumb question but how would we refactor your suggested workaround to actually download the artifact(s), like the original command of gh run download -n my-artifact is looking to do?
you've got the artifact dload urls, assuming they are private, do we need to curl it with the GITHUB_TOKEN passed as a header? or is there a different suggested approach?
Thanks so much @babakks, working well. Saving about ~40s in total (16,333 artifacts in the repo I was benchmarking in).
Just for posterity for anyone stumbling upon this issue, here was the full implementation in a github actions workflow environment:
Context
We build and generate a bundle report named output.txt on merges to main.
This is uploaded as an artifact, with a name of build-output.
On PR's, we want to download that build report, and compare to the PR's build report to highlight any anomalies
Implementation
My implementation only grabs the first non-expired url, not all of them, so take note of that difference in the jq query
Set the response to a variable so we can then pass to curl
- name: Retrieve latest bundle statsshell: bashenv:
GH_TOKEN: ${{ github.token }}run: | artifact_download_url=$(gh api '/repos/${{ github.repository }}/actions/artifacts?name=build-output' --jq '.artifacts | map(select(.expired == false)) | .[0].archive_download_url') curl -L -H "Authorization: Bearer $GH_TOKEN" -o output.zip "$artifact_download_url" unzip -o output.zip # You're left with output.txt in the working directory. Delete the zip file if your heart desires.
With error handling and logging
- name: Retrieve latest main bundle statsshell: bashenv:
GH_TOKEN: ${{ github.token }}run: | set -e echo "Fetching latest build stats artifact from ${{ github.repository }}..." artifact_download_url=$(gh api '/repos/${{ github.repository }}/actions/artifacts?name=build-output' --jq '.artifacts | map(select(.expired == false)) | .[0].archive_download_url') if [ "$artifact_download_url" == "null" ] || [ -z "$artifact_download_url" ]; then echo "❌ Error: No valid build-output artifact found for ${{ github.repository }}" echo "This could mean:" echo " - No previous builds have run on the main branch" echo " - All artifacts have expired (artifacts expire after 90 days by default)" echo " - The artifact name doesn't match 'build-output'" exit 1 fi echo "Found artifact, downloading from: $artifact_download_url" # Use a temporary netrc file to avoid exposing token in process list NETRC_FILE=$(mktemp) trap "rm -f $NETRC_FILE" EXIT echo "machine api.github.com login token password $GH_TOKEN" > "$NETRC_FILE" chmod 600 "$NETRC_FILE" if ! curl -L --netrc-file "$NETRC_FILE" -o output.zip "$artifact_download_url"; then echo "❌ Error: Failed to download artifact" exit 1 fi echo "Extracting artifact..." if ! unzip -o build-output.zip; then echo "❌ Error: Failed to extract artifact" exit 1 fi
By default, this command downloads the latest artifact created and uploaded through GitHub Actions. Because workflows can delete or overwrite artifacts, <run-id> must be used to select an artifact from a specific workflow run.
Appears to be really problematic documentation. It should at the very least call out that unless you use the run-id, potentially every artifact will be searched and this may be expensive (as it could conceivably run through your api quota).
Activity
babakks commentedon Oct 9, 2025
Thanks for the issue, @prousso! 🙏
As you've already figured, this is due to the large number of artifacts and
ghiterating over all of them. Note that the API endpoint thatghuses returns all artifacts regardless of their expiry.I looked into the code and have added notes about potential fix at the end. However, I don't think this is a bug, but rather this can be considered as an enhancement to
run download. So, I'll relabel this and keep the issue open for internal team discussion. I'll update you here.A workaround for your situation is to directly call the API endpoint like this:
This will print the download URL for unexpired artifacts.
Notes on potential implementation
The API endpoint to fetch the list of artifacts supports a
namequery parameter to narrow down the lookup. One idea could beghpopulating thenameparameter if--nameis provided. However, the gotcha is, unlike the--nameoption, thenamequery parameter does not support multiple values. So, if there are multiple names,ghhas to repeat the API call per--nameentries.I cannot say this is a good approach, but it's going to help with such situations.
github-actions commentedon Oct 9, 2025
Thank you for your issue! We have categorized it as a feature request, and it has been added to our backlog. In doing so, we are not committing to implementing this feature at this time, but, we will consider it for future releases based on community feedback and our own product roadmap.
Unless you see the help wantedContributions welcome
label, we are not currently looking for external contributions for this feature.
If you come across this issue and would like to see it implemented, please add a thumbs up! This will help us prioritize the feature. Please only comment if you have additional information or viewpoints to contribute.
codfish commentedon Oct 15, 2025
@babakks thanks for the workaround! might be a dumb question but how would we refactor your suggested workaround to actually download the artifact(s), like the original command of
gh run download -n my-artifactis looking to do?you've got the artifact dload urls, assuming they are private, do we need to curl it with the
GITHUB_TOKENpassed as a header? or is there a different suggested approach?babakks commentedon Oct 16, 2025
No worries, and yeah, you can use
curlwithAuthorizationheader and enabling follow redirects (-L).or, if you want to use
ghto provide the token (helpful when running on local):codfish commentedon Oct 16, 2025
Thanks so much @babakks, working well. Saving about ~40s in total (16,333 artifacts in the repo I was benchmarking in).
Just for posterity for anyone stumbling upon this issue, here was the full implementation in a github actions workflow environment:
Context
output.txton merges tomain.build-output.Implementation
jqqueryWith error handling and logging
babakks commentedon Oct 17, 2025
Just beware that
artifact_download_urlcan contain more than one URL (per unexpired artifacts).codfish commentedon Oct 17, 2025
@babakks yes good call. I had to add the map around select and .[0] before the property in order to just always grab the latest one
jsoref commentedon Jan 7, 2026
gh run downloaddoesn't download latest artifact from latest attempt #12437Appears to be really problematic documentation. It should at the very least call out that unless you use the run-id, potentially every artifact will be searched and this may be expensive (as it could conceivably run through your api quota).