Movatterモバイル変換


[0]ホーム

URL:


Fetching Authenticated API Data with GitHub Actions

Published 5 years ago
8 min read
12,456 views

Overview

I've been running a lot lately. I'm training for my first ever marathon and so far It's been tough. I'm proud of how far I've come in such a short period of time. It was only in October of 2019 that I didmy first half marathon in Baltimore and before then I didn't run or workout at all.

A picture of merunning

I track all of my runs on a website calledStrava and thought it would be kind of cool to show my latest run on my website. In order to make an API request to Strava you must provide an access token with the request. These access tokens expire every six hours and a new one can be requested using theoauth/token endpoint. This is not ideal for me for a few reasons:

I needed to find a way to securely request this data. Sure I could create anAWS Lambda or something similar but that seemed like more hassle than it was worth.

Enter GitHub Actions

I have some familiarity already with GitHub Actions, and thought this project would be a great use-case for them as I didn't care how up-to-date the data is. If it's a couple hours behind that's no big deal.

GitHub Actions like other CI tools out there allows you to run some code onany event that GitHub supports such as pushing, pull requests, tagging and even on a schedule. I thought a great way to retrieve this data would be to run a job every day that saves the running data as a.json file. I could then combine this workflow with theGitHub Pages Deploy Action that I built to push that data into my base branch. Once that base branch detects a change it automatically re-builds the page that ingests the.json file.

Building the Action

The action itself is going to be quite simple. It's going to make aPOST request for the token, and then afterwards aGET request to get the running data. At its core theTypeScript action code looked something like the following.

import{ getInput}from'@actions/core'// Retrieves the tokens for the data request.exportasyncfunctionretrieveTokens():Promise<object>{const configuration=JSON.parse(getInput('CONFIGURATION'))if(configuration.body){    configuration.body=JSON.stringify(configuration.body)}const response=awaitfetch('https://strava.com/api/v3/oauth/token',    configuration,)returnawait response.json()}// Retrieves the dataexportasyncfunctionretrieveData({  action,}:ActionInterface):Promise<object>{try{const tokens=awaitretrieveTokens(action)const response=awaitfetch('https://www.strava.com/api/v3/athlete/activities',{headers:{Authorization:`Bearer${tokens.access_token}`,},},)const data=await response.json()await fs.writeFile('fetch-api-data-action/data.json', data,'utf8')}catch(error){thrownewError(`There was an error fetching from the API:${error}`)}}

The function call togetInput allows you to retrieve data passed through thewith parameterwhen invoking an action in a workflow. These configurations are great because you can store secrets in the repositoriesSettings/Secrets menu and replace parts in a string with the${{ secrets.secret_name }} syntax. GitHub will automatically hide these strings if they get exposed in your logs.

name: Refresh Feedon:schedule:-cron: 25 17 * * 0-6jobs:refresh-feed:runs-on: ubuntu-lateststeps:-name: Fetch API Data 📦uses: JamesIves/fetch-api-data-action@releases/v1with:CONFIGURATION:'{ "method": "POST", "body": {"client_id": "${{ secrets.client_id }}", "client_secret": "${{ secrets.client_secret }}"} }'

With that said and done I was able to retrieve my running data on a scheduled job without exposing my credentials. But why stop there?

Making the Action Reusable

With some minor re-factors I figured this action could be used to retrieve data fromjust about any REST API. In order to achieve that I'm going to need to figure out how to get the following information:

I made some changes to my workflow configuration. TheTOKEN_ENDPOINT request with its accompanying configuration is now made first (if specified) followed by theENDPOINT data request. The only thing left to figure out is how to get the access token into theathlete/activities configuration.

name: Refresh Feedon:schedule:-cron: 25 17 * * 0-6jobs:refresh-feed:runs-on: ubuntu-lateststeps:-name: Fetch API Data 📦uses: JamesIves/fetch-api-data-action@releases/v1with:TOKEN_ENDPOINT: https://www.strava.com/api/v3/oauth/tokenTOKEN_CONFIGURATION:'{ "method": "POST", "body": {"client_id": "${{ secrets.client_id }}", "client_secret": "${{ secrets.client_secret }}"} }'ENDPOINT: https://www.strava.com/api/v3/athlete/activitiesCONFIGURATION:'...'

