├── .gitignore ├── CONTRIBUTORS.md ├── DESCRIPTION ├── LICENSE ├── README.md ├── assets └── examples.gif ├── bin └── circlecli ├── circle.yml ├── circlecli ├── __init__.py ├── circleapi.py └── circleutils.py ├── mocks ├── __init__.py ├── circlecli.py ├── delete │ └── circleci.com │ │ └── api │ │ └── v1 │ │ └── project │ │ └── therealbarack │ │ └── circlecli │ │ └── build-cache ├── get │ ├── circleci.com │ │ └── api │ │ │ └── v1 │ │ │ ├── me │ │ │ ├── project │ │ │ └── therealbarack │ │ │ │ └── circlecli │ │ │ │ ├── 1 │ │ │ │ └── envvar │ │ │ ├── projects │ │ │ └── recent-builds │ ├── myownhost.com │ │ └── me │ └── status.circleci.com └── post │ └── circleci.com │ └── api │ └── v1 │ └── project │ └── therealbarack │ └── circlecli │ └── envvar ├── pre-commit ├── requirements.txt ├── requirements_dev.txt ├── setup.py └── tests ├── test_circlecli.py ├── test_circlecli_builds.py ├── test_circlecli_keys.py └── test_circleutils.py /.gitignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | *.pyc 3 | *.*~ 4 | *.pyo 5 | *.swp 6 | *.log 7 | dist/ 8 | build/ 9 | *.egg-info/ 10 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | #### Joey Espinosa ([@TheRealJoeLinux](https://github.com/TheRealJoeLinux)) 4 | * Author 5 | 6 | #### Joseph W Becher ([@drazisil](https://github.com/drazisil)) 7 | * Unit tests 8 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Unofficial CircleCI CLI tool -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Joey Espinosa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | *THIS PROJECT IS DEPRECATED, CIRCLECI HAS THESE FEATURES BUILT-IN NOW* 2 | # CircleCLI ![PyPI Version](https://img.shields.io/pypi/v/circlecli.svg) ![PyPI License](https://img.shields.io/pypi/l/circlecli.svg) ![CircleCI Status](https://img.shields.io/circleci/project/TheRealJoeLinux/circlecli.svg) ![Codecov Status](https://img.shields.io/codecov/c/github/TheRealJoeLinux/circlecli.svg) 3 | Unofficial CircleCI cross-platform CLI tool, written in Python. 4 | 5 | ![Example commands](assets/examples.gif) 6 | 7 | ## Features 8 | * Cross-platform (Linux, BSD, OSX, Windows) 9 | * Easy, natural interaction with CircleCI's REST API 10 | * Easy-to-read output (you can still get original output) 11 | * Add multiple environment variables to your project at once (the API can't) 12 | * View current CircleCI site and component status 13 | * Validate your circle.yml files (and a pre-commit hook to do it automatically) 14 | * Read token from environment (great for scripting) 15 | * **Now supporting CircleCI Enterprise!** 16 | 17 | See the [Examples](#examples) section for common types of usage. 18 | 19 | ## Installation 20 | #### Using package management 21 | ``` 22 | pip install circlecli 23 | ``` 24 | #### Manually 25 | ``` 26 | python setup.py install 27 | ``` 28 | 29 | ## Configuration 30 | CircleCLI requires an API token from your CircleCI [Account Settings](https://circleci.com/account/api) page. 31 | You may set this token as an environment variable named `CIRCLE_TOKEN` (good for scripting), or CircleCLI will interactively ask you for this token on first run if no configuration file is present. 32 | If you let CircleCLI prompt you for info, then it will create a configuration file named `.circlecli.ini` in your user's home directory that looks like this: 33 | ``` 34 | [circlecli] 35 | token = XXXXX 36 | default_username = XXXXX 37 | baseurl = XXXXX 38 | ``` 39 | You can always edit your own values in this configuration file. 40 | 41 | ## Usage 42 | ``` 43 | usage: circlecli [--help] [--project PROJECT] [--username USERNAME] 44 | [--api-baseurl BASEURL] [--build-num BUILD_NUM] [--config] 45 | [--quiet] [--set ENVVARS] [--filter FILTERS] [--verbose] 46 | [action] [help] 47 | 48 | Execute CircleCI REST API commands from the CLI. 49 | 50 | positional arguments: 51 | action An action to perform (see below) 52 | help Get help on a particular action 53 | 54 | optional arguments: 55 | --help, -h Display this help text 56 | --project PROJECT, -p PROJECT 57 | The target project name 58 | --username USERNAME, -u USERNAME 59 | The username of the project/repo owner 60 | --api-baseurl BASEURL, -a BASEURL 61 | The target CircleCI API base URL 62 | --build-num BUILD_NUM, -b BUILD_NUM 63 | The target build number 64 | --config, -c Only print saved configuration values 65 | --quiet, -q Suppress output 66 | --set ENVVARS, -s ENVVARS 67 | Set a variable (used with 'env' action) 68 | --filter FILTERS, -f FILTERS 69 | Match only response objects matching filter 70 | --verbose, -v Return original full output from CircleCI 71 | 72 | available actions: 73 | me Provide information about the signed in user. 74 | projects List of all the projects you're following on CircleCI. 75 | builds Last 30 build summaries for the account (or for a project). 76 | artifacts List the artifacts produced by a given build. 77 | retry Retry a given build. 78 | cancel Cancel a given build. 79 | clear-cache Clear the cache for a project. 80 | env List or add environment variables for a project. 81 | check Ensure a circle.yml file is valid according to CircleCI docs. 82 | status Check CircleCI site status (http://status.circleci.com). 83 | help Display help text for a particular action 84 | ``` 85 | CircleCLI by default displays formatted, concise data for each action. 86 | To display the original response from CircleCI, simply add `-v` to your command. 87 | 88 | ## Examples 89 | #### Print information about your account 90 | ``` 91 | me@foobox:~$ circlecli me 92 | Name: Barack Obama 93 | Emails: president@whitehouse.gov 94 | Sign-In Count: 36 95 | Heroku API Key: None 96 | Containers: 1 97 | Parallelism: 1 98 | Username: TheRealBarack 99 | Admin: False 100 | Projects: https://github.com/whitehouse/constitution, https://github.com/whitehouse/education, https://github.com/whitehouse/us_web_design_standards 101 | ``` 102 | #### Print detected configuration and exit 103 | ``` 104 | me@foobox:~$ circlecli config 105 | token = THISISNOTAREALTOKEN 106 | default_username = 107 | ``` 108 | #### Validate a circle.yml file 109 | ``` 110 | me@foobox:~$ circlecli check /home/foo/projects/bar/circle.yml 111 | 'branch' or 'tag' required in 'deployment.staging' 112 | ``` 113 | #### Check status of CircleCI components (from [http://status.circleci.com](http://status.circleci.com)) 114 | ``` 115 | me@foobox:~$ circlecli status -v 116 | All Systems Operational 117 | 118 | CircleCI : Operational 119 | GitHub : Operational 120 | Heroku : Operational 121 | AWS : Operational 122 | Pusher Pusher REST API : Operational 123 | Pusher WebSocket client API : Operational 124 | OS X Builds : Operational 125 | Test Result Processing : Operational 126 | Ubuntu 14.04 Builds : Operational 127 | ``` 128 | #### List projects you follow 129 | ``` 130 | me@foobox:~$ circlecli projects 131 | whitehouse/constitution 132 | whitehouse/education 133 | whitehouse/us_web_design_standards 134 | ``` 135 | #### Filter only projects that build on OSX 136 | ``` 137 | me@foobox:~$ circlecli projects -f feature_flags.osx=true 138 | whitehouse/education 139 | ``` 140 | #### List builds for the account 141 | ``` 142 | me@foobox:~$ circlecli builds 143 | ... 144 | Build# : 11 145 | Author : Barack Obama 146 | Branch : master 147 | Queued : Fri, Jul 08, 2016 08:09PM EDT 148 | Trigger: github 149 | URL : https://circleci.com/gh/whitehouse/us_web_design_standards/11 150 | Result : canceled 151 | 152 | Build# : 12 153 | Author : None 154 | Tag : v1.1.1 155 | Queued : Fri, Jul 08, 2016 08:42PM EDT 156 | Trigger: github 157 | URL : https://circleci.com/gh/whitehouse/us_web_design_standards/12 158 | Result : success 159 | ``` 160 | #### Filter builds by a specific user 161 | ``` 162 | me@foobox.com:~$ circlecli builds -f user.login=TheRealBarack 163 | Build# : 11 164 | Author : Barack Obama 165 | Branch : master 166 | Queued : Fri, Jul 08, 2016 08:09PM EDT 167 | Trigger: github 168 | URL : https://circleci.com/gh/whitehouse/us_web_design_standards/11 169 | Result : canceled 170 | ``` 171 | #### View a specific build's details 172 | ``` 173 | me@foobox:~$ circlecli builds -u foo_user -p foo_project -b 12 174 | Build# : 12 175 | Author : None 176 | Tag : v1.1.1 177 | Queued : Fri, Jul 08, 2016 08:42PM EDT 178 | Trigger: github 179 | URL : https://circleci.com/gh/whitehouse/us_web_design_standards/12 180 | Result : success 181 | ``` 182 | #### Retry a build 183 | ``` 184 | me@foobox:~$ circlecli retry -u foo_user -p foo_project -b 12 185 | Build# : 13 186 | Author : Barack Obama 187 | Branch : master 188 | Queued : Sun, Jul 10, 2016 12:17AM EDT 189 | Trigger: retry 190 | URL : https://circleci.com/gh/whitehouse/us_web_design_standards/13 191 | Result : None 192 | ``` 193 | #### Cancel a build (using the default username) 194 | ``` 195 | me@foobox:~$ circlecli cancel -p foo_project -b 12 196 | Build# : 13 197 | Author : Barack Obama 198 | Branch : master 199 | Queued : Sun, Jul 10, 2016 12:17AM EDT 200 | Trigger: retry 201 | URL : https://circleci.com/gh/whitehouse/us_web_design_standards/13 202 | Result : canceled 203 | ``` 204 | #### Clear a project's cache 205 | ``` 206 | me@foobox:~$ circlecli clear-cache -p foo_project 207 | status: build dependency caches deleted 208 | ``` 209 | #### List a project's environment variables (using the default username) 210 | ``` 211 | me@foobox:~$ circlecli env -p foo_project 212 | DEPLOY_ENV: xxxxg 213 | ``` 214 | #### Set an environment variable 215 | ``` 216 | me@foobox:~$ circlecli env -u foo_user -p foo_project -s TEST_ENV=stage 217 | TEST_ENV: xxxxge 218 | ``` 219 | #### Set multiple environment variables (using the default username) 220 | ``` 221 | me@foobox:~$ circlecli env -p foo_project -s FOO=BAR -s BAZ=QUX 222 | FOO: xxxxR 223 | BAZ: xxxxX 224 | ``` 225 | -------------------------------------------------------------------------------- /assets/examples.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particledecay/circlecli/04db8d120dfb01b6e54d69ef1c19b2f586329888/assets/examples.gif -------------------------------------------------------------------------------- /bin/circlecli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import ConfigParser 4 | from os import environ as env 5 | 6 | # for reusing the module docstring 7 | docstring = 'Execute CircleCI REST API commands from the CLI.' 8 | __doc__ = docstring 9 | 10 | SECTION_NAME = 'circlecli' 11 | ENV_TOKEN_NAME = 'CIRCLE_TOKEN' 12 | ENV_BASE_URL = 'CIRCLE_BASEURL' 13 | STANDARD_BASEURL = 'https://circleci.com/api/v1' 14 | 15 | 16 | def read_config(config_file): 17 | """Load saved configuration parameters. 18 | 19 | Args: 20 | config_file (str): the path to the configuration file 21 | 22 | Returns: 23 | (dict) the configuration values 24 | """ 25 | cp = ConfigParser.SafeConfigParser() 26 | cp.read(config_file) 27 | 28 | # build the config object 29 | config = {} 30 | try: 31 | [config.setdefault(k, v) for k, v in cp.items(SECTION_NAME)] 32 | except ConfigParser.NoSectionError: 33 | pass 34 | 35 | return config 36 | 37 | 38 | def init_config(config_path): 39 | """Initialize configuration or create new one.""" 40 | config = read_config(config_path) 41 | 42 | # environment variable overrides 43 | if env.get(ENV_TOKEN_NAME): 44 | config['token'] = env[ENV_TOKEN_NAME] 45 | 46 | # CircleCI Enterprise or standard 47 | config['baseurl'] = env.get(ENV_BASE_URL, STANDARD_BASEURL) 48 | 49 | if not config.get('token'): 50 | # read new token 51 | print("Enter your CircleCI API token at the prompt (https://circleci.com/account/api).") 52 | config['token'] = raw_input("Token: ").strip() 53 | 54 | # ask other questions since we're here 55 | if not config.get('default_username'): 56 | print("") 57 | print("A default username ensures you don't have to specify one for every command.") 58 | print("(e.g., /api/v1/project/:default_username/:project)") 59 | config['default_username'] = raw_input("Default username (leave blank for none): ").strip() 60 | 61 | # write new config 62 | config_fp = open(config_path, 'w') 63 | cp = ConfigParser.SafeConfigParser() 64 | cp.add_section(SECTION_NAME) 65 | cp.set(SECTION_NAME, 'token', config['token']) 66 | cp.set(SECTION_NAME, 'baseurl', config['baseurl']) 67 | cp.set(SECTION_NAME, 'default_username', config['default_username']) 68 | cp.write(config_fp) 69 | config_fp.close() 70 | 71 | return config 72 | 73 | 74 | if __name__ == "__main__": 75 | import argparse 76 | import sys 77 | from collections import OrderedDict 78 | from os import path as op 79 | 80 | from circlecli import CircleAPI, circle_status, validate_circle_yml 81 | 82 | # read the config 83 | config = init_config(op.join(op.expanduser("~"), '.circlecli.ini')) 84 | 85 | # parse command line args 86 | parser = argparse.ArgumentParser(description=docstring, 87 | formatter_class=argparse.RawTextHelpFormatter, 88 | add_help=False) 89 | parser.add_argument('action', action='store', nargs='?', 90 | help="An action to perform (see below)") 91 | parser.add_argument('help', nargs='?', help="Get help on a particular action") 92 | parser.add_argument('--help', '-h', dest='opt_help', action='store_true', 93 | help="Display this help text") 94 | parser.add_argument('--project', '-p', dest='project', action='store', 95 | help="The target project name") 96 | parser.add_argument('--username', '-u', dest='username', action='store', 97 | default=config.get('default_username'), 98 | help="The username of the project/repo owner") 99 | parser.add_argument('--api-baseurl', '-a', dest='baseurl', action='store', 100 | default=config.get('baseurl'), 101 | help="The target CircleCI API base URL") 102 | parser.add_argument('--build-num', '-b', dest='build_num', action='store', 103 | type=int, help="The target build number") 104 | parser.add_argument('--config', '-c', dest='config', action='store_true', 105 | default=False, help="Only print saved configuration values") 106 | parser.add_argument('--quiet', '-q', dest='quiet', action='store_true', 107 | default=False, help="Suppress output") 108 | parser.add_argument('--set', '-s', dest='envvars', action='append', 109 | default=[], help="Set a variable (used with 'env' action)") 110 | parser.add_argument('--filter', '-f', dest='filters', action='append', 111 | default=[], help="Match only response objects matching filter") 112 | parser.add_argument('--verbose', '-v', dest='verbose', action='store_true', 113 | default=False, help="Return original full output from CircleCI") 114 | 115 | args = parser.parse_args() 116 | 117 | def print_full_help(): 118 | """Display the full parser help text, plus command help text.""" 119 | parser.print_help() 120 | print_action_help( 121 | """ 122 | available actions: 123 | me\t\t{} 124 | projects\t{} 125 | builds\t{} 126 | artifacts\t{} 127 | retry\t\t{} 128 | cancel\t{} 129 | clear-cache\t{} 130 | env\t\t{} 131 | check\t\t{} 132 | status\t{} 133 | help\tDisplay help text for a particular action 134 | """.format( 135 | circle.me.__doc__.split('\n')[0], 136 | circle.projects.__doc__.split('\n')[0], 137 | circle.builds.__doc__.split('\n')[0], 138 | circle.artifacts.__doc__.split('\n')[0], 139 | circle.retry_build.__doc__.split('\n')[0], 140 | circle.cancel_build.__doc__.split('\n')[0], 141 | circle.clear_cache.__doc__.split('\n')[0], 142 | circle.envvar.__doc__.split('\n')[0], 143 | validate_circle_yml.__doc__.split('\n')[0], 144 | circle_status.__doc__.split('\n')[0] 145 | ) 146 | ) 147 | sys.exit(0) 148 | 149 | def build_filters(filters): 150 | """Build a dict of provided filters.""" 151 | new_filters = {} 152 | for f in filters: 153 | if "=" in f: 154 | name, value = f.split("=") 155 | new_filters[name] = value 156 | return new_filters 157 | 158 | # set the filters 159 | filters = build_filters(args.filters) 160 | 161 | def next_action(arg, next_arg=None): 162 | """Check if the next arg matches a given action.""" 163 | idx = sys.argv.index(arg) 164 | next_idx = idx + 1 165 | if len(sys.argv) < (next_idx + 1): 166 | return False 167 | if next_arg is None: 168 | return sys.argv[next_idx] # just return the detected next arg 169 | return next_arg if sys.argv[next_idx] == next_arg else False 170 | 171 | def print_action_help(summary, kwargs={}, examples={}): 172 | """Print a help message for a particular action.""" 173 | msgs = [summary, ''] 174 | [msgs.append({k: v}) for k, v in kwargs.iteritems()] 175 | for msg in msgs: 176 | if not isinstance(msg, dict): 177 | print(msg) 178 | else: 179 | for k, v in msg.iteritems(): 180 | print("{}: {}".format(k, v)) 181 | 182 | if examples: 183 | print("") 184 | print("EXAMPLES") 185 | for title, ex in examples.iteritems(): 186 | print("{}:".format(title)) 187 | print("\t{}".format(ex)) 188 | print("") 189 | 190 | # instantiate the CircleAPI class 191 | circle = CircleAPI(token=config['token'], baseurl=config['baseurl']) 192 | 193 | if args.config or args.action == 'config': 194 | for k, v in config.iteritems(): 195 | print("{} = {}".format(k, v)) 196 | sys.exit(0) 197 | 198 | # help action 199 | if args.opt_help or args.action == 'help': 200 | print_full_help() 201 | 202 | # required args 203 | if not args.action: 204 | parser.error("you must specify an action") 205 | 206 | if args.action == 'check': 207 | filepath = next_action('check') 208 | if not filepath or filepath == 'help': 209 | help_args = OrderedDict() 210 | help_args[' '] = "\tFilepath to a circle.yml file to validate" 211 | help_args[' --quiet|-q'] = "\t(optional) Suppress output" 212 | 213 | ex_args = OrderedDict() 214 | ex_args['Ensure a circle.yml file is valid'] = "circlecli check /home/foo/projects/bar/circle.yml" 215 | print_action_help(validate_circle_yml.__doc__.split('\n')[0], help_args, ex_args) 216 | else: 217 | try: 218 | validate_circle_yml(filepath) 219 | except Exception as e: 220 | if not args.quiet: 221 | print(e.message) 222 | sys.exit(1) 223 | elif args.action == 'status': 224 | if next_action('status', 'help'): 225 | help_args = OrderedDict() 226 | help_args[' --verbose|-v'] = "\t(optional) Print status of all components" 227 | help_args[' --quiet|-q'] = "\t\t(optional) Suppress output" 228 | 229 | ex_args = OrderedDict() 230 | ex_args['Check status of CircleCI components'] = 'circlecli status' 231 | print_action_help(circle_status.__doc__.split('\n')[0], help_args, ex_args) 232 | else: 233 | try: 234 | res = circle_status(args.verbose) 235 | except Exception as e: 236 | # CircleCI might be down 237 | print("Could not reach CircleCI") 238 | sys.exit(10) 239 | 240 | # don't check for args.verbose, since response type doesn't change 241 | for name, status in res: 242 | if name is None: # overall page status 243 | print(status) 244 | if len(res) > 1: # we have more to print 245 | print("") 246 | else: 247 | print("{0:<30}: {1:<20}".format(name, status)) 248 | elif args.action == 'me': 249 | if next_action('me', 'help'): 250 | help_args = OrderedDict() 251 | help_args[' --verbose|-v'] = "\t(optional) Print original full output from CircleCI" 252 | help_args[' --quiet|-q'] = "\t\t(optional) Suppress output" 253 | 254 | ex_args = OrderedDict() 255 | ex_args['Print information about your account'] = "circlecli me" 256 | print_action_help(circle.me.__doc__.split('\n')[0], help_args, ex_args) 257 | else: 258 | try: 259 | res = circle.me(args.verbose) 260 | except Exception as e: 261 | if not args.quiet: 262 | print(e.message) 263 | sys.exit(2) 264 | if not args.verbose: 265 | for k, v in res.iteritems(): 266 | print("{}: {}".format(k, v)) 267 | else: 268 | print(res) 269 | elif args.action == 'projects': 270 | if next_action('projects', 'help'): 271 | help_args = OrderedDict() 272 | help_args[' --verbose|-v'] = "\t(optional) Print original full output from CircleCI" 273 | help_args[' --filter|-f KEY=VAL'] = "\t(optional) Filter by key/value pair (can include multiple)" 274 | help_args[' --quiet|-q'] = "\t\t(optional) Suppress output" 275 | 276 | ex_args = OrderedDict() 277 | ex_args['List projects followed by your account'] = "circlecli projects" 278 | ex_args['List only projects that build on OSX'] = "circlecli projects -f feature_flags.osx=true" 279 | print_action_help(circle.projects.__doc__.split('\n')[0], help_args, ex_args) 280 | else: 281 | try: 282 | res = circle.projects(args.verbose, filters=filters) 283 | except Exception as e: 284 | if not args.quiet: 285 | print(e.message) 286 | sys.exit(3) 287 | if not args.verbose: 288 | for k in res: 289 | print(k) 290 | else: 291 | print(res) 292 | elif args.action == 'builds': 293 | if next_action('builds', 'help'): 294 | help_args = OrderedDict() 295 | help_args[' --username|-u '] = "\tThe target username (or default if not provided)" 296 | help_args[' --project|-p '] = "\t(optional) A project to get build info" 297 | help_args[' --build_num|-b '] = "(optional) A build number to get specific build details" 298 | help_args[' --verbose|-v'] = "\t(optional) Print original full output from CircleCI" 299 | help_args[' --filter|-f KEY=VAL'] = "\t(optional) Filter by key/value pair (can include multiple)" 300 | 301 | ex_args = OrderedDict() 302 | ex_args['List builds for the account'] = "circlecli builds" 303 | ex_args['List only successful builds'] = "circlecli builds -f status=success" 304 | ex_args['List builds for a project'] = "circlecli builds -u foo_user -p foo_project" 305 | ex_args['List only successful builds for master branch (w/ default user)'] = "circlecli builds -p foo_project -f branch=master -f status=success" 306 | ex_args["List a specific build's details"] = "circlecli builds -u foo_user -p foo_project -b 14" 307 | print_action_help(circle.builds.__doc__.split('\n')[0], help_args, ex_args) 308 | else: 309 | if args.project and not args.username: 310 | parser.error("username (-u) is required") 311 | try: 312 | res = circle.builds(username=args.username, project=args.project, 313 | build_num=args.build_num, verbose=args.verbose, 314 | filters=filters) 315 | except Exception as e: 316 | if not args.quiet: 317 | print(e.message) 318 | sys.exit(4) 319 | if not args.verbose: 320 | for build in res: 321 | for k, v in build.iteritems(): 322 | print("{}: {}".format(k, v)) 323 | print("") 324 | else: 325 | print(res) 326 | elif args.action == 'artifacts': 327 | if next_action('artifacts', 'help'): 328 | help_args = OrderedDict() 329 | help_args[' --username|-u '] = "\tThe target username (or default if not provided)" 330 | help_args[' --project|-p '] = "\t(optional) A project to get build info" 331 | help_args[' --build_num|-b '] = "(optional) A build number to get artifacts" 332 | help_args[' --verbose|-v'] = "\t(optional) Print original full output from CircleCI" 333 | help_args[' --filter|-f KEY=VAL'] = "\t(optional) Filter by key/value pair (can include multiple)" 334 | help_args[' --quiet|-q'] = "\t\t(optional) Suppress output" 335 | 336 | ex_args = OrderedDict() 337 | ex_args['List artifacts for a build'] = "circlecli artifacts -u foo_user -p foo_project -b 14" 338 | print_action_help(circle.artifacts.__doc__.split('\n')[0], help_args, ex_args) 339 | else: 340 | if args.project and not args.username: 341 | parser.error("username (-u) is required") 342 | if not args.build_num: 343 | parser.error("build-num (-b) is required") 344 | try: 345 | res = circle.artifacts(username=args.username, project=args.project, 346 | build_num=args.build_num, verbose=args.verbose, 347 | filters=filters) 348 | except Exception as e: 349 | if not args.quiet: 350 | print(e.message) 351 | sys.exit(5) 352 | if not args.verbose: 353 | for k in res: 354 | print(k) 355 | else: 356 | print(res) 357 | elif args.action == 'retry': 358 | if next_action('retry', 'help'): 359 | help_args = OrderedDict() 360 | help_args[' --username|-u '] = "\tThe target username (or default if not provided)" 361 | help_args[' --project|-p '] = "\t(optional) A project to get build info" 362 | help_args[' --build_num|-b '] = "(optional) A build number to retry" 363 | help_args[' --verbose|-v'] = "\t(optional) Print original full output from CircleCI" 364 | help_args[' --filter|-f KEY=VAL'] = "\t(optional) Filter by key/value pair (can include multiple)" 365 | help_args[' --quiet|-q'] = "\t\t(optional) Suppress output" 366 | 367 | ex_args = OrderedDict() 368 | ex_args['Retry a build'] = "circlecli retry -u foo_user -p foo_project -b 14" 369 | print_action_help(circle.retry_build.__doc__.split('\n')[0], help_args, ex_args) 370 | else: 371 | if args.project and not args.username: 372 | parser.error("username (-u) is required") 373 | if not args.build_num: 374 | parser.error("build-num (-b) is required") 375 | try: 376 | res = circle.retry_build(username=args.username, project=args.project, 377 | build_num=args.build_num, verbose=args.verbose, 378 | filters=filters) 379 | except Exception as e: 380 | if not args.quiet: 381 | print(e.message) 382 | sys.exit(6) 383 | if not args.verbose: 384 | for k, v in res.iteritems(): 385 | print("{}: {}".format(k, v)) 386 | else: 387 | print(res) 388 | elif args.action == 'cancel': 389 | if next_action('cancel', 'help'): 390 | help_args = OrderedDict() 391 | help_args[' --username|-u '] = "\tThe target username (or default if not provided)" 392 | help_args[' --project|-p '] = "\t(optional) A project to get build info" 393 | help_args[' --build_num|-b '] = "(optional) A build number to cancel" 394 | help_args[' --verbose|-v'] = "\t(optional) Print original full output from CircleCI" 395 | help_args[' --filter|-f KEY=VAL'] = "\t(optional) Filter by key/value pair (can include multiple)" 396 | help_args[' --quiet|-q'] = "\t\t(optional) Suppress output" 397 | 398 | ex_args = OrderedDict() 399 | ex_args['Cancel a build'] = "circlecli cancel -u foo_user -p foo_project -b 14" 400 | print_action_help(circle.cancel_build.__doc__.split('\n')[0], help_args, ex_args) 401 | else: 402 | if args.project and not args.username: 403 | parser.error("username (-u) is required") 404 | if not args.build_num: 405 | parser.error("build-num (-b) is required") 406 | try: 407 | res = circle.cancel_build(username=args.username, project=args.project, 408 | build_num=args.build_num, verbose=args.verbose, 409 | filters=filters) 410 | except Exception as e: 411 | if not args.quiet: 412 | print(e.message) 413 | sys.exit(7) 414 | if not args.verbose: 415 | for k, v in res.iteritems(): 416 | print("{}: {}".format(k, v)) 417 | else: 418 | print(res) 419 | elif args.action == 'clear-cache': 420 | if next_action('clear-cache', 'help'): 421 | help_args = OrderedDict() 422 | help_args[' --username|-u '] = "\tThe target username (or default if not provided)" 423 | help_args[' --project|-p '] = "\t(optional) The project's cache to clear" 424 | help_args[' --verbose|-v'] = "\t(optional) Print original full output from CircleCI" 425 | help_args[' --quiet|-q'] = "\t\t(optional) Suppress output" 426 | 427 | ex_args = OrderedDict() 428 | ex_args["Clear a project's cache"] = "circlecli clear-cache -u foo_user -p foo_project" 429 | print_action_help(circle.clear_cache.__doc__.split('\n')[0], help_args, ex_args) 430 | else: 431 | if args.project and not args.username: 432 | parser.error("username (-u) is required") 433 | try: 434 | res = circle.clear_cache(username=args.username, project=args.project, 435 | verbose=args.verbose) 436 | except Exception as e: 437 | if not args.quiet: 438 | print(e.message) 439 | sys.exit(8) 440 | if not args.verbose: 441 | for k, v in res.iteritems(): 442 | print("{}: {}".format(k, v)) 443 | else: 444 | print(res) 445 | elif args.action == 'env': 446 | if next_action('env', 'help'): 447 | help_args = OrderedDict() 448 | help_args[' --username|-u '] = "\tThe target username (or default if not provided)" 449 | help_args[' --project|-p '] = "\tThe project to list or set environment variables" 450 | help_args[' --verbose|-v'] = "\t(optional) Print original full output from CircleCI" 451 | help_args[' --set|-s KEY=VAL'] = "\t(optional) Set a key/value pair (can include multiple)" 452 | help_args[' --quiet|-q'] = "\t\t(optional) Suppress output" 453 | 454 | ex_args = OrderedDict() 455 | ex_args["List a project's environment variables"] = "circlecli env -u foo_user -p foo_project" 456 | ex_args["Set two environment variables"] = "circlecli env -u foo_user -p foo_project -s FOO=BAR -s BAZ=QUX" 457 | print_action_help(circle.envvar.__doc__.split('\n')[0], help_args, ex_args) 458 | else: 459 | if not args.project: 460 | parser.error("project (-p) is required") 461 | if args.project and not args.username: 462 | parser.error("username (-u) is required") 463 | envvars = {} 464 | for envvar in args.envvars: 465 | if "=" in envvar: 466 | env_split = envvar.split("=") 467 | name = env_split[0] 468 | value = "=".join(env_split[1:]) # preserve any add'l "=" that may be part of value 469 | envvars[name] = value 470 | try: 471 | res = circle.envvar(username=args.username, project=args.project, 472 | verbose=args.verbose, **envvars) 473 | except Exception as e: 474 | if not args.quiet: 475 | print(e.message) 476 | sys.exit(9) 477 | if not args.verbose: 478 | for var in res: 479 | print(var) 480 | else: 481 | print(res) 482 | else: 483 | print("Unknown action: {}".format(args.action)) 484 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | general: 2 | artifacts: 3 | - cover 4 | 5 | dependencies: 6 | override: 7 | - pip install -r requirements_dev.txt 8 | 9 | test: 10 | override: 11 | - nosetests -v --cover-branches --with-coverage --cover-erase --cover-package=circlecli --cover-html --with-xunit 12 | post: 13 | - mkdir $CIRCLE_TEST_REPORTS/nosetests && cp nosetests.xml $CIRCLE_TEST_REPORTS/nosetests 14 | - bash <(curl -s https://codecov.io/bash) 15 | -------------------------------------------------------------------------------- /circlecli/__init__.py: -------------------------------------------------------------------------------- 1 | from .circleapi import CircleAPI 2 | from .circleutils import circle_status, validate_circle_yml -------------------------------------------------------------------------------- /circlecli/circleapi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """CLI wrapper for CircleCI's REST API.""" 3 | import json 4 | import requests 5 | from collections import OrderedDict 6 | from dateutil import parser as dp, tz 7 | from urllib import urlencode 8 | from urlparse import ParseResult, urlparse, urlunparse 9 | 10 | 11 | class CircleAPI(object): 12 | """Simple wrapper for requests specific to CircleCI.""" 13 | 14 | def __init__(self, token, baseurl="https://circleci.com/api/v1"): 15 | """Load the API token. 16 | 17 | Args: 18 | token (str): the CircleCI API token (obtained at https://circleci.com/account/api) 19 | baseurl (str): the CircleCI API base URL (supporting enterprise/self-hosted deployments) 20 | """ 21 | self._token = self._validate_token(token) 22 | self._base_url = baseurl 23 | 24 | def _validate_token(self, token): 25 | """Ensure the provided token is a valid CircleCI API token. 26 | 27 | Args: 28 | token (str): the CircleCI API token 29 | 30 | Returns: 31 | (str) a valid token 32 | """ 33 | if len(token) != 40: 34 | raise ValueError(u"Invalid API token: {}".format(token)) 35 | try: 36 | int(token, 16) 37 | except ValueError: 38 | raise ValueError(u"Invalid API token: {}".format(token)) 39 | return token 40 | 41 | def _build_url(self, endpoint, params={}): 42 | """Return the full URL for the desired endpoint. 43 | 44 | Args: 45 | endpoint (str): the API endpoint after base URL 46 | params (dict): any params to include in the request 47 | 48 | Returns: 49 | (str) the full URL of the request 50 | """ 51 | new_params = {'circle-token': self._token} 52 | new_params.update(params) 53 | 54 | parsed_url = urlparse(self._base_url) 55 | new_parse = ParseResult(scheme=parsed_url.scheme, netloc=parsed_url.netloc, 56 | path='/'.join((parsed_url.path, endpoint)), 57 | params='', query=urlencode(new_params), 58 | fragment='') 59 | 60 | return urlunparse(new_parse) 61 | 62 | def _get(self, endpoint, params={}, headers={}): 63 | """Send a GET request to `endpoint`. 64 | 65 | Args: 66 | endpoint (str): the API endpoint after base URL 67 | params (dict): any params to include in the request 68 | headers (dict): any headers to include in the request 69 | 70 | Returns: 71 | (dict) the JSON-converted response from the endpoint 72 | """ 73 | url = self._build_url(endpoint, params) 74 | new_headers = {'Content-Type': 'application/json', 'Accept': 'application/json'} 75 | new_headers.update(headers) 76 | 77 | r = requests.get(url, headers=new_headers) 78 | if r.status_code >= 400: 79 | raise Exception(u"Error sending GET request to {}".format(url)) 80 | 81 | return r.json() 82 | 83 | def _post(self, endpoint, data=None, headers={}): 84 | """Send a POST request to `endpoint`. 85 | 86 | Args: 87 | endpoint (str): the API endpoint after base URL 88 | data (dict): the body to submit with the request 89 | headers (dict): any headers to include in the request 90 | 91 | Returns: 92 | (dict) the JSON-converted response from the endpoint 93 | """ 94 | url = self._build_url(endpoint) 95 | new_headers = {'Content-Type': 'application/json', 'Accept': 'application/json'} 96 | new_headers.update(headers) 97 | 98 | r = requests.post(url, headers=new_headers, data=data) 99 | if r.status_code >= 400: 100 | raise Exception(u"Error sending POST request to {}".format(url)) 101 | 102 | return r.json() 103 | 104 | def _delete(self, endpoint, headers={}): 105 | """Send a DELETE request to `endpoint`. 106 | 107 | Args: 108 | endpoint (str): the API endpoint after base URL 109 | headers (dict): any headers to include in the request 110 | 111 | Returns: 112 | (dict) the JSON-converted response from the endpoint 113 | """ 114 | url = self._build_url(endpoint) 115 | new_headers = {'Content-Type': 'application/json', 'Accept': 'application/json'} 116 | new_headers.update(headers) 117 | 118 | r = requests.delete(url, headers=headers) 119 | if r.status_code >= 400: 120 | raise Exception(u"Error sending DELETE request to {}".format(url)) 121 | 122 | return r.json() 123 | 124 | def _retr_item(self, obj, key): 125 | """Retrieve an item from a dict, including sub-keys. 126 | 127 | Args: 128 | obj (dict): a dict object 129 | key (str): a key to retrieve from `obj` 130 | 131 | Returns: 132 | () the item located at `key` 133 | """ 134 | keys = key.split('.') 135 | item = obj 136 | for k in keys: 137 | if k not in item: 138 | raise KeyError 139 | item = item[k] 140 | return item 141 | 142 | def _filter_single(self, response, filters): 143 | """Filter a single response object by matching a provided set of filters. 144 | 145 | Args: 146 | response (dict): a response object 147 | filters (dict): a set of key/value pairs to match 148 | 149 | Returns: 150 | (dict) the response if it matches all filters 151 | """ 152 | for k, v in filters.iteritems(): 153 | try: 154 | item = self._retr_item(response, k) 155 | except KeyError: # we know the key doesn't exist 156 | return 157 | try: 158 | # things like "true" need to be JSON-loaded to match a boolean 159 | if item != v and item != json.loads(v): 160 | return 161 | except: 162 | return 163 | return response 164 | 165 | def _filter(self, response, filters={}): 166 | """Filter response by matching a provided set of filters. 167 | 168 | Args: 169 | filters (dict): a set of key/value pairs to match 170 | 171 | Returns: 172 | (list/dict) the original response filtered by the provided filters 173 | """ 174 | if not filters: 175 | return response 176 | 177 | if isinstance(response, list): 178 | new_response = [r for r in response if self._filter_single(r, filters)] 179 | else: 180 | new_response = self._filter_single(response, filters) 181 | 182 | return new_response 183 | 184 | def me(self, verbose=False): 185 | """Provide information about the signed in user. 186 | 187 | Args: 188 | verbose (bool): whether to return filtered info or the full response 189 | 190 | Returns: 191 | (dict) the JSON-converted response from the endpoint 192 | """ 193 | r_json = self._get('me') 194 | if verbose: 195 | return json.dumps(r_json, indent=2) 196 | 197 | resp = OrderedDict() 198 | resp['Name'] = r_json['name'] 199 | resp['Emails'] = ', '.join(r_json['all_emails']) 200 | resp['Sign-In Count'] = r_json['sign_in_count'] 201 | resp['Heroku API Key'] = r_json['heroku_api_key'] 202 | resp['Containers'] = r_json['containers'] 203 | resp['Parallelism'] = r_json['parallelism'] 204 | resp['Username'] = r_json['login'] 205 | resp['Admin'] = r_json['admin'] 206 | resp['Projects'] = ', '.join(r_json['projects'].keys()) 207 | 208 | return resp 209 | 210 | def projects(self, verbose=False, filters={}): 211 | """List of all the projects you're following on CircleCI. 212 | 213 | Args: 214 | verbose (bool): whether to return filtered info or the full response 215 | filters (dict): a set of key/value pairs to match 216 | 217 | Returns: 218 | (list) a list of all the projects and project info 219 | """ 220 | r_json = self._filter(self._get('projects'), filters) 221 | if verbose: 222 | return json.dumps(r_json, indent=2) 223 | 224 | resp = ['{}/{}'.format(j['username'], j['reponame']) for j in r_json] 225 | return resp 226 | 227 | def builds(self, username=None, project=None, build_num=None, verbose=False, 228 | filters={}): 229 | """Last 30 build summaries for the account (or for a project). 230 | 231 | Args: 232 | username (str): the owner of the project 233 | project (str): the project name 234 | build_num (int): the build number 235 | verbose (bool): whether to return filtered info or the full response 236 | filters (dict): a set of key/value pairs to match 237 | 238 | Returns: 239 | (list) the last 30 build summaries 240 | """ 241 | if username and project: 242 | if build_num: 243 | r_json = self._get('project/{username}/{project}/{build_num}'.format(**locals())) 244 | else: 245 | r_json = self._get('project/{username}/{project}'.format(**locals())) 246 | else: 247 | r_json = self._get('recent-builds') 248 | r_json = self._filter(r_json, filters) 249 | 250 | if verbose: 251 | return json.dumps(r_json, indent=2) 252 | 253 | resp = [] 254 | if not isinstance(r_json, list): 255 | r_json = [r_json] 256 | for build in r_json: 257 | o = OrderedDict() 258 | o['Build# '] = build['build_num'] 259 | o['Author '] = '{} <{}>'.format(build['author_name'], build['author_email']) if build['author_email'] else build['author_email'] 260 | if build['vcs_tag']: 261 | o['Tag '] = build['vcs_tag'] 262 | else: 263 | o['Branch '] = build['branch'] or 'Unknown' 264 | dt = dp.parse(build.get('queued_at', build.get('usage_queued_at'))).astimezone(tz.tzlocal()) 265 | o['Queued '] = dt.strftime('%a, %b %d, %Y %I:%M%p %Z') 266 | o['Trigger'] = build['why'] 267 | o['URL '] = build['build_url'] 268 | o['Result '] = build['outcome'] 269 | resp.insert(0, o) 270 | 271 | return resp 272 | 273 | def artifacts(self, username, project, build_num, verbose=False, filters={}): 274 | """List the artifacts produced by a given build. 275 | 276 | Args: 277 | username (str): the owner of the project 278 | project (str): the project name 279 | build_num (int): the build number 280 | verbose (bool): whether to return filtered info or the full response 281 | filters (dict): a set of key/value pairs to match 282 | 283 | Returns: 284 | (list) the artifacts produced by the build 285 | """ 286 | r_json = self._filter(self._get('project/{username}/{project}/{build_num}/artifacts'.format(**locals())), filters) 287 | if verbose: 288 | return json.dumps(r_json, indent=2) 289 | return [ar['url'] for ar in r_json] 290 | 291 | def retry_build(self, username, project, build_num, verbose=False, filters={}): 292 | """Retry a given build. 293 | 294 | Args: 295 | username (str): the owner of the project 296 | project (str): the project name 297 | build_num (int): the build number 298 | verbose (bool): whether to return filtered info or the full response 299 | filters (dict): a set of key/value pairs to match 300 | 301 | Returns: 302 | (dict) a summary of the new build 303 | """ 304 | r_json = self._filter(self._post('project/{username}/{project}/{build_num}/retry'.format(**locals())), filters) 305 | if verbose: 306 | return json.dumps(r_json, indent=2) 307 | 308 | resp = OrderedDict() 309 | resp['Build# '] = r_json['build_num'] 310 | resp['Author '] = '{} <{}>'.format(r_json['author_name'], r_json['author_email']) if r_json['author_email'] else r_json['author_email'] 311 | if r_json['vcs_tag']: 312 | resp['Tag '] = r_json['vcs_tag'] 313 | else: 314 | resp['Branch '] = r_json['branch'] or 'Unknown' 315 | dt = dp.parse(r_json.get('queued_at', r_json.get('usage_queued_at'))).astimezone(tz.tzlocal()) 316 | resp['Queued '] = dt.strftime('%a, %b %d, %Y %I:%M%p %Z') 317 | resp['Trigger'] = r_json['why'] 318 | resp['URL '] = r_json['build_url'] 319 | resp['Result '] = r_json['outcome'] 320 | 321 | return resp 322 | 323 | def cancel_build(self, username, project, build_num, verbose=False, filters={}): 324 | """Cancel a given build. 325 | 326 | Args: 327 | username (str): the owner of the project 328 | project (str): the project name 329 | build_num (int): the build number 330 | verbose (bool): whether to return filtered info or the full response 331 | filters (dict): a set of key/value pairs to match 332 | 333 | Returns: 334 | (dict) a summary of the canceled build 335 | """ 336 | r_json = self._filter(self._post("project/{username}/{project}/{build_num}/cancel".format(**locals())), filters) 337 | if verbose: 338 | return json.dumps(r_json, indent=2) 339 | 340 | resp = OrderedDict() 341 | resp['Build# '] = r_json['build_num'] 342 | resp['Author '] = '{} <{}>'.format(r_json['author_name'], r_json['author_email']) if r_json['author_email'] else r_json['author_email'] 343 | if r_json['vcs_tag']: 344 | resp['Tag '] = r_json['vcs_tag'] 345 | else: 346 | resp['Branch '] = r_json['branch'] or 'Unknown' 347 | dt = dp.parse(r_json.get('queued_at', r_json.get('usage_queued_at'))).astimezone(tz.tzlocal()) 348 | resp['Queued '] = dt.strftime('%a, %b %d, %Y %I:%M%p %Z') 349 | resp['Trigger'] = r_json['why'] 350 | resp['URL '] = r_json['build_url'] 351 | resp['Result '] = r_json['outcome'] 352 | 353 | return resp 354 | 355 | def ssh_users(self, username, project, build_num, verbose=False, filters={}): 356 | """Add a user to the build's SSH permissions. 357 | 358 | Args: 359 | username (str): the owner of the project 360 | project (str): the project name 361 | build_num (int): the build number 362 | verbose (bool): whether to return filtered info or the full response 363 | filters (dict): a set of key/value pairs to match 364 | 365 | Returns: 366 | (dict) confirmation of the added user 367 | """ 368 | raise NotImplementedError(u"This method has not yet been implemented.") 369 | 370 | def new_build(self, username, project, branch="master", data=None, verbose=False, 371 | filters={}): 372 | """Trigger a new build. 373 | 374 | Args: 375 | username (str): the owner of the project 376 | project (str): the project name 377 | branch (str): the branch to use for the build 378 | verbose (bool): whether to return filtered info or the full response 379 | filters (dict): a set of key/value pairs to match 380 | 381 | Returns: 382 | (dict) a summary of the new build 383 | """ 384 | r_json = self._filter(self._post("project/{username}/{project}/tree/{branch}".format(**locals()), 385 | data=data), filters) 386 | if verbose: 387 | return json.dumps(r_json, indent=2) 388 | 389 | resp = OrderedDict() 390 | resp['Build# '] = r_json['build_num'] 391 | resp['Author '] = '{} <{}>'.format(r_json['author_name'], r_json['author_email']) if r_json['author_email'] else r_json['author_email'] 392 | if r_json['vcs_tag']: 393 | resp['Tag '] = r_json['vcs_tag'] 394 | else: 395 | resp['Branch '] = r_json['branch'] or 'Unknown' 396 | dt = dp.parse(r_json['queued_at']).astimezone(tz.tzlocal()) 397 | resp['Queued '] = dt.strftime('%a, %b %d, %Y %I:%M%p %Z') 398 | resp['Trigger'] = r_json['why'] 399 | resp['URL '] = r_json['build_url'] 400 | resp['Result '] = r_json['outcome'] 401 | 402 | return resp 403 | 404 | def create_ssh(self, username, project, filters={}): 405 | """Create an SSH key used to access key-based external systems. 406 | 407 | Args: 408 | username (str): the owner of the project 409 | project (str): the project name 410 | filters (dict): a set of key/value pairs to match 411 | 412 | Returns: 413 | (dict) confirmation of the added key 414 | """ 415 | raise NotImplementedError(u"This method has not yet been implemented.") 416 | 417 | def list_checkout_keys(self, username, project, filters={}): 418 | """List checkout keys. 419 | 420 | Args: 421 | username (str): the owner of the project 422 | project (str): the project name 423 | filters (dict): a set of key/value pairs to match 424 | 425 | Returns: 426 | (list) the checkout keys 427 | """ 428 | return self._filter(self._get('project/{username}/{project}/checkout-key'.format(**locals())), filters) 429 | 430 | def create_checkout_key(self, username, project, filters={}): 431 | """List checkout keys. 432 | 433 | Args: 434 | username (str): the owner of the project 435 | project (str): the project name 436 | filters (dict): a set of key/value pairs to match 437 | 438 | Returns: 439 | (dict) confirmation of the added key 440 | """ 441 | raise NotImplementedError(u"This method has not yet been implemented.") 442 | 443 | def checkout_key(self, username, project, fingerprint, filters={}): 444 | """Get a checkout key. 445 | 446 | Args: 447 | username (str): the owner of the project 448 | project (str): the project name 449 | fingerprint (str): the fingerprint of the checkout key 450 | filters (dict): a set of key/value pairs to match 451 | 452 | Returns: 453 | (dict) a single checkout key 454 | """ 455 | return self._filter(self._get('project/{username}/{project}/checkout-key/{fingerprint}'.format(**locals())), filters) 456 | 457 | def delete_checkout_key(self, username, project, fingerprint, filters={}): 458 | """Delete a checkout key. 459 | 460 | Args: 461 | username (str): the owner of the project 462 | project (str): the project name 463 | fingerprint (str): the fingerprint of the checkout key 464 | filters (dict): a set of key/value pairs to match 465 | 466 | Returns: 467 | (dict) a single checkout key 468 | """ 469 | return self._filter(self._delete('project/{username}/{project}/checkout-key/{fingerprint}'.format(**locals())), filters) 470 | 471 | def clear_cache(self, username, project, verbose=False): 472 | """Clear the cache for a project. 473 | 474 | Args: 475 | username (str): the owner of the project 476 | project (str): the project name 477 | verbose (bool): whether to return filtered info or the full response 478 | 479 | Returns: 480 | (dict) confirmation of the cleared cache 481 | """ 482 | r_json = self._delete('project/{username}/{project}/build-cache'.format(**locals())) 483 | if verbose: 484 | return json.dumps(r_json, indent=2) 485 | 486 | resp = OrderedDict() 487 | resp['status'] = r_json['status'] 488 | 489 | return resp 490 | 491 | def add_circle_key(self): 492 | """Add a CircleCI key to your GitHub user account. 493 | 494 | Returns: 495 | (dict) confirmation of the key addition 496 | """ 497 | raise NotImplementedError(u"This method has not yet been implemented.") 498 | 499 | def add_heroku_key(self): 500 | """Add your Heroku API key to CircleCI. 501 | 502 | Returns: 503 | (dict) confirmation of the key addition 504 | """ 505 | raise NotImplementedError(u"This method has not yet been implemented.") 506 | 507 | def envvar(self, username, project, verbose=False, filters={}, **envvars): 508 | """List or add environment variables for a project. 509 | 510 | Args: 511 | username (str): the owner of the project 512 | project (str): the project name 513 | verbose (bool): whether to return filtered info or the full response 514 | filters (dict): a set of key/value pairs to match 515 | **envvars (dict): variables to set 516 | 517 | Return: 518 | (dict/list) confirmation of the variable addition or list of variables 519 | """ 520 | data = [{"name": k, "value": v} for k, v in envvars.iteritems()] 521 | 522 | resp = [] 523 | if len(data) > 0: 524 | for d in data: 525 | r_json = self._post("project/{username}/{project}/envvar".format(**locals()), 526 | data=json.dumps(d)) 527 | if r_json: 528 | resp.append(r_json) 529 | else: 530 | r_json = self._filter(self._get("project/{username}/{project}/envvar".format(**locals())), filters) 531 | if r_json: 532 | resp.extend(r_json) 533 | 534 | if verbose: 535 | return json.dumps(resp, indent=2) 536 | 537 | formatted_resp = [] 538 | for var in resp: 539 | formatted_resp.append("{}: {}".format(var['name'], var['value'])) 540 | return formatted_resp 541 | -------------------------------------------------------------------------------- /circlecli/circleutils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Utils for CircleCI.""" 3 | import os.path as op 4 | import requests 5 | import yaml 6 | from bs4 import BeautifulSoup 7 | 8 | 9 | class InvalidNameError(Exception): 10 | def __init__(self, message, *args, **kwargs): 11 | super(InvalidNameError, self).__init__(message, *args, **kwargs) 12 | 13 | 14 | class UnrecognizedSectionError(Exception): 15 | def __init__(self, message, *args, **kwargs): 16 | super(UnrecognizedSectionError, self).__init__(message, *args, **kwargs) 17 | 18 | 19 | class InvalidSectionError(Exception): 20 | def __init__(self, message, *args, **kwargs): 21 | super(InvalidSectionError, self).__init__(message, *args, **kwargs) 22 | 23 | 24 | def _errant_items(items, allowed_items): 25 | """Determine if any items are not allowed. 26 | 27 | Args: 28 | items (list): items to evaluate 29 | allowed_items (list): the only allowed items 30 | 31 | Returns: 32 | (set) unique errant values in `items` 33 | """ 34 | if not isinstance(allowed_items, set): 35 | allowed_items = set(allowed_items) 36 | if not isinstance(items, set): 37 | items = set(items) 38 | 39 | return items.difference(allowed_items) 40 | 41 | 42 | def validate_circle_yml(filepath): 43 | """Ensure a circle.yml file is valid according to CircleCI docs. 44 | 45 | Args: 46 | filepath (str): path to the circle.yml file 47 | 48 | Returns: 49 | (bool) True if a valid circle.yml 50 | """ 51 | # obviously the file must be named 'circle.yml' 52 | if op.basename(filepath) != 'circle.yml': 53 | raise InvalidNameError(u"Filename must be 'circle.yml'") 54 | 55 | allowed_sections = {'checkout', 'database', 'dependencies', 'deployment', 56 | 'experimental', 'general', 'machine', 'notify', 'test'} 57 | fd = open(filepath, 'r') # let it raise an IOError if no file exists 58 | circle_yml = yaml.load(fd) # will throw a ScannerError if not valid YaML 59 | fd.close() 60 | circle_sections = circle_yml.keys() 61 | 62 | # check for valid sections 63 | unrecognized_sections = _errant_items(circle_sections, allowed_sections) 64 | if len(unrecognized_sections) > 0: 65 | # we have an unrecognized section 66 | raise UnrecognizedSectionError(u"The following sections are unrecognized: {}".format(", ".join(unrecognized_sections))) 67 | 68 | # check each section 69 | for section in circle_sections: 70 | if section == 'machine': 71 | conditions = {'pre', 'post'} # override not allowed 72 | languages = {'ghc', 'java', 'node', 'php', 'python', 'ruby', 'xcode'} 73 | system = {'environment', 'hosts', 'services', 'timezone'} 74 | allowed = conditions.union(languages).union(system) 75 | try: 76 | subsections = circle_yml[section].keys() 77 | except AttributeError: 78 | raise InvalidSectionError(u"Invalid subsection format in '{}'".format(section)) 79 | 80 | # check for valid subsections 81 | invalid_sections = _errant_items(subsections, allowed) 82 | if len(invalid_sections) > 0: 83 | # we have an invalid section 84 | raise UnrecognizedSectionError(u"Subsections not allowed in '{}': {}".format(section, ", ".join(invalid_sections))) 85 | 86 | # check each subsection 87 | for subsection in subsections: 88 | item = circle_yml[section][subsection] 89 | if subsection in ('environment', 'hosts'): # dict requirement 90 | if not isinstance(item, dict): 91 | raise InvalidSectionError(u"Invalid subitem format in '{}.{}'".format(section, subsection)) 92 | elif subsection == 'timezone': # string requirement 93 | if not isinstance(item, basestring): 94 | raise InvalidSectionError(u"'{}.{}' subsection must be a single string".format(subsection)) 95 | elif subsection == 'services': # list requirement 96 | if not isinstance(item, list): 97 | raise InvalidSectionError(u"'{}.{}' subsection must be a list".format(subsection)) 98 | elif subsection in languages: 99 | if not isinstance(item, dict) or len(item.keys()) != 1 or not item.get('version'): 100 | raise InvalidSectionError(u"'{}.{}' subsection only supports 'version'".format(section, subsection)) 101 | elif section == 'checkout': 102 | allowed = {'post'} 103 | try: 104 | subsections = circle_yml[section].keys() 105 | except AttributeError: 106 | raise InvalidSectionError(u"Invalid subsection format in '{}'".format(section)) 107 | 108 | # check for valid subsections 109 | invalid_sections = _errant_items(subsections, allowed) 110 | if len(invalid_sections) > 0: 111 | # we have an invalid section 112 | raise UnrecognizedSectionError(u"Subsections not allowed in '{}': {}".format(section, ", ".join(invalid_sections))) 113 | 114 | # check the only subsection 115 | item = circle_yml[section]['post'] 116 | if not isinstance(item, list): 117 | raise InvalidSectionError(u"'{}' section must be a list".format('post')) 118 | elif section == 'dependencies': 119 | conditions = {'pre', 'override', 'post'} 120 | misc = {'bundler', 'cache_directories'} 121 | allowed = conditions.union(misc) 122 | try: 123 | subsections = circle_yml[section].keys() 124 | except AttributeError: 125 | raise InvalidSectionError(u"Invalid subsection format in '{}'".format(section)) 126 | 127 | # check for valid subsections 128 | invalid_sections = _errant_items(subsections, allowed) 129 | if len(invalid_sections) > 0: 130 | # we have an invalid section 131 | raise UnrecognizedSectionError(u"Subsections not allowed in '{}': {}".format(section, ", ".join(invalid_sections))) 132 | 133 | # check each subsection 134 | for subsection in subsections: 135 | item = circle_yml[section][subsection] 136 | if subsection in conditions or subsection == 'cache_directories': 137 | if not isinstance(item, list): 138 | raise InvalidSectionError(u"'{}' section must be a list".format(subsection)) 139 | else: 140 | if not isinstance(item, dict): 141 | raise InvalidSectionError(u"'{}.{}' subsection only supports 'without'".format(section, subsection)) 142 | allowed_subitems = {'without'} 143 | subitems = circle_yml[section][subsection].keys() 144 | 145 | # check for valid subitems 146 | invalid_subitems = _errant_items(subitems, allowed_subitems) 147 | if len(invalid_subitems) > 0: 148 | # we have an invalid section 149 | raise UnrecognizedSectionError(u"Subitems not allowed in '{}.{}': {}".format(section, subsection, ", ".join(invalid_subitems))) 150 | 151 | subsubitem = circle_yml[section][subsection]['without'] 152 | if not isinstance(subsubitem, list): 153 | raise InvalidSectionError(u"'{}.{}.{}' subitem must be a list".format(section, subsection, 'without')) 154 | elif section == 'database': 155 | allowed = {'pre', 'override', 'post'} 156 | try: 157 | subsections = circle_yml[section].keys() 158 | except AttributeError: 159 | raise InvalidSectionError(u"Invalid subsection format in '{}'".format(section)) 160 | 161 | # check for valid subsections 162 | invalid_sections = _errant_items(subsections, allowed) 163 | if len(invalid_sections) > 0: 164 | # we have an invalid section 165 | raise UnrecognizedSectionError(u"Subsections not allowed in '{}': {}".format(section, ", ".join(invalid_sections))) 166 | 167 | # check each subsection 168 | for subsection in subsections: 169 | item = circle_yml[section][subsection] 170 | if not isinstance(item, list): 171 | raise InvalidSectionError(u"'{}' section must be a list".format(subsection)) 172 | elif section == 'test': 173 | conditions = {'pre', 'override', 'post'} 174 | misc = {'minitest_globs'} 175 | allowed = conditions.union(misc) 176 | try: 177 | subsections = circle_yml[section].keys() 178 | except AttributeError: 179 | raise InvalidSectionError(u"Invalid subsection format in '{}'".format(section)) 180 | 181 | # check for valid subsections 182 | invalid_sections = _errant_items(subsections, allowed) 183 | if len(invalid_sections) > 0: 184 | # we have an invalid section 185 | raise UnrecognizedSectionError(u"Subsections not allowed in '{}': {}".format(section, ", ".join(invalid_sections))) 186 | 187 | # check each subsection 188 | for subsection in subsections: 189 | item = circle_yml[section][subsection] 190 | if not isinstance(item, list): 191 | raise InvalidSectionError(u"'{}' section must be a list".format(subsection)) 192 | elif section == 'deployment': 193 | # all subsection names are allowed except pre, override, post 194 | disallowed = {'pre', 'override', 'post'} 195 | try: 196 | subsections = circle_yml[section].keys() 197 | except AttributeError: 198 | raise InvalidSectionError(u"Invalid subsection format in '{}'".format(section)) 199 | 200 | # check for valid subsections 201 | invalid_sections = disallowed.intersection(set(subsections)) 202 | if len(invalid_sections) > 0: 203 | # we have an invalid section 204 | raise UnrecognizedSectionError(u"Subsections not allowed in '{}': {}".format(section, ", ".join(invalid_sections))) 205 | 206 | # check each subsection 207 | for subsection in subsections: 208 | allowed = {'branch', 'commands', 'heroku', 'owner', 'tag'} 209 | try: 210 | subitems = circle_yml[section][subsection].keys() 211 | except AttributeError: 212 | raise InvalidSectionError(u"Invalid subitem format in '{}.{}'".format(section, subsection)) 213 | 214 | # check for valid subitems 215 | invalid_subitems = _errant_items(subitems, allowed) 216 | if len(invalid_subitems) > 0: 217 | # we have an invalid subitem 218 | raise UnrecognizedSectionError(u"Subitems not allowed in '{}.{}': {}".format(section, subsection, ", ".join(invalid_subitems))) 219 | 220 | required = {'branch', 'tag'} 221 | found = required.intersection(set(subitems)) 222 | if len(found) == 0: 223 | raise InvalidSectionError(u"'branch' or 'tag' required in '{}.{}'".format(section, subsection)) 224 | branch = circle_yml[section][subsection][found.pop()] 225 | if not isinstance(branch, basestring) and not isinstance(branch, list): 226 | raise InvalidSectionError(u"'branch' value not a list or string in '{}.{}'".format(section, subsection)) 227 | commands = circle_yml[section][subsection].get('commands') 228 | if commands and not isinstance(commands, list): 229 | raise InvalidSectionError(u"'{}.{}.{}' subitem must be a list".format(section, subsection, 'commands')) 230 | heroku = circle_yml[section][subsection].get('heroku') 231 | if heroku and not isinstance(heroku, dict): 232 | raise InvalidSectionError(u"Invalid subitem format in '{}.{}.{}'".format(section, subsection, 'heroku')) 233 | owner = circle_yml[section][subsection].get('owner') 234 | if owner and not isinstance(owner, basestring): 235 | raise InvalidSectionError(u"'{}.{}.{}' subitem must be a string".format(section, subsection, 'owner')) 236 | elif section == 'notify': 237 | allowed = {'webhooks'} 238 | try: 239 | subsections = circle_yml[section].keys() 240 | except AttributeError: 241 | raise InvalidSectionError(u"Invalid subsection format in '{}'".format(section)) 242 | 243 | # check for valid subsections 244 | invalid_sections = _errant_items(subsections, allowed) 245 | if len(invalid_sections) > 0: 246 | # we have an invalid section 247 | raise UnrecognizedSectionError(u"Subsections not allowed in '{}': {}".format(section, ", ".join(invalid_sections))) 248 | 249 | webhooks = circle_yml[section]['webhooks'] 250 | if not isinstance(webhooks, list): 251 | raise InvalidSectionError(u"'{}.{}' subsection must be a list".format(section, 'webhooks')) 252 | for url_address in webhooks: 253 | if not isinstance(url_address, dict) or not url_address.get('url'): 254 | raise InvalidSectionError(u"'{}.{}' subsection must be a list of 'url: ' items".format(section, 'webhooks')) 255 | elif section == 'general': 256 | allowed = {'artifacts', 'branches', 'build_dir'} 257 | try: 258 | subsections = circle_yml[section].keys() 259 | except AttributeError: 260 | raise InvalidSectionError(u"Invalid subsection format in '{}'".format(section)) 261 | 262 | # check for valid subsections 263 | invalid_sections = _errant_items(subsections, allowed) 264 | if len(invalid_sections) > 0: 265 | # we have an invalid section 266 | raise UnrecognizedSectionError(u"Subsections not allowed in '{}': {}".format(section, ", ".join(invalid_sections))) 267 | 268 | for subsection in subsections: 269 | if subsection == 'artifacts': 270 | item = circle_yml[section][subsection] 271 | if not isinstance(item, list): 272 | raise InvalidSectionError(u"'{}.{}' subsection must be a list".format(section, subsection)) 273 | elif subsection == 'branches': 274 | allowed_subitems = {'ignore', 'only'} 275 | try: 276 | subitems = circle_yml[section][subsection].keys() 277 | except AttributeError: 278 | raise InvalidSectionError(u"Invalid subitem format in '{}.{}'".format(section, subsection)) 279 | 280 | # check for valid subitems 281 | invalid_subitems = _errant_items(subitems, allowed_subitems) 282 | if len(invalid_subitems) > 0: 283 | # we have an invalid subitem 284 | raise UnrecognizedSectionError(u"Subitems not allowed in '{}.{}': {}".format(section, ", ".join(invalid_sections))) 285 | 286 | for subitem in subitems: 287 | item = circle_yml[section][subsection][subitem] 288 | if not isinstance(item, list): 289 | raise InvalidSectionError(u"'{}.{}.{}' subitem must be a list".format(section, subsection, subitem)) 290 | elif subsection == 'build_dir': 291 | item = circle_yml[section][subsection] 292 | if not isinstance(item, basestring): 293 | raise InvalidSectionError(u"'{}.{}' subsection must be a string".format(section, subsection)) 294 | elif section == 'experimental': 295 | allowed = {'notify'} # currently only notify 296 | try: 297 | subsections = circle_yml[section].keys() 298 | except AttributeError: 299 | raise InvalidSectionError(u"Invalid subsection format in '{}'".format(section)) 300 | 301 | # check for valid subsections 302 | invalid_sections = _errant_items(subsections, allowed) 303 | if len(invalid_sections) > 0: 304 | # we have an invalid section 305 | raise UnrecognizedSectionError(u"Subsections not allowed in '{}': {}".format(section, ", ".join(invalid_sections))) 306 | 307 | allowed_subitems = {'branches'} 308 | try: 309 | subitems = circle_yml[section]['notify'].keys() 310 | except AttributeError: 311 | raise InvalidSectionError(u"Invalid subitem format in '{}.{}'".format(section, 'notify')) 312 | 313 | # check for valid subitems 314 | invalid_subitems = _errant_items(subitems, allowed_subitems) 315 | if len(invalid_subitems) > 0: 316 | # we have an invalid section 317 | raise UnrecognizedSectionError(u"Subitems not allowed in '{}.{}': {}".format(section, 'notify', ", ".join(invalid_subitems))) 318 | 319 | for subitem in subitems: 320 | allowed_subsubitems = {'ignore', 'only'} 321 | try: 322 | subsubitems = circle_yml[section]['notify'][subitem].keys() 323 | except AttributeError: 324 | raise InvalidSectionError(u"Invalid subitem format in '{}.{}.{}'".format(section, 'notify', subitem)) 325 | 326 | # check for valid subsubitems 327 | invalid_subsubitems = _errant_items(subsubitems, allowed_subsubitems) 328 | if len(invalid_subsubitems) > 0: 329 | # we have an invalid subsubitem 330 | raise UnrecognizedSectionError(u"Subitems not allowed in '{}.{}.{}': {}".format(section, 'notify', subitem, ", ".join(invalid_subsubitems))) 331 | 332 | for subsubitem in subsubitems: 333 | item = circle_yml[section]['notify'][subitem][subsubitem] 334 | if not isinstance(item, list): 335 | raise InvalidSectionError(u"'{}.{}.{}.{}' subitem must be a list".format(section, 'notify', subitem, subsubitem)) 336 | return True 337 | 338 | 339 | # status values for CircleCI status page (subject to change) 340 | CIRCLE_GREEN_COMPONENT = 'Operational' 341 | CIRCLE_GREEN_PAGE = 'All Systems Operational' 342 | 343 | 344 | def circle_status(verbose=False): 345 | """Check CircleCI site status (http://status.circleci.com).""" 346 | status_page = requests.get('http://status.circleci.com') 347 | soup = BeautifulSoup(status_page.content, "html.parser") 348 | 349 | response = [] 350 | # get overall page status 351 | page_component = soup.select('.page-status > .status') 352 | page_component = page_component[0] if len(page_component) > 0 else None 353 | if not page_component: 354 | # check if there is an incident 355 | page_incident = soup.select('.incident-title .actual-title') 356 | if page_incident: 357 | incident_title = page_incident[0].find(text=True).strip() 358 | response.append((None, incident_title)) 359 | else: 360 | # we don't know what's wrong 361 | response.append((None, "Could not determine CircleCI status")) 362 | return response 363 | 364 | page_status = page_component.find(text=True).strip() 365 | response.append((None, page_status)) 366 | 367 | # get component status 368 | all_components = [] 369 | components = soup.select('.components-section .component-inner-container') 370 | for component in components: 371 | name = component.select('.name') 372 | name = name[0].find(text=True).strip() if len(name) > 0 else "Unknown" 373 | 374 | status = component.select('.component-status') 375 | status = status[0].find(text=True).strip() if len(status) > 0 else "Unknown" 376 | 377 | all_components.append((name, status)) 378 | 379 | if not verbose: 380 | if response[0][1] != CIRCLE_GREEN_PAGE: 381 | bad_components = [c for c in all_components if c[1] != CIRCLE_GREEN_COMPONENT] 382 | return bad_components 383 | else: 384 | return response 385 | 386 | response.extend(all_components) 387 | return response -------------------------------------------------------------------------------- /mocks/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /mocks/circlecli.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Mocks for the CircleCLI API library tests. 5 | 6 | """ 7 | 8 | 9 | from httmock import response, urlmatch 10 | 11 | NETLOC = r'(.*\.)?[^.]+\.com$' 12 | HEADERS = {'content-type': 'application/json'} 13 | GET = 'get' 14 | DELETE = 'delete' 15 | 16 | class Resource: 17 | """ A CircleCli resource. 18 | 19 | :param path: The file path to the resource. 20 | 21 | """ 22 | 23 | def __init__(self, path): 24 | self.path = path 25 | 26 | def get(self): 27 | """ Perform a GET request on the resource. 28 | 29 | :rtype: str 30 | 31 | """ 32 | with open(self.path, 'r') as f: 33 | content = f.read() 34 | return content 35 | 36 | 37 | def delete(self): 38 | """ Perform a GET request on the resource. 39 | 40 | :rtype: str 41 | 42 | """ 43 | with open(self.path, 'r') as f: 44 | content = f.read() 45 | return content 46 | 47 | @urlmatch(netloc=NETLOC, method=GET) 48 | def resource_get(url, request): 49 | file_path = 'mocks/get/' + url.netloc + url.path 50 | file_path = file_path.rstrip('/') # in case of blank url.path 51 | try: 52 | content = Resource(file_path).get() 53 | except EnvironmentError: 54 | # catch any environment errors (i.e. file does not exist) and return a 55 | # 404. 56 | return response(404, {}, HEADERS, None, 5, request) 57 | return response(200, content, HEADERS, None, 5, request) 58 | 59 | @urlmatch(netloc=NETLOC, method=DELETE) 60 | def resource_delete(url, request): 61 | file_path = 'mocks/delete/' + url.netloc + url.path 62 | file_path = file_path.rstrip('/') # in case of blank url.path 63 | try: 64 | content = Resource(file_path).delete() 65 | except EnvironmentError: 66 | # catch any environment errors (i.e. file does not exist) and return a 67 | # 404. 68 | return response(404, {}, HEADERS, None, 5, request) 69 | return response(200, content, HEADERS, None, 5, request) 70 | -------------------------------------------------------------------------------- /mocks/delete/circleci.com/api/v1/project/therealbarack/circlecli/build-cache: -------------------------------------------------------------------------------- 1 | { 2 | "status" : "build caches deleted" 3 | } 4 | -------------------------------------------------------------------------------- /mocks/get/circleci.com/api/v1/me: -------------------------------------------------------------------------------- 1 | { 2 | "enrolled_betas" : [ ], 3 | "in_beta_program" : true, 4 | "selected_email" : "potus@whitehouse.gov", 5 | "avatar_url" : "https://avatars.githubusercontent.com/u/8675309?v=3", 6 | "trial_end" : "2015-10-31T07:05:29.209Z", 7 | "admin" : null, 8 | "basic_email_prefs" : "smart", 9 | "sign_in_count" : 44, 10 | "github_oauth_scopes" : [ "user:email", "repo" ], 11 | "analytics_id" : "4ed02b0a-af20-f612-8b03-444fc2f1691a", 12 | "name" : "Barack Obama", 13 | "gravatar_id" : null, 14 | "days_left_in_trial" : -253, 15 | "parallelism" : 1, 16 | "bitbucket_authorized" : true, 17 | "github_id" : 8675309, 18 | "bitbucket" : { 19 | "id" : "{1d42cca1-58c1-49f6-9b5b-be3f358ba40e}", 20 | "login" : "therealbarack" 21 | }, 22 | "dev_admin" : false, 23 | "all_emails" : [ "potus@whitehouse.gov", "b-man@whalumni.gov" ], 24 | "created_at" : "2010-01-20T017:10:00.103Z", 25 | "plan" : null, 26 | "heroku_api_key" : null, 27 | "projects" : { 28 | "https://github.com/therealbarack/circlecli" : { 29 | "on_dashboard" : true, 30 | "emails" : "default" 31 | }, 32 | "https://github.com/therealbarack/unstuff" : { 33 | "on_dashboard" : true, 34 | "emails" : "default" 35 | }, 36 | "https://github.com/therealbarack/kidshomework" : { 37 | "on_dashboard" : true, 38 | "emails" : "default" 39 | } 40 | }, 41 | "login" : "therealbarack", 42 | "organization_prefs" : { }, 43 | "containers" : 1, 44 | "pusher_id" : "559adb9619ed1f53e3e5d4bd81a158f35f21d0dc" 45 | } 46 | -------------------------------------------------------------------------------- /mocks/get/circleci.com/api/v1/project/therealbarack/circlecli/1: -------------------------------------------------------------------------------- 1 | { 2 | "compare" : null, 3 | "previous_successful_build" : null, 4 | "build_parameters" : null, 5 | "oss" : true, 6 | "all_commit_details_truncated" : false, 7 | "committer_date" : "2016-07-10T04:38:46Z", 8 | "steps" : [ { 9 | "name" : "Starting the build", 10 | "actions" : [ { 11 | "truncated" : false, 12 | "index" : 0, 13 | "parallel" : false, 14 | "failed" : null, 15 | "infrastructure_fail" : null, 16 | "name" : "Starting the build", 17 | "bash_command" : null, 18 | "status" : "success", 19 | "timedout" : null, 20 | "continue" : null, 21 | "end_time" : "2016-07-10T05:50:11.237Z", 22 | "type" : "infrastructure", 23 | "output_url" : "https://circle-production-action-output.s3.amazonaws.com/115d8e15d481871aaf8e4631692fe5291755bccb-therealbarack-circlecli-10-0?AWSAccessKeyId=115d8e15d481871aaf8e4631692fe5291755bccb&Expires=1626007390&Signature=115d8e15d481871aaf8e4631692fe5291755bccb", 24 | "start_time" : "2016-07-10T05:50:05.932Z", 25 | "exit_code" : null, 26 | "canceled" : null, 27 | "step" : 0, 28 | "run_time_millis" : 5305, 29 | "has_output" : true 30 | } ] 31 | }, { 32 | "name" : "Start container", 33 | "actions" : [ { 34 | "truncated" : false, 35 | "index" : 0, 36 | "parallel" : true, 37 | "failed" : null, 38 | "infrastructure_fail" : null, 39 | "name" : "Start container", 40 | "bash_command" : null, 41 | "status" : "success", 42 | "timedout" : null, 43 | "continue" : null, 44 | "end_time" : "2016-07-10T05:50:17.828Z", 45 | "source" : "config", 46 | "type" : "infrastructure", 47 | "output_url" : "https://circle-production-action-output.s3.amazonaws.com/115d8e15d481871aaf8e4631692fe5291755bccb-therealbarack-circlecli-10-0?AWSAccessKeyId=115d8e15d481871aaf8e4631692fe5291755bccb&Expires=1626007390&Signature=115d8e15d481871aaf8e4631692fe5291755bccb", 48 | "start_time" : "2016-07-10T05:50:11.244Z", 49 | "exit_code" : 0, 50 | "canceled" : null, 51 | "step" : 1, 52 | "run_time_millis" : 6584, 53 | "has_output" : true 54 | } ] 55 | }, { 56 | "name" : "Enable SSH", 57 | "actions" : [ { 58 | "truncated" : false, 59 | "index" : 0, 60 | "parallel" : true, 61 | "failed" : null, 62 | "infrastructure_fail" : null, 63 | "name" : "Enable SSH", 64 | "bash_command" : null, 65 | "status" : "success", 66 | "timedout" : null, 67 | "continue" : null, 68 | "end_time" : "2016-07-10T05:50:19.686Z", 69 | "type" : "infrastructure", 70 | "output_url" : "https://circle-production-action-output.s3.amazonaws.com/115d8e15d481871aaf8e4631692fe5291755bccb-therealbarack-circlecli-10-0?AWSAccessKeyId=115d8e15d481871aaf8e4631692fe5291755bccb&Expires=1626007390&Signature=115d8e15d481871aaf8e4631692fe5291755bccb", 71 | "start_time" : "2016-07-10T05:50:17.833Z", 72 | "exit_code" : null, 73 | "canceled" : null, 74 | "step" : 2, 75 | "run_time_millis" : 1853, 76 | "has_output" : true 77 | } ] 78 | }, { 79 | "name" : "Restore source cache", 80 | "actions" : [ { 81 | "truncated" : false, 82 | "index" : 0, 83 | "parallel" : false, 84 | "failed" : null, 85 | "infrastructure_fail" : null, 86 | "name" : "Restore source cache", 87 | "bash_command" : null, 88 | "status" : "success", 89 | "timedout" : null, 90 | "continue" : null, 91 | "end_time" : "2016-07-10T05:50:19.877Z", 92 | "source" : "cache", 93 | "type" : "checkout", 94 | "start_time" : "2016-07-10T05:50:19.693Z", 95 | "exit_code" : null, 96 | "canceled" : null, 97 | "step" : 3, 98 | "run_time_millis" : 184, 99 | "has_output" : false 100 | } ] 101 | }, { 102 | "name" : "Checkout using deploy key: 16:2d:54:cc:96:fd:f4:d8:40:b4:09:a4:40:55:fe:8c", 103 | "actions" : [ { 104 | "truncated" : false, 105 | "index" : 0, 106 | "parallel" : true, 107 | "failed" : null, 108 | "infrastructure_fail" : null, 109 | "name" : "Checkout using deploy key: 16:2d:54:cc:96:fd:f4:d8:40:b4:09:a4:40:55:fe:8c", 110 | "bash_command" : null, 111 | "status" : "success", 112 | "timedout" : null, 113 | "continue" : null, 114 | "end_time" : "2016-07-10T05:50:22.974Z", 115 | "source" : "config", 116 | "type" : "checkout", 117 | "output_url" : "https://circle-production-action-output.s3.amazonaws.com/115d8e15d481871aaf8e4631692fe5291755bccb-therealbarack-circlecli-10-0?AWSAccessKeyId=115d8e15d481871aaf8e4631692fe5291755bccb&Expires=1626007390&Signature=115d8e15d481871aaf8e4631692fe5291755bccb", 118 | "start_time" : "2016-07-10T05:50:19.882Z", 119 | "exit_code" : 0, 120 | "canceled" : null, 121 | "step" : 4, 122 | "run_time_millis" : 3092, 123 | "has_output" : true 124 | } ] 125 | }, { 126 | "name" : "Configure the build", 127 | "actions" : [ { 128 | "truncated" : false, 129 | "index" : 0, 130 | "parallel" : false, 131 | "failed" : null, 132 | "infrastructure_fail" : null, 133 | "name" : "Configure the build", 134 | "bash_command" : null, 135 | "status" : "success", 136 | "timedout" : null, 137 | "continue" : null, 138 | "end_time" : "2016-07-10T05:50:26.195Z", 139 | "source" : "cache", 140 | "type" : "machine", 141 | "start_time" : "2016-07-10T05:50:22.979Z", 142 | "exit_code" : null, 143 | "canceled" : null, 144 | "step" : 5, 145 | "run_time_millis" : 3216, 146 | "has_output" : false 147 | } ] 148 | }, { 149 | "name" : "Restore cache", 150 | "actions" : [ { 151 | "truncated" : false, 152 | "index" : 0, 153 | "parallel" : false, 154 | "failed" : null, 155 | "infrastructure_fail" : null, 156 | "name" : "Restore cache", 157 | "bash_command" : null, 158 | "status" : "success", 159 | "timedout" : null, 160 | "continue" : null, 161 | "end_time" : "2016-07-10T05:50:26.417Z", 162 | "source" : "cache", 163 | "type" : "machine", 164 | "start_time" : "2016-07-10T05:50:26.200Z", 165 | "exit_code" : null, 166 | "canceled" : null, 167 | "step" : 6, 168 | "run_time_millis" : 217, 169 | "has_output" : false 170 | } ] 171 | }, { 172 | "name" : "virtualenv", 173 | "actions" : [ { 174 | "truncated" : false, 175 | "index" : 0, 176 | "parallel" : true, 177 | "failed" : null, 178 | "infrastructure_fail" : null, 179 | "name" : "virtualenv", 180 | "bash_command" : null, 181 | "status" : "success", 182 | "timedout" : null, 183 | "continue" : null, 184 | "end_time" : "2016-07-10T05:50:30.658Z", 185 | "source" : "inference", 186 | "type" : "checkout", 187 | "output_url" : "https://circle-production-action-output.s3.amazonaws.com/115d8e15d481871aaf8e4631692fe5291755bccb-therealbarack-circlecli-10-0?AWSAccessKeyId=115d8e15d481871aaf8e4631692fe5291755bccb&Expires=1626007390&Signature=115d8e15d481871aaf8e4631692fe5291755bccb", 188 | "start_time" : "2016-07-10T05:50:26.422Z", 189 | "exit_code" : 0, 190 | "canceled" : null, 191 | "step" : 7, 192 | "run_time_millis" : 4236, 193 | "has_output" : true 194 | } ] 195 | }, { 196 | "name" : "pip install", 197 | "actions" : [ { 198 | "truncated" : false, 199 | "index" : 0, 200 | "parallel" : true, 201 | "failed" : null, 202 | "infrastructure_fail" : null, 203 | "name" : "pip install", 204 | "bash_command" : "if [ -e requirements.txt ]; then pip install -r requirements.txt;else pip install -r requirements.pip;fi", 205 | "status" : "success", 206 | "timedout" : null, 207 | "continue" : null, 208 | "end_time" : "2016-07-10T05:50:32.798Z", 209 | "source" : "inference", 210 | "type" : "dependencies", 211 | "output_url" : "https://circle-production-action-output.s3.amazonaws.com/115d8e15d481871aaf8e4631692fe5291755bccb-therealbarack-circlecli-10-0?AWSAccessKeyId=115d8e15d481871aaf8e4631692fe5291755bccb&Expires=1626007390&Signature=115d8e15d481871aaf8e4631692fe5291755bccb", 212 | "start_time" : "2016-07-10T05:50:30.665Z", 213 | "exit_code" : 0, 214 | "canceled" : null, 215 | "step" : 8, 216 | "run_time_millis" : 2133, 217 | "has_output" : true 218 | } ] 219 | }, { 220 | "name" : "python setup.py install", 221 | "actions" : [ { 222 | "truncated" : false, 223 | "index" : 0, 224 | "parallel" : true, 225 | "failed" : null, 226 | "infrastructure_fail" : null, 227 | "name" : "python setup.py install", 228 | "bash_command" : "python setup.py install", 229 | "status" : "success", 230 | "timedout" : null, 231 | "continue" : null, 232 | "end_time" : "2016-07-10T05:50:33.932Z", 233 | "source" : "inference", 234 | "type" : "dependencies", 235 | "output_url" : "https://circle-production-action-output.s3.amazonaws.com/115d8e15d481871aaf8e4631692fe5291755bccb-therealbarack-circlecli-10-0?AWSAccessKeyId=115d8e15d481871aaf8e4631692fe5291755bccb&Expires=1626007390&Signature=115d8e15d481871aaf8e4631692fe5291755bccb", 236 | "start_time" : "2016-07-10T05:50:32.805Z", 237 | "exit_code" : 0, 238 | "canceled" : null, 239 | "step" : 9, 240 | "run_time_millis" : 1127, 241 | "has_output" : true 242 | } ] 243 | }, { 244 | "name" : "Save cache", 245 | "actions" : [ { 246 | "truncated" : false, 247 | "index" : 0, 248 | "parallel" : false, 249 | "failed" : null, 250 | "infrastructure_fail" : null, 251 | "name" : "Save cache", 252 | "bash_command" : null, 253 | "status" : "success", 254 | "timedout" : null, 255 | "continue" : null, 256 | "end_time" : "2016-07-10T05:50:35.965Z", 257 | "source" : "cache", 258 | "type" : "database", 259 | "output_url" : "https://circle-production-action-output.s3.amazonaws.com/115d8e15d481871aaf8e4631692fe5291755bccb-therealbarack-circlecli-10-0?AWSAccessKeyId=115d8e15d481871aaf8e4631692fe5291755bccb&Expires=1626007390&Signature=115d8e15d481871aaf8e4631692fe5291755bccb", 260 | "start_time" : "2016-07-10T05:50:33.939Z", 261 | "exit_code" : null, 262 | "canceled" : null, 263 | "step" : 10, 264 | "run_time_millis" : 2026, 265 | "has_output" : true 266 | } ] 267 | }, { 268 | "name" : "Collect test metadata", 269 | "actions" : [ { 270 | "truncated" : false, 271 | "index" : 0, 272 | "parallel" : true, 273 | "failed" : null, 274 | "infrastructure_fail" : null, 275 | "name" : "Collect test metadata", 276 | "bash_command" : null, 277 | "status" : "success", 278 | "timedout" : null, 279 | "continue" : null, 280 | "end_time" : "2016-07-10T05:50:38.863Z", 281 | "type" : "teardown", 282 | "output_url" : "https://circle-production-action-output.s3.amazonaws.com/115d8e15d481871aaf8e4631692fe5291755bccb-therealbarack-circlecli-11-0?AWSAccessKeyId=115d8e15d481871aaf8e4631692fe5291755bccb&Expires=1626007390&Signature=115d8e15d481871aaf8e4631692fe5291755bccb", 283 | "start_time" : "2016-07-10T05:50:35.972Z", 284 | "exit_code" : null, 285 | "canceled" : null, 286 | "step" : 11, 287 | "run_time_millis" : 2891, 288 | "has_output" : true 289 | } ] 290 | }, { 291 | "name" : "Collect artifacts", 292 | "actions" : [ { 293 | "truncated" : false, 294 | "index" : 0, 295 | "parallel" : true, 296 | "failed" : null, 297 | "infrastructure_fail" : null, 298 | "name" : "Collect artifacts", 299 | "bash_command" : null, 300 | "status" : "success", 301 | "timedout" : null, 302 | "continue" : null, 303 | "end_time" : "2016-07-10T05:50:45.956Z", 304 | "type" : "teardown", 305 | "output_url" : "https://circle-production-action-output.s3.amazonaws.com/115d8e15d481871aaf8e4631692fe5291755bccb-therealbarack-circlecli-12-0?AWSAccessKeyId=115d8e15d481871aaf8e4631692fe5291755bccb&Expires=1626007390&Signature=115d8e15d481871aaf8e4631692fe5291755bccb", 306 | "start_time" : "2016-07-10T05:50:38.868Z", 307 | "exit_code" : null, 308 | "canceled" : null, 309 | "step" : 12, 310 | "run_time_millis" : 7088, 311 | "has_output" : true 312 | } ] 313 | }, { 314 | "name" : "Disable SSH", 315 | "actions" : [ { 316 | "truncated" : false, 317 | "index" : 0, 318 | "parallel" : true, 319 | "failed" : null, 320 | "infrastructure_fail" : null, 321 | "name" : "Disable SSH", 322 | "bash_command" : null, 323 | "status" : "success", 324 | "timedout" : null, 325 | "continue" : null, 326 | "end_time" : "2016-07-10T05:50:45.967Z", 327 | "type" : "teardown", 328 | "start_time" : "2016-07-10T05:50:45.962Z", 329 | "exit_code" : null, 330 | "canceled" : null, 331 | "step" : 15, 332 | "run_time_millis" : 5, 333 | "has_output" : false 334 | } ] 335 | } ], 336 | "body" : "", 337 | "usage_queued_at" : "2016-07-10T05:50:25.881Z", 338 | "fail_reason" : null, 339 | "retry_of" : null, 340 | "reponame" : "circlecli", 341 | "ssh_users" : [ ], 342 | "build_url" : "https://circleci.com/gh/therealbarack/circlecli/1", 343 | "parallel" : 1, 344 | "failed" : null, 345 | "branch" : "master", 346 | "username" : "therealbarack", 347 | "author_date" : "2016-07-10T04:38:46Z", 348 | "why" : "first-build", 349 | "user" : { 350 | "is_user" : true, 351 | "login" : "therealbarack", 352 | "avatar_url" : "https://avatars.githubusercontent.com/u/8675309?v=3", 353 | "name" : "Barack Obama", 354 | "vcs_type" : "github", 355 | "id" : 8675309 356 | }, 357 | "vcs_revision" : "115d8e15d481871aaf8e4631692fe5291755bccb", 358 | "owners" : [ "therealbarack" ], 359 | "vcs_tag" : null, 360 | "build_num" : 1, 361 | "infrastructure_fail" : false, 362 | "committer_email" : "potus@whitehouse.gov", 363 | "previous" : null, 364 | "status" : "no_tests", 365 | "committer_name" : "Barack Obama", 366 | "pull_request_urls" : [ ], 367 | "retries" : null, 368 | "subject" : "Improving the README significantly", 369 | "vcs_type" : "github", 370 | "timedout" : false, 371 | "dont_build" : null, 372 | "lifecycle" : "finished", 373 | "no_dependency_cache" : false, 374 | "stop_time" : "2016-07-10T05:50:46.100Z", 375 | "ssh_disabled" : false, 376 | "build_time_millis" : 40624, 377 | "circle_yml" : null, 378 | "messages" : [ { 379 | "type" : "warning", 380 | "message" : "It looks like we couldn't infer test settings for your project. Refer to our \"Setting your build up manually\" document to get started. It should only take a few minutes." 381 | } ], 382 | "is_first_green_build" : false, 383 | "job_name" : null, 384 | "start_time" : "2016-07-10T05:50:05.476Z", 385 | "canceler" : null, 386 | "all_commit_details" : [ { 387 | "committer_date" : "2016-07-10T04:38:46Z", 388 | "body" : "", 389 | "author_date" : "2016-07-10T04:38:46Z", 390 | "committer_email" : "potus@whitehouse.gov", 391 | "commit" : "115d8e15d481871aaf8e4631692fe5291755bccb", 392 | "committer_login" : "therealbarack", 393 | "committer_name" : "Barack Obama", 394 | "subject" : "Improving the README significantly", 395 | "commit_url" : "https://github.com/therealbarack/circlecli/commit/115d8e15d481871aaf8e4631692fe5291755bccb", 396 | "author_login" : "therealbarack", 397 | "author_name" : "Barack Obama", 398 | "author_email" : "barack@whitehouse.gov" 399 | } ], 400 | "outcome" : "no_tests", 401 | "vcs_url" : "https://github.com/therealbarack/circlecli", 402 | "author_name" : "Barack Obama", 403 | "node" : [ { 404 | "public_ip_addr" : "10.10.10.0", 405 | "port" : 64754, 406 | "username" : "ubuntu", 407 | "image_id" : "s3://lxc-images/circletar-1741-cc586-20160424T232626Z", 408 | "ssh_enabled" : null 409 | } ], 410 | "queued_at" : "2016-07-10T05:50:05.435Z", 411 | "canceled" : false, 412 | "author_email" : "potus@whitehouse.gov" 413 | } 414 | -------------------------------------------------------------------------------- /mocks/get/circleci.com/api/v1/project/therealbarack/circlecli/envvar: -------------------------------------------------------------------------------- 1 | [{"name":"foo","value":"xxxx"}] 2 | -------------------------------------------------------------------------------- /mocks/get/circleci.com/api/v1/projects: -------------------------------------------------------------------------------- 1 | [ { 2 | "irc_server" : null, 3 | "ssh_keys" : [ ], 4 | "branches" : { 5 | "master" : { 6 | "last_non_success" : { 7 | "outcome" : "failed", 8 | "status" : "failed", 9 | "build_num" : 21, 10 | "vcs_revision" : "115d8e15d481871aaf8e4631692fe5291755bccb", 11 | "pushed_at" : "2015-11-11T07:33:15.000Z", 12 | "added_at" : "2015-11-11T07:34:17.490Z" 13 | }, 14 | "last_success" : { 15 | "outcome" : "success", 16 | "status" : "fixed", 17 | "build_num" : 22, 18 | "vcs_revision" : "115d8e15d481871aaf8e4631692fe5291755bccb", 19 | "pushed_at" : "2015-11-11T07:35:47.000Z", 20 | "added_at" : "2015-11-11T07:36:46.087Z" 21 | }, 22 | "pusher_logins" : [ "therealbarack" ], 23 | "recent_builds" : [ { 24 | "outcome" : "success", 25 | "status" : "fixed", 26 | "build_num" : 22, 27 | "vcs_revision" : "115d8e15d481871aaf8e4631692fe5291755bccb", 28 | "pushed_at" : "2015-11-11T07:35:47.000Z", 29 | "added_at" : "2015-11-11T07:36:46.087Z" 30 | }, { 31 | "outcome" : "failed", 32 | "status" : "failed", 33 | "build_num" : 21, 34 | "vcs_revision" : "115d8e15d481871aaf8e4631692fe5291755bccb", 35 | "pushed_at" : "2015-11-11T07:33:15.000Z", 36 | "added_at" : "2015-11-11T07:34:17.490Z" 37 | }, { 38 | "outcome" : "failed", 39 | "status" : "failed", 40 | "build_num" : 20, 41 | "vcs_revision" : "115d8e15d481871aaf8e4631692fe5291755bccb", 42 | "pushed_at" : "2015-11-11T07:26:22.000Z", 43 | "added_at" : "2015-11-11T07:27:20.076Z" 44 | }, { 45 | "outcome" : "failed", 46 | "status" : "failed", 47 | "build_num" : 19, 48 | "vcs_revision" : "115d8e15d481871aaf8e4631692fe5291755bccb", 49 | "pushed_at" : "2015-11-11T07:22:57.000Z", 50 | "added_at" : "2015-11-11T07:24:53.798Z" 51 | }, { 52 | "outcome" : "failed", 53 | "status" : "failed", 54 | "build_num" : 18, 55 | "vcs_revision" : "115d8e15d481871aaf8e4631692fe5291755bccb", 56 | "pushed_at" : "2015-10-18T01:50:54.000Z", 57 | "added_at" : "2015-10-18T01:51:52.766Z" 58 | } ], 59 | "running_builds" : [ ] 60 | } 61 | }, 62 | "irc_keyword" : null, 63 | "oss" : true, 64 | "slack_channel" : null, 65 | "hipchat_notify_prefs" : null, 66 | "reponame" : "circlecli", 67 | "dependencies" : "", 68 | "aws" : { 69 | "keypair" : null 70 | }, 71 | "slack_webhook_url" : null, 72 | "irc_channel" : null, 73 | "parallel" : 1, 74 | "campfire_subdomain" : null, 75 | "followed" : true, 76 | "username" : "therealbarack", 77 | "campfire_notify_prefs" : null, 78 | "hipchat_notify" : null, 79 | "heroku_deploy_user" : null, 80 | "irc_username" : null, 81 | "slack_notify_prefs" : null, 82 | "scopes" : [ "write-settings", "view-builds", "read-settings", "trigger-builds", "all", "status", "none" ], 83 | "campfire_room" : null, 84 | "hipchat_api_token" : null, 85 | "campfire_token" : null, 86 | "slack_subdomain" : null, 87 | "has_usable_key" : true, 88 | "setup" : "", 89 | "vcs_type" : "github", 90 | "feature_flags" : { 91 | "trusty-beta" : false, 92 | "osx" : false, 93 | "set-github-status" : true, 94 | "build-prs-only" : false, 95 | "fleet" : null, 96 | "build-fork-prs" : false, 97 | "junit" : true, 98 | "oss" : true 99 | }, 100 | "irc_password" : null, 101 | "compile" : "", 102 | "irc_notify_prefs" : null, 103 | "extra" : "", 104 | "language" : "Python", 105 | "hipchat_room" : null, 106 | "flowdock_api_token" : null, 107 | "slack_channel_override" : null, 108 | "vcs_url" : "https://github.com/therealbarack/circlecli", 109 | "default_branch" : "master", 110 | "slack_api_token" : null, 111 | "test" : "" 112 | } ] 113 | -------------------------------------------------------------------------------- /mocks/get/circleci.com/api/v1/recent-builds: -------------------------------------------------------------------------------- 1 | [{ 2 | "compare" : "https://github.com/therealbarack/circlecli/compare/3012e5adc94c...b69cbfc3fb85", 3 | "previous_successful_build" : null, 4 | "build_parameters" : null, 5 | "oss" : true, 6 | "all_commit_details_truncated" : false, 7 | "committer_date" : "2016-07-11T09:03:37-04:00", 8 | "body" : "", 9 | "usage_queued_at" : "2016-07-11T13:04:08.314Z", 10 | "fail_reason" : null, 11 | "retry_of" : null, 12 | "reponame" : "circlecli", 13 | "ssh_users" : [ ], 14 | "build_url" : "https://circleci.com/gh/therealbarack/circlecli/69", 15 | "parallel" : 1, 16 | "failed" : true, 17 | "branch" : "unit-tests", 18 | "username" : "therealbarack", 19 | "author_date" : "2016-07-11T09:03:37-04:00", 20 | "why" : "github", 21 | "user" : { 22 | "is_user" : false, 23 | "login" : "therealbarack", 24 | "vcs_type" : "github" 25 | }, 26 | "vcs_revision" : "115d8e15d481871aaf8e4631692fe5291755bccb", 27 | "vcs_tag" : null, 28 | "build_num" : 69, 29 | "infrastructure_fail" : false, 30 | "committer_email" : "noreply@github.com", 31 | "has_artifacts" : true, 32 | "previous" : { 33 | "build_num" : 68, 34 | "status" : "fixed", 35 | "build_time_millis" : 77611 36 | }, 37 | "status" : "failed", 38 | "committer_name" : "GitHub", 39 | "retries" : null, 40 | "subject" : "corrected name of builds with project", 41 | "vcs_type" : "github", 42 | "timedout" : false, 43 | "dont_build" : null, 44 | "lifecycle" : "finished", 45 | "no_dependency_cache" : false, 46 | "stop_time" : "2016-07-11T13:04:26.139Z", 47 | "ssh_disabled" : false, 48 | "build_time_millis" : 48080, 49 | "circle_yml" : { 50 | "string" : "general:\n artifacts:\n - cover\n\ndependencies:\n override:\n - pip install -r requirements_dev.txt\n\ntest:\n override:\n - nosetests -v --cover-branches --with-coverage --cover-erase --cover-package=circlecli --cover-html --with-xunit\n post:\n - mkdir $CIRCLE_TEST_REPORTS/nosetests && cp nosetests.xml $CIRCLE_TEST_REPORTS/nosetests\n - bash <(curl -s https://codecov.io/bash)\n" 51 | }, 52 | "messages" : [ ], 53 | "is_first_green_build" : false, 54 | "job_name" : null, 55 | "start_time" : "2016-07-11T13:03:38.059Z", 56 | "canceler" : null, 57 | "all_commit_details" : [ { 58 | "committer_date" : "2016-07-11T09:03:37-04:00", 59 | "body" : "", 60 | "branch" : "unit-tests", 61 | "author_date" : "2016-07-11T09:03:37-04:00", 62 | "committer_email" : "noreply@github.com", 63 | "commit" : "115d8e15d481871aaf8e4631692fe5291755bccb", 64 | "committer_login" : "web-flow", 65 | "committer_name" : "GitHub", 66 | "subject" : "corrected name of builds with project", 67 | "commit_url" : "https://github.com/therealbarack/circlecli/commit/115d8e15d481871aaf8e4631692fe5291755bccb", 68 | "author_login" : "therealbarack", 69 | "author_name" : "Barack Obama", 70 | "author_email" : "therealbarack@users.noreply.github.com" 71 | } ], 72 | "outcome" : "failed", 73 | "vcs_url" : "https://github.com/therealbarack/circlecli", 74 | "author_name" : "Barack Obama", 75 | "node" : null, 76 | "queued_at" : "2016-07-11T13:03:37.716Z", 77 | "canceled" : false, 78 | "author_email" : "therealbarack@users.noreply.github.com" 79 | }, 80 | { 81 | "username": "therealjoebiden", 82 | "compare": null, 83 | "vcs_url": "https://github.com/therealjoebiden/homeland_security", 84 | "ssh_disabled": false, 85 | "oss": true, 86 | "build_url": "https://circleci.com/gh/therealjoebiden/homeland_security/1", 87 | "committer_date": "2016-07-10T14:36:42Z", 88 | "author_name": "Joe Biden", 89 | "build_num": 1, 90 | "canceled": false, 91 | "reponame": "homeland_security", 92 | "retry_of": null, 93 | "vcs_revision": "57b1f5afd9dfc7e8bb9cf9e68890097958d5d793", 94 | "vcs_tag": null, 95 | "previous": null, 96 | "build_time_millis": 45753, 97 | "committer_email": "noreply@github.com", 98 | "infrastructure_fail": false, 99 | "author_email": "eatatjoes@whitehouse.gov", 100 | "circle_yml": { 101 | "string": "general:\n artifacts:\n - cover\n\ndependencies:\n override:\n - pip install -r requirements_dev.txt\n\ntest:\n override:\n - nosetests --cover-branches --with-coverage --cover-erase --cover-package=circlecli --cover-html --with-xunit\n post:\n - mkdir $CIRCLE_TEST_REPORTS/nosetests && cp nosetests.xml $CIRCLE_TEST_REPORTS/nosetests\n" 102 | }, 103 | "no_dependency_cache": false, 104 | "is_first_green_build": true, 105 | "all_commit_details_truncated": false, 106 | "failed": null, 107 | "previous_successful_build": null, 108 | "stop_time": "2016-07-10T14:37:53.275Z", 109 | "branch": "master", 110 | "build_parameters": null, 111 | "queued_at": "2016-07-10T14:37:07.486Z", 112 | "has_artifacts": true, 113 | "body": "Circleci testing", 114 | "canceler": null, 115 | "lifecycle": "finished", 116 | "start_time": "2016-07-10T14:37:07.522Z", 117 | "timedout": false, 118 | "subject": "forgot to remove the patriot act", 119 | "ssh_users": [], 120 | "usage_queued_at": "2016-07-10T14:37:26.736Z", 121 | "user": { 122 | "is_user": false, 123 | "login": "none" 124 | }, 125 | "dont_build": null, 126 | "why": "first-build", 127 | "vcs_type": "github", 128 | "retries": null, 129 | "node": null, 130 | "outcome": "success", 131 | "fail_reason": null, 132 | "all_commit_details": [ 133 | { 134 | "body": "Circleci testing", 135 | "committer_email": "noreply@github.com", 136 | "commit_url": "https://github.com/therealjoebiden/homeland_security/commit/57b1f5afd9dfc7e8bb9cf9e68890097958d5d793", 137 | "author_email": "eatatjoes@whitehouse.gov", 138 | "author_login": "therealjoebiden", 139 | "committer_date": "2016-07-10T14:36:42Z", 140 | "author_name": "Joe Biden", 141 | "committer_login": "web-flow", 142 | "commit": "57b1f5afd9dfc7e8bb9cf9e68890097958d5d793", 143 | "author_date": "2016-07-10T14:36:42Z", 144 | "committer_name": "GitHub", 145 | "subject": "forgot to remove the patriot act" 146 | } 147 | ], 148 | "messages": [], 149 | "job_name": null, 150 | "status": "success", 151 | "parallel": 1, 152 | "author_date": "2016-07-10T14:36:42Z", 153 | "committer_name": "GitHub" 154 | }] 155 | -------------------------------------------------------------------------------- /mocks/get/myownhost.com/me: -------------------------------------------------------------------------------- 1 | { 2 | "enrolled_betas" : [ ], 3 | "in_beta_program" : true, 4 | "selected_email" : "potus@whitehouse.gov", 5 | "avatar_url" : "https://avatars.githubusercontent.com/u/8675309?v=3", 6 | "trial_end" : "2015-10-31T07:05:29.209Z", 7 | "admin" : null, 8 | "basic_email_prefs" : "smart", 9 | "sign_in_count" : 44, 10 | "github_oauth_scopes" : [ "user:email", "repo" ], 11 | "analytics_id" : "4ed02b0a-af20-f612-8b03-444fc2f1691a", 12 | "name" : "Barack Obama", 13 | "gravatar_id" : null, 14 | "days_left_in_trial" : -253, 15 | "parallelism" : 1, 16 | "bitbucket_authorized" : true, 17 | "github_id" : 8675309, 18 | "bitbucket" : { 19 | "id" : "{1d42cca1-58c1-49f6-9b5b-be3f358ba40e}", 20 | "login" : "therealbarack" 21 | }, 22 | "dev_admin" : false, 23 | "all_emails" : [ "potus@whitehouse.gov", "b-man@whalumni.gov" ], 24 | "created_at" : "2010-01-20T017:10:00.103Z", 25 | "plan" : null, 26 | "heroku_api_key" : null, 27 | "projects" : { 28 | "https://github.com/therealbarack/circlecli" : { 29 | "on_dashboard" : true, 30 | "emails" : "default" 31 | }, 32 | "https://github.com/therealbarack/unstuff" : { 33 | "on_dashboard" : true, 34 | "emails" : "default" 35 | }, 36 | "https://github.com/therealbarack/kidshomework" : { 37 | "on_dashboard" : true, 38 | "emails" : "default" 39 | } 40 | }, 41 | "login" : "therealbarack", 42 | "organization_prefs" : { }, 43 | "containers" : 1, 44 | "pusher_id" : "559adb9619ed1f53e3e5d4bd81a158f35f21d0dc" 45 | } 46 | -------------------------------------------------------------------------------- /mocks/get/status.circleci.com: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | CircleCI Status 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 84 | 289 | 290 | 291 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 316 |
317 |
318 |
319 |
320 |
321 | 333 |
334 | 335 | 336 | CircleCI 337 | 338 | 339 |
340 | 341 | 342 | 1060 |
1061 |
1062 |
1063 |
1064 | 1065 |
1066 |
1067 | 1068 | All Systems Operational 1069 | 1070 | 1071 | 1072 |
1073 |
1074 |
1075 |
1076 |
1077 | 1078 | CircleCI 1079 | 1080 | 1081 | Operational 1082 | 1083 | 1084 | 1085 |
1086 |
1087 |
1088 |
1089 | 1090 | GitHub 1091 | 1092 | 1093 | Operational 1094 | 1095 | 1096 | 1097 |
1098 |
1099 |
1100 |
1101 | 1102 | Heroku 1103 | 1104 | 1105 | Operational 1106 | 1107 | 1108 | 1109 |
1110 |
1111 |
1112 |
1113 | 1114 | AWS 1115 | 1116 | ? 1117 | 1118 | 1119 | 1120 | Operational 1121 | 1122 | 1123 | 1124 |
1125 |
1126 |
1127 |
1128 | 1129 | Pusher Pusher REST API 1130 | 1131 | 1132 | Operational 1133 | 1134 | 1135 | 1136 |
1137 |
1138 |
1139 |
1140 | 1141 | Pusher WebSocket client API 1142 | 1143 | 1144 | Operational 1145 | 1146 | 1147 | 1148 |
1149 |
1150 |
1151 |
1152 | 1153 | OS X Builds 1154 | 1155 | 1156 | Operational 1157 | 1158 | 1159 | 1160 |
1161 |
1162 |
1163 |
1164 | 1165 | Test Result Processing 1166 | 1167 | ? 1168 | 1169 | 1170 | 1171 | Operational 1172 | 1173 | 1174 | 1175 |
1176 |
1177 |
1178 |
1179 | 1180 | Ubuntu 14.04 Builds 1181 | 1182 | ? 1183 | 1184 | 1185 | 1186 | Operational 1187 | 1188 | 1189 | 1190 |
1191 |
1192 |
1193 |
1194 |
1195 | 1196 | 1197 | Operational 1198 |
1199 |
1200 | 1201 | 1202 | Degraded Performance 1203 |
1204 |
1205 | 1206 | 1207 | Partial Outage 1208 |
1209 |
1210 | 1211 | 1212 | Major Outage 1213 |
1214 |
1215 |
1216 |
1217 |
1218 | Past Incidents 1219 |
1220 |
1221 |
1222 | Jul 1223 | 1224 | 14 1225 | 1226 | , 1227 | 1228 | 2016 1229 | 1230 |
1231 |

