<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Cardinal</title>
    <description>The latest articles on Forem by Cardinal (@cardinalby).</description>
    <link>https://forem.com/cardinalby</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%2F764505%2Fda6fb551-8784-40bc-8d76-362870acdec2.png</url>
      <title>Forem: Cardinal</title>
      <link>https://forem.com/cardinalby</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/cardinalby"/>
    <language>en</language>
    <item>
      <title>Storing currency values: data types, caveats, best practices</title>
      <dc:creator>Cardinal</dc:creator>
      <pubDate>Tue, 10 Jan 2023 20:34:44 +0000</pubDate>
      <link>https://forem.com/cardinalby/handling-currency-values-facts-and-best-practices-3l81</link>
      <guid>https://forem.com/cardinalby/handling-currency-values-facts-and-best-practices-3l81</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;Repeatedly facing questions, debates, and mistakes related to storing and representing currency amounts, I decided to collect all facts and advice regarding this topic in one place. This article is not the final source of the truth, but it contains useful information that you should take into consideration when designing software.&lt;/p&gt;

&lt;h2&gt;
  
  
  Domain info: currencies
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Just facts:
&lt;/h3&gt;

&lt;p&gt;There is an &lt;a href="https://en.wikipedia.org/wiki/ISO_4217"&gt;&lt;code&gt;ISO 4217&lt;/code&gt;&lt;/a&gt; standard that describes currency codes and their minor units. Data are also available in XML and CSV representations (following the links from the &lt;a href="https://www.six-group.com/en/products-services/financial-information/data-standards.html"&gt;page&lt;/a&gt;).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A currency has a 3-letter code, a numeric code, and a name. A currency may have multiple listed locations.&lt;/li&gt;
&lt;li&gt;Some currencies have exchange rates that are pegged (fixed) to another currency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minor unit&lt;/strong&gt; is the smallest unit of a currency, e.g. 1 dollar equals 100 cents (with 2 decimals).&lt;/li&gt;
&lt;li&gt;Most currencies have 2 decimals; some have none, and some have 3 decimals.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://en.wikipedia.org/wiki/Mauritanian_ouguiya"&gt;Mauritania&lt;/a&gt; and 
&lt;a href="https://en.wikipedia.org/wiki/Malagasy_ariary"&gt;Madagascar&lt;/a&gt; does &lt;strong&gt;not&lt;/strong&gt; use a &lt;strong&gt;decimal&lt;/strong&gt; division of units, 
setting 1 &lt;em&gt;ouguiya&lt;/em&gt; = 5 &lt;em&gt;khoums&lt;/em&gt;, &lt;em&gt;ariary&lt;/em&gt; = 5 &lt;em&gt;iraimbilanja&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Cryptocurrencies can have up to 18 decimals (&lt;a href="https://beaconcha.in/tools/unitConverter"&gt;&lt;em&gt;ETH&lt;/em&gt;&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;The number of decimals &lt;a href="https://en.wikipedia.org/wiki/Ugandan_shilling"&gt;can&lt;/a&gt; &lt;strong&gt;change&lt;/strong&gt; over time due to inflation.&lt;/li&gt;
&lt;li&gt;The same can happen because of &lt;a href="https://en.wikipedia.org/wiki/Redenomination"&gt;redenomination&lt;/a&gt;, but a new currency code should be introduced.&lt;/li&gt;
&lt;li&gt;For some currencies, there are no &lt;a href="https://en.wikipedia.org/wiki/Cash_rounding"&gt;physical denominations&lt;/a&gt; for the minor unit.&lt;/li&gt;
&lt;li&gt;Storing prices of small units of goods (probably as a result of conversion from another currency) can require using &lt;strong&gt;more decimals&lt;/strong&gt; than are defined for a currency.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Storage requirements
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Obvious one: store currency amounts &lt;strong&gt;along with&lt;/strong&gt; a link to the currency &lt;strong&gt;specification&lt;/strong&gt;
(foreign key in databases, special class in programming languages) to interpret and operate with it correctly.&lt;/li&gt;
&lt;li&gt;Storing a &lt;strong&gt;specification&lt;/strong&gt; for a currency you should include:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;Minimum accountable unit&lt;/em&gt;&lt;/strong&gt; instead of or in addition to &lt;strong&gt;precision&lt;/strong&gt; (see &lt;em&gt;fact 5&lt;/em&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;Lowest physical denomination&lt;/em&gt;&lt;/strong&gt; of the currency if you deal with cash operations (see &lt;em&gt;fact 9&lt;/em&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Ensure &lt;strong&gt;precision&lt;/strong&gt; for currency amounts equals the max precision of all supported currencies.&lt;/li&gt;
&lt;li&gt;Consider adding &lt;strong&gt;additional precision&lt;/strong&gt; for operational needs: accumulators and intermediate calculations or for storing
prices of small units of goods. For example, you may want to accumulate a &lt;em&gt;10 %&lt;/em&gt; fee from 1 cent operations,
sum them up until they reach the &lt;em&gt;minimum accountable unit&lt;/em&gt; (cent) and withdraw from a client.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Data types
&lt;/h2&gt;

&lt;p&gt;There are different data types that can technically store money values. Let’s see how the listed requirements can be fulfilled by utilizing different data types.&lt;/p&gt;

&lt;h2&gt;
  
  
  1️⃣ Integer number of minor units
&lt;/h2&gt;

&lt;p&gt;One of the popular (&lt;a href="https://stripe.com/docs/currencies#zero-decimal"&gt;Stripe&lt;/a&gt; approaches) is storing an integer number of minor units. Simply put, you store &lt;strong&gt;&lt;em&gt;5 $&lt;/em&gt;&lt;/strong&gt; as &lt;strong&gt;&lt;em&gt;500 cents&lt;/em&gt;&lt;/strong&gt;. This way you can do accurate calculations and comparisons internally and then display the result formatting the number in a proper way as an amount of dollars.&lt;/p&gt;

&lt;p&gt;Taking into consideration the requirement about &lt;strong&gt;additional precision&lt;/strong&gt; (let's say 3 extra decimals) you will represent &lt;strong&gt;&lt;em&gt;5 $&lt;/em&gt;&lt;/strong&gt; as &lt;strong&gt;&lt;em&gt;500 000&lt;/em&gt;&lt;/strong&gt; of &lt;strong&gt;"micro units"&lt;/strong&gt;, where &lt;strong&gt;&lt;em&gt;1&lt;/em&gt;&lt;/strong&gt; &lt;em&gt;micro unit&lt;/em&gt; equals &lt;strong&gt;&lt;em&gt;1 000 cents&lt;/em&gt;&lt;/strong&gt;. &lt;/p&gt;

&lt;h3&gt;
  
  
  Issues and limitations:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;It's preferable to consider the &lt;strong&gt;precision beforehand&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;It's &lt;strong&gt;complicates&lt;/strong&gt; the business logic of the application and introduces &lt;strong&gt;error-prone&lt;/strong&gt; value &lt;strong&gt;conversions&lt;/strong&gt; between units, micro-units and normal currency amounts that are presented to a user or external systems.&lt;/li&gt;
&lt;li&gt;Due to the &lt;em&gt;fact 7&lt;/em&gt; (&lt;em&gt;minor unit&lt;/em&gt; of a currency can change) or because of the need to add &lt;em&gt;additional precision&lt;/em&gt; you may need to &lt;strong&gt;rescale&lt;/strong&gt; all values in the future.&lt;/li&gt;
&lt;li&gt;External systems you interact with can &lt;strong&gt;misinterpret&lt;/strong&gt; the &lt;strong&gt;magnitude&lt;/strong&gt; of an integer-represented
value &lt;strong&gt;after rescaling&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;3rd-party services/customers which are not aware of the rescaling.&lt;/li&gt;
&lt;li&gt;Your own services that can't be immediately updated with the new definitions of the currencies (limitation of the deployment process, caches).&lt;/li&gt;
&lt;li&gt;A message queue, or an event streaming platform where you can't modify messages during the rescaling process.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;This problem can be solved by &lt;strong&gt;explicitly passing&lt;/strong&gt; the &lt;strong&gt;scale&lt;/strong&gt; of a number everywhere along with the number.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Suggested type: BigInt
&lt;/h3&gt;

&lt;p&gt;It's a not-native data type that handles integers of arbitrary (or big enough) size and provides math operations on it. It's &lt;strong&gt;the most suitable&lt;/strong&gt; way of storing &lt;em&gt;minor units&lt;/em&gt; or &lt;em&gt;micro units&lt;/em&gt;. &lt;strong&gt;Don't confuse&lt;/strong&gt; it with SQL bigint that in fact is &lt;em&gt;Int64&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;Most of the &lt;a href="https://en.wikipedia.org/wiki/List_of_arbitrary-precision_arithmetic_software"&gt;languages&lt;/a&gt; (&lt;br&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt"&gt;JavaScript&lt;/a&gt;,&lt;br&gt;
&lt;a href="https://www.php.net/manual/en/book.bc.php"&gt;PHP&lt;/a&gt;,&lt;br&gt;
&lt;a href="https://pkg.go.dev/math/big"&gt;Go&lt;/a&gt;,&lt;br&gt;
&lt;a href="https://rushter.com/blog/python-integer-implementation/"&gt;Python&lt;/a&gt;,&lt;br&gt;
&lt;a href="https://docs.oracle.com/javase/7/docs/api/java/math/BigInteger.html"&gt;Java&lt;/a&gt;,&lt;br&gt;
&lt;a href="https://learn.microsoft.com/en-us/dotnet/api/system.numerics.biginteger?view=net-7.0"&gt;C#&lt;/a&gt;,&lt;br&gt;
&lt;a href="https://github.com/faheel/BigInt"&gt;C++&lt;/a&gt;&lt;br&gt;
) support it natively or with 3rd-party libraries.&lt;br&gt;
To store these values in &lt;strong&gt;databases&lt;/strong&gt; you should use &lt;strong&gt;Decimal&lt;/strong&gt; column type with &lt;code&gt;precision = 0&lt;/code&gt; (&lt;br&gt;
&lt;a href="https://www.sqlservertutorial.net/sql-server-basics/sql-server-decimal/"&gt;SQL databases&lt;/a&gt;,&lt;br&gt;
&lt;a href="https://www.mongodb.com/developer/products/mongodb/bson-data-types-decimal128/"&gt;MongoDB&lt;/a&gt;&lt;br&gt;
).&lt;/p&gt;

&lt;p&gt;For correct &lt;strong&gt;(de)serialization&lt;/strong&gt; you will probably need to use &lt;strong&gt;strings&lt;/strong&gt; for compatibility between different implementations. &lt;/p&gt;

&lt;p&gt;Usually, &lt;strong&gt;more memory&lt;/strong&gt; is required by &lt;em&gt;BigInt&lt;/em&gt; type, compared to native integers, and computations take &lt;strong&gt;longer&lt;/strong&gt; because CPUs don't have hardware support for this data type.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Actually, it can depend on the &lt;strong&gt;binary representation&lt;/strong&gt; of a value in a concrete implementation. &lt;br&gt;
Normally, standard libraries provide optimal&lt;br&gt;
(&lt;code&gt;2^32&lt;/code&gt;-base, or &lt;code&gt;2^64&lt;/code&gt;-base) representation and have only constant overhead, while 3rd-party string-based&lt;br&gt;
(&lt;a href="https://github.com/faheel/BigInt"&gt;C++&lt;/a&gt;, &lt;a href="https://www.php.net/manual/en/book.bc.php"&gt;PHP&lt;/a&gt;) types have linear&lt;br&gt;
overhead (due to &lt;code&gt;10&lt;/code&gt;-base representation). In most databases the binary representation of &lt;em&gt;decimal&lt;/em&gt; type is not&lt;br&gt;
optimal (&lt;br&gt;
&lt;a href="https://www.oninit.com/manual/informix/100/dapip/dapip83.htm#sii-03-15209"&gt;100-base&lt;/a&gt; or&lt;br&gt;
&lt;a href="https://www.postgresql.org/message-id/16572.1091489720@sss.pgh.pa.us"&gt;10000-base&lt;/a&gt;).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  With some concerns: Int64
&lt;/h3&gt;

&lt;p&gt;Some choose signed or unsigned &lt;strong&gt;&lt;em&gt;Int64&lt;/em&gt;&lt;/strong&gt; (also referenced as &lt;em&gt;BigInt&lt;/em&gt; in SQL, which can lead to confusion) for storing their cents or smaller subunits of currency. Even though it may seem to be sufficient for your use-case, I want to highlight the following &lt;strong&gt;issues&lt;/strong&gt; with it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It may not be sufficient for storing minimal units of &lt;strong&gt;cryptocurrencies&lt;/strong&gt; (see &lt;em&gt;fact 6&lt;/em&gt;) or values with &lt;strong&gt;extended precision&lt;/strong&gt; (see &lt;em&gt;requirement 5&lt;/em&gt;).&lt;/li&gt;
&lt;li&gt;Some languages require casting to &lt;em&gt;Float64&lt;/em&gt; for math operations (&lt;a href="https://pkg.go.dev/math"&gt;Go&lt;/a&gt;). The problem is &lt;em&gt;Float64&lt;/em&gt; has only 52 bits of mantissa; it's not enough to fit arbitrary &lt;em&gt;Int64&lt;/em&gt; value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not all&lt;/strong&gt; programming &lt;strong&gt;languages support&lt;/strong&gt; &lt;em&gt;Int64&lt;/em&gt; values:

&lt;ul&gt;
&lt;li&gt;PHP running on x32 architectures cannot handle &lt;em&gt;Int64&lt;/em&gt; values.&lt;/li&gt;
&lt;li&gt;Some languages (Java, PHP) do not support any unsigned integers.&lt;/li&gt;
&lt;li&gt;JavaScript uses signed &lt;em&gt;Float64&lt;/em&gt; as an internal representation for the &lt;em&gt;number&lt;/em&gt; data type. This means that even if one can serialize &lt;em&gt;Int64&lt;/em&gt; numbers to JSON in their backend application, by default a JavaScript application will overflow its number type trying to deserialize JSON containing this value.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problem with support of &lt;em&gt;Int64&lt;/em&gt; in external systems can be &lt;strong&gt;mitigated&lt;/strong&gt; by serializing &lt;em&gt;Int64&lt;/em&gt; &lt;strong&gt;to a string&lt;/strong&gt; and using &lt;em&gt;BigInt&lt;/em&gt; types to handle these values, but it reduces the benefits from using hardware-supported &lt;em&gt;Int64&lt;/em&gt; values.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h4&gt;
  
  
  Int64 in JavaScript
&lt;/h4&gt;

&lt;p&gt;The problem with JavaScript numeric type is relevant if you use &lt;code&gt;JSON.parse()&lt;/code&gt; and &lt;code&gt;JSON.stringify()&lt;/code&gt; without&lt;br&gt;
additional arguments - as many "HTTP request" libraries do. If you have control over these calls you can pass custom&lt;br&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify"&gt;&lt;em&gt;replacer&lt;/em&gt;&lt;/a&gt;&lt;br&gt;
argument for &lt;code&gt;JSON.stringify()&lt;/code&gt; and custom&lt;br&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse"&gt;&lt;em&gt;reviver&lt;/em&gt;&lt;/a&gt;&lt;br&gt;
argument for&lt;br&gt;
&lt;a href="https://stackoverflow.com/questions/18755125/node-js-is-there-any-proper-way-to-parse-json-with-large-numbers-long-bigin"&gt;&lt;code&gt;JSON.parse()&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
and manually handle &lt;strong&gt;Int64&lt;/strong&gt; values using a data type different from default number.&lt;/p&gt;

&lt;p&gt;What could this "different" type be?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Built-in
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt64Array"&gt;BigInt64Array&lt;/a&gt; type &lt;/li&gt;
&lt;li&gt;Built-in
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt"&gt;BigInt&lt;/a&gt; type. Note, that it
has limited math operations support.&lt;/li&gt;
&lt;li&gt;3rd-party &lt;a href="https://github.com/MikeMcl/bignumber.js/"&gt;libraries&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  2️⃣ Decimal
&lt;/h2&gt;

&lt;p&gt;This approach implies using special &lt;em&gt;Decimal&lt;/em&gt; numeric type that allows to store &lt;strong&gt;fractional decimal numbers&lt;/strong&gt; accurately with the specified precision (maximum precision differs in different databases).&lt;/p&gt;

&lt;p&gt;Most &lt;a href="https://en.wikipedia.org/wiki/List_of_arbitrary-precision_arithmetic_software"&gt;languages&lt;/a&gt; (&lt;br&gt;
&lt;a href="https://github.com/MikeMcl/bignumber.js/"&gt;JavaScript&lt;/a&gt;,&lt;br&gt;
&lt;a href="https://www.php.net/manual/en/book.gmp.php"&gt;PHP&lt;/a&gt;,&lt;br&gt;
&lt;a href="https://github.com/shopspring/decimal"&gt;Go&lt;/a&gt;,&lt;br&gt;
&lt;a href="https://docs.python.org/3/library/decimal.html"&gt;Python&lt;/a&gt;,&lt;br&gt;
&lt;a href="https://docs.oracle.com/javase/8/docs/api/java/math/BigDecimal.html"&gt;Java&lt;/a&gt;,&lt;br&gt;
&lt;a href="https://learn.microsoft.com/en-us/dotnet/api/system.decimal?view=net-7.0"&gt;C#&lt;/a&gt;,&lt;br&gt;
&lt;a href="https://stackoverflow.com/questions/14096026/c-decimal-data-types"&gt;C++&lt;/a&gt;&lt;br&gt;
) have built-in support or 3rd-party libraries for handling this data type.&lt;br&gt;
SQL databases offer own standard &lt;a href="https://www.sqlservertutorial.net/sql-server-basics/sql-server-decimal/"&gt;Decimal&lt;/a&gt; &lt;br&gt;
type.&lt;/p&gt;

&lt;p&gt;Note that there are &lt;strong&gt;2 different types of decimal&lt;/strong&gt; type implementations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;&lt;strong&gt;Decimal128&lt;/strong&gt;&lt;/em&gt; with limited number of significant digits (&lt;a href="https://www.mongodb.com/developer/products/mongodb/bson-data-types-decimal128/"&gt;MongoDB&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/system.decimal?view=net-7.0"&gt;C#&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;So-called &lt;em&gt;&lt;strong&gt;BigDecimal&lt;/strong&gt;&lt;/em&gt; type of arbitrary (or big enough) size (SQL Decimal, BigDecimal in Java) with different internal representations.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;As in the case of BigInt, the &lt;strong&gt;binary representation&lt;/strong&gt; can differ:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2 decimal digits in each byte (base 100, like it is done in databases).&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;BigInt&lt;/em&gt; with exponent in a manner similar to base 2 floating point values.&lt;/li&gt;
&lt;li&gt;Array or string of single decimal digits.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;strong&gt;performance concerns&lt;/strong&gt; described above for &lt;em&gt;BigInt&lt;/em&gt; are relevant for &lt;em&gt;Decimal&lt;/em&gt; types as well.&lt;/p&gt;

&lt;p&gt;The main &lt;strong&gt;advantages&lt;/strong&gt; of using &lt;em&gt;Decimals&lt;/em&gt; comparing to &lt;em&gt;BigInts&lt;/em&gt; are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No major overhead&lt;/strong&gt; if values are already stored as &lt;em&gt;Decimals&lt;/em&gt; in the database (you'll do it anyway even with &lt;em&gt;BigInts&lt;/em&gt;). Also, formatting &lt;em&gt;BigInt&lt;/em&gt; values requires computations similar to those within &lt;em&gt;Decimal&lt;/em&gt; type.&lt;/li&gt;
&lt;li&gt;More &lt;strong&gt;intuitive&lt;/strong&gt; representation of the values &lt;strong&gt;simplifies&lt;/strong&gt; the business logic and formatting numbers to a human-readable format.&lt;/li&gt;
&lt;li&gt;Changing minor units can be done by altering the precision of the &lt;em&gt;decimal&lt;/em&gt; column in the database; you &lt;strong&gt;don't have to rescale&lt;/strong&gt; the values.&lt;/li&gt;
&lt;li&gt;Value &lt;strong&gt;serialized&lt;/strong&gt; to string naturally &lt;strong&gt;includes&lt;/strong&gt; the information about &lt;strong&gt;precision&lt;/strong&gt;. If you change the precision, it &lt;strong&gt;cannot be misinterpreted&lt;/strong&gt; by external systems as in the case of &lt;em&gt;BigInts&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As in the case of &lt;em&gt;BigInt&lt;/em&gt; values, all &lt;strong&gt;serialization&lt;/strong&gt; goes through &lt;strong&gt;decimal strings&lt;/strong&gt; for compatibility between different libraries. Of course, serializing in the form of mantissa + exponent can be more efficient for performance-sensitive applications.&lt;/p&gt;

&lt;p&gt;However, you still need to keep track of minimal accountable units. Limiting the precision of &lt;em&gt;Decimal&lt;/em&gt; in the database requires remembering about &lt;strong&gt;precision reserve&lt;/strong&gt; for intermediate operations and accumulators.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bad choice
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Float, Double
&lt;/h3&gt;

&lt;p&gt;The first rule here is "&lt;a href="https://stackoverflow.com/questions/3730019/why-not-use-double-or-float-to-represent-currency"&gt;&lt;strong&gt;never&lt;/strong&gt;&lt;/a&gt; use floating-point data types for storing money amounts". Humans expect money calculations to be made in base 10, but &lt;a href="https://en.wikipedia.org/wiki/IEEE_754"&gt;floating-point arithmetic&lt;/a&gt; uses base 2 representation that can lead to results that are not expected in financial sense. The common example to illustrate the problem is &lt;code&gt;0.1 + 0.2 != 0.3&lt;/code&gt;. This rule is relevant for both programming languages and databases.&lt;/p&gt;

&lt;p&gt;Even though decimal representation also can't store all amounts precisely (e.g. &lt;code&gt;1/3&lt;/code&gt; - read bonus part at the end),&lt;br&gt;
it's  expected by people, financial institutions and regulations.&lt;/p&gt;

&lt;h3&gt;
  
  
  SQL Server MONEY
&lt;/h3&gt;

&lt;p&gt;This proprietary type of Microsoft SQL Server stores amounts as &lt;em&gt;Int64&lt;/em&gt; internally and&lt;br&gt;
&lt;a href="https://www.red-gate.com/hub/product-learning/sql-prompt/avoid-use-money-smallmoney-datatypes"&gt;is not recommended&lt;/a&gt;&lt;br&gt;
to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: rational numbers
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Rational_number"&gt;Rational number&lt;/a&gt; is a number that can be expressed as the &lt;strong&gt;fraction&lt;/strong&gt; &lt;code&gt;A/B&lt;/code&gt;. In most applications, their use is &lt;strong&gt;not necessary&lt;/strong&gt;, however, for some use cases, they are the only option for obtaining the desired precision.&lt;/p&gt;

&lt;p&gt;I can imagine a game or an expenses splitting application in which you need to take &lt;code&gt;1/3&lt;/code&gt; of &lt;code&gt;10 $&lt;/code&gt;, save it (to a database, message queue, or simply to memory) and later multiply it by &lt;code&gt;30&lt;/code&gt;. Using &lt;strong&gt;decimals&lt;/strong&gt; (both &lt;em&gt;Decimal&lt;/em&gt; type and &lt;em&gt;BigInt&lt;/em&gt; type with the number of cents) &lt;strong&gt;can't provide exact precision&lt;/strong&gt; - the result will never be exactly &lt;code&gt;100&lt;/code&gt;. But using rational numbers, you can save the intermediate value as &lt;code&gt;numerator = 10, denominator = 3&lt;/code&gt; and later do simple arithmetic to get the value &lt;code&gt;100/1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Operations on &lt;strong&gt;non-decimal units&lt;/strong&gt;, such as time and distance units, historical &lt;a href="https://en.wikipedia.org/wiki/Non-decimal_currency"&gt;non-decimal currencies&lt;/a&gt;, calculating probabilities, etc., may be candidates for introducing rational numbers when absolute precision is required.&lt;/p&gt;

&lt;p&gt;There is standard &lt;a href="https://docs.python.org/3/library/fractions.html"&gt;Fractions&lt;/a&gt; library in Python, and 3rd-party libraries in other languages:&lt;br&gt;
&lt;a href="https://github.com/infusion/Fraction.js/"&gt;JavaScript&lt;/a&gt;, &lt;br&gt;
&lt;a href="https://github.com/brick/math"&gt;PHP&lt;/a&gt;, &lt;br&gt;
&lt;a href="https://github.com/alex-ant/gomath"&gt;Go&lt;/a&gt;, &lt;br&gt;
&lt;a href="https://github.com/tompazourek/Rationals"&gt;C#&lt;/a&gt;, &lt;br&gt;
&lt;a href="https://www.boost.org/doc/libs/1_81_0/libs/rational/rational.html"&gt;C++&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Unfortunately, &lt;strong&gt;the only database&lt;/strong&gt; I know to be supporting rational numbers is &lt;strong&gt;PostgreSQL&lt;/strong&gt; with &lt;a href="https://github.com/begriffs/pg_rational"&gt;pg_rational&lt;/a&gt; &lt;strong&gt;extension&lt;/strong&gt;. Storing rational numbers in separate "numerator" and "denominator" columns limits the possibilities of math calculations directly in the database. &lt;/p&gt;

&lt;h2&gt;
  
  
  👏 Thank you for reading
&lt;/h2&gt;

&lt;p&gt;Any comments, criticism, and sharing of your own experience would be appreciated!&lt;/p&gt;

</description>
      <category>database</category>
      <category>money</category>
      <category>computerscience</category>
      <category>programming</category>
    </item>
    <item>
      <title>The main workflow</title>
      <dc:creator>Cardinal</dc:creator>
      <pubDate>Tue, 15 Mar 2022 10:31:08 +0000</pubDate>
      <link>https://forem.com/cardinalby/the-main-workflow-2iob</link>
      <guid>https://forem.com/cardinalby/the-main-workflow-2iob</guid>
      <description>&lt;p&gt;We have prepared all the needed composite actions and workflows, and we are finally ready to create the main workflow that triggers the entire pipeline.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--NE-_PaJq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cardinalby.github.io/blog/images/posts/github-actions/webext/the-main-workflow.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NE-_PaJq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cardinalby.github.io/blog/images/posts/github-actions/webext/the-main-workflow.png" alt="The main workflow" width="880" height="591"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;.github/workflows/publish-release-on-tag.yml&lt;/em&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Release and publish on tag&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tags&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;*.*.*'&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build-release-publish&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;github.ref_type == 'tag'&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cardinalby/export-env-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;envFile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;./.github/workflows/constants.env'&lt;/span&gt;
          &lt;span class="na"&gt;expand&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Look for an existing release&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;getRelease&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cardinalby/git-get-release-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;continue-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.ref_name }}&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.token }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build, test and pack to zip&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildPack&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;steps.getRelease.outcome != 'success'&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./.github/workflows/actions/build-test-pack&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create Release&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;createRelease&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;steps.getRelease.outcome != 'success'&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ncipollo/release-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;draft&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload zip asset to the release&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;steps.getRelease.outcome != 'success'&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-release-asset@v1&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;upload_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.createRelease.outputs.upload_url }}&lt;/span&gt;
          &lt;span class="na"&gt;asset_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.ZIP_FILE_PATH }}&lt;/span&gt;
          &lt;span class="na"&gt;asset_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.ZIP_FILE_NAME }}&lt;/span&gt;
          &lt;span class="na"&gt;asset_content_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/zip&lt;/span&gt;

      &lt;span class="c1"&gt;# Should trigger build-assets-on-release.yml&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish release&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;steps.getRelease.outcome != 'success'&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eregon/publish-release@v1&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.WORKFLOWS_TOKEN }}&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;release_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.createRelease.outputs.id }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish on Chrome Webstore&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;benc-uk/workflow-dispatch@v1&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;!contains(github.event.head_commit.message,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'[skip&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;chrome]')"&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;workflow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;publish-on-chrome-web-store&lt;/span&gt;
          &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.WORKFLOWS_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;wait-for-completion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish on Firefox Add-ons&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;benc-uk/workflow-dispatch@v1&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;!contains(github.event.head_commit.message,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'[skip&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;firefox]')"&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;workflow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;publish-on-firefox-add-ons&lt;/span&gt;
          &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.WORKFLOWS_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;wait-for-completion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;The workflow can be triggered by pushing &lt;em&gt;&lt;em&gt;.&lt;/em&gt;.*&lt;/em&gt; tag or by &lt;em&gt;workflow_dispatch&lt;/em&gt; event.&lt;/li&gt;