In order to get the keys from the first request in the secondI decided to use mustache.js to replace strings formatted with triple brackets. In my workflow configuration I can now use triple brackets with keys from the token request to populate the data request.

import{ render}from'mustache'const tokens={data:{access_token:'123',},}const formatted=render('{ "method": "GET", "headers": {"Authorization": "Bearer {{{ data.access_token }}}"} }',  tokens,)console.log(formatted)// '{ "method": "GET", "headers": {"Authorization": "Bearer 123"} }'

With this setup the workflow can look something like this:

name: Refresh Feedon:schedule:-cron: 25 17 * * 0-6jobs:refresh-feed:runs-on: ubuntu-lateststeps:-name: Fetch API Data 📦uses: JamesIves/fetch-api-data-action@releases/v1with:# The token endpoint is requested first. This retrieves the access token for the other endpoint.TOKEN_ENDPOINT: https://www.strava.com/api/v3/oauth/token# The configuration contains secrets held in the Settings/Secrets menu of the repository.TOKEN_CONFIGURATION:'{ "method": "POST", "body": {"client_id": "${{ secrets.client_id }}", "client_secret": "${{ secrets.client_secret }}"} }'# Once the token endpoint has fetched then this endpoint is requested.ENDPOINT: https://www.strava.com/api/v3/athlete/activities# The bearer token here is returned from the TOKEN_ENDPOINT call. The returned data looks like so: {data: {access_token: '123'}}, meaning it can be accessed using the triple bracket syntax.CONFIGURATION:'{ "method": "GET", "headers": {"Authorization": "Bearer {{{ data.access_token }}}"} }'

Looking good!

Using the Action

In order to use the action on my own site all I needed to do was create afeed.yml file in the.github/workflows directory. Thisworkflow is setup to download the data on a schedule from the Strava API, and then it uses theGitHub Pages Deploy action to push the data into the correct folder in my repository.

name: Refresh Feedon:schedule:-cron: 10 15 * * 0-6jobs:refresh-feed:runs-on: ubuntu-lateststeps:-name: Checkout 🛎️uses: actions/checkout@v2with:persist-credentials:false-name: Fetch API Data 📦uses: JamesIves/fetch-api-data-action@releases/v1with:TOKEN_ENDPOINT: https://www.strava.com/api/v3/oauth/tokenTOKEN_CONFIGURATION:'{ "method": "POST", "body": {"client_id": "${{ secrets.client_id }}", "client_secret": "${{ secrets.client_secret }}"} }'ENDPOINT: https://www.strava.com/api/v3/athlete/activitiesCONFIGURATION:'{ "method": "GET", "headers": {"Authorization": "Bearer {{{ data.access_token }}}"} }'SAVE_NAME: strava-name: Build and Deploy 🚀uses: JamesIves/github-pages-deploy-action@releases/v3with:ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN}}BRANCH: main# Pushes the updates to the main branch.FOLDER: fetch-api-data-action# The location of the data.json file saved by the Fetch API Data action.TARGET_FOLDER: src/data# Saves the data into the 'data' directory on the master branch.

I have another workflow setup calledbuild.yml that builds and deploys my page whenever a push is made to the master branch. This means whenever thefeed.yml workflow finishes running it will trigger the next one, making the updates automatic.

name: Build and Deployon:branches:- masterjobs:build-and-deploy:runs-on: ubuntu-lateststeps:-name: Checkout 🛎️uses: actions/checkout@v2with:persist-credentials:false-name: Install 🔧run:|          npm install          npm run-script build-name: Build and Deploy 🚀uses: JamesIves/github-pages-deploy-action@releases/v3with:ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN}}BRANCH: gh-pagesFOLDER: build

I can then use the data for my website by simply importing it. Depending on your project structure you may need to useWebpack or something similar to load the.json file type.

importdatafrom'./images/blog/2020-03-07-fetching-authenticated-api-data/data/strava.json'const latestRun= data.filter((item)=> item.type==='Run')[0]

Hackathon

This was my entry to the2020 GitHub Actions Hackathon, andit's available for you to use on the GitHub marketplace. I had a ton of fun making this and I hope other people find it equally as useful. If you encounter any problems using itplease create an issue.

RunnersMedal

Edit: Since the creation of this post I completed my first marathon!

Related Reading

Prev
Analyzing My Google Music Library
Next
Programming an Elevator

[8]ページ先頭

©2009-2025 Movatter.jp