├── LICENSE ├── README.md └── git-buildkite /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Samuel Cochran 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Git Buildkite 2 | 3 | Start [Buildkite](https://buildkite.com) builds using [Git](https://git-scm.com) with a clickable link: 4 | 5 | git buildkite building buildkite on buildkite 6 | 7 | Walks you through first time configuration. 8 | 9 | ## Installation 10 | 11 | Install on macOS using [Homebrew](https://brew.sh/): 12 | 13 | ``` 14 | brew install sj26/git-buildkite/git-buildkite 15 | ``` 16 | 17 | Or just download the git-buildkite script in this repository somewhere into your `$PATH`. It works on Linux, too. 18 | 19 | Add a Buildkite [API Access Token](https://buildkite.com/user/api-access-tokens) with: 20 | 21 | ``` 22 | $ git config --global buildkite.apikey my-api-key 23 | ``` 24 | 25 | Or store it more securely in your [macOS Keychain](https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man1/security.1.html): 26 | 27 | ``` 28 | $ security add-generic-password -s api.buildkite.com -a "$USER" -w 29 | password data for new item: 30 | retype password for new item: 31 | ``` 32 | 33 | Or in GNOME Keychain, or other secret stores which [Keyring](https://pypi.python.org/pypi/keyring) supports: 34 | 35 | ``` 36 | $ pip install keyring 37 | $ keyring set api.buildkite.com "$USER" 38 | Password: ***** 39 | ``` 40 | 41 | ## Usage 42 | 43 | Start a build on your current branch and commit: 44 | 45 | ``` 46 | $ git buildkite 47 | ``` 48 | 49 | Start a build on another branch and its current commit: 50 | 51 | ``` 52 | $ git buildkite my-branch-name 53 | ``` 54 | 55 | Start a build on another branch at a specific commit: 56 | 57 | ``` 58 | $ git builkdite my-branch-name 87cba321 59 | ``` 60 | 61 | `HEAD` can be used as an alias for the current branch name when you only want to specify a commit: 62 | 63 | ``` 64 | $ git buildkite HEAD 87cba321 65 | ``` 66 | 67 | Any [Git revision](https://git-scm.com/docs/gitrevisions) works as a commit: 68 | 69 | ``` 70 | $ git buildkite HEAD "@{1 week ago}" 71 | ``` 72 | 73 | You can automatically open the build in your browser, too: 74 | 75 | ``` 76 | $ git buildkite --browse 77 | ``` 78 | 79 | ## To Do 80 | 81 | * Better result parsing and error handling 82 | 83 | ## License 84 | 85 | MIT, see LICENSE. 86 | -------------------------------------------------------------------------------- /git-buildkite: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | VERSION="v0.6.0" 4 | 5 | BUILDKITE_ORGANIZATION="${BUILDKITE_ORGANIZATION:-$(git config buildkite.organization)}" 6 | 7 | if [[ -z "$BUILDKITE_ORGANIZATION" && -n "$BUILDKITE_ACCOUNT" ]]; then 8 | BUILDKITE_ORGANIZATION="$BUILDKITE_ACCOUNT" 9 | 10 | echo "Warning: \$BUILDKITE_ACCOUNT is deprecated, please use \$BUILDKITE_ORGANIZATION" 11 | echo 12 | fi 13 | 14 | if [[ -z "$BUILDKITE_ORGANIZATION" && -n "$(git config buildkite.account)" ]]; then 15 | BUILDKITE_ORGANIZATION="$(git config buildkite.account)" 16 | 17 | echo "Warning: Using buildkite.account in your Git config is deprecated, please use buildkite.organization:" 18 | echo 19 | echo " git config --local buildkite.organization $(printf "%q" "$BUILDKITE_ORGANIZATION")" 20 | echo " git config --unset buildkite.account" 21 | echo 22 | fi 23 | 24 | BUILDKITE_API_KEY="${BUILDKITE_API_KEY:-$(git config buildkite.apikey)}" 25 | 26 | # Store your API key in your macOS keychain: 27 | if [[ -z "$BUILDKITE_API_KEY" ]] && which security >/dev/null 2>&1; then 28 | # Try a per-org token first: 29 | # security add-generic-password -s api.buildkite.com/v2/organizations/YOUR-ORGANIZATION -a YOUR-USERNAME -w 30 | BUILDKITE_API_KEY="${BUILDKITE_API_KEY:-$(security find-generic-password -s "api.buildkite.com/v2/organizations/${BUILDKITE_ORGANIZATION}" -w 2>/dev/null)}" 31 | 32 | # Fall back to a global token: 33 | # security add-generic-password -s api.buildkite.com -a YOUR-USERNAME -w 34 | if [[ -z "$BUILDKITE_API_KEY" ]]; then 35 | BUILDKITE_API_KEY="${BUILDKITE_API_KEY:-$(security find-generic-password -s api.buildkite.com -w 2>/dev/null)}" 36 | fi 37 | fi 38 | 39 | # Store your API key in your keyring: 40 | # pip install keyring 41 | if [[ -z "$BUILDKITE_API_KEY" ]] && which keyring > /dev/null 2>&1; then 42 | # Try a per-org token first: 43 | # keyring set "api.buildkite.com/v2/organizations/${BUILDKITE_ORGANIZATION}" "$USER" 44 | if [[ -z "$BUILDKITE_API_KEY" ]] && which keyring > /dev/null 2>&1; then 45 | BUILDKITE_API_KEY="$(keyring get "api.buildkite.com/v2/organizations/${BUILDKITE_ORGANIZATION}" "$USER")" 46 | fi 47 | 48 | # Fall back to a global token: 49 | # keyring set api.buildkite.com "$USER" 50 | BUILDKITE_API_KEY="$(keyring get api.buildkite.com "$USER")" 51 | fi 52 | 53 | BUILDKITE_PIPELINE="${BUILDKITE_PIPELINE:-$(git config buildkite.pipeline)}" 54 | 55 | if [[ -z "$BUILDKITE_PIPELINE" && -n "$BUILDKITE_PROJECT" ]]; then 56 | BUILDKITE_PIPELINE="$BUILDKITE_PROJECT" 57 | 58 | echo "Warning: \$BUILDKITE_PROJECT is deprecated, please use \$BUILDKITE_PIPELINE" 59 | echo 60 | fi 61 | 62 | if [[ -z "$BUILDKITE_PIPELINE" && -n "$(git config buildkite.project)" ]]; then 63 | BUILDKITE_PIPELINE="$(git config buildkite.project)" 64 | 65 | echo "Warning: Using buildkite.project in your Git config is deprecated, please use buildkite.pipeline:" 66 | echo 67 | echo " git config --local buildkite.pipeline $(printf "%q" "$BUILDKITE_PIPELINE")" 68 | echo " git config --unset buildkite.project" 69 | echo 70 | fi 71 | 72 | function current_revision() { 73 | git rev-parse HEAD 74 | } 75 | 76 | function current_branch() { 77 | if [[ -f .git/rebase-merge/head-name ]]; then 78 | # We're rebasing, use the rebased head's name (without refs/heads/ prefix) 79 | echo "$(head="$(cat .git/rebase-merge/head-name)"; echo "${head#refs/heads/}")" 80 | else 81 | # Let git read the HEAD for a symbolic ref (the current branch) 82 | git symbolic-ref --short HEAD 2> /dev/null 83 | fi 84 | } 85 | 86 | function current_branch_exists() { 87 | [ "$(current_branch)" ] 88 | } 89 | 90 | function remote_upstream_branch() { 91 | git rev-parse --symbolic-full-name --abbrev-ref @{u} 2> /dev/null 92 | } 93 | 94 | function upstream_branch() { 95 | remote_and_branch=$(remote_upstream_branch) 96 | branch="${remote_and_branch#*/}" 97 | echo "$branch" 98 | } 99 | 100 | function upstream_branch_exists() { 101 | [ "$(upstream_branch)" ] 102 | } 103 | 104 | function remote_current_branch() { 105 | [ "$(current_branch)" ] && echo "origin/$(current_branch)" 106 | } 107 | 108 | function remote_current_branch_revision() { 109 | [ "$(remote_current_branch)" ] && 110 | git rev-parse --short "$(remote_current_branch)" 111 | } 112 | 113 | function remote_current_branch_current() { 114 | [ "$(remote_current_branch)" ] && 115 | git rev-parse --verify --quiet "$(remote_current_branch)" > /dev/null && 116 | [ "$(git rev-parse "$(current_branch)")" == "$(git rev-parse "$(remote_current_branch)")" ] 117 | } 118 | 119 | function remote_current_branch_behind() { 120 | [ "$(remote_current_branch)" ] && 121 | git rev-parse --verify --quiet "$(remote_current_branch)" > /dev/null && 122 | git merge-base --is-ancestor "$(remote_current_branch)" "$(current_branch)" 123 | } 124 | 125 | function confirm() { 126 | read -p "$1 [Y/n] " -n 1 -r 127 | echo 128 | [[ "$REPLY" =~ ^[Yy]$ || "$REPLY" == "" ]] 129 | } 130 | 131 | function current_head_on_remote() { 132 | [ -z "$(git rev-list $1..$(current_revision))" ] 133 | } 134 | 135 | function current_head_dirty() { 136 | [ "$(git status --short)" ] 137 | } 138 | 139 | function build() { 140 | # XXX: There seems no nice way to make curl give us a non-zero 141 | # status code and the response body on error, so we have to check 142 | # the content for errors later. 143 | curl -# "https://api.buildkite.com/v2/organizations/${BUILDKITE_ORGANIZATION}/pipelines/${BUILDKITE_PIPELINE}/builds" \ 144 | -A "git-buildkite/${VERSION}" \ 145 | -H "Authorization: Bearer ${BUILDKITE_API_KEY}" \ 146 | -X POST \ 147 | -F "branch=$1" \ 148 | -F "commit=$2" \ 149 | -F "message=$(git log --format=%B -n 1 "$2")" \ 150 | -F "ignore_pipeline_branch_filters=true" \ 151 | -F "meta_data[personal]=true" \ 152 | -F "meta_data[user_name]=$(git config user.name)" \ 153 | -F "meta_data[user_email]=$(git config user.email)" 154 | } 155 | 156 | function result_error() { 157 | [[ -z "$(result_build_url "$1")" ]] 158 | } 159 | 160 | function result_api_key_error() { 161 | grep -q api_key <<< "$1" 162 | } 163 | 164 | function result_build_url() { 165 | ruby -rjson -e 'print JSON.parse(STDIN.read)["web_url"].to_s rescue ""' <<< "$1" 166 | } 167 | 168 | if [ -z "$BUILDKITE_API_KEY" ]; then 169 | echo "You need to create a new Buildkite api key with the 'Modify Builds (write_builds)' scope. You can do that here:" 170 | echo 171 | echo " https://buildkite.com/user/api-access-tokens" 172 | echo 173 | echo "Then stick it in git:" 174 | echo 175 | echo " git config --global buildkite.apikey " 176 | echo 177 | 178 | exit 1 179 | fi 180 | 181 | if [ -z "$BUILDKITE_ORGANIZATION" -o -z "$BUILDKITE_PIPELINE" ]; then 182 | echo "You need to configure your Buildkite organization and pipeline name. They're in the URL when you visit your pipeline page. For example:" 183 | echo 184 | echo " https://buildkite.com/mycompany/mypipeline" 185 | echo 186 | echo "corresponds an organization named \"mycompany\" and pipeline named \"mypipeline\". Then stick it in git:" 187 | echo 188 | echo " git config --local buildkite.organization \"mycompany\"" 189 | echo " git config --local buildkite.pipeline \"mypipeline\"" 190 | echo 191 | 192 | exit 1 193 | fi 194 | 195 | # Parse flags before parsing arguments 196 | let i=1 197 | while [[ $i -le $# ]]; do 198 | case "${!i}" in 199 | "--browse") 200 | browse=yes 201 | shift $i 202 | esac 203 | let i=i+1 204 | done 205 | 206 | if [[ "$#" -eq 0 ]]; then 207 | if ! current_branch_exists; then 208 | echo "Woops, you don't seem to be on a branch." >&2 209 | echo 210 | echo "Try:" 211 | echo 212 | echo " git checkout -b my_branch HEAD" 213 | echo 214 | echo "And make sure you push it first:" 215 | echo 216 | echo " git push --set-upstream origin my_branch" 217 | echo 218 | 219 | exit 1 220 | fi 221 | 222 | if upstream_branch_exists; then 223 | echo "$(current_branch) exists on origin as $(upstream_branch)" 224 | echo 225 | 226 | BUILDKITE_BRANCH="$(upstream_branch)" 227 | else 228 | if remote_current_branch_current; then 229 | echo "$(current_branch) also exists on origin at the same commit, I'll assume that's what we're building." 230 | echo 231 | 232 | BUILDKITE_BRANCH="$(current_branch)" 233 | elif remote_current_branch_behind; then 234 | echo "$(current_branch) also exists on origin, but it's at an earlier commit:" 235 | echo 236 | echo " $(git show --oneline --no-patch $(remote_current_branch_revision))" 237 | echo 238 | echo "This will only work if the commit is in another ref on origin." 239 | echo 240 | 241 | if confirm "Do you want to try anyway?"; then 242 | echo 243 | echo "Yeah, I trust you." 244 | echo 245 | 246 | BUILDKITE_BRANCH="$(current_branch)" 247 | else 248 | echo 249 | echo "Cautious player, I like that." 250 | echo 251 | echo "Try pushing your branch first:" 252 | echo 253 | echo " git push origin $(current_branch)" 254 | echo 255 | exit 1 256 | fi 257 | else 258 | echo "Woops, I can't figure out what this branch is called on origin." 259 | echo 260 | echo "Have you pushed it? Try:" 261 | echo 262 | echo " git push --set-upstream origin $(current_branch)" 263 | echo 264 | echo "Otherwise maybe you just need to set the upstream for this branch:" 265 | echo 266 | echo " git branch --set-upstream-to origin/$(current_branch)" 267 | echo 268 | 269 | exit 1 270 | fi 271 | fi 272 | 273 | if ! current_head_on_remote $BUILDKITE_BRANCH; then 274 | echo "Woops, I can't see this commit on origin." 275 | echo 276 | echo "Have you pushed it? Try:" 277 | echo 278 | echo " git push --set-upstream origin $(current_branch)" 279 | echo 280 | 281 | if confirm "Do you want to try anyway?"; then 282 | echo 283 | echo "Yeah, let's do this." 284 | echo 285 | else 286 | echo 287 | echo "Okay, let me know when you're ready!" 288 | echo 289 | exit 1 290 | fi 291 | fi 292 | 293 | if current_head_dirty; then 294 | echo "Woops, it looks like your working tree is dirty:" 295 | echo 296 | git status 297 | echo 298 | echo "If you want to build with these changes you should commit and push them first." 299 | echo 300 | 301 | if confirm "Do you want to try anyway?"; then 302 | echo 303 | echo "Yeah, let's do this." 304 | echo 305 | else 306 | echo 307 | echo "Okay, let me know when you're ready!" 308 | echo 309 | exit 1 310 | fi 311 | fi 312 | 313 | BUILDKITE_COMMIT="$(current_revision)" 314 | 315 | elif [[ "$#" -eq 1 ]]; then 316 | # git buildkite 317 | 318 | BUILDKITE_BRANCH="$1" 319 | 320 | BUILDKITE_COMMIT="$(git rev-parse "$BUILDKITE_BRANCH")" || ( echo "Woops, I can't figure out what commit to build from that branch"; exit 1 ) 321 | 322 | elif [[ "$#" -eq 2 ]]; then 323 | # git buildkite 324 | 325 | if [[ "$1" == "HEAD" || "$1" == "@" ]]; then 326 | # "@" or "HEAD" are aliases for building the current branch at a specified commit. 327 | BUILDKITE_BRANCH="$(current_branch)" 328 | 329 | if ! current_branch_exists; then 330 | echo "Woops, you don't seem to be on a branch." >&2 331 | echo 332 | echo "Try:" 333 | echo 334 | echo " git checkout -b my_branch HEAD" 335 | echo 336 | echo "And make sure you push it first:" 337 | echo 338 | echo " git push --set-upstream origin my_branch" 339 | echo 340 | 341 | exit 1 342 | fi 343 | else 344 | BUILDKITE_BRANCH="$1" 345 | fi 346 | 347 | # rev-parse the commit so we can do things like HEAD~2 or :/history 348 | BUILDKITE_COMMIT="$(git rev-parse "$2")" 349 | else 350 | echo "Usage: git buildkite [branch [commit]]" 351 | fi 352 | 353 | echo "Starting a build for $BUILDKITE_BRANCH at $BUILDKITE_COMMIT" 354 | 355 | result="$(build "$BUILDKITE_BRANCH" "$BUILDKITE_COMMIT")" 356 | 357 | if result_error "$result"; then 358 | echo 359 | echo "Woops, Buildkite didn't like that:" 360 | echo 361 | echo "$result" 362 | echo 363 | 364 | if result_api_key_error "$result"; then 365 | echo "Sounds like it might be your API key Check it out:" 366 | echo 367 | echo " https://buildkite.com/user/api-access-tokens" 368 | echo 369 | echo "Here's what we've got:" 370 | echo 371 | echo " $BUILDKITE_API_KEY" 372 | echo 373 | echo "Change it with:" 374 | echo 375 | echo " git config --global buildkite.apikey " 376 | echo 377 | fi 378 | 379 | exit 1 380 | fi 381 | 382 | build_url="$(result_build_url "$result")" 383 | 384 | echo 385 | echo "Success! Watch it go:" 386 | echo 387 | echo " ${build_url}" 388 | echo 389 | 390 | # Open a browser if we can 391 | if [[ -n "$browse" ]]; then 392 | if which open >/dev/null 2>&1; then 393 | open "${build_url}" 394 | elif which xdg-open >/dev/null 2>&1; then 395 | xdg-open "${build_url}" 396 | else 397 | echo "Sorry but I don't know how to open your browser! Copy and paste the link above as you like." 398 | fi 399 | fi 400 | --------------------------------------------------------------------------------