&lt;li&gt;To perform the work we need a tag to create a release for, that's why we add &lt;code&gt;if: github.ref_type == 'tag'&lt;/code&gt; condition for the job to prevent running on branches.&lt;/li&gt;
&lt;li&gt;We use &lt;a href="https://github.com/marketplace/actions/git-get-release-action"&gt;git-get-release-action&lt;/a&gt; to find a release for the tag. &lt;code&gt;continue-on-error: true&lt;/code&gt; prevents the job from failing if release not found.&lt;/li&gt;
&lt;li&gt;If a release not found, we:

&lt;ul&gt;
&lt;li&gt;Call &lt;em&gt;&lt;strong&gt;build-test-pack&lt;/strong&gt;&lt;/em&gt; composite action to build &lt;strong&gt;zip&lt;/strong&gt; file.&lt;/li&gt;
&lt;li&gt;Call &lt;a href="https://github.com/marketplace/actions/create-release"&gt;release-action&lt;/a&gt; to create a draft release. This doesn't trigger &lt;code&gt;on: release&lt;/code&gt; event.&lt;/li&gt;
&lt;li&gt;Call &lt;a href="https://github.com/actions/upload-release-asset"&gt;upload-release-asset&lt;/a&gt; to upload &lt;strong&gt;zip&lt;/strong&gt; asset to the release (to be used by &lt;em&gt;&lt;strong&gt;publish-on-chrome-web-store&lt;/strong&gt;&lt;/em&gt; and &lt;em&gt;&lt;strong&gt;publish-on-firefox-add-ons&lt;/strong&gt;&lt;/em&gt; workflows later).&lt;/li&gt;
&lt;li&gt;Call &lt;a href="https://github.com/marketplace/actions/publish-release"&gt;eregon/publish-release&lt;/a&gt; to publish the draft release. This triggers &lt;code&gt;on: release&lt;/code&gt; event and &lt;a href="https://dev.to/cardinalby/creating-github-release-2mn"&gt;build-assets-on-release&lt;/a&gt; workflow.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;We use &lt;a href="https://github.com/marketplace/actions/workflow-dispatch"&gt;benc-uk/workflow-dispatch&lt;/a&gt; action to asynchronously dispatch:

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/cardinalby/publish-on-chrome-web-store-59oi/6-publish-on-chrome-web-store.md"&gt;publish-on-chrome-web-store&lt;/a&gt; workflow (if the commit message doesn't contain &lt;code&gt;[skip chrome]&lt;/code&gt; text).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/cardinalby/bonus-retry-of-chrome-web-store-releasing-steps-48l/5-publish-on-firefox-addons.md"&gt;publish-on-firefox-add-ons&lt;/a&gt; workflow (if the commit message doesn't contain &lt;code&gt;[skip firefox]&lt;/code&gt; text).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  👏 Thank you for reading
&lt;/h2&gt;

&lt;p&gt;Finally, we are done! You can try out your first deployment by pushing a new &lt;em&gt;&lt;em&gt;.&lt;/em&gt;.*&lt;/em&gt; tag to the repo (don't forget to check the extension version in &lt;em&gt;manifest.json&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;Any comments, critics and sharing your own experience would be appreciated.&lt;br&gt;
You can find a real example of the described CI/CD approach in my &lt;a href="https://github.com/cardinalby/memrise-audio-uploader"&gt;Memrise Audio Uploader&lt;/a&gt; extension.&lt;/p&gt;

</description>
      <category>github</category>
      <category>webextension</category>
      <category>actionshackathon</category>
      <category>devops</category>
    </item>
    <item>
      <title>Don't let Google refresh token expire</title>
      <dc:creator>Cardinal</dc:creator>
      <pubDate>Tue, 15 Mar 2022 10:26:17 +0000</pubDate>
      <link>https://forem.com/cardinalby/dont-let-google-refresh-token-expire-pie</link>
      <guid>https://forem.com/cardinalby/dont-let-google-refresh-token-expire-pie</guid>
      <description>&lt;p&gt;In this part we will finish setting up workflows related to publishing the extension on Google Web Store and create the workflow that isn't shown on &lt;a href="https://dev.to/cardinalby/releasing-webextension-using-github-actions-part-2-of-5-27d3"&gt;the workflow diagram&lt;/a&gt; because it stays aside and isn't included to the main pipeline.&lt;/p&gt;

&lt;p&gt;Dealing with Google API credentials we should be aware of the fact that, according to &lt;a href="https://developers.google.com/identity/protocols/oauth2#expiration"&gt;Google's guide&lt;/a&gt;, refresh token (which we use in the workflow from the previous part) might stop working if it has not been used for six months. And I can assure you, it happens 😞.&lt;/p&gt;

&lt;p&gt;To solve the issue we can use a great option GitHub offers called &lt;a href="https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule"&gt;"schedule event"&lt;/a&gt; for workflows.  &lt;/p&gt;

&lt;p&gt;&lt;em&gt;.github/workflows/touch-google-refresh-token.yml&lt;/em&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Touch google token&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2&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;*'&lt;/span&gt; &lt;span class="c1"&gt;# At 03:00 on day-of-month 2&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;fetchToken&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cardinalby/google-api-fetch-token-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;clientId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.G_CLIENT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;clientSecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.G_CLIENT_SECRET }}&lt;/span&gt;
          &lt;span class="na"&gt;refreshToken&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.G_REFRESH_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here I assume that you have already obtained and have added values required for Google API access to secrets (as it described at the previous post).&lt;/p&gt;

&lt;p&gt;Once a month GitHub will run our workflow and perform the single step: fetching access token using the credentials that we have already added to &lt;em&gt;secrets&lt;/em&gt; (see the previous post). It will prevent refresh token from invalidation.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;One note here:&lt;/strong&gt;&lt;/em&gt; GitHub will suspend the scheduled trigger for GitHub Action workflows if there is no commit in the repository for the past 60 days. The cron based triggers won't run unless a new commit is made. Probably, you can use the same trick (committing to the repo once a month by schedule) to circumvent this limitation.&lt;/p&gt;

</description>
      <category>github</category>
      <category>actionshackathon</category>
      <category>webextension</category>
      <category>devops</category>
    </item>
    <item>
      <title>Publish on Chrome Web Store</title>
      <dc:creator>Cardinal</dc:creator>
      <pubDate>Tue, 15 Mar 2022 10:20:01 +0000</pubDate>
      <link>https://forem.com/cardinalby/publish-on-chrome-web-store-59oi</link>
      <guid>https://forem.com/cardinalby/publish-on-chrome-web-store-59oi</guid>
      <description>&lt;p&gt;In this part we are going to create the workflow that will be responsible for publishing the extension&lt;br&gt;
on Chrome Web Store. This part is going to be a bit tricky comparing to the others.&lt;/p&gt;
&lt;h2&gt;
  
  
  🧱 Prepare
&lt;/h2&gt;

&lt;p&gt;❶ To set up Google Publish API access you need to obtain &lt;code&gt;clientId&lt;/code&gt;, &lt;code&gt;clientSecret&lt;/code&gt; and &lt;code&gt;refreshToken&lt;/code&gt; from Google. These articles can help you to do that:&lt;br&gt;
    * &lt;a href="https://developer.chrome.com/webstore/using_webstore_api"&gt;Using the Chrome Web Store Publish API&lt;/a&gt;&lt;br&gt;
    * &lt;a href="https://github.com/DrewML/chrome-webstore-upload/blob/master/How%20to%20generate%20Google%20API%20keys.md"&gt;How to generate Google API keys&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🔒 Add to the repository &lt;strong&gt;&lt;em&gt;secrets&lt;/em&gt;&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;G_CLIENT_ID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;G_CLIENT_SECRET&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;G_REFRESH_TOKEN&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;❷ You should find out the ID of your extension on Chrome Web Store. Normally, it is shown on your Developer Dashboard. It's not private information, but I prefer to store it in &lt;em&gt;secrets&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;🔒 Add to the repository &lt;strong&gt;&lt;em&gt;secrets&lt;/em&gt;&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;G_EXTENSION_ID&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;❸ Also, we have to create a new &lt;a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token"&gt;personal access token&lt;/a&gt; with write access to the repo. The automatically provided token e.g. &lt;code&gt;secrets.GITHUB_TOKEN&lt;/code&gt; can not be used, GitHub prevents this token from being able to fire the &lt;em&gt;workflow_dispatch&lt;/em&gt; and &lt;em&gt;repository_dispatch&lt;/em&gt; event.&lt;/p&gt;

&lt;p&gt;🔒 Add to the repository &lt;strong&gt;&lt;em&gt;secrets&lt;/em&gt;&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;WORKFLOWS_TOKEN&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;❹ The last preparation step is &lt;a href="https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#creating-an-environment"&gt;creating an environment&lt;/a&gt; in the repository settings. It's main purpose is providing a &lt;a href="https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#wait-timer"&gt;wait timer&lt;/a&gt; to delay a workflow run (details will be explained below). Let's call the environment &lt;code&gt;12hoursDelay&lt;/code&gt; and set its wait timer equal &lt;code&gt;720&lt;/code&gt; minutes (12 hours).&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;em&gt;publish-on-chrome-web-store&lt;/em&gt; workflow
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;On the one hand&lt;/em&gt;, the workflow will be similar to &lt;em&gt;&lt;strong&gt;publish-on-firefox-add-ons&lt;/strong&gt;&lt;/em&gt; workflow (described in the previous part): it is also triggered by &lt;code&gt;workflow_dispatch&lt;/code&gt; event (emitted manually or by &lt;em&gt;&lt;strong&gt;publish-release-on-tag&lt;/strong&gt;&lt;/em&gt;) and retrieves &lt;strong&gt;zip&lt;/strong&gt; asset in the same way.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;On the other hand&lt;/em&gt;, it has its own complications because of peculiarity of Webstore publishing. Also, we are going to include the additional job for downloading published &lt;strong&gt;crx&lt;/strong&gt; file from Webstore and attaching it to the release (if any).&lt;/p&gt;

&lt;p&gt;The publishing process consists of 2 steps: uploading a new version and publishing the extension. What is the peculiarity I'm speaking about?&lt;/p&gt;

&lt;p&gt;It's a Webstore behavior in the case when you are trying to upload a new version shortly after a previous one was uploaded to the store. In this case API call completes with an error and with the status called &lt;code&gt;IN_REVIEW&lt;/code&gt; (only one uploaded version can be in processing at the time). Unlike other errors, it doesn't mean that something is wrong with our extension and if we &lt;strong&gt;try&lt;/strong&gt; uploading &lt;strong&gt;later&lt;/strong&gt; it will succeed.&lt;/p&gt;

&lt;p&gt;That's exactly what we are going to do, it only remains to find a good technical solution to do that.&lt;br&gt;
If you are interested in learning about existing approaches, please read my &lt;a href="https://cardinalby.github.io/blog/post/github-actions/implementing-deferred-steps.md"&gt;"GitHub Actions: implementing deferred steps"&lt;/a&gt; post. Here we will&lt;br&gt;
use &lt;a href="https://cardinalby.github.io/blog/post/github-actions/implementing-deferred-steps.md#-dispatched-workflow-calling-itself--wait-timer"&gt;"Dispatched workflow calling itself + wait timer"&lt;/a&gt; approach to repeat a new version uploading after 12 hours delay (number was chosen arbitrary).&lt;/p&gt;

&lt;p&gt;Let's observe the workflow diagram and start with creating the workflow file:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--SwEIB5S4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cardinalby.github.io/blog/images/posts/github-actions/webext/publish-on-chrome-webstore.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SwEIB5S4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cardinalby.github.io/blog/images/posts/github-actions/webext/publish-on-chrome-webstore.png" alt="build-assets-on-release workflow" width="880" height="758"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, the main idea is the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We try to upload a new extension version.&lt;/li&gt;
&lt;li&gt;If it succeeded, we proceed to publishing the extension and downloading the published &lt;strong&gt;crx&lt;/strong&gt; file.&lt;/li&gt;
&lt;li&gt;If it didn't succeed due to &lt;em&gt;IN_REVIEW&lt;/em&gt; error, we dispatch the workflow with 12 hours delay to repeat it later. We will limit attempts number by incrementing an attemptNumber and passing it as an input to the workflow.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;.github/workflows/publish-on-chrome-webstore.yml&lt;/em&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;publish-on-chrome-web-store&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;attemptNumber&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Attempt&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;number'&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
        &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1'&lt;/span&gt;
      &lt;span class="na"&gt;maxAttempts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Max&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;attempts'&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
        &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;10'&lt;/span&gt;
      &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;publish-on-webstore&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;job&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;environment'&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
        &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# We will add 2 jobs here:&lt;/span&gt;
  &lt;span class="c1"&gt;# publish-on-webstore:&lt;/span&gt;
  &lt;span class="c1"&gt;#   ...&lt;/span&gt;
  &lt;span class="c1"&gt;# download-published-crx:&lt;/span&gt;
  &lt;span class="c1"&gt;#  ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Defined &lt;em&gt;workflow_dispatch&lt;/em&gt; event has 3 inputs that can be specified at the time of dispatching the workflow. You will see their usage in the following job. The first time the workflow is triggered, inputs will have their default values.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;em&gt;publish-on-webstore&lt;/em&gt; job
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;publish-on-webstore&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.inputs.environment }}&lt;/span&gt;
    &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.webStorePublish.outcome }}&lt;/span&gt;
      &lt;span class="na"&gt;releaseUploadUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.getZipAsset.releaseUploadUrl }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# Validate the inputs and increase the attemptNumber if less than maxAttempts&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Get the next attempt number&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;getNextAttemptNumber&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cardinalby/js-eval-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;attemptNumber&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.inputs.attemptNumber }}&lt;/span&gt;
          &lt;span class="na"&gt;maxAttempts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.inputs.maxAttempts }}&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;expression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;{&lt;/span&gt;
              &lt;span class="s"&gt;const &lt;/span&gt;
                &lt;span class="s"&gt;attempt = parseInt(env.attemptNumber),&lt;/span&gt;
                &lt;span class="s"&gt;max = parseInt(env.maxAttempts);&lt;/span&gt;
              &lt;span class="s"&gt;assert(attempt &amp;amp;&amp;amp; max &amp;amp;&amp;amp; max &amp;gt;= attempt);&lt;/span&gt;
              &lt;span class="s"&gt;return attempt &amp;lt; max ? attempt + 1 : '';&lt;/span&gt;
            &lt;span class="s"&gt;}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cardinalby/export-env-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;envFile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;./.github/workflows/constants.env'&lt;/span&gt;
          &lt;span class="na"&gt;expand&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Obtain packed zip&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;getZipAsset&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./.github/workflows/actions/get-zip-asset&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;githubToken&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Fetch Google API access token&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fetchAccessToken&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cardinalby/google-api-fetch-token-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;clientId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.G_CLIENT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;clientSecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.G_CLIENT_SECRET }}&lt;/span&gt;
          &lt;span class="na"&gt;refreshToken&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.G_REFRESH_TOKEN }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload to Google Web Store&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;webStoreUpload&lt;/span&gt;
        &lt;span class="na"&gt;continue-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cardinalby/webext-buildtools-chrome-webstore-upload-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;zipFilePath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.ZIP_FILE_PATH }}&lt;/span&gt;
          &lt;span class="na"&gt;extensionId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.G_EXTENSION_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;apiAccessToken&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.fetchAccessToken.outputs.accessToken }}&lt;/span&gt;
          &lt;span class="na"&gt;waitForUploadCheckCount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
          &lt;span class="na"&gt;waitForUploadCheckIntervalMs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;180000&lt;/span&gt;    &lt;span class="c1"&gt;# 3 minutes&lt;/span&gt;

      &lt;span class="c1"&gt;# Schedule a next attempt if store refused to accept new version because it&lt;/span&gt;
      &lt;span class="c1"&gt;# still has a previous one in review&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Start the next attempt with the delay&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aurelien-baudet/workflow-dispatch@v2&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;steps.getNextAttemptNumber.outputs.result &amp;amp;&amp;amp; &lt;/span&gt;
          &lt;span class="s"&gt;steps.webStoreUpload.outputs.inReviewError == 'true'&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;workflow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.workflow }}&lt;/span&gt;
          &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.WORKFLOWS_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;wait-for-completion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
          &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;{ &lt;/span&gt;
              &lt;span class="s"&gt;"attemptNumber": "${{ steps.getNextAttemptNumber.outputs.result }}",&lt;/span&gt;
              &lt;span class="s"&gt;"maxAttempts": "${{ github.event.inputs.maxAttempts }}",&lt;/span&gt;
              &lt;span class="s"&gt;"environment": "12hoursDelay"&lt;/span&gt;
            &lt;span class="s"&gt;}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Abort on unrecoverable upload error&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;!steps.webStoreUpload.outputs.newVersion &amp;amp;&amp;amp;&lt;/span&gt;
          &lt;span class="s"&gt;steps.webStoreUpload.outputs.sameVersionAlreadyUploadedError != 'true'&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;exit &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish on Google Web Store&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;webStorePublish&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cardinalby/webext-buildtools-chrome-webstore-publish-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;extensionId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.G_EXTENSION_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;apiAccessToken&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.fetchAccessToken.outputs.accessToken }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;environment: ${{ github.event.inputs.environment }}&lt;/code&gt; sets the environment for the job providing a required delay (via "wait timer" of the environment).&lt;/li&gt;
