General GitHub Notes & GitHub Actions concepts and setup reference
Last Updated: October 15, 2020 by Pepe Sandoval
If you find the information in this page useful and want to show your support, you can make a donation
Use PayPal
This will help me create more stuff and fix the existent content...
Disclaimer
a runner is any machine with the GitHub actions runner application installed
GitHub Workflows are written in YAML
{}
""
around keys or values, only needed if a value has spaces-
every dash is a new element or with []
separated by commas>
for long values to suppress new lines or |
to keep themname: Pepe active: True age: 20 array: - item1 - item2 - key: value - key2: value2 key3: value3 arrayjsonformat: [item1, item2, key: value] address: country: Malta city: Valletta long_text: > this is the long text
An Artifact in the GitHub Actions context is a file or log generated by a job, by default a job produces the artifact called log archive
your_repo/.github/workflows/your_file.yaml
name
on
jobs
. jobs will be key-value pairs where the key will be the name of the job and the value will be an object that sets the different options for the job, the steps
option is used to specify the steps of the job, it must be a list where each element is an object with the name
and run
keysname: Shell Commands on: [push] jobs: run-shell-commands: runs-on: ubuntu-latest steps: - name: echo a string run: echo "Hello World" - name: multiline commands run: | node -v npm -v - name: python cmd shell: python run: | import platform print(platform.processor())
ACTIONS_STEP_DEBUG
& ACTIONS_RUNNER_DEBUG
to true
as mentioned in the Enabling debug logging GitHub Documentationshell
option.It just need to be specified a the same indentation level of the other jobs and by default they will jobs run in parallel
If we want to specify a sequence for the jobs you can set the needs
option in the job which can be a list of the jobs (names of jobs) the current job depends on and it will only run if the jobs in these dependencies list finish successfully
name: Shell Commands on: [push] jobs: run-shell-commands: runs-on: ubuntu-latest steps: - name: echo a string run: echo "Hello World" - name: multiline commands run: | node -v npm -v - name: python cmd shell: python run: | import platform print(platform.processor()) run-windows-commands: runs-on: windows-latest needs: [run-shell-commands] steps: - name: print dir PS run: Get-Location - name: print dir BASH run: pwd shell: bash - name: python cmd shell: python run: | import platform print(platform.processor())
An Action is a script or code that can be written in JavaScript to perform some specific task, which then you execute in as a step in your workflow. This script is also called sometimes action file and can live in the same repo or in an external repo
user/repo@branch,version or commit
In a step we use the uses
option to specify reference to that action, the with
option to specify inputs and then we will need to access the .outputs
field of the step to access the outputs an action produces
name: Actions Workflow on: [push] jobs: run-github-actions: runs-on: ubuntu-latest steps: - name: Simple JS action id: greet uses: actions/hello-world-javascript-action@v1 with: who-to-greet: Pepe - name: Log Greeting output run: echo "Pepe ${{ steps.greet.outputs.time }}"
When a workflow is run on a GitHub Runner by default a folder is set up on the virtual machine the runner created, we refer to this folder as the Working Directory, for example /home/runner/work/github-actions-test/github-actions-test
but by default the runner will NOT clone the repo
The most common way to clone our repo in a workflow is to use the GitHub checkout action which is an official GitHub action (this just means it is an action created by GitHub), this action can take as input a reference ref
that can be branch, tag or SHA of the commit we want to use
If you want to clone the repo without using the checkout action you will need to use environment variables and data objects that are setup automatically by GitHub
$GITHUB_SHA
this is the commit ID of the commit that triggered the workflow$GITHUB_REPOSITORY
this is user name and repo name. e.x. sanpepe/github-actions-test
$GITHUB_WORKSPACE
this is the directory create by the runner for the workflow${{ github.token }}
token to auth with your repo, this will be hidden in the logs if you print it for security reasonsname: Actions Workflow on: [push] jobs: run-github-actions: runs-on: ubuntu-latest steps: - name: Checkout Repo uses: actions/checkout@v1 - name: List after checkout run: | pwd ls -al echo $GITHUB_SHA echo $GITHUB_REPOSITORY echo $GITHUB_WORKSPACE echo "Token = ${{ github.token }}" - name: Simple JS action id: greet uses: actions/hello-world-javascript-action@v1 with: who-to-greet: Pepe - name: Log Greeting output run: echo "Pepe ${{ steps.greet.outputs.time }}"
We can use the on
to define we want to run a workflow on events like push
, pull_request
, pull_request_review
, fork
, etc. (Webhook events that can trigger a workflow)
Some events can have Activity Types which are just a more specific type of event, to set these we need to use the types
array
Events use env variables that are set differently depending on the events that triggered the workflow, for example on a push and on a pull_request the GITHUB_REF
env variable is used to know what commit will be used, for push will be the latest of the branch we are pushing and for the pull request it will be the branch that requested the pull request (the one that wants to be merged)
name: Actions Workflow on: push: pull_request: types: [closed, assigned, opened, reopened] ...
To trigger on schedule we can use a cron
schedule expression and the schedule
option which is an array of objects with key cron
and value must be a cron
(expression but the minimum is every 5 minutes)
A cron
expression consist of 5 things: minutes hours day_of_month month day_of_week
each can be a number or symbol (*
means any value). Examples:
* * * * *
means run every minute of every hours of every day...1 * * * *
means run on minute 1 of every hour so 12:01
, 13:01
, 14:01
, etc0 20 * * *
means run at 20:00
(8pm) every dayname: Actions Workflow on: schedule: - cron: "10 * * * *" ...
repository_dispatch
the repository_dispatch
is an event that can be triggered manually by sending a post request to the GitHub API URL in format https://api.github.com/repos/<owner>/<repo_name>/dispatches
for example: https://api.github.com/repos/sanpepe/github-actions-test/dispatches
To be able to use the repository_dispatch
feature you will need to send a token to auth the POST request, for that go to Settings -> Developer Settings -> Personal access tokens, click on Generate new token, give it a name on the Note section, select repo scope, finally click on the generate button and copy the token
name: Actions Workflow on: push: repository_dispatch: types: build ...
POST example using curl, you must get an 204
response:
curl -w "%{http_code}" --user sanpepe:--header "Content-Type: application/json" --request POST --data '{"event_type":"build"}' https://api.github.com/repos/sanpepe/github-actions-test/dispatches
We can filter by branches, tags or paths to be more specific about when a workflow should run, for example if you only want to run when there is a push to a certain branch. To do this you need to add filter options to the events on the on
option
You can use <filter>-ignore
(branches-ignore
, tags-ignore
, paths-ignore
) on an event to signal you want to ignore those but you cannot have both <filter>
(branches
, tags
, paths
) and <filter>-ignore
if you want to ignore a filter can also use the !
with the <filter>
option. So for example you cannot use branches
and branches-ignore
at the same time but you can have a pattern branch with !
to signal it will be ignored
name: Actions Workflow on: push: branches: - master - 'feature/*' # matches feature/feat1, feature/feat2, etc - '!feature/featA' # ignores feature/featA tags: - v1 - v2.* path: - '**.py' # match push changes on python files pull_request: branches: - master ...
To set environment variables you can use the env
option which should be an object where key is the name of the env variable and the value will be the actual value of the env variable
If env
option is set at the top level of the YAML file the variables will be available for all jobs, if you want variables specific to a job or step they need to be set at the env
key but at the job or step level
name: Env Variables on: push env: WF_ENV: Available to all jobs jobs: log-env: runs-on: ubuntu-latest env: JOB_ENV: Available to all steps in log-env Job steps: - name: Log env Variables env: STEP_ENV: Available to only this step run: | echo "WF_ENV: ${WF_ENV}" echo "JOB_ENV: ${JOB_ENV}" echo "STEP_ENV: ${STEP_ENV}" - name: Log env Variables Reloaded run: | echo "WF_ENV: ${WF_ENV}" echo "JOB_ENV: ${JOB_ENV}" echo "STEP_ENV: ${STEP_ENV}" log-default-env: runs-on: ubuntu-latest steps: - name: Log Default env Variables run: | echo "HOME: ${HOME}" echo "GITHUB_WORKFLOW: ${GITHUB_WORKFLOW}" echo "GITHUB_ACTION: ${GITHUB_ACTION}" echo "GITHUB_ACTIONS: ${GITHUB_ACTIONS}" echo "GITHUB_ACTOR: ${GITHUB_ACTOR}" echo "GITHUB_REPOSITORY: ${GITHUB_REPOSITORY}" echo "GITHUB_EVENT_NAME: ${GITHUB_EVENT_NAME}" echo "GITHUB_WORKSPACE: ${GITHUB_WORKSPACE}" echo "GITHUB_SHA: ${GITHUB_SHA}" echo "GITHUB_REF: ${GITHUB_REF}" echo "WF_ENV: ${WF_ENV}" echo "JOB_ENV: ${JOB_ENV}" echo "STEP_ENV: ${STEP_ENV}"
secrets
object. Example:name: Secret Env Variables on: push env: MY_SECRET_ENV: ${{ secrets.MY_SECRET_ENV }}
${{ secrets.GITHUB_TOKEN }}
if you want to auth with GitHub API or use it for other operations that require auth to GitHub. The most common usage of this token is to give access to third party apps, scripts or actions to your repo. Examples:actions/labeler
which can automatically add labels to pull request but in order to do that it needs access to create labels in your reponame: ENV Variables on: push env: WF_ENV: Available to all jobs jobs: push_random_file: runs-on: ubuntu-latest steps: - name: Push a random file run: | pwd ls -a git init git remote add origin "https://$GITHUB_ACTOR:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY.git" git config --global user.email "my-bot@bot.com" git config --global user.name "my-bot" git fetch git checkout master git branch --set-upstream-to=origin/master git pull ls -a echo $RANDOM >> random.txt ls -a git add -A git commit -m"Random file" git push ...
You cannot trigger a workflow when you are using the
${{ secrets.GITHUB_TOKEN }}
this is the reason pushing from a workflow won't cause an endless loop of workflows running forever
gpg --symmetric --cipher-algo AES256 my_secret.json
), push the encrypted version, then create a GitHub secret with the passphrase and use it in your workflow to unencrypt the file (for example gpg --quiet --batch --yes --decrypt --passphrase="$PASSPHRASE" --output $HOME/my_secret.json my_secret.json.gpg
name: ENV Variables on: push jobs: decrypt: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Decrypt env: PASSPHRASE: ${{ secrets.PASSPHRASE }} run: gpg --quiet --batch --yes --decrypt --passphrase="$PASSPHRASE" --output $HOME/my_secret.json my_secret.json.gpg - name: Print our file content run: cat $HOME/my_secret.json
Context and expression syntax for GitHub Actions Documentation
The ${{}}
syntax is used to write what is called an expression in a workflow, an expression is something that needs to be evaluated, so besides access to and object's values you can have things like comparisons ("a" == "a"
) or functions (toJson()
)
Objects in GitHub are also called context and provide information about your workflow
name: Context workflow on: [push] jobs: dump-context: runs-on: ubuntu-latest steps: - name: Dump GitHub context env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - name: Dump steps context env: STEPS_CONTEXT: ${{ toJson(steps) }} run: echo "$STEPS_CONTEXT" functions: runs-on: ubuntu-latest steps: - name: functions examples run: | echo ${{ contains( 'hello', '11' ) }} echo ${{ startsWith( 'hello', 'he' ) }} echo ${{ endsWith( 'hello', '1o' ) }} echo ${{ format( 'Hello {0} {1} {2}', 'World', '!', '!' ) }}
Job status functions are functions that return status of the job and steps of the job
Job status functions are used with the if
key to determine if a job or step should run. Anything inside the if
key is treated as an expression
By default if a step fails the following steps in a job won't run unless we change that behavior using a job status functions
failure()
: returns true if previous step or job failssuccess()
: returns true if previous step or job successcanceled()
: returns true if previous step or job was canceledalwayts()
: always returns true used to run step or job that needs to run even if anything failed in the previous step or jobname: Context workflow on: [push] jobs: functions: runs-on: ubuntu-latest if: github.event_name == 'push' steps: - name: bad echo cmd run: | eccho "Bad echo command" - name: functions examples if: failure() run: | echo ${{ contains( 'hello', '11' ) }} echo ${{ startsWith( 'hello', 'he' ) }} echo ${{ endsWith( 'hello', '1o' ) }} echo ${{ format( 'Hello {0} {1} {2}', 'World', '!', '!' ) }}
You can add the continue-on-error: true
option to a step to signal all following steps should run even if the steps that has this set to true
fails
You can also add the timeout-minutes
to a job or step to set the max number of minutes before GitHub kills the process for this step or job
strategy is a special option we have have in GitHub workflows that can be specified in job to setup and environment matrix
A strategy can be used to run a job on different types of environments, for example if we want to a job with multiple versions of NodeJS and multiple Operating Systems we can setup a matrix of environments using the strategy option to do this
To set up a strategy we use the strategy
option
fail-fast
if set to false
each of the jobs will run independent of the results of other jobsmax-parallel
to limit the number of jobs that will run in parallel, by default github will maximize thismatrix
key that must contain any keys and the values of those keys must be arrays with the values that will be used on the execution of the job, on the job we need to use the ${{matrix.key_name}}
to reference the value that will change depending on which job is runningname: matrix on: [push] jobs: node-version: strategy: matrix: os: [macos-latest, ubuntu-latest, windows-latest] node_version: [6, 8, 10] runs-on: ${{matrix.os}} steps: - name: Log Node version before run: | node -v npm -v - name: use setup-node uses: actions/setup-node@v1 with: node-version: ${{matrix.node_version}} - name: Log Node version after run: | node -v npm -v
we can exclude an specific combination of the matrix using the exclude
inside the matrix
, the exclude option must have an array of objects where each object must have the keys and values that we want to exclude or include
the include
option is used to add extra values to a specific combination of the matrix
name: matrix on: [push] jobs: node-version: strategy: matrix: os: [macos-latest, ubuntu-latest, windows-latest] node_version: [6, 8, 10] include: - os: ubuntu-latest node_version: 8 is_ubuntu_8: "true" exclude: - os: windows-latest node_version: 6 - os: macos-latest node_version: 8 runs-on: ${{matrix.os}} ...
container
option and specifying a Docker imagename: Container on: [push] jobs: node-docker: runs-on: ubuntu-latest container: image: node:14.15.0-alpine3.10 steps: - name: "Log Node Version" run: | node -v cat /etc/os-release
Containers on Github Actions can only run on ubuntu
We can also do more complex stuff and specify multiple containers to run as services, for example an app that needs a DB to work can be run if we create and run a container that has the DB. For this we use the services
option and inside we can follow same syntax as in docker-compose.yml
file which let us specify multiple services that run on separate containers
we can use docker container as github action with the uses
and with
options in a step. E.x. uses: docker://node:14.15.0-alpine3.10
The power of being able to use docker images in Github actions lies in the fact that we can use the docker library of images. We can browse the library and if we find a docker image that does something we need we can use it as part of our workflow
CI (Continuous Integration) is a practice where one or more collaborators frequently commit code to a shared repository and if something breaks we want to detect the source of the error (the commit that caused the errors) as soon as possible
CI attempts to ensure that new code will not break any previously working tests or introduce any errors
CD (Continuous Deployment) is a practice where we want to be continuously deploying our application or in other words making releases of our application without impacting our clients/users
Two protected branches (Develop and Master), meaning developers cannot push directly to these they need to open a pull request to merge something to these branches
Master branch will contain deployable code, which means code that can be deployed to production. So we will never push anything into the master branch except if we want to deploy it to production. Our Develop branch on the other side will contain the latest development code that we would not like to deploy yet.
When a developer needs to work on something, he/she will or create a new branch from the Develop branch (e.x called Feature1)
When a developer work is done a pull request to merge into Develop branch will be opened, when this happens a workflow will run to execute tests (unit, integration & system), if test pass and no regression is detected this pull request will be in queue for a Reviewer to approve it
if reviewer approves this will be merged into the Develop this merge will run another workflow to run our tests again (unit, integration & system) and deploy to a staging server (beta)
When we want to make a release, we need to create pull request from the Develop branch to merge into master, this pull requests will trigger another workflow that will execute or tests again if our tests pass it will be in queue for a Reviewer to approve the release
if reviewer approves the release, this will be merged into Master this merge will run another workflow to run our tests again (unit, integration & system) and deploy to production
Github allow you to set a CODEOWNERS
file to set who owns certain files (your_repo/.github/CODEOWNERS
) you need to specify files, folders or patterns followed by Github username, this is used for pull-request to assign reviewers based on owners
*.js @sanpepe
*.yaml @sanpepe
To protect branches in Github 1) go to Settings -> Branches and Click on Add Rule 2) give the branch name and 3) Select the rules you want to enforce and click on Create (you may need to enter your password).
Protect branches in Github Example:
name: CI on: push: branches: [develop] push: branches: [develop] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Use Python 3.x uses: actions/setup-python@v1 with: python-version: "3.x" - run: pip install --no-index -r $./requirements.txt - name: run unit test run: python -m unittest .../tests/unit/my_unit_test.py PYTHONPATH: ./ - name: run integration tests if: github.event_name == 'push' run: python -m unittest .../tests/integration/my_int_test.py - name: Deploy to Staging if: github.event_name == 'push' run: echo "Some step to publish code"
Consider a merge to a branch needs a push so it you trigger on push that will also be executed when a merge happens
If you want to cache the sate of your dependencies you can use the cache
action for example to cache python dependencies
key
option, this is save in your caches so every time it runs it first sees if that cache is found it will be retrieved and used in your workflow. And if not a new cache key will be created so that when you run your workflow again, you can use itTo Upload and artifact in Github we need to use the upload-artifact
action and specify the path we want to upload, optionally give it an ID name to appear on Github
download-artifact
action... - name: Upload artifact uses: actions/upload-artifact@v1 with: name: my_app_debug path: ./logs/my_app_debug.log ...
semantic-release
Conventional commits: Have a format for the commits with special keywords to know what version to increment for the release
To Setup semantic release:
release.config.js
file in your repo and set it up according with semantic release documentationmodule.exports = {
branches: "master",
repositoryUrl: "<github repo URL here>",
plugins: [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/github"
]
}
Add a step in your workflow to run npx semantic release
and provide the env variable GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
If you want to upload extra assets to your release, you need to add them in the semantics-release
config file (we can only add files)
module.exports = {
branches: "master",
repositoryUrl: "<github repo URL here>",
plugins: [
"@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator",
["@semantic-release/github", {
assets: [
{ path: "build.zip", label: "Build" },
{ path: "coverage.zip", label: "Coverage" }
]
}]
]
}
To trigger an action after a workflow completion we usually use the release
and other events in a separate workflow
Send a message
name: Notify on Release on: release: types: [published] jobs: slack-message: runs-on: ubuntu-latest steps: - name: Slack Message run: | curl -X POST -H 'content-type: application/json' -- data '{"text":"New release ${{ github.event.release.tag_name }} is out, <${{ github.event.release.html_url }}|check it out now.>"}' ${{ secrets.SLACK_WEBHOOK }}
Remember you cannot trigger a workflow from inside another workflow using the Github token
secrets.GITHUB_TOKEN
that is automatically generated by Github, if you want to do this you need to generate a separate token from your personal account (Go to Settings -> Developer Settings -> personal access tokens)
failure()
condition which will only run if any of the steps in a job fails... - name: Open Issue if: failure() && github.event_name == 'pull_request' run: | curl --request POST \ --url https://api.github.com/repos/${{ github.repository }}/issues \ --header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \ --header 'content-type: application/json' \ --data '{ "title": "Automated issue for commit: ${{ github.sha }}", "body": "This issue was automatically created by the GitHub Action workflow **${{ github.workflow }}**. \n\n The commit hash was: _${{ github.sha }}_.", "assignees": ["${{ github.event.pull_request.user.login }}"] }' ...
![](https://github.com/<github_username_here>/<repo_name_here>/workflows/<name_of_workflow_here>/badge.svg?branch=<name_branch_here>&event=<event_name_here>)
![](https://github.com/sanpepe/github-actions-test/workflows/Container/badge.svg?branch=master&event=push)
Github has public actions that live in the Github market place and we can reference in our workflow without actually including any code in our project
We can also write private actions that live in your project and reference them in the workflow
We can write actions using JavaScript or Docker containers
.github
folder called actions and inside that create a folder for your actionaction.yml
file to define basic information of your action: name, author, inputs & outputs and runs
to specify entry point and type of action e.x. Javascript and index.js
inputs in a github action only accept strings
name: Hello World Pepe Action author: Pepe Sandoval description: Some description about action here inputs: who-to-say-hello: description: 'Who to Say Hello' required: true default: Pepe outputs: time: description: 'Time of Hello' runs: using: "node12" main: 'dist/index.js'
action.yml
file (e.x. index.js
), add here the code to run with your actions and use the GitHub Toolkit packages to get inputs and outputs.// index.js const core = require("@actions/core"); const github = require("@actions/github"); try { const name = core.getInput("who-to-say-hello"); console.log(`Hello World ${name}`); const time = new Date(); core.setOutput("time", time.toTimeString()); console.log(JSON.stringify(github, null, ' ')); } catch(error) { // You can call this function to make action and workflow fail core.setFailed(error.message) }
ncc
npm i -D @zeit/ncc
or npm i -D @vercel/ncc
npm install @actions/core
and npm install @actions/github
npx ncc build .github/actions/hello/index.js -o .github/actions/hello/dist
action.yml
to use the single compiled file e.x. main: dist/index.js
./github/actions/...
name: Test Actions on: [push] jobs: run-my-github-actions: runs-on: ubuntu-latest steps: - name: Checkout Repo uses: actions/checkout@v1 - name: Use my Action id: hello uses: ./.github/actions/hello with: who-to-say-hello: "Pepe Ch." - name: Log Greeting output run: echo "Pepe Action Time ${{ steps.hello.outputs.time }}"
When you do changes in the entry point JS file remember to recompile it
.github
folder called actions and inside that create a folder for your actionaction.yml
file to define basic information of your action: name, author, inputs & outputs and runs
to specify image or dockerfile, set the args
inside run
to pass arguments to your dockerfilename: Hello World Pepe Docker Action author: Pepe Sandoval description: Some description about action here inputs: who-to-say-hello: description: 'Who to Say Hello' required: true default: Pepe outputs: time: description: 'Time of Hello' runs: using: "docker" image: 'Dockerfile' args: - ${{ inputs.who-to-say-hello }}
Dockerfile
. A common practice is to use a shell script and use special echo strings to set outputsif you use an
.sh
script make sure to give the executable permissions before pushing to repo. e.x.chmod +x .github/actions/hello-docker/entrypoint.sh
andgit add --chmod=+x -- ./.github/actions/hello-docker/entrypoint.sh
FROM alpine:3.11 COPY entrypoint.sh /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"]
#!/bin/sh -l
echo "::debug ::Debug Message"
echo "Hello $1"
time=$(date)
echo "::set-output name=time::$time"
echo "::group::Some expandable logs"
echo 'Stuff 1'
echo 'Stuff 2'
echo 'Stuff 3'
echo "::endgroup::"
4. Use your action in your workflow, reference it using relative path ./github/actions/...
name: Test Actions Docker on: [push] jobs: run-my-github-actions: runs-on: ubuntu-latest steps: - name: Checkout Repo uses: actions/checkout@v1 - name: Use my Action id: hello uses: ./.github/actions/hello-docker with: who-to-say-hello: "Pepe Ch." - name: Log Greeting output run: echo "Pepe Docker Action Time ${{ steps.hello.outputs.time }}"
if you want your action to fail you just need to exit the
.sh
script with an exit code != 0
repo/.github/workflows/unit_test.yaml
and does the minimal to run a unittest
python script if this fails make sure your requirements.txt doesn't have issues and unit test python script worksname: Run Python Unit Test on: push jobs: run-py-unit-test: runs-on: ubuntu-latest steps: - name: Checkout Repo uses: actions/checkout@v2 - name: Setup Python 3.8 uses: actions/setup-python@v2 with: python-version: '3.8' - name: Install Python requirements run: | python -m pip install --upgrade pip pip install -r requirements.txt - name: Run Unit tests run: | python -m unittest tests/my_unit_test.py -v
If you find the information in this page useful and want to show your support, you can make a donation
Use PayPal
This will help me create more stuff and fix the existent content...