<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Sven Freiberg</title>
    <description>The latest articles on Forem by Sven Freiberg (@blurryroots).</description>
    <link>https://forem.com/blurryroots</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F513305%2F77bcb0ef-daf0-4c39-8f01-efbd4370754a.jpeg</url>
      <title>Forem: Sven Freiberg</title>
      <link>https://forem.com/blurryroots</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/blurryroots"/>
    <language>en</language>
    <item>
      <title>Smooth Packaging: Flowing from Source to PyPi with GitLab Pipelines</title>
      <dc:creator>Sven Freiberg</dc:creator>
      <pubDate>Thu, 18 Jan 2024 13:49:39 +0000</pubDate>
      <link>https://forem.com/blurryroots/smooth-packaging-flowing-from-source-to-pypi-with-gitlab-pipelines-1131</link>
      <guid>https://forem.com/blurryroots/smooth-packaging-flowing-from-source-to-pypi-with-gitlab-pipelines-1131</guid>
      <description>&lt;h1&gt;
  
  
  Overview
&lt;/h1&gt;

&lt;p&gt;In the process of releasing a tool called &lt;a href="https://gitlab.com/think-biq/piper-whistle" rel="noopener noreferrer"&gt;piper-whistle&lt;/a&gt;, making it available through &lt;a href="https://pip.pypa.io/en/stable/installation/" rel="noopener noreferrer"&gt;pip&lt;/a&gt; was ultimately the goal. Ideally, the concepts should be documented in the automation code itself. Yet it seemed useful to have a closer look at the architecture, details and quirks. The setup and inner workings of these most convenient mechanizations, I'd wish to share with you in this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Branch setup
&lt;/h2&gt;

&lt;p&gt;The concept is based on three main stages the source code can be in, which are represented by the &lt;a href="https://gitlab.com/think-biq/piper-whistle/-/branches" rel="noopener noreferrer"&gt;three branches&lt;/a&gt; through which the project is structured by.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cutting edge - main&lt;/li&gt;
&lt;li&gt;Simulation - staging&lt;/li&gt;
&lt;li&gt;The real thing - release&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cutting edge
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F743d48cvarj1p81wsh0w.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F743d48cvarj1p81wsh0w.jpeg" alt="Working on the cutting edge."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The state on the main branch is for development and most recent changes. When pushed, the main branch triggers a set of testing automation, to help detecting any new issues or regressions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simulation
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwtifojz0zeaci0y025yw.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwtifojz0zeaci0y025yw.jpeg" alt="Orbis Alius."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If a set of features is coming together and seem worthy of a new release, lose ends are tied up and the version number in &lt;code&gt;src/piper_whistle/version.py&lt;/code&gt; is increased in respect to the severity of the changes. Now the change-set is merged into the staging branch, which when pushed, will trigger a set of automatons, simulating the publishing of the final package. After the simulated publishing a test install is performed to see how the released version would behave.&lt;/p&gt;

&lt;h3&gt;
  
  
  The real thing
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fozhk0q057um13rwqmlan.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fozhk0q057um13rwqmlan.jpeg" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If everything appears fine and dandy, the next step is the proper release. For this, the changes are merged into the release branch. A tag with the name of the current version is created and associated with the latest commit. When pushed, the appropriate pipeline targets get triggered and the release candidate is processed and uploaded to the live &lt;a href="https://pypi.org/project/piper-whistle/" rel="noopener noreferrer"&gt;python package index&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preparations
&lt;/h2&gt;

&lt;p&gt;To get started, this setup needs access to several platforms, which are listed below. Please make sure to have them setup and ready, if you’d like to follow along. It also assumes that you wish to synchronize your repository between GitLab and GitHub.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A python package project &lt;a href="https://setuptools.pypa.io/en/latest/setuptools.html" rel="noopener noreferrer"&gt;buildable with setuptools&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source checked into git repository with three branch setup&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://gitlab.com/" rel="noopener noreferrer"&gt;Gitlab&lt;/a&gt; account&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; account&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pypi.org/" rel="noopener noreferrer"&gt;PyPi&lt;/a&gt; account&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://test.pypi.org/" rel="noopener noreferrer"&gt;PyPi staging / test&lt;/a&gt; account&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.gitlab.com/ee/ci/variables/index.html" rel="noopener noreferrer"&gt;CI/CD Variables&lt;/a&gt; set up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To prepare the environment, GitLab offers project specific CI/CD variable configurations. The following list names the variable names as used in the script and their qualifier. The settings can be reached by browsing your repository and selecting:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Setting -&amp;gt; CI/CD -&amp;gt; Variables (click Expand)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This will allow you to create all the environment variable necessary for this setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  GITHUB_API_KEY_FILE
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens" rel="noopener noreferrer"&gt;Personal access token&lt;/a&gt;, which allows for communication with github repository API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;github_pat_...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  GITHUB_DEPLOY_DOMAIN
&lt;/h3&gt;

&lt;p&gt;This will be used to construct the domain for ssh known host configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;github.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  GITHUB_DEPLOY_KEY_FILE
&lt;/h3&gt;

&lt;p&gt;Contains the &lt;a href="https://docs.github.com/en/authentication/connecting-to-github-with-ssh/managing-deploy-keys" rel="noopener noreferrer"&gt;private key deploy key&lt;/a&gt; generated through GitHub.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  GITHUB_REPO_LINK
&lt;/h3&gt;

&lt;p&gt;The ssh capable link to your GitHub repository.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git@github.com:think-biq/piper-whistle.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  GITHUB_USER_EMAIL
&lt;/h3&gt;

&lt;p&gt;This should be set to the user email you want to be used for the push / sync.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;your@login.email
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  GITHUB_USER_NAME
&lt;/h3&gt;

&lt;p&gt;The username you want to be used for the push / sync.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;YourGithubUsername
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  PYPI_CONFIG_FILE
&lt;/h3&gt;

&lt;p&gt;The contents of the &lt;a href="https://packaging.python.org/en/latest/specifications/pypirc/#using-a-pypi-token" rel="noopener noreferrer"&gt;PyPi test server&lt;/a&gt; deploy token.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[testpypi]
  username = __token__
  password = pypi-Your-PyPi-Test-Token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  PYPI_LIVE_CONFIG_FILE
&lt;/h3&gt;

