├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── bin └── xcskarel ├── fastlane ├── Fastfile └── README.md ├── lib ├── xcskarel.rb └── xcskarel │ ├── application.rb │ ├── config.rb │ ├── control.rb │ ├── filter.rb │ ├── log.rb │ ├── remote.rb │ ├── server.rb │ ├── shell.rb │ ├── version.rb │ └── xcsfile.rb ├── scripts └── bootstrap_xcsbuildd.sh ├── spec ├── application_spec.rb ├── default_spec.rb └── filter_spec.rb ├── xcsconfig └── botconfig_test_bot.json └── xcskarel.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /test/tmp/ 9 | /test/version_tmp/ 10 | /tmp/ 11 | 12 | ## Specific to RubyMotion: 13 | .dat* 14 | .repl_history 15 | build/ 16 | 17 | ## Documentation cache and generated files: 18 | /.yardoc/ 19 | /_yardoc/ 20 | /doc/ 21 | /rdoc/ 22 | 23 | ## Environment normalisation: 24 | /.bundle/ 25 | /vendor/bundle 26 | /lib/bundler/man/ 27 | 28 | # for a library or gem, you might want to ignore these files since the code is 29 | # intended to run in multiple environments; otherwise, check them in: 30 | # Gemfile.lock 31 | # .ruby-version 32 | # .ruby-gemset 33 | 34 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 35 | .rvmrc 36 | report.xml 37 | 38 | .idea 39 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | before_install: 3 | - gem update bundler 4 | rvm: 5 | - 2.0.0 6 | - 2.1.6 7 | - 2.2.2 8 | script: bundle exec fastlane ci_test 9 | notifications: 10 | email: 11 | on_success: never 12 | on_failure: change 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [0.16.1](https://github.com/czechboy0/xcskarel/tree/0.16.1) (2015-09-29) 4 | [Full Changelog](https://github.com/czechboy0/xcskarel/compare/0.16.0...0.16.1) 5 | 6 | **Closed issues:** 7 | 8 | - add an option to print xcs related headers [\#6](https://github.com/czechboy0/xcskarel/issues/6) 9 | 10 | ## [0.16.0](https://github.com/czechboy0/xcskarel/tree/0.16.0) (2015-09-06) 11 | [Full Changelog](https://github.com/czechboy0/xcskarel/compare/0.14.0...0.16.0) 12 | 13 | **Closed issues:** 14 | 15 | - Add travis & tests [\#4](https://github.com/czechboy0/xcskarel/issues/4) 16 | - Default --host to localhost [\#3](https://github.com/czechboy0/xcskarel/issues/3) 17 | - Migrate to fastlane for easy deployment [\#1](https://github.com/czechboy0/xcskarel/issues/1) 18 | 19 | ## [0.14.0](https://github.com/czechboy0/xcskarel/tree/0.14.0) (2015-09-03) 20 | [Full Changelog](https://github.com/czechboy0/xcskarel/compare/0.11.0...0.14.0) 21 | 22 | **Merged pull requests:** 23 | 24 | - New endpoint: Issues, more powerful JSON filtering [\#11](https://github.com/czechboy0/xcskarel/pull/11) ([czechboy0](https://github.com/czechboy0)) 25 | 26 | ## [0.11.0](https://github.com/czechboy0/xcskarel/tree/0.11.0) (2015-09-02) 27 | [Full Changelog](https://github.com/czechboy0/xcskarel/compare/0.10.0...0.11.0) 28 | 29 | **Merged pull requests:** 30 | 31 | - Fix: Status for Bots with 0 integrations, added Branch to status view [\#10](https://github.com/czechboy0/xcskarel/pull/10) ([czechboy0](https://github.com/czechboy0)) 32 | 33 | ## [0.10.0](https://github.com/czechboy0/xcskarel/tree/0.10.0) (2015-09-02) 34 | [Full Changelog](https://github.com/czechboy0/xcskarel/compare/0.8.0...0.10.0) 35 | 36 | **Merged pull requests:** 37 | 38 | - Start integration & see bot statuses commands [\#9](https://github.com/czechboy0/xcskarel/pull/9) ([czechboy0](https://github.com/czechboy0)) 39 | 40 | ## [0.8.0](https://github.com/czechboy0/xcskarel/tree/0.8.0) (2015-09-02) 41 | [Full Changelog](https://github.com/czechboy0/xcskarel/compare/0.7.1...0.8.0) 42 | 43 | **Merged pull requests:** 44 | 45 | - Bot Configs Managements [\#8](https://github.com/czechboy0/xcskarel/pull/8) ([czechboy0](https://github.com/czechboy0)) 46 | 47 | ## [0.7.1](https://github.com/czechboy0/xcskarel/tree/0.7.1) (2015-09-01) 48 | [Full Changelog](https://github.com/czechboy0/xcskarel/compare/0.7.0...0.7.1) 49 | 50 | ## [0.7.0](https://github.com/czechboy0/xcskarel/tree/0.7.0) (2015-09-01) 51 | [Full Changelog](https://github.com/czechboy0/xcskarel/compare/0.3.2...0.7.0) 52 | 53 | **Closed issues:** 54 | 55 | - Wrap `xcscontrol` commands for starting/stopping xcode server [\#2](https://github.com/czechboy0/xcskarel/issues/2) 56 | 57 | **Merged pull requests:** 58 | 59 | - Nested Filter etc [\#7](https://github.com/czechboy0/xcskarel/pull/7) ([czechboy0](https://github.com/czechboy0)) 60 | - Adding tests, fastlane infra [\#5](https://github.com/czechboy0/xcskarel/pull/5) ([czechboy0](https://github.com/czechboy0)) 61 | 62 | ## [0.3.2](https://github.com/czechboy0/xcskarel/tree/0.3.2) (2015-08-26) 63 | [Full Changelog](https://github.com/czechboy0/xcskarel/compare/0.3.1...0.3.2) 64 | 65 | ## [0.3.1](https://github.com/czechboy0/xcskarel/tree/0.3.1) (2015-08-26) 66 | [Full Changelog](https://github.com/czechboy0/xcskarel/compare/0.3...0.3.1) 67 | 68 | ## [0.3](https://github.com/czechboy0/xcskarel/tree/0.3) (2015-08-26) 69 | [Full Changelog](https://github.com/czechboy0/xcskarel/compare/0.2.1...0.3) 70 | 71 | ## [0.2.1](https://github.com/czechboy0/xcskarel/tree/0.2.1) (2015-08-26) 72 | [Full Changelog](https://github.com/czechboy0/xcskarel/compare/0.2...0.2.1) 73 | 74 | ## [0.2](https://github.com/czechboy0/xcskarel/tree/0.2) (2015-08-26) 75 | 76 | 77 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at czechboy0@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in .gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | xcskarel (0.16.1) 5 | colored (= 1.2) 6 | commander (= 4.3.5) 7 | excon (= 0.45.4) 8 | json (= 1.8.3) 9 | logger (= 1.2.8) 10 | net-ssh (= 2.9.2) 11 | terminal-table (= 1.4.5) 12 | 13 | GEM 14 | remote: https://rubygems.org/ 15 | specs: 16 | activesupport (4.2.6) 17 | i18n (~> 0.7) 18 | json (~> 1.7, >= 1.7.7) 19 | minitest (~> 5.1) 20 | thread_safe (~> 0.3, >= 0.3.4) 21 | tzinfo (~> 1.1) 22 | addressable (2.3.8) 23 | babosa (1.0.2) 24 | byebug (5.0.0) 25 | columnize (= 0.9.0) 26 | cert (1.4.0) 27 | fastlane_core (>= 0.29.1, < 1.0.0) 28 | spaceship (>= 0.22.0, < 1.0.0) 29 | claide (0.9.1) 30 | coderay (1.1.1) 31 | colored (1.2) 32 | colorize (0.7.7) 33 | columnize (0.9.0) 34 | commander (4.3.5) 35 | highline (~> 1.7.2) 36 | credentials_manager (0.15.0) 37 | colored 38 | commander (>= 4.3.5) 39 | highline (>= 1.7.1) 40 | security 41 | deliver (1.10.5) 42 | credentials_manager (>= 0.12.0, < 1.0.0) 43 | fastimage (~> 1.6) 44 | fastlane_core (>= 0.37.0, < 1.0.0) 45 | plist (~> 3.1.0) 46 | spaceship (>= 0.19.0, < 1.0.0) 47 | descendants_tracker (0.0.4) 48 | thread_safe (~> 0.3, >= 0.3.1) 49 | diff-lcs (1.2.5) 50 | domain_name (0.5.20160310) 51 | unf (>= 0.0.5, < 1.0.0) 52 | dotenv (2.1.0) 53 | excon (0.45.4) 54 | faraday (0.9.2) 55 | multipart-post (>= 1.2, < 3) 56 | faraday-cookie_jar (0.0.6) 57 | faraday (>= 0.7.4) 58 | http-cookie (~> 1.0.0) 59 | faraday_middleware (0.10.0) 60 | faraday (>= 0.7.4, < 0.10) 61 | fastimage (1.6.8) 62 | addressable (~> 2.3, >= 2.3.5) 63 | fastlane (1.66.0) 64 | addressable (~> 2.3.8) 65 | cert (>= 1.3.0, < 2.0.0) 66 | credentials_manager (>= 0.15.0, < 1.0.0) 67 | deliver (>= 1.10.1, < 2.0.0) 68 | fastlane_core (>= 0.36.8, < 1.0.0) 69 | frameit (>= 2.4.1, < 3.0.0) 70 | gym (>= 1.6.1, < 2.0.0) 71 | krausefx-shenzhen (>= 0.14.7) 72 | match (>= 0.3.0, < 1.0.0) 73 | pem (>= 1.2.0, < 2.0.0) 74 | pilot (>= 1.3.0, < 2.0.0) 75 | plist (~> 3.1.0) 76 | produce (>= 1.1.0, < 2.0.0) 77 | scan (>= 0.5.0, < 1.0.0) 78 | screengrab (>= 0.2.1, < 1.0.0) 79 | sigh (>= 1.3.1, < 2.0.0) 80 | slack-notifier (~> 1.3) 81 | snapshot (>= 1.7.0, < 2.0.0) 82 | spaceship (>= 0.22.0, < 1.0.0) 83 | supply (>= 0.4.0, < 1.0.0) 84 | terminal-notifier (~> 1.6.2) 85 | terminal-table (~> 1.4.5) 86 | xcodeproj (>= 0.20, < 2.0.0) 87 | xcpretty (>= 0.2.1) 88 | fastlane_core (0.37.0) 89 | babosa 90 | colored 91 | commander (= 4.3.5) 92 | credentials_manager (>= 0.11.0, < 1.0.0) 93 | excon (~> 0.45.0) 94 | highline (>= 1.7.2) 95 | json 96 | multi_json 97 | plist (~> 3.1) 98 | rubyzip (~> 1.1.6) 99 | sentry-raven (~> 0.15) 100 | terminal-table (~> 1.4.5) 101 | frameit (2.5.1) 102 | deliver (> 0.3) 103 | fastimage (~> 1.6.3) 104 | fastlane_core (>= 0.36.1, < 1.0.0) 105 | mini_magick (~> 4.0.2) 106 | github_api (0.12.4) 107 | addressable (~> 2.3) 108 | descendants_tracker (~> 0.0.4) 109 | faraday (~> 0.8, < 0.10) 110 | hashie (>= 3.4) 111 | multi_json (>= 1.7.5, < 2.0) 112 | nokogiri (~> 1.6.6) 113 | oauth2 114 | github_changelog_generator (1.10.1) 115 | colorize (~> 0.7) 116 | github_api (~> 0.12) 117 | google-api-client (0.9.4) 118 | addressable (~> 2.3) 119 | googleauth (~> 0.5) 120 | httpclient (~> 2.7) 121 | hurley (~> 0.1) 122 | memoist (~> 0.11) 123 | mime-types (>= 1.6) 124 | representable (~> 2.3.0) 125 | retriable (~> 2.0) 126 | thor (~> 0.19) 127 | googleauth (0.5.1) 128 | faraday (~> 0.9) 129 | jwt (~> 1.4) 130 | logging (~> 2.0) 131 | memoist (~> 0.12) 132 | multi_json (~> 1.11) 133 | os (~> 0.9) 134 | signet (~> 0.7) 135 | gym (1.6.2) 136 | fastlane_core (>= 0.36.1, < 1.0.0) 137 | plist 138 | rubyzip (>= 1.1.7) 139 | terminal-table 140 | xcpretty (>= 0.2.1) 141 | hashie (3.4.3) 142 | highline (1.7.8) 143 | http-cookie (1.0.2) 144 | domain_name (~> 0.5) 145 | httpclient (2.7.1) 146 | hurley (0.2) 147 | i18n (0.7.0) 148 | json (1.8.3) 149 | jwt (1.5.1) 150 | krausefx-shenzhen (0.14.7) 151 | commander (~> 4.3) 152 | dotenv (>= 0.7) 153 | faraday (~> 0.9) 154 | faraday_middleware (~> 0.9) 155 | highline (>= 1.7.2) 156 | json (~> 1.8) 157 | net-sftp (~> 2.1.2) 158 | plist (~> 3.1.0) 159 | rubyzip (~> 1.1) 160 | security (~> 0.1.3) 161 | terminal-table (~> 1.4.5) 162 | little-plugger (1.1.4) 163 | logger (1.2.8) 164 | logging (2.1.0) 165 | little-plugger (~> 1.1) 166 | multi_json (~> 1.10) 167 | match (0.3.0) 168 | cert (>= 1.2.8, < 2.0.0) 169 | credentials_manager (>= 0.13.0, < 1.0.0) 170 | fastlane_core (>= 0.36.1, < 1.0.0) 171 | security 172 | sigh (>= 1.2.2, < 2.0.0) 173 | spaceship (>= 0.18.1, < 1.0.0) 174 | memoist (0.14.0) 175 | method_source (0.8.2) 176 | mime-types (3.0) 177 | mime-types-data (~> 3.2015) 178 | mime-types-data (3.2016.0221) 179 | mini_magick (4.0.4) 180 | mini_portile2 (2.0.0) 181 | minitest (5.8.4) 182 | multi_json (1.11.2) 183 | multi_xml (0.5.5) 184 | multipart-post (2.0.0) 185 | net-sftp (2.1.2) 186 | net-ssh (>= 2.6.5) 187 | net-ssh (2.9.2) 188 | nokogiri (1.6.7.2) 189 | mini_portile2 (~> 2.0.0.rc2) 190 | oauth2 (1.1.0) 191 | faraday (>= 0.8, < 0.10) 192 | jwt (~> 1.0, < 1.5.2) 193 | multi_json (~> 1.3) 194 | multi_xml (~> 0.5) 195 | rack (>= 1.2, < 3) 196 | os (0.9.6) 197 | pem (1.3.0) 198 | fastlane_core (>= 0.36.1, < 1.0.0) 199 | spaceship (>= 0.22.0, < 1.0.0) 200 | pilot (1.4.1) 201 | credentials_manager (>= 0.3.0) 202 | fastlane_core (>= 0.36.5, < 1.0.0) 203 | spaceship (>= 0.20.0, < 1.0.0) 204 | terminal-table (~> 1.4.5) 205 | plist (3.1.0) 206 | produce (1.1.1) 207 | fastlane_core (>= 0.30.0, < 1.0.0) 208 | spaceship (>= 0.16.0) 209 | pry (0.10.1) 210 | coderay (~> 1.1.0) 211 | method_source (~> 0.8.1) 212 | slop (~> 3.4) 213 | pry-byebug (3.2.0) 214 | byebug (~> 5.0) 215 | pry (~> 0.10) 216 | rack (1.6.4) 217 | rake (11.1.1) 218 | representable (2.3.0) 219 | uber (~> 0.0.7) 220 | retriable (2.1.0) 221 | rouge (1.10.1) 222 | rspec (3.1.0) 223 | rspec-core (~> 3.1.0) 224 | rspec-expectations (~> 3.1.0) 225 | rspec-mocks (~> 3.1.0) 226 | rspec-core (3.1.7) 227 | rspec-support (~> 3.1.0) 228 | rspec-expectations (3.1.2) 229 | diff-lcs (>= 1.2.0, < 2.0) 230 | rspec-support (~> 3.1.0) 231 | rspec-mocks (3.1.3) 232 | rspec-support (~> 3.1.0) 233 | rspec-support (3.1.2) 234 | rubyzip (1.1.7) 235 | scan (0.5.2) 236 | fastlane_core (>= 0.36.1, < 1.0.0) 237 | slack-notifier (~> 1.3) 238 | terminal-table 239 | xcpretty (>= 0.2.1) 240 | xcpretty-travis-formatter (>= 0.0.3) 241 | screengrab (0.3.0) 242 | fastlane_core (>= 0.36.8, < 1.0.0) 243 | security (0.1.3) 244 | sentry-raven (0.15.6) 245 | faraday (>= 0.7.6) 246 | sigh (1.4.1) 247 | fastlane_core (>= 0.36.1, < 1.0.0) 248 | plist (~> 3.1) 249 | spaceship (>= 0.22.0, < 1.0.0) 250 | signet (0.7.2) 251 | addressable (~> 2.3) 252 | faraday (~> 0.9) 253 | jwt (~> 1.5) 254 | multi_json (~> 1.10) 255 | slack-notifier (1.5.1) 256 | slop (3.6.0) 257 | snapshot (1.11.1) 258 | fastimage (~> 1.6.3) 259 | fastlane_core (>= 0.36.1, < 1.0.0) 260 | plist (~> 3.1.0) 261 | xcpretty (>= 0.2.1) 262 | spaceship (0.23.0) 263 | colored 264 | credentials_manager (>= 0.9.0) 265 | faraday (~> 0.9) 266 | faraday-cookie_jar (~> 0.0.6) 267 | faraday_middleware (~> 0.9) 268 | fastimage (~> 1.6) 269 | multi_xml (~> 0.5) 270 | plist (~> 3.1) 271 | supply (0.5.2) 272 | credentials_manager (>= 0.15.0) 273 | fastlane_core (>= 0.35.0) 274 | google-api-client (~> 0.9.1) 275 | terminal-notifier (1.6.3) 276 | terminal-table (1.4.5) 277 | thor (0.19.1) 278 | thread_safe (0.3.5) 279 | tzinfo (1.2.2) 280 | thread_safe (~> 0.1) 281 | uber (0.0.15) 282 | unf (0.1.4) 283 | unf_ext 284 | unf_ext (0.0.7.2) 285 | xcodeproj (0.28.2) 286 | activesupport (>= 3) 287 | claide (~> 0.9.1) 288 | colored (~> 1.2) 289 | xcpretty (0.2.2) 290 | rouge (~> 1.8) 291 | xcpretty-travis-formatter (0.0.4) 292 | xcpretty (~> 0.2, >= 0.0.7) 293 | 294 | PLATFORMS 295 | ruby 296 | 297 | DEPENDENCIES 298 | bundler 299 | fastlane 300 | github_changelog_generator 301 | pry (= 0.10.1) 302 | pry-byebug (= 3.2.0) 303 | rake 304 | rspec (~> 3.1.0) 305 | xcskarel! 306 | 307 | BUNDLED WITH 308 | 1.11.2 309 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Honza Dvorsky 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. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xcskarel 2 | 3 | [![Travis Status](https://travis-ci.org/czechboy0/xcskarel.svg)](https://travis-ci.org/czechboy0/xcskarel) 4 | [![Gem Version](https://badge.fury.io/rb/xcskarel.svg)](http://badge.fury.io/rb/xcskarel) 5 | 6 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](http://en.wikipedia.org/wiki/MIT_License) 7 | [![Blog](https://img.shields.io/badge/blog-honzadvorsky.com-green.svg)](http://honzadvorsky.com) 8 | [![Twitter Czechboy0](https://img.shields.io/badge/twitter-czechboy0-green.svg)](http://twitter.com/czechboy0) 9 | 10 | Quickly manage your Xcode Server & Bots from the command line. 11 | 12 | # :white_check_mark: install 13 | Clone the repo or install from [RubyGems](https://rubygems.org/gems/xcskarel) 14 | 15 | ``` 16 | sudo gem install xcskarel 17 | ``` 18 | 19 | # :nut_and_bolt: usage 20 | To see all options, run `xcskarel --help`. 21 | 22 | ## bots 23 | 24 | ```sh 25 | $ xcskarel bots --host 10.99.0.57 26 | [ 27 | { 28 | "_id": "02440afb1e7a51dc9795319121038f17", 29 | "name": "buildasaur-release" 30 | } 31 | ] 32 | ``` 33 | 34 | ## integrations 35 | 36 | ```sh 37 | $ xcskarel integrations --bot "Builda Archiver" --host 192.168.1.64 38 | [ 39 | { 40 | "_id": "e5fed3c8bdf03d30278a016e64003a1d", 41 | "currentStep": "checkout", 42 | "number": 11 43 | }, 44 | { 45 | "_id": "a3ad85e42b7b7edbf43f9dca4a14390f", 46 | "currentStep": "completed", 47 | "number": 10, 48 | "result": "checkout-error" 49 | } 50 | ] 51 | ``` 52 | 53 | ## status 54 | 55 | ```sh 56 | $ xcskarel status --host 192.168.1.64 57 | +--------------------------------------------------+----------------------------------+---------+--------------+-----------+-------+ 58 | | https://192.168.1.64 | 59 | +--------------------------------------------------+----------------------------------+---------+--------------+-----------+-------+ 60 | | name | id | branch | current_step | result | count | 61 | +--------------------------------------------------+----------------------------------+---------+--------------+-----------+-------+ 62 | | Builda Archiver | 6b3de48352a8126ce7e08ecf85093613 | master | pending | | 11 | 63 | | Builda On Commit | 6c8d3225beff941b3a420554df16cb0d | master | checkout | | 70 | 64 | | BuildaBot [czechboy0/XcodeServerSDK] |-> swift-2 | 3ce25bc9ed5bfffb854947b02600166d | swift-2 | completed | succeeded | 6 | 65 | +--------------------------------------------------+----------------------------------+---------+--------------+-----------+-------+ 66 | ``` 67 | 68 | ## start an integration 69 | 70 | ```sh 71 | $ xcskarel integrate --bot "Builda Archiver" --host 192.168.1.64 72 | INFO [2015-09-03 17:04:59.14]: Successfully started integration 11 on Bot "Builda Archiver" 73 | +--------------------------------------------------+----------------------------------+---------+--------------+-----------+-------+ 74 | | https://192.168.1.64 | 75 | +--------------------------------------------------+----------------------------------+---------+--------------+-----------+-------+ 76 | | name | id | branch | current_step | result | count | 77 | +--------------------------------------------------+----------------------------------+---------+--------------+-----------+-------+ 78 | | Builda Archiver | 6b3de48352a8126ce7e08ecf85093613 | master | pending | | 11 | 79 | | Builda On Commit | 6c8d3225beff941b3a420554df16cb0d | master | checkout | | 70 | 80 | | BuildaBot [czechboy0/XcodeServerSDK] |-> swift-2 | 3ce25bc9ed5bfffb854947b02600166d | swift-2 | completed | succeeded | 6 | 81 | +--------------------------------------------------+----------------------------------+---------+--------------+-----------+-------+ 82 | ``` 83 | 84 | ## integration issues 85 | 86 | ```sh 87 | $ xcskarel issues --bot "Buildasaur Bot" 88 | { 89 | "buildServiceWarnings": [ 90 | { 91 | "message": "An error occurred while building, so archiving was skipped." 92 | } 93 | ], 94 | "errors": { 95 | "unresolvedIssues": [ 96 | { 97 | "message": "No code signing identities found: No valid signing identities (i.e. certificate and private key pair) matching the team ID “7BJ2984YDK” were found." 98 | } 99 | ] 100 | } 101 | } 102 | ``` 103 | 104 | you can also specify the Integration id like this `xcskarel issues --integration 6b3de48352a8126ce7e08ecf85093613`. When you specify a Bot id or a name, however, the last Integration is used. 105 | 106 | ## manage bot configurations in your git repo 107 | - `config list` Lists the Xcode Bot configurations found in this folder 108 | - `config new` Starts the interactive process of creating a new config from an existing Bot 109 | - `config show` Opens the selected config for editing 110 | 111 | ## others 112 | - `xcskarel logs --host 10.99.0.57 --user honzadvorsky` - prints build and control logs from the remote Xcode Server 113 | - `xcskarel health --host 10.99.0.57` - health information of the server like uptime etc 114 | 115 | ## server (wraps [xcscontrol](http://honzadvorsky.com/articles/2015-08-12-xcode_server_hacks_cli_xcscontrol/) commands) 116 | - `xcskarel server start` - fully starts a local Xcode Server instance 117 | - `xcskarel server stop` - stops the local Xcode Server instance 118 | - `xcskarel server restart` - restarts the local Xcode Server instance 119 | - `xcskarel server reset` - stops and deletes the local Xcode Server instance 120 | 121 | ## xcode 122 | - `xcskarel xcode select` - Interactive `xcode-select`, finds all local Xcode's and asks which you want to use from now on 123 | 124 | ## options 125 | 126 | Two options useful for all commands: 127 | 128 | ``` 129 | --no_pretty 130 | Disables output JSON prettification 131 | --no_filter 132 | Prints full JSON payload for objects instead of just filtering the important ones 133 | ``` 134 | 135 | You can also use environment variables to specify your preferred server host and credentials with `XCSKAREL_HOST`, `XCSKAREL_USER`, and `XCSKAREL_PASS`. If no host is specified, `localhost` is used. To always use the server at, e.g. `192.168.1.64`, just add this to your `~/.zshrc` or `~/.bash_profile`: 136 | 137 | ```sh 138 | export XCSKAREL_HOST="192.168.1.64" 139 | ``` 140 | 141 | # :octocat: my Xcode Server projects 142 | - [Xcode Server Tutorials](http://honzadvorsky.com/pages/xcode_server_tutorials/) - on how to set up Xcode Server on your Mac in minutes 143 | - [XcodeServerSDK](https://github.com/czechboy0/XcodeServerSDK) - full Xcode Server SDK written in Swift 144 | - [Buildasaur](https://github.com/czechboy0/Buildasaur) - connecting Xcode Server with GitHub Pull Requests 145 | - [XcodeServer-API-Docs](https://github.com/czechboy0/XcodeServer-API-Docs) - Unofficial attempt at full Xcode Server API documentation 146 | 147 | :blue_heart: Code of Conduct 148 | ------------ 149 | Please note that this project is released with a [Contributor Code of Conduct](./CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. 150 | 151 | # :gift_heart: Contributing 152 | Please create an issue with a description of a problem or a pull request with a fix. 153 | 154 | # :v: License 155 | MIT 156 | 157 | # :alien: Author 158 | Honza Dvorsky - http://honzadvorsky.com, [@czechboy0](http://twitter.com/czechboy0) 159 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | RSpec::Core::RakeTask.new(:spec) 4 | task default: :spec 5 | -------------------------------------------------------------------------------- /bin/xcskarel: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $LOAD_PATH.push File.expand_path('../../lib', __FILE__) 4 | 5 | require 'XCSKarel' 6 | require 'rubygems' 7 | require 'commander' 8 | 9 | XCSKarel.set_no_log(true) 10 | 11 | class XCSKarelApplication 12 | include Commander::Methods 13 | 14 | def add_xcs_creds(c) 15 | c.option '--host Hostname', 'Xcode Server\'s hostname or IP address (default: localhost) (also can be specified as the environment variable XCSKAREL_HOST)' 16 | c.option '--user Username', 'Xcode Server username (also can be specified as the environment variable XCSKAREL_USER)' 17 | c.option '--pass Password', 'Xcode Server password (also can be specified as the environment variable XCSKAREL_PASS)' 18 | end 19 | 20 | def add_xcs_options(c) 21 | add_xcs_creds(c) 22 | end 23 | 24 | def add_bot_options(c) 25 | c.option '--bot BOT_ID_OR_NAME', '(required) Bot identifier or name' 26 | end 27 | 28 | def add_bot_or_integration_options(c) 29 | c.option '--bot BOT_ID_OR_NAME', 'Bot identifier or name' 30 | c.option '--integration INTEGRATION_ID', 'Integration identifier' 31 | end 32 | 33 | def creds_from_options(options) 34 | host = options.host || ENV['XCSKAREL_HOST'] || "localhost" 35 | user = options.user || ENV['XCSKAREL_USER'] || ENV['USER'] 36 | pass = options.pass || ENV['XCSKAREL_PASS'] 37 | return host, user, pass 38 | end 39 | 40 | def create_server_from_options(options) 41 | creds = creds_from_options(options) 42 | XCSKarel::Server.new(*creds) 43 | end 44 | 45 | def create_connection_from_options(options) 46 | creds = creds_from_options(options) 47 | XCSKarel::Remote::Connection.new(*creds) 48 | end 49 | 50 | def run 51 | program :name, 'xcskarel' 52 | program :version, XCSKarel::VERSION 53 | program :description, 'Tool for managing your Xcode Server & Bot configurations' 54 | program :help, 'Author', 'Honza Dvorsky ' 55 | program :help, 'GitHub', 'https://github.com/czechboy0/xcskarel' 56 | 57 | global_option('--verbose', 'Print internal logs') { XCSKarel.set_no_log(false) } 58 | global_option('--no_pretty', 'Disables output JSON prettification') 59 | global_option('--no_filter', 'Prints full JSON payload for objects instead of just filtering the important ones') 60 | 61 | # Managing local xcsconfig folder 62 | 63 | command :'config list' do |c| 64 | c.syntax = 'xcskarel config list [options]' 65 | c.description = 'Lists the Xcode Bot configurations found in this folder' 66 | c.example 'lists all configurations stored in this folder', 'xcskarel config list' 67 | c.action do |args, options| 68 | config_folder = XCSKarel::XCSFile.get_config_folder 69 | return unless config_folder 70 | XCSKarel::Application.list_configs(config_folder) 71 | end 72 | end 73 | 74 | command :'config show' do |c| 75 | c.syntax = 'xcskarel config show [options]' 76 | c.description = 'Opens the selected config for editing' 77 | c.example 'opens a config of choice', 'xcskarel config show' 78 | c.action do |args, options| 79 | config_folder = XCSKarel::XCSFile.get_config_folder 80 | return unless config_folder 81 | XCSKarel::Application.show_config(config_folder) 82 | end 83 | end 84 | 85 | command :'config new' do |c| 86 | c.syntax = 'xcskarel config new [options]' 87 | c.description = 'Starts the interactive process of creating a new config from an existing Bot' 88 | c.example 'starts creating new config from server at 192.168.1.64', 'xcskarel config new --host 192.168.1.64' 89 | add_xcs_options(c) 90 | c.action do |args, options| 91 | 92 | # let user chose a bot from the server 93 | server = create_server_from_options(options) 94 | bot = XCSKarel::Application.choose_bot(server) 95 | 96 | # get our config folder 97 | config_folder = XCSKarel::XCSFile.get_config_folder 98 | return unless config_folder 99 | 100 | # dump the bot into that config folder under a random name 101 | XCSKarel::Application.save_bot(config_folder, bot, server.api_version) 102 | end 103 | end 104 | 105 | # Talking to Xcode Server API 106 | 107 | command :bots do |c| 108 | c.syntax = 'xcskarel bots [options]' 109 | c.description = 'Fetches all Bots found on the specified server' 110 | c.example 'get all bot names & identifiers', 'xcskarel bots --hostname 127.0.0.1' 111 | c.example 'get all bot metadata', 'xcskarel bots --no_filter --hostname 127.0.0.1' 112 | add_xcs_options(c) 113 | c.action do |args, options| 114 | server = create_server_from_options(options) 115 | all_bots = server.get_bots 116 | puts XCSKarel::Application.format(all_bots, options, ['name', '_id']) 117 | end 118 | end 119 | 120 | command :integrations do |c| 121 | c.syntax = 'xcskarel integrations [options]' 122 | c.description = 'Fetches all Integrations for a specified Bot identifier' 123 | c.example 'get all integration identifiers, numbers & results', 'xcskarel integrations --bot BOT_ID --hostname 127.0.0.1' 124 | add_xcs_options(c) 125 | add_bot_options(c) 126 | c.action do |args, options| 127 | raise "No Bot id was specified, please see `xcskarel integrations --help`" unless options.bot 128 | server = create_server_from_options(options) 129 | all_integrations = XCSKarel::Application.integrations(server, options.bot) 130 | puts XCSKarel::Application.format(all_integrations, options, ['number', '_id', 'currentStep', 'result']) 131 | end 132 | end 133 | 134 | command :issues do |c| 135 | c.syntax = 'xcskarel issues [options]' 136 | c.description = 'Fetches issues of an integration or a Bot\'s last integration' 137 | c.example 'fetches issues for a given integration on localhost', 'xcskarel issues --integration 448946985304230369392c2e6b05a821' 138 | c.example 'fetches issues for a given bot\'s last integration', 'xcskarel issues --bot "My Bot" --host 192.168.1.64' 139 | add_xcs_options(c) 140 | add_bot_or_integration_options(c) 141 | c.action do |args, options| 142 | server = create_server_from_options(options) 143 | bot = options.bot 144 | integration = options.integration 145 | raise "Exactly one of Bot or Integration must be specified, please see `xcskarel issues --help`" unless bot || integration 146 | all_issues = XCSKarel::Application.issues(server, bot, integration) 147 | key_paths = [ 148 | 'buildServiceErrors.*', 149 | 'buildServiceWarnings.message', 150 | 'triggerErrors.*', 151 | 'analyzerWarnings.*', 152 | 'errors.*.message', 153 | 'testFailures.*.testCase', 154 | 'testFailures.*.documentFilePath', 155 | 'testFailures.*.message', 156 | 'warnings.*.message', 157 | 'warnings.*.documentFilePath' 158 | ] 159 | puts XCSKarel::Application.format(all_issues, options, key_paths, false) 160 | end 161 | end 162 | 163 | command :integrate do |c| 164 | c.syntax = 'xcskarel integrate [options]' 165 | c.description = 'Kicks of an integration for the specified Bot id or name' 166 | c.example 'starts an integration on localhost for bot name', 'xcskarel integrate --bot bot_archiver' 167 | c.example 'starts an integration on a remote server for bot id', 'xcskarel integrate --bot 448946985304230369392c2e6b05a821 --host 192.168.1.64' 168 | add_xcs_options(c) 169 | add_bot_options(c) 170 | c.action do |args, options| 171 | bot = options.bot 172 | raise "No Bot id or name was specified, please see `xcskarel integrations --help`" unless bot 173 | server = create_server_from_options(options) 174 | XCSKarel::Application.integrate(server, bot) 175 | end 176 | end 177 | 178 | command :health do |c| 179 | c.syntax = 'xcskarel health [options]' 180 | c.description = 'Fetches health information of the specified server' 181 | c.example 'get health', 'xcskarel health --hostname 127.0.0.1' 182 | add_xcs_options(c) 183 | c.action do |args, options| 184 | server = create_server_from_options(options) 185 | all_health = server.get_health 186 | puts XCSKarel::Application.format(all_health, options, ['uptime']) 187 | end 188 | end 189 | 190 | command :status do |c| 191 | c.syntax = 'xcskarel status [options]' 192 | c.description = 'Fetches status information of the specified server' 193 | c.example 'get status', 'xcskarel status --hostname 127.0.0.1' 194 | add_xcs_options(c) 195 | c.action do |args, options| 196 | server = create_server_from_options(options) 197 | XCSKarel::Application.print_status(server) 198 | end 199 | end 200 | 201 | # Connecting to an Xcode Server machine 202 | 203 | command :logs do |c| 204 | c.syntax = 'xcskarel logs [options]' 205 | c.description = 'Gets build and control logs' 206 | c.example 'print logs', 'xcskarel logs --host 192.168.1.64 --user honzadvorsky' 207 | add_xcs_creds(c) 208 | c.action do |args, options| 209 | connection = create_connection_from_options(options) 210 | XCSKarel::Application.remote_logs(connection) 211 | end 212 | end 213 | 214 | # Managing a local Xcode Server 215 | 216 | command :'server start' do |c| 217 | c.syntax = 'xcskarel server start [options]' 218 | c.description = 'Start local Xcode Server' 219 | c.example 'start xcode server', 'xcskarel server start --path /Applications/Xcode.app' 220 | c.option '--path XCODE_PATH', 'Xcode path' 221 | c.action do |args, options| 222 | xcode = options.path 223 | unless options.path 224 | xcodes = XCSKarel::Control.installed_xcodes 225 | raise "No Xcode found, please provide --path" if xcodes.count == 0 226 | if xcodes.count > 1 227 | choice = choose("Select Xcode version", *xcodes) 228 | xcode = choice 229 | else 230 | xcode = xcodes.first 231 | end 232 | end 233 | 234 | XCSKarel::Control.start(xcode) 235 | end 236 | end 237 | 238 | command :'server stop' do |c| 239 | c.syntax = 'xcskarel server stop' 240 | c.description = 'Stop local Xcode Server' 241 | c.example 'stop xcode server', 'xcskarel server stop' 242 | c.action do |args, options| 243 | XCSKarel::Control.stop 244 | end 245 | end 246 | 247 | command :'server restart' do |c| 248 | c.syntax = 'xcskarel server restart' 249 | c.description = 'Restart local Xcode Server' 250 | c.example 'restart xcode server', 'xcskarel server restart' 251 | c.action do |args, options| 252 | XCSKarel::Control.restart 253 | end 254 | end 255 | 256 | command :'server reset' do |c| 257 | c.syntax = 'xcskarel server reset' 258 | c.description = 'Reset local Xcode Server (!!! Deletes all local Bots & Integrations !!!)' 259 | c.example 'reset xcode server', 'xcskarel server reset' 260 | c.action do |args, options| 261 | resp = agree("Are you sure you want to reset your local Xcode Server? This will delete all local Bots and Integrations! (y/n)".red) 262 | XCSKarel::Control.reset if resp 263 | end 264 | end 265 | 266 | # Managing Xcode 267 | 268 | command :'xcode select' do |c| 269 | c.syntax = 'xcskarel xcode select' 270 | c.description = 'Interactive xcode-select' 271 | c.example 'choose xcode', 'xcskarel xcode select' 272 | c.action do |args, options| 273 | xcodes = XCSKarel::Control.installed_xcodes 274 | raise "No Xcode found" if xcodes.count == 0 275 | xcode = choose("Select Xcode version", *xcodes) 276 | XCSKarel::Control.select(xcode) 277 | end 278 | end 279 | 280 | default_command :help 281 | 282 | run! 283 | end 284 | end 285 | 286 | XCSKarelApplication.new.run 287 | 288 | 289 | 290 | 291 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | 2 | # update_fastlane 3 | fastlane_version "1.22.0" 4 | 5 | before_all do 6 | 7 | end 8 | 9 | lane :ci_test do 10 | sh "cd .. && bundle install" 11 | sh "cd .. && rake install" 12 | sh "cd .. && rspec" 13 | end 14 | 15 | lane :local_test do 16 | sh "cd .. && bundle install" 17 | sh "cd .. && rake install" 18 | sh "cd .. && rspec" 19 | end 20 | 21 | lane :release do 22 | local_test 23 | validate_repo 24 | version = validate_version 25 | opts = { version: version } 26 | release = release_github(opts) 27 | release_gem(opts) 28 | github_url = release['html_url'] 29 | opts[:github_url] = github_url 30 | opts[:release] = release 31 | slack_brag(opts) 32 | end 33 | 34 | private_lane :validate_repo do 35 | ensure_no_debug_code(text: "pry", extension: ".rb", path: "./lib/") # debugging code 36 | # ensure_no_debug_code(text: "TODO", extension: ".rb", path: "./lib/") # TODOs 37 | ensure_git_status_clean 38 | ensure_git_branch(branch: 'master') 39 | git_pull 40 | push_to_git_remote 41 | end 42 | 43 | private_lane :validate_version do 44 | require "../lib/xcskarel/version" 45 | new_version = XCSKarel::VERSION 46 | old_version = JSON.parse(Excon.get("https://rubygems.org/api/v1/gems/xcskarel.json").body)["version"] 47 | if Gem::Version.new(new_version) <= Gem::Version.new(old_version) 48 | raise "Version number #{new_version} is already live!" 49 | else 50 | puts "Deploying version #{new_version}." 51 | end 52 | new_version 53 | end 54 | 55 | private_lane :release_gem do |options| 56 | sh "cd .. && rake install" 57 | gem_path = "./pkg/xcskarel-#{options[:version]}.gem" 58 | sh "cd .. && gem push #{gem_path}" 59 | end 60 | 61 | private_lane :changelog do 62 | # regenerate changelog 63 | sh "cd .. && github_changelog_generator -t $GITHUB_TOKEN" 64 | 65 | # commit the changes 66 | sh "cd .. && git commit -am 'changelog'" 67 | push_to_git_remote 68 | ensure_git_status_clean 69 | end 70 | 71 | private_lane :release_github do |options| 72 | version = options[:version] 73 | github_url = "czechboy0/xcskarel" 74 | 75 | # make sure release doesn't yet exist 76 | github_release = get_github_release(url: github_url, version: version) 77 | raise "GitHub release #{version} already exists!" if github_release 78 | 79 | # regenerate changelog 80 | # open the changelog so that we can copy the changes into clipboard 81 | sh "cd .. && github_changelog_generator -t $GITHUB_TOKEN && subl CHANGELOG.md" 82 | 83 | # ask for details about the release 84 | title = prompt(text: 'Title: ') 85 | description = prompt(text: "Please enter a changelog: ", 86 | multi_line_end_keyword: "END") 87 | 88 | ENV["FL_GITHUB_RELEASE_API_TOKEN"] = ENV["GITHUB_TOKEN"] 89 | new_release = set_github_release( 90 | commitish: 'master', 91 | repository_name: github_url, 92 | name: [version, title].join(" - "), 93 | tag_name: version, 94 | description: description, 95 | is_draft: false, 96 | is_prerelease: false 97 | ) 98 | 99 | # regenerate the changelog with the new release now 100 | changelog 101 | 102 | new_release 103 | end 104 | 105 | private_lane :slack_brag do |options| 106 | version = options[:version] 107 | github_url = options[:github_url] 108 | release = options[:release] 109 | 110 | if ENV['SLACK_URL'] 111 | slack( 112 | channel: "release", 113 | message: "Successfully released [xcskarel #{version}](#{github_url}) :rocket:", 114 | payload: { 115 | "New" => release['body'] 116 | } 117 | ) 118 | else 119 | puts "Not Slacking because no SLACK_URL was provided." 120 | end 121 | end 122 | 123 | # thanks @krausefx for fastlane's Fastfile which was of great inspiration here. 124 | -------------------------------------------------------------------------------- /fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ================ 3 | # Installation 4 | ``` 5 | sudo gem install fastlane 6 | ``` 7 | # Available Actions 8 | ### ci_test 9 | ``` 10 | fastlane ci_test 11 | ``` 12 | 13 | ### local_test 14 | ``` 15 | fastlane local_test 16 | ``` 17 | 18 | ### release 19 | ``` 20 | fastlane release 21 | ``` 22 | 23 | 24 | ---- 25 | 26 | This README.md is auto-generated and will be re-generated every time to run [fastlane](https://fastlane.tools). 27 | More information about fastlane can be found on [https://fastlane.tools](https://fastlane.tools). 28 | The documentation of fastlane can be found on [GitHub](https://github.com/fastlane/fastlane). -------------------------------------------------------------------------------- /lib/xcskarel.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'xcskarel/server' 3 | require 'xcskarel/version' 4 | require 'xcskarel/log' 5 | require 'xcskarel/filter' 6 | require 'xcskarel/control' 7 | require 'xcskarel/config' 8 | require 'xcskarel/xcsfile' 9 | require 'xcskarel/application' 10 | require 'xcskarel/shell' 11 | require 'xcskarel/remote' 12 | -------------------------------------------------------------------------------- /lib/xcskarel/application.rb: -------------------------------------------------------------------------------- 1 | require 'xcskarel/filter' 2 | require 'json' 3 | 4 | module XCSKarel 5 | module Application 6 | def self.choose_bot(server) 7 | all_bots = server.get_bots 8 | bot_names = all_bots.map { |json| "#{json['name']} (#{json['_id']})" } 9 | puts "Which Bot should be used as a template?" 10 | choice = choose(*bot_names) 11 | bot = all_bots[bot_names.index(choice)] 12 | XCSKarel.log.info "Chose Bot \"#{bot['name']}\"" 13 | return bot 14 | end 15 | 16 | def self.save_bot(config_folder, bot, api_version) 17 | rand_name = XCSKarel::XCSFile.random_name 18 | config_name = ask("Config name (hit Enter to accept generated name \"" + "#{rand_name}".yellow + "\"): ") 19 | config_name = rand_name if config_name.length == 0 20 | 21 | # preprocess the config name first 22 | require 'uri' 23 | config_name = URI::escape(config_name.gsub(" ", "_")) 24 | 25 | real_name = "botconfig_#{config_name}.json" 26 | new_config_path = XCSKarel::XCSFile.new_config_name(config_folder, real_name) 27 | new_config = XCSKarel::Config.new(bot, api_version, new_config_path) 28 | new_config.save 29 | 30 | XCSKarel.log.info "Saved Bot \"#{new_config.name}\" configuration to #{new_config_path}. Check this into your repository.".green 31 | system "open \"#{new_config_path}\"" 32 | end 33 | 34 | def self.list_configs(config_folder) 35 | configs = XCSKarel::XCSFile.load_configs(config_folder) 36 | if configs.count == 0 37 | XCSKarel.log.info "Found no existing configs in #{config_folder}".yellow 38 | else 39 | out = "\n" + configs.map { |c| "\"#{c.name}\"".yellow + " [#{File.basename(c.path)}]".yellow + " - from Bot " + "#{c.original_bot_name}".yellow }.join("\n") 40 | XCSKarel.log.info "Found #{configs.count} configs in \"#{config_folder}\":" 41 | XCSKarel.log.info out 42 | end 43 | end 44 | 45 | def self.show_config(config_folder) 46 | configs = XCSKarel::XCSFile.load_configs(config_folder) 47 | config_names = configs.map { |c| "Config " + "#{c.name}".yellow + " from Bot " + "#{c.original_bot_name}".yellow } 48 | puts "Which config?" 49 | choice = choose(*config_names) 50 | config = configs[config_names.index(choice)] 51 | XCSKarel.log.info "Editing config \"#{config.name}\"" 52 | system "open \"#{config.path}\"" 53 | end 54 | 55 | def self.colorize(key, value) 56 | value ||= "" 57 | case key 58 | when "current_step" 59 | case value 60 | when "completed" 61 | value = value.white 62 | when "pending" 63 | value = value.blue 64 | else 65 | value = value.yellow 66 | end 67 | when "result" 68 | case value 69 | when "succeeded" 70 | value = value.green 71 | when "canceled" 72 | value = value.yellow 73 | else 74 | value = value.red 75 | end 76 | end 77 | return value 78 | end 79 | 80 | def self.print_status(server) 81 | statuses = server.fetch_status 82 | require 'terminal-table' 83 | 84 | head = statuses.first.keys 85 | table = Terminal::Table.new do |t| 86 | statuses.each do |status| 87 | r = head.map { |h| self.colorize(h, status[h]) } 88 | t.add_row r 89 | end 90 | end 91 | table.title = server.host 92 | table.headings = head 93 | # table.style = {:width => 160} 94 | puts table.to_s 95 | end 96 | 97 | def self.integrations(server, bot_id_or_name) 98 | bot = server.find_bot_by_id_or_name(bot_id_or_name) 99 | XCSKarel.log.debug "Found Bot #{bot['name']} with id #{bot['_id']}".yellow 100 | server.get_integrations(bot['_id']) 101 | end 102 | 103 | def self.integrate(server, bot_id_or_name) 104 | 105 | # find bot by id or name 106 | bot = server.find_bot_by_id_or_name(bot_id_or_name) 107 | XCSKarel.log.debug "Found Bot #{bot['name']} with id #{bot['_id']}".yellow 108 | 109 | # kick off an integration 110 | server.integrate(bot) 111 | 112 | # print the new status (TODO: highlight the bot's row) 113 | self.print_status(server) 114 | end 115 | 116 | def self.issues(server, bot_id_or_name, integration_id) 117 | integration = nil 118 | if bot_id_or_name 119 | bot = server.find_bot_by_id_or_name(bot_id_or_name) 120 | # fetch last integration 121 | integration = (server.get_integrations(bot['_id']).first || {})['_id'] 122 | raise "No Integration found for Bot \"#{bot['name']}\"".red unless integration 123 | else 124 | integration = server.get_integration(integration_id)['_id'] 125 | raise "No Integration found for id #{integration_id}".red unless integration 126 | end 127 | 128 | # fetch issues 129 | issues = server.get_issues(integration) 130 | return issues 131 | end 132 | 133 | def self.format(object, options, allowed_key_paths, allow_empty_container_leaves=true) 134 | unless options.no_filter 135 | 136 | extra_filters = [] 137 | 138 | unless allow_empty_container_leaves 139 | # optionally add an override to filter out empty containers as leaves 140 | empty_leaves = lambda do |k,v| 141 | return true unless v.is_a?(Array) || v.is_a?(Hash) 142 | return v.count > 0 143 | end 144 | extra_filters << empty_leaves 145 | end 146 | 147 | # create a super-filter composed from all the gathered filters 148 | custom_filters = lambda do |k,v| 149 | extra_filters.each do |filter| 150 | return false unless filter.call(k,v) 151 | end 152 | return true 153 | end 154 | 155 | object = XCSKarel::Filter.filter_key_paths(object, allowed_key_paths, custom_filters) 156 | end 157 | out = options.no_pretty ? JSON.generate(object) : JSON.pretty_generate(object) 158 | return out 159 | end 160 | 161 | def self.remote_logs(connection) 162 | logs_path = "/Library/Developer/XcodeServer/Logs" 163 | log_control = File.join(logs_path, "xcscontrol.log") 164 | log_build = File.join(logs_path, "xcsbuildd.log") 165 | lives = [] 166 | [log_control, log_build].each do |log| 167 | puts "\n\n------- Printing output of #{log} at #{connection.host} -------\n".green 168 | res = connection.execute("tail -n 20 #{log}") 169 | puts res.yellow 170 | live = "ssh #{connection.user}@#{connection.host} tail -f #{log}".green 171 | lives << live 172 | end 173 | live_all = lives.map { |l| "\"#{l}\"" }.join("\n") 174 | XCSKarel.log.info "To connect to the logs live run either:\n#{live_all}" 175 | end 176 | 177 | end 178 | end -------------------------------------------------------------------------------- /lib/xcskarel/config.rb: -------------------------------------------------------------------------------- 1 | module XCSKarel 2 | class Config 3 | attr_reader :json 4 | attr_reader :path 5 | attr_reader :api_version 6 | 7 | def initialize(json, api_version, path) 8 | @json = json 9 | @path = path 10 | @api_version = api_version || (config_json ? config_json['api_version'] : nil) 11 | end 12 | 13 | def name 14 | (config_json['name'] || File.basename(@path).split('.').first).gsub("botconfig_", "") 15 | end 16 | 17 | def branch 18 | blueprint = @json['configuration']['sourceControlBlueprint'] 19 | primary_repo_key = blueprint['DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey'] 20 | location = blueprint['DVTSourceControlWorkspaceBlueprintLocationsKey'][primary_repo_key] 21 | # might be nil if we're pointing to a commit, for instance. 22 | return location['DVTSourceControlBranchIdentifierKey'] 23 | end 24 | 25 | def original_bot_name 26 | @json['name'] || config_json['original_bot_name'] 27 | end 28 | 29 | def config_json 30 | @json['xcsconfig'] || {} 31 | end 32 | 33 | def key_paths_for_persistance 34 | key_paths_for_xcode_server << "xcsconfig" 35 | end 36 | 37 | def key_paths_for_xcode_server 38 | ["configuration"] 39 | end 40 | 41 | def format_version 42 | 1 43 | end 44 | 45 | def json_for_persistence 46 | filtered = XCSKarel::Filter.filter_key_paths(@json, key_paths_for_persistance) 47 | 48 | # also add xcsconfig metadata 49 | unless filtered["xcsconfig"] 50 | filtered["xcsconfig"] = { 51 | format_version: format_version, 52 | app_version: XCSKarel::VERSION, 53 | original_bot_name: @json['name'], 54 | name: name, 55 | api_version: @api_version 56 | } 57 | end 58 | return filtered 59 | end 60 | 61 | def json_for_xcode_server 62 | XCSKarel::Filter.filter_key_paths(@json, key_paths_for_xcode_server) 63 | end 64 | 65 | def self.from_file(file_path) 66 | abs_path = File.absolute_path(file_path) 67 | raise "No file #{abs_path}" unless File.exist?(abs_path) 68 | config = self.new(JSON.parse(File.read(abs_path)), nil, abs_path) 69 | unless config.validate_loaded 70 | XCSKarel.log.warn "Skipping invalid config #{abs_path}".yellow 71 | return nil 72 | end 73 | return config 74 | end 75 | 76 | def validate_loaded 77 | return false unless config_json 78 | return true 79 | end 80 | 81 | def to_file(file_path) 82 | abs_path = File.absolute_path(file_path) 83 | raise "File #{abs_path} already exists. Choose a different name.".red if File.exist?(abs_path) 84 | FileUtils.mkdir_p(File.dirname(abs_path)) 85 | File.open(abs_path, 'w') do |f| 86 | f.puts JSON.pretty_generate(json_for_persistence) + "\n" 87 | end 88 | end 89 | 90 | def save 91 | to_file(@path) 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /lib/xcskarel/control.rb: -------------------------------------------------------------------------------- 1 | 2 | module XCSKarel 3 | module Control 4 | def self.installed_xcodes 5 | unless (`mdutil -s /` =~ /disabled/).nil? 6 | # indexing is turned off 7 | return nil 8 | end 9 | 10 | status, output = XCSKarel::Shell.execute('mdfind "kMDItemCFBundleIdentifier == \'com.apple.dt.Xcode\'" 2>/dev/null') 11 | raise "Failed to fetch Xcode paths: #{output}" if status != 0 12 | output.split("\n") 13 | end 14 | 15 | def self.start(xcode) 16 | raise "No Xcode path provided" unless xcode 17 | 18 | self.select(xcode) 19 | 20 | XCSKarel.log.info "Starting Xcode Server... This may take up to a minute.".yellow 21 | 22 | XCSKarel::Shell.exec_sudo("sudo xcrun xcscontrol --initialize") 23 | XCSKarel::Shell.exec_sudo("sudo xcrun xcscontrol --preflight") 24 | 25 | XCSKarel.log.info "Xcode Server started & running on localhost now!".green 26 | end 27 | 28 | def self.stop 29 | XCSKarel::Shell.exec_sudo("sudo xcrun xcscontrol --shutdown") 30 | end 31 | 32 | def self.restart 33 | XCSKarel::Shell.exec_sudo("sudo xcrun xcscontrol --restart") 34 | end 35 | 36 | def self.reset 37 | XCSKarel::Shell.exec_sudo("sudo xcrun xcscontrol --reset") 38 | end 39 | 40 | def self.select(xcode) 41 | XCSKarel::Shell.exec_sudo("sudo xcode-select -s #{xcode}") 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/xcskarel/filter.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'set' 3 | 4 | module XCSKarel 5 | module Filter 6 | # returns a copy of the passed-in hash with only the provided keypaths kept 7 | # e.g. "name" will keep the key "name" at the top level. 8 | # supports even nested keypaths 9 | def self.filter_key_paths(object, key_paths, custom_block=nil) 10 | 11 | # array? 12 | if object.is_a?(Array) 13 | return self.filter_array(object, key_paths, custom_block) 14 | end 15 | 16 | # hash? 17 | if object.is_a?(Hash) 18 | new_hash = Hash.new 19 | unique_key_paths = Set.new(key_paths) 20 | object.each do |k,v| 21 | 22 | next if !custom_block.nil? && !custom_block.call(k, v) 23 | 24 | keys = k.split('.') # key-paths must be separated by a period 25 | matches = unique_key_paths.select do |key| 26 | kp = key.split('.') 27 | kp.first == keys.first || kp.first == "*" 28 | end 29 | if matches.count > 0 30 | 31 | child_key_paths = [] 32 | split_matches = matches.map { |match| match.split('.') } 33 | 34 | # universal wildcard 35 | if matches.index("*") != nil 36 | child_key_paths = child_key_paths << "*" 37 | else 38 | # normal key-path (including key-pathed wildcard) 39 | child_key_paths = child_key_paths + split_matches.map { |split_match| split_match.drop(1).join('.') } 40 | end 41 | 42 | # if there are no more key paths, we just take everything (whitelisted by default) 43 | new_k = keys.first 44 | new_v = filter_key_paths(v, child_key_paths, custom_block) 45 | next if !custom_block.nil? && !custom_block.call(new_k, new_v) 46 | new_hash[new_k] = new_v 47 | end 48 | end 49 | return new_hash 50 | end 51 | 52 | # object? 53 | return object 54 | end 55 | 56 | private 57 | 58 | # filters each element 59 | def self.filter_array(array, key_paths, custom_block=nil) 60 | new_array = Array.new 61 | keys = Set.new(key_paths) 62 | array.each do |i| 63 | new_array << filter_key_paths(i, key_paths, custom_block) 64 | end 65 | return new_array 66 | end 67 | end 68 | end -------------------------------------------------------------------------------- /lib/xcskarel/log.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | require 'colored' 3 | 4 | module XCSKarel 5 | 6 | @@NO_LOG = false 7 | 8 | def self.set_no_log(no_log) 9 | @@NO_LOG = no_log 10 | end 11 | 12 | def self.log 13 | 14 | @@log ||= Logger.new($stdout) 15 | @@log.level = @@NO_LOG ? Logger::Severity::INFO : Logger::Severity::DEBUG 16 | @@log.formatter = proc do |severity, datetime, progname, msg| 17 | 18 | string = "#{severity} [#{datetime.strftime('%Y-%m-%d %H:%M:%S.%2N')}]: " 19 | second = "#{msg}\n" 20 | 21 | if severity == "DEBUG" 22 | string = string.magenta 23 | elsif severity == "INFO" 24 | string = string.white 25 | elsif severity == "WARN" 26 | string = string.yellow 27 | elsif severity == "ERROR" 28 | string = string.red 29 | elsif severity == "FATAL" 30 | string = string.red.bold 31 | end 32 | 33 | [string, second].join("") 34 | end 35 | @@log 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/xcskarel/remote.rb: -------------------------------------------------------------------------------- 1 | module XCSKarel 2 | module Remote 3 | 4 | require 'net/ssh' 5 | 6 | # preferably set up SSH keys to be able to SSH into the host 7 | # see http://serverfault.com/a/241593 8 | class Connection 9 | 10 | attr_reader :host 11 | attr_reader :user 12 | attr_reader :pass 13 | 14 | @ssh 15 | 16 | def initialize(host, user, pass) 17 | @host = host 18 | @user = user 19 | @pass = pass 20 | 21 | raise "Invalid host: \"#{host}\"".red if !host || host.empty? 22 | raise "Invalid user: \"#{user}\"".red if !user || user.empty? 23 | 24 | connect 25 | end 26 | 27 | def connect 28 | raise "Already connected".red if @ssh 29 | XCSKarel.log.info "Connecting to \"#{@host}\" as user \"#{@user}\" over SSH...".yellow 30 | @ssh = Net::SSH.start(host, user, :password => pass) 31 | end 32 | 33 | def disconnect 34 | @ssh.shutdown! 35 | @ssh = nil 36 | end 37 | 38 | def execute(script) 39 | raise "Not connected yet".red unless @ssh 40 | XCSKarel.log.debug "SSH: executing \"#{script}\"".green 41 | result = @ssh.exec!(script) 42 | result.strip! if result 43 | XCSKarel.log.debug "SSH: result\n \"#{result}\"".yellow 44 | return result 45 | end 46 | end 47 | end 48 | end -------------------------------------------------------------------------------- /lib/xcskarel/server.rb: -------------------------------------------------------------------------------- 1 | require 'excon' 2 | require 'xcskarel/log' 3 | require 'json' 4 | require 'base64' 5 | 6 | module XCSKarel 7 | 8 | class Server 9 | 10 | attr_reader :host 11 | attr_reader :user 12 | attr_reader :pass 13 | attr_reader :port 14 | attr_reader :api_version 15 | 16 | def initialize(host, user=nil, pass=nil, allow_self_signed=true) 17 | @port = 20343 18 | @host = validate_host(host) 19 | @user = user 20 | @pass = pass 21 | validate_connection(allow_self_signed) 22 | end 23 | 24 | def get_bots 25 | response = get_endpoint('/bots') 26 | raise "You are unauthorized to access data on #{@host}, please check that you're passing in a correct username and password.".red if response.status == 401 27 | raise "Failed to fetch Bots from Xcode Server at #{@host}, response: #{response.status}: #{response.body}.".red if response.status != 200 28 | bots = JSON.parse(response.body)['results'] 29 | 30 | # sort them alphabetically by name 31 | bots.sort_by { |bot| bot['name'] } 32 | end 33 | 34 | def get_integrations(bot_id, limit=nil) 35 | raise "No Bot id provided".red if !bot_id || bot_id.empty? 36 | limit_s = limit ? "?last=#{limit}" : "" 37 | response = get_endpoint("/bots/#{bot_id}/integrations#{limit_s}") 38 | raise "Failed to fetch Integrations for Bot #{bot_id} from Xcode Server at #{@host}, response: #{response.status}: #{response.body}".red if response.status != 200 39 | JSON.parse(response.body)['results'] 40 | end 41 | 42 | def get_integration(integration_id) 43 | raise "No Integration id provided".red if !integration_id || integration_id.empty? 44 | response = get_endpoint("/integrations/#{integration_id}") 45 | raise "Failed to fetch an Integrations with id #{integration_id} from Xcode Server at #{@host}, response: #{response.status}: #{response.body}".red if response.status != 200 46 | JSON.parse(response.body) 47 | end 48 | 49 | def get_issues(integration_id) 50 | raise "No Integration id provided".red if !integration_id || integration_id.empty? 51 | response = get_endpoint("/integrations/#{integration_id}/issues") 52 | raise "Failed to fetch issues for an Integrations with id #{integration_id} from Xcode Server at #{@host}, response: #{response.status}: #{response.body}".red if response.status != 200 53 | body = JSON.parse(response.body) 54 | return body['results'] || body # support both xcode 6 and 7 55 | end 56 | 57 | def get_health 58 | response = get_endpoint("/health") 59 | raise "Failed to get Health of #{@host}" if response.status != 200 60 | JSON.parse(response.body) 61 | end 62 | 63 | def fetch_status 64 | # all bots and their integration's statuses 65 | bot_statuses = [] 66 | bots = self.get_bots 67 | bots.map do |bot| 68 | status = {} 69 | status['name'] = bot['name'] 70 | status['id'] = bot['_id'] 71 | status['branch'] = XCSKarel::Config.new(bot, nil, nil).branch 72 | last_integration = self.get_integrations(bot['_id'], 1).first # sorted from newest to oldest 73 | if last_integration 74 | status['current_step'] = last_integration['currentStep'] 75 | status['result'] = last_integration['result'] 76 | status['count'] = last_integration['number'] 77 | else 78 | status['count'] = 0 79 | end 80 | bot_statuses << status 81 | end 82 | return bot_statuses 83 | end 84 | 85 | def integrate(bot) 86 | response = post_endpoint("/bots/#{bot['_id']}/integrations", nil) 87 | integration = response.body 88 | if response.status == 201 89 | require 'json' 90 | integration = JSON.parse(integration) 91 | XCSKarel.log.info "Successfully started integration #{integration['number']} on Bot \"#{bot['name']}\"".green 92 | else 93 | raise "Failed to integrate Bot #{bot_id}".red 94 | end 95 | return integration 96 | end 97 | 98 | def find_bot_by_id_or_name(id_or_name, raise_if_none_found=true) 99 | bots = self.get_bots 100 | found_bots = bots.select { |bot| [bot['name'], bot['_id']].index(id_or_name) != nil } 101 | raise "No Bot found for \"#{id_or_name}\"".red if raise_if_none_found && found_bots.count == 0 102 | XCSKarel.log.warn "More than one Bot found for \"#{id_or_name}\", taking the first one (you shouldn't have more Bots with the same name!)".red if found_bots.count > 1 103 | return found_bots.first 104 | end 105 | 106 | def headers 107 | headers = { 108 | 'user-agent' => 'xcskarel', # XCS wants user agent. for some API calls. not for others. sigh. 109 | 'X-XCSAPIVersion' => 6 # XCS API version with this API, currently 6 with Xcode 7 Beta 6. 110 | } 111 | 112 | if @user && @pass 113 | userpass = "#{@user}:#{@pass}" 114 | headers['Authorization'] = "Basic #{Base64.strict_encode64(userpass)}" 115 | end 116 | 117 | return headers 118 | end 119 | 120 | def get_endpoint(endpoint) 121 | call_endpoint("get", endpoint, nil) 122 | end 123 | 124 | def post_endpoint(endpoint, body) 125 | call_endpoint("post", endpoint, body) 126 | end 127 | 128 | private 129 | 130 | def call_endpoint(method, endpoint, body) 131 | method.downcase! 132 | url = url_for_endpoint(endpoint) 133 | case method 134 | when "get" 135 | response = Excon.get(url, headers: headers) 136 | when "post" 137 | response = Excon.post(url, headers: headers, body: body) 138 | else 139 | raise "Unrecognized method #{method}" 140 | end 141 | msg = "#{method.upcase} endpoint #{endpoint} => #{url} => Response #{response.data[:status_line].gsub("\n", "")}" 142 | 143 | case response.status 144 | when 200..300 145 | XCSKarel.log.debug msg.green 146 | else 147 | XCSKarel.log.warn msg.red 148 | end 149 | return response 150 | end 151 | 152 | def url_for_endpoint(endpoint) 153 | "#{@host}:#{@port}/api#{endpoint}" 154 | end 155 | 156 | def validate_host(host) 157 | comps = host.split(":") 158 | raise "Scheme must be unspecified or https, nothing else" if comps.count > 1 && comps[0] != 'https' 159 | host_with_https = comps.count > 1 ? host : "https://#{host}" 160 | return host_with_https 161 | end 162 | 163 | def validate_connection(allow_self_signed) 164 | 165 | # check for allowing self-signed certs 166 | Excon.defaults[:ssl_verify_peer] = !allow_self_signed 167 | 168 | # TODO: logout/login to validate user/pass 169 | 170 | # try to connect to the host 171 | begin 172 | response = get_endpoint("/ping") 173 | rescue Exception => e 174 | raise "Failed to validate - #{e}.\nPlease make sure your Xcode Server is up and running at #{@host}. Run `xcskarel server start` to start a new local Xcode Server instance.".red 175 | else 176 | raise "Failed to validate - Endpoint responded with #{response.data[:status_line]}".red if response.status != 204 177 | @api_version = response.headers['X-XCSAPIVersion'].to_s 178 | XCSKarel.log.debug "Validation of host #{@host} (API version #{@api_version}) succeeded.".green 179 | end 180 | end 181 | end 182 | end -------------------------------------------------------------------------------- /lib/xcskarel/shell.rb: -------------------------------------------------------------------------------- 1 | module XCSKarel 2 | module Shell 3 | def self.execute(script) 4 | XCSKarel.log.debug "Executing script: \"#{script}\"" 5 | exit_status = nil 6 | result = [] 7 | IO.popen(script, err: [:child, :out]) do |io| 8 | io.each do |line| 9 | result << line.strip 10 | end 11 | io.close 12 | exit_status = $?.exitstatus 13 | end 14 | [exit_status, result.join("\n")] 15 | end 16 | 17 | def self.exec_sudo(script) 18 | XCSKarel.log.warn "Running \'#{script}\', so we need root privileges." 19 | status, output = self.execute(script) 20 | raise "Failed to \'#{script}\':\n#{output}".red if status != 0 21 | XCSKarel.log.debug "Script \'#{script}\' output:\n#{output}" 22 | return status, output 23 | end 24 | end 25 | end -------------------------------------------------------------------------------- /lib/xcskarel/version.rb: -------------------------------------------------------------------------------- 1 | module XCSKarel 2 | VERSION = '0.16.1' 3 | end 4 | -------------------------------------------------------------------------------- /lib/xcskarel/xcsfile.rb: -------------------------------------------------------------------------------- 1 | module XCSKarel 2 | module XCSFile 3 | def self.folder_name 4 | "xcsconfig" 5 | end 6 | 7 | def self.file_name 8 | "xcsfile.json" 9 | end 10 | 11 | def self.find_config_folder_in_current_folder 12 | self.find_config_folder_in_folder(Dir.pwd) 13 | end 14 | 15 | def self.find_config_folder_in_folder(folder) 16 | # look for the xcsconfig folder with an xcsfile inside 17 | abs_folder = File.absolute_path(folder) 18 | found_folder = Dir[File.join(abs_folder, "/", "*")].select do |f| 19 | File.basename(f) == self.folder_name 20 | end.first 21 | return found_folder 22 | end 23 | 24 | def self.create_config_folder_in_current_folder 25 | self.create_config_folder_in_folder(Dir.pwd) 26 | end 27 | 28 | def self.create_config_folder_in_folder(folder) 29 | abs_folder = File.absolute_path(folder) 30 | config_folder = File.join(abs_folder, self.folder_name) 31 | FileUtils.mkdir_p(config_folder) 32 | return config_folder 33 | end 34 | 35 | def self.get_config_folder 36 | config_folder = XCSKarel::XCSFile.find_config_folder_in_current_folder 37 | unless config_folder 38 | should_create = agree("There is no xcsconfig folder found, should I create one for you? (y/n)".red) 39 | if should_create 40 | config_folder = XCSKarel::XCSFile.create_config_folder_in_current_folder unless config_folder 41 | XCSKarel.log.debug "Folder #{config_folder} created".yellow 42 | else 43 | return nil 44 | end 45 | end 46 | # we have a config folder 47 | XCSKarel.log.debug "Config folder found: #{config_folder}".green 48 | return config_folder 49 | end 50 | 51 | def self.load_configs(folder) 52 | require 'json' 53 | Dir[File.join(folder, "/", "botconfig_*.json")].map do |f| 54 | XCSKarel::Config.from_file(f) 55 | end.select do |c| 56 | c != nil 57 | end 58 | end 59 | 60 | def self.random_name 61 | require 'securerandom' 62 | "#{SecureRandom.hex(6)}" 63 | end 64 | 65 | def self.new_config_name(folder, name) 66 | name = name.split('.').first + ".json" 67 | File.join(folder, name) 68 | end 69 | 70 | end 71 | end -------------------------------------------------------------------------------- /scripts/bootstrap_xcsbuildd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # run this once on a newly reset xcode server 4 | 5 | # switch to sudo mode 6 | sudo -s 7 | 8 | # set the new shell of _xcsbuildd 9 | dscl localhost -change /Local/Default/Users/_xcsbuildd UserShell /bin/false /bin/bash 10 | 11 | # login as _xcsbuildd 12 | sudo su - _xcsbuildd 13 | 14 | # install proper ruby distribution 15 | \curl -sSL https://get.rvm.io | bash -s stable 16 | . ~/.profile 17 | rvm autolibs read-only 18 | rvm install ruby 19 | 20 | # install fastlane and cocoapods 21 | gem install fastlane 22 | gem install cocoapods 23 | -------------------------------------------------------------------------------- /spec/application_spec.rb: -------------------------------------------------------------------------------- 1 | require 'xcskarel/application' 2 | 3 | class Opts 4 | attr_accessor :no_pretty 5 | attr_accessor :no_filter 6 | end 7 | 8 | describe XCSKarel do 9 | describe XCSKarel::Application do 10 | describe "format" do 11 | 12 | def default_options 13 | opts = Opts.new 14 | opts.no_filter = false 15 | opts.no_pretty = false 16 | return opts 17 | end 18 | 19 | it "filters correctly empty leaf arrays" do 20 | object = [{ 21 | "errors" => { 22 | "unresolvedIssues" => [], 23 | "resolvedIssues" => [] 24 | }, 25 | "testFailures" => { 26 | "unresolvedIssues" => ["one"], 27 | "resolvedIssues" => [] 28 | } 29 | }] 30 | exp = JSON.pretty_generate([{ 31 | "testFailures" => { 32 | "unresolvedIssues" => ["one"] 33 | } 34 | }]) 35 | expect(XCSKarel::Application.format(object, default_options, ["errors.*", "testFailures.*"], false)).to eq(exp) 36 | end 37 | end 38 | end 39 | end -------------------------------------------------------------------------------- /spec/default_spec.rb: -------------------------------------------------------------------------------- 1 | require 'xcskarel' 2 | 3 | describe XCSKarel do 4 | describe XCSKarel::Control do 5 | describe "blah" do 6 | it "is happy" do 7 | expect("a").to eq("a") 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/filter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'xcskarel/filter' 2 | 3 | describe XCSKarel do 4 | describe XCSKarel::Filter do 5 | 6 | def test(inObj, key_paths, custom_block=nil) 7 | XCSKarel::Filter.filter_key_paths(inObj, key_paths, custom_block) 8 | end 9 | 10 | it "handles empty hash" do 11 | expect(test({}, [])).to eq({}) 12 | end 13 | 14 | it "handles empty array" do 15 | expect(test([], [])).to eq([]) 16 | end 17 | 18 | it "handles flat array of strings" do 19 | expect(test(["apples", "oranges"], [])).to eq(["apples", "oranges"]) 20 | end 21 | 22 | it "filters out everything from a simple hash" do 23 | expect(test({"apples" => "green", "oranges" => "orange"}, [])).to eq({}) 24 | end 25 | 26 | it "lets through only selected keys is a simple hash" do 27 | expect(test({"apples" => "green", "oranges" => "orange"}, ["oranges"])).to eq({"oranges" => "orange"}) 28 | end 29 | 30 | it "handles basic key path with a hash" do 31 | obj = { 32 | "apples" => "green", 33 | "oranges" => { 34 | "new" => 2, 35 | "old" => -2 36 | } 37 | } 38 | exp = { 39 | "oranges" => { 40 | "old" => -2 41 | } 42 | } 43 | expect(test(obj, ["oranges.old"])).to eq(exp) 44 | end 45 | 46 | it "keeps full values when hashes and key is already whitelisted" do 47 | obj = { 48 | "apples" => "green", 49 | "blackberries" => 12, 50 | "oranges" => { 51 | "new" => 2, 52 | "old" => -2 53 | } 54 | } 55 | exp = { 56 | "apples" => "green", 57 | "oranges" => { 58 | "new" => 2, 59 | "old" => -2 60 | } 61 | } 62 | expect(test(obj, ["oranges.*", "apples"])).to eq(exp) 63 | end 64 | 65 | it "handles basic key path with an array without popping the key path" do 66 | obj = [ 67 | "apples", 68 | { 69 | "new" => 2, 70 | "old" => -2 71 | } 72 | ] 73 | exp = [ 74 | "apples", 75 | { 76 | "new" => 2 77 | } 78 | ] 79 | expect(test(obj, ["new"])).to eq(exp) 80 | end 81 | 82 | it "custom block can override and filter out based on keys" do 83 | obj = [ 84 | "apples", 85 | { 86 | "new" => ["one", "two"], 87 | "old" => [] 88 | } 89 | ] 90 | exp = [ 91 | "apples", 92 | { 93 | "new" => ["one", "two"] 94 | } 95 | ] 96 | custom_block = lambda do |k,v| 97 | return true unless v.is_a?(Array) 98 | return v.count > 0 99 | end 100 | expect(test(obj, ["new", "old"], custom_block)).to eq(exp) 101 | end 102 | 103 | it "handles the wildcard symbol in the middle of a keypath" do 104 | obj = { 105 | "errors" => { 106 | "status" => 0, 107 | "data" => "1234abcd" 108 | }, 109 | "warnings" => { 110 | "status" => 1, 111 | "data" => "abcd1234" 112 | } 113 | } 114 | exp = { 115 | "errors" => { 116 | "status" => 0 117 | }, 118 | "warnings" => { 119 | "status" => 1 120 | } 121 | } 122 | expect(test(obj, ["*.status"])).to eq(exp) 123 | end 124 | end 125 | end -------------------------------------------------------------------------------- /xcsconfig/botconfig_test_bot.json: -------------------------------------------------------------------------------- 1 | { 2 | "configuration": { 3 | "builtFromClean": 2, 4 | "periodicScheduleInterval": 2, 5 | "codeCoveragePreference": 2, 6 | "performsTestAction": true, 7 | "triggers": [ 8 | { 9 | "phase": 1, 10 | "scriptBody": "cd XCSTutorialProject1\nfastlane prebuild", 11 | "type": 1, 12 | "name": "Run Script" 13 | }, 14 | { 15 | "phase": 2, 16 | "scriptBody": "cd XCSTutorialProject1\nfastlane postbuild", 17 | "type": 1, 18 | "name": "Run Script", 19 | "conditions": { 20 | "status": 2, 21 | "onWarnings": true, 22 | "onBuildErrors": true, 23 | "onInternalErrors": true, 24 | "onAnalyzerWarnings": true, 25 | "onFailingTests": true, 26 | "onSuccess": true 27 | } 28 | }, 29 | { 30 | "phase": 2, 31 | "scriptBody": "", 32 | "type": 2, 33 | "name": "Send Email Notification", 34 | "conditions": { 35 | "status": 2, 36 | "onWarnings": true, 37 | "onBuildErrors": true, 38 | "onInternalErrors": true, 39 | "onAnalyzerWarnings": true, 40 | "onFailingTests": true, 41 | "onSuccess": false 42 | }, 43 | "emailConfiguration": { 44 | "includeCommitMessages": true, 45 | "additionalRecipients": [ 46 | "extra-email@me.com" 47 | ], 48 | "emailCommitters": true, 49 | "scmOptions": { 50 | "43ABCED2B571DB1C2336479F8D53EBF39F6D1B5D": true 51 | }, 52 | "includeIssueDetails": true 53 | } 54 | } 55 | ], 56 | "performsAnalyzeAction": true, 57 | "schemeName": "XCSTutorialProject1", 58 | "exportsProductFromArchive": true, 59 | "testingDeviceIDs": [ 60 | 61 | ], 62 | "deviceSpecification": { 63 | "filters": [ 64 | { 65 | "platform": { 66 | "_id": "448946985304230369392c2e6b00226c", 67 | "displayName": "iOS", 68 | "_rev": "3-c50bae22b98a80c3919c7b5f1e7400a8", 69 | "simulatorIdentifier": "com.apple.platform.iphonesimulator", 70 | "identifier": "com.apple.platform.iphoneos", 71 | "buildNumber": "13A4325c", 72 | "version": "9.0" 73 | }, 74 | "filterType": 3, 75 | "architectureType": 0 76 | } 77 | ], 78 | "deviceIdentifiers": [ 79 | "448946985304230369392c2e6b00731e", 80 | "448946985304230369392c2e6b008ae1" 81 | ] 82 | }, 83 | "weeklyScheduleDay": 0, 84 | "minutesAfterHourToIntegrate": 30, 85 | "scheduleType": 1, 86 | "hourOfIntegration": 19, 87 | "performsArchiveAction": true, 88 | "testingDestinationType": 0, 89 | "sourceControlBlueprint": { 90 | "DVTSourceControlWorkspaceBlueprintLocationsKey": { 91 | "43ABCED2B571DB1C2336479F8D53EBF39F6D1B5D": { 92 | "DVTSourceControlBranchIdentifierKey": "step4-sigh", 93 | "DVTSourceControlBranchOptionsKey": 4, 94 | "DVTSourceControlWorkspaceBlueprintLocationTypeKey": "DVTSourceControlBranch" 95 | } 96 | }, 97 | "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey": "43ABCED2B571DB1C2336479F8D53EBF39F6D1B5D", 98 | "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey": { 99 | }, 100 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryAuthenticationStrategiesKey": { 101 | "43ABCED2B571DB1C2336479F8D53EBF39F6D1B5D": { 102 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryAuthenticationTypeKey": "DVTSourceControlAuthenticationStrategy" 103 | } 104 | }, 105 | "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey": { 106 | "43ABCED2B571DB1C2336479F8D53EBF39F6D1B5D": 0 107 | }, 108 | "DVTSourceControlWorkspaceBlueprintIdentifierKey": "7DFE7BE7-36F2-495F-93A8-833D9FC78BFB", 109 | "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey": { 110 | "43ABCED2B571DB1C2336479F8D53EBF39F6D1B5D": "XCSTutorialProject1/" 111 | }, 112 | "DVTSourceControlWorkspaceBlueprintNameKey": "XCSTutorialProject1", 113 | "DVTSourceControlWorkspaceBlueprintVersion": 204, 114 | "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey": "XCSTutorialProject1.xcworkspace", 115 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey": [ 116 | { 117 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey": "github.com:czechboy0/XCSTutorialProject1.git", 118 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey": "com.apple.dt.Xcode.sourcecontrol.Git", 119 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey": "43ABCED2B571DB1C2336479F8D53EBF39F6D1B5D" 120 | } 121 | ] 122 | } 123 | }, 124 | "xcsconfig": { 125 | "format_version": 1, 126 | "app_version": "0.7.1", 127 | "original_bot_name": "I am a Bot and I know it", 128 | "name": "test_bot", 129 | "api_version": "6" 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /xcskarel.gemspec: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.push File.expand_path('../lib', __FILE__) 2 | require 'xcskarel/version' 3 | 4 | Gem::Specification.new do |s| 5 | 6 | s.name = "xcskarel" 7 | s.version = XCSKarel::VERSION 8 | s.date = '2015-08-26' 9 | s.summary = "Manage your Xcode Server & Bots from the command line" 10 | s.description = "Tool for managing your Xcode Server & Bot configurations from the command line" 11 | s.author = "Honza Dvorsky" 12 | s.email = 'http://honzadvorsky.com' 13 | s.homepage = 'http://github.com/czechboy0/xcskarel' 14 | s.license = 'MIT' 15 | 16 | s.required_ruby_version = '>= 2.0.0' 17 | 18 | s.files = Dir["lib/**/*"] 19 | s.require_paths = ["lib"] 20 | s.test_files = Dir["spec/*_spec.rb"] 21 | 22 | s.executables = Dir["bin/*"].map { |f| File.basename(f) } 23 | 24 | s.add_dependency 'excon', '0.45.4' # http client 25 | s.add_dependency 'json', '1.8.3' # json parsing 26 | s.add_dependency 'logger', '1.2.8' # logging 27 | s.add_dependency 'colored', '1.2' # colored logging 28 | s.add_dependency 'commander', '4.3.5' # CLI parser 29 | s.add_dependency 'terminal-table', '1.4.5' # nice terminal tables 30 | s.add_dependency 'net-ssh', '2.9.2' # ssh logging into server 31 | 32 | s.add_development_dependency 'pry', '0.10.1' # debugging 33 | s.add_development_dependency 'pry-byebug', '3.2.0' # better debugging 34 | s.add_development_dependency 'bundler' 35 | s.add_development_dependency 'rake' 36 | s.add_development_dependency 'rspec', '~> 3.1.0' 37 | s.add_development_dependency 'fastlane' 38 | s.add_development_dependency 'github_changelog_generator' 39 | 40 | end 41 | 42 | # reading list 43 | # http://guides.rubygems.org/make-your-own-gem/ 44 | --------------------------------------------------------------------------------