&lt;li&gt;We use &lt;a href="https://github.com/marketplace/actions/js-eval-action"&gt;js-eval-action&lt;/a&gt; as a generic JS code interpreter to validate the inputs and calculated the incremented &lt;em&gt;attemptNumber&lt;/em&gt;. It is accessible as &lt;em&gt;result&lt;/em&gt; output of the step.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/cardinalby/firefox-add-ons-store-and-xpi-file-4g83#not-a-composite-action"&gt;As usual&lt;/a&gt;, at the beginning of each workflow we check out the repo and export env variables from &lt;em&gt;constants.env&lt;/em&gt; file.&lt;/li&gt;
&lt;li&gt;After calling &lt;strong&gt;&lt;em&gt;get-zip-asset&lt;/em&gt;&lt;/strong&gt; composite action we expect to have &lt;strong&gt;zip&lt;/strong&gt; file with packed and built extension at &lt;code&gt;env.ZIP_FILE_PATH&lt;/code&gt; path.&lt;/li&gt;
&lt;li&gt;We call &lt;a href="https://github.com/marketplace/actions/google-api-fetch-token-action"&gt;google-api-fetch-token-action&lt;/a&gt; to retrieve Google API access token that is needed for "upload" and "publish" steps.&lt;/li&gt;
&lt;li&gt;We use &lt;a href="https://github.com/marketplace/actions/webext-buildtools-chrome-webstore-upload-action"&gt;webext-buildtools-chrome-webstore-upload-action&lt;/a&gt; action to upload a new version to Webstore. &lt;code&gt;continue-on-error: true&lt;/code&gt; flag allows us not to fail immediately, but perform the next "dispatching" step and examine the error after it in a separate step (checking the action's &lt;a href="https://github.com/marketplace/actions/webext-buildtools-chrome-webstore-upload-action#outputs"&gt;outputs&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;We use &lt;a href="https://github.com/marketplace/actions/workflow-dispatch-and-wait"&gt;aurelien-baudet/workflow-dispatch&lt;/a&gt; action to dispatch the workflow in case of &lt;code&gt;IN_REVIEW&lt;/code&gt; error:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;workflow: ${{ github.workflow }}&lt;/code&gt; points to the current workflow file&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;token: ${{ secrets.WORKFLOWS_TOKEN }}&lt;/code&gt; makes use of the personal access token we created at the preparation step&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;inputs&lt;/code&gt; is JSON containing values of inputs for &lt;em&gt;workflow_dispatch&lt;/em&gt; event:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;attemptNumber&lt;/code&gt; is an incremented workflow input value read from the validation step.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;maxAttempts&lt;/code&gt; is the workflow input value passed without changes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;environment&lt;/code&gt; is the name of the environment that will be used to delay the execution.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;"Abort on unrecoverable upload error"&lt;/em&gt; step complements the "upload" step validating errors and fails the job if we can't proceed with publishing because the new version was not uploaded. The case when the same version has been already uploaded (&lt;code&gt;sameVersionAlreadyUploadedError&lt;/code&gt; output indicates that) is the exception - we still can publish it.&lt;/li&gt;
&lt;li&gt;Finally, we call &lt;a href="https://github.com/marketplace/actions/webext-buildtools-chrome-webstore-publish-action"&gt;webext-buildtools-chrome-webstore-publish-action&lt;/a&gt; action to publish the extension.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Finally, we are done. Keep in mind that in the sake of simplicity I omit some details and description of optional inputs of the used actions. There are a lot of things to tune up. Please, read the documentation for corresponding actions to learn more.&lt;/p&gt;

&lt;p&gt;In the next part we are going to solve one small but annoying issue with Google API refresh token expiration.&lt;/p&gt;

</description>
      <category>webextension</category>
      <category>github</category>
      <category>actionshackathon</category>
      <category>chrome</category>
    </item>
    <item>
      <title>System testing of GitHub Actions</title>
      <dc:creator>Cardinal</dc:creator>
      <pubDate>Tue, 08 Feb 2022 21:25:36 +0000</pubDate>
      <link>https://forem.com/cardinalby/system-testing-of-github-actions-1ick</link>
      <guid>https://forem.com/cardinalby/system-testing-of-github-actions-1ick</guid>
      <description>&lt;p&gt;It's the last and the shortest part of the series. Testing the whole action as a &lt;a href="https://en.wikipedia.org/wiki/Black-box_testing"&gt;black box&lt;/a&gt; can be done in 2 ways (as far as I can see).&lt;/p&gt;

&lt;h1&gt;
  
  
  github-action-ts-run-api again
&lt;/h1&gt;

&lt;p&gt;Use the same &lt;a href="https://github.com/cardinalby/github-action-ts-run-api"&gt;tool&lt;/a&gt; as for integration test, but run tests against the whole action.&lt;/p&gt;

&lt;h1&gt;
  
  
  Use &lt;em&gt;Act&lt;/em&gt; tool
&lt;/h1&gt;

&lt;p&gt;This approach implies that you should create special testing workflows that can be naturally run on GitHub Actions runner or can be run locally using &lt;a href="https://github.com/nektos/act"&gt;Act&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you need to debug an action on actual GitHub hosted runner, take a look at &lt;a href="https://github.com/marketplace/actions/debugging-with-tmate"&gt;debugging-with-tmate&lt;/a&gt; action on the Marketplace.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Testing workflows can be located in the action repository and refer to the action using the local &lt;code&gt;./&lt;/code&gt; path. It's especially convenient because each branch in the action repo will be tested against the version of the action stored in this branch.&lt;/p&gt;

&lt;p&gt;Here is a fragment of the testing workflow for &lt;a href="https://github.com/cardinalby/git-get-release-action"&gt;git-get-release-action&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Get 1.1.1 release by releaseId&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;getByReleaseId&lt;/span&gt;
  &lt;span class="c1"&gt;# referring to the action in the current repo&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./&lt;/span&gt;  
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;releaseId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;41301084&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check getByReleaseId step result&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;steps.getByReleaseId.outputs.tag_name != '1.1.1'&lt;/span&gt;
  &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;exit &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another option is creating a dedicated repo for testing workflows. It can also contain commits, releases, issues and other GitHub objects that can be read and modified by the action without bloating your main repository.&lt;/p&gt;

&lt;p&gt;The drawback of this approach is that you have to specify an exact version of the action in &lt;code&gt;uses&lt;/code&gt; key of each step:&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="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Get 1.1.1 release by releaseId&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;getByReleaseId&lt;/span&gt;
  &lt;span class="c1"&gt;# referring to the specific version (v1)&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cardinalby/git-get-release-action@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;releaseId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;41301084&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have multiple branches in the main repo and want to test them, you should create the same&lt;br&gt;
branches in the tesing repo and change all &lt;code&gt;uses&lt;/code&gt; keys to the appropriate version (&lt;code&gt;action-name@myBranch&lt;/code&gt;). &lt;/p&gt;

&lt;h2&gt;
  
  
  👏 Thank you for reading
&lt;/h2&gt;

&lt;p&gt;Any comments, critics and sharing your own experience would be appreciated!&lt;/p&gt;

&lt;p&gt;If you are interested in developing own Actions, you can read my other posts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cardinalby.github.io/blog/post/github-actions/dry-reusing-code-in-github-actions/"&gt;DRY: reusing code in GitHub Actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cardinalby.github.io/blog/post/github-actions/js-action-packing-and-releasing/"&gt;JavaScript GitHub Action packing and releasing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cardinalby.github.io/blog/post/github-actions/implementing-deferred-steps/"&gt;GitHub Actions: implementing deferred steps&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>github</category>
      <category>testing</category>
      <category>act</category>
      <category>devops</category>
    </item>
    <item>
      <title>Testing of Docker Actions</title>
      <dc:creator>Cardinal</dc:creator>
      <pubDate>Tue, 08 Feb 2022 20:41:57 +0000</pubDate>
      <link>https://forem.com/cardinalby/testing-of-docker-actions-74o</link>
      <guid>https://forem.com/cardinalby/testing-of-docker-actions-74o</guid>
      <description>&lt;p&gt;In this part I'm going to tell about approaches that can be used to test a Docker container Action.&lt;/p&gt;

&lt;h1&gt;
  
  
  Unit tests
&lt;/h1&gt;

&lt;p&gt;An approach here depends on what programming language you use inside a container. Each of them has own testing libraries that can be used to test an application in the container.&lt;/p&gt;

&lt;p&gt;If you use bare bash script, you can divide a single &lt;em&gt;entrypoint.sh&lt;/em&gt; file into the several small scripts considering them as units and testing separately. &lt;/p&gt;

&lt;p&gt;The following approaches can be used to isolate their effects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Redirecting scripts output to a temporary file.&lt;/li&gt;
&lt;li&gt;Interacting with the main script through environment variables.&lt;/li&gt;
&lt;li&gt;Don't hardcode path of filesystem that is modified by scripts, use env variables for that. It will allow you to use temp directories and files during testing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Take a look at &lt;a href="https://thorsteinssonh.github.io/bash_test_tools/"&gt;bash_test_tools&lt;/a&gt; - it's an analog of JavaScript test frameworks for bash scripts and executables.&lt;/p&gt;

&lt;p&gt;It's better to use an environment similar to the production one in tests. Thankfully, it's not a problem with Docker containers. Just create a separate &lt;em&gt;Dockerfile&lt;/em&gt; that runs tests in the same environment that the main &lt;em&gt;Dockerfile&lt;/em&gt; defines.&lt;/p&gt;

&lt;h1&gt;
  
  
  Integration tests
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;github-action-ts-run-api&lt;/em&gt; &lt;a href="https://github.com/cardinalby/github-action-ts-run-api/blob/master/docs/run-targets.md#docker-target"&gt;can run&lt;/a&gt; Docker actions locally on Linux with native Docker, on MacOS and Windows via Docker Desktop and on any CI where Docker is installed (only Linux GitHub-hosted runners have docker support at the moment). &lt;/p&gt;

&lt;p&gt;You still have the same TypeScript API for running, preparing all inputs, environment  and examining results of an action execution.&lt;/p&gt;

&lt;p&gt;Actually, it looks more like a system test (because action is black-boxed and executed as a whole) run using TypeScript API instead of normal yml workflow syntax. If you want to perform an integration test of only of some part of your code, probably, you need to create a separate Dockerfile and use &lt;em&gt;github-action-ts-run-api&lt;/em&gt; to run it.&lt;/p&gt;

&lt;p&gt;Working with a file system in a Docker action follows the common principle: don't use hardcoded paths, rely on &lt;a href="https://github.com/cardinalby/github-action-ts-run-api/blob/master/docs/run-targets/docker.md#paths-in-container"&gt;the environment variables&lt;/a&gt; provided by GitHub instead.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;github-action-ts-run-api&lt;/em&gt; allows you to prepare contents of all these dirs and files before the tested action run and examine their contents after it has finished. &lt;a href="https://github.com/cardinalby/github-action-ts-run-api/blob/master/docs/run-options.md#-setfakefsoptions"&gt;By default&lt;/a&gt;, temporary directories and files are created and attached as volumes to the tested action container.Corresponding environment variables are set to point to the mounting points of these volumes.&lt;/p&gt;

&lt;h1&gt;
  
  
  Stubbing external services
&lt;/h1&gt;

&lt;p&gt;Advice here is similar to the one given for JavaScript actions. Don't hardcode URLs of external services in your code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For GitHub API use the dedicated &lt;code&gt;GITHUB_API_URL&lt;/code&gt;, &lt;code&gt;GITHUB_SERVER_URL&lt;/code&gt;, &lt;code&gt;GITHUB_GRAPHQL_URL&lt;/code&gt; &lt;a href="https://docs.github.com/en/actions/learn-github-actions/environment-variables"&gt;environment variables&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;For other external services introduce custom env variables that can be set in tests. In they are empty action just uses real production URLs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  NodeJS server
&lt;/h2&gt;

&lt;p&gt;As in the case of JavaScript actions testing you can easily create a stub HTTP server right in the test suite managed by NodeJS and pass a base URL of the created stub server through environment variables to the tested action.&lt;/p&gt;

&lt;p&gt;In this case the base URL of stub server should be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;host.docker.internal&lt;/code&gt; for Docker Desktop&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;172.17.0.1&lt;/code&gt; for native Linux Docker&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check out &lt;a href="https://github.com/cardinalby/github-action-ts-run-api/blob/master/docs/run-targets/docker.md#-stubbing-github-api-by-local-nodejs-http-server"&gt;the example&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker container server
&lt;/h2&gt;

&lt;p&gt;Since we test a docker container action it would be natural to spin up a stub HTTP server in a docker container that will run along with the tested docker action container.&lt;/p&gt;

&lt;p&gt;The best way of doing it is to create a &lt;code&gt;docker-compose.yml&lt;/code&gt; file that defines a service with the stub HTTP server and a named network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.5"&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;fake-server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# Image with stub GitHub API HTTP server&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;httpServerDir&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;
&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Assign a name to the network to connect &lt;/span&gt;
    &lt;span class="c1"&gt;# the tested action container to it&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;testNet&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Execute &lt;code&gt;docker compose up&lt;/code&gt; before testing the action container.&lt;/li&gt;
&lt;li&gt;Attach the action container to the named network (&lt;code&gt;testNet&lt;/code&gt; in the example) and pass a base URL of the created stub server  through environment variables to the tested action (&lt;code&gt;fake-server&lt;/code&gt; in the example).&lt;/li&gt;
&lt;li&gt;Execute &lt;code&gt;docker compose down&lt;/code&gt; after the action container finished.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is convenient &lt;a href="https://github.com/cardinalby/github-action-ts-run-api/blob/master/src/actionRunner/docker/utils/withDockerCompose.ts"&gt;&lt;code&gt;withDockerCompose()&lt;/code&gt;&lt;/a&gt; utility function in &lt;em&gt;github-action-ts-run-api&lt;/em&gt; package for performing these steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;withDockerCompose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path/to/docker-compose.yml&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;RunTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dockerAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path/to/action.yml&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="c1"&gt;// network name defined in docker-compose.yml    &lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;testNet&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;RunOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="c1"&gt;// service name defined in docker-compose.yml&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setGithubContext&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;apiUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`http://fake-server:80`&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;out1&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fake_response&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check out &lt;a href="https://github.com/cardinalby/github-action-ts-run-api/blob/master/docs/run-targets/docker.md#-stubbing-github-api-by-http-server-container"&gt;the example&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  Remarks
&lt;/h1&gt;

&lt;p&gt;🔻 Docker Desktop for Windows and MacOS behaves differently from native docker on Linux. Be aware!&lt;/p&gt;

&lt;p&gt;🔻 Windows and MacOS GitHub hosted runners don't have installed docker.&lt;/p&gt;

</description>
      <category>github</category>
      <category>docker</category>
      <category>testing</category>
      <category>devops</category>
    </item>
    <item>
      <title>Testing of JavaScript Actions</title>
      <dc:creator>Cardinal</dc:creator>
      <pubDate>Tue, 08 Feb 2022 19:26:53 +0000</pubDate>
      <link>https://forem.com/cardinalby/testing-of-javascript-actions-20a6</link>
      <guid>https://forem.com/cardinalby/testing-of-javascript-actions-20a6</guid>
      <description>&lt;p&gt;Let's talk about JavaScript GitHub Actions and approaches that we can apply on the different levels of testing.&lt;/p&gt;

&lt;h1&gt;
  
  
  Unit tests
&lt;/h1&gt;

&lt;p&gt;From my point of view, unit testing of Actions doesn't have any differences from testing any other JavaScript code. In most of the examples of Actions available on the GitHub Marketplace authors don't care about &lt;a href="https://github.com/mawrkus/js-unit-testing-guide"&gt;writing testable code&lt;/a&gt;. But nothing prevents you from extracting abstractions and following &lt;a href="https://en.wikipedia.org/wiki/Dependency_inversion_principle"&gt;The Dependency Inversion Principle&lt;/a&gt; which will allow you to easily mock dependencies (such as &lt;code&gt;@actions/core&lt;/code&gt; , &lt;code&gt;@actions/github&lt;/code&gt;, &lt;code&gt;@actions/exec&lt;/code&gt; packages).&lt;/p&gt;

&lt;p&gt;You can use any of JavaScript testing frameworks and mocking libraries. I can advise taking a look at &lt;a href="https://github.com/jonabc/actions-mocks"&gt;actions-mocks&lt;/a&gt; package.&lt;/p&gt;

&lt;h1&gt;
  
  
  Integration tests
&lt;/h1&gt;

&lt;p&gt;I'm not going to duplicate &lt;a href="https://github.com/cardinalby/github-action-ts-run-api/blob/master/README.md"&gt;the documentation&lt;/a&gt; for the &lt;em&gt;github-action-ts-run-api&lt;/em&gt; package here, but I want to mention how it can be used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test a separate &lt;a href="https://github.com/cardinalby/github-action-ts-run-api/blob/master/docs/run-targets.md#single-function-target"&gt;JS function&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;You can import the main function of your action or the function that implements a part of action's logic and run it in the same process as testing code (but still have all required isolation). &lt;br&gt;
It's up to you to decide what granularity of testing you need. &lt;/p&gt;

&lt;p&gt;As in unit tests you can mock external services, but all outputs, inputs and environment will be handled by the package, no mocks here.&lt;/p&gt;

&lt;p&gt;Check out &lt;a href="https://github.com/cardinalby/github-action-ts-run-api#testing-isolated-javascript-function"&gt;the example&lt;/a&gt; of testing a simple function.&lt;/p&gt;

&lt;h3&gt;
  
  
  Remarks
&lt;/h3&gt;

&lt;p&gt;🔻 &lt;code&gt;process.exit(...)&lt;/code&gt; calls inside a function are not mocked and will lead to process termination without cleaning up test environment. Try to avoid them.&lt;/p&gt;

&lt;p&gt;🔻 Keep in mind that &lt;code&gt;require("@actions/github").context&lt;/code&gt; is cached inside the actions library which can cause troubles if you run multiple test cases. To get it around you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;new (require("@actions/github/lib/context").Context)()&lt;/code&gt; instead.&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;jest.resetModules()&lt;/code&gt; (or its analog) after each test case run.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Test a &lt;a href="https://github.com/cardinalby/github-action-ts-run-api/blob/master/docs/run-targets.md#js-file-target"&gt;JS file&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;It's executed in a child node process. It can be the main packed file of an action (specified in &lt;code&gt;runs.main&lt;/code&gt; section of &lt;em&gt;action.yml&lt;/em&gt; file) or one of source JS files. Normally, you pack a JS action to a single file using tools like&lt;br&gt;
&lt;a href="https://github.com/vercel/ncc"&gt;ncc&lt;/a&gt; before publishing.&lt;br&gt;
It makes debugging difficult if you use run test agains the packed file.&lt;/p&gt;

&lt;p&gt;Testing js files you can't directly mock classes and functions (which is ok for integration testing), but instead you should have the entire external services &lt;a href="https://en.wikipedia.org/wiki/Test_stub"&gt;stubs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filesystem modifications
&lt;/h2&gt;

&lt;p&gt;Working with a filesystem follows the common principle: don't use hardcoded paths, rely on &lt;a href="https://github.com/cardinalby/github-action-ts-run-api/blob/master/docs/run-targets/docker.md#paths-in-container"&gt;the environment variables&lt;/a&gt; provided by GitHub instead.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;github-action-ts-run-api&lt;/em&gt; allows you to prepare contents of all these dirs and files before the tested action run and examine their contents after it has finished. &lt;a href="https://github.com/cardinalby/github-action-ts-run-api/blob/master/docs/run-options.md#-setfakefsoptions"&gt;By default&lt;/a&gt;, temporary directories and files are created and corresponding environment variables are set to point to these temporary paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stubbing external services
&lt;/h2&gt;

&lt;p&gt;Don't hardcode URLs of external services in your code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For GitHub API use the dedicated &lt;code&gt;GITHUB_API_URL&lt;/code&gt;, &lt;code&gt;GITHUB_SERVER_URL&lt;/code&gt;, &lt;code&gt;GITHUB_GRAPHQL_URL&lt;/code&gt; &lt;a href="https://docs.github.com/en/actions/learn-github-actions/environment-variables"&gt;environment variables&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;For other external services introduce custom env variables that can be set in tests. In they are empty action just uses real production URLs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's easy to create a stub HTTP server right in the test suite and set these environment variables to &lt;code&gt;localhost&lt;/code&gt; URLs.&lt;/p&gt;

&lt;p&gt;Check out &lt;a href="https://github.com/cardinalby/github-action-ts-run-api/blob/master/docs/run-targets/js-file.md#-stubbing-github-api-by-local-nodejs-http-server"&gt;the example&lt;/a&gt; of stubbing GitHub API called using octokit library.&lt;/p&gt;

</description>
      <category>github</category>
      <category>testing</category>
      <category>javascript</category>
      <category>devops</category>
    </item>
    <item>
      <title>Testing of GitHub Actions. Intro</title>
      <dc:creator>Cardinal</dc:creator>
      <pubDate>Mon, 07 Feb 2022 15:09:35 +0000</pubDate>
      <link>https://forem.com/cardinalby/github-actions-testing-h3h</link>
      <guid>https://forem.com/cardinalby/github-actions-testing-h3h</guid>
      <description>&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;In this post series I want to share my experience and approaches with testing of GitHub Actions. Not using them to test your application, but test actions itself. I will mostly talk about testing of individual actions, not workflows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Individual actions&lt;/strong&gt; (steps) are "bricks" that workflows are built from, and we can consider testing them as unit testing of workflows. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One of the problems&lt;/strong&gt; of GitHub Actions as cloud-based service is that there is no out of the box way of test them locally. Also, support in developing tools is poor comparing to mainstream programming languages. These factors lead to the high errors rate and long feedback loop to find and fix these  errors. &lt;/p&gt;

&lt;p&gt;That's why I believe it's important to adapt best practices we use in software testing for GitHub Actions, and I'm going to share my vision in it.&lt;/p&gt;

&lt;h1&gt;
  
  
  Overview
&lt;/h1&gt;

&lt;p&gt;In the first part I give a general information about GitHub Actions and testing levels. Then I formulate requirements for testing tools and tell about my choise. &lt;/p&gt;

&lt;p&gt;If you want to see concrete recommendations and approaches, just jump to the next part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Action types
&lt;/h2&gt;

&lt;p&gt;At the moment, GitHub supports 3 kinds of Actions which I will refer to in this post:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions/creating-actions/creating-a-javascript-action"&gt;JavaScript actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions/creating-actions/creating-a-docker-container-action"&gt;Docker container actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions/creating-actions/creating-a-composite-action"&gt;Composite actions&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Levels of testing and tools
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🔸 Unit testing
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;A Unit is a smallest testable portion of system or application which can be compiled, liked, loaded, and executed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Depending on the action type, "unit" notion may have different meaning. I will cover it in&lt;br&gt;
"Docker actions" and "JavaScript actions" parts. &lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;composite actions&lt;/strong&gt;, individual steps can be considered units. If you don't hardcode &lt;code&gt;runs&lt;/code&gt; commands in steps, but extract them to the separate actions instead (thankfully, they can be saved locally in the repo), then the unit testing approach reduces to the testing of individual actions. That's exactly what this post is about.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔸 Integration testing
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;In this testing phase, different software modules are combined and tested as a group to make sure that integrated system is ready for system testing. Integrating testing checks the data flow from one module to other modules.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To perform integration testing of a GitHub Action we need a tool that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Runs locally and on CI runner (including GitHub runner).&lt;/li&gt;
&lt;li&gt;Runs the whole action or its part.&lt;/li&gt;
&lt;li&gt;Isolates running code and give testing code an access to action's inputs, outputs and environment.&lt;/li&gt;
&lt;li&gt;Allows stubbing external services used by an action, such as GitHub API.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's list what exactly we expect from such tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parsing action config (action.yml file)&lt;/li&gt;
&lt;li&gt;Setting up action &lt;a href="https://docs.github.com/en/enterprise-cloud@latest/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepswith"&gt;inputs&lt;/a&gt; and &lt;a href="https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#sending-values-to-the-pre-and-post-actions"&gt;saved state&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Setting up &lt;a href="https://docs.github.com/en/actions/learn-github-actions/environment-variables"&gt;environment variables&lt;/a&gt;: custom ones and service GitHub variables.&lt;/li&gt;
&lt;li&gt;Setting up &lt;code&gt;GITHUB_EVENT_PATH&lt;/code&gt; variable and faking JSON file with an event payload.&lt;/li&gt;
&lt;li&gt;Faking &lt;a href="https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#environment-files"&gt;command files&lt;/a&gt; and setting up correspondent env variables (&lt;code&gt;GITHUB_ENV&lt;/code&gt;, &lt;code&gt;GITHUB_PATH&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Faking temp and workspace directories (and corresponding &lt;code&gt;RUNNER_TEMP&lt;/code&gt; and &lt;code&gt;GITHUB_WORKSPACE&lt;/code&gt; variables)&lt;/li&gt;
&lt;li&gt;Intercepting and isolating stdout and stderr output. It's important, because being run on GitHub runner our tests can interfere with actual commands of test workflow.&lt;/li&gt;
&lt;li&gt;Parsing intercepted output and faked command files to extract &lt;a href="https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions"&gt;commands&lt;/a&gt; issued by tested code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I haven't found any handy solution that meet these requirements and it made me write my own TypeScript package for testing JavaScript and Docker actions called &lt;a href="https://github.com/cardinalby/github-action-ts-run-api"&gt;github-action-ts-run-api&lt;/a&gt;. It has well typed JavaScript API with reasonable defaults, can be used with any JavaScript test framework or alone and covers all listed requirements. &lt;/p&gt;

&lt;p&gt;In the following parts of the post I'm going to tell about the testing techniques that become&lt;br&gt;
possible with this package. For &lt;strong&gt;more code examples&lt;/strong&gt; take a look at the &lt;a href="https://github.com/cardinalby/github-action-ts-run-api#documentation"&gt;package documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔸 System testing
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;System testing is performed on a complete, integrated system. It allows checking system’s compliance as per the requirements. It tests the overall interaction of components. It involves load, performance, reliability and security testing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It can be debatable what to consider as system testing in case of GitHub Action.&lt;/p&gt;

&lt;h4&gt;
  
  
  Option 1
&lt;/h4&gt;

&lt;p&gt;Testing the whole action behavior using the same tool as we use for integration testing, but exclude external services stubs if it possible.&lt;/p&gt;

&lt;h4&gt;
  
  
  Option 2
&lt;/h4&gt;

&lt;p&gt;Testing action behavour in the workflow. The only existing solution for doing that locally is a great tool called &lt;a href="https://github.com/nektos/act"&gt;Act&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>github</category>
      <category>testing</category>
      <category>javascript</category>
      <category>devops</category>
    </item>
    <item>
      <title>DRY: reusing code in GitHub Actions</title>
      <dc:creator>Cardinal</dc:creator>
      <pubDate>Fri, 04 Feb 2022 14:47:46 +0000</pubDate>
      <link>https://forem.com/cardinalby/github-actions-make-it-reusable-3ho7</link>
      <guid>https://forem.com/cardinalby/github-actions-make-it-reusable-3ho7</guid>
      <description>&lt;p&gt;In this post I want to make a quick overview of the approaches of reusing steps of your workflow to avoid duplication of the same steps across different workflows or jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔸 Reusing workflows
&lt;/h2&gt;

&lt;p&gt;The obvious option is using the &lt;a href="https://docs.github.com/en/actions/using-workflows/reusing-workflows"&gt;"Reusable workflows" feature&lt;/a&gt; that allows you to call extract some steps into a separate "reusable" workflow and call this workflow as a job in other workflows.&lt;/p&gt;

&lt;h3&gt;
  
  
  🥡 Takeaways:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Reusable workflows can't call other reusable workflows.&lt;/li&gt;
&lt;li&gt;The strategy property is not supported in any job that calls a reusable workflow.&lt;/li&gt;
&lt;li&gt;Env variables and secrets are not inherited.&lt;/li&gt;
&lt;li&gt;It's not convenient if you need to extract and reuse several steps inside one job.&lt;/li&gt;
&lt;li&gt;Since it runs as a separate job, you have to use &lt;a href="https://docs.github.com/en/actions/advanced-guides/storing-workflow-data-as-artifacts"&gt;build artifacts&lt;/a&gt; to share files between reusable workflow and your main workflow.&lt;/li&gt;
&lt;li&gt;You can call reusable workflow in synchronous or asynchronous manner (managing it by jobs ordering using &lt;code&gt;needs&lt;/code&gt; keys).&lt;/li&gt;
&lt;li&gt;A reusable workflow can define outputs that extract outputs/outcomes from performed steps. They can easily used to pass data to the "main" workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🔸 Dispatched workflows
&lt;/h2&gt;

&lt;p&gt;Another possibility that GitHub gives us is &lt;a href="https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow"&gt;workflow_dispatch&lt;/a&gt; event that can trigger a workflow run. Simply put, you can trigger a workflow manually or through GitHub API and provide its inputs.&lt;/p&gt;

&lt;p&gt;There are &lt;a href="https://github.com/marketplace?type=actions&amp;amp;query=dispatch+workflow+"&gt;actions&lt;/a&gt; available on the Marketplace which allow you to trigger a "dispatched" workflow as a step of "main" workflow. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/marketplace/actions/workflow-dispatch-and-wait"&gt;Some&lt;/a&gt; of them also allow doing it in a synchronous manner (wait until dispatched workflow is finished). It is worth to say that this feature is implemented by polling statuses of repo workflows which is &lt;a href="https://github.com/aurelien-baudet/workflow-dispatch/blob/master/src/workflow-handler.ts#L122"&gt;not&lt;/a&gt; very reliable, especially in a concurrent environment. Also, it is bounded by GitHub API usage limits and therefore has a delay in finding out a status of dispatched workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  🥡 Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You can have multiple nested calls, triggering a workflow  from another triggered workflow. If done careless, can lead to an infinite loop.&lt;/li&gt;
&lt;li&gt;You need a special token with "workflows" permission; your usual &lt;code&gt;secrets.GITHUB_TOKEN&lt;/code&gt; doesn't allow you to dispatch a workflow.&lt;/li&gt;
&lt;li&gt;You can call run multiple dispatched workflows inside one job.&lt;/li&gt;
&lt;li&gt;There is no easy way to get some data back from dispatched workflows to the main one.&lt;/li&gt;
&lt;li&gt;Works better in "fire and forget" scenario. Waiting for a finish of dispatched workflow has some limitations.&lt;/li&gt;
&lt;li&gt;You can observe dispatched workflows runs and cancel them manually.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🔸 Composite Actions
&lt;/h2&gt;

&lt;p&gt;In this approach we extract steps to a distinct &lt;a href="https://docs.github.com/en/actions/creating-actions/creating-a-composite-action"&gt;composite action&lt;/a&gt;, that can be located in the same or separate repository.&lt;/p&gt;

&lt;p&gt;From your "main" workflow it looks as a usual action (a single step), but internally it comprises of multiple steps each of which can call own actions.&lt;/p&gt;

&lt;h3&gt;
  
  
  🥡 Takeaways:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Supports nesting: each step of a composite action can use another composite action.&lt;/li&gt;
&lt;li&gt;Bad visualisation of internal steps run: in the "main" workflow it's displayed as a usual step run. In raw logs you can find details of internal steps execution, but it doesn't look very friendly.&lt;/li&gt;
&lt;li&gt;Shares environment variables with a parent job, but doesn't share secrets, which should be passed explicitly via inputs.&lt;/li&gt;
&lt;li&gt;Supports inputs and outputs. Outputs are prepared from outputs/outcomes of internal steps and can be easily used  to pass data from composite action to the "main" workflow.&lt;/li&gt;
&lt;li&gt;A composite action runs inside the job of the "main" workflow. Since they share a common file system, there is no need to use build artifacts to transfer files from the composite action to the "main" workflow.&lt;/li&gt;
&lt;li&gt;You can't use &lt;code&gt;continue-on-error&lt;/code&gt; option inside a composite action.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  👏 Thank you for reading
&lt;/h2&gt;

&lt;p&gt;Any comments, critics and sharing your own experience would be appreciated!&lt;/p&gt;

&lt;p&gt;If you are interested in developing own Actions, I also recommend you reading "&lt;a href="https://cardinalby.github.io/blog/post/github-actions/testing/1-testing-of-github-actions-intro/"&gt;GitHub Actions Testing&lt;/a&gt;" series.&lt;/p&gt;

</description>
      <category>github</category>
      <category>actions</category>
    </item>
    <item>
      <title>Releasing WebExtension using GitHub Actions</title>
      <dc:creator>Cardinal</dc:creator>
      <pubDate>Wed, 08 Dec 2021 19:11:35 +0000</pubDate>
      <link>https://forem.com/cardinalby/releasing-webextension-using-github-actions-i9j</link>
      <guid>https://forem.com/cardinalby/releasing-webextension-using-github-actions-i9j</guid>
      <description>&lt;h3&gt;
  
  
  My Workflow
&lt;/h3&gt;

&lt;p&gt;I'm the author of an open-source &lt;a href="https://github.com/cardinalby/memrise-audio-uploader"&gt;browser extension&lt;/a&gt; and I want to share the workflow that I have created for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;releasing the extension to Chrome Web Store&lt;/li&gt;
&lt;li&gt;releasing the extension to Firefox Add-ons&lt;/li&gt;
&lt;li&gt;packaging the extension into various artifacts for offline distribution (&lt;em&gt;crx&lt;/em&gt; and &lt;em&gt;xpi&lt;/em&gt; files)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wasn't able to find any existing solutions that can meet these requirements and I have prepared a set of actions and a complete solution in form of several workflows to set up the whole process. &lt;/p&gt;

&lt;p&gt;They are based on my practical experience of dealing with all difficulties of interacting with the extension stores and their undocumented behaviour.&lt;/p&gt;

&lt;h3&gt;
  
  
  Submission Category:
&lt;/h3&gt;

&lt;p&gt;DIY Deployments&lt;/p&gt;

&lt;h3&gt;
  
  
  Yaml File or Link to Code
&lt;/h3&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--566lAguM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/cardinalby"&gt;
        cardinalby
      &lt;/a&gt; / &lt;a href="https://github.com/cardinalby/memrise-audio-uploader"&gt;
        memrise-audio-uploader
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/cardinalby/memrise-audio-uploader/workflows/buildAndDeploy/badge.svg"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--H3JvL0Jz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://github.com/cardinalby/memrise-audio-uploader/workflows/buildAndDeploy/badge.svg" alt="buildAndDeploy"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/cardinalby/memrise-audio-uploader/workflows/delayed-chrome-web-store-deploy/badge.svg"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--aEvpu7La--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://github.com/cardinalby/memrise-audio-uploader/workflows/delayed-chrome-web-store-deploy/badge.svg" alt="delayed-chrome-web-store-deploy"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/cardinalby/memrise-audio-uploader/workflows/fetch-google-access-token/badge.svg"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---NkPUng0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://github.com/cardinalby/memrise-audio-uploader/workflows/fetch-google-access-token/badge.svg" alt="fetch-google-access-token"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;
Memrise Audio Uploader&lt;/h1&gt;
&lt;p&gt;Chrome extension which allows you upload sounds of words and phrases prononsiation from &lt;a href="http://soundoftext.com" rel="nofollow"&gt;http://soundoftext.com&lt;/a&gt; to &lt;a href="http://memrise.com" rel="nofollow"&gt;http://memrise.com&lt;/a&gt; course&lt;/p&gt;
&lt;h2&gt;
Install from the stores&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://chrome.google.com/webstore/detail/memrise-audio-uploader/fonhjbpoimjmgfgbboichngpjlmilbmk" rel="nofollow"&gt;Chrome Web Store&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://addons.mozilla.org/firefox/addon/memrise-audio-uploader/" rel="nofollow"&gt;Firefox Add-ons&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;To download the offline versions please go to the
&lt;a href="https://github.com/cardinalby/memrise-audio-uploader/releases"&gt;Releases page&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
Screenshots&lt;/h2&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/cardinalby/memrise-audio-uploaderdesign/screenshots/screenshot1.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--x9BB9Sve--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://github.com/cardinalby/memrise-audio-uploaderdesign/screenshots/screenshot1.png" alt="Screenshot 1"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/cardinalby/memrise-audio-uploaderdesign/screenshots/screenshot2.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VO4kf3Nb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://github.com/cardinalby/memrise-audio-uploaderdesign/screenshots/screenshot2.png" alt="Screenshot 1"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/cardinalby/memrise-audio-uploaderdesign/screenshots/screenshot3.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ecBYDeDy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://github.com/cardinalby/memrise-audio-uploaderdesign/screenshots/screenshot3.png" alt="Screenshot 1"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;



&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/cardinalby/memrise-audio-uploader"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;&lt;a href="https://github.com/cardinalby/memrise-audio-uploader/blob/master/.github/workflows/buildAndDeploy.yml"&gt;the main workflow&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Additional Resources / Info
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;I have posted a &lt;a href="https://dev.to/cardinalby/webextension-deployment-and-publishing-using-github-actions-522o"&gt;detailed manual&lt;/a&gt; here on &lt;em&gt;dev.to&lt;/em&gt; that will help everybody to understand my workflow and build the similar one for their projects.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/cardinalby/scheduling-delayed-github-action-12a6"&gt;Another post&lt;/a&gt; related to the tricky way of using "delayed" workflows in GitHub Actions that was used in my workflow.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
    </item>
    <item>
      <title>Publish on Firefox Add-ons</title>
      <dc:creator>Cardinal</dc:creator>
      <pubDate>Wed, 08 Dec 2021 18:11:37 +0000</pubDate>
      <link>https://forem.com/cardinalby/bonus-retry-of-chrome-web-store-releasing-steps-48l</link>
      <guid>https://forem.com/cardinalby/bonus-retry-of-chrome-web-store-releasing-steps-48l</guid>
      <description>&lt;p&gt;In this part we are going to create the workflow that will be responsible for publishing the extension&lt;br&gt;
on Firefox Add-ons marketplace.&lt;/p&gt;
&lt;h2&gt;
  
  
  🧱 Prepare
&lt;/h2&gt;

&lt;p&gt;First, you need to find out your extension UUID. You can find it on your extension's page at Add-on Developer Hub in the "Technical Details" section.&lt;/p&gt;

&lt;p&gt;Next, follow the &lt;a href="https://addons-server.readthedocs.io/en/latest/topics/api/auth.html"&gt;official documentation&lt;/a&gt; and obtain &lt;code&gt;jwtIssuer&lt;/code&gt; and &lt;code&gt;jwtSecret&lt;/code&gt; values required for accessing the API.&lt;/p&gt;
&lt;h2&gt;
  
  
  🔒 Add these values to &lt;strong&gt;&lt;em&gt;secrets&lt;/em&gt;&lt;/strong&gt;:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;FF_EXTENSION_ID&lt;/code&gt; - UUID of your extension&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FF_JWT_ISSUER&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FF_JWT_SECRET&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  &lt;em&gt;publish-on-firefox-add-ons&lt;/em&gt; workflow
&lt;/h2&gt;

&lt;p&gt;The workflow will have the only trigger: &lt;em&gt;workflow_dispatch&lt;/em&gt; event. It can be dispatched:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Manually specifying any commit or tag as workflow &lt;em&gt;ref&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;By &lt;em&gt;&lt;strong&gt;publish-release-on-tag&lt;/strong&gt;&lt;/em&gt; workflow after it has prepared a release with &lt;strong&gt;zip&lt;/strong&gt; asset.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We will utilize already created &lt;em&gt;&lt;em&gt;&lt;a href="https://dev.to/cardinalby/firefox-add-ons-store-and-xpi-file-4g83#getzipasset-action"&gt;get-zip-asset&lt;/a&gt;&lt;/em&gt;&lt;/em&gt; composite action to obtain packed &lt;strong&gt;zip&lt;/strong&gt; that is needed for deploying the extension.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;.github/workflows/publish-on-firefox-addons.yml&lt;/em&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;publish-on-firefox-add-ons&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cardinalby/export-env-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;envFile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;./.github/workflows/constants.env'&lt;/span&gt;
          &lt;span class="na"&gt;expand&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Obtain packed zip&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./.github/workflows/actions/get-zip-asset&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;githubToken&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy to Firefox Addons&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;addonsDeploy&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cardinalby/webext-buildtools-firefox-addons-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;continue-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;zipFilePath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.ZIP_FILE_PATH }}&lt;/span&gt;
          &lt;span class="na"&gt;extensionId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.FF_EXTENSION_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;jwtIssuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.FF_JWT_ISSUER }}&lt;/span&gt;
          &lt;span class="na"&gt;jwtSecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.FF_JWT_SECRET }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Abort on upload error&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;steps.addonsDeploy.outcome == 'failure' &amp;amp;&amp;amp;&lt;/span&gt;
          &lt;span class="s"&gt;steps.addonsDeploy.outputs.sameVersionAlreadyUploadedError != 'true'&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;exit &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://dev.to/cardinalby/firefox-add-ons-store-and-xpi-file-4g83#not-a-composite-action"&gt;As usual&lt;/a&gt;, at the beginning of each workflow we check out the repo and export env variables from &lt;em&gt;constants.env&lt;/em&gt; file.&lt;/li&gt;