1232 | No incidents reported today. 1233 |

1234 |
1235 |
1236 |
1237 | Jul 1238 | 1239 | 13 1240 | 1241 | , 1242 | 1243 | 2016 1244 | 1245 |
1246 |

1247 | No incidents reported. 1248 |

1249 |
1250 |
1251 |
1252 | Jul 1253 | 1254 | 12 1255 | 1256 | , 1257 | 1258 | 2016 1259 | 1260 |
1261 |
1262 | 1267 |
1268 |
1269 | 1270 | Resolved 1271 | 1272 | - 1273 | 1274 | 1275 | GitHub events have been flowing and their status page shows them as Operational. We will continue to monitor things closely and please do reach out to our Support team if you have any questions. 1276 |
1277 | 1278 | Jul 12, 10:36 PDT 1279 | 1280 |
1281 |
1282 |
1283 | 1284 | Update 1285 | 1286 | - 1287 | 1288 | 1289 | We are continuing to closely monitor GitHub's status and recovery, and we are scaling to meet the expected influx of builds. Another update in 20(ish) minutes. 1290 |
1291 | 1292 | Jul 12, 10:04 PDT 1293 | 1294 |
1295 |
1296 |
1297 | 1298 | Monitoring 1299 | 1300 | - 1301 | 1302 | 1303 | GitHub is actively working to restore service and while you may be able to log in we are still not receiving build notifications. Will update in 20 minutes or when status changes. 1304 |
1305 | 1306 | Jul 12, 09:43 PDT 1307 | 1308 |
1309 |
1310 |
1311 | 1312 | Identified 1313 | 1314 | - 1315 | 1316 | 1317 | GitHub is currently showing an outage which impacts our ability to handle logins and receive build events, we are monitoring their status closely. 1318 |
1319 | 1320 | Jul 12, 09:25 PDT 1321 | 1322 |
1323 |
1324 |
1325 |
1326 |
1327 |
1328 |
1329 | Jul 1330 | 1331 | 11 1332 | 1333 | , 1334 | 1335 | 2016 1336 | 1337 |
1338 |

