├── .github
├── actions
│ └── config
│ │ ├── ansible.yaml
│ │ └── yamllint.yaml
└── workflows
│ └── ci.yaml
├── .gitignore
├── LICENSE
├── README.md
├── defaults
└── main.yaml
├── includes
├── download.yaml
├── install.yaml
└── uninstall.yaml
├── meta
└── main.yaml
└── tasks
└── main.yaml
/.github/actions/config/ansible.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | skip_list:
3 | # YAML lint is executed apart from Ansible lint to support custom
4 | # configurations
5 | - yaml
6 | - role-name
7 |
--------------------------------------------------------------------------------
/.github/actions/config/yamllint.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | rules:
3 | braces:
4 | min-spaces-inside: 0
5 | max-spaces-inside: 0
6 | min-spaces-inside-empty: -1
7 | max-spaces-inside-empty: -1
8 | brackets:
9 | min-spaces-inside: 0
10 | max-spaces-inside: 0
11 | min-spaces-inside-empty: -1
12 | max-spaces-inside-empty: -1
13 | colons:
14 | max-spaces-before: 0
15 | max-spaces-after: 1
16 | commas:
17 | max-spaces-before: 0
18 | min-spaces-after: 1
19 | max-spaces-after: 1
20 | comments:
21 | require-starting-space: yes
22 | min-spaces-from-content: 1
23 | document-end: disable
24 | document-start: enable
25 | empty-lines:
26 | max: 1
27 | max-start: 0
28 | max-end: 0
29 | hyphens:
30 | max-spaces-after: 1
31 | indentation:
32 | spaces: consistent
33 | indent-sequences: consistent
34 | check-multi-line-strings: no
35 | # key-duplicates: enable
36 | line-length:
37 | max: 120
38 | allow-non-breakable-words: yes
39 | new-line-at-end-of-file: enable
40 | new-lines:
41 | type: unix
42 | # trailing-spaces: disable
43 | truthy:
44 | allowed-values:
45 | - !!str yes
46 | - !!str true
47 | - !!str no
48 | - !!str false
49 | level: warning
50 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Continuous Integration
3 |
4 | "on": # Ref: https://github.com/adrienverge/yamllint/issues/430#issuecomment-1107440224
5 | push:
6 | branches: ["**"]
7 | pull_request:
8 | branches: [master]
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | strategy:
14 | fail-fast: false
15 | matrix:
16 | python-version:
17 | - "3.8"
18 |
19 | steps:
20 | - uses: actions/checkout@v2
21 |
22 | - name: Set up Python ${{ matrix.python-version }}
23 | uses: actions/setup-python@v2
24 | with:
25 | python-version: ${{ matrix.python-version }}
26 |
27 | - name: Enable cache for (pip) dependencies
28 | uses: actions/cache@v2
29 | with:
30 | path: ~/.cache/pip
31 | key: ${{ runner.os }}-pip
32 | restore-keys: |
33 | ${{ runner.os }}-pip-
34 |
35 | - name: Lint | Check Ansible and YAML
36 | run: |
37 | python -m pip install --upgrade pip
38 | python -m pip install yamllint ansible ansible-lint
39 |
40 | yamllint -c .github/actions/config/yamllint.yaml .
41 | ansible-lint -c .github/actions/config/ansible.yaml .
42 |
43 | - name: Galaxy | Import
44 | if: ${{ github.ref == 'refs/heads/master' }}
45 | run: |
46 | ansible-galaxy role import \
47 | --api-key ${ANSIBLE_GALAXY_API_KEY} \
48 | --branch master \
49 | macunha1 github_actions_runner
50 | env:
51 | ANSIBLE_GALAXY_API_KEY: ${{ secrets.ANSIBLE_GALAXY_API_KEY }}
52 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # IDEs
2 | .idea/
3 | .vscode/
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | =====================
3 |
4 | Copyright © 2021, Matheus Cunha
5 |
6 | Permission is hereby granted, free of charge, to any person
7 | obtaining a copy of this software and associated documentation
8 | files (the “Software”), to deal in the Software without
9 | restriction, including without limitation the rights to use,
10 | copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the
12 | Software is furnished to do so, subject to the following
13 | conditions:
14 |
15 | The above copyright notice and this permission notice shall be
16 | included in all copies or substantial portions of the Software.
17 |
18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
25 | OTHER DEALINGS IN THE SOFTWARE.
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
GitHub Actions self-hosted Runner Ansible role
2 |
3 | An Ansible role that installs and configures GitHub Actions self-hosted Runners
4 | inside one or multiple hosts, you can re-use it for many different URLs
5 | (repositories or organizations) inside the same host in order to re-use it as
6 | much as possible.
7 |
8 | Main goals of this role:
9 |
10 | - _avoid waste_: re-use the same host to provide a build environment for
11 | multiple repositories or organizations;
12 | - _idempotence_: executing the role many times won't make anything break, steps
13 | have checks that validate whether or not they should be executed;
14 |
15 | ## Variables
16 |
17 | For an exhaustive list of variables check the [defaults](defaults/main.yaml)
18 | file. Ideally, all values will have commentaries describing what are their
19 | purposes and by the default value you can tell the type.
20 |
21 | ### Required variables
22 |
23 | Following values are required since there is no way to register the self-hosted
24 | Runner without them
25 |
26 | | Name | Description |
27 | | ---------------------- | -------------------------------------------------- |
28 | | gh_runner_config_url | GitHub Repository or Organization URL |
29 | | gh_runner_config_token | GitHub Registration token to authenticate the host |
30 |
31 | ## Example Playbook
32 |
33 | Simplest use case: Single repository configuration on one host.
34 |
35 | ```yaml
36 | - hosts: foo
37 | roles:
38 | - role: macunha1.github_actions_runner
39 | vars:
40 | gh_runner_config_labels:
41 | - linux
42 | - self-hosted
43 |
44 | gh_runner_config_url: https://github.com/macunha1/ansible-github-actions-runner
45 | gh_runner_config_token: AC5TNLJP9SBAFNEKKLLBLF264J8XO
46 | ```
47 |
48 | Complex use case to which this role was created for
49 |
50 | ```yaml
51 | - hosts: foo
52 | roles:
53 | - role: macunha1.github_actions_runner
54 | vars:
55 | gh_runner_config_labels:
56 | - linux
57 | - self-hosted
58 |
59 | gh_runner_config_url: https://github.com/macunha1/ansible-github-actions-runner
60 | gh_runner_config_token: AC5TNLJP9SBAFNEKKLLBLF264J8XO
61 |
62 | - role: macunha1.github_actions_runner
63 | vars:
64 | gh_runner_config_url: https://github.com/macunha1/another-repository
65 | gh_runner_config_token: AC5CQV3IJRR2OAFGEFCPJ0WJPJQXO
66 |
67 | - role: macunha1.github_actions_runner
68 | vars:
69 | gh_runner_config_url: https://github.com/macunha-acme-corp
70 | gh_runner_config_token: ACYWUR9MHGR9U58C34W9ZK00UNBF
71 | ```
72 |
73 | Note that despite using the same host, each one of these GitHub Actions Runner
74 | configuration will have its own path and credentials. Therefore, they can live
75 | well in harmony without killing each other.
76 |
77 | ## Contribute
78 |
79 | [](http://makeapullrequest.com)
80 |
81 | Feel free to fill [an issue](https://github.com/macunha1/ansible-github-actions-runner/issues)
82 | containing feature request(s), or (even better) to send me a Pull request, I
83 | would be happy to collaborate with you.
84 |
85 | If this role didn't work for you, or if you found some bug during the execution,
86 | let me know.
87 |
--------------------------------------------------------------------------------
/defaults/main.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | # Temporary path to download the GitHub Actions runner archive
3 | tmp_download_path: /tmp
4 |
5 | # Whether or not to clean the downloaded package in the temporary path after
6 | # unpacking/installing it.
7 | tmp_clear_download: true
8 |
9 | # Base URL to fetch the GitHub Actions Runner package with scripts that is used
10 | # to install and configure the host.
11 | gh_runner_download_base_url: https://github.com/actions/runner/releases/download
12 |
13 | # GitHub Actions runner version to download and install. Multiple parallel
14 | # versions are supported since every installation+configuration (either
15 | # repository or organization) has its own path.
16 | gh_runner_version: 2.290.1
17 |
18 | # GitHub Actions runner architecture, used together with the version to compose
19 | # the complete URL that is used to download the package.
20 | gh_runner_architecture: linux-x64
21 |
22 | # Path to unarchive the GitHub Actions runner package, this will be the prefix
23 | # with multiple versions inside. Allowing to share the same host with many
24 | # self-hosted runners regardless of the version.
25 | gh_runner_installation_path: /usr/local/share/github-actions-runner
26 |
27 | # GitHub Actions runner workspace, i.e. path used by job executions to checkout
28 | # the code, write files from the workflow, and etc.
29 | gh_runner_workspace_path: /var/cache/github-actions-runner
30 |
31 | # Whether or not to remove the GitHub Actions runner, enable to clean and
32 | # deregister the host. Enable this to remove the host from GitHub Actions and
33 | # complement with the --tag uninstall
34 | gh_runner_remove_host: false
35 |
36 | # Run GitHub Actions service will be installed to run as this user.
37 | gh_runner_service_user: "{{ ansible_user_id }}"
38 |
39 | ## Variables used to set parameters to the `config.sh` script.
40 |
41 | # (REQUIRED) GitHub Actions repository URL to register this self-hosted runner.
42 | # gh_runner_config_url: ""
43 |
44 | # (REQUIRED) GitHub Actions self-hosted Runner registration token, used to
45 | # authenticate the host to GitHub (either to a GitHub repository or to an GitHub
46 | # organization). Keep this value secure, e.g. using Ansible Vault.
47 | # Ref: https://docs.github.com/en/actions/hosting-your-own-runners/adding-self-hosted-runners
48 | # gh_runner_config_token: ""
49 |
50 | # Define the Github Actions Runner tags, this list will be converted to a
51 | # comma-separated string
52 | gh_runner_config_labels: ["self-hosted"]
53 |
54 | # Set the name to register the runner as in GitHub.
55 | gh_runner_config_name: "{{ ansible_hostname }}"
56 |
57 | # Set the runner group to add the runner to. If left blank, the default runner group will be used.
58 | gh_runner_config_runnergroup: ""
59 |
60 | # Allow/Refuse to configure the runner as root. If left blank, the root user is not allowed to configure the runner.
61 | gh_runner_allow_runasroot: ""
62 |
--------------------------------------------------------------------------------
/includes/download.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: GitHub Actions Runner | Create workspace
3 | ansible.builtin.file:
4 | path: "{{ gh_runner_workspace_path }}"
5 | owner: "{{ gh_runner_service_user }}"
6 | state: directory
7 | mode: 0755
8 | become: true
9 |
10 | - name: GitHub Actions Runner | Check if versions is already installed
11 | ansible.builtin.stat:
12 | path: "{{ gh_runner_path }}"
13 | register: unarchived_package
14 |
15 | # Download the package only if it isn't found in the installed versions
16 | - name: GitHub Actions Runner | Download package if not already done
17 | when: not unarchived_package.stat.exists
18 | block:
19 | - name: GitHub Actions Runner | Downloading package {{ gh_runner_version }}
20 | ansible.builtin.get_url:
21 | # yamllint disable-line rule:line-length
22 | url: "{{ gh_runner_download_base_url }}/v{{ gh_runner_version }}/actions-runner-{{ gh_runner_architecture }}-{{ gh_runner_version }}.tar.gz"
23 | dest: "{{ tmp_download_path }}/actions-runner-{{ gh_runner_version }}.tar.gz"
24 | mode: 0400
25 | force: yes
26 | become: true
27 |
28 | - name: GitHub Actions Runner | Create installation directory
29 | ansible.builtin.file:
30 | path: "{{ gh_runner_path }}"
31 | state: directory
32 | mode: 0755
33 | become: true
34 |
35 | - name: GitHub Actions Runner | Unarchive package
36 | ansible.builtin.unarchive:
37 | src: "{{ tmp_download_path }}/actions-runner-{{ gh_runner_version }}.tar.gz"
38 | dest: "{{ gh_runner_path }}"
39 | remote_src: true
40 | register: gh_runner_path_unarchived
41 | become: true
42 |
43 | - name: GitHub Actions Runner | Set permissions
44 | ansible.builtin.file:
45 | path: "{{ gh_runner_path }}"
46 | owner: "{{ gh_runner_service_user }}"
47 | mode: 0755
48 | recurse: true
49 | become: true
50 |
51 | # Remove the package after extracting the content IF tmp_clear_download is
52 | # enabled (default: true).
53 | - name: GitHub Actions Runner | Remove archived package
54 | ansible.builtin.file:
55 | path: "{{ tmp_download_path }}/actions-runner-{{ gh_runner_version }}.tar.gz"
56 | state: absent
57 | become: true
58 | when: tmp_clear_download
59 |
--------------------------------------------------------------------------------
/includes/install.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: GitHub Actions Runner | Install dependencies
3 | ansible.builtin.command:
4 | cmd: "{{ gh_runner_path }}/bin/installdependencies.sh"
5 | chdir: "{{ gh_runner_path }}"
6 | become: true
7 | when:
8 | # Only install dependencies if the installation directory contains changes,
9 | # which is indicated in this case by the `unarchive` task.
10 | - gh_runner_path_unarchived is defined
11 | - gh_runner_path_unarchived.changed
12 | - ansible_os_family != 'Darwin'
13 | tags:
14 | - install
15 |
16 | # Check if the registration token was used before. config.sh executions aren't
17 | # idempotent, if called twice with the very same token and URL it will fail
18 | # asking to remove the host before configuring it.
19 | #
20 | # ["Cannot configure the runner because it is already configured. To
21 | # reconfigure the runner, run 'config.cmd remove' or './config.sh remove' first."]
22 | - name: GitHub Actions Runner | Check if this host URL+token was already registered
23 | ansible.builtin.stat:
24 | path: "{{ gh_runner_path }}/hosts/{{ gh_runner_config_token | hash('sha256') }}"
25 | register: is_this_host_token_registered
26 | tags:
27 | - configure
28 |
29 | - name: GitHub Actions Runner | Search for abandoned credentials
30 | ansible.builtin.stat:
31 | path: "{{ gh_runner_path }}/.credentials"
32 | register: abandoned_credentials
33 |
34 | # Search only if the host is not registered, which is a clear indicator that
35 | # any credentials inside that path are from previous executions or expired
36 | # tokens, since the registration token hash is not matching any state file.
37 | when: not is_this_host_token_registered.stat.exists
38 |
39 | # If abandoned credentials are found AND this host's CURRENT token is not registered, we
40 | # need to uninstall its service + remove it before proceeding, otherwise the
41 | # `config.sh` command to register the node will fail asking to remove it (which
42 | # we are proactively doing here).
43 | - name: GitHub Actions Runner | Pro-activelly uninstall before re-installing
44 |
45 | when:
46 | - not is_this_host_token_registered.stat.exists
47 | - abandoned_credentials is defined
48 | - abandoned_credentials.stat.exists
49 |
50 | tags:
51 | - configure
52 |
53 | block:
54 | - name: GitHub Actions Runner | Uninstall previous service
55 | ansible.builtin.command:
56 | cmd: "{{ gh_runner_path }}/svc.sh uninstall"
57 | chdir: "{{ gh_runner_path }}"
58 | changed_when: abandoned_credentials.stat.exists
59 | become: "{{ ansible_os_family != 'Darwin' }}"
60 |
61 | - name: GitHub Actions Runner | Remove previous configuration before proceeding
62 | become: true
63 | become_user: "{{ gh_runner_service_user }}"
64 | environment:
65 | RUNNER_ALLOW_RUNASROOT: "{{ gh_runner_allow_runasroot }}"
66 | ansible.builtin.command:
67 | cmd: |-
68 | {{ gh_runner_path }}/config.sh remove \
69 | --token {{ gh_runner_config_token }}
70 | chdir: "{{ gh_runner_path }}"
71 | changed_when: abandoned_credentials.stat.exists
72 |
73 | - name: GitHub Actions Runner | Configure Runner
74 | become: true
75 | become_user: "{{ gh_runner_service_user }}"
76 | environment:
77 | RUNNER_ALLOW_RUNASROOT: "{{ gh_runner_allow_runasroot }}"
78 | ansible.builtin.command:
79 | cmd: |-
80 | {{ gh_runner_path }}/config.sh \
81 | --unattended --replace --disableupdate \
82 | --url {{ gh_runner_config_url }} \
83 | --token {{ gh_runner_config_token }} \
84 | --name {{ gh_runner_config_name }} \
85 | --runnergroup "{{ gh_runner_config_runnergroup }}" \
86 | --work {{ gh_runner_workspace_path }} \
87 | --labels {{ gh_runner_config_labels | join(',') }}
88 | chdir: "{{ gh_runner_path }}"
89 | register: config_host_command
90 |
91 | when: not is_this_host_token_registered.stat.exists
92 | tags:
93 | - configure
94 |
95 | - name: GitHub Actions Runner | Check if path for state files exists
96 | ansible.builtin.stat:
97 | path: "{{ gh_runner_path }}/hosts"
98 | register: registered_hosts_path
99 | tags:
100 | - configure
101 |
102 | - name: GitHub Actions Runner | Create hosts directory
103 | ansible.builtin.file:
104 | path: "{{ gh_runner_path }}/hosts"
105 | state: directory
106 | owner: "{{ gh_runner_service_user }}"
107 | mode: 0755
108 | become: true
109 | when: not registered_hosts_path.stat.exists
110 | tags:
111 | - configure
112 |
113 | # Create a state file to notify next Ansible executions that a given
114 | # registration token was already used. i.e. the is_this_host_token_registered
115 | # variable registered and used in the credential above.
116 | - name: GitHub Actions Runner | Create state file
117 | ansible.builtin.file:
118 | path: "{{ gh_runner_path }}/hosts/{{ gh_runner_config_token | hash('sha256') }}"
119 | state: touch
120 | mode: 0400
121 | when:
122 | - config_host_command is defined
123 | - config_host_command is success
124 | become: true
125 | tags:
126 | - configure
127 |
128 | - name: GitHub Actions Runner | Install service
129 | ansible.builtin.command:
130 | cmd: "{{ gh_runner_path }}/svc.sh install {{ gh_runner_service_user }}"
131 | chdir: "{{ gh_runner_path }}"
132 | become: "{{ ansible_os_family != 'Darwin' }}"
133 | when: not is_this_host_token_registered.stat.exists
134 | tags:
135 | - install
136 |
137 | - name: GitHub Actions Runner | Check service status
138 | ansible.builtin.command:
139 | cmd: "{{ gh_runner_path }}/svc.sh status"
140 | chdir: "{{ gh_runner_path }}"
141 | register: gh_runner_service_status
142 | changed_when: false # never changes, this is just a read-only command
143 | become: "{{ ansible_os_family != 'Darwin' }}"
144 | tags:
145 | - configure
146 |
147 | - name: GitHub Actions Runner | Start service
148 | ansible.builtin.command:
149 | cmd: "{{ gh_runner_path }}/svc.sh start"
150 | chdir: "{{ gh_runner_path }}"
151 | when: not '"active (running)"' in gh_runner_service_status.stdout
152 | become: "{{ ansible_os_family != 'Darwin' }}"
153 | tags:
154 | - configure
155 |
--------------------------------------------------------------------------------
/includes/uninstall.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: GitHub Actions Runner [!!] DESTROY! | Uninstall service
3 | ansible.builtin.command:
4 | cmd: "{{ gh_runner_path }}/svc.sh uninstall"
5 | chdir: "{{ gh_runner_path }}"
6 | changed_when: gh_runner_remove_host
7 | become: "{{ ansible_os_family != 'Darwin' }}"
8 |
9 | - name: GitHub Actions Runner [!!] DESTROY! | De-register Runner
10 | ansible.builtin.command:
11 | cmd: |-
12 | {{ gh_runner_path }}/config.sh remove \
13 | --token "{{ gh_runner_config_token }}"
14 | chdir: "{{ gh_runner_path }}"
15 | changed_when: gh_runner_remove_host
16 |
17 | - name: GitHub Actions Runner [!!] DESTROY! | Delete workspace and installations
18 | ansible.builtin.file:
19 | state: absent
20 | path: "{{ item }}"
21 | with_items:
22 | - "{{ gh_runner_installation_path }}/"
23 | - "{{ gh_runner_workspace_path }}/"
24 | changed_when: gh_runner_remove_host
25 | become: true
26 |
--------------------------------------------------------------------------------
/meta/main.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | galaxy_info:
3 | role_name: github_actions_runner
4 | author: Matheus Cunha
5 | description: >
6 | Idempotent Ansible role that installs and configures self-hosted GitHub
7 | Actions Runners (yeah, plural!)
8 | license: MIT
9 | company: None
10 | min_ansible_version: "2.2"
11 | platforms:
12 | - name: Debian
13 | versions:
14 | - all
15 | - name: EL
16 | versions:
17 | - all
18 | - name: Fedora
19 | versions:
20 | - all
21 | - name: Ubuntu
22 | versions:
23 | - all
24 | galaxy_tags:
25 | - github
26 | - actions
27 | - workflows
28 | - runner
29 |
--------------------------------------------------------------------------------
/tasks/main.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Variables | Check mandatory
3 | ansible.builtin.assert:
4 | that:
5 | - gh_runner_config_url is defined
6 | - gh_runner_config_token is defined
7 |
8 | - name: Variables | Set installation path
9 | ansible.builtin.set_fact:
10 | # Set a exclusive path for the GitHub Actions Runner in order to support
11 | # multiple repository or organizations configured inside the same host.
12 | #
13 | # WHY? `config.sh` when registering the host will write some credentials and
14 | # files to the path in order to identify the host, although inoffensive,
15 | # this approach make it complicated to share the same host among multiple
16 | # GitHub Actions Runners (even though it is possible).
17 | #
18 | # Therefore, each GitHub repository or organization URL will be hashed to
19 | # compose the GitHub Actions Runner path.
20 | #
21 | # yamllint disable-line rule:line-length
22 | gh_runner_path: "{{ gh_runner_installation_path }}/{{ gh_runner_version }}/{{ gh_runner_config_url | hash('sha256') }}"
23 |
24 | tags:
25 | - install
26 | - configure
27 | - uninstall
28 |
29 | - name: GitHub Actions Runner | Import Download tasks
30 | ansible.builtin.import_tasks: "../includes/download.yaml"
31 | tags:
32 | - install
33 |
34 | - name: GitHub Actions Runner | Import Install tasks
35 | ansible.builtin.import_tasks: "../includes/install.yaml"
36 | tags:
37 | - install
38 | - configure
39 |
40 | - name: GitHub Actions Runner | Include Uninstall tasks
41 | ansible.builtin.include_tasks: "../includes/uninstall.yaml"
42 | when: gh_runner_remove_host
43 | tags:
44 | - uninstall
45 |
--------------------------------------------------------------------------------