├── Dockerfile ├── action.yml ├── entrypoint.py ├── key.py └── readme.md /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:bionic 2 | 3 | LABEL maintainer="jrandiny " 4 | 5 | ENV DEBIAN_FRONTEND=noninteractive 6 | 7 | RUN apt update 8 | RUN apt install -y reprepro gpg python3 python3-git python3-gnupg expect python3-debian 9 | 10 | COPY entrypoint.py /entrypoint.py 11 | COPY key.py /key.py 12 | 13 | ENTRYPOINT ["python3","/entrypoint.py"] 14 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: "Github pages APT repo" 2 | description: "Setup and manage APT repo on Github pages" 3 | inputs: 4 | github_token: 5 | description: "Github token with commit and push scope" 6 | required: true 7 | repo_supported_arch: 8 | description: "Newline-delimited list of supported architecture" 9 | required: true 10 | repo_supported_version: 11 | description: "Newline-delimited list of supported (linux) version" 12 | required: true 13 | file: 14 | description: "Newline-delimited list of .deb files" 15 | required: true 16 | file_target_version: 17 | description: "Newline-delimited list of version target of supplied .deb file" 18 | required: true 19 | public_key: 20 | description: "GPG public key for apt repo" 21 | required: false 22 | private_key: 23 | description: "GPG private key for signing apt repo" 24 | required: true 25 | key_passphrase: 26 | description: "Passphrase of GPG private key" 27 | required: false 28 | page_branch: 29 | description: "Branch of Github pages" 30 | required: false 31 | default: "gh-pages" 32 | repo_folder: 33 | description: "Location of APT repo folder relative to root of Github pages" 34 | required: false 35 | default: "repo" 36 | debug: 37 | description: "Print debug log" 38 | required: false 39 | runs: 40 | using: "docker" 41 | image: "Dockerfile" 42 | branding: 43 | icon: "upload-cloud" 44 | color: "red" 45 | -------------------------------------------------------------------------------- /entrypoint.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import logging 4 | import gnupg 5 | import git 6 | import shutil 7 | import re 8 | import json 9 | from debian.debfile import DebFile 10 | from key import detectPublicKey, importPrivateKey 11 | 12 | debug = os.environ.get('INPUT_DEBUG', False) 13 | 14 | if debug: 15 | logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG) 16 | else: 17 | logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.INFO) 18 | 19 | if __name__ == '__main__': 20 | logging.info('-- Parsing input --') 21 | 22 | github_token = os.environ.get('INPUT_GITHUB_TOKEN') 23 | supported_arch = os.environ.get('INPUT_REPO_SUPPORTED_ARCH') 24 | supported_version = os.environ.get('INPUT_REPO_SUPPORTED_VERSION') 25 | deb_file_path = os.environ.get('INPUT_FILE') 26 | deb_file_target_version = os.environ.get('INPUT_FILE_TARGET_VERSION') 27 | github_repo = os.environ.get('GITHUB_REPOSITORY') 28 | 29 | gh_branch = os.environ.get('INPUT_PAGE_BRANCH', 'gh-pages') 30 | apt_folder = os.environ.get('INPUT_REPO_FOLDER', 'repo') 31 | 32 | if None in (github_token, supported_arch, supported_version, deb_file_path): 33 | logging.error('Required key is missing') 34 | sys.exit(1) 35 | 36 | supported_arch_list = supported_arch.strip().split('\n') 37 | supported_version_list = supported_version.strip().split('\n') 38 | deb_file_path = deb_file_path.strip() 39 | deb_file_version = deb_file_target_version.strip() 40 | 41 | logging.debug(supported_arch_list) 42 | logging.debug(supported_version_list) 43 | logging.debug(deb_file_path) 44 | logging.debug(deb_file_version) 45 | 46 | if deb_file_version not in supported_version_list: 47 | logging.error('File version target is not listed in repo supported version list') 48 | sys.exit(1) 49 | 50 | key_public = os.environ.get('INPUT_PUBLIC_KEY') 51 | key_private = os.environ.get('INPUT_PRIVATE_KEY') 52 | key_passphrase = os.environ.get('INPUT_KEY_PASSPHRASE') 53 | 54 | logging.debug(github_token) 55 | logging.debug(supported_arch_list) 56 | logging.debug(supported_version_list) 57 | 58 | logging.info('-- Done parsing input --') 59 | 60 | # Clone repo 61 | 62 | logging.info('-- Cloning current Github page --') 63 | 64 | github_user = github_repo.split('/')[0] 65 | github_slug = github_repo.split('/')[1] 66 | 67 | if os.path.exists(github_slug): 68 | shutil.rmtree(github_slug) 69 | 70 | git_repo = git.Repo.clone_from( 71 | 'https://{}@github.com/{}.git'.format(github_token, github_repo), 72 | github_slug, 73 | ) 74 | 75 | git_refs = git_repo.remotes.origin.refs 76 | git_refs_name = list(map(lambda x: str(x).split('/')[-1], git_refs)) 77 | 78 | logging.debug(git_refs_name) 79 | 80 | if gh_branch not in git_refs_name: 81 | git_repo.git.checkout(b=gh_branch) 82 | else: 83 | git_repo.git.checkout(gh_branch) 84 | 85 | # Generate metadata 86 | logging.debug("cwd : {}".format(os.getcwd())) 87 | logging.debug(os.listdir()) 88 | 89 | deb_file_handle = DebFile(filename=deb_file_path) 90 | deb_file_control = deb_file_handle.debcontrol() 91 | 92 | current_metadata = { 93 | 'format_version': 1, 94 | 'sw_version': deb_file_control['Version'], 95 | 'sw_architecture': deb_file_control['Architecture'], 96 | 'linux_version': deb_file_version 97 | } 98 | 99 | current_metadata_str = json.dumps(current_metadata) 100 | logging.debug('Metadata {}'.format(current_metadata_str)) 101 | 102 | # Get metadata 103 | all_commit = git_repo.iter_commits(gh_branch) 104 | all_apt_action_commit = list(filter(lambda x: (x.message[:12] == '[apt-action]'), all_commit)) 105 | apt_action_metadata_str = list( 106 | map( 107 | lambda x: re.findall('apt-action-metadata({.+})$', x.message), 108 | all_apt_action_commit, 109 | ) 110 | ) 111 | apt_action_valid_metadata_str = list(filter(lambda x: len(x) > 0, apt_action_metadata_str)) 112 | apt_action_metadata = list(map(lambda x: json.loads(x[0]), apt_action_valid_metadata_str)) 113 | 114 | logging.debug(all_apt_action_commit) 115 | logging.debug(apt_action_valid_metadata_str) 116 | 117 | for check_metadata in apt_action_metadata: 118 | if (check_metadata == current_metadata): 119 | logging.info('Loop detected, exiting') 120 | sys.exit(0) 121 | 122 | logging.info('-- Done cloning current Github page --') 123 | 124 | # Prepare key 125 | 126 | logging.info('-- Importing key --') 127 | 128 | key_dir = os.path.join(github_slug, 'public.key') 129 | gpg = gnupg.GPG() 130 | 131 | detectPublicKey(gpg, key_dir, key_public) 132 | private_key_id = importPrivateKey(gpg, key_private) 133 | 134 | logging.info('-- Done importing key --') 135 | 136 | # Prepare repo 137 | 138 | logging.info('-- Preparing repo directory --') 139 | 140 | apt_dir = os.path.join(github_slug, apt_folder) 141 | apt_conf_dir = os.path.join(apt_dir, 'conf') 142 | 143 | if not os.path.isdir(apt_dir): 144 | logging.info('Existing repo not detected, creating new repo') 145 | os.mkdir(apt_dir) 146 | os.mkdir(apt_conf_dir) 147 | 148 | logging.debug('Creating repo config') 149 | 150 | with open(os.path.join(apt_conf_dir, 'distributions'), 'w') as distributions_file: 151 | for codename in supported_version_list: 152 | distributions_file.write('Description: {}\n'.format(github_repo)) 153 | distributions_file.write('Codename: {}\n'.format(codename)) 154 | distributions_file.write('Architectures: {}\n'.format(' '.join(supported_arch_list))) 155 | distributions_file.write('Components: main\n') 156 | distributions_file.write('SignWith: {}\n'.format(private_key_id)) 157 | distributions_file.write('\n\n') 158 | 159 | logging.info('-- Done preparing repo directory --') 160 | 161 | # Fill repo 162 | 163 | logging.info('-- Adding package to repo --') 164 | 165 | logging.info('Adding {}'.format(deb_file_path)) 166 | os.system( 167 | 'reprepro -b {} --export=silent-never includedeb {} {}'.format( 168 | apt_dir, 169 | deb_file_version, 170 | deb_file_path, 171 | ) 172 | ) 173 | 174 | logging.debug('Signing to unlock key on gpg agent') 175 | gpg.sign('test', keyid=private_key_id, passphrase=key_passphrase) 176 | 177 | logging.debug('Export and sign repo') 178 | os.system('reprepro -b {} export'.format(apt_dir)) 179 | 180 | logging.info('-- Done adding package to repo --') 181 | 182 | # Commiting and push changes 183 | 184 | logging.info('-- Saving changes --') 185 | 186 | git_repo.config_writer().set_value( 187 | 'user', 'email', '{}@users.noreply.github.com'.format(github_user) 188 | ) 189 | 190 | git_repo.git.add('*') 191 | git_repo.index.commit( 192 | '[apt-action] Update apt repo\n\n\napt-action-metadata{}'.format(current_metadata_str) 193 | ) 194 | git_repo.git.push('--set-upstream', 'origin', gh_branch) 195 | 196 | logging.info('-- Done saving changes --') 197 | -------------------------------------------------------------------------------- /key.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | 5 | 6 | def detectPublicKey(gpg, key_dir, pub_key): 7 | logging.info('Detecting public key') 8 | 9 | logging.debug('Detecting existing public key') 10 | 11 | key_exists = os.path.isfile(key_dir) 12 | 13 | logging.debug('Existing public key file exists? {}'.format(key_exists)) 14 | 15 | if not key_exists: 16 | logging.info('Directory doesn\'t contain public.key trying to import') 17 | if pub_key is None: 18 | logging.error('Please specify public key for setup') 19 | sys.exit(1) 20 | 21 | logging.debug('Trying to import key') 22 | 23 | public_import_result = gpg.import_keys(pub_key) 24 | public_import_result.ok_reason 25 | 26 | logging.debug(public_import_result) 27 | 28 | if public_import_result.count != 1: 29 | logging.error('Invalid public key provided, please provide 1 valid key') 30 | sys.exit(1) 31 | 32 | with open(key_dir, 'w') as key_file: 33 | key_file.write(pub_key) 34 | 35 | logging.info('Public key valid') 36 | 37 | 38 | def importPrivateKey(gpg, sign_key): 39 | logging.info('Importing private key') 40 | 41 | private_import_result = gpg.import_keys(sign_key) 42 | 43 | if private_import_result.count != 1: 44 | logging.error('Invalid private key provided, please provide 1 valid key') 45 | sys.exit(1) 46 | 47 | logging.debug(private_import_result) 48 | 49 | if not any(data['ok'] >= '16' for data in private_import_result.results): 50 | logging.error('Key provided is not a secret key') 51 | sys.exit(1) 52 | 53 | private_key_id = private_import_result.results[0]['fingerprint'] 54 | 55 | logging.info('Private key valid') 56 | 57 | logging.debug('Key id: {}'.format(private_key_id)) 58 | 59 | logging.info('-- Done importing key --') 60 | 61 | return private_key_id -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Github pages APT repo 2 | 3 | This action will setup and manage a simple APT repo on your github pages 4 | 5 | ## Inputs 6 | 7 | ### `github_token` 8 | 9 | **Required** Personal access token with commit and push scope granted. 10 | 11 | ### `repo_supported_arch` 12 | 13 | **Required** Newline-delimited list of supported architecture 14 | 15 | ### `repo_supported_version` 16 | 17 | **Required** Newline-delimited list of supported (linux) version 18 | 19 | ### `file` 20 | 21 | **Required** .deb files to be included 22 | 23 | ### `file_target_version` 24 | 25 | **Required** Version target of supplied .deb file 26 | 27 | ### `private_key` 28 | 29 | **Required** GPG private key for signing APT repo 30 | 31 | ### `public_key` 32 | 33 | GPG public key for APT repo 34 | 35 | ### `key_passphrase` 36 | 37 | Passphrase of GPG private key 38 | 39 | ### `page_branch` 40 | 41 | Branch of Github pages. Defaults to `gh-pages` 42 | 43 | ### `repo_folder` 44 | 45 | Location of APT repo folder relative to root of Github pages. Defaults to `repo` 46 | 47 | ## Example usage 48 | 49 | ```yaml 50 | uses: jrandiny/apt-repo-action@v1 51 | with: 52 | github_token: ${{ secrets.PAT }} 53 | arch: | 54 | amd64 55 | i386 56 | version: | 57 | bionic 58 | trusty 59 | file: my_program_bionic.deb 60 | file_target_version: bionic 61 | public_key: ${{ secrets.PUBLIC }} 62 | private_key: ${{ secrets.PRIVATE }} 63 | key_passphrase: ${{ secrets.SECRET }} 64 | ``` --------------------------------------------------------------------------------