&lt;li&gt;After calling &lt;strong&gt;&lt;em&gt;get-zip-asset&lt;/em&gt;&lt;/strong&gt; composite action we expect to have &lt;strong&gt;zip&lt;/strong&gt; file with packed and built extension at &lt;code&gt;env.ZIP_FILE_PATH&lt;/code&gt; path. &lt;/li&gt;
&lt;li&gt;We pass its path along with other required inputs to &lt;em&gt;&lt;a href="https://github.com/marketplace/actions/webext-buildtools-firefox-addons-action"&gt;webext-buildtools-firefox-addons-action&lt;/a&gt;&lt;/em&gt; action to publish the extension. We use &lt;code&gt;continue-on-error: true&lt;/code&gt; flag to prevent the step from failing immediately in case of error and validate the result at the following step according to our preferences.&lt;/li&gt;
&lt;li&gt;Examining &lt;em&gt;addonsDeploy&lt;/em&gt; step outputs we can find out the reason of its failure. If it failed because the version we try to publish is already published, we don't consider it as error.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Timeout notes
&lt;/h3&gt;

&lt;p&gt;Also, the publishing action can sometimes fail with &lt;code&gt;timeoutError == 'true'&lt;/code&gt; output. It means, the extension was uploaded but the waiting for its processing by Addons server was timed out. I didn't include a handling of this error to the workflow, but you can: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Specify longer timeout with &lt;code&gt;timeoutMs&lt;/code&gt; input of &lt;em&gt;webext-buildtools-firefox-addons-action&lt;/em&gt; action. Default timeout is &lt;em&gt;600000&lt;/em&gt; ms (10 min).&lt;/li&gt;
&lt;li&gt;Do not fail the job in the last step if &lt;code&gt;steps.addonsDeploy.outputs.timeoutError == 'true'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Just rerun the workflow after a while in the case of timeout. If the extension has been processed after the first run, the workflow will pass (with &lt;code&gt;steps.addonsDeploy.outputs.sameVersionAlreadyUploadedError != 'true'&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webextension</category>
      <category>actionshackathon21</category>
      <category>github</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>Build release assets</title>
      <dc:creator>Cardinal</dc:creator>
      <pubDate>Wed, 08 Dec 2021 14:04:03 +0000</pubDate>
      <link>https://forem.com/cardinalby/creating-github-release-2mn</link>
      <guid>https://forem.com/cardinalby/creating-github-release-2mn</guid>
      <description>&lt;h1&gt;
  
  
  &lt;em&gt;build-assets-on-release&lt;/em&gt; workflow
&lt;/h1&gt;

&lt;p&gt;Let's create the first workflow that utilizes &lt;em&gt;&lt;strong&gt;build-test-pack&lt;/strong&gt;&lt;/em&gt; action and builds release assets for offline distribution once a release has been published.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--a3xDABcd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cardinalby.github.io/blog/images/posts/github-actions/webext/build-assets-on-release.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--a3xDABcd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cardinalby.github.io/blog/images/posts/github-actions/webext/build-assets-on-release.png" alt="build-assets-on-release workflow" width="880" height="632"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;.github/workflows/build-assets-on-release.yml:&lt;/em&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build release assets&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;release&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Creating draft releases will not trigger it&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;published&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# We will add 3 jobs here...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The workflow will have 3 jobs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;em&gt;ensure-zip&lt;/em&gt;: Ensuring we have &lt;strong&gt;zip&lt;/strong&gt; release asset.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;build-signed-crx-asset&lt;/em&gt;: Building &lt;strong&gt;crx&lt;/strong&gt; asset.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;build-signed-xpi-asset&lt;/em&gt;: Building &lt;strong&gt;xpi&lt;/strong&gt; asset.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  &lt;em&gt;ensure-zip&lt;/em&gt; job
&lt;/h2&gt;

&lt;p&gt;The first job will find &lt;strong&gt;zip&lt;/strong&gt; asset in the release or build  it if not found:&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;ensure-zip&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;zipAssetId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt; 
        &lt;span class="s"&gt;${{ steps.getZipAssetId.outputs.result || &lt;/span&gt;
            &lt;span class="s"&gt;steps.uploadZipAsset.outputs.id }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cardinalby/export-env-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;envFile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;./.github/workflows/constants.env'&lt;/span&gt;
          &lt;span class="na"&gt;expand&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Find out zip asset id from the release&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;getZipAssetId&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cardinalby/js-eval-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;ASSETS_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.release.assets_url }}&lt;/span&gt;
          &lt;span class="na"&gt;ASSET_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.ZIP_FILE_NAME }}&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;expression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;(await octokit.request("GET " + env.ASSETS_URL)).data&lt;/span&gt;
              &lt;span class="s"&gt;.find(asset =&amp;gt; asset.name == env.ASSET_NAME)?.id || ''&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build, test and pack&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;!steps.getZipAssetId.outputs.result'&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildPack&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./.github/workflows/actions/build-test-pack&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload "extension.zip" asset to the release&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uploadZipAsset&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;!steps.getZipAssetId.outputs.result'&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-release-asset@v1&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;upload_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.release.upload_url }}&lt;/span&gt;
          &lt;span class="na"&gt;asset_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.ZIP_FILE_PATH }}&lt;/span&gt;
          &lt;span class="na"&gt;asset_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.ZIP_FILE_NAME }}&lt;/span&gt;
          &lt;span class="na"&gt;asset_content_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/zip&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;At the beginning of each workflow we check out the repo and export env variables from &lt;em&gt;constants.env&lt;/em&gt; file.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getZipAssetId&lt;/code&gt; step uses &lt;code&gt;github.event.release.assets_url&lt;/code&gt; to get the release assets list