&lt;p&gt;The contents of the &lt;a href="https://packaging.python.org/en/latest/specifications/pypirc/#using-a-pypi-token" rel="noopener noreferrer"&gt;PyPi live server&lt;/a&gt; deploy token.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[pypi]&lt;/span&gt;
  &lt;span class="py"&gt;username&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;__token__&lt;/span&gt;
  &lt;span class="py"&gt;password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;pypi-Your-PyPi-Token&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  The script
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fct7f5ypmym9apa65y7sp.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fct7f5ypmym9apa65y7sp.jpeg" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Working with pipelines is done through defining different targets (or jobs), which will then be run at certain stages and under certain conditions. &lt;a href="https://gitlab.com/think-biq/piper-whistle/-/blob/release/.gitlab-ci.yml?ref_type=heads" rel="noopener noreferrer"&gt;The script&lt;/a&gt; file is picked up if you name it &lt;em&gt;.gitlab-ci.yml&lt;/em&gt; and place it at root level of your repository. Each target spins up a new &lt;a href="https://docs.gitlab.com/ee/ci/yaml/#image" rel="noopener noreferrer"&gt;container image&lt;/a&gt; and then executes a &lt;a href="https://docs.gitlab.com/ee/ci/yaml/script.html" rel="noopener noreferrer"&gt;set of instructions&lt;/a&gt;. It is possible to define a base image for each target, define a global default one and combine both, leading to cascaded overrides (except when you disable it via the inherit attribute).&lt;/p&gt;

&lt;h2&gt;
  
  
  Imagination
&lt;/h2&gt;

&lt;p&gt;I created the base container &lt;a href="https://gitlab.com/think-biq/docker-ue-plugin-staging" rel="noopener noreferrer"&gt;ue-plugin-staging&lt;/a&gt; to prepare releases of Unreal Engine plugins and found it useful for other automation purposes (hence the name). You may create your own, or use another one which provides the same set of tools.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;thinkbiq/ue-plugin-staging:latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  All in stages
&lt;/h2&gt;

&lt;p&gt;Now we define the stages used through the pipeline. Which are essentially testing, building and releasing with extra utility layers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pre-release-test&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;release&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;grace&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;post-release-test&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;finalize&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Journey to the testing grounds
&lt;/h2&gt;

&lt;p&gt;The first major layer is testing. It is split in testing before any release happens and tests which are run after the release has been successfully concluded. Let’s look at the tests running before anything else is started.&lt;/p&gt;

&lt;h3&gt;
  
  
  Smoke test
&lt;/h3&gt;

&lt;p&gt;First in line of the pipeline is a functional test defined in the testing package of the &lt;code&gt;piper-whistle&lt;/code&gt; source. This is what test-functional is responsible for. It checks functionality of the tool holistically by running the smoke / integration tests implemented through the class &lt;em&gt;CliCommandTests&lt;/em&gt; defined in &lt;code&gt;src/testing/functionality.py&lt;/code&gt; using &lt;a href="https://docs.python.org/3/library/unittest.html#basic-example" rel="noopener noreferrer"&gt;unittest&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; unittest src/testing/functional.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The target is available for manual trigger on the main as well as staging branch and simply installs all necessary pip requirements and invokes the test through the &lt;a href="https://www.gnu.org/software/make/manual/make.html" rel="noopener noreferrer"&gt;makefile&lt;/a&gt; target &lt;code&gt;run-tests&lt;/code&gt;, which in turn invokes the unittest runner on functional.py.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;test-functional&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pre-release-test&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH&lt;/span&gt;
      &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;manual&lt;/span&gt;
      &lt;span class="na"&gt;allow_failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_REF_NAME == "staging"&lt;/span&gt;
      &lt;span class="na"&gt;allow_failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;make pip-update-all&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;make run-tests&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Security audit
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmxn1bcjit3syjmta8748.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmxn1bcjit3syjmta8748.jpeg" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next up is making sure, none of the &lt;a href="https://gitlab.com/think-biq/piper-whistle/-/blob/main/requirements.txt?ref_type=heads" rel="noopener noreferrer"&gt;dependencies&lt;/a&gt; used throughout the project brings with it any already identified security issue. The makefile target audit, invokes the handy tool &lt;a href="https://pypi.org/project/pip-audit/" rel="noopener noreferrer"&gt;pip-audit&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip-audit &lt;span class="nt"&gt;--desc&lt;/span&gt;
pip-audit &lt;span class="nt"&gt;--fix&lt;/span&gt; &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With its help, the pipeline target test-security runs a quick scan on the packages installed and looks for known vulnerabilities and proposes fixes (if any).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;test-security&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pre-release-test&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH&lt;/span&gt;
      &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;manual&lt;/span&gt;
      &lt;span class="na"&gt;allow_failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_REF_NAME == "staging"&lt;/span&gt;
      &lt;span class="na"&gt;allow_failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;make pip-update-all&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;make audit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It can be triggered manually on the main branch. When automatically triggered on branch staging, this needs to pass without any errors or warnings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Linting
&lt;/h3&gt;

