sab
sab

Reputation: 5022

URL of the last artifact of a GitHub-action build

I use GitHub-action for my build, and it generates multiple artifacts (with a different name).

Is there a way to predict the URL of the artifacts of the last successful build? Without knowing the sha1, only the name of the artifact and the repo?

Upvotes: 26

Views: 13904

Answers (5)

ErikMD
ErikMD

Reputation: 14733

Wrap-up

I recently faced with a similar use case, with the aim to make the build artifacts of a given GitHub Actions workflow more visible, with a single click in the commit statuses (albeit requiring users to be logged in github.com).

As pointed out by @geoff-hutchison in his answer:

However:

  • It is impossible to query the list of artifacts generated by a workflow from a job of the current workflow; deferring the request in a subsequent workflow is needed.
  • The archive_download_url URLs contained in the corresponding JSON response do not seem practical enough (they are GitHub API URLs redirecting to ephemeral URLs).

Fortunately:

  • Browsing the workflow build artifacts URLs already proposed in the GitHub Actions page clearly shows that they have the form https://github.com/${{ github.repository }}/suites/${check_suite_id}/artifacts/${artifact_id}, and these URLs are static (albeit only available for logged users, and for 90 days maximum, given the expiration of artifacts).

Implementation

Hence the following, generic code I developed (under MIT license) to pin all artifacts of a given workflow in commit statuses (just replace the 3 TODO-strings):

.github/workflows/pin-artifacts.yml:

name: Pin artifacts
on:
  workflow_run:
    workflows:
      - "TODO-Name Of Existing Workflow"
    types: ["completed"]
jobs:
  # Make artifacts links more visible for the upstream project
  pin-artifacts:
    permissions:
      statuses: write
    name: Add artifacts links to commit statuses
    if: ${{ github.event.workflow_run.conclusion == 'success' && github.repository == 'TODO-orga/TODO-repo' }}
    runs-on: ubuntu-latest
    steps:
      - name: Add artifacts links to commit status
        run: |
          set -x
          workflow_id=${{ github.event.workflow_run.workflow_id }}
          run_id=${{ github.event.workflow_run.id }}  # instead of ${{ github.run_id }}
          run_number=${{ github.event.workflow_run.run_number }}
          head_branch=${{ github.event.workflow_run.head_branch }}
          head_sha=${{ github.event.workflow_run.head_sha }}  # instead of ${{ github.event.pull_request.head.sha }} (or ${{ github.sha }})
          check_suite_id=${{ github.event.workflow_run.check_suite_id }}
          set +x

          curl \
          -H "Accept: application/vnd.github+json" \
          "https://api.github.com/repos/${{ github.repository }}/actions/runs/${run_id}/artifacts" \
          | jq '[.artifacts | .[] | {"id": .id, "name": .name, "created_at": .created_at, "expires_at": .expires_at, "archive_download_url": .archive_download_url}] | sort_by(.name)' \
          > artifacts.json

          cat artifacts.json

          < artifacts.json jq -r ".[] | \
            .name + \"§\" + \
            ( .id | tostring | \"https://github.com/${{ github.repository }}/suites/${check_suite_id}/artifacts/\" + . ) + \"§\" + \
            ( \"Link to \" + .name + \".zip [\" + ( .created_at | sub(\"T.*\"; \"→\") ) + ( .expires_at | sub(\"T.*\"; \"] (you must be logged)\" ) ) )" \
          | while IFS="§" read name url descr; do
              curl \
              -X POST \
              -H "Accept: application/vnd.github+json" \
              -H "Authorization: Bearer ${{ github.token }}" \
              "https://api.github.com/repos/${{ github.repository }}/statuses/${head_sha}" \
              -d "$( printf '{"state":"%s","target_url":"%s","description":"%s","context":"%s"}' "${{ github.event.workflow_run.conclusion }}" "$url" "$descr" "$name (artifact)" )"
          done

