#default_exp actions
Functionality for helping to create GitHub Actions workflows in Python
#export
from fastcore.utils import *
from fastcore.script import *
from fastcore.foundation import *
from fastcore.meta import *
from ghapi.core import *
from ghapi.templates import *
import textwrap
from enum import Enum
#hide
from nbdev import *
In your GitHub Actions workflow, include the following in your run
step:
env:
CONTEXT_GITHUB: ${{ toJson(github) }}
This stores the full github context, which includes information such as the name of the current workflow being run, the GitHub access token, and so forth.
As well as the github
context, you can do that same thing for any of the other GitHub Actions contexts, which are:
github
env
job
steps
runner
secrets
strategy
matrix
needs
For instance, for the needs context, information about previous jobs specified in your needs
clause, add this underneath your CONTEXT_GITHUB
line:
CONTEXT_NEEDS: ${{ toJson(needs) }}
Note that here's no harm having entries that are not used -- GitHub Actions will set them to an empty dictionary by default.
#export
# So we can run this outside of GitHub actions too, read from file if needed
for a,b in (('CONTEXT_GITHUB',context_example), ('CONTEXT_NEEDS',needs_example), ('GITHUB_REPOSITORY','octocat/Hello-World')):
if a not in os.environ: os.environ[a] = b
contexts = 'github', 'env', 'job', 'steps', 'runner', 'secrets', 'strategy', 'matrix', 'needs'
for context in contexts:
globals()[f'context_{context}'] = dict2obj(loads(os.getenv(f"CONTEXT_{context.upper()}", "{}")))
#export
_all_ = ['context_github', 'context_env', 'context_job', 'context_steps', 'context_runner', 'context_secrets', 'context_strategy', 'context_matrix', 'context_needs']
GitHub also adds a number of GITHUB_*
environment variables to all runners. These are available through the env_github
AttrDict
, with the GITHUB_
prefix removed, and remainder converted to lowercase. For instance:
#export
env_github = dict2obj({k[7:].lower():v for k,v in os.environ.items() if k.startswith('GITHUB_')})
env_github.repository
'octocat/Hello-World'
#export
def user_repo():
"List of `user,repo` from `env_github.repository"
return env_github.repository.split('/')
user_repo()
['octocat', 'Hello-World']
The possible events are available in the Event
enum
.
#hide
print(','.join(repr(o) for o in Path('examples/').ls(file_exts=['.json']).attrgot('stem')))
'page_build','content_reference','repository_import','create','workflow_run','delete','organization','sponsorship','project_column','push','context','milestone','project_card','project','package','pull_request','repository_dispatch','team_add','workflow_dispatch','member','meta','code_scanning_alert','public','needs','check_run','security_advisory','pull_request_review_comment','org_block','commit_comment','watch','marketplace_purchase','star','installation_repositories','check_suite','github_app_authorization','team','status','repository_vulnerability_alert','pull_request_review','label','installation','release','issues','repository','gollum','membership','deployment','deploy_key','issue_comment','ping','deployment_status','fork'
#export
Event = str_enum('Event',
'page_build','content_reference','repository_import','create','workflow_run','delete','organization','sponsorship',
'project_column','push','context','milestone','project_card','project','package','pull_request','repository_dispatch',
'team_add','workflow_dispatch','member','meta','code_scanning_alert','public','needs','check_run','security_advisory',
'pull_request_review_comment','org_block','commit_comment','watch','marketplace_purchase','star','installation_repositories',
'check_suite','github_app_authorization','team','status','repository_vulnerability_alert','pull_request_review','label',
'installation','release','issues','repository','gollum','membership','deployment','deploy_key','issue_comment','ping',
'deployment_status','fork','schedule')
', '.join(Event)
'page_build, content_reference, repository_import, create, workflow_run, delete, organization, sponsorship, project_column, push, context, milestone, project_card, project, package, pull_request, repository_dispatch, team_add, workflow_dispatch, member, meta, code_scanning_alert, public, needs, check_run, security_advisory, pull_request_review_comment, org_block, commit_comment, watch, marketplace_purchase, star, installation_repositories, check_suite, github_app_authorization, team, status, repository_vulnerability_alert, pull_request_review, label, installation, release, issues, repository, gollum, membership, deployment, deploy_key, issue_comment, ping, deployment_status, fork, schedule'
# export
def _create_file(path:Path, fname:str, contents):
if contents and not (path/fname).exists(): (path/fname).write_text(contents)
def _replace(s:str, find, repl, i:int=0, suf:str=''):
return s.replace(find, textwrap.indent(repl, ' '*i)+suf)
# export
def create_workflow_files(fname:str, workflow:str, build_script:str, prebuild:bool=False):
"Create workflow and script files in suitable places in `github` folder"
if not os.path.exists('.git'): return print('This does not appear to be the root of a git repo')
wf_path = Path('.github/workflows')
scr_path = Path('.github/scripts')
wf_path .mkdir(parents=True, exist_ok=True)
scr_path.mkdir(parents=True, exist_ok=True)
_create_file(wf_path, f'{fname}.yml', workflow)
_create_file(scr_path, f'build-{fname}.py', build_script)
if prebuild: _create_file(scr_path, f'prebuild-{fname}.py', build_script)
# export
def fill_workflow_templates(name:str, event, run, context, script, opersys='ubuntu', prebuild=False):
"Function to create a simple Ubuntu workflow that calls a Python `ghapi` script"
c = wf_tmpl
if event=='workflow_dispatch:': event=''
needs = ' needs: [prebuild]' if prebuild else None
for find,repl,i in (('NAME',name,0), ('EVENT',event,2), ('RUN',run,8), ('CONTEXTS',context,8),
('OPERSYS',f'[{opersys}]',0), ('NEEDS',needs,0), ('PREBUILD',pre_tmpl if prebuild else '',2)):
c = _replace(c, f'${find}', str(repl), i)
create_workflow_files(name, c, script, prebuild=prebuild)
event
is the event to trigger on. run
is the shell lines to run before running the script, such as a pip install
step. context
are the env var context lines to include in the env:
section of the workflow, normally created with env_contexts
. opersys
can be a string containing a comma-separated list of operating systems, e.g. macos, ubuntu, windows
, which will be used to create a parallel matrix build.
The prebuild
bool tells ghapi
to include a prebuild job, which contains the following workflow:
runs-on: ubuntu-latest
outputs:
out: ${{ toJson(steps) }}
steps:
- uses: actions/checkout@v1
- uses: actions/setup-python@v2
with: {python-version: '3.8'}
- name: Create release
id: step1
env:
CONTEXT_GITHUB: ${{ toJson(github) }}
run: |
pip install -q ghapi
python .github/scripts/prebuild.py
#export
def env_contexts(contexts):
"Create a suitable `env:` line for a workflow to make a context available in the environment"
contexts = uniqueify(['github'] + listify(contexts))
return "\n".join("CONTEXT_" + o.upper() + ": ${{ toJson(" + o.lower() + ") }}" for o in contexts)
#export
def_pipinst = 'pip install -Uq ghapi'
# export
def create_workflow(name:str, event:Event, contexts:list=None, opersys='ubuntu', prebuild=False):
"Function to create a simple Ubuntu workflow that calls a Python `ghapi` script"
script = "from fastcore.all import *\nfrom ghapi import *"
fill_workflow_templates(name, f'{event}:', def_pipinst, env_contexts(contexts),
script=script, opersys=opersys, prebuild=prebuild)
create_workflow('test', Event.release)
To create a basic ghapi
workflow, call create_workflow
, passing in the event that you wish to respond to, and a name for your workflow.
#export
@call_parse
def gh_create_workflow(
name:Param("Name of the workflow file", str),
event:Param("Event to listen for", str),
contexts:Param("Space-delimited extra contexts to include in `env` in addition to 'github'", str)=''
):
"Supports `gh-create-workflow`, a CLI wrapper for `create_workflow`."
create_workflow(name, Event[event], contexts.split())
The information from these variables are provided by context_github
, context_needs
, and so forth for each named context. These variables are AttrDict
objects.
L(context_github)
(#26) ['token','job','ref','sha','repository','repository_owner','repositoryUrl','run_id','run_number','retention_days'...]
context_github.ref
'refs/heads/master'
If you use our recommended workflow template, you will have this included in your prebuild step (if you have any):
outputs:
out: ${{ toJson(steps) }}
You can access this content as a dictionary like so:
loads(nested_idx(context_needs, "prebuild", "outputs", "out"))
{'step1': {'outputs': {'tag': 'v0.79.0'}, 'outcome': 'success', 'conclusion': 'success'}}
#export
_example_url = 'https://raw.githubusercontent.com/fastai/ghapi/master/examples/{}.json'
#export
def example_payload(event):
"Get an example of a JSON payload for `event`"
return dict2obj(urljson(_example_url.format(event)))
#export
def github_token():
"Get GitHub token from `GITHUB_TOKEN` env var if available, or from `github` context"
return os.getenv('GITHUB_TOKEN', context_github.get('token', None))
#export
def actions_output(name, value):
"Print the special GitHub Actions `::set-output` line for `name::value`"
print(f"::set-output name={name}::{value}")
Details in the GitHub Documentation for set-output
.
#export
def actions_debug(message):
"Print the special `::debug` line for `message`"
print(f"::debug::{message}")
Details in the GitHub Documentation for debug
. Note that you must create a secret named ACTIONS_STEP_DEBUG
with the value true to see the debug messages set by this command in the log.
#export
def actions_warn(message, details=''):
"Print the special `::warning` line for `message`"
print(f"::warning {details}::{message}")
Details in the GitHub Documentation for warning
. For the optional details
, you can provide comma-delimited file, line, and column information, e.g.: file=app.js,line=1,col=5
.
#export
def actions_error(message, details=''):
"Print the special `::error` line for `message`"
print(f"::error {details}::{message}")
Details in the GitHub Documentation for error
. For the optional details
, you can provide comma-delimited file, line, and column information, e.g.: file=app.js,line=1,col=5
.
#export
def actions_group(title):
"Print the special `::group` line for `title`"
print(f"::group::{title}")
#export
def actions_endgroup():
"Print the special `::endgroup`"
print(f"::endgroup::")
Details in the GitHub Documentation for grouping log lines.
#export
def actions_mask(value):
"Print the special `::add-mask` line for `value`"
print(f"::add-mask::{value}")
Details in the GitHub Documentation for add-mask
.
#export
def set_git_user(api=None):
"Set git user name/email to authenticated user (if `api`) or GitHub Actions bot (otherwise)"
if api:
user = api.users.get_authenticated().name
email = first(api.users.list_emails_for_authenticated(), attrgetter('primary')).email
else:
user = 'github-actions[bot]'
email = 'github-actions[bot]@users.noreply.github.com'
run(f'git config --global user.email "{email}"')
run(f'git config --global user.name "{user}"')
When pushing to git from a workflow, you'll need to set your username and email address. You can set them to the GhApi
authenticated user's details by passing api
. Otherwise, github-actions[bot]
and github-actions[bot]@users.noreply.github.com
will be used, which will make a push appear to be from "GitHub Actions".
#hide
from nbdev.export import notebook2script
notebook2script()
Converted 00_core.ipynb. Converted 01_actions.ipynb. Converted 10_cli.ipynb. Converted 50_fullapi.ipynb. Converted 90_build_lib.ipynb. Converted index.ipynb. Converted tutorial_actions.ipynb.