&lt;p&gt;There are multiple linting libraries and tools out there. For this scenario, I was looking for a reasonable balance of (proper) standards and ability to configure the process in regards to my customized (arguably foolish) code style. Some may be dismayed by this, which is why I take the liberty of quoting &lt;a href="https://peps.python.org/pep-0008/" rel="noopener noreferrer"&gt;Guido van Rossum&lt;/a&gt; here:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A style guide is about consistency. Consistency with this style guide is important. Consistency within a project is more important. Consistency within one module or function is the most important.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Adding more weight to ease of setup and configurability, the choice came down on &lt;a href="https://flake8.pycqa.org/en/latest/" rel="noopener noreferrer"&gt;flake8&lt;/a&gt;. It is easy to integrate, since its also available through pip and let’s you configure which standards you want to omit by simply stating them as a list via the --ignore switch. Moving to &lt;a href="https://github.com/astral-sh/ruff" rel="noopener noreferrer"&gt;ruff&lt;/a&gt; appears &lt;a href="https://pypi.org/project/flake8-to-ruff/" rel="noopener noreferrer"&gt;quite smooth&lt;/a&gt;, so future updates may do so.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;test-linting&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pre-release-test&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH&lt;/span&gt;
      &lt;span class="na"&gt;allow_failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_REF_NAME == "staging"&lt;/span&gt;
      &lt;span class="na"&gt;allow_failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;make pip-update-all&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;make lint&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Turning the wheel of fortune
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1ekaxxfvj4udv3q2ngnr.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1ekaxxfvj4udv3q2ngnr.jpeg" alt="Fortunam insanam esse et caecam et brutam perhibent philosophi."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The target &lt;code&gt;build-wheel&lt;/code&gt; - sole constituent of the build layer - installs all dependencies and resolves the &lt;a href="https://gitlab.com/think-biq/piper-whistle/-/blob/release/etc/readme.md.tmpl?ref_type=heads" rel="noopener noreferrer"&gt;readme markdown template&lt;/a&gt; to its release form (using the tmplr tool), since the &lt;code&gt;readme.md&lt;/code&gt; file is packaged with the &lt;a href="https://pythonwheels.com/" rel="noopener noreferrer"&gt;wheel file&lt;/a&gt;. This is followed by the generation of a compressed source file release (&lt;a href="https://linuxize.com/post/how-to-extract-unzip-tar-gz-file/" rel="noopener noreferrer"&gt;tar.gz&lt;/a&gt;) and &lt;a href="https://realpython.com/python-wheels/" rel="noopener noreferrer"&gt;building a wheel file&lt;/a&gt; (whl) using &lt;a href="https://build.pypa.io/en/stable/" rel="noopener noreferrer"&gt;build&lt;/a&gt; and setuptools respectively.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;build-wheel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_REF_NAME == "release"&lt;/span&gt;
      &lt;span class="na"&gt;allow_failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_REF_NAME == "staging"&lt;/span&gt;
      &lt;span class="na"&gt;allow_failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH&lt;/span&gt;
      &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;manual&lt;/span&gt;
      &lt;span class="na"&gt;allow_failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;make pip-update-all&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;make readme-build&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;make release&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;export VERSION_TAG=$(etc/./query-tag-most-relevant-version.sh)&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "VERSION_TAG=${VERSION_TAG}" &amp;gt; pypi.env&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;export WHEEL_FILE_NAME="piper_whistle-${VERSION_TAG}-py3-none-any.whl"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "WHEEL_FILE_NAME=${WHEEL_FILE_NAME}" &amp;gt;&amp;gt; pypi.env&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;export DEP_URL_BASE="https://pypi.debian.net/piper-whistle"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;export WHEEL_FILE_URL="${DEP_URL_BASE}/${WHEEL_FILE_NAME}"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "WHEEL_FILE_URL=${WHEEL_FILE_URL}" &amp;gt;&amp;gt; pypi.env&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "pypi.env:"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cat pypi.env&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Generating release page for ${VERSION_TAG} ..."&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;if ! etc/./query-tag-message.sh ${VERSION_TAG} &amp;gt; release-notes.md; then&lt;/span&gt;
        &lt;span class="s"&gt;echo "Could not query tag ${VERSION_TAG}. Defaulting to empty.";&lt;/span&gt;
        &lt;span class="s"&gt;echo "" &amp;gt; release-notes.md;&lt;/span&gt;
      &lt;span class="s"&gt;fi&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "release-notes.md:"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cat release-notes.md&lt;/span&gt;
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build/release&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;release-notes.md&lt;/span&gt;
    &lt;span class="na"&gt;reports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;dotenv&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pypi.env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both of those are then &lt;a href="https://docs.gitlab.com/ee/ci/jobs/job_artifacts.html" rel="noopener noreferrer"&gt;exposed as artifacts&lt;/a&gt;, along side some &lt;a href="https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportsdotenv" rel="noopener noreferrer"&gt;environment variables&lt;/a&gt; as well as the release notes, which were obtained by invoking &lt;code&gt;query-tag-message.sh&lt;/code&gt;. &lt;a href="https://gitlab.com/think-biq/piper-whistle/-/blob/release/etc/query-tag-message.sh" rel="noopener noreferrer"&gt;This shell script&lt;/a&gt; queries the git log for the message attached to the current version tag. Or if run on a non-tagged commit, leaves it empty. The &lt;a href="https://gitlab.com/think-biq/piper-whistle/-/blob/release/etc/query-tag-most-relevant-version.sh" rel="noopener noreferrer"&gt;version name is obtained&lt;/a&gt; via &lt;code&gt;query-tag-most-relevant-version.sh&lt;/code&gt;, which either takes the name of the tag attached to the current commit, or queries the &lt;a href="https://gitlab.com/think-biq/piper-whistle/-/blob/main/src/piper_whistle/version.py?ref_type=heads" rel="noopener noreferrer"&gt;whistle’s version module&lt;/a&gt;. Targets wanting access to these artifacts, simply have to specify the &lt;a href="https://docs.gitlab.com/ee/ci/yaml/#needsartifacts" rel="noopener noreferrer"&gt;target name and indicate access to artifacts&lt;/a&gt; through the needs attribute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Playtime Preparation: Setting stage for the package
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpm0scx6mslhz63t9uwg3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpm0scx6mslhz63t9uwg3.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://psfmember.org/civicrm/contribute/transact/?reset=1&amp;amp;id=13" rel="noopener noreferrer"&gt;PyPi team&lt;/a&gt; allows you to use a testing mirror of the live repository at &lt;a href="https://test.pypi.org/" rel="noopener noreferrer"&gt;test.pypi.org&lt;/a&gt; to rehearse the release of your python package. I'm using &lt;a href="https://pypi.org/project/twine/" rel="noopener noreferrer"&gt;twine&lt;/a&gt; to communicate with PyPi staging as well as live servers. The upload can be commenced by &lt;a href="https://gitlab.com/think-biq/piper-whistle/-/blob/release/etc/upload-pypi.sh?ref_type=heads" rel="noopener noreferrer"&gt;invoking the script&lt;/a&gt; &lt;code&gt;upload-pypi.sh&lt;/code&gt;, which looks something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;twine upload &lt;span class="nt"&gt;--non-interactive&lt;/span&gt; &lt;span class="nt"&gt;--verbose&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--config-file&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;configpath&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;repo&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--skip-existing&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nv"&gt;$*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://packaging.python.org/en/latest/specifications/pypirc/" rel="noopener noreferrer"&gt;format of the config&lt;/a&gt; file is defined by &lt;a href="https://docs.python.org/3.10/library/distutils.html" rel="noopener noreferrer"&gt;distutils&lt;/a&gt;. The pipeline script uses separate configuration files for test and live server of PyPi. You may as well put them in one file all together and only select the repository to use with the &lt;code&gt;-r&lt;/code&gt; switch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[testpypi]&lt;/span&gt;
  &lt;span class="py"&gt;username&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;__token__&lt;/span&gt;
  &lt;span class="py"&gt;password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;pypi-ToKeN&lt;/span&gt;