and find &lt;strong&gt;zip&lt;/strong&gt; asset id. It may not exist if the workflow was triggered by a release created by a user directly. Used &lt;a href="https://github.com/marketplace/actions/js-eval-action"&gt;&lt;code&gt;js-eval-action&lt;/code&gt;&lt;/a&gt; is an action for executing general-purpose JavaScript code.&lt;/li&gt;
&lt;li&gt;If it hasn't been found, we call &lt;em&gt;&lt;strong&gt;build-test-pack&lt;/strong&gt;&lt;/em&gt; composite action to build &lt;strong&gt;zip&lt;/strong&gt; asset from the scratch, save it to &lt;code&gt;env.ZIP_FILE_PATH&lt;/code&gt; and then attach to the release.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;zipAssetId&lt;/code&gt; job output will have a value from either &lt;code&gt;getZipAssetId&lt;/code&gt; step (if &lt;strong&gt;zip&lt;/strong&gt; asset was found in the release) or or &lt;code&gt;uploadZipAsset&lt;/code&gt; step if it has been built and uploaded in the job.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The following 2 jobs will run in parallel using &lt;strong&gt;zip&lt;/strong&gt; asset provided by the job we just created.&lt;br&gt;
This order is defined using &lt;code&gt;needs: ensure-zip&lt;/code&gt; key in the jobs.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;em&gt;build-signed-crx-asset&lt;/em&gt; job
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;crx&lt;/code&gt; file is a distribution package of your extension. When you install an extension from the store, &lt;code&gt;crx&lt;/code&gt; file gets transmitted and installed to your browser. But it can be also installed in offline mode manually or via automation tools. Read more about alternative extension distribution options &lt;a href="https://developer.chrome.com/docs/extensions/mv2/external_extensions/"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Actually, &lt;code&gt;crx&lt;/code&gt; file is a kind of &lt;code&gt;zip&lt;/code&gt; file that contains the extension dir. But it also contains some additional metadata required by Chrome browser. Namely, it's signed using a developer's private key.&lt;/p&gt;
&lt;h3&gt;
  
  
  🧱 Prepare
