<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Jupyter Blog - DevOps</title><link href="https://jasongrout.github.io/medium-archive/pelican/" rel="alternate"/><link href="https://jasongrout.github.io/medium-archive/pelican/feeds/tag-devops.atom.xml" rel="self"/><id>https://jasongrout.github.io/medium-archive/pelican/</id><updated>2024-10-28T18:09:00+00:00</updated><subtitle>The Project Jupyter blog: news, releases, and community stories, archived from blog.jupyter.org.</subtitle><entry><title>Automate your releases with the Jupyter Releaser 🚀</title><link href="https://jasongrout.github.io/medium-archive/pelican/posts/2024/automate-your-releases-with-the-jupyter-releaser/" rel="alternate"/><published>2024-10-28T18:09:00+00:00</published><updated>2024-10-28T18:09:00+00:00</updated><author><name>Jeremy Tuloup</name></author><id>tag:jasongrout.github.io,2024-10-28:/medium-archive/pelican/posts/2024/automate-your-releases-with-the-jupyter-releaser/</id><summary type="html">&lt;p&gt;Jupyter Releaser is a tool developed by the Jupyter team to streamline and standardize the release process across Jupyter projects&lt;/p&gt;
</summary><content type="html">&lt;p&gt;In the world of open-source software, releases ensure that the latest features, bug fixes, and improvements reach users promptly. However, the process of managing releases, especially for large projects, can be complex and error-prone. This is where &lt;strong&gt;Jupyter Releaser&lt;/strong&gt; comes into play.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Jupyter Releaser&lt;/strong&gt; is an automation tool developed by the Jupyter team to streamline and standardize the release process across Jupyter projects. Its goal is simple: reduce the manual burden on maintainers, ensure consistency, best practices, improve confidence, and eliminate the potential for human error.&lt;/p&gt;
&lt;h2 id="the-problem-manual-release-processes"&gt;😔 The Problem: Manual Release Processes&lt;/h2&gt;
&lt;p&gt;Many maintainers of libraries have experienced the pains of a manual release process:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Versioning Issues&lt;/strong&gt;: Manually updating version numbers across multiple files can lead to inconsistencies and errors.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Changelog Generation&lt;/strong&gt;: Creating and maintaining accurate changelogs can be time-consuming.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Publishing&lt;/strong&gt;: Whether it’s for PyPI or npm, the process involves multiple steps that are easy to mess up, and requires maintainers to manage their own credentials and tokens.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These pain points are not just frustrating; they can also delay the release of new versions and introduce issues that affect the end-users.&lt;/p&gt;
&lt;h2 id="the-solution-jupyter-releaser"&gt;🤩 The Solution: Jupyter Releaser&lt;/h2&gt;
&lt;p&gt;Jupyter Releaser automates and standardizes the release process, ensuring that everything from versioning to changelog generation and publishing is handled efficiently. It helps maintainers reduce toil and save time in the release process by enforcing best practices to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Automate a changelog for every release&lt;/li&gt;
&lt;li&gt;Verify the install and import of dist assets, such as Python wheels and npm packages, to ensure packaging is done correctly&lt;/li&gt;
&lt;li&gt;Commit a message with hashes of dist files&lt;/li&gt;
&lt;li&gt;Annotate the git tag in a standard format&lt;/li&gt;
&lt;li&gt;Create a GitHub release with a changelog entry, and automatically populate a changelog file&lt;/li&gt;
&lt;li&gt;Trigger a dry run publish on CI, to make sure the project is in a releasable state at any time&lt;/li&gt;
&lt;li&gt;Consistently publish final packages to PyPI and npm&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="getting-started-with-jupyter-releaser"&gt;➡️ Getting Started with Jupyter Releaser&lt;/h2&gt;
&lt;h2 id="step-0-set-up"&gt;Step 0: Set up&lt;/h2&gt;
&lt;p&gt;Setting up a repository to use the Jupyter Releaser involves a few steps which are listed on &lt;a href="https://jupyter-releaser.readthedocs.io/en/latest/how_to_guides/convert_repo_from_repo.html#checklist-for-adoption"&gt;this page of the documentation.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The first one is to create a GitHub app for the organization or profile that owns the repo, following &lt;a href="https://docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/about-creating-github-apps#github-apps-that-act-on-their-own-behalf"&gt;this guide&lt;/a&gt;. Then continue with the steps in the documentation to fill in the values for the app.&lt;/p&gt;
&lt;p&gt;To follow best practices it is also recommended to create a &lt;code&gt;release&lt;/code&gt; environment on the repo to add the &lt;code&gt;APP_ID&lt;/code&gt; variable and &lt;code&gt;APP_PRIVATE_KEY&lt;/code&gt; secret.&lt;/p&gt;
&lt;p&gt;If you publish a package to PyPI, you will also need to add a trusted publisher, which usually corresponds to the source repository.&lt;/p&gt;
&lt;p&gt;For npm, don’t forget to specify a &lt;code&gt;&amp;quot;publishConfig&amp;quot;: { &amp;quot;access&amp;quot;: &amp;quot;public&amp;quot; }&lt;/code&gt; to the &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Finally, copy the &lt;code&gt;prep-release.yml&lt;/code&gt; and &lt;code&gt;publish-release.yml&lt;/code&gt; workflows from the &lt;a href="https://github.com/jupyter-server/jupyter_releaser/tree/main/example-workflows"&gt;example-workflows&lt;/a&gt; folder to your repository.&lt;/p&gt;
&lt;p&gt;Quite a few steps, but you should now be ready to go! Fortunately this setup needs to be done only once per repository. If you release multiple projects hosted under the same organization, you can reuse the same GitHub app.&lt;/p&gt;
&lt;p&gt;Again, feel free to refer to the documentation for the &lt;a href="https://jupyter-releaser.readthedocs.io/en/latest/how_to_guides/convert_repo_from_repo.html#checklist-for-adoption"&gt;complete checklist&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="step-1-prep-release"&gt;Step 1: Prep Release&lt;/h2&gt;
&lt;p&gt;To start the release, go to the “Actions” tab of the repo and select “Step 1: Prep Release”. Open the “Run Workflow” dropdown and fill in the appropriate parameters.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Running the “Prep Release” workflow for a JupyterLab extension (speed x5)" src="https://jasongrout.github.io/medium-archive/pelican/posts/2024/automate-your-releases-with-the-jupyter-releaser/images/001-1_FbGYR9TzYpyDBD7HRmHMyg.mp4" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Running the “Prep Release” workflow for a JupyterLab extension (speed x5)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Running this workflow will create a draft GitHub release with the generated changelog for the new version. You can edit the changelog by editing the release if needed. &lt;strong&gt;Don’t forget to click on “Save Draft” and not “Publish Release” if you make any changes!&lt;/strong&gt; (the releaser will do the publishing in the next step)&lt;/p&gt;
&lt;h2 id="step-2-publish-release"&gt;Step 2: Publish Release&lt;/h2&gt;
&lt;p&gt;If the changelog and draft GitHub release look good, go back to the “Actions” tab and click on “Step 2: Prep Release”.&lt;/p&gt;
&lt;p&gt;In most cases, you can leave the fields empty and the releaser will automatically publish the GitHub Release previously created.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Publishing the extension (speed x5)" src="https://jasongrout.github.io/medium-archive/pelican/posts/2024/automate-your-releases-with-the-jupyter-releaser/images/002-1_ViuKQhOWZPVULL6kQOPwPw.mp4" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Publishing the extension (speed x5)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Congrats, you just made your first release with the Jupyter Releaser! 🎉&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2024/automate-your-releases-with-the-jupyter-releaser/images/003-1_GRhRcQOAClvvQbBvWFkOjg.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;h2 id="going-further"&gt;Going further&lt;/h2&gt;
&lt;p&gt;Jupyter Releaser also provides a set of hooks and options, such as running an arbitrary command after a given step, or providing a custom strategy for versioning packages. This allows for great flexibility in more complex repositories.&lt;/p&gt;
&lt;p&gt;Check out the &lt;a href="https://jupyter-releaser.readthedocs.io/"&gt;documentation&lt;/a&gt; for more information.&lt;/p&gt;
&lt;h2 id="how-jupyter-releaser-fits-into-the-jupyter-ecosystem-and-beyond"&gt;🌐 How Jupyter Releaser Fits into the Jupyter Ecosystem, and beyond&lt;/h2&gt;
&lt;p&gt;Releases are moments of &lt;a href="https://www.youtube.com/watch?v=9jK-NcRmVcw"&gt;celebration&lt;/a&gt; in any project, a chance to deliver new features, enhancements, and bug fixes to users. But they shouldn’t be a source of stress. Jupyter Releaser makes releases smoother for maintainers across the Jupyter ecosystem and beyond.&lt;/p&gt;
&lt;p&gt;Jupyter Releaser is now used for most of the projects in the Jupyter stack with great success, such a JupyterLab, Jupyter Notebook, Jupyter Server, ipykernel, and many more. It greatly simplified the previously complex JupyterLab release process, which used to take 30 minutes, and now boils down to clicking on just a few buttons on GitHub and waiting for the workflows to complete.&lt;/p&gt;
&lt;p&gt;Although its name has “Jupyter” in it, the releaser is not limited to releasing Jupyter related projects only, If you maintain a simple Python or npm package, you can also use the Jupyter Releaser to publish your package(s) to PyPI, npm or both!&lt;/p&gt;
&lt;p&gt;In the end, Jupyter Releaser can simplify your workflow and save you valuable time. Give it a try and check out the repo &lt;a href="https://github.com/jupyter-server/jupyter_releaser"&gt;here&lt;/a&gt; to see how it can make software releases a breeze!&lt;/p&gt;
&lt;h2 id="acknowledgments"&gt;🙏 Acknowledgments&lt;/h2&gt;
&lt;p&gt;Many thanks to &lt;a href="https://github.com/blink1073"&gt;Steven Silvester&lt;/a&gt; for creating the Jupyter Releaser and helping with its adoption across the Jupyter stack.&lt;/p&gt;
&lt;p&gt;Jupyter Releaser is an official Jupyter project, and the Jupyter Releaser maintainers would also like to thank the contributors in the Jupyter community who help improve the tool and provide feedback ❤️&lt;/p&gt;
</content><category term="DevOps"/><category term="releases"/></entry><entry><title>Accurately counting Daily, Weekly &amp; Monthly active users on JupyterHub</title><link href="https://jasongrout.github.io/medium-archive/pelican/posts/2023/accurately-counting-daily-weekly-monthly-active-users/" rel="alternate"/><published>2023-03-27T09:16:00+00:00</published><updated>2023-03-27T09:16:00+00:00</updated><author><name>yuvipanda</name></author><id>tag:jasongrout.github.io,2023-03-27:/medium-archive/pelican/posts/2023/accurately-counting-daily-weekly-monthly-active-users/</id><summary type="html">&lt;p&gt;Being able to say ‘we served X unique users over the last month’ (the Monthly Active User metric) is very helpful when advocating for…&lt;/p&gt;
</summary><content type="html">&lt;p&gt;Being able to say ‘we served X unique users over the last month’ (the &lt;a href="https://en.wikipedia.org/wiki/Active_users"&gt;Monthly Active User metric&lt;/a&gt;) is very helpful when advocating for resources for a JupyterHub your organization is running. However, until now, figuring out that number &lt;em&gt;accurately&lt;/em&gt; has been difficult, requiring keeping and analysing JupyterHub logs.&lt;/p&gt;
&lt;p&gt;That changes with JupyterHub 3.1! &lt;a href="https://github.com/jupyterhub/jupyterhub/pull/4214"&gt;This Pull Request&lt;/a&gt; adds daily, weekly, and monthly active user metrics to JupyterHub, accessible via the prometheus interface by hitting the &lt;code&gt;/metrics&lt;/code&gt; URL on your JupyterHub. These metrics are calculated by JupyterHub itself, and are pretty accurate as it already keeps track of when a user was last active. This relies metric on each JupyterHub user matching an actual user, so if you are using your JupyterHub deployment purely as an API with ephemeral users (as &lt;a href="https://github.com/jupyterhub/binderhub/"&gt;binderhub&lt;/a&gt; does, for example) or delete inactive users, these will not be useful numbers.&lt;/p&gt;
&lt;p&gt;Ideally, you should have a &lt;a href="https://prometheus.io/"&gt;prometheus&lt;/a&gt; instance to scrape and store metrics from your JupyterHub over time, so you can track this metric over time. However, in a pinch, you can also go directly to &lt;code&gt;https://&amp;lt;your-hub-url&amp;gt;/hub/api/metrics&lt;/code&gt; and see the current value of these metrics. They are in the &lt;a href="https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md"&gt;prometheus exposition format&lt;/a&gt;, but you can look for something like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# HELP jupyterhub_active_users number of users who were active in the given time period&lt;/span&gt;
&lt;span class="c1"&gt;# TYPE jupyterhub_active_users gauge&lt;/span&gt;
jupyterhub_active_users&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;period&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;24h&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;610&lt;/span&gt;.0
jupyterhub_active_users&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;period&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;7d&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2800&lt;/span&gt;.0
jupyterhub_active_users&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;period&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;30d&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4526&lt;/span&gt;.0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This denotes that this particular hub had 610 daily active users (active over the last 24 hours), 2800 weekly users (over last 7d) and 4526 monthly ones (over the last 30 days). Very helpful if you want to quickly add numbers to a report :)&lt;/p&gt;
&lt;p&gt;Note that depending on your hub’s configuration, access to the &lt;code&gt;/metrics&lt;/code&gt; endpoint might &lt;a href="https://jupyterhub.readthedocs.io/en/stable/api/app.html#jupyterhub.app.JupyterHub.authenticate_prometheus"&gt;require authentication&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you &lt;em&gt;do&lt;/em&gt; have a prometheus installation for your JupyterHub, you may benefit from deploying &lt;a href="https://github.com/jupyterhub/grafana-dashboards"&gt;the JupyterHub Grafana Dashboards&lt;/a&gt;. These are targeted at installations of &lt;a href="https://z2jh.jupyter.org"&gt;zero-to-jupyterhub on kubernetes&lt;/a&gt;, and provide a lot of useful usage &amp;amp; diagnostic information.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Grafana Dashboard showing “Hub Usage Stats” for a JupyterHub deployment. Four panels, clockwise: Current Active Users, Daily Active Users, Weekly Active Users, Monthly Active Users." src="https://jasongrout.github.io/medium-archive/pelican/posts/2023/accurately-counting-daily-weekly-monthly-active-users/images/001-0_VYHSKwQ5Jp-L3ArS.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Grafana Dashboard showing “Hub Usage Stats” for a JupyterHub deployment. Four panels, clockwise: Current Active Users, Daily Active Users, Weekly Active Users, Monthly Active Users.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Happy report writing!&lt;/p&gt;
</content><category term="DevOps"/><category term="JupyterHub"/></entry><entry><title>Introducing Jupyter Scheduler</title><link href="https://jasongrout.github.io/medium-archive/pelican/posts/2022/introducing-jupyter-scheduler/" rel="alternate"/><published>2022-12-14T15:26:00+00:00</published><updated>2023-02-01T18:20:00+00:00</updated><author><name>Jason Weill</name></author><id>tag:jasongrout.github.io,2022-12-14:/medium-archive/pelican/posts/2022/introducing-jupyter-scheduler/</id><summary type="html">&lt;p&gt;The Open Source Jupyter team at AWS is proud to announce the release of Jupyter Scheduler, a JupyterLab extension that can run your Jupyter…&lt;/p&gt;
</summary><content type="html">&lt;p&gt;The Open Source Jupyter team at AWS is proud to announce the release of &lt;a href="https://github.com/jupyter-server/jupyter-scheduler/"&gt;Jupyter Scheduler&lt;/a&gt;, a JupyterLab extension that can run your Jupyter notebooks in the background. Jupyter Scheduler was developed from the start as an open-source project under the umbrella of the Jupyter project and governance. You can set up your notebooks to run once or on a schedule. By default, Jupyter Scheduler runs notebooks on your local system where Jupyter Server is installed; you can also extend the scheduler to run your notebooks in the cloud, on an on-premise cluster, on JupyterHub, or even on supercomputers.&lt;/p&gt;
&lt;p&gt;We hope you’ll find Jupyter Scheduler useful for running notebooks periodically, such as daily or weekly reports. You can install it in JupyterLab by running &lt;code&gt;pip install jupyter_scheduler&lt;/code&gt;, then restarting JupyterLab. You can create &lt;strong&gt;jobs&lt;/strong&gt; (single runs of an individual notebook) and &lt;strong&gt;job definitions&lt;/strong&gt; (scheduled recurring notebook jobs) in two ways. You can right-click on a notebook in the file browser and choose “Create Notebook Job” from the context menu, or you can use the new toolbar button in an open notebook file.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Screen shot of the file browser showing the context menu for a notebook" src="https://jasongrout.github.io/medium-archive/pelican/posts/2022/introducing-jupyter-scheduler/images/001-1_nUcvmd-_anAtX4rT5MGPfA.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;When you right-click on a notebook file in the file browser, a “Create Notebook Job” option appears in the context menu.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img alt="Screen shot of a notebook showing a “Create Notebook Job” button in the toolbar" src="https://jasongrout.github.io/medium-archive/pelican/posts/2022/introducing-jupyter-scheduler/images/002-1_OOLLSTNnsaCLnX_g46Rq9A.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;In a notebook, the “Create Notebook Job” option appears on the right of the toolbar.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;You can give your notebook job or job definition a name, choose an environment to run it in, select different output formats, and provide parameters that are set as local variables when your notebook gets executed. This parameterized execution is similar to &lt;a href="https://papermill.readthedocs.io/en/latest/"&gt;Papermill&lt;/a&gt;; Jupyter Scheduler provides a UI for managing and accessing the outputs of your notebook jobs inside JupyterLab.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Screen shot of the Create Job form with sample input values provided" src="https://jasongrout.github.io/medium-archive/pelican/posts/2022/introducing-jupyter-scheduler/images/003-1_YGJD3HWvIenbouO7R8bJYg.jpeg" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;You can use the Create Job form to run a notebook once or to create a job definition that runs on a schedule.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;In addition, if you select “Run on a schedule,” you can run your job on a recurring schedule. The scheduler provides a few useful shortcuts to, for example, run your notebook at a specified interval by the minute, hour, day or week. You can also specify a custom schedule in &lt;a href="https://www.man7.org/linux/man-pages/man5/crontab.5.html"&gt;crontab format&lt;/a&gt;.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Screen shot of the “run on a schedule” user interface in the Create Job form, with the “weekday” interval selected" src="https://jasongrout.github.io/medium-archive/pelican/posts/2022/introducing-jupyter-scheduler/images/004-1_TxAfPAOcW65yMfvmMiCi9A.jpeg" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;You can schedule a job definition using preset intervals, such as every weekday.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img alt="Screen shot of the “run on a schedule” user interface in the Create Job form, with the custom schedule” interval selected" src="https://jasongrout.github.io/medium-archive/pelican/posts/2022/introducing-jupyter-scheduler/images/005-1_pBEzuVXkDtPiopR_5DxQsA.jpeg" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;You can also select “custom schedule” and specify a cron expression for the notebook job definition’s schedule.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Once you’ve created a job or job schedule, you can use the “Notebook Jobs” icon on the launcher to view lists of jobs and job definitions. Each item’s name links to a detail view.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Screen shot of JupyterLab launcher showing “Notebook Jobs” button in the “Other” section" src="https://jasongrout.github.io/medium-archive/pelican/posts/2022/introducing-jupyter-scheduler/images/006-1_Bwbm_RIsRiZz-zfCsv43Hg.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;With the Jupyter Scheduler extension installed, JupyterLab’s launcher screen shows a “Notebook Jobs” button in the “Other” section. Click on it to view a list of jobs or job definitions.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img alt="Screen shot of the List Notebook Jobs view, showing many jobs, with links and information for each" src="https://jasongrout.github.io/medium-archive/pelican/posts/2022/introducing-jupyter-scheduler/images/007-1_RqogrC-XJlQLQhDJ-wml6A.jpeg" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;The list view shows a list of notebook jobs and a list of notebook job definitions. Each one includes links to view details, to download files, and buttons to take quick actions on them.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img alt="Screen shot of the Job Detail view, showing additional information and controls for a single job" src="https://jasongrout.github.io/medium-archive/pelican/posts/2022/introducing-jupyter-scheduler/images/008-1_BpShpinXD9ifmkfzeT30Cg.jpeg" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;The Job Detail view shows all available details for one job or one job definition.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;When one of your jobs has completed, you can download its files from the list or detail view. When your notebook runs, the scheduler saves a copy of the input file, so that even if you later change the file, it captures the state of the notebook at the time your job ran. Jupyter Scheduler uses unique names for the input and output files so that rerunning the same notebook produces new files every time.&lt;/p&gt;
&lt;p&gt;Jupyter Scheduler uses a REST API to create, update, delete and manage jobs and job definitions. You can also customize Jupyter Scheduler by plugging in your own scheduler class, for example, to replace the existing SQLite database or to run jobs in the cloud. AWS SageMaker Studio actually uses Jupyter Scheduler, with custom classes, as part of its automated notebooks feature, &lt;a href="https://aws.amazon.com/blogs/aws/next-generation-sagemaker-notebooks-now-with-built-in-data-preparation-real-time-collaboration-and-notebook-automation/"&gt;announced&lt;/a&gt; earlier this year. See the &lt;a href="https://jupyter-scheduler.readthedocs.io/en/latest/operators/index.html"&gt;Jupyter Scheduler operator documentation&lt;/a&gt; for more information about how to write and use your own scheduler.&lt;/p&gt;
&lt;p&gt;We’d love to hear what you think about Jupyter Scheduler! You can check out &lt;a href="https://jupyter-scheduler.readthedocs.io/"&gt;Jupyter Scheduler’s documentation on ReadTheDocs&lt;/a&gt;. Please &lt;a href="https://github.com/jupyter-server/jupyter-scheduler/issues"&gt;open issues&lt;/a&gt; with ideas for enhancements and about any bugs you find. If you’d like to contribute code, see the &lt;a href="https://jupyter-scheduler.readthedocs.io/en/latest/contributors/index.html"&gt;Jupyter Scheduler contributors guide&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;This article does not represent the official position of amazon.com or any of its subsidiaries.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;About the authors&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2022/introducing-jupyter-scheduler/images/009-1_7kFoTl09bhq0WbJbO2kFtw.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/andrii-i/"&gt;Andrii Ieroshenko&lt;/a&gt; is a Software Development Engineer at AWS. Previously, Andrii worked on the &lt;a href="https://github.com/jupytercalpoly/jupyterlab-notifications"&gt;Notifications&lt;/a&gt; project as a part of the Jupyter Cal Poly student internship program.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2022/introducing-jupyter-scheduler/images/010-1_3REz_2GLxridP9Uv6ZJjmQ.jpeg" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/3coins"&gt;Piyush Jain&lt;/a&gt; is a Software Development Engineer at AWS working on open source contributions to Project Jupyter. He is also a member of the Jupyter Server team, actively participating in maintenance and development of Jupyter server project.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2022/introducing-jupyter-scheduler/images/011-1_HrkANk8wyWRqXocQdnQ5mA.jpeg" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/dlqqq/"&gt;David Qiu&lt;/a&gt; is a Software Development Engineer at AWS working on Project Jupyter. Previously, David was researching physical chemistry at the University of Illinois at Urbana-Champaign.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2022/introducing-jupyter-scheduler/images/012-1_E40JRdqbPa7diRn6TXoCMQ.jpeg" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/JasonWeill"&gt;Jason Weill&lt;/a&gt; is a Senior Front-End Engineer at Amazon Web Services; a contributor to JupyterLab, Jupyter Governance, and Jupyter Scheduler; and a member of the JupyterLab Council, the Jupyter Security Working Group, and the Jupyter Diversity, Equity, and Inclusion Standing Committee.&lt;/p&gt;
</content><category term="DevOps"/><category term="Jupyter Server"/><category term="JupyterLab"/></entry><entry><title>How I automated authorised cloud deployments from Pull Requests with GitHub Actions</title><link href="https://jasongrout.github.io/medium-archive/pelican/posts/2021/how-i-automated-authorised-cloud-deployments-from-pull/" rel="alternate"/><published>2021-11-22T18:30:00+00:00</published><updated>2021-11-22T18:30:00+00:00</updated><author><name>Sarah Gibson</name></author><id>tag:jasongrout.github.io,2021-11-22:/medium-archive/pelican/posts/2021/how-i-automated-authorised-cloud-deployments-from-pull/</id><summary type="html">&lt;p&gt;I recently did some work on the mybinder.org deployment infrastructure to solve a problem with testing Pull Requests before deployment. It…&lt;/p&gt;
</summary><content type="html">&lt;p&gt;I recently did some work on the mybinder.org deployment infrastructure to solve a problem with testing Pull Requests before deployment. It had not been possible to test Pull Requests on our staging deployment because our automated workflows don’t have access to secrets. This resulted in my writing the &lt;a href="https://github.com/sgibson91/test-this-pr-action"&gt;test-this-pr action&lt;/a&gt; and this blog is a retrospective of what I learned over that process.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="what-was-the-problem-we-were-trying-to-solve"&gt;&lt;strong&gt;What was the problem we were trying to solve?&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;It is generally considered best practice to have a staging environment when running complex applications or platforms that have a large userbase, such as &lt;a href="https://mybinder.org"&gt;mybinder.org&lt;/a&gt;. This provides developers a space to test infrastructure changes safely with the knowledge that users won’t be affected should anything go wrong. The deployment configuration for mybinder.org is managed &lt;a href="https://github.com/jupyterhub/mybinder.org-deploy"&gt;in a GitHub repository&lt;/a&gt; and deploys are handled &lt;a href="https://github.com/jupyterhub/mybinder.org-deploy/blob/master/.github/workflows/cd.yml"&gt;in a CI/CD pipeline run on GitHub Actions&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The mybinder.org operating team’s usual workflow will be very familiar to anyone working as part of a distributed development team: fork the main repository, do some development (fix a bug or implement a feature), and then open a Pull Request (PR) back to the default branch of the main repository. The difficulty arises when we want to test the PR on our staging infrastructure before merging and deploying for real.&lt;/p&gt;
&lt;p&gt;Various secrets are required to make a deployment, even to our staging environment. These include a git-crypt secret that decrypts certain sensitive files and credentials to push Docker images to the repositories connected to our clusters, among others. These are stored as encrypted repository secrets so they are available to GitHub Actions when our CD pipeline runs. However by default, repository secrets are not made available to PRs from forks, which introduces a problem for the workflow I described above.&lt;/p&gt;
&lt;p&gt;So to summarise our problem:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Contributors often make changes to the code in their &lt;em&gt;fork&lt;/em&gt; of the repository&lt;/li&gt;
&lt;li&gt;Testing any change requires deploying to an actively-running staging cluster&lt;/li&gt;
&lt;li&gt;The secrets needed to make those deploys are only available to the original repository, not to a forked repository&lt;/li&gt;
&lt;li&gt;So we needed a way to trigger a test deployment on changes &lt;em&gt;from a forked repository&lt;/em&gt; but that ran &lt;em&gt;in the origin repository&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="what-solutions-did-we-already-have-in-place"&gt;What solutions did we already have in place?&lt;/h2&gt;
&lt;p&gt;We had one team practice, and one technical solution to address this issue, both of which didn’t quite do what we wanted.&lt;/p&gt;
&lt;p&gt;One solution was a team deployment practice of “just merge the PR anyway and file a revert PR if things go wrong”. Our CI/CD pipeline is broken into two jobs: deploy to staging infrastructure, and a matrix deployment to each production cluster in &lt;a href="https://mybinder-sre.readthedocs.io/en/latest/operation_guide/federation.html"&gt;the BinderHub federation&lt;/a&gt;. The production jobs require the staging job to pass first before they are triggered. This means that if the deploy to staging fails, the changes will never propagate to our production clusters.&lt;/p&gt;
&lt;p&gt;Another solution was a “test-staging” label we could apply to open PRs. This triggered the deploy to staging job without a merge. However, this only worked for PRs opened from branches &lt;em&gt;within the origin repository&lt;/em&gt;, and as mentioned above, this is not a common workflow for maintainers. Instead they open PRs from forked repositories, which do not have the proper secrets to deploy to staging.&lt;/p&gt;
&lt;h2 id="what-other-solutions-were-available"&gt;What other solutions were available?&lt;/h2&gt;
&lt;p&gt;We briefly considered &lt;a href="https://github.com/imjohnbo/ok-to-test"&gt;ok-to-test&lt;/a&gt; but were put off by the requirement for a GitHub App. However as we will learn, it was probably a mistake to dismiss this so quickly!&lt;/p&gt;
&lt;h2 id="what-did-i-end-up-implementing"&gt;What did I end up implementing?&lt;/h2&gt;
&lt;p&gt;And so I built my first GitHub Action!&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://github.com/sgibson91/test-this-pr-action"&gt;test-this-pr action&lt;/a&gt; allows you to trigger a GitHub Action &lt;em&gt;from the origin repository&lt;/em&gt; when an authorised user adds a comment to the PR of a forked repository.&lt;/p&gt;
&lt;p&gt;test-this-pr is triggered when a user leaves a comment on the PR you’d like to test, but only if the commenter has the appropriate permissions on the repository, e.g. MEMBER. This protects us from malicious code being run automatically on our staging deployment and ensures that project maintainers have vetted the code in some way before triggering the test.&lt;/p&gt;
&lt;p&gt;When triggered, test-this-pr uses a small Python script that queries the GitHub API to do the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It creates a new branch in the origin repository that uses the merge reference from the PR.&lt;/li&gt;
&lt;li&gt;This triggers the test suite in the new branch, because we have configured the origin repository to automatically run deployment tests on branches that match a particular name pattern (in our case, test-this-pr/*, see below)&lt;/li&gt;
&lt;li&gt;Adds comments on the original PR with a link to the newly-running CI test logs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This script is wrapped up into a Dockerfile, which allows GitHub Actions to run the action using its docker runner.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="The test-this-workflow bot in action." src="https://jasongrout.github.io/medium-archive/pelican/posts/2021/how-i-automated-authorised-cloud-deployments-from-pull/images/001-0_CxrmnyCYbWVYXn0B.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;An example of the &lt;code&gt;test-this-pr&lt;/code&gt; workflow in action.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="extra-configuration-needed-to-use-this-action"&gt;Extra configuration needed to use this action&lt;/h2&gt;
&lt;p&gt;There were a few extra pieces I configured that couldn’t (and shouldn’t!) be implemented in the test-this-pr action, but contribute to the overall workflow.&lt;/p&gt;
&lt;p&gt;I also &lt;a href="https://github.com/jupyterhub/mybinder.org-deploy/blob/5442082cbb408f889f433792c1abf4ede02e67ec/.github/workflows/cd.yml#L318-L341"&gt;created a CI job that polls the running staging job&lt;/a&gt;. Upon completion, it posts a second comment to the original PR stating whether the test passed or failed. This had to be separate from the test-this-pr action since this job needs to run within the same workflow run in order to access the status of the staging job.&lt;/p&gt;
&lt;p&gt;Once the pass/fail status of the staging job has been reported, the branch created by test-this-pr is then deleted. We decided this was the best path forward so that the original PR remained the “single source of truth” and maintainers wouldn’t be confused by extra branches in the repository.&lt;/p&gt;
&lt;h2 id="for-reference-if-youd-like-to-understand-this-implementation"&gt;For reference if you’d like to understand this implementation&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/jupyterhub/mybinder.org-deploy/blob/master/.github/workflows/test-this-pr.yml"&gt;Here is our configuration to trigger test-this-pr&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/jupyterhub/mybinder.org-deploy/blob/master/.github/workflows/cd.yml#L17-L21"&gt;Here we configure our deployment action to run on branches with test-this-pr/*&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/jupyterhub/mybinder.org-deploy/blob/5442082cbb408f889f433792c1abf4ede02e67ec/.github/workflows/cd.yml#L318-L341"&gt;Here is the CI job that polls the running staging job and posts a comment with its status before deleting the branch&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="what-did-i-learn-along-the-way"&gt;What did I learn along the way?&lt;/h2&gt;
&lt;h3 id="there-is-a-lot-you-can-do-with-the-github-rest-api"&gt;There is A LOT you can do with the GitHub REST API&lt;/h3&gt;
&lt;p&gt;I had originally configured the Python script to shell out to git for tasks like creating the new branch and pulling the PR merge ref, and this worked fine when I ran Python locally. However when running the docker container, I struggled to authorise git correctly, even when passing my Personal Access Token. And so I made the decision to switch to the &lt;a href="https://docs.github.com/en/rest"&gt;GitHub REST API&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;While a lot of GitHub native features can be accessed and manipulated via the API, some git-specific actions can be achieved as well via the &lt;a href="https://docs.github.com/en/rest/reference/git"&gt;git database endpoint&lt;/a&gt;, which is the endpoint to use to create branches, commits, and much more. This section of the API might require a deeper dive into the inner-workings of git, but can unlock a lot of automation potential once you have your mental model!&lt;/p&gt;
&lt;p&gt;Switching to the GitHub REST API from the git command line meant that I no longer needed to install git into my docker container or locally clone the repository to carry out the actions, making my image much smaller.&lt;/p&gt;
&lt;h3 id="triggering-github-actions-from-other-actions"&gt;Triggering GitHub Actions from other Actions&lt;/h3&gt;
&lt;p&gt;You can’t trigger another GitHub Actions workflow from an event that was authorised with the GITHUB_TOKEN from within a previous workflow. This is why the comments left by the test-this-pr workflows in the Binder repository come from my account as I had to provide a Personal Access Token for the staging job to be correctly triggered.&lt;/p&gt;
&lt;p&gt;The downside of this is that I’m automatically subscribed to notifications for any Binder PR where someone is using test-this-pr. It’s not the worst situation in the world as I have email filters set up, but also not ideal.&lt;/p&gt;
&lt;p&gt;This is also why &lt;a href="https://github.com/imjohnbo/ok-to-test"&gt;ok-to-test&lt;/a&gt; recommends setting up a GitHub App for authorisation, so you don’t have to sacrifice your own account and all the notifications that come with it!&lt;/p&gt;
&lt;p&gt;However, this “actions triggering actions” scenario could be smoothed out by using the upcoming &lt;a href="https://docs.github.com/en/actions/learn-github-actions/reusing-workflows"&gt;Reusable Workflows&lt;/a&gt; beta.&lt;/p&gt;
&lt;h3 id="building-actions-with-dockerfiles"&gt;Building Actions with Dockerfiles&lt;/h3&gt;
&lt;p&gt;Providing a Dockerfile with a custom Action will allow you to write your Action in whatever language you like. However, it is one of the slowest ways to implement custom Actions because GitHub’s runner will build the image every time the workflow is triggered.&lt;/p&gt;
&lt;h2 id="what-would-i-do-differently-now"&gt;What would I do differently now?&lt;/h2&gt;
&lt;h3 id="setup-a-github-app-for-authorisation"&gt;Setup a GitHub App for authorisation&lt;/h3&gt;
&lt;p&gt;If only to spare my inbox from the notifications! 😂&lt;/p&gt;
&lt;h3 id="move-away-from-a-python-implementation"&gt;Move away from a Python implementation&lt;/h3&gt;
&lt;p&gt;The test-this-pr action ultimately ended up being a handful of calls to the GitHub REST API. As opposed to maintaining a Python script and Dockerfile, I would probably reimplement this using the &lt;a href="https://github.com/marketplace/actions/github-script"&gt;github-script action&lt;/a&gt;, which is a JavaScript wrapper for the GitHub API that can be directly run from GitHub Actions. This will be less code to maintain and the test-this-pr action itself could then be reworked as a &lt;a href="https://docs.github.com/en/actions/creating-actions/creating-a-composite-action"&gt;composite action&lt;/a&gt; which would build faster than the current Docker-based implementation.&lt;/p&gt;
&lt;h2 id="closing-remarks"&gt;Closing Remarks&lt;/h2&gt;
&lt;figure&gt;
&lt;img alt="Members of the mybinder operating team discussing test-this-pr in gitter" src="https://jasongrout.github.io/medium-archive/pelican/posts/2021/how-i-automated-authorised-cloud-deployments-from-pull/images/002-1_8XZ7K2yXsNdRUTUPhfCYwA.jpeg" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Members of the mybinder operating team discussing test-this-pr in gitter&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;While there is a lot I would change about test-this-pr, including fundamental changes that would make it look radically different, I needed to go on this developmental journey to gain the better understanding of GitHub Actions that I have now. And hopefully by sharing that journey, other folk can learn from it!&lt;/p&gt;
&lt;p&gt;Also the mybinder operating team love using this feature and it’s adding value to our development workflow, so I don’t see a need to dive back in and start changing things again right now 🙂&lt;/p&gt;
&lt;p&gt;You can read the source code for &lt;a href="https://github.com/sgibson91/test-this-pr-action"&gt;&lt;code&gt;test-this-pr&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/jupyterhub/mybinder.org-deploy/blob/master/.github/workflows/cd.yml"&gt;mybinder.org’s deployment workflow&lt;/a&gt; in full.&lt;/p&gt;
</content><category term="DevOps"/></entry><entry><title>How we automated mybinder.org dependency upgrades in 10 steps</title><link href="https://jasongrout.github.io/medium-archive/pelican/posts/2019/automating-mybinder-org-dependency-upgrades-in-10-steps/" rel="alternate"/><published>2019-07-17T15:34:00+00:00</published><updated>2019-07-17T15:34:00+00:00</updated><author><name>Chris Hench</name></author><id>tag:jasongrout.github.io,2019-07-17:/medium-archive/pelican/posts/2019/automating-mybinder-org-dependency-upgrades-in-10-steps/</id><summary type="html">&lt;p&gt;BinderHub and repo2docker are key components of the service mybinder.org; depending on them for the latest functionality demands that…&lt;/p&gt;
</summary><content type="html">&lt;p&gt;&lt;a href="https://github.com/jupyterhub/binderhub"&gt;BinderHub&lt;/a&gt; and &lt;a href="https://github.com/jupyter/repo2docker"&gt;repo2docker&lt;/a&gt; are key components of the service at &lt;a href="https://mybinder.org/"&gt;mybinder.org&lt;/a&gt;. In order to give Binder users the best experience, the Binder SRE team must continuously upgrade the version of these tools that mybinder.org uses. To avoid merging in massive updates at irregular intervals, it is desirable to merge updates in frequent intervals of smaller changes in order to more easily identify any breaking changes from the dependency upgrades.&lt;/p&gt;
&lt;p&gt;While this process only takes a few minutes following processes outlined in the “&lt;a href="https://mybinder-sre.readthedocs.io/en/latest/"&gt;Site Reliability Guide&lt;/a&gt;,” it is prone to human error (e.g., remembering to use the right SHA in upgrading the packages), and the team must remember to regularly do it in the first place. In the interest of automation, the Binder team decided to use a bot to relieve this burden, and we’ve decided to highlight its functionality in this blog post!&lt;/p&gt;
&lt;h2 id="what-does-the-mybinderorg-upgrade-bot-do"&gt;What does the mybinder.org upgrade bot do?&lt;/h2&gt;
&lt;p&gt;The upgrade bot should automatically update the versions of BinderHub and repo2docker that are deployed on mybinder.org. These are defined &lt;a href="https://github.com/jupyterhub/mybinder.org-deploy/blob/master/mybinder/values.yaml#L64"&gt;in the mybinder.org helm chart&lt;/a&gt;. To check whether an upgrade is needed, we want the bot to first “diff” the latest commit hash for both repo2docker and BinderHub repos against the deployed versions in the &lt;a href="http://mybinder.org/"&gt;mybinder.org&lt;/a&gt; repo. If either or both are different, the upgrade bot does the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fork the &lt;a href="https://github.com/jupyterhub/mybinder.org-deploy"&gt;mybinder.org-deploy&lt;/a&gt; repo&lt;/li&gt;
&lt;li&gt;Clone the fork locally&lt;/li&gt;
&lt;li&gt;Checkout a new branch for the bump&lt;/li&gt;
&lt;li&gt;Make the appropriate edit to update the commit hash in the &lt;a href="http://mybinder.org/"&gt;mybinder.org&lt;/a&gt; fork repo&lt;/li&gt;
&lt;li&gt;Add and commit the change&lt;/li&gt;
&lt;li&gt;Push to the branch in the forked repo&lt;/li&gt;
&lt;li&gt;Create a PR to the main &lt;a href="http://mybinder.org/"&gt;mybinder.org&lt;/a&gt; repo&lt;/li&gt;
&lt;li&gt;Remove the locally cloned repo&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Additionally, it would be ideal if the bot could update an existing PR instead of creating new ones for the version bumps. We’d also like to provide some information in the comments of the PR as to what high level changes were made so we have some idea about what we’re merging in.&lt;/p&gt;
&lt;p&gt;Here’s what we’re aiming for. The PR body:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2019/automating-mybinder-org-dependency-upgrades-in-10-steps/images/001-0_Q2M1l10DYVxDsTsl.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;The PR diff:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2019/automating-mybinder-org-dependency-upgrades-in-10-steps/images/002-0_eMFYBJMC-Rktt1T3.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;Now that we’ve broken it down a bit, let’s write up some Python code. Once we have a functioning script, we can worry about how we will run this in the cloud (cron job vs. web app).&lt;/p&gt;
&lt;h2 id="writing-the-bot"&gt;Writing the bot&lt;/h2&gt;
&lt;p&gt;If you don’t care about the step-by-step, you can skip to the &lt;a href="https://github.com/henchbot/mybinder.org-upgrades/blob/master/henchbot.py"&gt;final version of the code&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In the interest of linear understanding and simplicity for a bot-writing tutorial, the step-by-step below will not write functions or classes but just list the raw code necessary to carry out the tasks. The final version of the code linked above is one way to refactor it.&lt;/p&gt;
&lt;h2 id="step-1-retrieve-current-deployed-mybinderorg-dependency-versions"&gt;Step 1: Retrieve current deployed &lt;a href="http://mybinder.org/"&gt;mybinder.org&lt;/a&gt; dependency versions&lt;/h2&gt;
&lt;p&gt;The first step is to see if any changes are necessary in the first place. Fortunately, &lt;a href="https://github.com/choldgraf"&gt;@choldgraf&lt;/a&gt; had already made a &lt;a href="https://github.com/jupyterhub/mybinder.org-deploy/blob/master/scripts/list_new_commits.py"&gt;script&lt;/a&gt; to do this.&lt;/p&gt;
&lt;p&gt;To find the current live commit SHA for BinderHub in &lt;a href="http://mybinder.org/"&gt;mybinder.org&lt;/a&gt;, we simply check the &lt;a href="https://raw.githubusercontent.com/jupyterhub/mybinder.org-deploy/master/mybinder/requirements.yaml"&gt;&lt;code&gt;requirements.yaml&lt;/code&gt;&lt;/a&gt; file. We’ll need Python’s &lt;code&gt;yaml&lt;/code&gt; and &lt;code&gt;requests&lt;/code&gt; modules to make the GET request and parse the yaml in the response. Note that this is also conveniently the file we’d want to change to upgrade the version.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;yaml&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;safe_load&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;load&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;url_requirements&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;https://raw.githubusercontent.com/jupyterhub/mybinder.org-deploy/master/mybinder/requirements.yaml&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;requirements&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url_requirements&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;binderhub_dep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ii&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ii&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;requirements&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;dependencies&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ii&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;name&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;binderhub&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;bhub_live&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;binderhub_dep&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;version&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bhub_live&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Similarly, for repo2docker, we check the &lt;a href="http://mybinder.org/"&gt;mybinder.org&lt;/a&gt; &lt;a href="https://raw.githubusercontent.com/jupyterhub/mybinder.org-deploy/master/mybinder/values.yaml"&gt;&lt;code&gt;values.yaml&lt;/code&gt;&lt;/a&gt; file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;url_helm_chart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;https://raw.githubusercontent.com/jupyterhub/mybinder.org-deploy/master/mybinder/values.yaml&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;helm_chart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url_helm_chart&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;helm_chart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;helm_chart&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r2d_live&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;helm_chart&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;binderhub&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;config&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;BinderHub&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;build_image&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;:&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r2d_live&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Let’s store these SHAs in a dictionary we can use for later reference:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;commit_info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
               &lt;span class="s1"&gt;&amp;#39;repo2docker&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
               &lt;span class="s1"&gt;&amp;#39;binderhub&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
              &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;commit_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;binderhub&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;live&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bhub_live&lt;/span&gt;
&lt;span class="n"&gt;commit_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;repo2docker&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;live&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r2d_live&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;commit_info&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="step-2-retrieve-latest-commits-from-the-dependency-repos"&gt;Step 2: Retrieve latest commits from the dependency repos&lt;/h2&gt;
&lt;p&gt;When we get the latest commit SHAs for repo2docker and BinderHub, we need to be careful and make sure we don’t automatically grab the latest one from GitHub. The travis build for &lt;a href="http://mybinder.org/"&gt;mybinder.org&lt;/a&gt; looks for the repo2docker Docker image from &lt;a href="https://hub.docker.com/v2/repositories/jupyter/repo2docker/tags/"&gt;DockerHub&lt;/a&gt;, and the latest BinderHub from the &lt;a href="https://raw.githubusercontent.com/jupyterhub/helm-chart/gh-pages/index.yaml"&gt;JupyterHub helm chart&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Let’s get the repo2docker version first:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;https://hub.docker.com/v2/repositories/jupyter/repo2docker/tags/&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r2d_master&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;results&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;name&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r2d_master&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now we can do BinderHub:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;url_helm_chart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;https://raw.githubusercontent.com/jupyterhub/helm-chart/gh-pages/index.yaml&amp;#39;&lt;/span&gt;
&lt;span class="n"&gt;helm_chart_yaml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url_helm_chart&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# sort by date created&lt;/span&gt;
&lt;span class="n"&gt;updates_sorted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;helm_chart_yaml&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;entries&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;binderhub&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;created&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;bh_master&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;updates_sorted&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;version&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bh_master&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Let’s add these to our dictionary too:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# add to commit_info dictionary&lt;/span&gt;
&lt;span class="n"&gt;commit_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;repo2docker&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;latest&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r2d_master&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;repo2docker&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;commit_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;repo2docker&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;live&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;commit_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;repo2docker&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;latest&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;commit_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;binderhub&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;latest&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bh_master&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;binderhub&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;commit_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;binderhub&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;live&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;commit_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;binderhub&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;latest&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;commit_info&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Great, now we should have all the information we need to determine &lt;em&gt;whether&lt;/em&gt; an update needs to be made or not, &lt;em&gt;and&lt;/em&gt; what the new commit SHA should be!&lt;/p&gt;
&lt;h2 id="step-3-fork-mybinderorg-repo"&gt;Step 3: Fork &lt;a href="http://mybinder.org/"&gt;mybinder.org&lt;/a&gt; repo&lt;/h2&gt;
&lt;p&gt;If we determine an upgrade for the repo is necessary, we need to fork the &lt;a href="http://mybinder.org/"&gt;mybinder.org&lt;/a&gt; &lt;a href="https://github.com/jupyterhub/mybinder.org-deploy"&gt;repository&lt;/a&gt;, make the change, commit, push, and make a PR. Fortunately, the GitHub API has all the functionality we need! Let’s just make a fork first.&lt;/p&gt;
&lt;p&gt;If you have permissions to a bunch of repos and organizations on GitHub, you may want to &lt;a href="https://help.github.com/en/articles/signing-up-for-a-new-github-account"&gt;create a new account or organization&lt;/a&gt; so that you don’t accidentally start automating git commands through an account that has write access to so much, especially while developing and testing the bot. I created the &lt;a href="https://github.com/henchbot"&gt;henchbot&lt;/a&gt; account for this.&lt;/p&gt;
&lt;p&gt;Once you know which account you want to be making the PRs with, you’ll need to &lt;a href="https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line"&gt;create a personal access token&lt;/a&gt; from within that account. I’ve set this as an environment variable so it isn’t hard-coded in the script.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;os&lt;/span&gt;
&lt;span class="n"&gt;TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;HENCHBOT_TOKEN&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;binderhub&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;repo2docker&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;commit_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;live&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;commit_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;latest&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;https://api.github.com/repos/jupyterhub/mybinder.org-deploy/forks&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
			    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Authorization&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;token &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TOKEN&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Using the API for a post request to the &lt;code&gt;forks&lt;/code&gt; endpoint will fork the repo to your account. That’s it!&lt;/p&gt;
&lt;h2 id="step-4-clone-your-fork"&gt;Step 4: Clone your fork&lt;/h2&gt;
&lt;p&gt;You should be quite used to this! We’ll use Python’s &lt;code&gt;subprocess&lt;/code&gt; module to run all of our &lt;code&gt;bash&lt;/code&gt; commands. We’ll need to run these within the for-loop above.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_call&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;git&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;clone&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;https://github.com/henchbot/mybinder.org-deploy&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Let’s also &lt;code&gt;cd&lt;/code&gt; into it and check out a new branch.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mybinder.org-deploy&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_call&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;git&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;checkout&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;-b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s1"&gt;_bump&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="step-5-make-the-file-changes"&gt;Step 5: Make the file changes&lt;/h2&gt;
&lt;p&gt;Now we need to edit the file like we would for an upgrade.&lt;/p&gt;
&lt;p&gt;For repo2docker, we edit the same &lt;code&gt;values.yaml&lt;/code&gt; file we checked above and replace the old SHA (“live”) with the “latest”.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;repo2docker&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mybinder/values.yaml&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;r&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
	        &lt;span class="n"&gt;values_yaml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;updated_yaml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;values_yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
			   &lt;span class="s2"&gt;&amp;quot;jupyter/repo2docker:&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;commit_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;upgrade&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;live&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
			   &lt;span class="s2"&gt;&amp;quot;jupyter/repo2docker:&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;commit_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;upgrade&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;latest&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;   
	    &lt;span class="n"&gt;fname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;mybinder/values.yaml&amp;#39;&lt;/span&gt;
	    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;w&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
	        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updated_yaml&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For BinderHub, we edit the same &lt;code&gt;requirements.yaml&lt;/code&gt; file we checked above and replace the old SHA (“live”) with the “latest”.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;binderhub&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mybinder/requirements.yaml&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;r&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;requirements_yaml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;updated_yaml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requirements_yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                           &lt;span class="s2"&gt;&amp;quot;version: 0.2.0-&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;commit_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;upgrade&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;live&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
                           &lt;span class="s2"&gt;&amp;quot;version: 0.2.0-&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;commit_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;upgrade&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;latest&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;   
            &lt;span class="n"&gt;fname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;mybinder/requirements.yaml&amp;#39;&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;w&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updated_yaml&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="step-6-stage-commit-push"&gt;Step 6: Stage, commit, push&lt;/h2&gt;
&lt;p&gt;Now that we’ve edited the correct files, we can stage and commit the changes. We’ll make the commit message the name of the repo and the compare URL for the commit changes so people can see what has changed between versions for the dependency.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="c1"&gt;# use var fname from editing step&lt;/span&gt;
        &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_call&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;git&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;add&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fname&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;repo2docker&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;commit_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;repo2docker: https://github.com/jupyter/repo2docker/compare/&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                              &lt;span class="n"&gt;commit_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;repo2docker&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;live&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="n"&gt;commit_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;repo2docker&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;latest&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;binderhub&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;commit_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;binderhub: https://github.com/jupyterhub/binderhub/compare/&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                              &lt;span class="n"&gt;commit_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;binderhub&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;live&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;commit_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;binderhub&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;latest&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

        &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_call&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;git&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;config&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;user.name&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;henchbot&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_call&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;git&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;config&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;user.email&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;henchbot.github@gmail.com&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_call&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;git&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;commit&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;-m&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;commit_message&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_call&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;git&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;push&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;https://henchbot:&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s1"&gt;@github.com/henchbot/mybinder.org-deploy&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TOKEN&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;_bump&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Awesome, we now have a fully updated fork ready to make a PR to the main repo!&lt;/p&gt;
&lt;h2 id="step-7-make-the-body-for-the-pr"&gt;Step 7: Make the body for the PR&lt;/h2&gt;
&lt;p&gt;We want the PR to have a nice comment explaining what’s happening and linking any helpful information so that the merger knows what they’re doing. We’ll note that this is a version bump and link the URL diff so it can be clicked to see what has changed.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;repo2docker&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;compare_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;https://github.com/jupyter/repo2docker/compare/&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                           &lt;span class="n"&gt;commit_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;repo2docker&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;live&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; 
                           &lt;span class="n"&gt;commit_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;repo2docker&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;latest&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;This is a repo2docker version bump. See the link below for a diff of new changes:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compare_url&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39; &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;binderhub&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;compare_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;https://github.com/jupyterhub/binderhub/compare/&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                           &lt;span class="n"&gt;commit_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;binderhub&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;live&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; 
                           &lt;span class="n"&gt;commit_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;binderhub&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;latest&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;This is a binderhub version bump. See the link below for a diff of new changes:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compare_url&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39; &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="step-8-make-the-pr"&gt;Step 8: Make the PR&lt;/h2&gt;
&lt;p&gt;We can use the GitHub API to make a pull request by calling the &lt;code&gt;pulls&lt;/code&gt; endpoint with the &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;body&lt;/code&gt;, &lt;code&gt;base&lt;/code&gt;, and &lt;code&gt;head&lt;/code&gt;. We’ll use the nice body we formatted above, call the title the same as the commit message we made with the repo name and the two SHAs, and put the &lt;code&gt;base&lt;/code&gt; as &lt;code&gt;master&lt;/code&gt; and the &lt;code&gt;head&lt;/code&gt; the name of our fork. Then we just make a POST request to the &lt;code&gt;pulls&lt;/code&gt; endpoint of the main repo.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="n"&gt;pr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="s1"&gt;&amp;#39;title&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s1"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                            &lt;span class="n"&gt;commit_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;live&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                                            &lt;span class="n"&gt;commit_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;latest&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
              &lt;span class="s1"&gt;&amp;#39;body&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="s1"&gt;&amp;#39;base&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;master&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="s1"&gt;&amp;#39;head&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;henchbot:&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s1"&gt;_bump&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;https://api.github.com/repos/jupyterhub/mybinder.org-deploy/pulls&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                             &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Authorization&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;token &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TOKEN&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="step-9-confirm-and-merge"&gt;Step 9: Confirm and merge!&lt;/h2&gt;
&lt;p&gt;If we check the &lt;a href="https://github.com/jupyterhub/mybinder.org-deploy/pulls"&gt;mybinder.org PRs&lt;/a&gt;, we would now see the automated PR from our account!&lt;/p&gt;
&lt;h2 id="step-10-automating-the-script-cron"&gt;Step 10: Automating the script (cron)&lt;/h2&gt;
&lt;p&gt;Now that we have a script we can simply execute to create a PR (&lt;code&gt;$ python henchbot.py&lt;/code&gt;), we want to make this as hands-off as possible. Generally we have two options: (1) set this script to be run as a &lt;a href="https://en.wikipedia.org/wiki/Cron"&gt;cron job&lt;/a&gt;; (2) have a web app listener that gets pinged whenever a change is made and executes your script as a reaction to the ping.&lt;/p&gt;
&lt;p&gt;Given that these aren’t super urgent updates that need to be made seconds or minutes after a repository update, we will go for the easier and less computationally-expensive option of cron.&lt;/p&gt;
&lt;p&gt;If you aren’t familiar with cron, it’s simply a system program that will run whatever command you want at whatever time or time interval you want. For now, we’ve decided that we want to execute this script every hour.&lt;/p&gt;
&lt;p&gt;Cron can be run on your local computer (though it would need to be continuously running) or a remote server. I’ve elected to throw it on my raspberry pi, which is always running. Since I have a few projects going on, I like to keep the cron jobs in a file.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;vim&lt;span class="w"&gt; &lt;/span&gt;crontab-jobs
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You can define your cron jobs here with the correct syntax (space-separated). Check out &lt;a href="https://crontab.guru/every-1-hour"&gt;this site&lt;/a&gt; for help with the crontab syntax. Since we want to run this every hour, we will set it to run on the 0 minutes, for every hour, every day, every month, every year. We also need to make sure it has the correct environment variable with the GitHub personal access token we created, so we’ll add that to the command.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;home&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;pi&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;projects&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;mybinder&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="ow"&gt;or&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;upgrades&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HENCHBOT_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;&amp;#39;&lt;/span&gt;&lt;span class="n"&gt;XXXXX&lt;/span&gt;&lt;span class="err"&gt;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;home&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;pi&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;miniconda3&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;henchbot&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now we point our cron to the file we’ve created to load the jobs.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;crontab&lt;span class="w"&gt; &lt;/span&gt;crontab-jobs
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To see our active crontab, we can list it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;crontab&lt;span class="w"&gt; &lt;/span&gt;-l
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That’s it! At the top of every hour, our bot will check to see if an update needs to be made, and if so, create a PR. To clean up files and handle existing PRs, in addition to some other details, I’ve written a few other functions. It is also implemented as a class with appropriate methods. You can check out the final code &lt;a href="https://github.com/henchbot/mybinder.org-upgrades/blob/master/henchbot.py"&gt;here&lt;/a&gt;.&lt;/p&gt;
</content><category term="Binder"/><category term="DevOps"/><category term="JupyterHub"/><category term="repo2docker"/></entry><entry><title>DevOps Intelligence</title><link href="https://jasongrout.github.io/medium-archive/pelican/posts/2019/devops-intelligence/" rel="alternate"/><published>2019-04-06T14:50:00+00:00</published><updated>2019-04-06T23:23:00+00:00</updated><author><name>Jürgen Hermann</name></author><id>tag:jasongrout.github.io,2019-04-06:/medium-archive/pelican/posts/2019/devops-intelligence/</id><summary type="html">&lt;p&gt;A JupyterHub Showcase&lt;/p&gt;
</summary><content type="html">&lt;figure&gt;
&lt;img alt="Responding to internal stakeholders via feedback loops relies on readily available knowledge." src="https://jasongrout.github.io/medium-archive/pelican/posts/2019/devops-intelligence/images/001-1_eWpDMkTE2C-pC5wCPicrvw.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Responding to internal stakeholders via feedback loops relies on readily available knowledge.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="a-jupyterhub-showcase"&gt;A JupyterHub Showcase&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;DevOps Intelligence&lt;/em&gt; turns data from software development and delivery processes into actionable insight, just like BI does for the business side. Jupyter is the ideal instrument for that, with its combination of powerful coding environments and a user interface facilitating experimentation with ultra-short feedback cycles.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2019/devops-intelligence/images/002-1_2AUEkHst1aNbAvAyRjvqCg.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;A Jupyter-based setup supports risk analysis and decision making within development and operations processes — typical business intelligence / data science procedures can be applied to the ‘business of making and running software’. The idea is to create feedback loops, and facilitate human decision making by automatically providing reliable input in form of up-to-date facts. After all development is our business — so let’s have KPIs for developing, releasing, and operating software.&lt;/p&gt;
&lt;h3 id="typical-use-cases-in-devops-intelligence"&gt;Typical Use-Cases in DevOps Intelligence&lt;/h3&gt;
&lt;p&gt;Here are some obvious application areas where data analysis can be helpful on the technical side.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Migration processes of all kinds (current state, progress tracking, achievement of objectives).&lt;/li&gt;
&lt;li&gt;Inventory reporting for increased transparency and support of operational decisions.&lt;/li&gt;
&lt;li&gt;Automate internal reporting processes to free up scarce assets and human expertise.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="platform-architecture"&gt;Platform Architecture&lt;/h3&gt;
&lt;p&gt;A simple &lt;a href="https://jupyter.org/hub"&gt;JupyterHub&lt;/a&gt; setup can enable you to do analysis on your already available but under-used and hardly understood data, without any great investment of effort or capital. By adding a single JupyterHub host, you can use the built-in Python3 kernel to access existing internal data sources.&lt;/p&gt;
&lt;p&gt;The following diagram shows what role JupyterHub can play in an existing environment.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Integration of JupyterHub into an existing development and operations system landscape." src="https://jasongrout.github.io/medium-archive/pelican/posts/2019/devops-intelligence/images/003-0_OISxo-kkYAl_9j0O.jpg" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Integration of JupyterHub into an existing development and operations system landscape.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;To make such a deployment easy, the &lt;a href="https://github.com/1and1/debianized-jupyterhub#jupyterhub-debian-packaging"&gt;1and1/debianized-jupyterhub&lt;/a&gt; project provides a JupyterHub service including a fully equipped Python3 kernel as a &lt;em&gt;single Debian package&lt;/em&gt; — only Python3, NodeJS, and Chromium packages must be installed in addition to the &lt;code&gt;jupyterhub&lt;/code&gt; one. If you raised an eyebrow on Chromium being in that list, it’s used by JavaScript-based visualization frameworks to render PNG images.&lt;/p&gt;
&lt;p&gt;Including a &lt;a href="https://github.com/1and1/debianized-jupyterhub#securing-your-jupyterhub-web-service-with-an-ssl-off-loader"&gt;NginX-powered SSL off-loader&lt;/a&gt;, the &lt;a href="https://github.com/1and1/debianized-jupyterhub#how-to-set-up-a-simple-service-instance"&gt;complete setup&lt;/a&gt; can be done in under an hour by an experienced Linux administrator.&lt;/p&gt;
&lt;h3 id="use-case-migration-reporting"&gt;Use-Case: Migration Reporting&lt;/h3&gt;
&lt;p&gt;At the time of this writing (early 2019), a widespread challenge is migration from Oracle Java to other vendors, and also to start migration from Java 8 to newer versions (Java 11). If you do that at scale across many machines and teams, you definitely need some kind of governance, and constant feedback on the current status and the rate of progress.&lt;/p&gt;
&lt;p&gt;What follows is an excerpt from a productive notebook, with anonymized data about &lt;a href="https://adoptopenjdk.net/"&gt;AdoptOpenJDK&lt;/a&gt; deployments. That data was originally retrieved from a system called &lt;em&gt;“Patch Management Reporting”&lt;/em&gt;, which collects information about installed packages for all hosts in the data center. We’re in the yellow &lt;em&gt;“Data Sources”&lt;/em&gt; box of the above figure here.&lt;/p&gt;
&lt;p&gt;First off, we read the data and show the value sets of categorical columns, plus a sample.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;numpy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;pandas&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;pd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;raw_data = pd.read_csv(&amp;quot;../data/cmdb-aoj.csv&amp;quot;, sep=&amp;#39;,&amp;#39;)
print(&amp;#39;♯ of Records: {}\n&amp;#39;.format(len(raw_data)))
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;1:&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Last &amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;=&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fillna&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)))))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;print(); print(raw_data.head(2).transpose())
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;♯ of Records: 104
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Distribution = [&amp;#39;Debian 8.10&amp;#39;, &amp;#39;Debian 8.11&amp;#39;, &amp;#39;Debian 8.6&amp;#39;,
                &amp;#39;Debian 8.9&amp;#39;, &amp;#39;Debian 9.6&amp;#39;, &amp;#39;Debian 9.7&amp;#39;, 
                &amp;#39;Debian 9.8&amp;#39;]
Architecture = [&amp;#39;amd64&amp;#39;]
Environment = [&amp;#39;--&amp;#39;, &amp;#39;DEV&amp;#39;, &amp;#39;LIVE&amp;#39;, &amp;#39;QA&amp;#39;]
Team = [&amp;#39;Team Blue&amp;#39;, &amp;#39;Team Green&amp;#39;, &amp;#39;Team Red&amp;#39;, &amp;#39;Team Yellow&amp;#39;]
Installed version = [&amp;#39;11.0.2.9-83(amd64)&amp;#39;, &amp;#39;11.0.2.9-85(amd64)&amp;#39;, &amp;#39;8.202.b08-66(amd64)&amp;#39;, &amp;#39;8.202.b08-83(amd64)&amp;#39;, &amp;#39;8.202.b08-85(amd64)&amp;#39;]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;                                    0                   1
CMDB_Id                     108380195           298205230
Distribution              Debian 8.11          Debian 9.6
Architecture                    amd64               amd64
Environment                       DEV                  --
Team                         Team Red            Team Red
Last seen            2019-03-18 06:42    2019-03-18 06:42
Last modified        2019-03-18 06:42    2019-03-18 06:42
Installed version  11.0.2.9-83(amd64)  11.0.2.9-83(amd64)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Next comes the usual data cleanup. The &lt;code&gt;Distribution&lt;/code&gt; column is a bit diverse, and not everyone has Debian code names and associated major versions memorized. The &lt;code&gt;map_distro&lt;/code&gt; function fixes that.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;def map_distro(name):
    &amp;quot;&amp;quot;&amp;quot;Helper to create canonical OS names.&amp;quot;&amp;quot;&amp;quot;
    return (name.split(&amp;#39;.&amp;#39;, 1)[0]
        .replace(&amp;#39;Debian 7&amp;#39;, &amp;#39;wheezy&amp;#39;)
        .replace(&amp;#39;Debian 8&amp;#39;, &amp;#39;jessie&amp;#39;)
        .replace(&amp;#39;Debian 9&amp;#39;, &amp;#39;stretch&amp;#39;)
        .replace(&amp;#39;Debian 10&amp;#39;, &amp;#39;buster&amp;#39;)
        .replace(&amp;#39;squeeze&amp;#39;, &amp;#39;Squeeze [6]&amp;#39;)
        .replace(&amp;#39;wheezy&amp;#39;, &amp;#39;Wheezy [7]&amp;#39;)
        .replace(&amp;#39;jessie&amp;#39;, &amp;#39;Jessie [8]&amp;#39;)
        .replace(&amp;#39;stretch&amp;#39;, &amp;#39;Stretch [9]&amp;#39;)
        .replace(&amp;#39;buster&amp;#39;, &amp;#39;Buster [10]&amp;#39;)
    )
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Together with other cleanup steps, the mapper function is applied in a &lt;a href="https://towardsdatascience.com/dplyr-style-data-manipulation-with-pipes-in-python-380dcb137000"&gt;dfply&lt;/a&gt; pipeline. The result can be controlled by showing a sample of data points with unique version numbers.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;dfply&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;cleaned = (raw_data
    &amp;gt;&amp;gt; mutate(Version=X[&amp;#39;Installed version&amp;#39;]
                      .str.split(&amp;#39;[()]&amp;#39;, 1, expand=True)[0])
    &amp;gt;&amp;gt; mutate(Environment=X.Environment.fillna(&amp;#39;--&amp;#39;)
                           .str.replace(&amp;#39;--&amp;#39;, &amp;#39;N/A&amp;#39;)
                           .str.upper())
    &amp;gt;&amp;gt; mutate(Distribution=X.Distribution.apply(map_distro))
    &amp;gt;&amp;gt; drop(X.CMDB_Id, X[&amp;#39;Last seen&amp;#39;], 
            X[&amp;#39;Last modified&amp;#39;], X[&amp;#39;Installed version&amp;#39;])
)
print((cleaned &amp;gt;&amp;gt; distinct(X.Version)).transpose())
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;                       0            13            14            62  \
Distribution   Jessie [8]  Stretch [9]    Jessie [8]   Stretch [9]   
Architecture        amd64        amd64         amd64         amd64   
Environment           DEV          N/A           DEV           DEV   
Team             Team Red    Team Blue      Team Red     Team Blue   
Version       11.0.2.9-83  11.0.2.9-85  8.202.b08-83  8.202.b08-85
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;                        68  
Distribution    Jessie [8]  
Architecture         amd64  
Environment            DEV  
Team             Team Blue  
Version       8.202.b08-66
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Time to present the cleaned up data, starting with a table of teams and their number of installed packages. In the production notebook, an API of the corporate identity management is used to enrich the table with contact data of the team leads. Having the organizational data available also makes it possible to filter or aggregate the data by business units.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;counts = cleaned.groupby([&amp;#39;Team&amp;#39;]).size()
print(counts.reset_index(name=&amp;#39;Count&amp;#39;))
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;          Team  Count
0    Team Blue     42
1   Team Green     16
2     Team Red     45
3  Team Yellow      1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To create a heatmap of how diverse a team’s version spectrum is, we calculate percentages of versions per team.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;percentage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;[&amp;#39;&lt;/span&gt;&lt;span class="n"&gt;Team&lt;/span&gt;&lt;span class="p"&gt;&amp;#39;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;#39;&lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;&amp;#39;]).&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;reset_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;#39;&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;&amp;#39;)&lt;/span&gt;
&lt;span class="n"&gt;percentage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;percentage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Percent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;percentage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lambda&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;x:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;100.0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mh"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mh"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mh"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;percentage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        Team       Version  Count    Percent
0  Team Blue   11.0.2.9-85      1   2.380952
1  Team Blue  8.202.b08-66      1   2.380952
2  Team Blue  8.202.b08-85     40  95.238095
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;a href="http://holoviews.org/"&gt;HoloViews&lt;/a&gt; makes creating the heatmap including a label overlay a breeze.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;holoviews&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;hv&lt;/span&gt;
&lt;span class="n"&gt;hv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;extension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;bokeh&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;publish&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;  &lt;span class="c1"&gt;# publishing or interactive mode?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;heatmap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HeatMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;percentage&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Version&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Team&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Percent&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Count&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;hv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HeatMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Version Distribution by Team&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;480&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;xrotation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;zlim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cmap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;kbc_r&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;clipping_colors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NaN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;#ffffe0&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;colorbar&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;hover&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;toolbar&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;None&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;publish&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;right&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;label_dimension = hv.Dimension(
    &amp;#39;Percent&amp;#39;, value_format=lambda x: &amp;#39;%.1f&amp;#39; % x)
labels = hv.Labels(heatmap, vdims=label_dimension).opts(
    hv.opts.Labels(
        text_color=&amp;#39;Percent&amp;#39;, 
        text_font_size=&amp;#39;10pt&amp;#39;,
        text_font_style=&amp;#39;bold&amp;#39;,
    )
)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;chart = heatmap * labels
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;phantomjs_bin&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;IPython.display&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HTML&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clear_output&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nf"&gt;%env&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;BOKEH_PHANTOMJS_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;phantomjs_bin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executable_path&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;chart_img&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;#39;&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;aoj&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;heatmap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;png&lt;/span&gt;&lt;span class="err"&gt;&amp;#39;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;hv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;chart_img&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;chart&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HTML&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;{}?{}&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;&amp;#39;&lt;/span&gt;
&lt;span class="w"&gt;                 &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chart_img&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;clear_output&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;chart
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2019/devops-intelligence/images/004-1_TafHbpdZRN28gzxWoSn2_Q.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;From the heatmap, you can easily glance whether a team uses predominantly one version, and how recent the used versions are.&lt;/p&gt;
&lt;h3 id="conclusion"&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Using a platform powered by &lt;a href="https://twitter.com/ProjectJupyter"&gt;Project Jupyter&lt;/a&gt; and a big chunk of the scientific Python stack lets you easily mold your data into the shape you need, and then choose from a wide range of visualization options to bring your message across.&lt;/p&gt;
</content><category term="DevOps"/></entry><entry><title>Incident Report: Jupyter services down</title><link href="https://jasongrout.github.io/medium-archive/pelican/posts/2017/incident-report-jupyter-services-down/" rel="alternate"/><published>2017-12-14T19:52:00+00:00</published><updated>2017-12-14T20:46:00+00:00</updated><author><name>M Bussonnier</name></author><id>tag:jasongrout.github.io,2017-12-14:/medium-archive/pelican/posts/2017/incident-report-jupyter-services-down/</id><summary type="html">&lt;p&gt;update: December 14, 20:45 UTC, all services should be restored and back up.&lt;/p&gt;
</summary><content type="html">&lt;p&gt;&lt;strong&gt;update:&lt;/strong&gt; December 14, 20:45 UTC, all services should be restored and back up.&lt;/p&gt;
&lt;p&gt;On December 13, at 22:10 UTC (4:10pm EST), a large number of Jupyter-provided services stopped responding. This included, but was not limited to &lt;a href="https://nbviewer.jupyter.org,"&gt;https://nbviewer.jupyter.org,&lt;/a&gt; &lt;a href="https://try.jupyter.org"&gt;https://try.jupyter.org&lt;/a&gt; (powered by tmpnb) and &lt;a href="https://cdn.jupyter.org"&gt;https://cdn.jupyter.org&lt;/a&gt;. We quickly narrowed this down to an issue with our hosting provider and have been working with them to resolve the issue as fast as possible.&lt;/p&gt;
&lt;p&gt;When outages happen, the &lt;a href="http://status.jupyter.org/"&gt;Jupyter Status page&lt;/a&gt; should show which services are affected and we publish updates there.&lt;/p&gt;
&lt;h2 id="how-are-jupyter-services-hosted"&gt;How are Jupyter services hosted?&lt;/h2&gt;
&lt;p&gt;To understand the cause of the outage, we need to understand how the Jupyter services are hosted and maintained. As Jupyter is an open organization which is mostly maintained by volunteers, we do not have a dev-ops team assigned to maintaining our infrastructure. Even with full-time developers hired through universities or companies, the time spent fixing infrastructure is taken on nights and weekends. These developers are often stretched thin and cannot be available 24/7.&lt;/p&gt;
&lt;p&gt;Most of our cloud infrastructure is donated to us by companies like &lt;a href="https://www.cloudflare.com/"&gt;CloudFlare&lt;/a&gt;, &lt;a href="https://www.rackspace.com/"&gt;Rackspace&lt;/a&gt;, &lt;a href="http://cloudflare"&gt;Fastly&lt;/a&gt;, &lt;a href="https://cloud.google.com/"&gt;Google&lt;/a&gt;, and &lt;a href="https://azure.microsoft.com"&gt;Microsoft&lt;/a&gt;. Donating resources can be challenging, both technically and legally. In this particular case, Rackspace graciously created a special account for Jupyter that handles invoices on our behalf, thereby making resources free to the project. Following a hiccup, this Jupyter account was suspended and all services are unavailable as a result.&lt;/p&gt;
&lt;h2 id="temporary-resolution"&gt;Temporary resolution&lt;/h2&gt;
&lt;p&gt;As nbviewer is one of the most used services provided by Jupyter, we’ve moved it to one of our personal account at another cloud-provider. &lt;a href="https://www.fastly.com/"&gt;Fastly&lt;/a&gt; was set up to load-balance on the yet-to-come-back-up instances as well as this newly created instance, so all should be fine now.&lt;/p&gt;
&lt;p&gt;The other services (tmpnb, mails@jupyter.org, cdn.jupyter.org, …) will still unavailable or highly degraded until a permanent solution is found, or the services are restarted. &lt;code&gt;try.jupyter.org&lt;/code&gt; will likely redirect to a repo on &lt;a href="https://mybinder.org"&gt;https://mybinder.org&lt;/a&gt; in the meantime so people can still try out Jupyter.&lt;/p&gt;
&lt;h2 id="low-bus-factor"&gt;Low bus factor&lt;/h2&gt;
&lt;p&gt;The outage of all these services lasted for a significant time (more than 18 hours). Which perturbed many of you relying on these services. We understand that this is hardly acceptable and we hope you’ll indulge us as these services are provided for free and without ads. One of the factors leading to the slow reestablishment of service was a relatively low &lt;a href="https://en.wikipedia.org/wiki/Bus_factor"&gt;bus factor&lt;/a&gt;, with only one and a half of our developers knowing how to deploy and maintain these services. Documentation and access to credentials was also limited.&lt;/p&gt;
&lt;p&gt;This is one of the challenges in a distributed team like Jupyter where contributors self-organize. It is easy to forget that new code is not the only way to contribute and that &lt;a href="https://www.nytimes.com/2017/07/22/opinion/sunday/lets-get-excited-about-maintenance.html"&gt;infrastructure and maintenance&lt;/a&gt; are crucial.&lt;/p&gt;
&lt;p&gt;We also overly rely on a single vendor (in this case Rackspace), and while we are happy with Rackspace and have no reason to move to another provider, we should have a plan to restore critical services even temporarily in case of failure.&lt;/p&gt;
&lt;p&gt;A couple of months ago, the subject was brought to our attention, and we developed a plan to move many of our deployment to &lt;a href="https://k8s.io"&gt;Kubernetes&lt;/a&gt; (which is provider agnostic). We underestimated the probability to need an emergency plan this early.&lt;/p&gt;
&lt;h2 id="how-can-you-help"&gt;How can you help&lt;/h2&gt;
&lt;p&gt;Jupyter is mainly governed by the community all around the world. Contributing is not limited to writing code! We need members with knowledge in multiple languages, in design, dev-ops, etc. Whether you are an expert, or still learning, we would like you to &lt;a href="https://jupyter.org/community.html"&gt;get involved&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Thanks everyone for your patience and the kind words when you reached to us when discovering the services were down.&lt;/p&gt;
</content><category term="cloud computing"/><category term="DevOps"/></entry></feed>