(If need be, see also my PR ocaml-sf/learn-ocaml#501 to see an implementation example and screenshots of this GHA workflow.)

Upvotes: 2

prabushi samarakoon
prabushi samarakoon

Reputation: 549

You can use the jq command-line JSON processor along with curl to extract the URL as follows.

curl -s https://api.github.com/repos/<OWNER>/<REPO_NAME>/actions/artifacts\?per_page\=<NUMBER_OF_ARTIFACTS_PER_BUILD> | jq '[.artifacts[] | {name : .name, archive_download_url : .archive_download_url}]' | jq -r '.[] | select (.name == "<NAME_OF_THE_ARTIFACT>") | .archive_download_url'

For example;

curl -s https://api.github.com/repos/ballerina-platform/ballerina-distribution/actions/artifacts\?per_page\=9 | jq '[.artifacts[] | {name : .name, archive_download_url : .archive_download_url}]' | jq -r '.[] | select (.name == "Linux Installer deb") | .archive_download_url'

Here, curl -s https://api.github.com/repos/<OWNER>/<REPO_NAME>/actions/artifacts\?per_page\=<NUMBER_OF_ARTIFACTS_PER_BUILD> returns the array of artifacts related to the latest build.

jq '[.artifacts[] | {name : .name, archive_download_url : .archive_download_url}]' extracts the artifacts array and filters required data.

jq -r '.[] | select (.name == "<NAME_OF_THE_ARTIFACT>") | .archive_download_url' selects the url for the given artifact name.

Upvotes: 5

benaja
benaja

Reputation: 147

I am not a GitHub and jq guru. Probably there are more optimal solutions out there.

jq playground link: https://jqplay.org/s/Gm0kRcv63C - to test my solution and other possible ideas. I dropped some irrelevant field to shrink the sample JSON size (for example: node_id, size_in_bytes, created_at...) Further details on the methods in the code samples below.

####### You can get the max date of your artifacts.
####### Then you need to choose the artifact entry by this date.
#######
####### NOTE: I just pre-formatted the first command "line".
####### 2nd "line" has a very similar, but simplified structure.
####### (at least easy to copy-paste into jq playground)

####### NOTE: ASSUMPTION:
####### First "page" of json response contains the most recent entries
#######   AND includes artifact(s) with that specific name.
#######
####### (if other artifacts flood your API response, you can increase
#######  the page size of it or do iteration on pages as a workaround)

bash$ cat artifact_response.json | \
  jq '
    (
      [
        .artifacts[]
        | select(.name == "my-artifact" and .expired == false)
        | .updated_at
      ]
      | max
    ) as $max_date
    | { $max_date }'

####### output
{ "max_date": "2021-04-29T11:22:20Z" }

Another way:

####### Latest ID of non-expired artifacts with a specific name.
####### Probably this is better solution than the above since you
####### can use the "id" instantly in your download url construction:
#######
####### "https://api.github.com/repos/blabla.../actions/artifacts/92394368/zip"
#######
####### ASSUMPTION: higher "id" means higher "update date" in your workflow
####### (there is no post-update of artifacts aka creation and
#######  update dates are identical for an artifact)


cat artifact_response.json | \
jq '[ .artifacts[] | select(.name == "my-artifact" and .expired == false) | .id ] | max'

####### output
92394368

More compact filter assuming in reverse order by date or id in the API response:

####### no full command line, just jq filter string
#######
####### no "max" function, only pick the first element by index
#######
'[ .artifacts[] | select(.name == "my-artifact" and .expired == false) | .id ][0]'

Upvotes: 0

Oleh Prypin
Oleh Prypin

Reputation: 34116

I have developed a service that exposes predictable URLs to either the latest or a particular artifact of a repository's branch+workflow.

https://nightly.link/
https://github.com/oprypin/nightly.link

This is implemented as a GitHub App, and communication with GitHub is authenticated, but users that only download don't need to even log in to GitHub.


The implementation goes and fetches this through the API, in 3 steps:

  • https://api.github.com/repos/:owner/:repo/actions/workflows/someworkflow.yml/runs?per_page=1&branch=master&event=push&status=success
  • https://api.github.com/repos/:owner/:repo/actions/runs/123456789/artifacts?per_page=100
  • https://api.github.com/repos/:owner/:repo/actions/artifacts/87654321/zip

(the last one redirects you to an ephemeral direct download URL)

Note that authentication is required. For OAuth that's public_repo (or repo if appropriate). For GitHub Apps that's "Actions"/"Read-only".


There is indeed no more direct way to do this.

Some relevant issues are

Upvotes: 16

Geoff Hutchison
Geoff Hutchison

Reputation: 454

At the moment, no, according to comments from staff although this may change with future versions of the upload-artifact action.

After poking around myself, it is possible to get this using the GitHub actions API: https://developer.github.com/v3/actions/artifacts/

GET /repos/:owner/:repo/actions/runs/:run_id/artifacts

So you can receive a JSON reply and iterate through the "artifacts" array to get the corresponding "archive_download_url". A workflow can fill in the URL like so:

/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts

Upvotes: 8

Related Questions