&lt;/h3&gt;

&lt;p&gt;For this step you need to have a &lt;code&gt;pem&lt;/code&gt; private key that is used for offline signing. You can use &lt;a href="https://www.openssl.org/docs/manmaster/man1/genrsa.html"&gt;openssl&lt;/a&gt; to &lt;a href="https://www.scottbrady91.com/openssl/creating-rsa-keys-using-openssl"&gt;generate it&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  🔒 Add the key to the repo &lt;strong&gt;&lt;em&gt;secrets&lt;/em&gt;&lt;/strong&gt;:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CHROME_CRX_PRIVATE_KEY&lt;/code&gt; - a string containing the private key.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Add the job to the workflow:
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;build-signed-crx-asset&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ensure-zip&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cardinalby/export-env-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;envFile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;./.github/workflows/constants.env'&lt;/span&gt;
          &lt;span class="na"&gt;expand&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Download zip release asset&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cardinalby/download-release-asset-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;assetId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ needs.ensure-zip.outputs.zipAssetId }}&lt;/span&gt;
          &lt;span class="na"&gt;targetPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.ZIP_FILE_PATH }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build offline crx&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildOfflineCrx&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cardinalby/webext-buildtools-chrome-crx-action@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;zipFilePath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.ZIP_FILE_PATH }}&lt;/span&gt;
          &lt;span class="na"&gt;crxFilePath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.OFFLINE_CRX_FILE_PATH }}&lt;/span&gt;
          &lt;span class="na"&gt;privateKey&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CHROME_CRX_PRIVATE_KEY }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload offline crx release asset&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-release-asset@v1&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;upload_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.release.upload_url }}&lt;/span&gt;
          &lt;span class="na"&gt;asset_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.OFFLINE_CRX_FILE_PATH }}&lt;/span&gt;
          &lt;span class="na"&gt;asset_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.OFFLINE_CRX_FILE_NAME }}&lt;/span&gt;
          &lt;span class="na"&gt;asset_content_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/x-chrome-extension&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;The job uses &lt;code&gt;zipAssetId&lt;/code&gt; output from &lt;code&gt;ensure-zip&lt;/code&gt; job to download the asset.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/marketplace/actions/webext-buildtools-chrome-crx-action"&gt;&lt;code&gt;webext-buildtools-chrome-crx-action&lt;/code&gt;&lt;/a&gt; action uses the private key from &lt;em&gt;secrets&lt;/em&gt; to build and sign  &lt;strong&gt;crx&lt;/strong&gt; file for offline distribution. This action doesn't interact with Chrome Web Store.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  &lt;em&gt;build-signed-xpi-asset&lt;/em&gt; job
