<?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>DEV Community: Maxim Radugin</title>
    <description>The latest articles on DEV Community by Maxim Radugin (@maxim_radugin).</description>
    <link>https://dev.to/maxim_radugin</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%2F1743941%2F0933f5bd-f463-4f32-835d-26001b663555.png</url>
      <title>DEV Community: Maxim Radugin</title>
      <link>https://dev.to/maxim_radugin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/maxim_radugin"/>
    <language>en</language>
    <item>
      <title>Monitor Progress of Data Transfer in Console</title>
      <dc:creator>Maxim Radugin</dc:creator>
      <pubDate>Thu, 09 Oct 2025 20:15:06 +0000</pubDate>
      <link>https://dev.to/maxim_radugin/monitor-progress-of-data-transfer-in-console-2igj</link>
      <guid>https://dev.to/maxim_radugin/monitor-progress-of-data-transfer-in-console-2igj</guid>
      <description>&lt;p&gt;Linux and UNIX-based OS'es provide at least two handy command-line tools to monitor data transfer progress: &lt;code&gt;bar&lt;/code&gt; and &lt;code&gt;pv&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Both display a one-line progress bar with speed, elapsed or estimated time, and provide customizable formatting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Examples with &lt;code&gt;bar&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If data size is unknown:&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="nb"&gt;dd &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/zero | bar | &lt;span class="nb"&gt;dd &lt;/span&gt;&lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/null
   2.3GB at  581.8MB/s  elapsed:   0:00:04
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If data size is known:&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="nb"&gt;dd &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/zero &lt;span class="nv"&gt;iflag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;count_bytes &lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10G | bar &lt;span class="nt"&gt;--size&lt;/span&gt; 10G | &lt;span class="nb"&gt;dd &lt;/span&gt;&lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/null
   1.2GB at  592.8MB/s  eta:   0:00:15   11% &lt;span class="o"&gt;[=====&lt;/span&gt;                               &lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Examples with &lt;code&gt;pv&lt;/code&gt;:&lt;/strong&gt;&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="nb"&gt;dd &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/zero &lt;span class="nv"&gt;iflag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;count_bytes &lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10G | pv | &lt;span class="nb"&gt;dd &lt;/span&gt;&lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/null
2.59GiB 0:00:06 &lt;span class="o"&gt;[&lt;/span&gt; 397MiB/s] &lt;span class="o"&gt;[&lt;/span&gt;      &amp;lt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;                                            &lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;dd &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/zero &lt;span class="nv"&gt;iflag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;count_bytes &lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10G | pv &lt;span class="nt"&gt;--size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10G | &lt;span class="nb"&gt;dd &lt;/span&gt;&lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/null
2.59GiB 0:00:06 &lt;span class="o"&gt;[&lt;/span&gt; 467MiB/s] &lt;span class="o"&gt;[========&amp;gt;&lt;/span&gt;                            &lt;span class="o"&gt;]&lt;/span&gt; 25% ETA 0:00:17
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pv&lt;/code&gt; can also count lines instead of bytes, useful for monitoring log or message rates:&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="nb"&gt;sudo &lt;/span&gt;dmesg &lt;span class="nt"&gt;-w&lt;/span&gt; | pv &lt;span class="nt"&gt;--line&lt;/span&gt; &lt;span class="nt"&gt;--rate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;span class="o"&gt;[&lt;/span&gt; 496k/s]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a side note, &lt;code&gt;dd&lt;/code&gt; can also report progress at runtime, when &lt;code&gt;status=progress&lt;/code&gt; is specified.&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="nb"&gt;dd &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/zero &lt;span class="nv"&gt;iflag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;count_bytes &lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10G &lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/null &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;progress
2791971840 bytes &lt;span class="o"&gt;(&lt;/span&gt;2.8 GB, 2.6 GiB&lt;span class="o"&gt;)&lt;/span&gt; copied, 3 s, 931 MB/s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>linux</category>
      <category>terminal</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>Run Applications in Rosetta on macOS</title>
      <dc:creator>Maxim Radugin</dc:creator>
      <pubDate>Thu, 09 Oct 2025 20:05:24 +0000</pubDate>
      <link>https://dev.to/maxim_radugin/run-applications-in-rosetta-on-macos-770</link>
      <guid>https://dev.to/maxim_radugin/run-applications-in-rosetta-on-macos-770</guid>
      <description>&lt;p&gt;I needed to run &lt;code&gt;Terminal&lt;/code&gt; in &lt;code&gt;x86_64&lt;/code&gt; mode (Rosetta) in parallel with &lt;code&gt;Terminal&lt;/code&gt; in &lt;code&gt;arm64&lt;/code&gt; (Apple Silicon native mode), but in the latest versions of macOS, the old way of creating a copy of the &lt;code&gt;Terminal&lt;/code&gt; application and selecting &lt;code&gt;Open using Rosetta&lt;/code&gt; in &lt;code&gt;Get Info&lt;/code&gt; is not available, since &lt;code&gt;Terminal&lt;/code&gt; can't be copied anymore due to security restrictions.&lt;/p&gt;

&lt;p&gt;To overcome this, you can use the following command to open any application in Rosetta mode from either &lt;code&gt;Terminal&lt;/code&gt; or &lt;code&gt;Automator&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;open &lt;span class="nt"&gt;-a&lt;/span&gt; Terminal &lt;span class="nt"&gt;--new&lt;/span&gt; &lt;span class="nt"&gt;--arch&lt;/span&gt; x86_64
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will open a new instance of the &lt;code&gt;Terminal&lt;/code&gt; application. To verify Intel emulation mode, running the &lt;code&gt;arch&lt;/code&gt; command should output &lt;code&gt;i386&lt;/code&gt; as opposed to &lt;code&gt;arm64&lt;/code&gt; when running natively.&lt;/p&gt;

&lt;p&gt;To switch to &lt;code&gt;x86_64&lt;/code&gt; in an already running &lt;code&gt;Terminal&lt;/code&gt; session, execute the following command. Note that &lt;code&gt;~/.zprofile&lt;/code&gt; and other shell setup scripts will not be sourced, and the new shell running under &lt;code&gt;x86_64&lt;/code&gt; will inherit the initial shell environment.&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="nb"&gt;arch&lt;/span&gt; &lt;span class="nt"&gt;-arch&lt;/span&gt; x86_64 &lt;span class="nv"&gt;$SHELL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>osx</category>
      <category>rosetta</category>
      <category>terminal</category>
    </item>
    <item>
      <title>Creating and Restoring Disk Images on macOS: A Guide to Using dd and diskutil</title>
      <dc:creator>Maxim Radugin</dc:creator>
      <pubDate>Thu, 09 Oct 2025 19:49:05 +0000</pubDate>
      <link>https://dev.to/maxim_radugin/creating-and-restoring-disk-images-on-macos-a-guide-to-using-dd-and-diskutil-34a7</link>
      <guid>https://dev.to/maxim_radugin/creating-and-restoring-disk-images-on-macos-a-guide-to-using-dd-and-diskutil-34a7</guid>
      <description>&lt;h2&gt;
  
  
  🚀 Introduction
&lt;/h2&gt;

&lt;p&gt;Many IoT and embedded devices, including single-board computers like Raspberry Pi, Radxa, and Orange Pi, use removable SD, CFast cards, or similar media for storing firmware, OS, and user data. &lt;br&gt;
It is desirable to make copies of these media devices before installing updates for backup purposes, or when a new copy of media is needed to be installed in another device.&lt;/p&gt;

&lt;p&gt;This article will guide you through the process of working directly with physical disks and raw disk images on macOS from the console:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔍 Listing physical disks&lt;/li&gt;
&lt;li&gt;⚙️ Preparing for backup or restore operations&lt;/li&gt;
&lt;li&gt;💾 Saving and restoring disk images&lt;/li&gt;
&lt;li&gt;✅ Verifying disk images are properly saved or restored&lt;/li&gt;
&lt;li&gt;📦 Optimizing space used by disk images&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On macOS, &lt;code&gt;diskutil&lt;/code&gt; and &lt;code&gt;dd&lt;/code&gt; command-line utilities will be used to accomplish the above tasks. &lt;/p&gt;

&lt;p&gt;&lt;code&gt;diskutil&lt;/code&gt; is macOS's built-in utility to manage local disks and volumes, while &lt;code&gt;dd&lt;/code&gt; is a UNIX utility for working with files, including special device files that represent physical disks in the system. &lt;/p&gt;

&lt;p&gt;⚠️ Special care should be taken when working with these utilities, as in most cases operations are irreversible. If used improperly, they can result in data damage or loss.  &lt;/p&gt;
&lt;h2&gt;
  
  
  ⚡ TL;DR
&lt;/h2&gt;

&lt;p&gt;If you understand the risks and know what you are doing, you can use commands from this section to save and restore disk images. &lt;/p&gt;

&lt;p&gt;If you are not sure or want to understand the details, proceed with the sections below. &lt;/p&gt;
&lt;h3&gt;
  
  
  💾 Save Disk to an Image File
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Identify disk:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  diskutil list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Replace &lt;code&gt;&amp;lt;X&amp;gt;&lt;/code&gt; in &lt;code&gt;disk&amp;lt;X&amp;gt;&lt;/code&gt; and &lt;code&gt;/dev/rdisk&amp;lt;X&amp;gt;&lt;/code&gt; below with the numeric identifier of the desired disk, and replace &lt;code&gt;&amp;lt;disk-image-name&amp;gt;&lt;/code&gt; with the desired name of the disk image file.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unmount volumes:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  diskutil unmountDisk disk&amp;lt;X&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;Save disk to a compressed image file:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="nb"&gt;sudo dd &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/rdisk&amp;lt;X&amp;gt; &lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1M &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;progress | xz &lt;span class="nt"&gt;--compress&lt;/span&gt; &lt;span class="nt"&gt;-2&lt;/span&gt; &lt;span class="nt"&gt;--stdout&lt;/span&gt; &lt;span class="nt"&gt;--threads&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &amp;lt;disk-image-name&amp;gt;.img.xz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;Prepare for device removal:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  diskutil eject disk&amp;lt;X&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  🔄 Restore Disk from an Image File
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Identify disk:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  diskutil list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Replace &lt;code&gt;&amp;lt;X&amp;gt;&lt;/code&gt; in &lt;code&gt;disk&amp;lt;X&amp;gt;&lt;/code&gt; and &lt;code&gt;/dev/rdisk&amp;lt;X&amp;gt;&lt;/code&gt; below with the numeric identifier of the desired disk, and replace &lt;code&gt;&amp;lt;disk-image-name&amp;gt;&lt;/code&gt; with the name of the disk image file.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unmount volumes:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  diskutil unmountDisk disk&amp;lt;X&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;Restore image from a compressed image file:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  xz &lt;span class="nt"&gt;--decompress&lt;/span&gt; &lt;span class="nt"&gt;--stdout&lt;/span&gt; &amp;lt;disk-image-name&amp;gt;.img.xz | &lt;span class="nb"&gt;sudo dd &lt;/span&gt;&lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/rdisk&amp;lt;X&amp;gt; &lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1M &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;progress 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;Prepare for device removal:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  diskutil eject disk&amp;lt;X&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  ⚠️ macOS Unreadable Disk Warning Dialog
&lt;/h2&gt;

&lt;p&gt;When attaching disks or inserting media that contain only partition types that are not supported by macOS, it will issue the warning dialog below. &lt;br&gt;
It is safe to press the &lt;code&gt;Ignore&lt;/code&gt; button and continue, or after writing the image to the disk, you can press &lt;code&gt;Eject&lt;/code&gt; and skip ejecting &lt;br&gt;
the disk from the console with the &lt;code&gt;diskutil eject&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.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%2Fuzyk88nperernoxewimm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.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%2Fuzyk88nperernoxewimm.png" alt="macOS Unreadable Disk Warning" width="372" height="376"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  🔍 Identify Physical Disk
&lt;/h2&gt;

&lt;p&gt;The first and most important step is to list and identify the disk of interest using &lt;code&gt;diskutil&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Note: Perform the steps below every time you turn on, restart, or wake up your computer, plug/unplug external devices, or insert/remove media, as disk identifiers and device nodes are volatile and may change between these events! Do not hard-code device identifiers in scripts!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diskutil list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will list all the disks and their partitions that are currently connected to the computer. Stripped output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/dev/disk0 (internal, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      GUID_partition_scheme                        *1.0 TB     disk0
   1:             Apple_APFS_ISC Container disk1         524.3 MB   disk0s1
   2:                 Apple_APFS Container disk3         994.7 GB   disk0s2
   3:        Apple_APFS_Recovery Container disk2         5.4 GB     disk0s3

/dev/disk3 (synthesized):
   ...

/dev/disk4 (internal, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:     FDisk_partition_scheme                        *8.0 GB     disk4
   1:                      Linux                         8.0 GB     disk4s1

/dev/disk6 (external, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:     FDisk_partition_scheme                        *16.0 GB    disk6
   1:                      Linux                         134.2 MB   disk6s1
   2:                      Linux                         536.9 MB   disk6s2
                    (free space)                         15.3 GB    -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, identify the disk of interest. Always skip disks that contain &lt;code&gt;Apple&lt;/code&gt; partitions or are of &lt;code&gt;synthesized&lt;/code&gt; type. In the example above, I have two external disks connected - an SD card inserted into the MacBook's built-in SD card reader, and another - an external USB CFast reader. In the above output, they are listed as &lt;code&gt;disk4&lt;/code&gt; and &lt;code&gt;disk6&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If unsure which one is which, use disk utility to get more information about the disks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diskutil info disk4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stripped output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   Device Identifier:         disk4
   ...
   Device / Media Name:       Built In SDXC Reader
   ...
   Protocol:                  Secure Digital
   ...
   Disk Size:                 8.0 GB (7956594688 Bytes) (exactly 15540224 512-Byte-Units)
   ...
   Device Location:           Internal
   ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diskutil info disk6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stripped output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   Device Identifier:         disk6
   ...
   Device / Media Name:       Transcend
   ...
   Protocol:                  USB
   ...
   Disk Size:                 16.0 GB (16013942784 Bytes) (exactly 31277232 512-Byte-Units)
   ...
   Device Location:           External
   ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the above listings, look for &lt;code&gt;Device Identifier&lt;/code&gt;, &lt;code&gt;Protocol&lt;/code&gt;, &lt;code&gt;Disk Size&lt;/code&gt;, &lt;code&gt;Device / Media Name&lt;/code&gt;, and &lt;code&gt;Device Location&lt;/code&gt; fields to identify the device of interest. &lt;/p&gt;

&lt;p&gt;In my case, &lt;code&gt;disk4&lt;/code&gt; is the built-in SD card reader with an &lt;code&gt;8.0 GB&lt;/code&gt; card, and &lt;code&gt;disk6&lt;/code&gt; is the USB CFast reader with a &lt;code&gt;16.0 GB&lt;/code&gt; card.&lt;br&gt;
If unsure which disk corresponds to which physical device, try unplugging and removing unrelated devices from the system and performing the above steps again. If still unsure, stop here to avoid data loss.&lt;br&gt;
In further steps, I will be performing operations on &lt;code&gt;disk6&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  ⚙️ Prepare Disk for Direct IO Operations
&lt;/h2&gt;

&lt;p&gt;Before performing any direct IO operations on the disk, all partitions and volumes should be unmounted! Unmounting will ensure that any pending write operations on the volume level are finished and the filesystem is in a consistent state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diskutil unmountDisk disk6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for the output below. If you get an error, close user applications that might still be using volumes of the selected disk and try unmounting again. If no success - stop here to avoid data loss.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unmount of all volumes on disk6 was successful
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  💾 Backup Disk to an Image File
&lt;/h2&gt;

&lt;p&gt;Now it is time to use the &lt;code&gt;dd&lt;/code&gt; utility.&lt;/p&gt;

&lt;p&gt;The command below will copy the whole &lt;code&gt;disk6&lt;/code&gt; content (device file &lt;code&gt;/dev/rdisk6&lt;/code&gt;) into &lt;code&gt;cfast-disk.img&lt;/code&gt; file in the current directory, while reading and writing data in &lt;code&gt;1 MB&lt;/code&gt; chunks, and displaying progress information. &lt;/p&gt;

&lt;p&gt;Please note that the &lt;code&gt;r&lt;/code&gt; in &lt;code&gt;/dev/rdisk6&lt;/code&gt; is not a typo - this device file corresponds to the raw disk, and operations on this device are much faster than on &lt;code&gt;/dev/disk6&lt;/code&gt;.&lt;br&gt;
In most cases, the device block size is &lt;code&gt;512 bytes&lt;/code&gt;, but it is advised to perform read and write operations in larger chunks to improve I/O performance. &lt;br&gt;
Don't worry if the disk size is not a multiple of the selected buffer size - in our case, &lt;code&gt;1 MB&lt;/code&gt;, as &lt;code&gt;dd&lt;/code&gt; will handle remainders properly. &lt;/p&gt;

&lt;p&gt;Accessing the raw disk device file requires administrator privileges, hence &lt;code&gt;sudo&lt;/code&gt; is used when invoking &lt;code&gt;dd&lt;/code&gt;, which will ask you to grant permissions.&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="nb"&gt;sudo dd &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/rdisk6 &lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cfast-disk.img &lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1M &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;progress
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  15972958208 bytes (16 GB, 15 GiB) transferred 72.004s, 222 MB/s   
15272+1 records in
15272+1 records out
16013942784 bytes transferred in 74.675073 secs (214448304 bytes/sec)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🔄 Restore Disk from an Image File
&lt;/h2&gt;

&lt;p&gt;To restore the image, just swap &lt;code&gt;if&lt;/code&gt; (input file) and &lt;code&gt;of&lt;/code&gt; (output file):&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="nb"&gt;sudo dd &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cfast-disk.img &lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/rdisk6 &lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1M &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;progress
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  16002318336 bytes (16 GB, 15 GiB) transferred 308.004s, 52 MB/s   
15272+1 records in
15272+1 records out
16013942784 bytes transferred in 308.249739 secs (51951197 bytes/sec)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run eject disk to make sure all write operations are complete before unplugging the device or removing the media:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diskutil eject disk6    
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait for the output below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Disk disk6 ejected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the device can be safely unplugged or the media removed from the reader.&lt;/p&gt;

&lt;h2&gt;
  
  
  📦 Optimize Space Used by Disk Images
&lt;/h2&gt;

&lt;p&gt;When the disk image is saved as a regular file, it will take the same amount of space as the original disk size. &lt;br&gt;
In many cases, disks are not fully packed with useful data and may contain unallocated or empty spaces. &lt;br&gt;
Below are two methods to reduce the space taken by disk images.&lt;/p&gt;
&lt;h3&gt;
  
  
  🕳️ Use of Sparse Files
&lt;/h3&gt;

&lt;p&gt;Sparse files are special files that hold only non-zero data and metadata about empty areas (holes) in the file. &lt;br&gt;
This makes them very efficient for disk images that have large amounts of empty space.&lt;/p&gt;

&lt;p&gt;The command below will create a sparse disk image file. It will read data in &lt;code&gt;1 MB&lt;/code&gt; chunks and write in &lt;code&gt;512 bytes&lt;/code&gt; chunks, marking any &lt;code&gt;512 bytes&lt;/code&gt; chunks filled with zeroes as being empty, i.e., "holes".&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="nb"&gt;sudo dd &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/rdisk6 &lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cfast-disk-sparse.img &lt;span class="nv"&gt;ibs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1M &lt;span class="nv"&gt;obs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;512 &lt;span class="nv"&gt;conv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sparse &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;progress
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  15890120704 bytes (16 GB, 15 GiB) transferred 71.002s, 224 MB/s   
15272+1 records in
31277232+0 records out
16013942784 bytes transferred in 71.548794 secs (223818487 bytes/sec)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the resulting file sizes using &lt;code&gt;ls&lt;/code&gt;:&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="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-al&lt;/span&gt; cfast-disk.img cfast-disk-sparse.img
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ls&lt;/code&gt; shows file sizes taking into account sparse file "holes", hence both files appear as having the same size:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-rw-r--r--  1 root  staff  16013942784 May  4 13:56 cfast-disk-sparse.img
-rw-r--r--  1 root  staff  16013942784 May  4 12:34 cfast-disk.img
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now compare the actual space used by sparse and regular disk images using the &lt;code&gt;du&lt;/code&gt; utility:&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="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-hs&lt;/span&gt; cfast-disk.img cfast-disk-sparse.img
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my case, the sparse disk image uses less than &lt;code&gt;2%&lt;/code&gt; of the total disk size! And that is expected - if you look at the output of &lt;code&gt;diskutil list&lt;/code&gt; above, &lt;br&gt;
&lt;code&gt;disk6&lt;/code&gt; contains two partitions of &lt;code&gt;134.2 MB&lt;/code&gt; and &lt;code&gt;536.9 MB&lt;/code&gt;, which, I also know, are not fully taken.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; 15G    cfast-disk.img
282M    cfast-disk-sparse.img
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To verify that the content of files is equal, SHA1 hash calculation can be used:&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="nb"&gt;sha1sum &lt;/span&gt;cfast-disk.img cfast-disk-sparse.img
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;7a7d26c342dc44d9e814de00d1de8a6d2b26e17e  cfast-disk.img
7a7d26c342dc44d9e814de00d1de8a6d2b26e17e  cfast-disk-sparse.img
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note on copying sparse files: not all filesystems and file copying utilities preserve sparse files and might replace "holes" with zeroes.&lt;/p&gt;

&lt;p&gt;Generally, the second method of using compressed disk images is more preferred over this one, as it produces even smaller disk image files &lt;br&gt;
with negligible performance drop when saving and restoring images. And these files can be safely copied across different systems without worrying about sparse file support. &lt;/p&gt;
&lt;h3&gt;
  
  
  🗜️ Compress Disk Images
&lt;/h3&gt;

&lt;p&gt;I prefer xz-compressed images since they provide great performance in terms of compression ratio and decompression speed.&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="nb"&gt;sudo dd &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/rdisk6 &lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1M &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;progress | xz &lt;span class="nt"&gt;--compress&lt;/span&gt; &lt;span class="nt"&gt;-2&lt;/span&gt; &lt;span class="nt"&gt;--stdout&lt;/span&gt; &lt;span class="nt"&gt;--threads&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; cfast-disk.img.xz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  15922626560 bytes (16 GB, 15 GiB) transferred 73.004s, 218 MB/s   
15272+1 records in
15268+9 records out
16013942784 bytes transferred in 73.422991 secs (218105291 bytes/sec)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this particular case, the output image size was only &lt;code&gt;80 MB&lt;/code&gt;. And the read + compression speed reached &lt;code&gt;218 MB/s&lt;/code&gt;, which is comparable to &lt;code&gt;224 MB/s&lt;/code&gt; when no compression was used.&lt;/p&gt;

&lt;p&gt;To uncompress:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xz &lt;span class="nt"&gt;--decompress&lt;/span&gt; &lt;span class="nt"&gt;--stdout&lt;/span&gt; cfast-disk.img.xz | &lt;span class="nb"&gt;sudo dd &lt;/span&gt;&lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/rdisk6 &lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1M &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;progress 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a side note - by default, &lt;code&gt;xz&lt;/code&gt; produces sparse uncompressed files. Moreover, xz-uncompressed files take even less space compared to ones&lt;br&gt;
 produced by &lt;code&gt;dd&lt;/code&gt;, since &lt;code&gt;dd&lt;/code&gt; operates on fixed block boundaries, while &lt;code&gt;xz&lt;/code&gt; looks for continuous empty regions in uncompressed data.&lt;/p&gt;

&lt;p&gt;This was verified with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xz &lt;span class="nt"&gt;--decompress&lt;/span&gt; cfast-disk.img.xz &lt;span class="nt"&gt;--stdout&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; cfast-disk-unxz.img
&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-sh&lt;/span&gt; cfast-disk-sparse.img cfast-disk-unxz.img
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;282M    cfast-disk-sparse.img
272M    cfast-disk-unxz.img
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ✅ Verify Disk Image
&lt;/h2&gt;

&lt;p&gt;To ensure the integrity of your disk images, you can calculate and verify SHA1 hashes at different stages of the process.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating and Verifying Disk Image
&lt;/h3&gt;

&lt;p&gt;When creating a disk image, you can calculate its SHA1 hash while writing it to a file using process substitution:&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="nb"&gt;sudo dd &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/rdisk6 &lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1M &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;progress | &lt;span class="nb"&gt;tee&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="nb"&gt;sha1sum&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; | xz &lt;span class="nt"&gt;--compress&lt;/span&gt; &lt;span class="nt"&gt;-2&lt;/span&gt; &lt;span class="nt"&gt;--stdout&lt;/span&gt; &lt;span class="nt"&gt;--threads&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; cfast-disk.img.xz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read from the disk using &lt;code&gt;dd&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Split the output using &lt;code&gt;tee&lt;/code&gt;:

&lt;ul&gt;
&lt;li&gt;One stream goes to &lt;code&gt;sha1sum&lt;/code&gt; for hash calculation&lt;/li&gt;
&lt;li&gt;Another stream goes to &lt;code&gt;xz&lt;/code&gt; for compression&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Save the compressed output to &lt;code&gt;cfast-disk.img.xz&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The output will look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  16002318336 bytes (16 GB, 15 GiB) transferred 79.003s, 203 MB/s   
15272+1 records in
15263+19 records out
16013942784 bytes transferred in 79.060099 secs (202554044 bytes/sec)
7a7d26c342dc44d9e814de00d1de8a6d2b26e17e  -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: The difference in record counts (&lt;code&gt;15272+1 records in&lt;/code&gt; vs &lt;code&gt;15263+19 records out&lt;/code&gt;) is normal and occurs because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;dd&lt;/code&gt; reads data in 1MB blocks (as specified by &lt;code&gt;bs=1M&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;When the data is piped through multiple commands (&lt;code&gt;tee&lt;/code&gt;, &lt;code&gt;xz&lt;/code&gt;), the blocks might be split or combined differently&lt;/li&gt;
&lt;li&gt;The total number of bytes transferred remains the same (16013942784 bytes)&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;+1&lt;/code&gt; and &lt;code&gt;+19&lt;/code&gt; indicate partial blocks at the end of the transfer&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This difference doesn't affect data integrity, as the SHA1 hash verifies the actual content.&lt;/p&gt;

&lt;p&gt;Save the hash value (&lt;code&gt;7a7d26c342dc44d9e814de00d1de8a6d2b26e17e&lt;/code&gt;) for later verification.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verifying Restored Disk Image
&lt;/h3&gt;

&lt;p&gt;To verify that the disk image was correctly written to a disk, you can calculate the hash of the target disk and compare it with the original:&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="nb"&gt;sudo dd &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/rdisk6 &lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1M &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;progress | &lt;span class="nb"&gt;sha1sum&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the hash matches the one you saved earlier, the disk image was written correctly. &lt;/p&gt;

&lt;p&gt;⚠️ This is only true when the image file size exactly matches the target disk size. If the physical disk is larger, the hash calculation will include data from the area after the disk image, resulting in a mismatched hash. &lt;/p&gt;

&lt;p&gt;To overcome this limitation, we need to specify the exact size of the image when calculating the hash. However, the macOS version of &lt;code&gt;dd&lt;/code&gt; only allows limiting data to read by an integer number of blocks. &lt;/p&gt;

&lt;p&gt;My disk image size is not evenly divisible by &lt;code&gt;1 MB&lt;/code&gt;, also using a block size of &lt;code&gt;512 bytes&lt;/code&gt; is impractical as it would result in very slow disk read operations. &lt;br&gt;
Instead, still very impractical, I calculated that my image consists of &lt;code&gt;20686&lt;/code&gt; blocks of &lt;code&gt;774144 bytes&lt;/code&gt; each, &lt;br&gt;
and with below command I have verified that image was successfully written on the disk of a larger size.&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="nb"&gt;sudo dd &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/rdisk6 &lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;774144 &lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20686 &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;progress | &lt;span class="nb"&gt;sha1sum&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Verifying Compressed Disk Image
&lt;/h3&gt;

&lt;p&gt;To verify the integrity of a compressed disk image without decompressing it to a file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xz &lt;span class="nt"&gt;--decompress&lt;/span&gt; &lt;span class="nt"&gt;--stdout&lt;/span&gt; cfast-disk.img.xz | &lt;span class="nb"&gt;sha1sum&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will decompress the image in memory and calculate its hash, which should match the original hash.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎯 Conclusion
&lt;/h2&gt;

&lt;p&gt;macOS provides powerful command-line utilities for working with disk images, making it possible to create reliable backups of removable media and restore them when needed. The combination of &lt;code&gt;diskutil&lt;/code&gt; and &lt;code&gt;dd&lt;/code&gt; offers a robust solution for disk imaging tasks, while tools like &lt;code&gt;xz&lt;/code&gt; compression and sparse files help optimize storage space.&lt;/p&gt;

&lt;p&gt;Key takeaways from this guide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔍 Always identify the correct disk using &lt;code&gt;diskutil list&lt;/code&gt; and &lt;code&gt;diskutil info&lt;/code&gt; before performing any operations&lt;/li&gt;
&lt;li&gt;⚙️ Unmount all volumes before working with raw disk devices&lt;/li&gt;
&lt;li&gt;💾 Use &lt;code&gt;/dev/rdiskX&lt;/code&gt; for faster raw disk access&lt;/li&gt;
&lt;li&gt;📦 Consider using compression to save storage space&lt;/li&gt;
&lt;li&gt;✅ Always verify disk images using checksums&lt;/li&gt;
&lt;li&gt;⏏️ Eject disks properly after operations are complete&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Remember that working with raw disk devices requires administrator privileges and carries risks of data loss if not done carefully. Always double-check disk identifiers and ensure you're working with the correct device before proceeding with any operations.&lt;/p&gt;

&lt;p&gt;With these tools and precautions in mind, you can confidently create and restore disk images for your IoT devices, embedded systems, and other removable media.&lt;/p&gt;

</description>
      <category>backup</category>
      <category>osx</category>
      <category>raspberrypi</category>
      <category>iot</category>
    </item>
    <item>
      <title>Enter ROS2 Development Container</title>
      <dc:creator>Maxim Radugin</dc:creator>
      <pubDate>Thu, 17 Apr 2025 18:32:42 +0000</pubDate>
      <link>https://dev.to/maxim_radugin/enter-ros2-development-container-23pb</link>
      <guid>https://dev.to/maxim_radugin/enter-ros2-development-container-23pb</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;The robotics world has witnessed a significant shift in recent years with the growing adoption of ROS2 (Robot Operating System 2). As the successor to the original ROS, this framework brings improved performance, better security, and enhanced support for real-time systems. However, despite its increasing popularity, ROS2 development has faced a persistent challenge: the lack of an official, convenient development environment where both C++ and Python nodes can be seamlessly developed and debugged. These challenges can significantly slow down development cycles and create barriers for newcomers to the ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Devcontainer Advantage
&lt;/h2&gt;

&lt;p&gt;Meanwhile, development containers (devcontainers) and their support in VS Code have been around for several years now (since 2019), which makes them an ideal environment for ROS2 project development. The technology has matured significantly, offering robust features for containerized development workflows that perfectly complement ROS2's architectural needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ROS2 Growing Pains
&lt;/h2&gt;

&lt;p&gt;Surprisingly, the official ROS2 documentation only briefly touches on the topic of using devcontainers and VS Code for ROS2 development. It provides only a minimalistic guide without an actual starting point workspace, leaving developers to piece together their own solutions. The process is time-consuming and requires numerous trial and error cycles, including: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Managing complex dependencies across different operating systems&lt;/li&gt;
&lt;li&gt;Configuring build systems that work efficiently with both C++ and Python&lt;/li&gt;
&lt;li&gt;Setting up debugging tools that function properly across languages&lt;/li&gt;
&lt;li&gt;Creating clean project structure&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Enter ros2-devcontainer
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/mradugin/ros2-devcontainer" rel="noopener noreferrer"&gt;ros2-devcontainer&lt;/a&gt; project aims to address these pain points head-on and leverages Visual Studio Code's development container capabilities to provide a clean, consistent, yet powerful environment for ROS2 development. It also integrates essential VS Code extensions for C++, Python, and ROS2, creating a comprehensive development experience with syntax highlighting, code completion, and debugging for ROS2 applications.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Features
&lt;/h3&gt;

&lt;p&gt;The ros2-devcontainer project offers several advantages that streamline the ROS2 development workflow:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Containerized Development Environment&lt;/strong&gt;&lt;br&gt;
By utilizing VS Code's devcontainer support, the project ensures that every developer works with identical dependencies and configurations, eliminating the classic "it works on my machine" problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Language-Agnostic Development&lt;/strong&gt;&lt;br&gt;
The environment seamlessly supports both C++ and Python development, allowing developers to choose the right language for each component without sacrificing development experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Optimized Build System&lt;/strong&gt;&lt;br&gt;
The project includes a carefully configured build system that maximizes compilation speed while maintaining proper dependency handling between packages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Integrated Debugging Experience&lt;/strong&gt;&lt;br&gt;
Perhaps most importantly, ros2-devcontainer provides a unified debugging experience across languages. Developers can set breakpoints, inspect variables, and step through code regardless of whether they're working with C++ or Python nodes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Clean Project Structure&lt;/strong&gt;&lt;br&gt;
The repository offers a logical, well-organized project structure that makes it easy to add new packages and components while maintaining code clarity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real-World Benefits
&lt;/h3&gt;

&lt;p&gt;For teams working on complex robotics applications, the benefits of adopting ros2-devcontainer can be substantial:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reduced onboarding time&lt;/strong&gt; for new team members who can start contributing quickly without spending days configuring their development environment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improved collaboration&lt;/strong&gt; through consistent tooling and environments across the team&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faster debugging cycles&lt;/strong&gt; with integrated tools that work across languages&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Getting Started
&lt;/h3&gt;

&lt;p&gt;Getting started with ros2-devcontainer is straightforward. With VS Code and Docker installed, developers can clone the repository and have a fully functional development environment running within minutes. The project's documentation provides clear instructions for both new and experienced ROS2 developers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Further Improvements
&lt;/h3&gt;

&lt;p&gt;While the ros2-devcontainer project provides a solid foundation for starting ROS2 development, it currently maintains a focused scope. The implementation is intentionally streamlined and ready for expansion with additional components that would enhance its functionality. Currently, visualization tools like RViz2 or Gazebo aren't included, likewise examples for unit testing ROS2 nodes are yet to be added. &lt;/p&gt;

&lt;p&gt;If you find the project useful and want to help expand its capabilities, consider forking the repository and contributing these missing pieces. The project welcomes contributions that could help make ROS2 development more accessible to everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;As ROS2 continues to mature and gain adoption, it is important to have a workspace, like ros2-devcontainer provides, that can jump-start development of a new project, enabling developers to focus on what matters most: building innovative robotics applications.&lt;/p&gt;

&lt;p&gt;While community-driven solutions like ros2-devcontainer provide excellent alternatives, it would be beneficial to see similar environment readily available as part of the official ROS2 project and tutorials. Having standardized development environments officially supported by the ROS2 team would:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Provide newcomers with a consistent starting point&lt;/li&gt;
&lt;li&gt;Ensure compatibility with each ROS2 release&lt;/li&gt;
&lt;li&gt;Reduce fragmentation in development approaches&lt;/li&gt;
&lt;li&gt;Establish best practices for containerized ROS2 development&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Official support for devcontainer configurations would significantly lower the barrier to entry for robotics developers and help consolidate the ecosystem around proven development patterns. This would be particularly valuable for educational institutions and companies introducing new developers to ROS2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/mradugin/ros2-devcontainer" rel="noopener noreferrer"&gt;ros2-devcontainer GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ros</category>
      <category>docker</category>
      <category>vscode</category>
      <category>robotics</category>
    </item>
    <item>
      <title>GitLab CI: Tips and Tricks</title>
      <dc:creator>Maxim Radugin</dc:creator>
      <pubDate>Tue, 22 Oct 2024 07:53:32 +0000</pubDate>
      <link>https://dev.to/maxim_radugin/gitlab-ci-tips-and-tricks-132k</link>
      <guid>https://dev.to/maxim_radugin/gitlab-ci-tips-and-tricks-132k</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;GitLab is a second most-popular DevOps platform that provides a complete solution for source version control, project management, issue tracking, and powerful CI/CD system.&lt;/p&gt;

&lt;p&gt;GitLab is constantly working to extend and improve CI/CD functionality, and provides comprehensive documentation for all the new features.&lt;/p&gt;

&lt;p&gt;I've been using GitLab for over a decade now for professional and personal projects. In this article I'd like to share some of my findings about useful features and workarounds for some limitations I know of.&lt;/p&gt;

&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Referencing Scripts from Other Jobs
&lt;/h3&gt;

&lt;p&gt;When writing CI/CD pipelines sometimes it is handy to reuse commands between different jobs. GitLab features the &lt;code&gt;!reference&lt;/code&gt; custom YAML tag to accomplish this. Below commands from &lt;code&gt;script&lt;/code&gt; of &lt;code&gt;.configure-and-build&lt;/code&gt; are reused in &lt;code&gt;build-project&lt;/code&gt; and &lt;code&gt;build-sub-project&lt;/code&gt; jobs.&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;.configure-and-build&lt;/span&gt;&lt;span class="pi"&gt;:&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;cmake -G Xcode -B build&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cmake --build build&lt;/span&gt;

&lt;span class="na"&gt;build-project&lt;/span&gt;&lt;span class="pi"&gt;:&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="kt"&gt;!reference&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;.configure-and-build&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;build-sub-project&lt;/span&gt;&lt;span class="pi"&gt;:&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;cd sub-project&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!reference&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;.configure-and-build&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Using Job Scripts as Functions
&lt;/h3&gt;

&lt;p&gt;The above approach can be extended to use variables as "arguments" to referenced script commands. In below example &lt;code&gt;GENERATOR&lt;/code&gt; and &lt;code&gt;BUILD_DIR&lt;/code&gt; variables are used to customize behavior of &lt;code&gt;.configure-and-build&lt;/code&gt; script. In &lt;code&gt;build-project-macos&lt;/code&gt; job &lt;code&gt;.configure-and-build&lt;/code&gt; script is invoked multiple times with different &lt;code&gt;GENERATOR&lt;/code&gt; and &lt;code&gt;BUILD_DIR&lt;/code&gt; values.&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;.configure-and-build&lt;/span&gt;&lt;span class="pi"&gt;:&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;cmake -G ${GENERATOR} -B build&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cmake --build ${BUILD_DIR}&lt;/span&gt;

&lt;span class="na"&gt;build-project-windows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;...&lt;/span&gt;
    &lt;span class="s"&gt;variables&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;GENERATOR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Visual&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Studio&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;16&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2019"&lt;/span&gt;
        &lt;span class="na"&gt;BUILD_DIR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;build"&lt;/span&gt;
&lt;span class="err"&gt;    &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="kt"&gt;!reference&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;.configure-and-build&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;build-project-macos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;...&lt;/span&gt;
    &lt;span class="s"&gt;script&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;export GENERATOR="Xcode"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;export BUILD_DIR="build_xcode"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!reference&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;.configure-and-build&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&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;export GENERATOR="Ninja"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;export BUILD_DIR="build_ninja"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!reference&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;.configure-and-build&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&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;export GENERATOR="Unix Makefiles"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;export BUILD_DIR="build_make"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!reference&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;.configure-and-build&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Multi-line Command Invocation
&lt;/h3&gt;

&lt;p&gt;This is not strictly GitLab CI/CD feature, but rather feature of &lt;code&gt;yaml&lt;/code&gt; language which is used to describe CI/CD pipelines in GitLab.&lt;/p&gt;

&lt;p&gt;Sometimes it is desired to split long command into multiple lines for readability, but unfortunately &lt;code&gt;bash&lt;/code&gt; and &lt;code&gt;powershell&lt;/code&gt; use different characters to split multiline command - &lt;code&gt;\&lt;/code&gt; and &lt;code&gt;`&lt;/code&gt; respectively. Therefore, it is not possible to re-use commands for both platforms.&lt;br&gt;
Consider following example, where both windows and macos build jobs have to be duplicated:&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-macos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;...&lt;/span&gt;
    &lt;span class="s"&gt;script&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;cmake -G Xcode \&lt;/span&gt;
            &lt;span class="s"&gt;-B build \&lt;/span&gt;
            &lt;span class="s"&gt;-DOPTION1=YES \&lt;/span&gt;
            &lt;span class="s"&gt;-DANOTHER_OPTION=Test \&lt;/span&gt;
            &lt;span class="s"&gt;-DYET_ANOTHER_OPTION=1 \&lt;/span&gt;
            &lt;span class="s"&gt;-DAND_ANOTHER_OPTION=ON &lt;/span&gt;

&lt;span class="na"&gt;build-windows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;...&lt;/span&gt;
    &lt;span class="s"&gt;script&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;cmake -G "Visual Studio 16 2019" `&lt;/span&gt;
            &lt;span class="s"&gt;-B build `&lt;/span&gt;
            &lt;span class="s"&gt;-DOPTION1=YES `&lt;/span&gt;
            &lt;span class="s"&gt;-DANOTHER_OPTION=Test `&lt;/span&gt;
            &lt;span class="s"&gt;-DYET_ANOTHER_OPTION=1 `&lt;/span&gt;
            &lt;span class="s"&gt;-DAND_ANOTHER_OPTION=ON&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To reduce duplication a "Folded Block Scalar" &lt;code&gt;&amp;gt;&lt;/code&gt; can be used to folds newlines to spaces, which unlocks the ability to write multi-line commands that are compatible with both &lt;code&gt;bash&lt;/code&gt; and &lt;code&gt;powershell&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;.build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;...&lt;/span&gt;
    &lt;span class="s"&gt;script&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; 
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt; 
            &lt;span class="s"&gt;cmake -G "${GENERATOR}"&lt;/span&gt;
            &lt;span class="s"&gt;-B build&lt;/span&gt;
            &lt;span class="s"&gt;-DOPTION1=YES&lt;/span&gt;
            &lt;span class="s"&gt;-DANOTHER_OPTION=Test&lt;/span&gt;
            &lt;span class="s"&gt;-DYET_ANOTHER_OPTION=1&lt;/span&gt;
            &lt;span class="s"&gt;-DAND_ANOTHER_OPTION=ON&lt;/span&gt;

&lt;span class="na"&gt;build-macos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;...&lt;/span&gt;
    &lt;span class="s"&gt;extends&lt;/span&gt;&lt;span class="err"&gt;:&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;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;GENERATOR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Xcode"&lt;/span&gt;

&lt;span class="na"&gt;build-windows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;...&lt;/span&gt;
    &lt;span class="s"&gt;extends&lt;/span&gt;&lt;span class="err"&gt;:&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;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;GENERATOR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Visual&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Studio&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;16&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2019"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Exclusive Use of Runners
&lt;/h3&gt;

&lt;p&gt;Sometimes it is desirable to restrict concurrent use of specific runner for a specific job. For example, you might have a runner that can execute unit-tests concurrently, but due to limitations of the underlying framework can only run one instance of end-to-end test application. &lt;/p&gt;

&lt;p&gt;Limiting the number of concurrent jobs for the whole runner is suboptimal, since unit-tests can safely be executed in parallel. To resolve this GitLab &lt;code&gt;resource_group&lt;/code&gt; keyword can be used to disable execution of a specific job from different pipelines concurrently.&lt;/p&gt;

&lt;p&gt;Depending on runner concurrency configuration, multiple &lt;code&gt;unit-test&lt;/code&gt; instances can be executed in parallel, but only one instance of &lt;code&gt;test-e2e&lt;/code&gt; will be executed concurrently due to presence of &lt;code&gt;resource_group&lt;/code&gt; key.&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;unit-test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;...&lt;/span&gt;
    &lt;span class="s"&gt;tags&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test-machine-tag&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 "running unit-tests non-exclusively"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sleep &lt;/span&gt;&lt;span class="m"&gt;60&lt;/span&gt;

&lt;span class="na"&gt;test-e2e&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;...&lt;/span&gt;
    &lt;span class="s"&gt;tags&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test-machine-tag&lt;/span&gt;
    &lt;span class="na"&gt;resource_group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test-e2e"&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 "running e2e tests exclusively"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sleep &lt;/span&gt;&lt;span class="m"&gt;60&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Customizing Job Execution Using Rules
&lt;/h3&gt;

&lt;p&gt;GitLab provides powerful &lt;code&gt;rules&lt;/code&gt; keyword that is used to customize "when" job is run. Additionally &lt;code&gt;rules&lt;/code&gt; supports specifying value for variables on specific conditions that can help to customize "how" job is being executed. &lt;/p&gt;

&lt;p&gt;Consider the example below where variable &lt;code&gt;SIGN&lt;/code&gt; will be set to &lt;code&gt;YES&lt;/code&gt; only when build job is executed for tagged commit, in other cases it will be &lt;code&gt;NO&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;build-macos&lt;/span&gt;&lt;span class="pi"&gt;:&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;SIGN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NO"&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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$CI_COMMIT_TAG"&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;SIGN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YES"&lt;/span&gt;
        &lt;span class="pi"&gt;-&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;always&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 "Executables will be signed: ${SIGN}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Clearing Working Directory for Specific Job
&lt;/h3&gt;

&lt;p&gt;Recently GitLab added option to clean build directory before downloading artifacts and executing job scripts. To achieve this, &lt;code&gt;GIT_STRATEGY&lt;/code&gt; variable should be set to &lt;code&gt;empty&lt;/code&gt;. Per GitLab documentation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use the &lt;code&gt;empty&lt;/code&gt; Git strategy when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You do not need the repository data to be present.&lt;/li&gt;
&lt;li&gt;You want a clean, controlled, or customized starting state every time a job runs.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Example:&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;job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;...&lt;/span&gt;
    &lt;span class="s"&gt;variables&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;GIT_STRATEGY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;empty&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;ls -al&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Workarounds
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Spaces in Variables
&lt;/h3&gt;

&lt;p&gt;Consider a scenario where you want to have a variable that has list of file paths to be passed to some script and file paths may contain spaces. Example below contains list of files to be passed to signing script:&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;job&lt;/span&gt;&lt;span class="pi"&gt;:&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;FILES_FOR_SIGNING&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;'Test&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;App&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1/Test&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;App&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'Test&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;App&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2/Test&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;App&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2'"&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 $FILES_FOR_SIGNING&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./sign.sh $FILES_FOR_SIGNING&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;sign.sh script:&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="c"&gt;#!/bin/sh&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$# &lt;/span&gt;&lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"No arguments provided"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;i&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="k"&gt;for &lt;/span&gt;arg &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&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;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="nv"&gt;$arg&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nv"&gt;i&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;i &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output of pipeline execution will look like below - with arguments split by spaces, without quotes being taken into account.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ echo $FILES_FOR_SIGNING
'Test App 1/Test App 1' 'Test App 2/Test App 2'
$ ./sign.sh $FILES_FOR_SIGNING
1: 'Test
2: App
3: 1/Test
4: App
5: 1'
6: 'Test
7: App
8: 2/Test
9: App
10: 2'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I have tried different combinations of quotes and escaped quotes, but the result was the same all the time. But, if you have noticed, &lt;code&gt;echo $FILES_FOR_SIGNING&lt;/code&gt; produces string with single quotes preserved. Combining it with &lt;code&gt;eval&lt;/code&gt; solves the problem! &lt;/p&gt;

&lt;p&gt;Replacing &lt;code&gt;./sign.sh $FILES_FOR_SIGNING&lt;/code&gt; with &lt;code&gt;eval $(echo ./sign.sh $FILES_FOR_SIGNING)&lt;/code&gt; solved the issue and the output was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ eval $(echo ./sign.sh $FILES_FOR_SIGNING)
1: Test App 1/Test App 1
2: Test App 2/Test App 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A note on security: using &lt;code&gt;eval&lt;/code&gt; in certain conditions is considered unsafe, even with current example &lt;code&gt;FILES_FOR_SIGNING&lt;/code&gt; can be easily constructed so that it can execute arbitrary commands, use with caution!&lt;/p&gt;

&lt;h3&gt;
  
  
  Colon+Space in Values
&lt;/h3&gt;

&lt;p&gt;I have stumbled upon this issue several times and each time it took me a while to figure out what is wrong. In short - if you want to use colon and space &lt;code&gt;:&lt;/code&gt; in the value, whole value string must be quoted.  &lt;/p&gt;

&lt;p&gt;Obviously, this will work fine:&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;job&lt;/span&gt;&lt;span class="pi"&gt;:&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;ANIMALS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Animals:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;dog,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;cat,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;mouse"&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 $ANIMALS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But this will not, as it is not compliant with above rule:&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;job&lt;/span&gt;&lt;span class="pi"&gt;:&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 "Animals&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dog, cat, mouse"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To make it compliant, whole line should be quoted, no matter it is dictionary or a list value, like this:&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;job&lt;/span&gt;&lt;span class="pi"&gt;:&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;echo&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Animals:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;dog,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;cat,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;mouse"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;GitLab is a powerful and extensive tool that offers a wide range of features for creating advanced CI/CD pipelines. Throughout this article, we've explored several techniques and workarounds that can help you create more efficient and flexible pipelines, streamline your development process and accelerate your software delivery.&lt;br&gt;
Remember that GitLab is constantly evolving, so it's worth staying up-to-date with the latest features and best practices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Materials
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.gitlab.com/ee/ci/yaml/index.html" rel="noopener noreferrer"&gt;CI/CD YAML syntax reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://yaml.org/spec/" rel="noopener noreferrer"&gt;YAML Syntax Specification&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gitlab</category>
      <category>productivity</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Cleaning up Obsolete Cloudflare Page Deployments</title>
      <dc:creator>Maxim Radugin</dc:creator>
      <pubDate>Sat, 31 Aug 2024 18:21:15 +0000</pubDate>
      <link>https://dev.to/maxim_radugin/cleaning-up-obsolete-cloudflare-page-deployments-3f7b</link>
      <guid>https://dev.to/maxim_radugin/cleaning-up-obsolete-cloudflare-page-deployments-3f7b</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In the previous post I have created a Hugo blog which is being deployed to Cloudflare pages. After spending some time playing around and tuning it, I have generated quite a few deployments to Cloudflare pages. Now all of them are visible in Cloudflare dashboard under Workers &amp;amp; Pages section for each site. Why would I need all of them and how to clean all that mess?&lt;/p&gt;

&lt;h2&gt;
  
  
  Value of Deployment History
&lt;/h2&gt;

&lt;p&gt;For production deployments - useful if latest deployments have problems, then it is easy and straightforward to rollback to the previous version. This can be done from Cloudflare dashboard by finding deployment to rollback to and selecting &lt;code&gt;Rollback to this deployment&lt;/code&gt; in the "..." menu. &lt;/p&gt;

&lt;p&gt;For preview deployments - while a new feature or content is being created it is good to test how a site performs in different environments after specific changes, track overall feature development progress, and share with other people for review. But after active development is finished, content ready, and everything is deployed in production and tested, I see no much value in keeping deployment history of work-in-progress changes. Compared to git, regular practice is to squash commits and delete source branches when merging features or bugfixes to the default branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleaning up Deployment History
&lt;/h2&gt;

&lt;p&gt;As stated above, I see value in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keeping couple of latest production deployments, just in case&lt;/li&gt;
&lt;li&gt;keeping preview deployments while actively working on them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's now see how this can be achieved.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cleaning from Cloudflare
&lt;/h3&gt;

&lt;p&gt;Deployments can be deleted manually from the Cloudflare dashboard under Workers &amp;amp; Pages section for each site. I've tried this approach, and if only few deployments are to be deleted then it is fine and failsafe, but deleting many deployments - very tedious and annoying process, because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deployments can only be deleted one by one, no bulk delete&lt;/li&gt;
&lt;li&gt;each delete operation has to be confirmed&lt;/li&gt;
&lt;li&gt;if deployment has an alias, alias name should be typed-in into edit field to confirm deletion&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Automating Cleanup
&lt;/h3&gt;

&lt;p&gt;I was hoping that the &lt;code&gt;wrangler&lt;/code&gt; tool would have something to manage existing deployments, but not... it can only &lt;a href="https://developers.cloudflare.com/workers/wrangler/commands/#deployment-list" rel="noopener noreferrer"&gt;list deployments&lt;/a&gt; in human-readable format, but not any standard machine-friendly format.&lt;/p&gt;

&lt;p&gt;Fortunately, Cloudflare has &lt;a href="https://developers.cloudflare.com/api/operations/pages-project-get-projects" rel="noopener noreferrer"&gt;Pages APIs&lt;/a&gt; that provide full control over deployments, including listing and deleting. Official example are &lt;a href="https://developers.cloudflare.com/pages/configuration/api/#examples" rel="noopener noreferrer"&gt;here&lt;/a&gt;. &lt;br&gt;
I will use these APIs to create a script to do obsolete deployment cleanup that are older than certain number of days, while always keeping specific number of latest deployments in case rollback would be needed. &lt;/p&gt;

&lt;p&gt;With ChatGPT doing most of typing, I've created following python script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;usage: cleanup-deployments.py [-h] --environment {production,preview} --count COUNT --days DAYS [--dry-run]

Fetch and delete obsolete page deployments

options:
  -h, --help            show this help message and exit
  --environment {production,preview}
                        deployment environment
  --count COUNT         number of deployments to keep
  --days DAYS           number of days to keep
  --dry-run             perform a dry run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apart from command line arguments it also expects certain environment variables to be defined, for authentication - &lt;code&gt;CLOUDFLARE_API_TOKEN&lt;/code&gt; and &lt;code&gt;CLOUDFLARE_ACCOUNT_ID&lt;/code&gt; are used, to select pages project - &lt;code&gt;CLOUDFLARE_PROJECT_NAME&lt;/code&gt;. Names were chosen to match variables that are already available in my CI/CD environment in GitLab and used by deployment jobs and the &lt;code&gt;wrangler&lt;/code&gt; tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  Integrating into CI/CD
&lt;/h3&gt;

&lt;p&gt;I took &lt;a href="https://github.com/mradugin/deploy-hugo-from-gitlab-to-cloudflare-pages/tree/deploy-hugo-from-gitlab-to-cloudflare-pages" rel="noopener noreferrer"&gt;.gitlab-ci.yml&lt;/a&gt; from previous article and made following modifications to: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;define deployment cleanup jobs for production and preview environment with their specific options&lt;/li&gt;
&lt;li&gt;make cleanup jobs runnable on schedule&lt;/li&gt;
&lt;li&gt;exclude build and deployment jobs from scheduled pipelines
&lt;/li&gt;
&lt;/ul&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;build&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cleanup&lt;/span&gt;

&lt;span class="na"&gt;workflow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;...&lt;/span&gt;
      &lt;span class="s"&gt;# Always run on schedule&lt;/span&gt;
    &lt;span class="s"&gt;- if&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_PIPELINE_SOURCE == "schedule"&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;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_PIPELINE_SOURCE == "schedule"&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;never&lt;/span&gt;
    &lt;span class="s"&gt;...&lt;/span&gt; 

&lt;span class="na"&gt;deploy:preview&lt;/span&gt;&lt;span class="pi"&gt;:&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_PIPELINE_SOURCE == "schedule"&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;never&lt;/span&gt;
    &lt;span class="s"&gt;...&lt;/span&gt;

&lt;span class="na"&gt;deploy:production&lt;/span&gt;&lt;span class="pi"&gt;:&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_PIPELINE_SOURCE == "schedule"&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;never&lt;/span&gt;
    &lt;span class="s"&gt;...&lt;/span&gt;

&lt;span class="na"&gt;.cleanup&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;python:3.11-alpine&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;cleanup&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;CLEANUP_KEEP_DAYS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;
  &lt;span class="na"&gt;before_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;pip install requests python-dateutil&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;python scripts/cleanup-deployments.py --environment ${CLEANUP_ENVIRONMENT} --days ${CLEANUP_KEEP_DAYS} --count ${CLEANUP_KEEP_COUNT}&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_PIPELINE_SOURCE == "schedule"&lt;/span&gt;

&lt;span class="na"&gt;cleanup:preview&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;extends&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;.cleanup&lt;/span&gt;&lt;span class="pi"&gt;]&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;CLEANUP_ENVIRONMENT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;preview"&lt;/span&gt;
    &lt;span class="na"&gt;CLEANUP_KEEP_COUNT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

&lt;span class="na"&gt;cleanup:production&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;extends&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;.cleanup&lt;/span&gt;&lt;span class="pi"&gt;]&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;CLEANUP_ENVIRONMENT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;production"&lt;/span&gt;
    &lt;span class="na"&gt;CLEANUP_KEEP_COUNT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Complete &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; and &lt;code&gt;cleanup-deployments.py&lt;/code&gt; script source code are available on &lt;a href="https://github.com/mradugin/deploy-hugo-from-gitlab-to-cloudflare-pages/tree/cloudflare-cleanup-page-deployments" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Next, I have configured scheduled pipeline in my GitLab project under &lt;code&gt;Build&lt;/code&gt; -&amp;gt; &lt;code&gt;Pipeline schedules&lt;/code&gt; menu:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.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%2F1zytvqw1qt9n7rfbunnc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.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%2F1zytvqw1qt9n7rfbunnc.png" alt="GitLab pipeline schedule config" width="800" height="587"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.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%2Fck6ko45uwhthptddp9oi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.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%2Fck6ko45uwhthptddp9oi.png" alt="GitLab pipeline schedule overview" width="800" height="117"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, it is all set and you have to wait for the next run to happen or press the "Play" button to create the scheduled pipeline manually.&lt;br&gt;
This is how successful scheduled pipeline should look like:&lt;br&gt;
&lt;a href="https://media2.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%2F7lbxmvm9u0g2lyt5ktmd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.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%2F7lbxmvm9u0g2lyt5ktmd.png" alt="GitLab successful cleanup pipeline" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And runner log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
Installing collected packages: urllib3, six, idna, charset-normalizer, certifi, requests, python-dateutil
Successfully installed certifi-2024.2.2 charset-normalizer-3.3.2 idna-3.7 python-dateutil-2.9.0.post0 requests-2.31.0 six-1.16.0 urllib3-2.2.1
$ python scripts/cleanup-deployments.py --environment ${CLEANUP_ENVIRONMENT} --days ${CLEANUP_KEEP_DAYS} --count ${CLEANUP_KEEP_COUNT}
Fetching all production page deployments...
Found 2 production page deployments.
Deleting obsolete production page deployments older than 7 days, while keeping 2 latest...
0 obsolete production page deployments have been deleted.
Cleaning up project directory and file based variables
00:01
Job succeeded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Cloudflare is a great service for hosting static content, it provides complete history of page deployments&lt;/li&gt;
&lt;li&gt;Rolling back production deployment can be done from Cloudflare dashboard&lt;/li&gt;
&lt;li&gt;Cleaning deployments from Cloudflare dashboard is tedious task&lt;/li&gt;
&lt;li&gt;Cloudflare provides flexible API which allow listing and cleaning up deployments&lt;/li&gt;
&lt;li&gt;Scripting skills are required to make use of Cloudflare API&lt;/li&gt;
&lt;li&gt;Cleanup job can be added to GitLab CI/CD and configured to run on schedule&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cicd</category>
      <category>gitlab</category>
      <category>cloudflare</category>
      <category>automation</category>
    </item>
    <item>
      <title>Deploying Hugo from Self-Hosted GitLab to Cloudflare Pages</title>
      <dc:creator>Maxim Radugin</dc:creator>
      <pubDate>Sat, 31 Aug 2024 18:15:08 +0000</pubDate>
      <link>https://dev.to/maxim_radugin/deploying-hugo-from-self-hosted-gitlab-to-cloudflare-pages-4nnc</link>
      <guid>https://dev.to/maxim_radugin/deploying-hugo-from-self-hosted-gitlab-to-cloudflare-pages-4nnc</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;As you may have noticed &lt;a href="https://radugin.com" rel="noopener noreferrer"&gt;my main blog&lt;/a&gt; site is built using &lt;a href="https://gohugo.io/" rel="noopener noreferrer"&gt;Hugo&lt;/a&gt; and &lt;a href="https://github.com/adityatelange/hugo-PaperMod/" rel="noopener noreferrer"&gt;Papermod&lt;/a&gt; theme. &lt;br&gt;
After playing around with Hugo and getting comfortable with it, I wanted to push the initial version of my blog site to the git repository and set up an automatic deployment pipeline to be able to easily publish updates to the web. &lt;/p&gt;

&lt;p&gt;Since I am the lucky owner of a self-hosted GitLab instance which I've been using for more than a decade for my projects, it was an obvious decision to push my blog site there too. Same story with Cloudflare - I use their DNS services, and since they also provide static content hosting services - &lt;a href="https://pages.cloudflare.com/" rel="noopener noreferrer"&gt;Pages&lt;/a&gt;, decided to give it a try. &lt;/p&gt;

&lt;p&gt;Fast googling revealed that Cloudflare supports automatic deployments of Hugo-based sites from GitHub and GitLab..., but only for &lt;a href="https://blog.cloudflare.com/cloudflare-pages-partners-with-gitlab" rel="noopener noreferrer"&gt;cloud-based SaaS version&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Luckily Cloudflare has &lt;a href="https://developers.cloudflare.com/workers/wrangler/" rel="noopener noreferrer"&gt;Wrangler&lt;/a&gt; - command-line interface to manage &lt;em&gt;Worker projects&lt;/em&gt;. You may ask what &lt;em&gt;workers&lt;/em&gt; have to do with &lt;em&gt;static pages&lt;/em&gt;? Well, they are managed by the same tool, see &lt;a href="https://developers.cloudflare.com/workers/wrangler/commands/#pages" rel="noopener noreferrer"&gt;pages&lt;/a&gt; command documentation of Wrangler tool.&lt;/p&gt;

&lt;p&gt;Below I will explain how to tie Wrangler together with GitLab CI/CD to perform deployment to Cloudflare Pages. Further steps assume you have knowledge and experience of using Cloudflare services, git, Hugo and GitLab CI/CD. I will not go into details for some of the steps. &lt;/p&gt;
&lt;h2&gt;
  
  
  Setting and Testing Locally
&lt;/h2&gt;

&lt;p&gt;Below are the steps to prepare project, repository and test everything locally: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create Hugo project&lt;/li&gt;
&lt;li&gt;Change into project directory&lt;/li&gt;
&lt;li&gt;Login with Wrangler CLI into your Cloudflare account: &lt;code&gt;npx wrangler login&lt;/code&gt;, follow instructions&lt;/li&gt;
&lt;li&gt;Create new Pages project, I will call mine &lt;code&gt;pages-for-article&lt;/code&gt;, make sure your project git default branch matches with what you pass in &lt;code&gt;--production-branch&lt;/code&gt; argument. Cloudflare will use it later to determine if you are publishing to &lt;code&gt;production&lt;/code&gt; or &lt;code&gt;preview&lt;/code&gt; environments: &lt;code&gt;npx wrangler pages project create pages-for-article --production-branch main&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Initialize git repository, do this before deploying to Pages, as Wrangler uses git metadata for deployments&lt;/li&gt;
&lt;li&gt;Build Hugo project: &lt;code&gt;hugo&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Now, run deployment: &lt;code&gt;npx wrangler pages deploy public --project-name pages-for-article&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;This is what you should see from Cloudflare dashboard &lt;code&gt;Workers &amp;amp; Pages&lt;/code&gt; section, notice &lt;code&gt;main&lt;/code&gt; branch and &lt;code&gt;Production&lt;/code&gt; environment in the screenshot:
&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhn2h2czcelmo6y8zuci5.png" alt="Pages project in Cloudflare" width="800" height="226"&gt;
&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;public/&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;Commit and push your project to git&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now we are all set to move to the GitLab CI/CD configuration.&lt;/p&gt;
&lt;h2&gt;
  
  
  Setting GitLab CI/CD
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Requirements
&lt;/h3&gt;

&lt;p&gt;Before going into configuration details, I like to define my requirements for CI/CD pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I want to have two environments - &lt;code&gt;production&lt;/code&gt; and &lt;code&gt;preview&lt;/code&gt; (staging)&lt;/li&gt;
&lt;li&gt;I want to deploy changes from default branch, tags or merge requests only&lt;/li&gt;
&lt;li&gt;I want to be able to see draft content in the &lt;code&gt;preview&lt;/code&gt; environment&lt;/li&gt;
&lt;li&gt;I want to be able to deploy any changes except tags to &lt;code&gt;preview&lt;/code&gt; environment&lt;/li&gt;
&lt;li&gt;I want to be able to deploy to &lt;code&gt;production&lt;/code&gt; environment changes only from default branch or tags&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  GitLab Project Configuration
&lt;/h3&gt;

&lt;p&gt;Wrangler requires certain authentication environment variables to be present to operate from CI/CD. See &lt;a href="https://developers.cloudflare.com/workers/wrangler/ci-cd/" rel="noopener noreferrer"&gt;Run Wrangler in CI/CD guide&lt;/a&gt;. Add these variables in the GitLab project under &lt;code&gt;Settings&lt;/code&gt; -&amp;gt; &lt;code&gt;CI/CD&lt;/code&gt; -&amp;gt; &lt;code&gt;Variables&lt;/code&gt;. Make variables masked, unprotected and non-expandable.&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ferf3tuuis54d4147lom3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ferf3tuuis54d4147lom3.png" alt="GitLab CI/CD variables for Cloudflare" width="800" height="198"&gt;&lt;/a&gt;&lt;br&gt;
Make sure that the project has at least one runner with a docker execution environment under &lt;code&gt;Settings&lt;/code&gt; -&amp;gt; &lt;code&gt;CI/CD&lt;/code&gt; -&amp;gt; &lt;code&gt;Runners&lt;/code&gt;. &lt;/p&gt;
&lt;h3&gt;
  
  
  Pipeline Definition
&lt;/h3&gt;

&lt;p&gt;GitLab uses &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; to describe jobs that make up the CI/CD pipeline. I will not go into detail of every aspect of that file, but rather highlight parts that are made up to fulfill above requirements. Complete &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; file is available at &lt;a href="https://github.com/mradugin/deploy-hugo-from-gitlab-to-cloudflare-pages/tree/deploy-hugo-from-gitlab-to-cloudflare-pages" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;workflow:rules&lt;/code&gt; describes the requirement to create and run a pipeline only for the default branch, merge requests commits and tags, omitting commits to branches without merge requests.&lt;/p&gt;

&lt;p&gt;Because of the requirement to have draft content available in the &lt;code&gt;preview&lt;/code&gt; environment, two distinct build jobs are needed - &lt;code&gt;build:staging&lt;/code&gt; and &lt;code&gt;build:production&lt;/code&gt;, first one will build a site with drafts, second - without, see &lt;code&gt;script&lt;/code&gt; section. Both jobs inherit from common &lt;code&gt;.build&lt;/code&gt; template, which describes common build configuration - using &lt;code&gt;alpine&lt;/code&gt; image for runtime environment and installing hugo using &lt;code&gt;apk&lt;/code&gt; package manager, collecting build artifacts from &lt;code&gt;public&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;The most interesting part of &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; is the &lt;code&gt;.deploy&lt;/code&gt; template. What it does - it runs Wrangler deploy command with additional arguments - project name, branch, commit hash, and commit message. Values for the latter three arguments are taken from corresponding variables defined in the same template, which in turn reference pre-defined GitLab CI/CD variables by default.&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;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;CLOUDFLARE_BRANCH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$CI_COMMIT_BRANCH"&lt;/span&gt;
  &lt;span class="na"&gt;CLOUDFLARE_COMMIT_HASH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$CI_COMMIT_SHA"&lt;/span&gt;
  &lt;span class="na"&gt;CLOUDFLARE_COMMIT_MESSAGE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$CI_COMMIT_MESSAGE"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You may ask - why is it so complex? As mentioned earlier, Wrangler can extract this information from git itself. Yes, indeed, it can, but Wrangler also makes a decision on where to deploy - &lt;code&gt;production&lt;/code&gt; or &lt;code&gt;preview&lt;/code&gt; environment based on the branch name. Default branch always goes to production, period, you can't affect it, this basically breaks requirements 4, 5. &lt;/p&gt;

&lt;p&gt;Luckily, Wrangler has these arguments and you can pass whatever you want making it possible to workaround behavior of always publishing default branch changes to &lt;code&gt;production&lt;/code&gt;. And this is what is being done in &lt;code&gt;deploy:staging&lt;/code&gt; and &lt;code&gt;deploy:production&lt;/code&gt; which inherit from the &lt;code&gt;.deploy&lt;/code&gt; template.&lt;/p&gt;

&lt;p&gt;Here is a closer look at &lt;code&gt;deploy:staging&lt;/code&gt; &lt;code&gt;variables&lt;/code&gt; and &lt;code&gt;rules&lt;/code&gt; section:&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;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;CLOUDFLARE_BRANCH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME"&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$CI_COMMIT_BRANCH&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$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;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;CLOUDFLARE_BRANCH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$CI_COMMIT_BRANCH-preview"&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$CI_COMMIT_TAG'&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;never&lt;/span&gt;
    &lt;span class="pi"&gt;-&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;First rule makes it possible to deploy default branch changes to &lt;code&gt;preview&lt;/code&gt; environment by adding &lt;code&gt;-preview&lt;/code&gt; postfix to the default branch name, this way Wrangler will treat it as a non-default branch. &lt;/li&gt;
&lt;li&gt;Second rule disallows deployment of tags to &lt;code&gt;preview&lt;/code&gt;, since I will use tags as release milestones for this project.&lt;/li&gt;
&lt;li&gt;For all other cases, i.e. merge request commits - merge request source branch name will be used, as defined in &lt;code&gt;variables&lt;/code&gt; section.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Please note that &lt;code&gt;preview&lt;/code&gt; deployments are manual actions and require launching these jobs from pipeline view in the GitLab by hand. Also for pipelines not to appear as "blocked" in GitLab, &lt;code&gt;deploy:staging&lt;/code&gt; has &lt;code&gt;allow_failure: true&lt;/code&gt; set. &lt;/p&gt;

&lt;p&gt;Cool thing about Cloudflare Pages is that preview deployments can be accessed via &lt;code&gt;&amp;lt;branch-name&amp;gt;.pages-for-article.pages.dev&lt;/code&gt; address. For default branch preview for this project it is &lt;a href="https://main-preview.pages-for-article.pages.dev" rel="noopener noreferrer"&gt;https://main-preview.pages-for-article.pages.dev&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;These are &lt;code&gt;deploy:production&lt;/code&gt; rules:&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;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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$CI_COMMIT_BRANCH&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$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="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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$CI_COMMIT_TAG'&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;always&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;CLOUDFLARE_BRANCH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$CI_DEFAULT_BRANCH"&lt;/span&gt;
        &lt;span class="na"&gt;CLOUDFLARE_COMMIT_MESSAGE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$CI_COMMIT_TAG&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$CI_COMMIT_TAG_MESSAGE"&lt;/span&gt;
    &lt;span class="pi"&gt;-&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;never&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;First rule allows manually deploying anything from the default branch to the &lt;code&gt;production&lt;/code&gt; environment; this normally is not used, but sometimes may come in handy.&lt;/li&gt;
&lt;li&gt;Second rule - automatically deploys to &lt;code&gt;production&lt;/code&gt; environment when tag is created. Since Wrangler has no support for tags, passing actual tag name into branch name would result in tag being deployed into &lt;code&gt;preview&lt;/code&gt; environment instead of &lt;code&gt;production&lt;/code&gt;, to workaround this - default branch name is fed into branch name, and actual tag name along with tag message is set in commit message. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it, these rules satisfy my requirements set above, and I can use GitLab to track and publish my blog changes to Cloudflare pages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Further Improvements
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;I am using bare &lt;code&gt;alpine&lt;/code&gt; and &lt;code&gt;node&lt;/code&gt; docker images which download &lt;code&gt;hugo&lt;/code&gt; and &lt;code&gt;wrangler&lt;/code&gt; for each job invocation. This negatively affects pipeline speed and generates excess traffic, a better approach would be to use pre-built images containing these tools. On the other hand, impact is negligible, as whole pipeline takes around 30 seconds to build and deploy.&lt;/li&gt;
&lt;li&gt;Add a separate deployment job to the &lt;code&gt;preview&lt;/code&gt; environment without draft content to make it identical to what is going to be deployed to the &lt;code&gt;production&lt;/code&gt; environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;GitLab and Cloudflare are great services with lots of useful features and customization options. With some exploration and trial and error approach I was able to create CI/CD pipeline configuration that meets my requirements for publishing Hugo blog to Cloudflare Pages from a self-hosted GitLab instance.&lt;/p&gt;

&lt;p&gt;Complete &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; file is available on &lt;a href="https://github.com/mradugin/deploy-hugo-from-gitlab-to-cloudflare-pages/" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>hugo</category>
      <category>gitlab</category>
      <category>cicd</category>
      <category>cloudflare</category>
    </item>
    <item>
      <title>Bridging Jira and GitLab: Automating CI/CD Pipelines for Releases</title>
      <dc:creator>Maxim Radugin</dc:creator>
      <pubDate>Sun, 25 Aug 2024 10:32:34 +0000</pubDate>
      <link>https://dev.to/maxim_radugin/bridging-jira-and-gitlab-automating-cicd-pipelines-for-releases-3lob</link>
      <guid>https://dev.to/maxim_radugin/bridging-jira-and-gitlab-automating-cicd-pipelines-for-releases-3lob</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Jira Cloud and GitLab are popular productivity and DevOps platforms widely used by many organizations and software development teams. While the integration between these platforms has significantly improved in recent years, there are still areas in common software development tasks that require manual effort or custom integration solutions.&lt;/p&gt;

&lt;p&gt;One such area is software releases. In this post, I will demonstrate how to streamline and automate this process, it will be useful for teams who:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Plan and manage tasks in Jira;&lt;/li&gt;
&lt;li&gt;Use Jira versions for planning product milestones and releases;&lt;/li&gt;
&lt;li&gt;Keep their source code in GitLab;&lt;/li&gt;
&lt;li&gt;Use GitLab's CI/CD pipelines for deploying and releasing products.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  User Story
&lt;/h2&gt;

&lt;p&gt;As a Release Manager, I want GitLab to automatically trigger a release CI/CD pipeline when I change a Jira version's status to 'Released' and its name matches a specific format (e.g., 'v1.2.3'), so that release information is immediately synchronized between Jira and GitLab, reducing manual work and potential errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;To make this happen, we need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set up Jira to trigger an action when a version is released.&lt;/li&gt;
&lt;li&gt;Use the GitLab API to create a matching tag in git repository.&lt;/li&gt;
&lt;li&gt;Configure GitLab CI/CD to run a release or deploy job when this tag is created.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We'll go through these steps in detail for both Jira and GitLab. This guide uses GitLab's cloud version, but the process is similar for self-hosted GitLab instances.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitLab Configuration
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Create Personal Access Token
&lt;/h4&gt;

&lt;p&gt;You can skip this step if you already have a Personal Access Token (PAT) with the &lt;code&gt;api&lt;/code&gt; scope that you can reuse. If not, follow these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In GitLab, navigate to your preferences by clicking on your avatar icon, then &lt;code&gt;Preferences&lt;/code&gt; -&amp;gt; &lt;code&gt;Access tokens&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;code&gt;Add new token&lt;/code&gt;, enter a token name (e.g., &lt;code&gt;Jira Release Automation&lt;/code&gt;), extend the expiration date, and select the &lt;code&gt;api&lt;/code&gt; scope
&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv0yvusmuikt0dutom5mq.png" alt="GitLab PAT configuration" width="800" height="401"&gt;
&lt;/li&gt;
&lt;li&gt;Press &lt;code&gt;Create personal access token&lt;/code&gt; and note down the token value
&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1zt43wu2t52nyrsb5wbe.png" alt="GitLab PAT created" width="800" height="152"&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Modify CI/CD Configuration
&lt;/h4&gt;

&lt;p&gt;In your GitLab project, add the following rule to the release job in your &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; file. This ensures the job is triggered only for git tags with a &lt;code&gt;vX.Y.Z&lt;/code&gt; format, where X, Y, Z are non-negative integers:&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;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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$CI_COMMIT_TAG&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;=~&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/^v([0-9]+)\.([0-9]+)\.([0-9]+)$/'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can find a full example of the &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; file &lt;a href="https://gitlab.com/radugin.com/jira-gitlab-release-automation/-/blob/main/.gitlab-ci.yml?ref_type=heads" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Jira Project Configuration
&lt;/h3&gt;

&lt;p&gt;We'll use Jira's built-in Automation to trigger the release pipeline by creating "release" tag in the desired GitLab project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;From the project sidebar, select &lt;code&gt;Automation&lt;/code&gt; -&amp;gt; &lt;code&gt;Create rule&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;In the rule editor, add a trigger: &lt;code&gt;Version released&lt;/code&gt;. Expand &lt;code&gt;More options&lt;/code&gt; and set &lt;code&gt;Version name filter&lt;/code&gt; to &lt;code&gt;^v([0-9]+)\.([0-9]+)\.([0-9]+)$&lt;/code&gt;.
&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyupb4mgyt0los5yb467c.png" alt="Version released trigger configuration" width="800" height="431"&gt;
&lt;/li&gt;
&lt;li&gt;Press &lt;code&gt;Next&lt;/code&gt;, then on the workflow press &lt;code&gt;Add component&lt;/code&gt;. Select &lt;code&gt;THEN: Add an action&lt;/code&gt;, and add the &lt;code&gt;Send web request&lt;/code&gt; component.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Configure the &lt;code&gt;Send web request&lt;/code&gt; component:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In &lt;code&gt;Web request URL&lt;/code&gt;, enter &lt;code&gt;https://gitlab.com/api/v4/projects/&amp;lt;group&amp;gt;%2F&amp;lt;project name&amp;gt;/repository/tags&lt;/code&gt;, replacing &lt;code&gt;&amp;lt;group&amp;gt;&lt;/code&gt; with your GitLab group and &lt;code&gt;&amp;lt;project name&amp;gt;&lt;/code&gt; with your actual project name. If you wonder what &lt;code&gt;%2F&lt;/code&gt; is - it is url-encoded value of &lt;code&gt;/&lt;/code&gt; character. In my case final URL is &lt;code&gt;https://gitlab.com/api/v4/projects/radugin.com%2Fjira-gitlab-release-automation/repository/tags&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;Leave &lt;code&gt;HTTP method&lt;/code&gt; as &lt;code&gt;POST&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;Web request body&lt;/code&gt; to &lt;code&gt;Custom data&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Set &lt;code&gt;Custom data&lt;/code&gt; to:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "tag_name": {{version.name.asJsonString}},
    "ref": "main",
    "message": {{version.description.asJsonString}}
}
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;This request will create a new tag in specified GitLab repository, Jira version name will be used as a tag name, tag will be created from branch specified in &lt;code&gt;ref&lt;/code&gt; (usually &lt;code&gt;main&lt;/code&gt; or &lt;code&gt;master&lt;/code&gt;), tag message will be populated from Jira version description field.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In &lt;code&gt;Headers&lt;/code&gt;, add a hidden &lt;code&gt;PRIVATE-TOKEN&lt;/code&gt; with your GitLab PAT value.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpd1prysxd9r3ga1ly59n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpd1prysxd9r3ga1ly59n.png" alt="Send web request action configuration" width="800" height="435"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Press &lt;code&gt;Next&lt;/code&gt;, then &lt;code&gt;Turn on rule&lt;/code&gt;. Enter a &lt;code&gt;Rule name&lt;/code&gt; (e.g., &lt;code&gt;GitLab Release Automation&lt;/code&gt;), and click &lt;code&gt;Turn on rule&lt;/code&gt; again.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Verifying Automation
&lt;/h3&gt;

&lt;h4&gt;
  
  
  From Jira
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Create a version, for example, &lt;code&gt;v1.0.0&lt;/code&gt;, add version description.&lt;/li&gt;
&lt;li&gt;Perform a version release.&lt;/li&gt;
&lt;li&gt;Navigate to &lt;code&gt;Automation&lt;/code&gt; -&amp;gt; &lt;code&gt;Audit log&lt;/code&gt; and verify that the rule execution was successful.
&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyjvpx06ju4vj517dj797.png" alt="Automation audit log" width="800" height="286"&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  From GitLab
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to the &lt;code&gt;Tags&lt;/code&gt; section and observe the newly created tag.
&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmkbea7t1imcl4luxhar9.png" alt="GitLab tags view" width="800" height="180"&gt;
&lt;/li&gt;
&lt;li&gt;Click on the ✅ next to the tag to open the pipeline view, and verify that the release job (in current example - deploy job) was created.
&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffjcojg2400pfwvi7tkd1.png" alt="GitLab pipelines view" width="800" height="262"&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This integration represents a step towards automating and streamlining the release management process for teams using GitLab and Jira, as it bridges an important gap in the DevOps workflow.&lt;br&gt;
The benefits of this automation include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reduced manual effort in managing releases;&lt;/li&gt;
&lt;li&gt;Improved synchronization of release information between Jira and GitLab;&lt;/li&gt;
&lt;li&gt;Decreased likelihood of errors in the release process;&lt;/li&gt;
&lt;li&gt;Enhanced traceability of releases from project management to code deployment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While the setup requires some initial configuration, the long-term benefits in efficiency and reliability make it a valuable addition to any team's automation toolset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Materials
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://support.atlassian.com/jira-software-cloud/docs/enable-releases-and-versions/" rel="noopener noreferrer"&gt;Article on how to enable releases and versions in Jira&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.atlassian.com/agile/tutorials/versions" rel="noopener noreferrer"&gt;Tutorial on using versions in Jira&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.gitlab.com/ee/api/tags.html" rel="noopener noreferrer"&gt;GitLab tags API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.gitlab.com/ee/ci/jobs/job_rules.html" rel="noopener noreferrer"&gt;GitLab job rules examples&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cicd</category>
      <category>jira</category>
      <category>gitlab</category>
      <category>devops</category>
    </item>
    <item>
      <title>Improve Productivity with CMake and Compiler Cache Integration</title>
      <dc:creator>Maxim Radugin</dc:creator>
      <pubDate>Sun, 14 Jul 2024 22:18:22 +0000</pubDate>
      <link>https://dev.to/maxim_radugin/improve-productivity-with-cmake-and-compiler-cache-integration-36p0</link>
      <guid>https://dev.to/maxim_radugin/improve-productivity-with-cmake-and-compiler-cache-integration-36p0</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Build times can significantly impact developers' productivity. While what constitutes a slow or fast build is subjective, it is clear that slow builds introduce longer feedback loops and lead to more context switches, resulting in decreased productivity.&lt;/p&gt;

&lt;p&gt;C++ compilers are slow, and the use of the standard C++ library and other popular libraries, especially header-only ones, exacerbates this. &lt;a href="https://artificial-mind.net/projects/compile-health/" rel="noopener noreferrer"&gt;Here&lt;/a&gt;, you can explore the cost in terms of build time for including certain headers.&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://www.computer.org/csdl/magazine/so/2023/04/10176199/1OAJyfknInm" rel="noopener noreferrer"&gt;study&lt;/a&gt; shows that even modest improvements in build time can positively affect developers' productivity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proven Way of Optimizing Build Times
&lt;/h2&gt;

&lt;p&gt;One proven way to optimize build times is by using compiler cache programs such as &lt;a href="https://ccache.dev/" rel="noopener noreferrer"&gt;ccache&lt;/a&gt; or &lt;a href="https://github.com/mozilla/sccache" rel="noopener noreferrer"&gt;sccache&lt;/a&gt;. These programs cache compilation results and reuse them, omitting subsequent compiler calls if the inputs have not changed.&lt;/p&gt;

&lt;p&gt;According to &lt;a href="https://ccache.dev/performance.html" rel="noopener noreferrer"&gt;performance measurements&lt;/a&gt; conducted by the ccache team, build time improvements on subsequent compilations can range from 5x to 145x.&lt;/p&gt;

&lt;p&gt;From my personal experience, realistic improvements can range from 1.5x to 10x, depending on multiple factors such as the build machine hardware, OS, compiler, and project source code.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, What's the Deal?
&lt;/h2&gt;

&lt;p&gt;If compiler cache programs can bring such improvements, why aren't they used everywhere for every C++ project? One possible answer is the difficulty of integration. There are multiple build systems and compiler types, each with its own peculiarities when integrating compiler cache programs. The cross-platform nature of modern software adds even more complexity to the integration process.&lt;/p&gt;

&lt;p&gt;Nowadays, for many cross-platform C/C++ software projects, CMake is the go-to build system. According to the &lt;a href="https://survey.stackoverflow.co/2023/#most-popular-technologies-tools-tech" rel="noopener noreferrer"&gt;Stack Overflow Developer Survey 2023&lt;/a&gt;, 14.34% of developers use CMake, with only Make being more widely used as a cross-platform C++ build system. However, if you look at the sccache &lt;a href="https://github.com/mozilla/sccache?tab=readme-ov-file#usage" rel="noopener noreferrer"&gt;README&lt;/a&gt;, there's a lengthy section on how to start using a compiler cache and make it somewhat usable with CMake. This section does not cover all the possible combinations of OSes, compilers, and generators supported by CMake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution for CMake
&lt;/h2&gt;

&lt;p&gt;To solve this problem, the &lt;a href="https://github.com/mradugin/cmake-findccache" rel="noopener noreferrer"&gt;cmake-findccache&lt;/a&gt; module was created. As the name suggests, it finds a suitable compiler cache program and configures the CMake project to enable the use of the compiler cache.&lt;/p&gt;

&lt;p&gt;It supports Xcode, Ninja, Unix Makefiles, and Visual Studio generators, and has been tested with gcc, clang, and MSVC compilers on Linux, macOS, and Windows.&lt;/p&gt;

&lt;p&gt;As shown in the &lt;a href="https://github.com/mradugin/cmake-findccache/blob/main/example/CMakeLists.txt" rel="noopener noreferrer"&gt;example&lt;/a&gt; project, in most cases, only a couple of lines are needed to enable the use of ccache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cmake"&gt;&lt;code&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;APPEND CMAKE_MODULE_PATH &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CMAKE_SOURCE_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/.."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;find_package&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;ccache&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, the module will only look for &lt;code&gt;ccache&lt;/code&gt;. To try other compiler cache programs, define the &lt;code&gt;CCACHE_PROGRAMS&lt;/code&gt; variable with the list of desired compiler cache programs in order of preference, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cmake"&gt;&lt;code&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;CCACHE_PROGRAMS &lt;span class="s2"&gt;"ccache;sccache;buildcache"&lt;/span&gt; CACHE STRING _ FORCE&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a ccache program is found and the generator is supported, the CMake configuration phase log should contain lines similar to the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- Found ccache: /usr/bin/ccache
-- Using compiler cache: YES
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the first successful build of the project, inspect the ccache statistics by running &lt;code&gt;ccache --show-stats&lt;/code&gt;. The result should be similar to the output below. Cacheable calls statistics should be non-zero, and after the first build, most calls will result in cache misses due to an empty cache.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cacheable calls:      2 /   2 (100.0%)
  Hits:               0 /   2 ( 0.00%)
    Direct:           0
    Preprocessed:     0
  Misses:             2 /   2 (100.0%)
Local storage:
  Cache size (GiB): 0.0 / 5.0 ( 0.00%)
  Hits:               0 /   2 ( 0.00%)
  Misses:             2 /   2 (100.0%)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On subsequent builds, cacheable call statistics should continue to increment, along with the number of hits. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cacheable calls:      4 /   4 (100.0%)
  Hits:               2 /   4 (50.00%)
    Direct:           2 /   2 (100.0%)
    Preprocessed:     0 /   2 ( 0.00%)
  Misses:             2 /   4 (50.00%)
Local storage:
  Cache size (GiB): 0.0 / 5.0 ( 0.00%)
  Hits:               2 /   4 (50.00%)
  Misses:             2 /   4 (50.00%)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is when compilation time should decrease, thanks to using cached results instead of invoking the compiler!&lt;/p&gt;

&lt;h2&gt;
  
  
  But... Still Experimental and Fragile
&lt;/h2&gt;

&lt;p&gt;After &lt;a href="https://github.com/mradugin/cmake-findccache/actions" rel="noopener noreferrer"&gt;examining more combinations&lt;/a&gt; of OSes, generators, and compilers, it turned out that in some combinations, the compiler cache does not work as expected:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On Windows with MSYS2, neither &lt;code&gt;mingw64&lt;/code&gt; nor &lt;code&gt;clang64&lt;/code&gt; works with the &lt;code&gt;Ninja&lt;/code&gt; generator, resulting in a &lt;code&gt;ccache: error: Could not find compiler "D:\a\_temp\msys64\clang64\bin\clang++.exe" in PATH&lt;/code&gt; error during compilation.&lt;/li&gt;
&lt;li&gt;On Windows GitHub Runner (&lt;code&gt;Windows Server 2019&lt;/code&gt;) with the &lt;code&gt;CL&lt;/code&gt; compiler and &lt;code&gt;Visual Studio 17 2022&lt;/code&gt; generator, ccache behaves oddly. It caches compilation results from the first run of the build, but for the second run, compilations are not accounted for in the statistics at all. However, on &lt;code&gt;Windows 10 Pro&lt;/code&gt;, a similar setup works just fine.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Further Improvements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Fix or find workarounds for the above issues.&lt;/li&gt;
&lt;li&gt;Add support for more compilers, such as &lt;code&gt;clang-cl&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Test with other cache tools, like &lt;code&gt;sccache&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feel free to clone and submit pull requests to the &lt;a href="https://github.com/mradugin/cmake-findccache" rel="noopener noreferrer"&gt;cmake-findccache&lt;/a&gt; project to improve compiler cache support in CMake!&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Compiler cache support can bring significant build time improvements and increase productivity. However, even with the improved integration between CMake and compiler cache programs provided by &lt;a href="https://github.com/mradugin/cmake-findccache" rel="noopener noreferrer"&gt;cmake-findccache&lt;/a&gt;, it remains an experimental or unavailable feature on some platforms and configurations.&lt;/p&gt;

</description>
      <category>cmake</category>
      <category>productivity</category>
      <category>ccache</category>
      <category>cpp</category>
    </item>
    <item>
      <title>Extensible Control of Reaper via OSC and Scripts</title>
      <dc:creator>Maxim Radugin</dc:creator>
      <pubDate>Sun, 07 Jul 2024 21:33:33 +0000</pubDate>
      <link>https://dev.to/maxim_radugin/extensible-control-of-reaper-via-osc-and-scripts-2n27</link>
      <guid>https://dev.to/maxim_radugin/extensible-control-of-reaper-via-osc-and-scripts-2n27</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://reaper.fm" rel="noopener noreferrer"&gt;REAPER&lt;/a&gt; is a powerful Digital Audio Workstation (DAW) with enormous customization possibilities. Its scripting support, external control capabilities, support for many DAW plugin formats, and compatibility with MacOS and Windows make it an obvious choice for building all sorts of integrations and automation. At Sonarworks, we use REAPER as a plugin host as part of our DAW plugin test automation framework.&lt;/p&gt;

&lt;p&gt;Despite its great functionality, REAPER features minimal documentation, which makes it challenging to unleash its full potential.&lt;/p&gt;

&lt;p&gt;In this article, I'll provide a complete guide on how to extend REAPER's control possibilities when interacting with it via the &lt;a href="https://opensoundcontrol.stanford.edu/" rel="noopener noreferrer"&gt;Open Sound Control (OSC)&lt;/a&gt; interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Out-of-the-Box Functionality
&lt;/h2&gt;

&lt;p&gt;Out of the box, REAPER supports a limited set of functions via the OSC interface, which are defined in &lt;code&gt;.ReaperOSC&lt;/code&gt; files located in &lt;code&gt;~/Library/Application\ Support/REAPER/OSC/&lt;/code&gt; on MacOS. You can modify the address patterns of OSC messages REAPER will react to and call pre-defined functions, but you can't define your own functions directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defining and Calling Custom Actions
&lt;/h2&gt;

&lt;p&gt;Fortunately, REAPER supports calling custom actions via &lt;code&gt;/action/_COMMAND_ID&lt;/code&gt; OSC messages.&lt;/p&gt;

&lt;p&gt;Below, I'll demonstrate the setup. Please note that I'll be modifying some of REAPER's configuration files directly, as certain modifications via REAPER itself are not possible or are very unhandy. Always backup your files before making any modifications to avoid losing your work or breaking your existing setup.&lt;/p&gt;

&lt;p&gt;Custom actions are stored in &lt;code&gt;~/Library/Application\ Support/REAPER/reaper-kb.ini&lt;/code&gt; on MacOS. The file format description can be found &lt;a href="https://mespotin.uber.space/Ultraschall/Reaper-Filetype-Descriptions.html#Reaper-kb.ini" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For my use case, I only need my custom actions, so I started with an empty file and added the following line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SCR 4 0 MY_COMPANY_TEST_SIMPLE_OSC "MyCompany: test simple OSC trigger" "MyCompany/test_simple_osc.lua"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This defines a custom action. For the meaning of &lt;code&gt;SCR 4 0&lt;/code&gt;, see the &lt;a href="https://mespotin.uber.space/Ultraschall/Reaper-Filetype-Descriptions.html#Reaper-kb.ini" rel="noopener noreferrer"&gt;reaper-kb.ini&lt;/a&gt; documentation. &lt;code&gt;_MY_COMPANY_TEST_SIMPLE_OSC&lt;/code&gt; (note the underscore) is the Command ID of the &lt;code&gt;test_simple_osc.lua&lt;/code&gt; script, which should be placed in the &lt;code&gt;~/Library/Application\ Support/REAPER/Scripts/MyCompany/&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;Content of &lt;code&gt;test_simple_osc.lua&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;reaper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShowMessageBox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"This is a simple script that can be invoked via OSC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Info"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After manually editing files, REAPER must be restarted for changes to take effect!&lt;/p&gt;

&lt;p&gt;To verify the script and &lt;code&gt;reaper-kb.ini&lt;/code&gt; are correct, run REAPER, and from the &lt;code&gt;Actions&lt;/code&gt; dialog (&lt;code&gt;Shift&lt;/code&gt; + &lt;code&gt;?&lt;/code&gt;), find the &lt;code&gt;MyCompany: test simple OSC trigger&lt;/code&gt; action and run it. You should see a message box with the text "This is a simple script that can be invoked via OSC".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1l9oo1w03mvswtiabsso.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1l9oo1w03mvswtiabsso.png" alt="Message Box that was triggered from the script" width="744" height="664"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If not done already, from REAPER &lt;code&gt;Preferences&lt;/code&gt; (&lt;code&gt;Command&lt;/code&gt; + &lt;code&gt;,&lt;/code&gt;), navigate to the &lt;code&gt;Control/OSC/web&lt;/code&gt; section and add a new &lt;code&gt;OSC (Open Sound Control)&lt;/code&gt; control surface. Leave the &lt;code&gt;Default&lt;/code&gt; pattern config, select &lt;code&gt;Local port&lt;/code&gt; mode, and note the &lt;code&gt;Local listen port&lt;/code&gt;. Other settings can remain at their default values for now, as shown in the screenshot.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fojt81tukr0s6eos97ccg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fojt81tukr0s6eos97ccg.png" alt="REAPER OSC Config" width="800" height="607"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now you can send an OSC message to trigger the custom action. For testing purposes, you can use &lt;a href="https://github.com/yoggy/sendosc" rel="noopener noreferrer"&gt;sendosc&lt;/a&gt; (which can be installed on MacOS using &lt;code&gt;brew install yoggy/tap/sendosc&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;From the terminal, run: &lt;code&gt;sendosc localhost 8000 /action/_MY_COMPANY_TEST_SIMPLE_OSC&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If everything worked correctly, you should see the same message box with the text "This is a simple script that can be invoked via OSC".&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting OSC Communication
&lt;/h2&gt;

&lt;p&gt;To ensure OSC messages are reaching REAPER, navigate to &lt;code&gt;Preferences&lt;/code&gt; (&lt;code&gt;Command&lt;/code&gt; + &lt;code&gt;,&lt;/code&gt;), then the &lt;code&gt;Control/OSC/web&lt;/code&gt; section. Select the existing &lt;code&gt;OSC&lt;/code&gt; control from the list and press &lt;code&gt;Edit&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;From the &lt;code&gt;Control Surface Settings&lt;/code&gt; window, press &lt;code&gt;Listen...&lt;/code&gt;, and observe incoming OSC messages. Each line represents a single received OSC message, for example: &lt;code&gt;/test_message_with_multi_arg [sf] "test string" 123.449997&lt;/code&gt;, where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/test_message_with_multi_arg&lt;/code&gt; is the message address;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[sf]&lt;/code&gt; describes argument types, s - string, f - float number, i - integer number. In this example, two arguments were passed: the first of type string, the second a float;&lt;/li&gt;
&lt;li&gt;followed by argument values &lt;code&gt;"test string" 123.449997&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc6ch3h53uquspcc2n8uq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc6ch3h53uquspcc2n8uq.png" alt="Incoming OSC Messages" width="800" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Passing Arguments to Custom Actions
&lt;/h2&gt;

&lt;p&gt;The above method works well for many use cases, but it has one downside: it's impossible to pass any arguments when calling actions. If the variance of possible argument values is low, one can create multiple actions. For example, to enable or disable a certain option: &lt;code&gt;/action/_ENABLE_OPTION&lt;/code&gt;, &lt;code&gt;/action/_DISABLE_OPTION&lt;/code&gt;. This is fine, but such an approach is not suitable for passing numeric or string arguments to an action.&lt;/p&gt;

&lt;p&gt;Is there a way to pass an argument to an action? Yes, as REAPER supports binding OSC messages to trigger actions. When an action is invoked this way, a single (first) OSC message argument of string or float type can be retrieved from the script.&lt;/p&gt;

&lt;p&gt;To start, make sure binding OSC messages to actions is allowed: In REAPER, navigate to &lt;code&gt;Preferences&lt;/code&gt; (&lt;code&gt;Command&lt;/code&gt; + &lt;code&gt;,&lt;/code&gt;), then the &lt;code&gt;Control/OSC/web&lt;/code&gt; section. Select the existing &lt;code&gt;OSC&lt;/code&gt; control from the list and press &lt;code&gt;Edit&lt;/code&gt;.&lt;br&gt;
Check the &lt;code&gt;Allow binding messages to REAPER actions and FX learn&lt;/code&gt; option and press &lt;code&gt;Ok&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;osc.lua&lt;/code&gt; in &lt;code&gt;~/Library/Application\ Support/REAPER/Scripts/MyCompany/&lt;/code&gt; with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- osc.lua&lt;/span&gt;

&lt;span class="c1"&gt;-- Utility functions to get and parse OSC message and argument from REAPER action context&lt;/span&gt;

&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;osc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nc"&gt;osc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="c1"&gt;-- Extract the message&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"^osc:/([^:[]+)"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c1"&gt;-- Extract float or string value&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;value_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;":([fs])=([^%]]+)"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"f"&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
        &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;tonumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elseif&lt;/span&gt; &lt;span class="n"&gt;value_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"s"&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
        &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nc"&gt;osc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;is_new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reaper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_action_context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;osc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;osc&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next to it, create &lt;code&gt;test_osc_with_arg.lua&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Retrieve the directory of the current script.&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;script_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;debug.getinfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"S"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"@?(.*/)"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;-- Set the package path to include the other scripts in the directory&lt;/span&gt;
&lt;span class="nb"&gt;package.path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;package.path&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="s1"&gt;';'&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;script_path&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="s1"&gt;'?.lua'&lt;/span&gt;
&lt;span class="c1"&gt;-- Require the osc module&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;osc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'osc'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;osc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="n"&gt;reaper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShowMessageBox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"OSC address: "&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="s2"&gt;", argument: "&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s2"&gt;"(nil)"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s2"&gt;"Info"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;reaper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShowMessageBox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Invalid or no OSC message"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the following line to &lt;code&gt;~/Library/Application\ Support/REAPER/reaper-kb.ini&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SCR 4 0 MY_COMPANY_TEST_OSC_WITH_ARG "MyCompany: test OSC trigger with argument" "MyCompany/test_osc_with_arg.lua"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, the newly created action can be mapped to OSC messages. This can be done in two ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;By editing the &lt;code&gt;~/Library/Application\ Support/REAPER/reaper-osc-actions.ini&lt;/code&gt; file; &lt;/li&gt;
&lt;li&gt;Or by creating a shortcut from the &lt;code&gt;Actions&lt;/code&gt; dialog by listening for an incoming OSC message.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I prefer the former method. Add the following line to &lt;code&gt;~/Library/Application\ Support/REAPER/reaper-osc-actions.ini&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"/test_message_with_arg" 0 0 _MY_COMPANY_TEST_OSC_WITH_ARG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This instructs REAPER to invoke the &lt;code&gt;_MY_COMPANY_TEST_OSC_WITH_ARG&lt;/code&gt; action when a &lt;code&gt;"/test_message_with_arg"&lt;/code&gt; OSC message is received.&lt;/p&gt;

&lt;p&gt;Restart REAPER for changes to take effect.&lt;/p&gt;

&lt;p&gt;To test that everything works, from the Terminal run: &lt;code&gt;sendosc localhost 8000 /test_message_with_arg s "test string"&lt;/code&gt;. This should invoke the &lt;code&gt;test_osc_with_arg.lua&lt;/code&gt; script in REAPER and display a message box with the text "OSC address: test_message_with_arg, argument: test string".&lt;/p&gt;

&lt;p&gt;Running &lt;code&gt;sendosc localhost 8000 /test_message_with_arg f 123.456&lt;/code&gt; will display the message "OSC address: test_message_with_arg, argument: 123.456001".&lt;/p&gt;

&lt;p&gt;Running &lt;code&gt;sendosc localhost 8000 /test_message_with_arg&lt;/code&gt; will display the message "OSC address: test_message_with_arg, argument: (nil)".&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;REAPER has the following limitations when it comes to working with OSC messages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scripts can only receive the first argument of the OSC message. As a workaround, a string argument can be used to encapsulate any number of arguments.&lt;/li&gt;
&lt;li&gt;Only string and float argument types are supported. Again, a string can be used to hold any required datatype that can be represented as a string.&lt;/li&gt;
&lt;li&gt;Sending OSC messages from scripts is not supported. As a workaround, &lt;code&gt;sendosc&lt;/code&gt; or a similar utility can be invoked using &lt;code&gt;os.execute()&lt;/code&gt;, or third-party REAPER extensions can be used to accomplish this task.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;REAPER is a powerful DAW offering scripting and extensible external control capabilities, which makes it suitable for use as part of various automation applications, including DAW plugin test automation frameworks. Unfortunately, its minimalistic documentation makes feature discovery problematic and requires additional effort to make things work. Some basic functionality is still missing and requires workarounds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Materials
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://mespotin.uber.space/Ultraschall/Reaper-Filetype-Descriptions.html" rel="noopener noreferrer"&gt;REAPER file type description&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.reaper.fm/sdk/reascript/reascripthelp.html" rel="noopener noreferrer"&gt;REAPER API functions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.reaper.fm/sdk/reascript/reascript.php" rel="noopener noreferrer"&gt;REAPER ReaScript&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.reaper.fm/sdk/osc/osc.php" rel="noopener noreferrer"&gt;REAPER OSC&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reaper</category>
      <category>daw</category>
      <category>osc</category>
      <category>lua</category>
    </item>
  </channel>
</rss>