&lt;span class="nn"&gt;[pypi]&lt;/span&gt;
  &lt;span class="py"&gt;username&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;__token__&lt;/span&gt;
  &lt;span class="py"&gt;password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;pypi-ToKeN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Being pushy
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh3f0h8nzloag9jku0m0u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh3f0h8nzloag9jku0m0u.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First in line is &lt;code&gt;release-pypi-test&lt;/code&gt;, which fetches the artifacts produced by &lt;code&gt;build-wheel&lt;/code&gt; and pushes them to the test server using the authentication configuration store in &lt;code&gt;PYPI_CONFIG_FILE&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;release-pypi-test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;release&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_REF_NAME == "staging"&lt;/span&gt;
      &lt;span class="na"&gt;allow_failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH&lt;/span&gt;
      &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;manual&lt;/span&gt;
      &lt;span class="na"&gt;allow_failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build-wheel&lt;/span&gt;
      &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;python3 -m pip install twine&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;etc/./upload-pypi.sh testpypi "${PYPI_CONFIG_FILE}" build/release/*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Patience is a virtue
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcgdp0quh54t3rt10soj6.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcgdp0quh54t3rt10soj6.jpeg" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Since the PyPi package cache seem to need a few cycles to catch up with the updated index, &lt;code&gt;delay-before-checking-pypi-test&lt;/code&gt; sits between the upload and following test target.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;delay-before-checking-pypi-test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;grace&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_REF_NAME == "staging"&lt;/span&gt;
  &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;DELAY_IN_SECONDS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;13&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Sleeping for ${DELAY_IN_SECONDS} seconds ..."&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sleep ${DELAY_IN_SECONDS}&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Done."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Let's go live! (well almost)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--trusted-host&lt;/span&gt; test.pypi.org &lt;span class="nt"&gt;--trusted-host&lt;/span&gt; test-files.pythonhosted.org &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--index-url&lt;/span&gt; https://test.pypi.org/simple/ &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--extra-index-url&lt;/span&gt; https://pypi.org/simple/ &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;piper_whistle&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; src.piper_whistle.version&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After patiently waiting our turn, we check if pip is able to fetch the newly released wheel file via a carefully crafted &lt;code&gt;pip install&lt;/code&gt; invocation. The &lt;em&gt;--index-url&lt;/em&gt; argument specifies the test server index and the &lt;em&gt;--trusted-host&lt;/em&gt; arguments ensures seamless binary downloads from the test servers. It turned out, that some dependencies where not available on the test server, which necessitated the &lt;code&gt;--extra-index-url&lt;/code&gt; parameter pointing to the index of the live server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvlrboggm0crbdy04zd6p.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvlrboggm0crbdy04zd6p.jpeg" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, to ensure the current latest version of the staging branch is fetched from the test server and not the most recent version from the live server, the version module of piper-whistle is invoked to obtain the semantic version name. In the build script, this &lt;a href="https://gitlab.com/think-biq/piper-whistle/-/blob/release/etc/install-test-pypi.sh?ref_type=heads" rel="noopener noreferrer"&gt;command is contained&lt;/a&gt; within the &lt;code&gt;install-test-pypi.sh&lt;/code&gt; shell script for brevity. The target &lt;code&gt;test-release-pypi-test&lt;/code&gt; is only available on the staging branch and is automatically triggered.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;test-release-pypi-test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;post-release-test&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_REF_NAME == "staging"&lt;/span&gt;
      &lt;span class="na"&gt;allow_failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;python3 -m pip install -U pip&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;export PGK_NAME="piper_whistle==$(python3 -m src.piper_whistle.version)"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;etc/./install-test-pypi.sh ${PGK_NAME}&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;piper_whistle -h&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Setting up search index ..."&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;piper_whistle refresh&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Checking if HU is supported ..."&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;if [ "hu_HU" = "$(piper_whistle guess hung)" ];&lt;/span&gt;
        &lt;span class="s"&gt;then echo "Found hungarian language support";&lt;/span&gt;
        &lt;span class="s"&gt;else exit 13;&lt;/span&gt;
      &lt;span class="s"&gt;fi&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Checking if model at index 1 is available ..."&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;piper_whistle list -U -l hu_HU -i &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Installing model 1 ..."&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;piper_whistle install hu_HU &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Fetching path of model 1 ..."&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;export HU_VOICE_INFO=$(piper_whistle list -S -l hu_HU -i 1)&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;export VOICE_NAME="$(echo ${HU_VOICE_INFO} | awk '{ print $1 }')"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;piper_whistle path ${VOICE_NAME}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After installing the package it simply goes through refresh, setup and downloads a voice to test basic functionality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting serious
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fanmsordmv1xffdl0wuf0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fanmsordmv1xffdl0wuf0.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Publishing to the main index
&lt;/h3&gt;

&lt;p&gt;Analogous to the test release target, &lt;code&gt;release-pypi&lt;/code&gt; is virtually the same, except it is trigger on the release branch and uses the release server configuration in &lt;code&gt;PYPI_LIVE_CONFIG_FILE&lt;/code&gt; instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;release-pypi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;release&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_REF_NAME == "release"&lt;/span&gt;
      &lt;span class="na"&gt;allow_failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build-wheel&lt;/span&gt;
      &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Uploading ..."&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ls -lav build/release&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;python3 -m pip install twine&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;etc/./upload-pypi.sh pypi "${PYPI_LIVE_CONFIG_FILE}" build/release/*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Gitlab release on repository site
&lt;/h3&gt;

&lt;p&gt;To allow people to &lt;a href="https://gitlab.com/think-biq/piper-whistle/-/releases" rel="noopener noreferrer"&gt;directly download the latest built&lt;/a&gt; from piper-whistle's repository, the &lt;code&gt;release-gitlab&lt;/code&gt; target makes use of GitLab's base image &lt;a href="https://gitlab.com/gitlab-org/release-cli" rel="noopener noreferrer"&gt;release-cli&lt;/a&gt; exposing attributes for the creation of a new release page. It uses GitLab's release tool to process said attributes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;release-gitlab&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.gitlab.com/gitlab-org/release-cli:latest&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;release&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_REF_NAME == "release"&lt;/span&gt;
      &lt;span class="na"&gt;allow_failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build-wheel&lt;/span&gt;
      &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo Tag name = "${VERSION_TAG}"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo Release name = "piper-whistle ${VERSION_TAG} wheel"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo Release notes = "$(cat release-notes.md)"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo Wheel file link = "${WHEEL_FILE_URL}"&lt;/span&gt;
  &lt;span class="na"&gt;release&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;piper-whistle&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${VERSION_TAG}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;wheel'&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;release-notes.md'&lt;/span&gt;
    &lt;span class="na"&gt;tag_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${VERSION_TAG}'&lt;/span&gt;
    &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$CI_COMMIT_SHA'&lt;/span&gt;
    &lt;span class="na"&gt;assets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;links&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${WHEEL_FILE_NAME}'&lt;/span&gt;
          &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${WHEEL_FILE_URL}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we make use of the environment variables exposed in the &lt;code&gt;build-wheel&lt;/code&gt; target, as well as use the release notes stored in &lt;code&gt;release-notes.md&lt;/code&gt; for the description.&lt;/p&gt;

&lt;p&gt;The wheel file download link is generated with the help of a &lt;a href="https://pypi.debian.net/" rel="noopener noreferrer"&gt;front-end to PyPi&lt;/a&gt; provided by the &lt;a href="https://www.debian.org/" rel="noopener noreferrer"&gt;Debian Project&lt;/a&gt;. It may take several minutes until the front-end can find the package in the cache.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8c4qo63ovya6pkmac5o5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8c4qo63ovya6pkmac5o5.png" alt="Release 1.6.188 via GitLab's repository website."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Github release
&lt;/h3&gt;

&lt;p&gt;With the help of &lt;a href="https://gitlab.com/think-biq/piper-whistle/-/blob/release/src/tools/rota.py?ref_type=heads" rel="noopener noreferrer"&gt;rota&lt;/a&gt;, we can make the new release available on the github hosted repository via the &lt;code&gt;github-release&lt;/code&gt; command. It needs an access token stored in a file, the repository ID in the form of &lt;code&gt;{username}/{project}&lt;/code&gt;, the version name and the release message.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;release-github&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;release&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_REF_NAME == "release"&lt;/span&gt;
      &lt;span class="na"&gt;allow_failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build-wheel&lt;/span&gt;
      &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo Tag name = "${VERSION_TAG}"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo Release name = "piper-whistle ${VERSION_TAG} wheel"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo Release notes = "$(cat release-notes.md)"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo Wheel file link = "${WHEEL_FILE_URL}"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Uploading release "${VERSION_TAG}" to github ..."&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;python3 -m src.tools.rota github-release&lt;/span&gt;
      &lt;span class="s"&gt;--token-file "${GITHUB_API_KEY_FILE}"&lt;/span&gt;
      &lt;span class="s"&gt;--attachment "build/release/${WHEEL_FILE_NAME}"&lt;/span&gt;
      &lt;span class="s"&gt;think-biq/piper-whistle release "${VERSION_TAG}" "$(cat release-notes.md)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It sends a request to the &lt;a href="https://docs.github.com/en/rest/releases?apiVersion=2022-11-28" rel="noopener noreferrer"&gt;github API&lt;/a&gt; asking for a new release to be created. If that was successful, it retrieves the dedicated upload endpoint for the specific release and &lt;a href="https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#upload-a-release-asset" rel="noopener noreferrer"&gt;uploads the file&lt;/a&gt; specified via &lt;em&gt;--attachment&lt;/em&gt; (if any).&lt;/p&gt;

&lt;h2&gt;
  
  
  Another page in the book
&lt;/h2&gt;

&lt;p&gt;To keep the documentation &lt;a href="https://think-biq.gitlab.io/piper-whistle/" rel="noopener noreferrer"&gt;up to date and readily available&lt;/a&gt;, the target pages builds a web release of the &lt;a href="https://peps.python.org/pep-0257/" rel="noopener noreferrer"&gt;doc-strings&lt;/a&gt; found throughout the source code via &lt;a href="https://doxygen.nl/" rel="noopener noreferrer"&gt;doxygen&lt;/a&gt; and hosts it through &lt;a href="https://docs.gitlab.com/ee/user/project/pages/" rel="noopener noreferrer"&gt;GitLab’s pages&lt;/a&gt; infrastructure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;pages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;finalize&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_REF_NAME == "release"&lt;/span&gt;
      &lt;span class="na"&gt;allow_failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH&lt;/span&gt;
      &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;manual&lt;/span&gt;
      &lt;span class="na"&gt;allow_failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Install requirements ..."&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;make pip-update-all&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Building documentation ..."&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;make docs-build&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Exposing as public ..."&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mv docs/published public&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Result:"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ls -lav public&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Hosting ..."&lt;/span&gt;
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;public&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  readthedocs.org
&lt;/h3&gt;

&lt;p&gt;Additionally, a copy of most &lt;a href="https://piper-whistle.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;recent documentation based off of the main branch&lt;/a&gt; is maintained on readthedocs.org servers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2lucrywk6zu6sm5i1mp2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2lucrywk6zu6sm5i1mp2.jpg" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When pushing to the main branch, readthedocs will scan the root path of the repository and look for a &lt;code&gt;.readthedocs.yaml&lt;/code&gt; configuration file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;

&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-20.04&lt;/span&gt;
  &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;python&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.8"&lt;/span&gt;
  &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Install dependencies ..."&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;make pip-update-all || echo 💥&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Building documentation using doxygen ..."&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;make docs-build || echo 💥&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Ensuring html directory ..."&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mkdir -p "$(realpath ${READTHEDOCS_OUTPUT})/html"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Moving page assets ..."&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mv docs/published/* "$(realpath ${READTHEDOCS_OUTPUT})/html" || echo 🚚💥&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Per Aspera Ad Astra 🚀"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It’s very convenient, to have straight forward support for the static page setup which is produced by &lt;em&gt;doxygen&lt;/em&gt;. We only need to make sure to move the generated artifacts to the right place. This would be &lt;code&gt;${READTHEDOCS_OUTPUT}/html&lt;/code&gt;. Since I’ve encountered issues where the path was malformed (i.e. double /), &lt;code&gt;realpath&lt;/code&gt; is used to normalize the path before handing it off to the &lt;code&gt;mv&lt;/code&gt; command. The way to the stars seems a bit clearer now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Harmonious Hustle: GitLab CI/CD's Dance with GitHub for Seamless Repository Sync
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhdyn3jso9unba97v9r1r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhdyn3jso9unba97v9r1r.png" alt="In sync."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To increase the reach of piper-whistle and a greater change of someone contributing or reporting any issues, keeping the &lt;a href="https://github.com/think-biq/piper-whistle" rel="noopener noreferrer"&gt;project in sync with GitHub&lt;/a&gt; seems like a reasonable ambition. Let's look at how me way do so with a pipeline target called &lt;code&gt;sync&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;sync&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;finalize&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_REF_NAME == "release"&lt;/span&gt;
      &lt;span class="na"&gt;allow_failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH&lt;/span&gt;
      &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;manual&lt;/span&gt;
      &lt;span class="na"&gt;allow_failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;apt-get update -y&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;apt-get install -yqqf openssh-client sshpass --fix-missing&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;eval $(ssh-agent -s)&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cat "${GITHUB_DEPLOY_KEY_FILE}" | tr -d '\r' | ssh-add - &amp;gt; /dev/null&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mkdir -p ~/.ssh&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;chmod 700 ~/.ssh&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ssh-keyscan ${GITHUB_DEPLOY_DOMAIN} &amp;gt;&amp;gt; ~/.ssh/known_hosts&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;chmod 644 ~/.ssh/known_hosts&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;git config --global user.email "${GITHUB_USER_EMAIL}"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;git config --global user.name "${GITHUB_USER_NAME}"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;git fetch --all&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;git checkout staging&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;git checkout release&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;git checkout main&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;git remote rm origin 2&amp;gt; /dev/null&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;git remote add origin ${GITHUB_REPO_LINK}&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;git push --all&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;git push --tags&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To achieve this, the setup uses a container local ssh user setup, leveraging the &lt;a href="https://docs.github.com/en/authentication/connecting-to-github-with-ssh/managing-deploy-keys" rel="noopener noreferrer"&gt;deploy key infrastructure&lt;/a&gt; offered by GitHub. After setting up the key and known_host configuration, changing the remote origin entry allows for pushing new commits as well as tags. Additionally, it may be appropriate to only offer this automation on the main branch and set it's trigger to manual, so not every commit grinds the pipeline budget.&lt;/p&gt;

&lt;p&gt;You could just do this manually, by adding a separate remote for GitHub and pushing directly, but where would the fun be with this?&lt;/p&gt;

&lt;h2&gt;
  
  
  Extra fun
&lt;/h2&gt;

&lt;p&gt;For some extra spice, the current status of the release pipeline and readthedocs status is shown as badges on the top of the readme.&lt;/p&gt;

&lt;p&gt;To obtain a badge for your projects pipeline, only taking into account non-skipped targets:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgitlab.com%2Fthink-biq%2Fpiper-whistle%2Fbadges%2Frelease%2Fpipeline.svg%3Fignore_skipped%3Dtrue" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgitlab.com%2Fthink-biq%2Fpiper-whistle%2Fbadges%2Frelease%2Fpipeline.svg%3Fignore_skipped%3Dtrue" alt="release"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;f"https://gitlab.com/{namespace}/{project}/badges/{branch}/pipeline.svg?ignore_skipped=true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A badge showing the latest / most recent release tag, you may use:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgitlab.com%2Fthink-biq%2Fpiper-whistle%2F-%2Fbadges%2Frelease.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgitlab.com%2Fthink-biq%2Fpiper-whistle%2F-%2Fbadges%2Frelease.svg" alt="release"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;f"https://gitlab.com/{namespace}/{project}/-/badges/release.svg"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The readthedocs.org badge may be constructed via:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fuser-content.gitlab-static.net%2F1febe753d6976b6ea4d05c8d04b5be704c426fc5%2F68747470733a2f2f72656164746865646f63732e6f72672f70726f6a656374732f70697065722d77686973746c652f62616467652f3f76657273696f6e3d6c6174657374" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fuser-content.gitlab-static.net%2F1febe753d6976b6ea4d05c8d04b5be704c426fc5%2F68747470733a2f2f72656164746865646f63732e6f72672f70726f6a656374732f70697065722d77686973746c652f62616467652f3f76657273696f6e3d6c6174657374" alt="release"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;f"https://readthedocs.org/projects/{project_slug}/badge/?version={version}&amp;amp;style={style}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;Thank you for reading and I wish you all the best on your automation adventures. To celebrate the mysteries before us, let’s share in spirit with these attributed sayings:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“At last concluded that no creature was more miserable than man, for that all other creatures are content with those bounds that nature set them, only man endeavors to exceed them.” ― Desiderius Erasmus Roterodamus&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd77t3iwmc48ey6t8x0k4.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd77t3iwmc48ey6t8x0k4.jpeg" alt="Traveling; north, east, south or west. Where's the wisdom, where's your zest."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I have so many ideas that may perhaps be of some use in time if others more penetrating than I go deeply into them someday and join the beauty of their minds to the labour of mine.” ― Gottfried Wilhelm Leibniz&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Encore
&lt;/h1&gt;

&lt;p&gt;So, you are still reading? Let’s do some math and statistics then. Using the following script to stalk through the repository and separating the code for the package in &lt;code&gt;piper_whistle&lt;/code&gt; from all the rest of the scripts and configuration files, we can calculate a ratio of &lt;strong&gt;lines of code to produce&lt;/strong&gt; to &lt;strong&gt;lines of code in production&lt;/strong&gt;. Something like a build-infrastructure efficiency coefficient, or &lt;strong&gt;biec&lt;/strong&gt; for short.&lt;/p&gt;

&lt;h2&gt;
  
  
  The script: biec.sh
&lt;/h2&gt;

&lt;p&gt;This approach is not omitting any comments or white-spaces, so keep that in mind.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;count-lines-of-code-package &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;# Counts only source files in main package.&lt;/span&gt;

  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find ./src/piper_whistle &lt;span class="nt"&gt;-iname&lt;/span&gt; &lt;span class="s2"&gt;"*.py"&lt;/span&gt; &lt;span class="nt"&gt;-type&lt;/span&gt; f&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;files&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{ print $1 }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{ s+=$1 } END { printf "%.0f", s }'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;0
&lt;span class="o"&gt;}&lt;/span&gt;

count-lines-of-code-tooling &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;# Collects all source files relevant for tooling, building and configuring.&lt;/span&gt;

  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-iname&lt;/span&gt; &lt;span class="s2"&gt;"*.py"&lt;/span&gt; &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/src/piper_whistle/*"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/build/lib/*"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-iname&lt;/span&gt; &lt;span class="s2"&gt;"*.sh"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-iname&lt;/span&gt; &lt;span class="s2"&gt;"makefile"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-iname&lt;/span&gt; &lt;span class="s2"&gt;"pip.conf"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-iname&lt;/span&gt; &lt;span class="s2"&gt;"*.cfg"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-iname&lt;/span&gt; &lt;span class="s2"&gt;"*.yaml"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-iname&lt;/span&gt; &lt;span class="s2"&gt;"*.yml"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;files&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{ print $1 }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{ s+=$1 } END { printf "%.0f", s }'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;0
&lt;span class="o"&gt;}&lt;/span&gt;

main &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;# Investigate the codebase and create a ratio of 'lines of code in production' to 'lines of code to produce'&lt;/span&gt;

  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;root_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;package_lines&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;tool_lines&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;ratio&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0

  &lt;span class="nb"&gt;pushd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;root_path&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
    &lt;span class="nv"&gt;package_lines&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;count-lines-of-code-package&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;tool_lines&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;count-lines-of-code-tooling&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;popd&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
  &lt;span class="nv"&gt;ratio&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;tool_lines&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; / &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;package_lines&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.0"&lt;/span&gt; | bc &lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"package: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;package_lines&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"tooling: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;tool_lines&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ratio: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ratio&lt;/span&gt;:0:6&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;0
&lt;span class="o"&gt;}&lt;/span&gt;

main &lt;span class="nv"&gt;$*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hint: Make sure to not place this script in the actual directory of the repository :)&lt;/p&gt;

&lt;h2&gt;
  
  
  Analyzing
&lt;/h2&gt;

&lt;p&gt;Invoking it via &lt;code&gt;biec.sh ~/Workspace/Remote/piper-whistle&lt;/code&gt; on release &lt;a href="https://gitlab.com/think-biq/piper-whistle/-/tree/1.6.213?ref_type=tags" rel="noopener noreferrer"&gt;1.6.213&lt;/a&gt; yields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;package&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2050&lt;/span&gt;
&lt;span class="na"&gt;tooling&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4080&lt;/span&gt;
&lt;span class="na"&gt;ratio&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.9902&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So I have virtually twice as much lines of code supporting the maintenance, testing and publishing of the actual &lt;code&gt;piper-whistle&lt;/code&gt; python package. Looking at the files that were taken into account, the heaviest appears to be the doxygen documentation configuration file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;2 ./setup.cfg
70 ./makefile
36 ./etc/query-wheel-url-from-debian.sh
45 ./etc/query-tag-most-relevant-version.sh
12 ./etc/install-test-pypi.sh
20 ./etc/query-tag-message.sh
19 ./etc/upload-pypi.sh
8 ./src/__init__.py
1 ./src/testing/__init__.py
380 ./src/testing/functional.py
0 ./src/tools/__init__.py
217 ./src/tools/tmplr.py
363 ./src/tools/rota.py
19 ./.readthedocs.yaml
20 ./.github/workflows/publish.yml
58 ./setup.py
19 ./docs/makefile
2534 ./docs/docs.cfg
4 ./pip.conf
253 ./.gitlab-ci.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It alone is responsible for 2534 &lt;a href="https://en.wikipedia.org/wiki/Source_lines_of_code" rel="noopener noreferrer"&gt;LOC&lt;/a&gt; (62.1%). When excluding this config, we have a more reasonable result of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;package&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2050&lt;/span&gt;
&lt;span class="na"&gt;tooling&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1546&lt;/span&gt;
&lt;span class="na"&gt;ratio&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.7541&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Discussion
&lt;/h2&gt;

&lt;p&gt;So roughly for every 4 lines of package code, there are 3 lines of supporting infrastructure code. I would be so bold as to state that the lines of code needed to be included when scaling the actual program does not grow as fast as the lines of code in the package. I would estimate about &lt;em&gt;10% growth&lt;/em&gt; of infrastructure code in relation to the product code. Under this assumption, we would reach a &lt;strong&gt;biec&lt;/strong&gt; of &lt;strong&gt;0.5&lt;/strong&gt; at about &lt;em&gt;3352 LOC&lt;/em&gt; for the product. As a reference, &lt;a href="https://pypi.org/project/PyYAML/" rel="noopener noreferrer"&gt;&lt;em&gt;PyYAML&lt;/em&gt;&lt;/a&gt; version 6.0.1 has &lt;em&gt;5890 LOC&lt;/em&gt;. Recalculating for that line count, would result in a &lt;strong&gt;biec&lt;/strong&gt; of about &lt;strong&gt;0.33&lt;/strong&gt; for PyYAML.&lt;br&gt;
Might be fun to test this on other (build-)setups and packages as well.&lt;/p&gt;

&lt;h1&gt;
  
  
  Follow-up
&lt;/h1&gt;

&lt;p&gt;If you feel inspired and want to get in touch, you may reach me on &lt;a href="https://twitter.com/blurryroots" rel="noopener noreferrer"&gt;twitter / X&lt;/a&gt;, &lt;a href="https://discordapp.com/users/232489168477880320" rel="noopener noreferrer"&gt;discord&lt;/a&gt; or &lt;a href="https://www.linkedin.com/in/blurryroots" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>automation</category>
      <category>gitlab</category>
      <category>pypi</category>
    </item>
    <item>
      <title>When life gives you lemons ...</title>
      <dc:creator>Sven Freiberg</dc:creator>
      <pubDate>Sun, 31 Dec 2023 14:21:13 +0000</pubDate>
      <link>https://forem.com/blurryroots/when-life-gives-you-lemons--316m</link>
      <guid>https://forem.com/blurryroots/when-life-gives-you-lemons--316m</guid>
      <description>&lt;p&gt;Recently, I found myself reminiscing about games that really stuck with me. One of those titles is &lt;em&gt;Theme Hospital&lt;/em&gt;. Luckily, there are people with enough love for these &lt;a href="https://www.gog.com/"&gt;good old games&lt;/a&gt;, as to make them available to be run on modern operating systems. Although &lt;em&gt;GOG&lt;/em&gt; usually has most bases covered, &lt;em&gt;Theme Hospital&lt;/em&gt; appears to be an exception. A &lt;a href="https://en.wikipedia.org/wiki/GNU/Linux_naming_controversy"&gt;GNU/Linux&lt;/a&gt; version is unfortunately not available, which prompted me to investigate the available ports and see if there may be a way to get them running on my machine anyhow. It turns out the &lt;em&gt;MacOS&lt;/em&gt; package has everything we would need for just such an endeavor. After downloading the most recent &lt;a href="https://www.fileextensiongeek.com/pak/"&gt;PAK&lt;/a&gt; file (at the time of this writing theme_hospital_enUS_1_0_3_33062.pkg), we need a way to extract its contents. The container is compressed using &lt;a href="https://docs.fileformat.com/compression/xar/"&gt;XAR&lt;/a&gt;, so options are limited. Luckily, &lt;a href="https://www.7-zip.org/"&gt;7z&lt;/a&gt; does support this format.&lt;/p&gt;

&lt;p&gt;Let's setup a temporary place for the archive contents and change the directory via&lt;br&gt;
&lt;code&gt;mkdir -p /tmp/theme-hospital-pak &amp;amp;&amp;amp; cd /tmp/theme-hospital-pak&lt;/code&gt;. Now we can extract the contents via &lt;code&gt;7z x ${HOME}/theme_hospital_enUS_1_0_3_33062.pkg&lt;/code&gt;, which tells &lt;em&gt;7 zip&lt;/em&gt; to place the contents into the directory we are currently executing this command from. The result should look something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;-rw-rw-r--&lt;/span&gt; 1 goggy  780 Oct 21 13:26  Distribution
drwx------ 1 goggy    0 Jan  1  1970  package.pkg/
drwx------ 1 goggy    0 Jan  1  1970  Resources/
&lt;span class="nt"&gt;-rw-rw-r--&lt;/span&gt; 1 goggy  14K Oct 21 13:26 &lt;span class="s1"&gt;'[TOC].xml'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now if we look into &lt;code&gt;package.pkg/&lt;/code&gt; we find two files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;package.pkg
├── PackageInfo
└── Scripts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file Scripts is another archive compressed via &lt;a href="https://www.gzip.org/"&gt;gzip&lt;/a&gt;. So let's decompress via &lt;code&gt;cat package.pkg/Scripts | gzip -dc &amp;gt; unzipped&lt;/code&gt;. The newly created file unzipped is another archive. It can be processed by &lt;a href="https://www.gnu.org/software/cpio/"&gt;cpio&lt;/a&gt;. Let's prepare a directory &lt;code&gt;mkdir /tmp/theme-hospital-pak/raw&lt;/code&gt; and extract the raw contents into it via &lt;code&gt;cat unzipped | cpio -i -D raw&lt;/code&gt;, after which it will tell us how many blocks it restored. Something like: &lt;code&gt;399282 blocks&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Looking into raw, we find a directory called &lt;code&gt;payload&lt;/code&gt;. In it, we can spot the actual game data, an embedded &lt;a href="https://www.dosbox.com/"&gt;DOSBox&lt;/a&gt; version, as well as scripts to start the game. Since this is a &lt;em&gt;MacOS&lt;/em&gt; package, we cannot use this version of &lt;em&gt;DOSBox&lt;/em&gt; and need a local installation for whichever &lt;em&gt;GNU/Linux&lt;/em&gt; flavor you are running. If we wish to start a single-player session, we'd need to make use of the core &lt;em&gt;DOSBox&lt;/em&gt; config file, and the single player specific config file.&lt;/p&gt;

&lt;p&gt;First, we need to change the execution environment to be the same directory as the game directory. In this case, &lt;code&gt;cd /tmp/theme-hospital-pak/raw/payload/Contents/Resources&lt;/code&gt;. Assuming the local &lt;em&gt;DOSBox&lt;/em&gt; installation is available in &lt;code&gt;$PATH&lt;/code&gt;, we may now call &lt;code&gt;dosbox -conf game/dosboxTH.conf -conf game/dosboxTH_single.conf&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Since I thought this process might as well be documented in code, I came up with a tool called &lt;em&gt;goggy&lt;/em&gt;. It allows you to do this process with the aforementioned classic &lt;code&gt;Theme Hospital&lt;/code&gt; and any other game packaged in a similar way. Check out the source code at &lt;a href="https://github.com/BlurryRoots/ws-goggy"&gt;https://github.com/BlurryRoots/ws-goggy&lt;/a&gt; and have a great time looking after &lt;a href="https://retrogamer.biz/wp-content/uploads/2015/10/Theme-Hospital-manual.pdf"&gt;those patients in need of assistance&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>gamedev</category>
      <category>tooling</category>
      <category>compression</category>
    </item>
    <item>
      <title>Setting up local speech synthesizing using piper and piper-whistle</title>
      <dc:creator>Sven Freiberg</dc:creator>
      <pubDate>Mon, 18 Dec 2023 14:48:18 +0000</pubDate>
      <link>https://forem.com/blurryroots/setting-up-local-speech-synthesizing-using-piper-and-piper-whistle-32n6</link>
      <guid>https://forem.com/blurryroots/setting-up-local-speech-synthesizing-using-piper-and-piper-whistle-32n6</guid>
      <description>&lt;p&gt;Pre-requisits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GNU/Linux (equivalent to Ubuntu 20.04)&lt;/li&gt;
&lt;li&gt;python &amp;gt;=3.8&lt;/li&gt;
&lt;li&gt;piper-whistle - &lt;a href="https://gitlab.com/think-biq/piper-whistle/-/releases"&gt;https://gitlab.com/think-biq/piper-whistle/-/releases&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;piper - &lt;a href="https://github.com/rhasspy/piper/releases"&gt;https://github.com/rhasspy/piper/releases&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;aplay - &lt;a href="https://linux.die.net/man/1/aplay"&gt;https://linux.die.net/man/1/aplay&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's take a look at how one may setup an ad-hoc, local / offline text-to-speech synthesizer with great voice quality using piper.&lt;/p&gt;

&lt;p&gt;To setup piper on a GNU/Linux based system, I'll describe a general architecture using named pipes, which is straight forward enough to allow for system wide text-to-speech, with a little bit of manual setup, the help of piper-whistle and some minor trade-offs (it's simple, yet it won't support parallel speech processing).&lt;/p&gt;

&lt;p&gt;To start, let's fetch the latest piper stand-alone built from its repository hosted on github (&lt;em&gt;2023.11.14-2&lt;/em&gt; at the time of writing this). After downloading the compressed archive, we'll create a directory structure for our setup. The root directory shall be at &lt;code&gt;/opt/wind&lt;/code&gt; and the following sub-directories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/opt/wind/piper&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/opt/wind/channels&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Decompress and copy the piper built into &lt;code&gt;/opt/wind/piper&lt;/code&gt;.&lt;br&gt;
For managing piper's voice models, we use piper-whistle.&lt;sup id="fnref1"&gt;1&lt;/sup&gt; A command-line utility, which makes it more convenient to download and access voices.&lt;/p&gt;

&lt;p&gt;You can get the latest release using &lt;code&gt;pip install piper-whistle&lt;/code&gt; or download the wheel file from its gitlab releases page. After installing whistle, let's fetch a voice to generate speech with. First step is updating the database by calling &lt;code&gt;piper_whistle -vR&lt;/code&gt;. For English speech, I quite like the female voice called alba. Using whistle, we can get list all available english (GB) voices using &lt;code&gt;piper_whistle list -l en_GB&lt;/code&gt;. The voice is at index 2. So to install simply call &lt;code&gt;piper_whistle install en_GB 2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Next, let's create the neccessary named pipes. The resulting structure will look like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/opt/wind/channels/speak&lt;/code&gt; (accepts json payload)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/opt/wind/channels/input&lt;/code&gt; (read by piper)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/opt/wind/channels/ouput&lt;/code&gt; (written by piper)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To create a named pipe, we can use the following command: &lt;code&gt;mkfifo -m 755 /opt/wind/channels/input&lt;/code&gt;&lt;br&gt;
Finally, we create three processes in separate shells:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tty0: &lt;code&gt;tail -F /opt/wind/channels/speak | tee /opt/wind/channels/input&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;tty1: &lt;code&gt;/opt/wind/piper/piper -m $(piper_whistle path alba@medium) --debug --json-input --output_raw &amp;lt; /opt/wind/channels/input &amp;gt; /opt/wind/channels/output&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;tty2: &lt;code&gt;aplay --buffer-size=777 -r 22050 -f S16_LE -t raw &amp;lt; /opt/wind/channels/output&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The process on tty0, ensures the pipe is kept open after the processing by piper or aplay. This way, we can subsequently queue TTS requests. Using the structure above, we now use piper-whistle to generate speech, for example:&lt;br&gt;
&lt;code&gt;piper_whistle speak "If the infinite had not desired man to be wise, he would not have bestowed upon him the faculty of knowing."&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Hope this will help you with your text-to-speech needs. If you have any ideas for improvement or found and issue with piper-whistle, feel free to open a &lt;a href="https://gitlab.com/think-biq/piper-whistle/-/issues"&gt;ticket on its repository&lt;/a&gt;, &lt;a href="https://twitter.com/blurryroots"&gt;reach out on twitter&lt;/a&gt; or &lt;a href="https://discord.gg/hwTFBWBZSK"&gt;join my discord&lt;/a&gt;. Thanks for reading and until next time.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;Yes, I'm the author of the package :) ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>opensource</category>
      <category>tooling</category>
    </item>
  </channel>
</rss>
