Cloudy with a Chance of Git Pulls: Automated Weather Forecasts With GitHub Actions

Written by woodrock | Published 2021/02/11
Tech Story Tags: github-actions | nodejs | javascript | weather | api | weather-api-for-web-dev | github | automation | web-monetization

TLDRvia the TL;DR App

This tutorial covers creating a custom GitHub Action to generate automated Weather Forecasts as seen below (and [here]). It relies on the Open Weather API and Nodejs. Once you understand the basic steps involves here, you will be able to apply them to automate all nature of things.
Wellington

18.4°C
Clouds: 75%
Humidity: 77%
Wind: 8.75 m/s
Pressure: 1001hpa

More..
Actions are often an important part of the continuous integration and development process. As developers, we enjoy being as lazy as possible. We can do this, by automating tedious, dull, and repetitive tasks. This reduces the human resources cost and improves the efficiency of your work.
The more you can automate in your day to day life, the more time you have to spend on what is meaningful. My boss once told me that I am the laziest worker he had ever seen, but he said he meant that as the highest form of praise to describe how efficiently I worked. Or perhaps, he has just trying to be nice.
The repository relies on four main files:
  • README.md
  • action.yml
  • action.js
  • weather.yml
We will go into detail about the implementation details of each of those below. But these are the basic components required to build our own custom GitHub action.

Setting us Node

We use
nodejs
to run our action on a virtual server provided by GitHub. For this script to work, we need to create a node project:
Install the necessary packages:
npm i @vercel/ncc node-fetch fs @actions/core
Then append our
packages.json
to include the following script:
"scripts": {
    "build": "ncc build src/action.js -o dist",
    ...
  },
Before pushing this repository to GitHub make sure to include a
.gitignore
file with
node_modules
in at the root level of your directory:
node_modules
This ignores the very large and unused directory
node_modules
, which stores the code for all the libraries we just imported. It is not needed by our GitHub action to run.

README.md