1339 | No incidents reported. 1340 |

1341 |
1342 |
1343 |
1344 | Jul 1345 | 1346 | 10 1347 | 1348 | , 1349 | 1350 | 2016 1351 | 1352 |
1353 |

1354 | No incidents reported. 1355 |

1356 |
1357 |
1358 |
1359 | Jul 1360 | 1361 | 9 1362 | 1363 | , 1364 | 1365 | 2016 1366 | 1367 |
1368 |

1369 | No incidents reported. 1370 |

1371 |
1372 |
1373 |
1374 | Jul 1375 | 1376 | 8 1377 | 1378 | , 1379 | 1380 | 2016 1381 | 1382 |
1383 |

1384 | No incidents reported. 1385 |

1386 |
1387 |
1388 |
1389 | Jul 1390 | 1391 | 7 1392 | 1393 | , 1394 | 1395 | 2016 1396 | 1397 |
1398 |

1399 | No incidents reported. 1400 |

1401 |
1402 |
1403 |
1404 | Jul 1405 | 1406 | 6 1407 | 1408 | , 1409 | 1410 | 2016 1411 | 1412 |
1413 |

1414 | No incidents reported. 1415 |

1416 |
1417 |
1418 |
1419 | Jul 1420 | 1421 | 5 1422 | 1423 | , 1424 | 1425 | 2016 1426 | 1427 |
1428 |