&lt;/h2&gt;

&lt;p&gt;Firefox also has options for &lt;a href="https://extensionworkshop.com/documentation/publish/self-distribution/"&gt;self-distribution&lt;/a&gt; of add-ons. Firefox's own format of extension package is called &lt;code&gt;xpi&lt;/code&gt; and it also has to be signed. Unlike Chrome's, this signing procedure is online: we have to ask Firefox server to do it for us.&lt;/p&gt;
&lt;h3&gt;
  
  
  🧱 Prepare
&lt;/h3&gt;

&lt;p&gt;It's recommended to create a separate entity for the offline distributed extension on Add-on Developer Hub and use its id for signing &lt;strong&gt;xpi&lt;/strong&gt; files. Otherwise, a new entity will be added for every build. You can find extension UUID in the "Technical Details" section of the created entity.&lt;/p&gt;

&lt;p&gt;Next, follow the &lt;a href="https://addons-server.readthedocs.io/en/latest/topics/api/auth.html"&gt;official documentation&lt;/a&gt; and obtain &lt;code&gt;jwtIssuer&lt;/code&gt; and &lt;code&gt;jwtSecret&lt;/code&gt; values required for accessing the API.&lt;/p&gt;
&lt;h3&gt;
  
  
  🔒 Add these values to repo &lt;strong&gt;&lt;em&gt;secrets&lt;/em&gt;&lt;/strong&gt;:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;FF_OFFLINE_EXT_ID&lt;/code&gt; - UUID of the add-on entity created for offline distribution.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FF_JWT_ISSUER&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FF_JWT_SECRET&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally, add the following job to the workflow:&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-signed-xpi-asset&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ensure-zip&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cardinalby/export-env-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;envFile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;./.github/workflows/constants.env'&lt;/span&gt;
          &lt;span class="na"&gt;expand&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Download zip release asset&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cardinalby/download-release-asset-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;assetId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ needs.ensure-zip.outputs.zipAssetId }}&lt;/span&gt;
          &lt;span class="na"&gt;targetPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.ZIP_FILE_PATH }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Sign Firefox xpi for offline distribution&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ffSignXpi&lt;/span&gt;
        &lt;span class="na"&gt;continue-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cardinalby/webext-buildtools-firefox-sign-xpi-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;timeoutMs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1200000&lt;/span&gt;
          &lt;span class="na"&gt;extensionId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.FF_OFFLINE_EXT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;zipFilePath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.ZIP_FILE_PATH }}&lt;/span&gt;
          &lt;span class="na"&gt;xpiFilePath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.XPI_FILE_PATH }}&lt;/span&gt;
          &lt;span class="na"&gt;jwtIssuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.FF_JWT_ISSUER }}&lt;/span&gt;
          &lt;span class="na"&gt;jwtSecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.FF_JWT_SECRET }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Abort on sign error&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;steps.ffSignXpi.outcome == 'failure' &amp;amp;&amp;amp;&lt;/span&gt;
          &lt;span class="s"&gt;steps.ffSignXpi.outputs.sameVersionAlreadyUploadedError != 'true'&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;exit &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload offline xpi release asset&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;steps.ffSignXpi.outcome == 'success'&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-release-asset@v1&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;upload_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.release.upload_url }}&lt;/span&gt;
          &lt;span class="na"&gt;asset_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.XPI_FILE_PATH }}&lt;/span&gt;
          &lt;span class="na"&gt;asset_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.XPI_FILE_NAME }}&lt;/span&gt;
          &lt;span class="na"&gt;asset_content_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/x-xpinstall&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/marketplace/actions/webext-buildtools-firefox-sign-xpi-action"&gt;&lt;code&gt;webext-buildtools-firefox-sign-xpi-action&lt;/code&gt;&lt;/a&gt; action uses the keys from &lt;em&gt;secrets&lt;/em&gt; to build and sign
&lt;strong&gt;xpi&lt;/strong&gt; file for offline distribution. Practice shows that this step can take quite a long time to complete. &lt;code&gt;timeoutMs&lt;/code&gt; input allows you to configure the timeout.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;continue-on-error: true&lt;/code&gt; key of &lt;code&gt;ffSignXpi&lt;/code&gt; step is used to prevent the step to fail immediately in case of error and examine an error at the next step.&lt;/li&gt;
&lt;li&gt;At the next step we examine an error (if any) and fail the job, except the case where the error happened because this version was already signed. It's a peculiarity of Firefox Add-ons signing process: it doesn't allow to sign the same version twice. So we just suppress the error and don't
fail the entire workflow and just skip the following "upload" step instead.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webextension</category>
      <category>github</category>
      <category>actionshackathon</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
