{"url":"https://api.github.com/gists/e1b038084d14d577b9ecd49f8e332ffb","forks_url":"https://api.github.com/gists/e1b038084d14d577b9ecd49f8e332ffb/forks","commits_url":"https://api.github.com/gists/e1b038084d14d577b9ecd49f8e332ffb/commits","id":"e1b038084d14d577b9ecd49f8e332ffb","node_id":"MDQ6R2lzdGUxYjAzODA4NGQxNGQ1NzdiOWVjZDQ5ZjhlMzMyZmZi","git_pull_url":"https://gist.github.com/e1b038084d14d577b9ecd49f8e332ffb.git","git_push_url":"https://gist.github.com/e1b038084d14d577b9ecd49f8e332ffb.git","html_url":"https://gist.github.com/michaellihs/e1b038084d14d577b9ecd49f8e332ffb","files":{"jenkins-cli.md":{"filename":"jenkins-cli.md","type":"text/markdown","language":"Markdown","raw_url":"https://gist.githubusercontent.com/michaellihs/e1b038084d14d577b9ecd49f8e332ffb/raw/fc9ec8355f7928c77642b3b1fa01f6168ee8fcec/jenkins-cli.md","size":13959,"truncated":false,"content":"Jenkins CLI & Hashicorp Vault\n=============================\n\n[TOC levels=4]: # \"## Table of contents\"\n\n## Table of contents\n- [Table of Contents](#table-of-contents)\n- [Setup Jenkins Locally in Docker](#setup-jenkins-locally-in-docker)\n- [Jenkins Job](#jenkins-job)\n- [CLI Configuration](#cli-configuration)\n    - [Configure nginx as reverse proxy for Jenkins](#configure-nginx-as-reverse-proxy-for-jenkins)\n- [Trigger Parametrized Jenkins Build](#trigger-parametrized-jenkins-build)\n- [Installing Vault](#installing-vault)\n    - [Configure TLS for Vault](#configure-tls-for-vault)\n    - [Configure Vault Autostart on Mac](#configure-vault-autostart-on-mac)\n    - [Re-generate Certificates for Vault](#re-generate-certificates-for-vault)\n    - [Stopping and Re-Starting Vault on Mac](#stopping-and-re-starting-vault-on-mac)\n    - [Automatically unseal Vault with a Password stored in 1Password](#automatically-unseal-vault-with-a-password-stored-in-1password)\n- [Using Vault in Jenkins](#using-vault-in-jenkins)\n    - [Set up App Role in Vault](#set-up-app-role-in-vault)\n    - [Jenkins Pipeline that reads secrets from Vault](#jenkins-pipeline-that-reads-secrets-from-vault)\n- [References](#references)\n\n\n\n\n\nSetup Jenkins Locally in Docker\n-------------------------------\n\nrun\n\n    docker run -d -p 49001:8080 -v $PWD/jenkins:/var/jenkins_home:z -t jenkins/jenkins:lts\n\nJenkins will be accessible after a while on http://localhost:49001/\n\n\nJenkins Job\n-----------\n\nSample Jenkins Job:\n\n````groovy\nproperties([\n        parameters([\n                string(name: 'cfuser', defaultValue: 'Jenkins', description: \"CF user\"),\n                password(name: 'cfpassword', description: \"CF password\"),\n                booleanParam(name: 'deploy', description: 'Really deploy?')\n        ])\n])\n\nnode {\n    stage('Echo Stage') {\n        echo params.cfpassword.getPlainText()\n        echo params.cfuser \n        echo params.deploy.toString()\n    }\n}\n````\n\n\nCLI Configuration\n-----------------\n\n* Given, you have Jenkins running in Docker, download CLI from http://localhost:49001/nlpJars/jenkins-cli.jar\n* Copy the `jenkins-cli.jar` into `~/jenkins/`\n* Create auth token in http://localhost:49001/user/admin/configure\n* Create a shell alias:\n\n   ```\n   alias jc=\"java -jar ~/jenkins/jenkins-cli.jar -s http://localhost:49001 -auth admin:11278ecdcddb512c7da2a91f2683a541e2\"\n   ```\n\n* Check whether everything is working via\n\n   ```\n   jc help\n   ```\n\n### Configure nginx as reverse proxy for Jenkins\n\nThe jenkins cli running over http requires buffering turned off in the nginx reverse proxy (see https://issues.jenkins-ci.org/browse/JENKINS-43666):\n\n````\n        location / {\n            # First attempt to serve request as file, then\n            # as directory, then fall back to displaying a 404.\n            # try_files $uri $uri/ =404;\n            include /etc/nginx/proxy_params;\n            proxy_pass          http://localhost:8080;\n            proxy_read_timeout  90s;\n            # Fix potential \"It appears that your reverse proxy set up is broken\" error.\n            proxy_redirect      http://localhost:8080 https://jenkins.tld.com;\n\n            # Required for new HTTP-based CLI\n            proxy_http_version 1.1;\n            proxy_request_buffering off;\n            proxy_buffering off;\n       }\n````\n\n\nTrigger Parametrized Jenkins Build\n----------------------------------\n\nRun\n\n    jc build 'cli-test' -p cfuser=lalalauser -p cfpassword=lalapassword -p deploy=true -s -v\n\nto trigger the before-mentioned job with the two parameters.\n\n\nInstalling Vault\n----------------\n\nRun\n\n    brew install https://raw.githubusercontent.com/petems/homebrew-vault-prebuilt/master/Formula/vault.rb\n    vault -autocomplete-install\n    # restart your shell\n\nwe use https://github.com/petems/homebrew-vault-prebuilt since the default homebrew `vault` offering does not contain the UI.\n\nCreate a config file `~/vault/vault.config` with\n\n    ui = true\n\n    storage \"file\" {\n      path = \"/Users/USERNAME/vault/data\"\n    }\n\n    listener \"tcp\" {\n      address     = \"127.0.0.1:8200\"\n      tls_disable = 1\n    }\n\nStart the vault server with the given configuration via\n\n    vault server -config ~/vault/vault.config\n\nYou can now access the vault UI via http://localhost:8200/ui\n\nRun\n\n    export VAULT_ADDR='http://127.0.0.1:8200'\n\ncheck Vault status\n\n    vault status\n\n\n### Configure TLS for Vault\n\nSee https://www.monterail.com/blog/2017/lets-encrypt-vault-free-ssl-tls-certificate\n\nSince we do not have a public domain for Vault, we cannot use `letsencrypt` - instead create your own self-signed certificate for `vault.localhost`:\n\n    openssl req -x509 -out vault.localhost.crt -keyout vault.localhost.key \\\n      -newkey rsa:2048 -nodes -days 712 -sha256 \\\n      -subj '/CN=vault.localhost' -extensions EXT -config <( \\\n       printf \"[dn]\\nCN=vault.localhost\\n[req]\\ndistinguished_name = dn\\n[EXT]\\nsubjectAltName=DNS:vault.localhost\\nkeyUsage=digitalSignature\\nextendedKeyUsage=serverAuth\")\n\nMove `vault.localhost.crt` and `vault.localhost.key` to `~/vault/`\n\n    mv vault.localhost.* ~/vault/\n\nAdd the certificate to the system's keystore:\n\n    sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ~/vault/vault.localhost.crt\n\nAdd a hosts entry in `/etc/hosts`\n\n    127.0.0.1 vault.localhost\n\nConfigure vault server to use the certificates - within `~/vault/vault.config`:\n\n    listener \"tcp\" {\n          address = \"vault.localhost:8200\"\n          tls_cert_file = \"/Users/USER/vault/vault.localhost.crt\"\n          tls_key_file = \"/Users/USER/vault/vault.localhost.key\"\n    }\n\nRestart vault, via CTRL+C in the terminal window running vault server and then running\n\n    vault server -config ~/vault/vault.config\n\nYou can now access your Vault UI via https://vault.localhost:8200\n\n\n### Configure Vault Autostart on Mac\n\nSee https://blog.alanthatcher.io/fun-and-profit-with-vault-part-2/\n\nWithin `/Library/LaunchDaemons/` create a new file `com.hashicorp.vault.plist`:\n\n    vi /Library/LaunchDaemons/com.hashicorp.vault.plist\n\nAdd the following content:\n\n    <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n    <!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n    <plist version=\"1.0\">\n    <dict>\n        <key>Label</key>             <string>com.hashicorp.vault</string>\n        <key>Disabled</key>          <false/>\n        <key>RunAtLoad</key>         <true/>\n        <key>KeepAlive</key>         <false/>\n        <key>LaunchOnlyOnce</key>    <true/>\n        <key>ProgramArguments</key>\n            <array>\n                <string>/usr/local/bin/vault</string>\n                <string>server</string>\n                <string>-config</string>\n                <string>/Users/USER/vault/vault.config</string>\n            </array>\n    </dict>\n    </plist>\n\nYou can test your configuration via\n\n    sudo launchctl load -w /Library/LaunchDaemons/com.hashicorp.vault.plist\n\nWrite a vault config script\n\n    vi /Users/USERS/vault/user-settings.sh\n\nwith the following content\n\n    export VAULT_ADDR=https://vault.localhost:8200\n\nsource it in your shell startup script (e.g. `~/.zshrc`):\n\n    . ~/vault/user-settings.sh\n\n\n### Re-generate Certificates for Vault\n\nUse the following command to re-generate the SSL certificates:\n\n    openssl req -x509 -out vault.localhost.crt -keyout vault.localhost.key \\\n          -newkey rsa:2048 -nodes -sha256 \\\n          -subj '/CN=vault.localhost' -extensions EXT -config <( \\\n           printf \"[dn]\\nCN=vault.localhost\\n[req]\\ndistinguished_name = dn\\n[EXT]\\nsubjectAltName=DNS:vault.localhost\\nkeyUsage=digitalSignature\\nextendedKeyUsage=serverAuth\")\n\nCopy the certificates to `~/vault/` via `mv vault.localhost.* ~/vault/`. Restart Vault.\n\n\n### Stopping and Re-Starting Vault on Mac\n\n* Stop Vault via\n\n   ```shell\n   sudo launchctl unload -w /Library/LaunchDaemons/com.hashicorp.vault.plist\n   ```\n\n* Start Vault via\n\n   ```shell\n   sudo launchctl load -w /Library/LaunchDaemons/com.hashicorp.vault.plist\n   ```\n\n\n### Automatically unseal Vault with a Password stored in 1Password\n\nSee https://www.reddit.com/r/1Password/comments/8zq79d/how_to_get_password_for_use_in_applescript/\n\nHere is a hack for unsealing Vault with a password that was previously stored in 1Password with the name `Vault Unseal (Master Key)`\n\n    #!/usr/bin/env bash\n\n    vault_unseal_name='Vault Unseal (Master Key)'\n\n    osascript <<-END\n       on run\n          tell application \"System Events\" to tell process \"1Password mini\"\n               open location \"onepassword://extension/search/${vault_unseal_name}\"\n               delay 0.2\n               keystroke \"C\" using {command down, shift down}\n               set unseal_key to the clipboard as text\n          end tell\n          do shell script \"vault operator unseal \" & unseal_key\n       end run\n    END\n\n\nUsing Vault in Jenkins\n----------------------\n\n* [Jenkins Vault Plugin](https://github.com/jenkinsci/hashicorp-vault-plugin)\n* [Reading Vault Secrets in Jenkinsfile](http://nicolas.corrarello.com/general/vault/security/ci/2017/04/23/Reading-Vault-Secrets-in-your-Jenkins-pipeline.html)\n\n\n### Set up App Role in Vault\n\nFor enabling Jenkins to access Vault we will use an [app role](https://blog.alanthatcher.io/vault-approle-authentication/).\n\nA description of what needs to be done to setup the app role can be found [here](http://nicolas.corrarello.com/general/vault/security/ci/2017/04/23/Reading-Vault-Secrets-in-your-Jenkins-pipeline.html).\n\n\n1. Create a new KV store in Vault for our secrets:\n\n   ```\n   vault mount -path=jenkins-secrets kv\n   ```\n\n   You can check whether this worked with `vault mounts`\n\n2. For testing, put a secret in that newly created store:\n\n   ```\n   vault write jenkins-secrets/test-secret value='my-secret-value'\n   ```\n\n   You can check whether this worked with `vault kv get jenkins-secrets/test-secret`\n\n3. Generate a policy `jenkins-ro` (for read-only) for the jenkins role in Vault:\n\n   ```\n   echo 'path \"jenkins-secrets/*\" {\n       capabilities = [\"read\", \"list\"]\n   }' | vault policy-write jenkins-ro -\n   ```\n\n   You can check whether this worked with `vault read /sys/policy/jenkins-ro`\n\n4. Create a Vault token with the afore-created policy assigned:\n\n   ```\n   vault token create -policy=\"jenkins-ro\"\n   \n   Key                  Value\n   ---                  -----\n   token                d9a343cf-a559-ebfc-a702-60c57faf4e7c\n   token_accessor       3aa9c348-2797-89d7-18cf-ab2f019e1540\n   token_duration       768h\n   token_renewable      true\n   token_policies       [\"default\" \"jenkins-ro\"]\n   identity_policies    []\n   policies             [\"default\" \"jenkins-ro\"]\n   ```\n\n   Next, try to authenticate with the token created before:\n\n   ```\n   vault auth d9a343cf-a559-ebfc-a702-60c57faf4e7c\n   ```\n\n   And read the `jenkins-secret` with the applied role, this should work:\n\n   ```\n   vault read jenkins-secrets/test-secret\n   \n   Key                 Value\n   ---                 -----\n   refresh_interval    768h\n   value2              another-secret-value\n   ```\n\n5. Re-authenticate with an Vault admin user `vault auth <ADMIN TOKEN>`\n\n6. Enable the `app-role` as authentication backend:\n\n   ```\n   vault auth-enable approle\n   ```\n\n7. Now create an app role for jenkins:\n\n   ```\n   vault write auth/approle/role/jenkins-ro secret_id_ttl=1m secret_id_num_uses=1 token_num_uses=3 token_ttl=10m token_max_ttl=30m policies=jenkins-ro\n   ```\n\n   You can check whether everything works as expected via\n\n   ```\n   vault read auth/approle/role/jenkins-ro\n   ```\n\n8. Get the internal ID of the app role:\n\n   ```\n   export ROLE_ID=$(vault read -field role_id auth/approle/role/jenkins-ro/role-id)\n   ```\n\n9. Now generate a secret ID for the app role:\n\n   ```\n   export SECRET_ID=$(vault write -field secret_id -f auth/approle/role/jenkins-ro/secret-id)\n   ```\n\n10. Login using the app role and afore created `ROLE_ID` and `SECRET_ID` (you have the `secret_id_ttl` time to do so after you created the `SECRET_ID`):\n\n   ```\n   export APP_ROLE_TOKEN=$(vault write -field token auth/approle/login role_id=${ROLE_ID} secret_id=${SECRET_ID})\n   ```\n\n11. You can now check the generated token via:\n\n   ```\n   vault login $APP_ROLE_TOKEN\n   vault read jenkins-secrets/test-secret\n   \n   Key                 Value\n   ---                 -----\n   refresh_interval    768h\n   value               my-secret-value\n   ```\n\nThe tricky thing to understand is that we use the app role to generate a token that we then use for authentication. We do not directly authenticate to Vault using the app role's secret id.\n\n\n### Jenkins Pipeline that reads secrets from Vault\n\nHere is an example of a `Jenkinsfile` that reads secrets from Vault:\n\n````groovy\nnode {\n\n    stage('Reading from Vault') {\n        sh 'curl -o vault.zip https://releases.hashicorp.com/vault/0.11.3/vault_0.11.3_linux_amd64.zip ; yes | unzip vault.zip'\n        sh '''\n          # set to +x later on to avoid log output containing sensitive data\n          set -ex\n          export VAULT_ADDR='https://docker.for.mac.localhost:8200'\n          export VAULT_SKIP_VERIFY=true\n          ## needs to be the token generated for being able to create the secret_id\n          ## we want to query this via a input field...\n          export VAULT_TOKEN='e0c4dc53-369b-8d3e-09db-cf34a4610fc4'\n          ## the id of the 'jenkins-ro' role\n          export ROLE_ID='949be848-5368-ffac-b4fb-c61288ca9d6b'\n          export SECRET_ID=$(./vault write -field=secret_id -f auth/approle/role/jenkins-ro/secret-id)\n          export VAULT_TOKEN=$(./vault write -field=token auth/approle/login role_id=${ROLE_ID} secret_id=${SECRET_ID})\n          export TEST_VAR=$(./vault kv get -field=-value jenkins-secrets/test-secret)\n          #echo $TEST_VAR\n        '''\n\n    }\n\n}\n````\n\n\nReferences\n----------\n\n* [Digital Ocean Tutorial for Vault](https://www.digitalocean.com/community/tutorials/how-to-securely-manage-secrets-with-hashicorp-vault-on-ubuntu-16-04)","encoding":"utf-8"}},"public":true,"created_at":"2018-10-19T21:05:38Z","updated_at":"2019-02-22T11:30:03Z","description":"Jenkins CLI","comments":0,"user":null,"comments_enabled":true,"comments_url":"https://api.github.com/gists/e1b038084d14d577b9ecd49f8e332ffb/comments","owner":{"login":"michaellihs","id":575011,"node_id":"MDQ6VXNlcjU3NTAxMQ==","avatar_url":"https://avatars.githubusercontent.com/u/575011?v=4","gravatar_id":"","url":"https://api.github.com/users/michaellihs","html_url":"https://github.com/michaellihs","followers_url":"https://api.github.com/users/michaellihs/followers","following_url":"https://api.github.com/users/michaellihs/following{/other_user}","gists_url":"https://api.github.com/users/michaellihs/gists{/gist_id}","starred_url":"https://api.github.com/users/michaellihs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/michaellihs/subscriptions","organizations_url":"https://api.github.com/users/michaellihs/orgs","repos_url":"https://api.github.com/users/michaellihs/repos","events_url":"https://api.github.com/users/michaellihs/events{/privacy}","received_events_url":"https://api.github.com/users/michaellihs/received_events","type":"User","user_view_type":"public","site_admin":false},"forks":[],"history":[{"user":{"login":"michaellihs","id":575011,"node_id":"MDQ6VXNlcjU3NTAxMQ==","avatar_url":"https://avatars.githubusercontent.com/u/575011?v=4","gravatar_id":"","url":"https://api.github.com/users/michaellihs","html_url":"https://github.com/michaellihs","followers_url":"https://api.github.com/users/michaellihs/followers","following_url":"https://api.github.com/users/michaellihs/following{/other_user}","gists_url":"https://api.github.com/users/michaellihs/gists{/gist_id}","starred_url":"https://api.github.com/users/michaellihs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/michaellihs/subscriptions","organizations_url":"https://api.github.com/users/michaellihs/orgs","repos_url":"https://api.github.com/users/michaellihs/repos","events_url":"https://api.github.com/users/michaellihs/events{/privacy}","received_events_url":"https://api.github.com/users/michaellihs/received_events","type":"User","user_view_type":"public","site_admin":false},"version":"304527f9b619cc421adddbe18f2e8016a3f28665","committed_at":"2019-02-22T11:30:02Z","change_status":{"total":2,"additions":1,"deletions":1},"url":"https://api.github.com/gists/e1b038084d14d577b9ecd49f8e332ffb/304527f9b619cc421adddbe18f2e8016a3f28665"},{"user":{"login":"michaellihs","id":575011,"node_id":"MDQ6VXNlcjU3NTAxMQ==","avatar_url":"https://avatars.githubusercontent.com/u/575011?v=4","gravatar_id":"","url":"https://api.github.com/users/michaellihs","html_url":"https://github.com/michaellihs","followers_url":"https://api.github.com/users/michaellihs/followers","following_url":"https://api.github.com/users/michaellihs/following{/other_user}","gists_url":"https://api.github.com/users/michaellihs/gists{/gist_id}","starred_url":"https://api.github.com/users/michaellihs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/michaellihs/subscriptions","organizations_url":"https://api.github.com/users/michaellihs/orgs","repos_url":"https://api.github.com/users/michaellihs/repos","events_url":"https://api.github.com/users/michaellihs/events{/privacy}","received_events_url":"https://api.github.com/users/michaellihs/received_events","type":"User","user_view_type":"public","site_admin":false},"version":"8d865046e138ce4c3827cfd978df43ffc6f9d9af","committed_at":"2019-02-22T10:41:37Z","change_status":{"total":4,"additions":2,"deletions":2},"url":"https://api.github.com/gists/e1b038084d14d577b9ecd49f8e332ffb/8d865046e138ce4c3827cfd978df43ffc6f9d9af"},{"user":{"login":"michaellihs","id":575011,"node_id":"MDQ6VXNlcjU3NTAxMQ==","avatar_url":"https://avatars.githubusercontent.com/u/575011?v=4","gravatar_id":"","url":"https://api.github.com/users/michaellihs","html_url":"https://github.com/michaellihs","followers_url":"https://api.github.com/users/michaellihs/followers","following_url":"https://api.github.com/users/michaellihs/following{/other_user}","gists_url":"https://api.github.com/users/michaellihs/gists{/gist_id}","starred_url":"https://api.github.com/users/michaellihs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/michaellihs/subscriptions","organizations_url":"https://api.github.com/users/michaellihs/orgs","repos_url":"https://api.github.com/users/michaellihs/repos","events_url":"https://api.github.com/users/michaellihs/events{/privacy}","received_events_url":"https://api.github.com/users/michaellihs/received_events","type":"User","user_view_type":"public","site_admin":false},"version":"dfb8cb01568124a8b04d7170a683a33fd3fca4d2","committed_at":"2019-01-29T09:34:29Z","change_status":{"total":2,"additions":1,"deletions":1},"url":"https://api.github.com/gists/e1b038084d14d577b9ecd49f8e332ffb/dfb8cb01568124a8b04d7170a683a33fd3fca4d2"},{"user":{"login":"michaellihs","id":575011,"node_id":"MDQ6VXNlcjU3NTAxMQ==","avatar_url":"https://avatars.githubusercontent.com/u/575011?v=4","gravatar_id":"","url":"https://api.github.com/users/michaellihs","html_url":"https://github.com/michaellihs","followers_url":"https://api.github.com/users/michaellihs/followers","following_url":"https://api.github.com/users/michaellihs/following{/other_user}","gists_url":"https://api.github.com/users/michaellihs/gists{/gist_id}","starred_url":"https://api.github.com/users/michaellihs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/michaellihs/subscriptions","organizations_url":"https://api.github.com/users/michaellihs/orgs","repos_url":"https://api.github.com/users/michaellihs/repos","events_url":"https://api.github.com/users/michaellihs/events{/privacy}","received_events_url":"https://api.github.com/users/michaellihs/received_events","type":"User","user_view_type":"public","site_admin":false},"version":"ec1e96c3b2ef33731f28cda6732d7c5e178fa429","committed_at":"2018-11-20T16:11:20Z","change_status":{"total":104,"additions":68,"deletions":36},"url":"https://api.github.com/gists/e1b038084d14d577b9ecd49f8e332ffb/ec1e96c3b2ef33731f28cda6732d7c5e178fa429"},{"user":{"login":"michaellihs","id":575011,"node_id":"MDQ6VXNlcjU3NTAxMQ==","avatar_url":"https://avatars.githubusercontent.com/u/575011?v=4","gravatar_id":"","url":"https://api.github.com/users/michaellihs","html_url":"https://github.com/michaellihs","followers_url":"https://api.github.com/users/michaellihs/followers","following_url":"https://api.github.com/users/michaellihs/following{/other_user}","gists_url":"https://api.github.com/users/michaellihs/gists{/gist_id}","starred_url":"https://api.github.com/users/michaellihs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/michaellihs/subscriptions","organizations_url":"https://api.github.com/users/michaellihs/orgs","repos_url":"https://api.github.com/users/michaellihs/repos","events_url":"https://api.github.com/users/michaellihs/events{/privacy}","received_events_url":"https://api.github.com/users/michaellihs/received_events","type":"User","user_view_type":"public","site_admin":false},"version":"76d4552ebbf105c8b16e0ef7e6457823e75215d4","committed_at":"2018-10-26T05:27:33Z","change_status":{"total":23,"additions":23,"deletions":0},"url":"https://api.github.com/gists/e1b038084d14d577b9ecd49f8e332ffb/76d4552ebbf105c8b16e0ef7e6457823e75215d4"},{"user":{"login":"michaellihs","id":575011,"node_id":"MDQ6VXNlcjU3NTAxMQ==","avatar_url":"https://avatars.githubusercontent.com/u/575011?v=4","gravatar_id":"","url":"https://api.github.com/users/michaellihs","html_url":"https://github.com/michaellihs","followers_url":"https://api.github.com/users/michaellihs/followers","following_url":"https://api.github.com/users/michaellihs/following{/other_user}","gists_url":"https://api.github.com/users/michaellihs/gists{/gist_id}","starred_url":"https://api.github.com/users/michaellihs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/michaellihs/subscriptions","organizations_url":"https://api.github.com/users/michaellihs/orgs","repos_url":"https://api.github.com/users/michaellihs/repos","events_url":"https://api.github.com/users/michaellihs/events{/privacy}","received_events_url":"https://api.github.com/users/michaellihs/received_events","type":"User","user_view_type":"public","site_admin":false},"version":"7295c2d531babfe89e9f9aa3ebb064c88b7f5b41","committed_at":"2018-10-21T19:32:26Z","change_status":{"total":95,"additions":95,"deletions":0},"url":"https://api.github.com/gists/e1b038084d14d577b9ecd49f8e332ffb/7295c2d531babfe89e9f9aa3ebb064c88b7f5b41"},{"user":{"login":"michaellihs","id":575011,"node_id":"MDQ6VXNlcjU3NTAxMQ==","avatar_url":"https://avatars.githubusercontent.com/u/575011?v=4","gravatar_id":"","url":"https://api.github.com/users/michaellihs","html_url":"https://github.com/michaellihs","followers_url":"https://api.github.com/users/michaellihs/followers","following_url":"https://api.github.com/users/michaellihs/following{/other_user}","gists_url":"https://api.github.com/users/michaellihs/gists{/gist_id}","starred_url":"https://api.github.com/users/michaellihs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/michaellihs/subscriptions","organizations_url":"https://api.github.com/users/michaellihs/orgs","repos_url":"https://api.github.com/users/michaellihs/repos","events_url":"https://api.github.com/users/michaellihs/events{/privacy}","received_events_url":"https://api.github.com/users/michaellihs/received_events","type":"User","user_view_type":"public","site_admin":false},"version":"e153707702c9225126d90532ccb284fb9e6673a4","committed_at":"2018-10-21T17:37:27Z","change_status":{"total":23,"additions":23,"deletions":0},"url":"https://api.github.com/gists/e1b038084d14d577b9ecd49f8e332ffb/e153707702c9225126d90532ccb284fb9e6673a4"},{"user":{"login":"michaellihs","id":575011,"node_id":"MDQ6VXNlcjU3NTAxMQ==","avatar_url":"https://avatars.githubusercontent.com/u/575011?v=4","gravatar_id":"","url":"https://api.github.com/users/michaellihs","html_url":"https://github.com/michaellihs","followers_url":"https://api.github.com/users/michaellihs/followers","following_url":"https://api.github.com/users/michaellihs/following{/other_user}","gists_url":"https://api.github.com/users/michaellihs/gists{/gist_id}","starred_url":"https://api.github.com/users/michaellihs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/michaellihs/subscriptions","organizations_url":"https://api.github.com/users/michaellihs/orgs","repos_url":"https://api.github.com/users/michaellihs/repos","events_url":"https://api.github.com/users/michaellihs/events{/privacy}","received_events_url":"https://api.github.com/users/michaellihs/received_events","type":"User","user_view_type":"public","site_admin":false},"version":"4c5bcdc1689ef121e17713dc91d3f942ce92dc35","committed_at":"2018-10-21T17:30:27Z","change_status":{"total":0,"additions":0,"deletions":0},"url":"https://api.github.com/gists/e1b038084d14d577b9ecd49f8e332ffb/4c5bcdc1689ef121e17713dc91d3f942ce92dc35"},{"user":{"login":"michaellihs","id":575011,"node_id":"MDQ6VXNlcjU3NTAxMQ==","avatar_url":"https://avatars.githubusercontent.com/u/575011?v=4","gravatar_id":"","url":"https://api.github.com/users/michaellihs","html_url":"https://github.com/michaellihs","followers_url":"https://api.github.com/users/michaellihs/followers","following_url":"https://api.github.com/users/michaellihs/following{/other_user}","gists_url":"https://api.github.com/users/michaellihs/gists{/gist_id}","starred_url":"https://api.github.com/users/michaellihs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/michaellihs/subscriptions","organizations_url":"https://api.github.com/users/michaellihs/orgs","repos_url":"https://api.github.com/users/michaellihs/repos","events_url":"https://api.github.com/users/michaellihs/events{/privacy}","received_events_url":"https://api.github.com/users/michaellihs/received_events","type":"User","user_view_type":"public","site_admin":false},"version":"6283184d76ce7849cc17f1f342a04e4118fa4c71","committed_at":"2018-10-21T17:29:44Z","change_status":{"total":12,"additions":11,"deletions":1},"url":"https://api.github.com/gists/e1b038084d14d577b9ecd49f8e332ffb/6283184d76ce7849cc17f1f342a04e4118fa4c71"},{"user":{"login":"michaellihs","id":575011,"node_id":"MDQ6VXNlcjU3NTAxMQ==","avatar_url":"https://avatars.githubusercontent.com/u/575011?v=4","gravatar_id":"","url":"https://api.github.com/users/michaellihs","html_url":"https://github.com/michaellihs","followers_url":"https://api.github.com/users/michaellihs/followers","following_url":"https://api.github.com/users/michaellihs/following{/other_user}","gists_url":"https://api.github.com/users/michaellihs/gists{/gist_id}","starred_url":"https://api.github.com/users/michaellihs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/michaellihs/subscriptions","organizations_url":"https://api.github.com/users/michaellihs/orgs","repos_url":"https://api.github.com/users/michaellihs/repos","events_url":"https://api.github.com/users/michaellihs/events{/privacy}","received_events_url":"https://api.github.com/users/michaellihs/received_events","type":"User","user_view_type":"public","site_admin":false},"version":"3f5470e12e8e2f0b29aa254722315f8ddf129e89","committed_at":"2018-10-21T17:28:17Z","change_status":{"total":52,"additions":52,"deletions":0},"url":"https://api.github.com/gists/e1b038084d14d577b9ecd49f8e332ffb/3f5470e12e8e2f0b29aa254722315f8ddf129e89"},{"user":{"login":"michaellihs","id":575011,"node_id":"MDQ6VXNlcjU3NTAxMQ==","avatar_url":"https://avatars.githubusercontent.com/u/575011?v=4","gravatar_id":"","url":"https://api.github.com/users/michaellihs","html_url":"https://github.com/michaellihs","followers_url":"https://api.github.com/users/michaellihs/followers","following_url":"https://api.github.com/users/michaellihs/following{/other_user}","gists_url":"https://api.github.com/users/michaellihs/gists{/gist_id}","starred_url":"https://api.github.com/users/michaellihs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/michaellihs/subscriptions","organizations_url":"https://api.github.com/users/michaellihs/orgs","repos_url":"https://api.github.com/users/michaellihs/repos","events_url":"https://api.github.com/users/michaellihs/events{/privacy}","received_events_url":"https://api.github.com/users/michaellihs/received_events","type":"User","user_view_type":"public","site_admin":false},"version":"61414ec8efa03d8f1b887e6e5a8ec04e02ccebba","committed_at":"2018-10-20T20:15:55Z","change_status":{},"url":"https://api.github.com/gists/e1b038084d14d577b9ecd49f8e332ffb/61414ec8efa03d8f1b887e6e5a8ec04e02ccebba"},{"user":{"login":"michaellihs","id":575011,"node_id":"MDQ6VXNlcjU3NTAxMQ==","avatar_url":"https://avatars.githubusercontent.com/u/575011?v=4","gravatar_id":"","url":"https://api.github.com/users/michaellihs","html_url":"https://github.com/michaellihs","followers_url":"https://api.github.com/users/michaellihs/followers","following_url":"https://api.github.com/users/michaellihs/following{/other_user}","gists_url":"https://api.github.com/users/michaellihs/gists{/gist_id}","starred_url":"https://api.github.com/users/michaellihs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/michaellihs/subscriptions","organizations_url":"https://api.github.com/users/michaellihs/orgs","repos_url":"https://api.github.com/users/michaellihs/repos","events_url":"https://api.github.com/users/michaellihs/events{/privacy}","received_events_url":"https://api.github.com/users/michaellihs/received_events","type":"User","user_view_type":"public","site_admin":false},"version":"a87d7a34951f69bc3dc7b6b74ae10a7f8390d566","committed_at":"2018-10-20T19:42:42Z","change_status":{},"url":"https://api.github.com/gists/e1b038084d14d577b9ecd49f8e332ffb/a87d7a34951f69bc3dc7b6b74ae10a7f8390d566"},{"user":{"login":"michaellihs","id":575011,"node_id":"MDQ6VXNlcjU3NTAxMQ==","avatar_url":"https://avatars.githubusercontent.com/u/575011?v=4","gravatar_id":"","url":"https://api.github.com/users/michaellihs","html_url":"https://github.com/michaellihs","followers_url":"https://api.github.com/users/michaellihs/followers","following_url":"https://api.github.com/users/michaellihs/following{/other_user}","gists_url":"https://api.github.com/users/michaellihs/gists{/gist_id}","starred_url":"https://api.github.com/users/michaellihs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/michaellihs/subscriptions","organizations_url":"https://api.github.com/users/michaellihs/orgs","repos_url":"https://api.github.com/users/michaellihs/repos","events_url":"https://api.github.com/users/michaellihs/events{/privacy}","received_events_url":"https://api.github.com/users/michaellihs/received_events","type":"User","user_view_type":"public","site_admin":false},"version":"11c9630257f82cf579d259a92d35c503e093a317","committed_at":"2018-10-20T19:33:27Z","change_status":{},"url":"https://api.github.com/gists/e1b038084d14d577b9ecd49f8e332ffb/11c9630257f82cf579d259a92d35c503e093a317"},{"user":{"login":"michaellihs","id":575011,"node_id":"MDQ6VXNlcjU3NTAxMQ==","avatar_url":"https://avatars.githubusercontent.com/u/575011?v=4","gravatar_id":"","url":"https://api.github.com/users/michaellihs","html_url":"https://github.com/michaellihs","followers_url":"https://api.github.com/users/michaellihs/followers","following_url":"https://api.github.com/users/michaellihs/following{/other_user}","gists_url":"https://api.github.com/users/michaellihs/gists{/gist_id}","starred_url":"https://api.github.com/users/michaellihs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/michaellihs/subscriptions","organizations_url":"https://api.github.com/users/michaellihs/orgs","repos_url":"https://api.github.com/users/michaellihs/repos","events_url":"https://api.github.com/users/michaellihs/events{/privacy}","received_events_url":"https://api.github.com/users/michaellihs/received_events","type":"User","user_view_type":"public","site_admin":false},"version":"bed9f9dee6a674295733514b9f43c09e8b561031","committed_at":"2018-10-20T18:59:40Z","change_status":{},"url":"https://api.github.com/gists/e1b038084d14d577b9ecd49f8e332ffb/bed9f9dee6a674295733514b9f43c09e8b561031"},{"user":{"login":"michaellihs","id":575011,"node_id":"MDQ6VXNlcjU3NTAxMQ==","avatar_url":"https://avatars.githubusercontent.com/u/575011?v=4","gravatar_id":"","url":"https://api.github.com/users/michaellihs","html_url":"https://github.com/michaellihs","followers_url":"https://api.github.com/users/michaellihs/followers","following_url":"https://api.github.com/users/michaellihs/following{/other_user}","gists_url":"https://api.github.com/users/michaellihs/gists{/gist_id}","starred_url":"https://api.github.com/users/michaellihs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/michaellihs/subscriptions","organizations_url":"https://api.github.com/users/michaellihs/orgs","repos_url":"https://api.github.com/users/michaellihs/repos","events_url":"https://api.github.com/users/michaellihs/events{/privacy}","received_events_url":"https://api.github.com/users/michaellihs/received_events","type":"User","user_view_type":"public","site_admin":false},"version":"5944ec8b412789d68feeb437d52ab8ec4f00a382","committed_at":"2018-10-20T18:14:46Z","change_status":{},"url":"https://api.github.com/gists/e1b038084d14d577b9ecd49f8e332ffb/5944ec8b412789d68feeb437d52ab8ec4f00a382"},{"user":{"login":"michaellihs","id":575011,"node_id":"MDQ6VXNlcjU3NTAxMQ==","avatar_url":"https://avatars.githubusercontent.com/u/575011?v=4","gravatar_id":"","url":"https://api.github.com/users/michaellihs","html_url":"https://github.com/michaellihs","followers_url":"https://api.github.com/users/michaellihs/followers","following_url":"https://api.github.com/users/michaellihs/following{/other_user}","gists_url":"https://api.github.com/users/michaellihs/gists{/gist_id}","starred_url":"https://api.github.com/users/michaellihs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/michaellihs/subscriptions","organizations_url":"https://api.github.com/users/michaellihs/orgs","repos_url":"https://api.github.com/users/michaellihs/repos","events_url":"https://api.github.com/users/michaellihs/events{/privacy}","received_events_url":"https://api.github.com/users/michaellihs/received_events","type":"User","user_view_type":"public","site_admin":false},"version":"d8ecdb945c991c9edef3dbd15dcaf3edbe98728d","committed_at":"2018-10-19T21:29:57Z","change_status":{},"url":"https://api.github.com/gists/e1b038084d14d577b9ecd49f8e332ffb/d8ecdb945c991c9edef3dbd15dcaf3edbe98728d"},{"user":{"login":"michaellihs","id":575011,"node_id":"MDQ6VXNlcjU3NTAxMQ==","avatar_url":"https://avatars.githubusercontent.com/u/575011?v=4","gravatar_id":"","url":"https://api.github.com/users/michaellihs","html_url":"https://github.com/michaellihs","followers_url":"https://api.github.com/users/michaellihs/followers","following_url":"https://api.github.com/users/michaellihs/following{/other_user}","gists_url":"https://api.github.com/users/michaellihs/gists{/gist_id}","starred_url":"https://api.github.com/users/michaellihs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/michaellihs/subscriptions","organizations_url":"https://api.github.com/users/michaellihs/orgs","repos_url":"https://api.github.com/users/michaellihs/repos","events_url":"https://api.github.com/users/michaellihs/events{/privacy}","received_events_url":"https://api.github.com/users/michaellihs/received_events","type":"User","user_view_type":"public","site_admin":false},"version":"d75eb6ab8821b8ee555fb86bde2f3324dd280fc5","committed_at":"2018-10-19T21:26:51Z","change_status":{},"url":"https://api.github.com/gists/e1b038084d14d577b9ecd49f8e332ffb/d75eb6ab8821b8ee555fb86bde2f3324dd280fc5"},{"user":{"login":"michaellihs","id":575011,"node_id":"MDQ6VXNlcjU3NTAxMQ==","avatar_url":"https://avatars.githubusercontent.com/u/575011?v=4","gravatar_id":"","url":"https://api.github.com/users/michaellihs","html_url":"https://github.com/michaellihs","followers_url":"https://api.github.com/users/michaellihs/followers","following_url":"https://api.github.com/users/michaellihs/following{/other_user}","gists_url":"https://api.github.com/users/michaellihs/gists{/gist_id}","starred_url":"https://api.github.com/users/michaellihs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/michaellihs/subscriptions","organizations_url":"https://api.github.com/users/michaellihs/orgs","repos_url":"https://api.github.com/users/michaellihs/repos","events_url":"https://api.github.com/users/michaellihs/events{/privacy}","received_events_url":"https://api.github.com/users/michaellihs/received_events","type":"User","user_view_type":"public","site_admin":false},"version":"6f141e77ef442acc33d1a0434e30b0d3dbd05ca3","committed_at":"2018-10-19T21:17:35Z","change_status":{},"url":"https://api.github.com/gists/e1b038084d14d577b9ecd49f8e332ffb/6f141e77ef442acc33d1a0434e30b0d3dbd05ca3"},{"user":{"login":"michaellihs","id":575011,"node_id":"MDQ6VXNlcjU3NTAxMQ==","avatar_url":"https://avatars.githubusercontent.com/u/575011?v=4","gravatar_id":"","url":"https://api.github.com/users/michaellihs","html_url":"https://github.com/michaellihs","followers_url":"https://api.github.com/users/michaellihs/followers","following_url":"https://api.github.com/users/michaellihs/following{/other_user}","gists_url":"https://api.github.com/users/michaellihs/gists{/gist_id}","starred_url":"https://api.github.com/users/michaellihs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/michaellihs/subscriptions","organizations_url":"https://api.github.com/users/michaellihs/orgs","repos_url":"https://api.github.com/users/michaellihs/repos","events_url":"https://api.github.com/users/michaellihs/events{/privacy}","received_events_url":"https://api.github.com/users/michaellihs/received_events","type":"User","user_view_type":"public","site_admin":false},"version":"e95cb6179bd1a8242064d099d40b9af1a747a9d1","committed_at":"2018-10-19T21:05:37Z","change_status":{},"url":"https://api.github.com/gists/e1b038084d14d577b9ecd49f8e332ffb/e95cb6179bd1a8242064d099d40b9af1a747a9d1"}],"truncated":false}