1429 | No incidents reported. 1430 |

1431 |
1432 |
1433 |
1434 | Jul 1435 | 1436 | 4 1437 | 1438 | , 1439 | 1440 | 2016 1441 | 1442 |
1443 |

1444 | No incidents reported. 1445 |

1446 |
1447 |
1448 |
1449 | Jul 1450 | 1451 | 3 1452 | 1453 | , 1454 | 1455 | 2016 1456 | 1457 |
1458 |

1459 | No incidents reported. 1460 |

1461 |
1462 |
1463 |
1464 | Jul 1465 | 1466 | 2 1467 | 1468 | , 1469 | 1470 | 2016 1471 | 1472 |
1473 |

1474 | No incidents reported. 1475 |

1476 |
1477 |
1478 |
1479 | Jul 1480 | 1481 | 1 1482 | 1483 | , 1484 | 1485 | 2016 1486 | 1487 |
1488 |
1489 | 1494 |
1495 |
1496 | 1497 | Resolved 1498 | 1499 | - 1500 | 1501 | 1502 | We've fully recovered. 1503 |
1504 | 1505 | Jul 1, 00:09 PDT 1506 | 1507 |
1508 |
1509 |
1510 | 1511 | Monitoring 1512 | 1513 | - 1514 | 1515 | 1516 | We've recovered and will continue to monitor. Please let us know if you see unexpectedly high queue times. 1517 |
1518 | 1519 | Jun 30, 23:18 PDT 1520 | 1521 |
1522 |
1523 |
1524 | 1525 | Investigating 1526 | 1527 | - 1528 | 1529 | 1530 | We lost several of our OS X builder instances and so we're running at reduced capacity. We anticipate increased queue times until we recover. 1531 |
1532 | 1533 | Jun 30, 22:54 PDT 1534 | 1535 |
1536 |
1537 |
1538 |
1539 |
1540 |
1541 | 1555 |
1556 |
1557 | 1558 | 1559 | 1560 | 1562 | 1563 | 1575 | 1579 | 1581 | 1583 | 1585 | 1604 | 1609 | 1611 | 1613 | 1615 | 1616 | 1617 | 1622 | 1623 | 1624 | -------------------------------------------------------------------------------- /mocks/post/circleci.com/api/v1/project/therealbarack/circlecli/envvar: -------------------------------------------------------------------------------- 1 | {"name":"foo","value":"xxxx"} 2 | -------------------------------------------------------------------------------- /pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # A hook script to ensure that circle.yml files are valid. 4 | # 5 | # To enable this hook, move to .git/hooks/pre-commit 6 | PROJECT_ROOT=$(git rev-parse --show-toplevel) 7 | CIRCLECLI_BIN=$(which circlecli) 8 | 9 | # verify that circlecli is installed and available 10 | if [ -z "$CIRCLECLI_BIN" ]; then 11 | echo "CircleCLI not installed. Install with \`pip install circlecli\`." 12 | exit 1 13 | fi 14 | 15 | # for validating a single file 16 | function validate_config { 17 | filepath=$1 18 | if [ ! -f "$filepath" ]; then 19 | return 1 20 | fi 21 | 22 | output=$( $CIRCLECLI_BIN check $filepath ) 23 | [ ! -z "$output" ] && printf "${filepath}:\n ${output}\n" && return 2 24 | 25 | return 0 26 | } 27 | 28 | # uncomment the following line and comment the next one to verify ALL 29 | # circle.yml files within the project 30 | # files_to_check=($(find $PROJECT_ROOT -name 'circle.yml')) 31 | files_to_check=("${PROJECT_ROOT}/circle.yml") 32 | 33 | all_valid=1 34 | for f in ${files_to_check[*]}; do 35 | validate_config $f 36 | exit_status=$? 37 | if [ $exit_status -eq 1 ]; then 38 | printf "${f} does not exist." # this should never happen 39 | exit 1 40 | fi 41 | if [ $exit_status -ne 0 ]; then 42 | all_valid=0 43 | fi 44 | done 45 | 46 | # a circle.yml file was invalid 47 | [ $all_valid -eq 0 ] && exit 1 48 | # all circle.yml files are valid 49 | exit 0 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-dateutil==2.5.3 2 | PyYAML==3.11 3 | requests==2.10.0 4 | beautifulsoup4==4.4.1 5 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | coverage==4.1 3 | httmock==1.2.2 4 | nose2==0.6.5 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup 3 | from os import path as op 4 | 5 | 6 | def _read(filename): 7 | """Read a file and return its contents.""" 8 | try: 9 | return open(op.join(op.abspath(op.dirname(__file__)), filename)).read() 10 | except IOError: 11 | return '' 12 | 13 | 14 | req_path = 'requirements.txt' 15 | install_requires = [ln for ln in _read(req_path).split('\n') if ln and not ln.startswith('#')] 16 | print repr(install_requires) 17 | 18 | 19 | setup( 20 | name='circlecli', 21 | version='1.3.0', 22 | url='https://github.com/TheRealJoeLinux/circlecli', 23 | description=_read('DESCRIPTION'), 24 | long_description=_read('README.md'), 25 | author='Joey Espinosa', 26 | author_email='jlouis.espinosa@gmail.com', 27 | license='MIT License', 28 | keywords=['circleci', 'cicd', 'rest', 'api', 'cli', 'command', 'command-line'], 29 | install_requires=install_requires, 30 | packages=['circlecli'], 31 | py_modules=['circlecli'], 32 | classfiers=[ 33 | 'Development Status :: 2 - Pre-Alpha', 34 | 'Environment :: Console', 35 | 'Intended Audience :: System Administrators', 36 | 'License :: OSI Approved :: MIT License', 37 | 'Operating System :: MacOS :: MacOS X', 38 | 'Operating System :: Microsoft :: Windows', 39 | 'Operating System :: OS Independent', 40 | 'Operating System :: POSIX', 41 | 'Operating System :: Unix', 42 | 'Topic :: Utilities', 43 | ], 44 | scripts=['bin/circlecli'], 45 | use_2to3=True, 46 | ) 47 | -------------------------------------------------------------------------------- /tests/test_circlecli.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Tests for the CircleCLI API library. 5 | 6 | """ 7 | 8 | import binascii 9 | import json 10 | import os 11 | import unittest 12 | 13 | from httmock import with_httmock 14 | 15 | from circlecli import CircleAPI 16 | import mocks.circlecli 17 | 18 | 19 | # 40-character hexadecimal string 20 | FAKE_TOKEN = binascii.b2a_hex(os.urandom(20)) 21 | 22 | 23 | class TestCircleCLISetup(unittest.TestCase): 24 | 25 | """ CircleAPI.init() should check the token is a 40-digit hex string 26 | and return an error if it is not 27 | """ 28 | def test_valid_token(self): 29 | circlecli = CircleAPI(FAKE_TOKEN) 30 | 31 | def test_invalid_token(self): 32 | # not long enough 33 | self.assertRaises(ValueError, CircleAPI, 'fff') 34 | 35 | # not hex 36 | self.assertRaises(ValueError, CircleAPI, 'ququququququququququququququququququququ') 37 | 38 | 39 | class TestCircleCLI(unittest.TestCase): 40 | 41 | def setUp(self): 42 | self.circlecli = CircleAPI(FAKE_TOKEN) 43 | 44 | """ CircleAPI.me() 45 | test results as a dict 46 | """ 47 | @with_httmock(mocks.circlecli.resource_get) 48 | def test_me_as_dict(self): 49 | results = self.circlecli.me(verbose=False) 50 | 51 | self.assertEqual(results['Username'], 'therealbarack') 52 | 53 | """ CircleAPI.me() 54 | test results as json 55 | """ 56 | @with_httmock(mocks.circlecli.resource_get) 57 | def test_me_as_json(self): 58 | 59 | results = self.circlecli.me(verbose=True) 60 | 61 | data = json.loads(results) 62 | 63 | self.assertEqual(data['login'], 'therealbarack') 64 | 65 | """ CircleAPI.projects() 66 | test results as a dict 67 | """ 68 | @with_httmock(mocks.circlecli.resource_get) 69 | def test_projects_as_dict(self): 70 | results = self.circlecli.projects(verbose=False) 71 | 72 | self.assertEqual(results, ['therealbarack/circlecli']) 73 | 74 | """ CircleAPI.projects() 75 | test results as json 76 | """ 77 | @with_httmock(mocks.circlecli.resource_get) 78 | def test_projects_as_json(self): 79 | 80 | results = self.circlecli.projects(verbose=True) 81 | 82 | data = json.loads(results) 83 | 84 | self.assertEqual(data[0]['branches']['master']['last_success']['status'], 'fixed') 85 | 86 | """ CircleAPI.clear_cache() 87 | 88 | """ 89 | @with_httmock(mocks.circlecli.resource_delete) 90 | def test_clear_cache_as_dict(self): 91 | results = self.circlecli.clear_cache('therealbarack', 'circlecli', verbose=False) 92 | print results 93 | 94 | self.assertEqual(results['status'], 'build caches deleted') 95 | 96 | """ CircleAPI.clear_cache() 97 | 98 | """ 99 | @with_httmock(mocks.circlecli.resource_delete) 100 | def test_clear_cache_as_json(self): 101 | 102 | results = self.circlecli.clear_cache('therealbarack', 'circlecli', verbose=True) 103 | print results 104 | 105 | data = json.loads(results) 106 | 107 | self.assertEqual(data['status'], 'build caches deleted') 108 | 109 | """ CircleAPI.envar() 110 | 111 | """ 112 | @with_httmock(mocks.circlecli.resource_get) 113 | @unittest.skip("test not written yet") 114 | def test_envar_as_dict(self): 115 | results = self.circlecli.envvar(verbose=False) 116 | print results 117 | 118 | self.assertEqual(results['Username'], 'therealbarack') 119 | 120 | """ CircleAPI.envar() 121 | 122 | """ 123 | @with_httmock(mocks.circlecli.resource_get) 124 | @unittest.skip("test not written yet") 125 | def test_envar_as_json(self): 126 | 127 | results = self.circlecli.envvar(verbose=True) 128 | print results 129 | 130 | data = json.loads(results) 131 | 132 | self.assertEqual(data['login'], 'therealbarack') 133 | 134 | 135 | class TestHostedCircleCLI(unittest.TestCase): 136 | """A self-hosted CircleCI installation should work.""" 137 | 138 | def setUp(self): 139 | self.circlecli = CircleAPI(token=FAKE_TOKEN, baseurl="https://myownhost.com") 140 | 141 | """ CircleAPI.me() 142 | test results as a dict 143 | """ 144 | @with_httmock(mocks.circlecli.resource_get) 145 | def test_me_as_dict(self): 146 | results = self.circlecli.me(verbose=False) 147 | 148 | self.assertEqual(results['Username'], 'therealbarack') 149 | 150 | """ CircleAPI.me() 151 | test results as json 152 | """ 153 | @with_httmock(mocks.circlecli.resource_get) 154 | def test_me_as_json(self): 155 | 156 | results = self.circlecli.me(verbose=True) 157 | 158 | data = json.loads(results) 159 | 160 | self.assertEqual(data['login'], 'therealbarack') 161 | 162 | 163 | if __name__ == '__main__': 164 | unittest.main() 165 | -------------------------------------------------------------------------------- /tests/test_circlecli_builds.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Tests for the CircleCLI API library. 5 | 6 | """ 7 | 8 | import binascii 9 | import json 10 | import os 11 | import unittest 12 | 13 | from httmock import with_httmock 14 | 15 | from circlecli import CircleAPI 16 | import mocks.circlecli 17 | 18 | 19 | # 40-character hexadecimal string 20 | FAKE_TOKEN = binascii.b2a_hex(os.urandom(20)) 21 | 22 | 23 | class TestCircleCLIBuilds(unittest.TestCase): 24 | 25 | def setUp(self): 26 | self.circlecli = CircleAPI(FAKE_TOKEN) 27 | 28 | """ CircleAPI.builds() 29 | test results as dict 30 | """ 31 | @with_httmock(mocks.circlecli.resource_get) 32 | def test_builds_as_dict(self): 33 | results = self.circlecli.builds(verbose=False) 34 | print results 35 | 36 | self.assertEqual(len(results), 2) 37 | contributors = [r['Author '] for r in results] 38 | self.assertTrue('Barack Obama ' in contributors) 39 | self.assertTrue('Joe Biden ' in contributors) 40 | 41 | """ CircleAPI.builds() 42 | test results as json 43 | """ 44 | @with_httmock(mocks.circlecli.resource_get) 45 | def test_builds_as_json(self): 46 | 47 | results = self.circlecli.builds(verbose=True) 48 | print results 49 | 50 | data = json.loads(results) 51 | 52 | self.assertEqual(len(data), 2) 53 | subjects = [d['subject'] for d in data] 54 | self.assertTrue('corrected name of builds with project' in subjects) 55 | 56 | """ CircleAPI.builds() 57 | test results as dict with project 58 | """ 59 | @with_httmock(mocks.circlecli.resource_get) 60 | def test_builds_with_project_as_dict(self): 61 | results = self.circlecli.builds('therealbarack', 'circlecli', 1, verbose=False) 62 | print results 63 | 64 | self.assertEqual(results[0]['Result '], 'no_tests') 65 | 66 | """ CircleAPI.builds() 67 | test results as json with project 68 | """ 69 | @with_httmock(mocks.circlecli.resource_get) 70 | def test_builds_with_project_as_json(self): 71 | 72 | results = self.circlecli.builds('therealbarack', 'circlecli', 1, verbose=True) 73 | print results 74 | 75 | data = json.loads(results) 76 | 77 | self.assertEqual(data['outcome'], 'no_tests') 78 | 79 | """ CircleAPI.artifacts() 80 | 81 | """ 82 | @with_httmock(mocks.circlecli.resource_get) 83 | @unittest.skip("test not written yet") 84 | def test_artifacts_as_dict(self): 85 | results = self.circlecli.artifacts(verbose=False) 86 | print results 87 | 88 | self.assertEqual(results['Username'], 'therealbarack') 89 | 90 | """ CircleAPI.artifacts() 91 | 92 | """ 93 | @with_httmock(mocks.circlecli.resource_get) 94 | @unittest.skip("test not written yet") 95 | def test_artifacts_as_json(self): 96 | 97 | results = self.circlecli.artifacts(verbose=True) 98 | print results 99 | 100 | data = json.loads(results) 101 | 102 | self.assertEqual(data['login'], 'therealbarack') 103 | 104 | """ CircleAPI.retry_build() 105 | 106 | """ 107 | @with_httmock(mocks.circlecli.resource_get) 108 | @unittest.skip("test not written yet") 109 | def test_retry_build_as_dict(self): 110 | results = self.circlecli.retry_build(verbose=False) 111 | print results 112 | 113 | self.assertEqual(results['Username'], 'therealbarack') 114 | 115 | """ CircleAPI.retry_build() 116 | 117 | """ 118 | @with_httmock(mocks.circlecli.resource_get) 119 | @unittest.skip("test not written yet") 120 | def test_retry_build_as_json(self): 121 | 122 | results = self.circlecli.retry_build(verbose=True) 123 | print results 124 | 125 | data = json.loads(results) 126 | 127 | self.assertEqual(data['login'], 'therealbarack') 128 | 129 | """ CircleAPI.cancel_build() 130 | 131 | """ 132 | @with_httmock(mocks.circlecli.resource_get) 133 | @unittest.skip("test not written yet") 134 | def test_cancel_build_as_dict(self): 135 | results = self.circlecli.cancel_build(verbose=False) 136 | print results 137 | 138 | self.assertEqual(results['Username'], 'therealbarack') 139 | 140 | """ CircleAPI.cancel_build() 141 | 142 | """ 143 | @with_httmock(mocks.circlecli.resource_get) 144 | @unittest.skip("test not written yet") 145 | def test_cancel_build_as_json(self): 146 | 147 | results = self.circlecli.cancel_build(verbose=True) 148 | print results 149 | 150 | data = json.loads(results) 151 | 152 | self.assertEqual(data['login'], 'therealbarack') 153 | 154 | """ CircleAPI.new_build() 155 | 156 | """ 157 | @with_httmock(mocks.circlecli.resource_get) 158 | @unittest.skip("test not written yet") 159 | def test_new_build_as_dict(self): 160 | results = self.circlecli.new_build(verbose=False) 161 | print results 162 | 163 | self.assertEqual(results['Username'], 'therealbarack') 164 | 165 | """ CircleAPI.new_build() 166 | 167 | """ 168 | @with_httmock(mocks.circlecli.resource_get) 169 | @unittest.skip("test not written yet") 170 | def test_new_build_as_json(self): 171 | 172 | results = self.circlecli.new_build(verbose=True) 173 | print results 174 | 175 | data = json.loads(results) 176 | 177 | self.assertEqual(data['login'], 'therealbarack') 178 | 179 | @with_httmock(mocks.circlecli.resource_get) 180 | def test_filter_with_match(self): 181 | """Should match one of two values in mock data.""" 182 | results = self.circlecli.builds('therealbarack', verbose=False, 183 | filters={'user.login': 'therealbarack'}) 184 | 185 | self.assertEqual(len(results), 1) # should only be one of two 186 | self.assertEqual(results[0]['Author '], 'Barack Obama ') 187 | 188 | @with_httmock(mocks.circlecli.resource_get) 189 | def test_filter_without_match(self): 190 | """Should return no results (no match).""" 191 | results = self.circlecli.builds('therealbarack', verbose=False, 192 | filters={'user.login': 'therealjoebiden'}) 193 | 194 | self.assertEqual(len(results), 0) 195 | 196 | if __name__ == '__main__': 197 | unittest.main() 198 | -------------------------------------------------------------------------------- /tests/test_circlecli_keys.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Tests for the CircleCLI API library. 5 | 6 | """ 7 | 8 | import binascii 9 | import json 10 | import os 11 | import unittest 12 | 13 | from httmock import with_httmock 14 | 15 | from circlecli import CircleAPI 16 | import mocks.circlecli 17 | 18 | 19 | # 40-character hexadecimal string 20 | FAKE_TOKEN = binascii.b2a_hex(os.urandom(20)) 21 | 22 | 23 | class TestCircleCLIKeys(unittest.TestCase): 24 | 25 | def setUp(self): 26 | self.circlecli = CircleAPI(FAKE_TOKEN) 27 | 28 | """ CircleAPI.ssh_users() 29 | 30 | """ 31 | @with_httmock(mocks.circlecli.resource_get) 32 | @unittest.skip("test not written yet") 33 | def test_ssh_users_as_dict(self): 34 | results = self.circlecli.ssh_users(verbose=False) 35 | print results 36 | 37 | self.assertEqual(results['Username'], 'therealbarack') 38 | 39 | """ CircleAPI.ssh_users() 40 | 41 | """ 42 | @with_httmock(mocks.circlecli.resource_get) 43 | @unittest.skip("test not written yet") 44 | def test_ssh_users_as_json(self): 45 | 46 | results = self.circlecli.ssh_users(verbose=True) 47 | print results 48 | 49 | data = json.loads(results) 50 | 51 | self.assertEqual(data['login'], 'therealbarack') 52 | 53 | """ CircleAPI.create_ssh() 54 | 55 | """ 56 | @with_httmock(mocks.circlecli.resource_get) 57 | @unittest.skip("test not written yet") 58 | def test_create_ssh_as_dict(self): 59 | results = self.circlecli.create_ssh(verbose=False) 60 | print results 61 | 62 | self.assertEqual(results['Username'], 'therealbarack') 63 | 64 | """ CircleAPI.create_ssh() 65 | 66 | """ 67 | @with_httmock(mocks.circlecli.resource_get) 68 | @unittest.skip("test not written yet") 69 | def test_create_ssh_as_json(self): 70 | 71 | results = self.circlecli.create_ssh(verbose=True) 72 | print results 73 | 74 | data = json.loads(results) 75 | 76 | self.assertEqual(data['login'], 'therealbarack') 77 | 78 | """ CircleAPI.list_checkout_keys() 79 | 80 | """ 81 | @with_httmock(mocks.circlecli.resource_get) 82 | @unittest.skip("test not written yet") 83 | def test_list_checkout_keys_as_dict(self): 84 | results = self.circlecli.list_checkout_keys(verbose=False) 85 | print results 86 | 87 | self.assertEqual(results['Username'], 'therealbarack') 88 | 89 | """ CircleAPI.list_checkout_keys() 90 | 91 | """ 92 | @with_httmock(mocks.circlecli.resource_get) 93 | @unittest.skip("test not written yet") 94 | def test_list_checkout_keys_as_json(self): 95 | 96 | results = self.circlecli.list_checkout_keys(verbose=True) 97 | print results 98 | 99 | data = json.loads(results) 100 | 101 | self.assertEqual(data['login'], 'therealbarack') 102 | 103 | """ CircleAPI.create_checkout_key() 104 | 105 | """ 106 | @with_httmock(mocks.circlecli.resource_get) 107 | @unittest.skip("test not written yet") 108 | def test_create_checkout_key_as_dict(self): 109 | results = self.circlecli.create_checkout_key(verbose=False) 110 | print results 111 | 112 | self.assertEqual(results['Username'], 'therealbarack') 113 | 114 | """ CircleAPI.create_checkout_key() 115 | 116 | """ 117 | @with_httmock(mocks.circlecli.resource_get) 118 | @unittest.skip("test not written yet") 119 | def test_create_checkout_key_as_json(self): 120 | 121 | results = self.circlecli.create_checkout_key(verbose=True) 122 | print results 123 | 124 | data = json.loads(results) 125 | 126 | self.assertEqual(data['login'], 'therealbarack') 127 | 128 | """ CircleAPI.checkout_key() 129 | 130 | """ 131 | @with_httmock(mocks.circlecli.resource_get) 132 | @unittest.skip("test not written yet") 133 | def test_checkout_key_as_dict(self): 134 | results = self.circlecli.checkout_key(verbose=False) 135 | print results 136 | 137 | self.assertEqual(results['Username'], 'therealbarack') 138 | 139 | """ CircleAPI.checkout_key() 140 | 141 | """ 142 | @with_httmock(mocks.circlecli.resource_get) 143 | @unittest.skip("test not written yet") 144 | def test_checkout_key_as_json(self): 145 | 146 | results = self.circlecli.checkout_key(verbose=True) 147 | print results 148 | 149 | data = json.loads(results) 150 | 151 | self.assertEqual(data['login'], 'therealbarack') 152 | 153 | """ CircleAPI.delete_checkout_key() 154 | 155 | """ 156 | @with_httmock(mocks.circlecli.resource_get) 157 | @unittest.skip("test not written yet") 158 | def test_delete_checkout_key_as_dict(self): 159 | results = self.circlecli.delete_checkout_key(verbose=False) 160 | print results 161 | 162 | self.assertEqual(results['Username'], 'therealbarack') 163 | 164 | """ CircleAPI.delete_checkout_key() 165 | 166 | """ 167 | @with_httmock(mocks.circlecli.resource_get) 168 | @unittest.skip("test not written yet") 169 | def test_delete_checkout_key_as_json(self): 170 | 171 | results = self.circlecli.delete_checkout_key(verbose=True) 172 | print results 173 | 174 | data = json.loads(results) 175 | 176 | self.assertEqual(data['login'], 'therealbarack') 177 | 178 | """ CircleAPI.add_circle_key() 179 | 180 | """ 181 | @with_httmock(mocks.circlecli.resource_get) 182 | @unittest.skip("test not written yet") 183 | def test_add_circle_key_as_dict(self): 184 | results = self.circlecli.add_circle_key(verbose=False) 185 | print results 186 | 187 | self.assertEqual(results['Username'], 'therealbarack') 188 | 189 | """ CircleAPI.add_circle_key() 190 | 191 | """ 192 | @with_httmock(mocks.circlecli.resource_get) 193 | @unittest.skip("test not written yet") 194 | def test_add_circle_key_as_json(self): 195 | 196 | results = self.circlecli.add_circle_key(verbose=True) 197 | print results 198 | 199 | data = json.loads(results) 200 | 201 | self.assertEqual(data['login'], 'therealbarack') 202 | 203 | """ CircleAPI.add_heroku_key() 204 | 205 | """ 206 | @with_httmock(mocks.circlecli.resource_get) 207 | @unittest.skip("test not written yet") 208 | def test_add_heroku_key_as_dict(self): 209 | results = self.circlecli.add_heroku_key(verbose=False) 210 | print results 211 | 212 | self.assertEqual(results['Username'], 'therealbarack') 213 | 214 | """ CircleAPI.add_heroku_key() 215 | 216 | """ 217 | @with_httmock(mocks.circlecli.resource_get) 218 | @unittest.skip("test not written yet") 219 | def test_add_heroku_key_as_json(self): 220 | 221 | results = self.circlecli.add_heroku_key(verbose=True) 222 | print results 223 | 224 | data = json.loads(results) 225 | 226 | self.assertEqual(data['login'], 'therealbarack') 227 | 228 | if __name__ == '__main__': 229 | unittest.main() 230 | -------------------------------------------------------------------------------- /tests/test_circleutils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | import mocks.circlecli 5 | from httmock import with_httmock 6 | 7 | from circlecli.circleutils import InvalidSectionError, UnrecognizedSectionError, \ 8 | circle_status, validate_circle_yml 9 | 10 | 11 | class TestValidConfigMachine(unittest.TestCase): 12 | """Test a valid config for the Machine section.""" 13 | 14 | def setUp(self): 15 | """Create a mock circle.yml file.""" 16 | self.circle = open('mocks/circle.yml', 'w') 17 | config = """ 18 | machine: 19 | pre: 20 | - curl -k -L -o phantomjs.tar.bz2 http://phantomjs.googlecode.com/files/phantomjs-1.8.2-linux-x86_64.tar.bz2 21 | - tar -jxf phantomjs.tar.bz2 22 | environment: 23 | foo: bar 24 | baz: 123 25 | hosts: 26 | dev.circleci.com: 127.0.0.1 27 | foobar: 1.2.3.4 28 | timezone: 29 | America/Los_Angeles 30 | java: 31 | version: openjdk7 32 | node: 33 | version: 0.11.8 34 | ruby: 35 | version: 1.9.3-p0-falcon 36 | php: 37 | version: 5.4.5 38 | python: 39 | version: 2.7.5 40 | ghc: 41 | version: 7.8.3 42 | services: 43 | - redis 44 | - elasticsearch 45 | - neo4j 46 | - cassandra 47 | - riak 48 | - docker 49 | - memcached 50 | - rabbitmq-server 51 | - beanstalkd 52 | - couchbase-server 53 | - sphinxsearch 54 | """ 55 | self.circle.write(config) 56 | self.circle.close() 57 | 58 | def tearDown(self): 59 | """Remove the mock circle.yml file.""" 60 | os.remove(self.circle.name) 61 | 62 | def test_valid_config(self): 63 | """Should detect a valid config.""" 64 | self.assertTrue(validate_circle_yml(self.circle.name)) 65 | 66 | 67 | class TestInvalidConfigMachine(unittest.TestCase): 68 | """Test an unaccepted format for the Machine section.""" 69 | 70 | def setUp(self): 71 | """Create a mock circle.yml file.""" 72 | self.circle = open('mocks/circle.yml', 'w') 73 | config = """ 74 | machine: 75 | timezone: 76 | America/Los_Angeles 77 | ruby: 78 | - rvm install 1.9.3 79 | """ 80 | self.circle.write(config) 81 | self.circle.close() 82 | 83 | def tearDown(self): 84 | """Remove the mock circle.yml file.""" 85 | os.remove(self.circle.name) 86 | 87 | def test_invalid_config(self): 88 | """Should detect an invalid config.""" 89 | self.assertRaises(InvalidSectionError, validate_circle_yml, self.circle.name) 90 | 91 | 92 | class TestUnrecognizedConfigMachine(unittest.TestCase): 93 | """Test an unrecognized subsection for the Machine section.""" 94 | 95 | def setUp(self): 96 | """Create a mock circle.yml file.""" 97 | self.circle = open('mocks/circle.yml', 'w') 98 | config = """ 99 | machine: 100 | timezone: 101 | America/Los_Angeles 102 | override: 103 | - rvm install 1.9.3 104 | """ 105 | self.circle.write(config) 106 | self.circle.close() 107 | 108 | def tearDown(self): 109 | """Remove the mock circle.yml file.""" 110 | os.remove(self.circle.name) 111 | 112 | def test_invalid_config(self): 113 | """Should detect an invalid config.""" 114 | self.assertRaises(UnrecognizedSectionError, validate_circle_yml, self.circle.name) 115 | 116 | 117 | class TestValidConfigCheckout(unittest.TestCase): 118 | """Test a valid config for the Checkout section.""" 119 | 120 | def setUp(self): 121 | """Create a mock circle.yml file.""" 122 | self.circle = open('mocks/circle.yml', 'w') 123 | config = """ 124 | checkout: 125 | post: 126 | - git submodule sync 127 | - git submodule update --init 128 | - mv config/.app.yml config/app.yml 129 | """ 130 | self.circle.write(config) 131 | self.circle.close() 132 | 133 | def tearDown(self): 134 | """Remove the mock circle.yml file.""" 135 | os.remove(self.circle.name) 136 | 137 | def test_valid_config(self): 138 | """Should detect a valid config.""" 139 | self.assertTrue(validate_circle_yml(self.circle.name)) 140 | 141 | 142 | class TestInvalidConfigCheckout(unittest.TestCase): 143 | """Test an unaccepted format for the Machine section.""" 144 | 145 | def setUp(self): 146 | """Create a mock circle.yml file.""" 147 | self.circle = open('mocks/circle.yml', 'w') 148 | config = """ 149 | checkout: 150 | post: 151 | foo: bar 152 | """ 153 | self.circle.write(config) 154 | self.circle.close() 155 | 156 | def tearDown(self): 157 | """Remove the mock circle.yml file.""" 158 | os.remove(self.circle.name) 159 | 160 | def test_invalid_config(self): 161 | """Should detect an invalid config.""" 162 | self.assertRaises(InvalidSectionError, validate_circle_yml, self.circle.name) 163 | 164 | 165 | class TestUnrecognizedConfigCheckout(unittest.TestCase): 166 | """Test an unrecognized subsection for the Machine section.""" 167 | 168 | def setUp(self): 169 | """Create a mock circle.yml file.""" 170 | self.circle = open('mocks/circle.yml', 'w') 171 | config = """ 172 | checkout: 173 | pre: 174 | - git submodule sync 175 | """ 176 | self.circle.write(config) 177 | self.circle.close() 178 | 179 | def tearDown(self): 180 | """Remove the mock circle.yml file.""" 181 | os.remove(self.circle.name) 182 | 183 | def test_invalid_config(self): 184 | """Should detect an invalid config.""" 185 | self.assertRaises(UnrecognizedSectionError, validate_circle_yml, self.circle.name) 186 | 187 | 188 | class TestValidConfigDependencies(unittest.TestCase): 189 | """Test a valid config for the Dependencies section.""" 190 | 191 | def setUp(self): 192 | """Create a mock circle.yml file.""" 193 | self.circle = open('mocks/circle.yml', 'w') 194 | config = """ 195 | dependencies: 196 | pre: 197 | - gem uninstall bundler 198 | - gem install bundler --pre 199 | override: 200 | - npm install 201 | post: 202 | - pip install -r requirements.txt 203 | bundler: 204 | without: [production, osx] 205 | cache_directories: 206 | - "assets/cache" 207 | - "~/assets/output" 208 | """ 209 | self.circle.write(config) 210 | self.circle.close() 211 | 212 | def tearDown(self): 213 | """Remove the mock circle.yml file.""" 214 | os.remove(self.circle.name) 215 | 216 | def test_valid_config(self): 217 | """Should detect a valid config.""" 218 | self.assertTrue(validate_circle_yml(self.circle.name)) 219 | 220 | 221 | class TestInvalidConfigDependencies(unittest.TestCase): 222 | """Test an unaccepted format for the Dependencies section.""" 223 | 224 | def setUp(self): 225 | """Create a mock circle.yml file.""" 226 | self.circle = open('mocks/circle.yml', 'w') 227 | config = """ 228 | dependencies: 229 | override: foo 230 | pre: 231 | bundler install: 232 | foo: bar 233 | """ 234 | self.circle.write(config) 235 | self.circle.close() 236 | 237 | def tearDown(self): 238 | """Remove the mock circle.yml file.""" 239 | os.remove(self.circle.name) 240 | 241 | def test_invalid_config(self): 242 | """Should detect an invalid config.""" 243 | self.assertRaises(InvalidSectionError, validate_circle_yml, self.circle.name) 244 | 245 | 246 | class TestUnrecognizedConfigDependencies(unittest.TestCase): 247 | """Test an unrecognized subsection for the Dependencies section.""" 248 | 249 | def setUp(self): 250 | """Create a mock circle.yml file.""" 251 | self.circle = open('mocks/circle.yml', 'w') 252 | config = """ 253 | dependencies: 254 | override: 255 | - npm install 1.4.0 256 | notify: foo 257 | """ 258 | self.circle.write(config) 259 | self.circle.close() 260 | 261 | def tearDown(self): 262 | """Remove the mock circle.yml file.""" 263 | os.remove(self.circle.name) 264 | 265 | def test_invalid_config(self): 266 | """Should detect an invalid config.""" 267 | self.assertRaises(UnrecognizedSectionError, validate_circle_yml, self.circle.name) 268 | 269 | 270 | class TestValidConfigDatabase(unittest.TestCase): 271 | """Test a valid config for the Database section.""" 272 | 273 | def setUp(self): 274 | """Create a mock circle.yml file.""" 275 | self.circle = open('mocks/circle.yml', 'w') 276 | config = """ 277 | database: 278 | pre: 279 | - wget -O dump.sql https://foo.us-east-a.s3.amazonaws.com/dump.sql 280 | override: 281 | - mysql -u ubuntu circle_test < dump.sql 282 | post: 283 | - bundle exec rake db:create db:schema:load --trace 284 | """ 285 | self.circle.write(config) 286 | self.circle.close() 287 | 288 | def tearDown(self): 289 | """Remove the mock circle.yml file.""" 290 | os.remove(self.circle.name) 291 | 292 | def test_valid_config(self): 293 | """Should detect a valid config.""" 294 | self.assertTrue(validate_circle_yml(self.circle.name)) 295 | 296 | 297 | class TestInvalidConfigDatabase(unittest.TestCase): 298 | """Test an unaccepted format for the Database section.""" 299 | 300 | def setUp(self): 301 | """Create a mock circle.yml file.""" 302 | self.circle = open('mocks/circle.yml', 'w') 303 | config = """ 304 | database: 305 | pre: 306 | foo: bar 307 | override: 308 | - mysql -u ubuntu circle_test < dump.sql 309 | post: 310 | - bundle exec rake db:create db:schema:load --trace 311 | """ 312 | self.circle.write(config) 313 | self.circle.close() 314 | 315 | def tearDown(self): 316 | """Remove the mock circle.yml file.""" 317 | os.remove(self.circle.name) 318 | 319 | def test_invalid_config(self): 320 | """Should detect an invalid config.""" 321 | self.assertRaises(InvalidSectionError, validate_circle_yml, self.circle.name) 322 | 323 | 324 | class TestUnrecognizedConfigDatabase(unittest.TestCase): 325 | """Test an unrecognized subsection for the Database section.""" 326 | 327 | def setUp(self): 328 | """Create a mock circle.yml file.""" 329 | self.circle = open('mocks/circle.yml', 'w') 330 | config = """ 331 | database: 332 | pre: 333 | foo: bar 334 | override: 335 | - mysql -u ubuntu circle_test < dump.sql 336 | without: 337 | - bundle exec rake db:create db:schema:load --trace 338 | """ 339 | self.circle.write(config) 340 | self.circle.close() 341 | 342 | def tearDown(self): 343 | """Remove the mock circle.yml file.""" 344 | os.remove(self.circle.name) 345 | 346 | def test_invalid_config(self): 347 | """Should detect an invalid config.""" 348 | self.assertRaises(UnrecognizedSectionError, validate_circle_yml, self.circle.name) 349 | 350 | 351 | class TestValidConfigTest(unittest.TestCase): 352 | """Test a valid config for the Test section.""" 353 | 354 | def setUp(self): 355 | """Create a mock circle.yml file.""" 356 | self.circle = open('mocks/circle.yml', 'w') 357 | config = """ 358 | test: 359 | pre: 360 | - pip install nose2==0.6.5 361 | override: 362 | - phpunit my/special/subdirectory/tests 363 | post: 364 | - bundle exec rake spinach: 365 | environment: 366 | RAILS_ENV: test 367 | minitest_globs: 368 | - test/integration/**/*.rb 369 | - test/extra-dir/**/*.rb 370 | """ 371 | self.circle.write(config) 372 | self.circle.close() 373 | 374 | def tearDown(self): 375 | """Remove the mock circle.yml file.""" 376 | os.remove(self.circle.name) 377 | 378 | def test_valid_config(self): 379 | """Should detect a valid config.""" 380 | self.assertTrue(validate_circle_yml(self.circle.name)) 381 | 382 | 383 | class TestInvalidConfigTest(unittest.TestCase): 384 | """Test an unaccepted format for the Test section.""" 385 | 386 | def setUp(self): 387 | """Create a mock circle.yml file.""" 388 | self.circle = open('mocks/circle.yml', 'w') 389 | config = """ 390 | test: 391 | pre: 392 | - pip install nose2==0.6.5 393 | override: 394 | - phpunit my/special/subdirectory/tests 395 | post: 396 | - bundle exec rake spinach: 397 | environment: 398 | RAILS_ENV: test 399 | minitest_globs: 400 | first_dir: test/integration/**/*.rb 401 | second_dir: test/extra-dir/**/*.rb 402 | """ 403 | self.circle.write(config) 404 | self.circle.close() 405 | 406 | def tearDown(self): 407 | """Remove the mock circle.yml file.""" 408 | os.remove(self.circle.name) 409 | 410 | def test_invalid_config(self): 411 | """Should detect an invalid config.""" 412 | self.assertRaises(InvalidSectionError, validate_circle_yml, self.circle.name) 413 | 414 | 415 | class TestUnrecognizedConfigTest(unittest.TestCase): 416 | """Test an unrecognized subsection for the Test section.""" 417 | 418 | def setUp(self): 419 | """Create a mock circle.yml file.""" 420 | self.circle = open('mocks/circle.yml', 'w') 421 | config = """ 422 | test: 423 | pre: 424 | - pip install nose2==0.6.5 425 | override: 426 | - phpunit my/special/subdirectory/tests 427 | post: 428 | - bundle exec rake spinach: 429 | environment: 430 | RAILS_ENV: test 431 | bundler: 432 | first_dir: test/integration/**/*.rb 433 | second_dir: test/extra-dir/**/*.rb 434 | """ 435 | self.circle.write(config) 436 | self.circle.close() 437 | 438 | def tearDown(self): 439 | """Remove the mock circle.yml file.""" 440 | os.remove(self.circle.name) 441 | 442 | def test_invalid_config(self): 443 | """Should detect an invalid config.""" 444 | self.assertRaises(UnrecognizedSectionError, validate_circle_yml, self.circle.name) 445 | 446 | 447 | class TestValidConfigDeployment(unittest.TestCase): 448 | """Test a valid config for the Deployment section.""" 449 | 450 | def setUp(self): 451 | """Create a mock circle.yml file.""" 452 | self.circle = open('mocks/circle.yml', 'w') 453 | config = """ 454 | deployment: 455 | production: 456 | branch: production 457 | commands: 458 | - ./deploy_prod.sh 459 | automerge: 460 | branch: [dev_alice, dev_bob, dev_carol] 461 | commands: 462 | - ./merge_to_master.sh 463 | feature: 464 | branch: /feature_.*/ 465 | commands: 466 | - ./deploy_feature.sh 467 | master: 468 | branch: master 469 | owner: circleci 470 | commands: 471 | - ./deploy_master.sh 472 | release: 473 | tag: /v[0-9]+(\.[0-9]+)*/ 474 | owner: circleci 475 | heroku: 476 | appname: h-circleci-prod 477 | """ 478 | self.circle.write(config) 479 | self.circle.close() 480 | 481 | def tearDown(self): 482 | """Remove the mock circle.yml file.""" 483 | os.remove(self.circle.name) 484 | 485 | def test_valid_config(self): 486 | """Should detect a valid config.""" 487 | self.assertTrue(validate_circle_yml(self.circle.name)) 488 | 489 | 490 | class TestInvalidConfigDeployment(unittest.TestCase): 491 | """Test an unaccepted format for the Deployment section.""" 492 | 493 | def setUp(self): 494 | """Create a mock circle.yml file.""" 495 | self.circle = open('mocks/circle.yml', 'w') 496 | config = """ 497 | deployment: 498 | production: 499 | branch: production 500 | commands: foo 501 | automerge: 502 | branch: [dev_alice, dev_bob, dev_carol] 503 | commands: 504 | - ./merge_to_master.sh 505 | feature: 506 | branch: /feature_.*/ 507 | commands: 508 | - ./deploy_feature.sh 509 | master: 510 | branch: master 511 | owner: circleci 512 | commands: 513 | - ./deploy_master.sh 514 | release: 515 | tag: /v[0-9]+(\.[0-9]+)*/ 516 | owner: circleci 517 | commands: 518 | - ./deploy_master.sh 519 | """ 520 | self.circle.write(config) 521 | self.circle.close() 522 | 523 | def tearDown(self): 524 | """Remove the mock circle.yml file.""" 525 | os.remove(self.circle.name) 526 | 527 | def test_invalid_config(self): 528 | """Should detect an invalid config.""" 529 | self.assertRaises(InvalidSectionError, validate_circle_yml, self.circle.name) 530 | 531 | 532 | class TestUnrecognizedConfigDeployment(unittest.TestCase): 533 | """Test an unrecognized subsection for the Deployment section.""" 534 | 535 | def setUp(self): 536 | """Create a mock circle.yml file.""" 537 | self.circle = open('mocks/circle.yml', 'w') 538 | config = """ 539 | deployment: 540 | pre: 541 | - ./gcloud-auth.sh 542 | production: 543 | branch: production 544 | commands: 545 | - ./deploy_prod.sh 546 | automerge: 547 | branch: [dev_alice, dev_bob, dev_carol] 548 | commands: 549 | - ./merge_to_master.sh 550 | feature: 551 | branch: /feature_.*/ 552 | commands: 553 | - ./deploy_feature.sh 554 | master: 555 | branch: master 556 | owner: circleci 557 | commands: 558 | - ./deploy_master.sh 559 | release: 560 | tag: /v[0-9]+(\.[0-9]+)*/ 561 | owner: circleci 562 | commands: 563 | - ./deploy_master.sh 564 | """ 565 | self.circle.write(config) 566 | self.circle.close() 567 | 568 | def tearDown(self): 569 | """Remove the mock circle.yml file.""" 570 | os.remove(self.circle.name) 571 | 572 | def test_invalid_config(self): 573 | """Should detect an invalid config.""" 574 | self.assertRaises(UnrecognizedSectionError, validate_circle_yml, self.circle.name) 575 | 576 | 577 | class TestMissingBranchConfigDeployment(unittest.TestCase): 578 | """Test a missing required subsection in Deployment section.""" 579 | 580 | def setUp(self): 581 | """Create a mock circle.yml file.""" 582 | self.circle = open('mocks/circle.yml', 'w') 583 | config = """ 584 | deployment: 585 | production: 586 | branch: production 587 | commands: 588 | - ./deploy_prod.sh 589 | automerge: 590 | branch: [dev_alice, dev_bob, dev_carol] 591 | commands: 592 | - ./merge_to_master.sh 593 | feature: 594 | branch: /feature_.*/ 595 | commands: 596 | - ./deploy_feature.sh 597 | master: 598 | owner: circleci 599 | commands: 600 | - ./deploy_master.sh 601 | release: 602 | tag: /v[0-9]+(\.[0-9]+)*/ 603 | owner: circleci 604 | commands: 605 | - ./deploy_master.sh 606 | """ 607 | self.circle.write(config) 608 | self.circle.close() 609 | 610 | def tearDown(self): 611 | """Remove the mock circle.yml file.""" 612 | os.remove(self.circle.name) 613 | 614 | def test_invalid_config(self): 615 | """Should detect an invalid config.""" 616 | self.assertRaises(InvalidSectionError, validate_circle_yml, self.circle.name) 617 | 618 | 619 | class TestValidConfigNotify(unittest.TestCase): 620 | """Test a valid config for the Notify section.""" 621 | 622 | def setUp(self): 623 | """Create a mock circle.yml file.""" 624 | self.circle = open('mocks/circle.yml', 'w') 625 | config = """ 626 | notify: 627 | webhooks: 628 | - url: https://example.com/hooks/circle 629 | """ 630 | self.circle.write(config) 631 | self.circle.close() 632 | 633 | def tearDown(self): 634 | """Remove the mock circle.yml file.""" 635 | os.remove(self.circle.name) 636 | 637 | def test_valid_config(self): 638 | """Should detect a valid config.""" 639 | self.assertTrue(validate_circle_yml(self.circle.name)) 640 | 641 | 642 | class TestInvalidConfigNotify(unittest.TestCase): 643 | """Test an unaccepted format for the Notify section.""" 644 | 645 | def setUp(self): 646 | """Create a mock circle.yml file.""" 647 | self.circle = open('mocks/circle.yml', 'w') 648 | config = """ 649 | notify: 650 | webhooks: 651 | url: https://example.com/hooks/circle 652 | """ 653 | self.circle.write(config) 654 | self.circle.close() 655 | 656 | def tearDown(self): 657 | """Remove the mock circle.yml file.""" 658 | os.remove(self.circle.name) 659 | 660 | def test_invalid_config(self): 661 | """Should detect an invalid config.""" 662 | self.assertRaises(InvalidSectionError, validate_circle_yml, self.circle.name) 663 | 664 | 665 | class TestUnrecognizedConfigNotify(unittest.TestCase): 666 | """Test an unrecognized subsection for the Notify section.""" 667 | 668 | def setUp(self): 669 | """Create a mock circle.yml file.""" 670 | self.circle = open('mocks/circle.yml', 'w') 671 | config = """ 672 | notify: 673 | pre: 674 | - sudo apt-get install growl 675 | webhooks: 676 | - url: https://example.com/hooks/circle 677 | """ 678 | self.circle.write(config) 679 | self.circle.close() 680 | 681 | def tearDown(self): 682 | """Remove the mock circle.yml file.""" 683 | os.remove(self.circle.name) 684 | 685 | def test_invalid_config(self): 686 | """Should detect an invalid config.""" 687 | self.assertRaises(UnrecognizedSectionError, validate_circle_yml, self.circle.name) 688 | 689 | 690 | class TestValidConfigGeneral(unittest.TestCase): 691 | """Test a valid config for the General section.""" 692 | 693 | def setUp(self): 694 | """Create a mock circle.yml file.""" 695 | self.circle = open('mocks/circle.yml', 'w') 696 | config = """ 697 | general: 698 | branches: 699 | ignore: 700 | - gh-pages 701 | - /release\/.*/ 702 | build_dir: api 703 | artifacts: 704 | - "selenium/screenshots" 705 | - "~/simplecov" 706 | - "test.txt" 707 | """ 708 | self.circle.write(config) 709 | self.circle.close() 710 | 711 | def tearDown(self): 712 | """Remove the mock circle.yml file.""" 713 | os.remove(self.circle.name) 714 | 715 | def test_valid_config(self): 716 | """Should detect a valid config.""" 717 | self.assertTrue(validate_circle_yml(self.circle.name)) 718 | 719 | 720 | class TestInvalidConfigGeneral(unittest.TestCase): 721 | """Test an unaccepted format for the General section.""" 722 | 723 | def setUp(self): 724 | """Create a mock circle.yml file.""" 725 | self.circle = open('mocks/circle.yml', 'w') 726 | config = """ 727 | general: 728 | branches: 729 | - gh-pages 730 | - /release\/.*/ 731 | """ 732 | self.circle.write(config) 733 | self.circle.close() 734 | 735 | def tearDown(self): 736 | """Remove the mock circle.yml file.""" 737 | os.remove(self.circle.name) 738 | 739 | def test_invalid_config(self): 740 | """Should detect an invalid config.""" 741 | self.assertRaises(InvalidSectionError, validate_circle_yml, self.circle.name) 742 | 743 | 744 | class TestUnrecognizedConfigGeneral(unittest.TestCase): 745 | """Test an unrecognized subsection for the General section.""" 746 | 747 | def setUp(self): 748 | """Create a mock circle.yml file.""" 749 | self.circle = open('mocks/circle.yml', 'w') 750 | config = """ 751 | general: 752 | branches: 753 | ignore: 754 | - gh-pages 755 | - /release\/.*/ 756 | build_dir: api 757 | artifacts: 758 | - "selenium/screenshots" 759 | - "~/simplecov" 760 | - "test.txt" 761 | post: 762 | - mkdir $CIRCLE_ARTIFACTS/json_output 763 | - mv solo/target/*.json $CIRCLE_ARTIFACTS/json_output 764 | """ 765 | self.circle.write(config) 766 | self.circle.close() 767 | 768 | def tearDown(self): 769 | """Remove the mock circle.yml file.""" 770 | os.remove(self.circle.name) 771 | 772 | def test_invalid_config(self): 773 | """Should detect an invalid config.""" 774 | self.assertRaises(UnrecognizedSectionError, validate_circle_yml, self.circle.name) 775 | 776 | 777 | class TestValidConfigExperimental(unittest.TestCase): 778 | """Test a valid config for the Experimental section.""" 779 | 780 | def setUp(self): 781 | """Create a mock circle.yml file.""" 782 | self.circle = open('mocks/circle.yml', 'w') 783 | config = """ 784 | experimental: 785 | notify: 786 | branches: 787 | only: 788 | - /feature.*/ 789 | ignore: 790 | - /feature\.experiment.*/ 791 | """ 792 | self.circle.write(config) 793 | self.circle.close() 794 | 795 | def tearDown(self): 796 | """Remove the mock circle.yml file.""" 797 | os.remove(self.circle.name) 798 | 799 | def test_valid_config(self): 800 | """Should detect a valid config.""" 801 | self.assertTrue(validate_circle_yml(self.circle.name)) 802 | 803 | 804 | class TestInvalidConfigExperimental(unittest.TestCase): 805 | """Test an unaccepted format for the Experimental section.""" 806 | 807 | def setUp(self): 808 | """Create a mock circle.yml file.""" 809 | self.circle = open('mocks/circle.yml', 'w') 810 | config = """ 811 | experimental: 812 | notify: 813 | branches: 814 | only: /feature.*/ 815 | ignore: /feature\.experiment.*/ 816 | """ 817 | self.circle.write(config) 818 | self.circle.close() 819 | 820 | def tearDown(self): 821 | """Remove the mock circle.yml file.""" 822 | os.remove(self.circle.name) 823 | 824 | def test_invalid_config(self): 825 | """Should detect an invalid config.""" 826 | self.assertRaises(InvalidSectionError, validate_circle_yml, self.circle.name) 827 | 828 | 829 | class TestUnrecognizedConfigExperimental(unittest.TestCase): 830 | """Test an unrecognized subsection for the Experimental section.""" 831 | 832 | def setUp(self): 833 | """Create a mock circle.yml file.""" 834 | self.circle = open('mocks/circle.yml', 'w') 835 | config = """ 836 | experimental: 837 | notify: 838 | webhooks: 839 | - url: https://example.com/hooks/circleci 840 | branches: 841 | only: 842 | - /feature.*/ 843 | ignore: 844 | - /feature\.experiment.*/ 845 | """ 846 | self.circle.write(config) 847 | self.circle.close() 848 | 849 | def tearDown(self): 850 | """Remove the mock circle.yml file.""" 851 | os.remove(self.circle.name) 852 | 853 | def test_invalid_config(self): 854 | """Should detect an invalid config.""" 855 | self.assertRaises(UnrecognizedSectionError, validate_circle_yml, self.circle.name) 856 | 857 | 858 | class TestCircleCIStatus(unittest.TestCase): 859 | """Test the ability to parse CircleCI page status.""" 860 | 861 | @with_httmock(mocks.circlecli.resource_get) 862 | def test_circleci_status_scrape(self): 863 | """Should scape the CircleCI status page and print status.""" 864 | results = circle_status(verbose=False) 865 | print results 866 | 867 | self.assertEqual(results[0][1], 'All Systems Operational') 868 | 869 | @with_httmock(mocks.circlecli.resource_get) 870 | def test_circleci_status_scrape_verbose(self): 871 | """Should scape the CircleCI status page and print status.""" 872 | results = circle_status(verbose=True) 873 | print results 874 | 875 | self.assertEqual(len(results), 10) 876 | self.assertEqual(results[0][1], 'All Systems Operational') 877 | self.assertEqual(results[1][0], 'CircleCI') 878 | self.assertEqual(results[1][1], 'Operational') --------------------------------------------------------------------------------