An example of a README file for this action is shown below.
# Weather Forecast
![Weather](https://github.com/woodRock/expert-chainsaw/workflows/Weather/badge.svg)

Tumeric single-origin coffee taiyaki, literally craft beer enamel pin plaid chia direct trade 90's distillery retro vaporware. PBR&B hexagon blue bottle banh mi mumblecore synth, pitchfork actually vegan church-key bicycle rights iceland occupy street art. Normcore disrupt you probably haven't heard of them, crucifix single-origin coffee cornhole listicle. Distillery iPhone enamel pin direct trade venmo quinoa jianbing four loko bespoke unicorn.

## Forecast
<!-- FEED-START -->
<!-- FEED-END -->
It is the page that is displayed to the user when they first visit the URL for a GitHub repo. It uses the Markdown markup language to produce styled text. The Markdown spec even allows us to include HTML snippets (i.e., divs or comments).
It is important to note, that to avoid security vulnerabilities such as XSS (Cross-Site Scripting), GitHub does not allow JavaScript to embedded into their statically hosted README files. We will come back to why this is important soon.
At the top of this file, we have a title,
# Weather Forecast
, this is similar to the
<h1>
tag from HTML, where the number of hashes
#
denotes the weight of the header (i.e.,
##
equals
<h2>
,
###
equals
<h3>
, etc ... ). Also worth noting, [Hipster Ipsum](https://hipsum.co/) can be used to create amusing placeholder text as seen above.
We then have an embedded Action's workflow badge. These can be generated by clicking the
Actions
tab on the repository. Selecting a workflow, in our case
weather
. Expanding the three dots
...
option in the top right, and clicking
create status badge
. This automatically generates the embedded HTML necessary to be added to our Markdown file. Programmers love these badges, and adorn them to their repositories as badges of honour, so welcome to the club!
We then have a secondary header for our weather forecast
## Forecast
, followed by an HTML comment. This comment is hidden when viewing the README on the URL, but it is there when we edit the document. Notice the tags
FEED-START
and
FEED-END
. This let our
action.js
script (to be discussed later) know where we want to append our weather forecast. We use these tags so that we do not overwrite the existing content of the README, each time a new forecast is given.

action.yml

This is the YAML file for our action. It describes the necessary meta-data, such that we can publish it to the GitHub market place, and the inputs that are required for the action to function.
name: "Weather Forecast Action"
description: "Display weather forecast for any area in a project README"
author: "woodrock"

inputs:
  GITHUB_TOKEN:
    description: "GitHub token"
    required: true
  OPEN_WEATHER_TOKEN:
    description: "Open Weather API Token"
    required: true
  CITY:
    description: "City to get the forecast for"
    required: true

runs:
  using: "node12"
  main: "dist/index.js"
Here we have three inputs:
  • GITHUB_TOKEN
  • OPEN_WEATHER_TOKEN
  • CITY
These are environment variables that are used by our scripts. The first two provide access to the APIs, so we can generate the necessary content. The
GITHUB_TOKEN
is automatically generated by the repository, so we do not need to worry about that.
The
OPEN_WEATHER_TOKEN
is an API key from the Open Weather API. You will need to register an account for this site, should you wish to recreate this action. Once registered, click on your username on the top right of the navigation bar, then select 'My API Keys'. You can either use the default key provided by your account or create an isolated key, to be used specifically by this application. Copy that key into your clipboard.
Now we need to create a GitHub secret. This is an environment variable stored securely by GitHub so that we don't expose sensitive information, such as our API Key, to the public. Return to the repository. Click
Settings
. On the navigation bar to the left, select
secrets
. Select
New Repository Secret
, call it
OPEN_WEATHER_TOKEN
and copy and paste your Open Weather API key as its value.
We specify the city in the
weather.yml
. This is included so that this action can be reused for different locations. Now, it can be used to forecast the weather anywhere in the world, as opposed to hard-coding a city value in our
action.js
. Generic solutions in the software are more maintainable as they encourage re-use without the need to refactor.

action.js

The script that performs the work is shown below. It is located in the
/src
directory.
const fetch = require("node-fetch");
const fs = require("fs");
const core = require("@actions/core");

const FILE_NAME = "./README.md";
const ENCODING = "utf8";
const TAG_OPEN = `<!-- FEED-START -->`;
const TAG_CLOSE = `<!-- FEED-END -->`;

async function fetchWeather() {
  const API_KEY = core.getInput("OPEN_WEATHER_TOKEN");
  const city = core.getInput("CITY");
  const URL = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${API_KEY}&mode=html`;
  return fetch(URL)
    .then((response) => response.text())
    .then((html) => html);
}

async function run() {
  const readme = fs.readFileSync(FILE_NAME, ENCODING);
  const indexBefore = readme.indexOf(TAG_OPEN) + TAG_OPEN.length;
  const indexAfter = readme.indexOf(TAG_CLOSE);
  const before = readme.substring(0, indexBefore);
  const after = readme.substring(indexAfter);
  const input = (await fetchWeather()).replace(/<script.*>.*<\/script>/ims, "");
  const edited = `
${before}
${input}
${after}`;
  fs.writeFileSync(FILE_NAME, edited.trim());
}

try {
  run();
} catch (error) {
  console.log(error);
}
At the top, we import all the packages we need. We use
fetch
to make an API call using javascript asynchronous functions.
fs
allows us to perform FileIO operations, such as reading and writing to a
README.md
.
@actions/core
allows us to request access to the repository secrets, our
OPEN_WEATHER_TOKEN
, and the
CITY
environment variable.
The
fetchWeather()
method requests the Open Weather API token and performs an API call using
fetch()
. We use string templating to structure a custom call to this API:
We specify the city and the API key and request that the response from the API in an HTML format so that we can simply embed this into our
README
. The weather API returns an HTML object, that includes
<script>
tags to neatly render the response.
However, as mentioned previously, we cannot embed JavaScript into a static
README
hosted by GitHub. So instead, we strip the HTML of the
<script>
tags and the content inside them. This is done using the
replace()
method:
await fetchWeather().replace(/<script.*>.*<\/script>/ims, "")
Then we perform some arithmetic, to find where our
FEED-START
and
FEED-END
tags are, and append the embedded weather forecast within these tags. Without overwriting any of the existing document outside these tags.
This script must be built by running the following command in your terminal:
npm run build
This ensures that our action a compiled version of our current program.

weather.yml

This is the GitHub Workflow that runs our script. It is located in the
.github/workflows
directory.
name: "Weather"

on:
  workflow_dispatch:
  schedule:
    - cron: "*/30 * * * *" # Runs every 30 minutes

jobs:
  update_blogs:
    name: "Update Blogs"
    runs-on: ubuntu-latest
    steps:
      - name: "📥 Fetching Repository Contents"
        uses: actions/checkout@main
      - name: "🌨️ Fetching Weather Forecast"
        uses: ./
        with:
          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
          OPEN_WEATHER_TOKEN: ${{secrets.OPEN_WEATHER_TOKEN}}
          CITY: "Wellington, New Zealand"
      - name: "🛠️ Push changes to GitHub"
        run:
            git config --global user.email [email protected] && git config --global user.name readme-bot;
            git diff --quiet && git diff --staged --quiet || git commit -am '[BOT] Update Readme' && git push;
We use the
workflow_dispatch
so we can trigger our script to be run automatically at certain intervals. We specify that interval using cron. This is a time-based job scheduler from Unix machines. Here
*/30 * * * *
we set the workflow to be run once every 30 minutes, every hour, every month, every day of the week.
Our workflow has three jobs. The first uses
actions/checkout@main
to clone our repository to the virtual machine running the workflow. The second run our script using the
node12
docker image we specified in the
action.yml
. With the main script being
dist/action.js
. We provide access repository secretes, the
GITHUB_TOKEN
and
OPEN_WEATHER_TOKEN
, under the
with
section. We can specify the
CITY
of our choice here, in this case, I specify my home city
Wellington, New Zealand
.
Finally, we push our changes to the repository. The workflow is set to have a GitHub user with the name
readme-bot
. This makes it clear that updates to the
README
in our repository are being done by a service account, not an actual user. This is an important distinction, for separating intentional changes by users, from automated ones by our GitHub Action. To reinforce this idea we include
[BOT]
in the commit message as well.

Conclusion

And we are done. This is how you build an automated weather forecasting bot using GitHub Actions. The principles learned here can be applied to any other API of your choice; here is a list just to name a few, the possibilities are endless. After completing this tutorial, you should feel comfortable implementing APIs of your own. Each free GitHub account has access to 2,000 Workflow minutes, so don't let them go to waste.

Appendix

To see the whole thing in action, please check out the original repository, feel free to fork this, and build your amazing Actions.

Written by woodrock | Leave the world a better place you found it. Software Engineer, PhD student in AI
Published by HackerNoon on 2021/02/11