In order to create this library, we will start out by following the initial steps of the nbdev tutorial to create a basic project structure. We also need to install ghapi, by following the instructions on that link.

Once we have a repo cloned, based on the nbdev_template template, we run in our terminal:

gh-create-workflow tweet release --contexts secrets

Note that we add --contexts secrets because we'll need access to the secrets context, which we'll be using to store our Twitter API keys.

The basic workflow skeleton that this creates is:

.github/workflows/tweet-release.yml
.github/scripts/build-tweet-release.py

Setting up Twitter authentication

In order to send tweets, we'll send to use the Twitter API -- we'll use the tweepy library for this:

import tweepy

You'll need a Twitter API login. If you need to send Tweets on behalf of a different user to the one attached to your login, you'll need to authenticate with Twitter as that user. Run tweetrel-auth in your terminal and follow the instructions to get those access tokens.

auth[source]

auth()

Function for tweetrel-auth CLI command

The Twitter part of this tutorial isn't the main thing we want to explain, but the thing to note carefully here is how to access secrets in your workflow.

The Twitter API requires authentication, so we'll store our details in a GitHub secret. We'll just use one secret, we our keys stored space delimited. We generally use an organization secret, so that we can update secrets in one place as needed.

Contexts are passed to ghapi as json-encoded environment variables. Each variable name starts with CONTEXT_ -- so for instance the secrets context is CONTEXT_SECRETS. In order to simulate this when testing, ensure that you have set an appropriate environment variable before importing ghapi. We'll put our JSON encoded secrets for testing into a .secrets file (which we add to .gitignore so it won't be pushed to our repo). We put the four parts (consumer_key, consumer_secret, access_token, and access_token_secret) into a single space delimited secret.

os.environ['CONTEXT_SECRETS'] = Path('.secrets').read_text()

Now we can import ghapi.

from ghapi.all import *

If you now check the context_secrets variable, you should find your secrets available as an AttrDict:

assert 'TWITTER' in context_secrets

Now we'll create a function to unpack our keys, tokens, and secrets for Twitter auth, and login to the API:

twitter_api[source]

twitter_api()

def twitter_api():
    consumer_key,consumer_secret,access_token,access_token_secret = context_secrets.TWITTER.split()
    auth = tweepy.OAuthHandler(consumer_key,consumer_secret)
    auth.set_access_token(access_token,access_token_secret)
    return tweepy.API(auth)

We can test our login by creating and deleting a tweet:

import time
twapi = twitter_api()
stat = twapi.update_status("Please ignore - testing API")
time.sleep(1)
twapi.destroy_status(stat.id)

Responding to the release event

We can get a sample release event payload as follows:

example_rel = example_payload(Event.release)
list(example_rel), example_rel.action
(['action', 'release', 'repository', 'sender'], 'published')

The release section has the following keys:

', '.join(example_rel.release)
'url, assets_url, upload_url, html_url, id, node_id, tag_name, target_commitish, name, draft, author, prerelease, created_at, published_at, assets, tarball_url, zipball_url, body'

We can create a function that formats a tweet based on this information, which will take the template from an environment variable if available, otherwise will use a reasonable default:

tweet_text[source]

tweet_text(payload)

def tweet_text(payload):
    if 'workflow' in payload: payload = example_rel
    def_tmpl = "New #{repo} release: v{tag_name}. {html_url}\n\n{body}"
    tweet_tmpl = os.getenv('TWEETREL_TEMPLATE', def_tmpl)
    rel = payload.release
    owner,repo = payload.repository.full_name.split('/')
    res = tweet_tmpl.format(repo=repo, tag_name=rel.tag_name, html_url=rel.html_url, body=rel.body)
    return res if len(res)<=280 else (res[:279] + "…")

...and test it:

print(tweet_text(example_rel))
New #Hello-World release: v0.0.1. https://github.com/Codertocat/Hello-World/releases/tag/0.0.1

None

The sample release payload from GitHub happens to have an empty body, but other than that, this looks good.

That's all we need to create our function:

send_tweet[source]

send_tweet()

def send_tweet():
    payload = context_github.event
    if 'workflow' in payload: payload = example_payload(Event.release)
    if payload.action == 'published': return twitter_api().update_status(tweet_text(payload))

We can pop this into our python script, along with send_tweet(). Since we're using nbdev, we can enter the following in our terminal to do all that:

nbdev_build_lib
cp tweetrel/core.py .github/scripts/build-tweet-release.py
echo -e "\nsend_tweet()" >>  .github/scripts/build-tweet-release.py

After we push to GitHub, we can test it out in the same way we showed in the GitHub Actions tutorial, first by logging in and getting a reference to our workflow:

api = GhApi(owner='fastai', repo='tweetrel', token=github_token())
wf = api.actions.get_workflow('tweet-release.yml')

and then running it:

api.actions.create_workflow_dispatch(wf.id, ref='master')

After you run this, you should find the workflow appears in your "Actions" tab on GitHub, and a tweet will appear in your twitter timeline.

Some little improvements

One issue is that currently you'll see three workflows being triggered on the Actions tab in GitHub. That's because the "created" and "released" types are resulting in a trigger, as well as "published". Our function is checking which is being used, and only tweeting for "published", but the runs are still being recorded. This isn't necessarily a big problem, but if you'd like things to be cleaner, you can edit the tweet-release.yml file to add the following line after the release: line:

types: [published]

It's also possible to do a more complete end-to-end test by actually making a release, checking that a new run is created, and checking the result of that run:

n_runs = api.actions.list_workflow_runs(wf.id).total_count
try:
    rel = api.create_release('test', body='body')
    time.sleep(30)
    runs = api.actions.list_workflow_runs(wf.id)
    test(runs.total_count, n_runs, gt)
    test_eq(runs.workflow_runs[0].conclusion, 'success')
finally: api.delete_release(rel)

Distributing your new Action

The above steps are all that's needed to create a Python-based workflow for use in a single project. To make it accessible for any project (including other people's projects), we need to distribute it. The easiest way to do that is with pip. pip can install modules directly from GitHub, or from the PyPi repository.

Our first step is to add a command that makes it easy to install the tweetrel workflow into a project. We can use fastcore.script.call_parse to create a function that can be run from the terminal.

In the function, we'll use fill_workflow_templates instead of gh-create-workflow because it allows us to customize each part of the YAML workflow file exactly as we need it:

install[source]

install()

@call_parse
def install():
    fill_workflow_templates(
        name='tweet', event="release:\n  types: [published]",
        run='pip install -Uq tweetrel',
        context=env_contexts('secrets'),
        script="import tweetrel\ntweetrel.send_tweet()"
    )

In this case, we just used tweetrel in the "pip install" line, which assumes that the project has been uploaded to pypi (which you can do by typing make release in your terminal, if you use nbdev or pypi_template. If you are just installing from GitHub directly, you'd use this instead:

run='pip install -Uq git+https://github.com/fastai/tweetrel.git',

Since install uses call_parse, we can run it as a standard Python function, so we can test it by executing the function:

install()

Alternatively, we can run it through the CLI, by first adding to our settings.ini:

console_scripts = tweetrel-install=tweetrel.core:install

...and then installing our library locally:

pip install -e .

...and finaly running the command:

tweetrel-install

If this all works, then you can run make release to submit your action to pypi and conda, making it available for anyone to use in their workflows.

Conclusion

By following these steps, we have created a Python-based GitHub Action, which can be installed using pip or conda, and can be added to any repo with a single command. See the overview page for this module to see how we document this for end users of the action.