<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Laura Vuorenoja</title>
    <description>The latest articles on DEV Community by Laura Vuorenoja (@lauravuo).</description>
    <link>https://dev.to/lauravuo</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%2F455496%2F7f9bb9d7-19ba-4a54-b87f-917b530d96c5.jpeg</url>
      <title>DEV Community: Laura Vuorenoja</title>
      <link>https://dev.to/lauravuo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lauravuo"/>
    <language>en</language>
    <item>
      <title>My First Time at GopherCon</title>
      <dc:creator>Laura Vuorenoja</dc:creator>
      <pubDate>Wed, 30 Aug 2023 08:20:23 +0000</pubDate>
      <link>https://dev.to/lauravuo/my-first-time-at-gophercon-2109</link>
      <guid>https://dev.to/lauravuo/my-first-time-at-gophercon-2109</guid>
      <description>&lt;p&gt;At the beginning of this year, I set myself a target to speak at a Go programming language conference. There were several reasons to do so. Go has been one of my favorite tools for years, and I have longed for an excuse to join a Go event. Giving a speech was perfect for that purpose. Plus, I have&lt;br&gt;
multiple excellent topics to share with the community as we do open-source development in our project, and I can share our learnings along with our code more freely. Furthermore, I want to do my part in having more diverse speakers at tech conferences.&lt;/p&gt;

&lt;p&gt;As Go released 1.20, it inspired me to experiment with &lt;a href="https://go.dev/testing/coverage/"&gt;the new feature to gather coverage data&lt;/a&gt; for binaries built with Go tooling. I refactored our application testing pipelines and thought this would be an excellent topic to share with the community. I was lucky to get my speech accepted at &lt;a href="https://www.gophercon.co.uk/"&gt;GopherCon UK&lt;/a&gt; in London, so it was finally time to join my first Go conference.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--P1lkmj5_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/66wqsct40cb7hylacjp5.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--P1lkmj5_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/66wqsct40cb7hylacjp5.jpeg" alt="The Brewery" width="800" height="1067"&gt;&lt;/a&gt; &lt;em&gt;The Brewery hosted the event. Surprisingly for London, weather was excellent during the whole conference.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The conference was held in the Brewery, a relaxed event venue in London City. The word on the conference halls was that the number of event sponsors had decreased from the previous year, and therefore, it had been challenging to organize the event. Luckily, the organizers were able to pull things still together.&lt;/p&gt;

&lt;p&gt;Apart from the recession, the times are now interesting for Gophers. Many good things are happening in the Go world. As Cameron Balahan pointed out in his talk "State of the Go Nation," &lt;a href="https://thenewstack.io/developers-most-likely-to-learn-go-and-rust-in-2023-survey-says/"&gt;Go is more popular than ever&lt;/a&gt;. More and more programmers have been adding Go to their tool pack in recent years, pushing language developers to add new and better features. Moreover, Go is not only a programming language anymore; it is a whole ecosystem with support for many kinds of tooling. Newcomers have a way easier job to start with development than, for example, I had seven years ago. Balahan stated that improving the onboarding of new developers is still one of the Go team's top priorities. He mentioned that they are working on the libraries, documentation, and error messages to help newcomers and all Go developers be more productive.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--k6MqMb-Y--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2v7yql1i3lyckpbliozi.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--k6MqMb-Y--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2v7yql1i3lyckpbliozi.jpeg" alt="Cameron" width="800" height="600"&gt;&lt;/a&gt; &lt;em&gt;Cameron Balahan is the product lead for Go.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Automated Testing and Test Coverage
&lt;/h2&gt;

&lt;p&gt;The topic of my talk was &lt;a href="https://findy-network.github.io/docs/slides/gophercon-uk-2023/"&gt;"Boosting Test Coverage for Microservices."&lt;/a&gt; I described in the presentation how vital automated testing has become for our small team. Automated testing is usually the part you skip when time is running out, but I tried to convince the audience that this might not be the best approach – missing tests may bite you back in the end.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--MOzGkE2u--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hdogrs6x4e0qbsughysf.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--MOzGkE2u--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hdogrs6x4e0qbsughysf.jpeg" alt="On the stage." width="800" height="551"&gt;&lt;/a&gt; &lt;em&gt;On the stage. Photo by Tapan Avasthi&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Furthermore, I discussed test coverage in the presentation along with how one can measure test coverage for unit tests and now even - with the Go new tooling – for application tests, i.e., tests you run with the compiled application binary instead of unit testing tooling.&lt;/p&gt;

&lt;p&gt;The audience received the talk well, and I got many interesting questions. People are struggling with similar issues when it comes to testing. It is tough to decide which functionality to simulate in the CI pipeline. Also, we discussed problems when moving on to automated testing with a legacy code base. The Go's new coverage feature was unknown to most people, and some were eager to try it out instantly after my session.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--5_zPXrP2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ufvep5qyhq6duj6gbn98.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--5_zPXrP2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ufvep5qyhq6duj6gbn98.jpeg" alt="Gophers" width="800" height="1067"&gt;&lt;/a&gt; &lt;em&gt;All participants were given adorable Gopher mascots.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Unfortunately, when you are a speaker at a conference, you cannot concentrate fully on the conference program because one needs to prepare for the talk. However, I was lucky enough to join some other sessions as well. There were mainly three themes that I gained valuable insights from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logging and tracing
&lt;/h2&gt;

&lt;p&gt;Generating better logs and traces for applications seems to be a hot topic – and no wonder why. Services with high loads can generate countless amounts of data, and for the developers to use the logs for fixing issues efficiently, they must be able to filter and search them. The ability to debug each request separately is essential.&lt;/p&gt;

&lt;p&gt;Jonathan Amsterdam from Google gave an inspiring presentation on &lt;a href="https://go.dev/blog/slog"&gt;the slog package&lt;/a&gt;, the newest addition to the standard library regarding logging. Go's default logging capabilities have always lacked features. The missing log levels have been the greatest pain point in my daily developer life. More importantly, the ability to send structured data to analysis tools is crucial for production systems. Until now, teams have had to use different kinds of 3rd party libraries for this to happen.&lt;/p&gt;

&lt;p&gt;Now, the slog package fixes these shortcomings, with the ability to handle and merge the data from existing structured logging tools. The presentation revealed how the team refined the requirements for the new package together with the developer community. Also, it was fascinating to hear which kind of memory management tricks the team used, as the performance requirements for logging are demanding.&lt;/p&gt;

&lt;p&gt;Another exciting presentation handled also the capability of solving problems quickly, but instead of logs, the emphasis was on tracing. Tracing provides a more detailed view of the program's data flow than logs and is especially useful when detecting performance bottlenecks. Konstantin Ostrovsky described how their team is using &lt;a href="https://opentelemetry.io/docs/instrumentation/go/getting-started/"&gt;OpenTelemetry&lt;/a&gt; to add traceability to incoming requests. Using this approach, they do not need other logs in their codebase (excluding errors).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--kSD5BB3g--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/keuvdi3uzjnxnrlazck8.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--kSD5BB3g--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/keuvdi3uzjnxnrlazck8.jpeg" alt="Konstantin" width="800" height="600"&gt;&lt;/a&gt; &lt;em&gt;Konstantin Ostrovsky presenting OpenTelemetry usage with Go.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;OpenTelemetry tracing uses &lt;a href="https://opentelemetry.io/docs/concepts/signals/traces/#spans"&gt;the concept of spans&lt;/a&gt; in the code. One can utilize the spans to store the request data parameters and call relationships. Different analysis tools can then visualize this data for a single request. According to Konstantin, these visualizations help developers solve problems faster than searching and filtering ordinary logs. However, in the presentation Q&amp;amp;A, he reminded us that one should use the spans sparingly for performance reasons.&lt;/p&gt;

&lt;h2&gt;
  
  
  Service Weaver
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://serviceweaver.dev/"&gt;Service Weaver&lt;/a&gt; is an open-source project that another Google developer, Robert Grandl, presented at the conference. The tool allows one to develop a system as a monolith, as a single binary, but the underlying framework splits the components into microservices in deployment time. Therefore, development is easier when you do not have to worry about setting up the full microservices architecture on your local machine. In addition, the deployment might be more straightforward when you can work at a higher level.&lt;/p&gt;

&lt;p&gt;I participated in &lt;a href="https://github.com/serviceweaver/workshops"&gt;a workshop&lt;/a&gt; that allowed participants to try the Service Weaver hands-on. The target was to build a full-blown web application with a web UI and a backend service from which the UI could request data. Other sections described testing the weaver components, routing from one service to another, and even calling external 3rd party services.&lt;/p&gt;

&lt;p&gt;The workshop suited me well; I could learn more than just listening to a presentation. Furthermore, the topic interested me, and I will dig into it more in the coming days to better understand which kind of projects would benefit from this kind of development model the most. The workshop organizer promises that Google will not stop investing in the product. They are searching for collaborators to get more feedback to develop the product further.&lt;/p&gt;

&lt;h2&gt;
  
  
  UI development with Go
&lt;/h2&gt;

&lt;p&gt;Another topic that got my interest was a discussion group for UI development with Go. Andrew Williams hosted this discussion and presented a project called &lt;a href="https://fyne.io/"&gt;Fyne&lt;/a&gt; that allows Gophers to write applications with graphical user interfaces for several platforms. UI development is not my favorite thing to spend my time on; therefore, I am always curious to find better, more fun ways to implement the mandatory user-clickable parts. Using Go would undoubtedly click the fun box. So, I added another technology experiment to my TODO list.&lt;/p&gt;

&lt;p&gt;In addition to these three themes, one session that handled JWT security was also memorable. Patrycja Wegrzynowicz hacked the audience live with the help of a small sample application she had built for this purpose. It demonstrated which kind of vulnerabilities we Go developers might have in our JWT implementations.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--tXNNtC8y--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8wtzvzu87kdkiltaoo2k.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--tXNNtC8y--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8wtzvzu87kdkiltaoo2k.jpeg" alt="Patrycja" width="800" height="600"&gt;&lt;/a&gt; &lt;em&gt;Patrycja hacking the audience with JWTs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The presentation was informative and entertaining with the live hacking, and the audience understood the problems well due to the hands-on examples. The session proved that no well-known battle-tested documentation exists on handling the JWTs. We have (too) many different libraries with different qualities, and it is easy to make mistakes with the token generation and validation. No wonder the audience asked for a book on the subject from Patrycja – we need better resources for a topic as important as this.&lt;/p&gt;

&lt;h2&gt;
  
  
  See you in Florence
&lt;/h2&gt;

&lt;p&gt;Overall, the event was well-organized, had a great atmosphere, and was fun to visit. Meeting fellow Gophers, especially &lt;a href="https://www.womenwhogo.org/"&gt;the Women Who Go&lt;/a&gt; members, was a unique treat. Let's set up our chapter in Finland soon. (If you are a Finland-based woman who writes Go code, please reach out!) I also got to spend some free time in London and share the World Cup final atmosphere with the English supporters cheering for their team.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--3Z4XaNds--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tdkcrr26jcryoej1o9r6.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3Z4XaNds--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tdkcrr26jcryoej1o9r6.jpeg" alt="Public viewing" width="800" height="600"&gt;&lt;/a&gt; &lt;em&gt;Public viewing event for the World Cup final.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Bye til the next event; I hope we meet in &lt;a href="https://golab.io/"&gt;Florence&lt;/a&gt; in November! In the meantime, check out videos of the GopherCon UK 2023 sessions once they are published - I will do the same for the ones I missed live!&lt;/p&gt;

</description>
      <category>go</category>
      <category>speaking</category>
      <category>opensource</category>
      <category>diversity</category>
    </item>
    <item>
      <title>Listing Organisation Contributors</title>
      <dc:creator>Laura Vuorenoja</dc:creator>
      <pubDate>Tue, 23 May 2023 13:53:30 +0000</pubDate>
      <link>https://dev.to/lauravuo/listing-organisation-contributors-3ap9</link>
      <guid>https://dev.to/lauravuo/listing-organisation-contributors-3ap9</guid>
      <description>&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;GitHub Action for easily generating a markdown file that lists all organisation contributors. The file can be linked e.g. to GitHub organisation profile page.&lt;/p&gt;

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

&lt;p&gt;Maintainer Must-Haves&lt;/p&gt;

&lt;h3&gt;
  
  
  App Link
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/lauravuo/fetch-contributors-action"&gt;https://github.com/lauravuo/fetch-contributors-action&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Screenshots
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Result markdown file
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--T04nBMPC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ra5ogx6y5opygj9sq30e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--T04nBMPC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ra5ogx6y5opygj9sq30e.png" alt="Image description" width="800" height="519"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  The contributors file can be linked to organisation profile
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--tTrI0PtV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/verzw29hyfu0d5sgvvq9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--tTrI0PtV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/verzw29hyfu0d5sgvvq9.png" alt="Image description" width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Scheduled action updates the data regularly
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--5IScEW_n--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k07z2cx6pwepaaj0u726.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--5IScEW_n--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k07z2cx6pwepaaj0u726.png" alt="Image description" width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Description
&lt;/h3&gt;

&lt;p&gt;The action utilizes GitHub API for fetching the contributor data for all organisation repositories. It then generates a markdown file for the data and stores the result file to the repository.&lt;/p&gt;

&lt;p&gt;The organisation maintainers can integrate this action to organisation's &lt;code&gt;.github&lt;/code&gt;-repository so that the contributors data can be linked or even listed directly in the organisation's profile page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Link to Source Code
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/lauravuo/fetch-contributors-action"&gt;https://github.com/lauravuo/fetch-contributors-action&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Permissive License
&lt;/h3&gt;

&lt;p&gt;Apache 2.0&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;I have wanted to learn more about GitHub API for a while. In one open-source project, there became a need to list the organization's contributors to determine who has been making code contributions. The contributors can differ from the organization members, so the organization member listing does not give this information directly. This need was a perfect excuse to learn about the GitHub API more.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I built it
&lt;/h3&gt;

&lt;p&gt;I built a Javascript GitHub Action and learned a lot about crafting GitHub Actions using Typescript, and how to test them. I also know now more about the GitHub API in general.&lt;/p&gt;

</description>
      <category>githubhack23</category>
    </item>
    <item>
      <title>Getting Started with SSI Service Agent Development</title>
      <dc:creator>Laura Vuorenoja</dc:creator>
      <pubDate>Wed, 08 Feb 2023 11:47:26 +0000</pubDate>
      <link>https://dev.to/lauravuo/getting-started-with-ssi-service-agent-development-2jp7</link>
      <guid>https://dev.to/lauravuo/getting-started-with-ssi-service-agent-development-2jp7</guid>
      <description>&lt;p&gt;&lt;em&gt;Self-sovereign identity sounds like an exciting concept for most, but starting with the development may seem overwhelming. I have gathered simple samples that get you full speed towards integrating the SSI functionality into your application.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The text was originally published in &lt;a href="https://findy-network.github.io/blog/2023/01/30/getting-started-with-ssi-service-agent-development/"&gt;the Findy Agency project blog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the new SSI world, we craft digital services according to the self-sovereign identity model. We will have applications that issue credentials for the data they possess and applications that can verify these credentials. The central entity is the digital wallet owner that can hold these credentials and present them when needed.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Ok, sounds great!"&lt;/em&gt; you may think. &lt;em&gt;"I want to utilize credentials also in my web application. But where to start?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Developing decentralized applications is tricky as it usually requires setting up multiple applications on your local computer or acquiring access to services set up by others. Using Findy Agency tackles this hurdle. It is an SSI solution that offers a complete set of tools for managing your digital wallet and agent via a user interface or an API.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--1hZk4MgO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jq6lrr99gsvacuyd3yd7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--1hZk4MgO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jq6lrr99gsvacuyd3yd7.png" alt="Findy Agency provides tools for playing each role in the trust triangle: CLI and API clients&amp;lt;br&amp;gt;
have the complete tool pack, and the web wallet user can currently hold and prove credentials." width="880" height="495"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Findy Agency provides tools for playing each role in the trust triangle: CLI and API clients have the complete tool pack, and the web wallet user can currently hold and prove credentials.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The Agency tooling provides you with &lt;a href="https://github.com/findy-network/findy-wallet-pwa"&gt;a web wallet&lt;/a&gt; and a CLI tool that you can use to test your service's issuing and verifying features. You can easily setup the whole Findy Agency software to your local computer &lt;a href="https://github.com/findy-network/findy-wallet-pwa/tree/dev/tools/env#agency-setup-for-local-development"&gt;using Docker containers and a simulated ledger&lt;/a&gt;. Or, if you have an agency cloud installation available, you can utilize it for your service agent development without using any extra proxies or network tools.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"So, I have the agency up and running. What next?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; Take a look at the examples found in &lt;a href="https://github.com/findy-network/identity-hackathon-2023"&gt;the sample repository&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;The sample repository provides simple yet comprehensive examples to start issuing and verifying using the CLI tool or with the agency API. The easiest path is to start with the CLI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the CLI
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/findy-network/findy-agent-cli"&gt;"findy-agent-cli"&lt;/a&gt; is a command-line tool that provides all the required agent manipulation functionality. It provides means to quickly test out&lt;br&gt;
the issuing and verifying &lt;em&gt;before writing any code&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/findy-network/identity-hackathon-2023/tree/master/cli#cli-example"&gt;The sample script&lt;/a&gt; is a good starting point. It shows how to allocate an agent in the cloud and issue and verify credentials using a simple chatbot. You can run it by cloning &lt;a href="https://github.com/findy-network/identity-hackathon-2023"&gt;the repository&lt;/a&gt; and following the instructions in the README.&lt;/p&gt;

&lt;h3&gt;
  
  
  CLI Script Initialization Phase
&lt;/h3&gt;

&lt;p&gt;The sample script initialization phase allocates a new agent from the agency (&lt;strong&gt;1&lt;/strong&gt;) and authenticates the CLI user (&lt;strong&gt;2-3&lt;/strong&gt;). The authentication returns a JWT token exposed to the script environment so that further CLI calls can utilize it automatically.&lt;/p&gt;

&lt;p&gt;For the agent to issue credentials, an applicable schema needs to exist. The schema describes the contents of a credential, i.e., which attributes the credential contains. The sample script creates a schema "foobar" with a single attribute "foo" (&lt;strong&gt;4-5&lt;/strong&gt;).&lt;/p&gt;

&lt;p&gt;There needs to be more than the mere schema for the issuing process; the agent needs also to create and publish its credential definition (&lt;strong&gt;6-7&lt;/strong&gt;) attached to the created schema so that it can issue credentials and verifiers can verify the proof presentations against the published credential definition.&lt;/p&gt;

&lt;p&gt;We assume that the holder operates a web wallet and &lt;a href="https://github.com/findy-network/findy-wallet-pwa#registerlogin"&gt;has taken it into use&lt;/a&gt;. Note that you can play the holder role also with the CLI tool.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---CMPYDGK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ui6wyddinrkns205vjub.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---CMPYDGK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ui6wyddinrkns205vjub.png" alt="Sequence 1" width="880" height="786"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  CLI Script Issuing Credential
&lt;/h3&gt;

&lt;p&gt;The next task is to create a pairwise connection between the agent operated by the CLI user and the web wallet user. The pairwise connection is an encrypted pipe between the two agents that they can use to exchange data securely. The CLI script creates an invitation (&lt;strong&gt;1-2&lt;/strong&gt;) and prints it out (&lt;strong&gt;3&lt;/strong&gt;) as a QR code that the web wallet user can read (&lt;strong&gt;5&lt;/strong&gt;).&lt;/p&gt;

&lt;p&gt;Once the new connection ID is known, the CLI script starts a chatbot (&lt;strong&gt;4&lt;/strong&gt;) for the new connection. The bot logic follows the rules for changing the bot states in the YAML file. Therefore, the bot handles the rest of the issuing process (&lt;strong&gt;6-7&lt;/strong&gt;).&lt;/p&gt;

&lt;p&gt;Once the issuer bot notices that credential issuing succeeded, it stops the bot (&lt;strong&gt;10-11&lt;/strong&gt;), and the sample script moves on to verifying the same credential.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--FLe_qahj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x8qvt0km82gpdjvbkdsj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--FLe_qahj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x8qvt0km82gpdjvbkdsj.png" alt="Sequence 2" width="880" height="833"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  CLI Script Verifying Credential
&lt;/h3&gt;

&lt;p&gt;Steps &lt;strong&gt;1-6&lt;/strong&gt; proceed similarly to the issuing: first, the agents form a new pairwise connection. However, the process continues with a proof request sent by the verifier bot (&lt;strong&gt;7&lt;/strong&gt;). The proof request&lt;br&gt;
contains the attributes the bot wishes the holder to present. The web wallet user sees the requested data once they receive the message (&lt;strong&gt;8&lt;/strong&gt;), and they can either accept or reject the request.&lt;/p&gt;

&lt;p&gt;After the proof is accepted (&lt;strong&gt;9&lt;/strong&gt;), the agency verifies it cryptographically. If the verification succeeds, the agency notifies the verifier bot with the proof values (&lt;strong&gt;10&lt;/strong&gt;). It can reject the proof if the values are not acceptable by the business logic. The sample bot accepts all attribute values, so the verifying process is continued without extra validation (&lt;strong&gt;11&lt;/strong&gt;). The bot exits when the proof is completed (&lt;strong&gt;12-13&lt;/strong&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--sYvOVnn_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/07t5ij8fbpfklr5x4rh7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--sYvOVnn_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/07t5ij8fbpfklr5x4rh7.png" alt="Sequence 3" width="880" height="933"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  CLI as a Test Tool
&lt;/h3&gt;

&lt;p&gt;Note that you can also utilize the CLI for testing. It is an excellent tool to simulate the functionality on the other end.&lt;/p&gt;

&lt;p&gt;For instance, let’s say you are developing an issuer service. You can use the CLI tool to act as the holder client and to receive the credential. Or you can use the web wallet to hold the credential and create another client with the CLI tool to verify the issued data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback
&lt;/h2&gt;

&lt;p&gt;The CLI sample script presented above demonstrates all the core features of verified data flow. It should make you well-equipped to play around with the CLI tool yourself!&lt;/p&gt;

&lt;p&gt;Your tool pack will extend even more with our next blog posts. They will describe how to use the agency API programmatically and dive deep into crafting verified data supporting chatbot state machines.&lt;/p&gt;

&lt;p&gt;Let us know if you have any feedback regarding&lt;br&gt;
the Findy Agency functionality or documentation. You can reach us, for example &lt;a href="https://github.com/findy-network/findy-agent/issues"&gt;creating an issue&lt;/a&gt; or &lt;a href="https://github.com/findy-network/findy-agent/discussions"&gt;starting a discussion&lt;/a&gt; in GitHub.&lt;/p&gt;

&lt;p&gt;You can also reach me via these SoMe channels: &lt;a href="https://github.com/lauravuo/"&gt;GitHub&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/lauravuorenoja/"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://fosstodon.org/@lauravuo"&gt;Mastodon&lt;/a&gt;, and &lt;a href="https://twitter.com/vuorenoja"&gt;Twitter&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Good luck on your journey into the SSI world!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>identity</category>
      <category>ssi</category>
      <category>tutorial</category>
      <category>verifiablecredentials</category>
    </item>
    <item>
      <title>GitHub Actions Reporting My ❤️ Music</title>
      <dc:creator>Laura Vuorenoja</dc:creator>
      <pubDate>Wed, 08 Dec 2021 13:50:27 +0000</pubDate>
      <link>https://dev.to/lauravuo/github-actions-reporting-my-music-8</link>
      <guid>https://dev.to/lauravuo/github-actions-reporting-my-music-8</guid>
      <description>&lt;p&gt;This project is a PoC for utilizing GitHub Action workflows for static website generation.&lt;/p&gt;

&lt;p&gt;The idea is to create static websites with low or zero coding. The project generates &lt;a href="https://lauravuo.github.io/my-heart-music/"&gt;a website&lt;/a&gt; that lists my monthly top tracks from Spotify. &lt;/p&gt;

&lt;h3&gt;
  
  
  My Workflow
&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/lauravuo"&gt;
        lauravuo
      &lt;/a&gt; / &lt;a href="https://github.com/lauravuo/my-heart-music"&gt;
        my-heart-music
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      PoC project for utilizing static website generation through GitHub actions workflow.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;h1&gt;
my-❤️-music&lt;/h1&gt;
&lt;p&gt;This project is a PoC for utilizing GitHub Action workflows for static website generation.&lt;/p&gt;
&lt;p&gt;The idea is to create static websites with low or zero coding:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fetch data from an API (in the PoC case, &lt;a href="https://developer.spotify.com/console/get-current-user-top-artists-and-tracks/" rel="nofollow"&gt;Spotify top tracks&lt;/a&gt;) and export the data to markdown&lt;/li&gt;
&lt;li&gt;Append the generated  markdown to a &lt;a href="https://gohugo.io/" rel="nofollow"&gt;Hugo&lt;/a&gt; static website project&lt;/li&gt;
&lt;li&gt;Build the Hugo site and deploy it to GitHub pages.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href="https://lauravuo.github.io/my-heart-music/" rel="nofollow"&gt;The result site&lt;/a&gt; displays my top tracks from Spotify with &lt;a href="https://github.com/panr/hugo-theme-terminal"&gt;Hugo Terminal theme&lt;/a&gt;. The page is updated with a scheduled GitHub action, once per week.&lt;/p&gt;
&lt;p&gt;A similar model could be easily used to generate different kind of sites, by variating the data sources and Hugo themes.&lt;/p&gt;
&lt;h2&gt;
Action for Fetching Data&lt;/h2&gt;
&lt;p&gt;The inspiration to use the Spotify API for this project came to me when browsing Spotify related actions in GitHub Actions Marketplace. &lt;a href="https://github.com/marketplace/actions/spotify-box"&gt;Spotify Box&lt;/a&gt; is an action that fetches the user's…&lt;/p&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/lauravuo/my-heart-music"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The repository includes the static website skeletons. It utilizes different GitHub actions to generate the website.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fetch data from an API (in the PoC case, &lt;a href="https://developer.spotify.com/console/get-current-user-top-artists-and-tracks/"&gt;Spotify top tracks&lt;/a&gt;) and export the data to markdown&lt;/li&gt;
&lt;li&gt;Append the generated  markdown to a &lt;a href="https://gohugo.io/"&gt;Hugo&lt;/a&gt; static website project&lt;/li&gt;
&lt;li&gt;Build the Hugo site and deploy it to GitHub pages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://lauravuo.github.io/my-heart-music/"&gt;The result site&lt;/a&gt; displays my top tracks from Spotify with &lt;a href="https://github.com/panr/hugo-theme-terminal"&gt;Hugo Terminal theme&lt;/a&gt;. The page is updated with a scheduled GitHub action, once per week.&lt;/p&gt;

&lt;p&gt;A similar model could be easily used to generate different kind of sites, by variating the data sources and Hugo themes.&lt;/p&gt;

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

&lt;p&gt;Wacky Wildcards&lt;/p&gt;

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

&lt;h4&gt;
  
  
  Action for Fetching Data
&lt;/h4&gt;

&lt;p&gt;The inspiration to use the Spotify API for this project came to me when browsing Spotify related actions in GitHub Actions Marketplace. &lt;a href="https://github.com/marketplace/actions/spotify-box"&gt;Spotify Box&lt;/a&gt; is an action that fetches the user's top tracks and updates those to a Gist. I took spotify-box as the basis and modified &lt;a href="https://github.com/lauravuo/spotify-box"&gt;my fork&lt;/a&gt; according to my needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Instead of exporting the result to a Gist, I export the API result to json (&lt;code&gt;tracks.json&lt;/code&gt;) and markdown (&lt;code&gt;tracks.md&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;I removed unneeded dependencies and added &lt;code&gt;node_modules&lt;/code&gt; to version control, so that also external projects (other than the action repository) can easily take the action in use.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The use of the action is easy, the trickiest part is to acquire the needed Spotify keys and tokens. Luckily, spotify-box authors have described this process &lt;a href="https://github.com/marketplace/actions/spotify-box#1-create-new-spotify-application"&gt;very detailed&lt;/a&gt;. The keys and tokens need to be added as secrets to the repository using the action, so that they are not exposed accidentally when the action is run.&lt;/p&gt;

&lt;p&gt;After the API action is run, the result data files are saved and pushed to this repository. Markdown file is copied to target path &lt;code&gt;hugo-site/content/index.md&lt;/code&gt;. It will be the source markdown for the site's landing page.&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;updateTopTracks&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;lauravuo/spotify-box@main&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;SPOTIFY_CLIENT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SPOTIFY_CLIENT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;SPOTIFY_CLIENT_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SPOTIFY_CLIENT_SECRET }}&lt;/span&gt;
          &lt;span class="na"&gt;SPOTIFY_REFRESH_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SPOTIFY_REFRESH_TOKEN }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;git config --global user.email "spotify-bot"&lt;/span&gt;
          &lt;span class="s"&gt;git config --global user.name "spotify-bot"&lt;/span&gt;
          &lt;span class="s"&gt;cp tracks.md hugo-site/content/index.md&lt;/span&gt;
          &lt;span class="s"&gt;git add tracks.*&lt;/span&gt;
          &lt;span class="s"&gt;git commit -a -m "Add latest tracks."&lt;/span&gt;
          &lt;span class="s"&gt;git push&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Deploying the Hugo Site
&lt;/h4&gt;

&lt;p&gt;If the data fetch from previous step succeeds, the workflow continues by building the static website with Hugo. Hugo is setup using action &lt;a href="https://github.com/marketplace/actions/hugo-setup"&gt;peaceiris/actions-hugo&lt;/a&gt;. When the files are ready, the result is published to GitHub pages, using another GitHub action, &lt;a href="https://github.com/marketplace/actions/github-pages-action"&gt;peaceiris/actions-gh-pages&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="na"&gt;deploy&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;updateTopTracks&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-20.04&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="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;main'&lt;/span&gt;
          &lt;span class="na"&gt;submodules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;recursive&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&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;Setup Hugo&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;peaceiris/actions-hugo@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;hugo-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;latest'&lt;/span&gt;
          &lt;span class="na"&gt;extended&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;Build&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;cd hugo-site &amp;amp;&amp;amp; hugo --minify&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&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;peaceiris/actions-gh-pages@v3&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;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;publish_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./hugo-site/public&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Used tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://gohugo.io/"&gt;Hugo&lt;/a&gt; static site generator&lt;/li&gt;
&lt;li&gt;Hugo &lt;a href="https://github.com/panr/hugo-theme-terminal"&gt;Terminal&lt;/a&gt; theme&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/marketplace/actions/spotify-box"&gt;spotify-box&lt;/a&gt; GitHub action&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/marketplace/actions/hugo-setup"&gt;Hugo setup&lt;/a&gt; Github action&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/marketplace/actions/github-pages-action"&gt;Github pages deploy action&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>actionshackathon21</category>
    </item>
    <item>
      <title>MacGyvering the OS X Menu Bar</title>
      <dc:creator>Laura Vuorenoja</dc:creator>
      <pubDate>Sun, 06 Dec 2020 09:40:41 +0000</pubDate>
      <link>https://dev.to/lauravuo/macgyvering-the-os-x-menu-bar-5d66</link>
      <guid>https://dev.to/lauravuo/macgyvering-the-os-x-menu-bar-5d66</guid>
      <description>&lt;p&gt;What I love about today's desktop tools such as Slack, Telegram, etc. is that they enable me to execute tasks in the context I am already in without opening yet another program. Besides, I can run my apps through them and there is no need to build a separate graphical or command-line user interface. However, when I am the only one in need of the tool, building a slackbot or similar app that can be accessed from the web is quite an overkill.&lt;/p&gt;

&lt;p&gt;Fortunately, a while back a colleague of mine from work reminded me that there exists a handy framework for adding custom functionality to the OS X menu bar, &lt;a href="https://github.com/matryer/bitbar"&gt;BitBar&lt;/a&gt;. BitBar works by running scripts or programs and presenting their output in the menu bar. So actually one can add a self-written program to the menu bar without the need to write any Objective-C code or knowledge for OS X programming.&lt;/p&gt;

&lt;p&gt;I thought that this kind of custom menu bar button would be quite handy to execute some of my chores, like refreshing the content of &lt;a href="https://dev.to/levelupkoodarit/diy-christmas-radio-31k4"&gt;my Christmas radio&lt;/a&gt; I coded recently. Wouldn't it be just cool to trigger my Christmas radio update flow from the menu?&lt;/p&gt;

&lt;p&gt;So I decided to try out BitBar. Here are the steps in case you would like to try something similar. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;BitBar installation via brew cask&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https://brew.sh/"&gt;Homebrew&lt;/a&gt; is an OS X package manager and its cask extension is intended specifically for GUI application management.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Open new Terminal window and run:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
brew &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--cask&lt;/span&gt; bitbar

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


&lt;p&gt;Output should look like something similar to this:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
Updating Homebrew...
...
&lt;span class="o"&gt;==&amp;gt;&lt;/span&gt; Downloading https://github.com/matryer/bitbar/releases/download/v1.9.2/BitBar-v1.9.2.zip
&lt;span class="o"&gt;==&amp;gt;&lt;/span&gt; Downloading from https://github-production-release-asset-2e65be.s3.amazonaws.com/14376285/807c40
&lt;span class="c"&gt;######################################################################## 100.0%&lt;/span&gt;
&lt;span class="o"&gt;==&amp;gt;&lt;/span&gt; Installing Cask bitbar
&lt;span class="o"&gt;==&amp;gt;&lt;/span&gt; Moving App &lt;span class="s1"&gt;'BitBar.app'&lt;/span&gt; to &lt;span class="s1"&gt;'/Applications/BitBar.app'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create a folder for your BitBar plugins. I decided to create the folder in my Christmas radio repository &lt;a href="https://github.com/lauravuo/kaneli/tree/main/bitbar"&gt;&lt;code&gt;kaneli/bitbar&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Open Finder and launch application from Applications folder&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--goZG1pxM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/lauravuo/kaneli/main/docs/bitbar01.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--goZG1pxM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/lauravuo/kaneli/main/docs/bitbar01.png" alt="Finder folder"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Answer "Open" to the confirmation dialog&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--t0fBiDls--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/lauravuo/kaneli/main/docs/bitbar02.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--t0fBiDls--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/lauravuo/kaneli/main/docs/bitbar02.png" alt="Confirmation dialog"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Browse to the plugin folder created in step 2 and tap "Use as Plugins Directory"&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--fEQBnWGy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/lauravuo/kaneli/main/docs/bitbar03.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fEQBnWGy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/lauravuo/kaneli/main/docs/bitbar03.png" alt="File dialog"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Create the script for the menu bar&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create a new script file in the plugins folder. The script &lt;a href="https://github.com/matryer/bitbar#configure-the-refresh-time"&gt;file name format&lt;/a&gt; should be defined as the following: &lt;code&gt;{name}.{time}.{ext}&lt;/code&gt; The &lt;code&gt;{time}&lt;/code&gt;part in the file name defines the menu item refresh interval.&lt;/p&gt;

&lt;p&gt;Why we need a refresh interval? For example, if you would display in the item a clock with second precision, you would want to define the time interval as &lt;code&gt;1s&lt;/code&gt;. Bitbar would call your script once per second, and the clock in the menu bar would update correctly.&lt;/p&gt;

&lt;p&gt;In my case, however, the menu content will be unchanged so I defined a long refresh interval and named my script as&lt;code&gt;kaneli.9999d.sh&lt;/code&gt; according to the app name.&lt;/p&gt;

&lt;p&gt;After creating the script it needs to be given execution rights:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="nb"&gt;chmod &lt;/span&gt;a+x kaneli.9999d.sh

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

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Write the menu bar script&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The menu bar script should echo the UI controls definitions to the standard output. Bitbar parses the output and constructs the menu based on that.&lt;/p&gt;

&lt;p&gt;If I would like to implement the clock example from the previous step, the script would be really simple:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
  &lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
  &lt;span class="nb"&gt;date&lt;/span&gt;

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


&lt;p&gt;It would just output the current time.&lt;/p&gt;

&lt;p&gt;However, my goal was to have a button in the menu bar that would show a menu with two items when clicking it. The first menu item would open &lt;a href="https://open.spotify.com/playlist/5x5mdsVit4ngNyvglqkO8f"&gt;my Christmas radio playlist&lt;/a&gt; in Spotify and the second one would fetch and update new songs to it by using the kaneli app.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; :christmas_tree:
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"---"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Open radio | href=https://open.spotify.com/playlist/5x5mdsVit4ngNyvglqkO8f"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Update kaneli | bash=~/work/github.com/lauravuo/kaneli/run.sh terminal=false"&lt;/span&gt;

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


&lt;p&gt;The first row defines the item that is shown in the menu bar. In this case, I use a Christmas tree emoji. The second row defines a separator line between the bar button and the menu items.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/matryer/bitbar#plugin-api"&gt;Options for the menu items&lt;/a&gt; are defined after the pipe &lt;code&gt;|&lt;/code&gt; character. The first menu item has the text &lt;code&gt;Open radio&lt;/code&gt; and a &lt;code&gt;href&lt;/code&gt; option with which you can define a hyperlink that is opened when clicking the item.&lt;/p&gt;

&lt;p&gt;The second item has the playlist update functionality. It will call another script in my file system that will call kaneli with the needed parameters. The option &lt;code&gt;terminal&lt;/code&gt; can be used to define if a Terminal window is opened when the command is run.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--LDQh1FXU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/lauravuo/kaneli/main/docs/bitbar04.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--LDQh1FXU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/lauravuo/kaneli/main/docs/bitbar04.png" alt="BitBar menu"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Try it!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the BitBar menu, there is &lt;code&gt;Preferences/Refresh all&lt;/code&gt; functionality that will reload your menu script if you make any changes.&lt;/p&gt;

&lt;p&gt;That's it. Try it out!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6Zg78RvF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://raw.githubusercontent.com/lauravuo/kaneli/main/docs/kaneli.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6Zg78RvF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://raw.githubusercontent.com/lauravuo/kaneli/main/docs/kaneli.gif" alt="Demo"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Check &lt;a href="https://github.com/matryer/bitbar#writing-plugins"&gt;BitBar documentation&lt;/a&gt; for more instructions. There is also &lt;a href="https://github.com/matryer/bitbar-plugins"&gt;a bunch of plugins&lt;/a&gt; written by the community that you can just download and use. &lt;/p&gt;

&lt;p&gt;&lt;span&gt;Cover photo by &lt;a href="https://unsplash.com/@hnhmarketing?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText"&gt;Hunter Haley&lt;/a&gt; on &lt;a href="https://unsplash.com/s/photos/tools?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText"&gt;Unsplash&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>showdev</category>
      <category>bash</category>
      <category>productivity</category>
    </item>
    <item>
      <title>DIY Christmas Radio</title>
      <dc:creator>Laura Vuorenoja</dc:creator>
      <pubDate>Sun, 29 Nov 2020 17:32:30 +0000</pubDate>
      <link>https://dev.to/levelupkoodarit/diy-christmas-radio-31k4</link>
      <guid>https://dev.to/levelupkoodarit/diy-christmas-radio-31k4</guid>
      <description>&lt;p&gt;Many people who are starting their programming journey often wonder where to get the ideas for their side projects. My advice is that work with a topic you are passionate about. For example, if you love cooking, make yourself a cookbook service or perhaps a digital egg timer. Or if you like to wander in nature, code an app that tracks your routes or helps you to identify which bird is singing. You will have an extra motivational boost when solving the problem will benefit you also otherwise.&lt;/p&gt;

&lt;p&gt;"The thing" for me has always been music, so most of my side projects have been related to music in a way or another. One of my favorite apps is Spotify and luckily for me, they have published excellent APIs that one can utilize versatilely in their projects.&lt;/p&gt;




&lt;p&gt;So it is no surprise that my latest project is also built on Spotify API. As Christmas is getting closer, my daily listening queue is filled with Christmas tunes. However, I tend to listen to the same songs every Christmas. This year I decided to find some new favorites.&lt;/p&gt;

&lt;p&gt;We have an excellent internet radio for Christmas songs in Finland called &lt;a href="https://www.jouluradio.fi/" rel="noopener noreferrer"&gt;Jouluradio&lt;/a&gt;. The only problem when listening to Jouluradio is if you like some new song you hear, saving it for later listening is quite cumbersome. You need to find the track information from the Jouluradio web page and manually search the track from Spotify.&lt;/p&gt;

&lt;p&gt;Fortunately, Jouluradio publishes its playlist of the last 20 songs on the service web page. I decided to make "a radio app" that grabs this information. If the tracks are found in Spotify, they are added to my personal Christmas radio playlist and when playing it I can easily save the ones I like for later listening.&lt;/p&gt;




&lt;p&gt;My radio app, named &lt;a href="https://github.com/lauravuo/kaneli" rel="noopener noreferrer"&gt;Kaneli&lt;/a&gt; (cinnamon in Finnish) is a command-line program written in Golang. The trickiest part of the project was to implement the user OAuth 2.0 authentication to Spotify that I wrote about in &lt;a href="https://dev.to/lauravuo/how-to-oauth-from-the-command-line-47j0"&gt;my last week's post&lt;/a&gt;. The user authentication and authorization part is required for the app to have permission to add tracks to the user-owned Spotify playlist.&lt;/p&gt;

&lt;p&gt;Otherwise, the program is mainly about fetching and posting JSON to various endpoints.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F6a9xgtdzt7newf9kf0ya.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F6a9xgtdzt7newf9kf0ya.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;API providers and app interaction flow&lt;/em&gt;&lt;/p&gt;



&lt;p&gt;When the songs' information is acquired from Jouluradio, the app searches from Spotify if the track is found there. The Spotify &lt;a href="https://developer.spotify.com/documentation/web-api/reference/search/search/" rel="noopener noreferrer"&gt;search API endpoint&lt;/a&gt; provides handy tools for this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;
&lt;span class="o"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;track&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PageLastPlayed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RecentlyPlayed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Songs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryEscape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"artist:%s track:%s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;track&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Artist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;track&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Song&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="c"&gt;// search for song by artist and title&lt;/span&gt;
  &lt;span class="n"&gt;trackResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trackErr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;doGetRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.spotify.com/v1/search?type=track&amp;amp;q=%s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;bearerHeader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;trackErr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Unable to fetch track data %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;continue&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;trackData&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;SpotifyResponse&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unmarshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trackResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;trackData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Unable to parse track data %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;continue&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c"&gt;// just pick the first found track&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trackData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tracks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;trackData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tracks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Add track %s: %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;track&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Artist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;track&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Song&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;songIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;songIds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;URI&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;removeIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;removeIds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;SpotifyRemoveItem&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;URI&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;URI&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="o"&gt;...&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The search query is done for each track using the artist and song title information. If the search returns results, the first found result id is saved for later use.&lt;/p&gt;




&lt;p&gt;When the looping is done, it's time to add the results to the user's playlist. For this Spotify provides &lt;a href="https://developer.spotify.com/documentation/web-api/reference/playlists/add-tracks-to-playlist/" rel="noopener noreferrer"&gt;a playlist API endpoint&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In this phase, I had one problem though: how to prevent the program from adding duplicate tracks. First I tried to find an API that would allow me to search if the playlist contains the specific track already. But luckily, there didn't exist this kind of API. Instead, I figured out a more efficient way of avoiding the duplicates: before adding the items, the program would make a delete request for the tracks that are about to be added.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;
&lt;span class="o"&gt;...&lt;/span&gt;

&lt;span class="n"&gt;apiPath&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.spotify.com/v1/playlists/%s/tracks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;playlistID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// first delete all tracks with similar id to avoid duplicates&lt;/span&gt;
&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doJSONRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MethodDelete&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;SpotifyPlaylistDelete&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Tracks&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;removeIds&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;bearerHeader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// then add all tracks to the start of the list&lt;/span&gt;
&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doJSONRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MethodPost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;SpotifyPlaylistModify&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;URIs&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;songIds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Position&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;bearerHeader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="o"&gt;...&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Both the delete and the add request are done by utilizing batches, so it is more efficient than removing or adding the tracks one-by-one.&lt;/p&gt;




&lt;p&gt;The codes can be found in &lt;a href="https://github.com/lauravuo/kaneli" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; if you want to take a closer look. To run the app, you need to register your application in &lt;a href="https://developer.spotify.com/dashboard/" rel="noopener noreferrer"&gt;the Spotify developer dashboard&lt;/a&gt; and check the further instructions in the repository README.&lt;/p&gt;

&lt;p&gt;The current version of the app loops through a couple of different radios dedicated to specific genres and adds the tracks to the user-defined playlist. So far I have been running the program manually at random times but I guess it would be possible to create &lt;a href="https://en.wikipedia.org/wiki/Cron" rel="noopener noreferrer"&gt;a cron job&lt;/a&gt; for it to run e.g. once per hour. That is if one would like a really extensive radio list 😄&lt;/p&gt;

&lt;p&gt;You can find my Christmas radio &lt;a href="https://open.spotify.com/playlist/5x5mdsVit4ngNyvglqkO8f?si=Qu24MltlRmKBWFoLP_xN0A" rel="noopener noreferrer"&gt;on Spotify&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;span&gt;Cover photo by &lt;a href="https://unsplash.com/@markusspiske?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Markus Spiske&lt;/a&gt; on &lt;a href="https://unsplash.com/s/photos/fm-radio?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>spotify</category>
      <category>sideprojects</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to OAuth from the Command Line</title>
      <dc:creator>Laura Vuorenoja</dc:creator>
      <pubDate>Sun, 22 Nov 2020 10:54:44 +0000</pubDate>
      <link>https://dev.to/lauravuo/how-to-oauth-from-the-command-line-47j0</link>
      <guid>https://dev.to/lauravuo/how-to-oauth-from-the-command-line-47j0</guid>
      <description>&lt;p&gt;This week I came up with an idea about a personal Christmas radio. I am an eager listener of Christmas tunes and every year I try to find some new favorite songs. I decided to code me a little helper that would ease the discovery process and automatically add new Christmas songs to my chosen Spotify playlist.&lt;/p&gt;

&lt;p&gt;However, I encountered an authentication-related problem when implementing the functionality. &lt;a href="https://developer.spotify.com/documentation/general/guides/authorization-guide/" rel="noopener noreferrer"&gt;Accessing the Spotify user APIs&lt;/a&gt; (such as modifying the playlists on behalf of the user) requires permission both from Spotify and the user. The user's permission is acquired through browser interaction (user logs in to Spotify and authorizes the app in the web UI), but my app was designed to work from the command line. So the problem was how to make the command line program interact with the browser flow?&lt;/p&gt;

&lt;p&gt;The Spotify API authentication is implemented according to the popular OAuth 2.0 specification. I decided to use &lt;a href="https://tools.ietf.org/html/rfc6749#section-1.3.1" rel="noopener noreferrer"&gt;the authorization code flow&lt;/a&gt; that would suit best for my purposes. The flow has two parts: first, the client application (my radio app) directs the user to an authorization server that handles the user authentication. Then the authorization server directs the request back to the client application (to a predefined redirect URI). With this redirect is delivered a code that the client application can use for requesting the actual API access token from another endpoint.&lt;/p&gt;

&lt;p&gt;Although this protocol may sound a bit complex at first, it provides important security benefits: the user's credentials are never shared with the client application. And on the other hand, the final access token is delivered directly to the client application. This way it's not being exposed to the browser or the user and actually, only the client application can receive the token.&lt;/p&gt;

&lt;p&gt;So the solution was to use a temporary localhost webserver. The server lives only as long as the redirect endpoint is called and the authorization code is received. The following figure describes the steps:&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;The user launches the app from the command line&lt;/li&gt;
&lt;li&gt;The client program starts the temporary server&lt;/li&gt;
&lt;li&gt;The client program launches the browser to the API authentication page.&lt;/li&gt;
&lt;li&gt;The user authenticates in the browser and authorizes the client application to access the API on her behalf.&lt;/li&gt;
&lt;li&gt;The authorization server redirects the request to the predefined redirect URL (localhost).&lt;/li&gt;
&lt;li&gt;The client program parses the redirect request and receives the authorization code. &lt;/li&gt;
&lt;li&gt;The client program exchanges the authorization code to the API access token calling the authorization server endpoint.&lt;/li&gt;
&lt;li&gt;The client program receives the API access token and can make API requests on behalf of the user.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I used &lt;a href="https://golang.org/" rel="noopener noreferrer"&gt;golang&lt;/a&gt; to implement the app and the sample code is attached here:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;

&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"encoding/base64"&lt;/span&gt;
    &lt;span class="s"&gt;"encoding/json"&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"log"&lt;/span&gt;
    &lt;span class="s"&gt;"math/rand"&lt;/span&gt;
    &lt;span class="s"&gt;"net/http"&lt;/span&gt;
    &lt;span class="s"&gt;"net/url"&lt;/span&gt;
    &lt;span class="s"&gt;"os"&lt;/span&gt;

    &lt;span class="s"&gt;"github.com/pkg/browser"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;AuthResponse&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;AccessToken&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"access_token"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;fetchUserToken&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;redirectURL&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"http://localhost:4321"&lt;/span&gt;
        &lt;span class="n"&gt;spotifyLoginURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://accounts.spotify.com/authorize?client_id=%s&amp;amp;response_type=code&amp;amp;redirect_uri=%s&amp;amp;scope=%s&amp;amp;state=%s"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;clientID&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SPOTIFY_CLIENT_ID"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;clientSecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SPOTIFY_CLIENT_SECRET"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;authHeader&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Basic %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StdEncoding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EncodeToString&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clientID&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s"&gt;":"&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;clientSecret&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;clientID&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;clientSecret&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"spotify client ID and secret missing"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// authorization code - received in callback&lt;/span&gt;
    &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
    &lt;span class="c"&gt;// local state parameter for cross-site request forgery prevention&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rand&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="c"&gt;// scope of the access: we want to modify user's playlists&lt;/span&gt;
    &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;"playlist-modify-public&amp;amp;playlist-modify-private"&lt;/span&gt;
    &lt;span class="c"&gt;// loginURL&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spotifyLoginURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clientID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redirectURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// channel for signaling that server shutdown can be done&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// callback handler, redirect from authentication is handled here&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// check that the state parameter matches&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="s"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c"&gt;// code is received as query parameter&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;codes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="s"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;codes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c"&gt;// save code and signal shutdown&lt;/span&gt;
                &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;codes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="c"&gt;// redirect user's browser to spotify home page&lt;/span&gt;
        &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"https://www.spotify.com/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusSeeOther&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c"&gt;// open user's browser to login page&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OpenURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to open browser for authentication %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Addr&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;":4321"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c"&gt;// go routine for shutting down the server&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;okToClose&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;okToClose&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Shutdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to shutdown server"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}()&lt;/span&gt;
    &lt;span class="c"&gt;// start listening for callback - we don't continue until server is shut down&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListenAndServe&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="c"&gt;// authentication complete - fetch the access token&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Values&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"grant_type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"authorization_code"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"redirect_uri"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redirectURL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;doPostRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"https://accounts.spotify.com/api/token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;authHeader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;AuthResponse&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unmarshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c"&gt;// happy end: token parsed successfully&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AccessToken&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"unable to acquire Spotify user token"&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;Go provides quite nice tools for implementing this kind of concurrency handling that is needed here: the localhost server is shut down using &lt;a href="https://tour.golang.org/concurrency/1" rel="noopener noreferrer"&gt;goroutines&lt;/a&gt; and &lt;a href="https://tour.golang.org/concurrency/2" rel="noopener noreferrer"&gt;channels&lt;/a&gt;. I encourage you to check them out if Go is something new for you.&lt;/p&gt;

&lt;p&gt;So, now I have the access token. Now I need just to make the functionality for adding the songs 🙂&lt;/p&gt;

&lt;p&gt;P.S. If you want to mess around with the Spotify API, remember first to &lt;a href="https://developer.spotify.com/dashboard/" rel="noopener noreferrer"&gt;register&lt;/a&gt; your application to get the client id and secret.&lt;/p&gt;

&lt;p&gt;&lt;span&gt;Photo by &lt;a href="https://unsplash.com/@steve3p_0?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Steve Halama&lt;/a&gt; on &lt;a href="https://unsplash.com/s/photos/christmas-music?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;

</description>
      <category>oauth</category>
      <category>spotify</category>
      <category>go</category>
      <category>authentication</category>
    </item>
    <item>
      <title>Hacking the Orb</title>
      <dc:creator>Laura Vuorenoja</dc:creator>
      <pubDate>Sun, 15 Nov 2020 20:03:57 +0000</pubDate>
      <link>https://dev.to/lauravuo/hacking-the-orb-54o1</link>
      <guid>https://dev.to/lauravuo/hacking-the-orb-54o1</guid>
      <description>&lt;p&gt;I remember times when setting up CI meant long hours of server setup, Jenkins studies, XML configuration pain, and Java memory issues. No wonder continuous testing and integration was not the first thing that came to mind when starting a new project.&lt;/p&gt;

&lt;p&gt;Luckily, nowadays things are different. Services like &lt;a href="https://github.com/features/actions"&gt;GitHub Actions&lt;/a&gt;, &lt;a href="https://circleci.com/"&gt;CircleCI&lt;/a&gt; or &lt;a href="https://travis-ci.org/"&gt;Travis&lt;/a&gt; (just to name a few) are cloud services that handle the CI flows swiftly for you. Pipeline configurations can be stored neatly next to your code in the version control repository and the changes to them are picked up automatically whenever new commits are introduced. Furthermore, containerization technologies have simplified setting up external dependencies, so even complex end-2-end test environments can be configured only with a couple of lines of YAML.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And the best part is that these services are usually free for open source projects.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;However, one thing that has puzzled me over the years when using the aforementioned services is how to avoid &lt;a href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself"&gt;DRY a.k.a. Don't Repeat Yourself&lt;/a&gt; principle. Many projects have similar CI configurations especially if they are related or implemented using the same technology. In the past, I have found myself copy-pasting configurations between projects or repeating similar steps over and over again and this is something I would rather always avoid.&lt;/p&gt;

&lt;p&gt;Let's take an example from the world of Node.js. Unit testing CI process for a node project consists typically of the following steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select the base OS and OS version for the job&lt;/li&gt;
&lt;li&gt;Select (and install) Node.js version&lt;/li&gt;
&lt;li&gt;Clone the project sources&lt;/li&gt;
&lt;li&gt;(If a cache is found) load cached dependencies &lt;/li&gt;
&lt;li&gt;Install dependencies&lt;/li&gt;
&lt;li&gt;Run tests&lt;/li&gt;
&lt;li&gt;Save dependencies to cache&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Steps 1-2 may be combined if you use a system that allows the jobs to be run in a container: the base container can have all the global bells and whistles preinstalled that your project needs. But the rest of the steps you usually cannot avoid defining and actually only one, step number 6, interests me as the application or service developer. After figuring out and defining this process for the first time, I would like to be able to reuse the configuration in multiple projects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fortunately, the CI services are constantly introducing new features and most of them have enabled the sharing of common modules in a way or another.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Especially before GitHub Actions was made available for all GitHub users, my standard choice of CI was CircleCI. During last &lt;a href="https://hacktoberfest.digitalocean.com/"&gt;Hacktoberfest&lt;/a&gt; I happened to notice that CircleCI had an extra challenge for crafting &lt;a href="https://circleci.com/orbs/"&gt;orbs&lt;/a&gt;. I got intrigued and it turned out that the orbs were just the thing I had been missing a few years back: &lt;em&gt;"A reusable package of YAML configuration that condenses repeated pieces of config into a single line of code."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So I decided to give the orbs a go and replaced the unit test job in one of my personal Node.js projects with a job from an orb. The CI configuration was trimmed exactly as I wanted: I got rid of the lines related to cloning, dependencies installation, and cache handling, and all that was left was the test command. The CI configuration was trimmed by 15 lines and everything worked as before the change.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Bx2KRjuU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/zj3rmdcpmieerlatfh55.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Bx2KRjuU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/zj3rmdcpmieerlatfh55.png" alt="Change commit"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https://github.com/lauravuo/react-project-template/commit/d58224afe964fd72eea4bab0505cc8280cc0335b?branch=d58224afe964fd72eea4bab0505cc8280cc0335b&amp;amp;diff=split"&gt;Example of orb usage&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In this case, the common functionality was authored by CircleCI. &lt;a href="https://circleci.com/developer/orbs/orb/circleci/node"&gt;Node orb&lt;/a&gt; that I utilized in my personal project above provides Node.js related basic workflows related to dependency management and testing. But how about creating an own orb? The Hacktoberfest challenge was just about that, i.e. providing some meaningful CI functionality as an orb module that could be used by multiple repositories.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;As it happened, I had just come across an interesting documentation tool, &lt;a href="https://alexjs.com/"&gt;alexjs&lt;/a&gt;, that lints documentation files for insensitive and inconsiderate writing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Alex helps to find &lt;em&gt;"gender favouring, polarising, race related, religion inconsiderate, or other unequal phrasing"&lt;/em&gt;. I thought that Alex would be a good addition to many of my projects, especially as I am a non-native English speaker and take gladly all the help I can get when authoring the project documentation. Also, the use of alex requires the npm toolchain as it is implemented with Node.js. So the additional motivation to implement an orb for alex was to use it in projects based on other technologies than Node.js.&lt;/p&gt;

&lt;p&gt;CircleCI hosts &lt;a href="https://circleci.com/developer/orbs"&gt;a registry&lt;/a&gt; for all available orbs so before you can use your self-authored orb in a CI workflow, you need to publish it to the registry. The easiest way to launch your orb development is to install &lt;a href="https://circleci.com/docs/2.0/local-cli/#installation"&gt;circleci cli&lt;/a&gt; and use &lt;a href="https://circleci.com/docs/2.0/orb-author/#orb-development-kit"&gt;an orb development kit&lt;/a&gt; that sets up &lt;a href="https://github.com/CircleCI-Public/Orb-Project-Template"&gt;a project template&lt;/a&gt; for you to get started with the development. Make sure to &lt;a href="https://circleci.com/docs/2.0/local-cli/#configuring-the-cli"&gt;configure circleci cli&lt;/a&gt; first if you try to use the development kit at home.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The orb project template may seem overwhelming at first.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the source folder there are placeholders for the following sections:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;concept&lt;/th&gt;
&lt;th&gt;description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;commands&lt;/td&gt;
&lt;td&gt;steps and their parameters and mapping of those to shell scripts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;examples&lt;/td&gt;
&lt;td&gt;use-case examples, displayed in registry documentation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;executors&lt;/td&gt;
&lt;td&gt;environment in which the steps of a job will be run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jobs&lt;/td&gt;
&lt;td&gt;full jobs definitions that may combine steps even from other orbs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;scripts&lt;/td&gt;
&lt;td&gt;shell scripts that implement the actual command functionality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tests&lt;/td&gt;
&lt;td&gt;test scripts for the shell scripts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For me it took a while to get my head around these concepts in order to achieve the end result I wanted: provide a preconfigured job that would do the code checkout and linting with alex. The target was that the orb user could just import the orb and call the job in the workflow just as with Node.js testing example before.&lt;/p&gt;

&lt;p&gt;The other thing that was a bit challenging to get working at first was the orb CI pipeline. The template project is preconfigured to use CircleCI for orb linting and testing. It even packs and publishes the development version of the orb to the registry and executes the integration tests with an actually deployed version of the orb before publishing the final production version. (I'd say nicely designed flow 😊)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--bjtgVU6r--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/z7kw97o7lhfdc03r1stk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bjtgVU6r--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/z7kw97o7lhfdc03r1stk.png" alt="Testing steps in CI"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;CI execution log after successful PR merge and deployment&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;However, with some documentation browsing, detective work and just trying out things, I finally got the things working as I had planned and as a result I was able to see &lt;a href="https://circleci.com/developer/orbs/orb/lauravuo/alexjs-orb"&gt;my orb in the registry&lt;/a&gt;. One key thing was to learn how to setup the CircleCI credentials to &lt;a href="https://circleci.com/docs/2.0/contexts/#creating-and-using-a-context"&gt;the organization context&lt;/a&gt; so that CI was able to publish the orb on my behalf successfully.&lt;/p&gt;

&lt;p&gt;Using the orb is now straightforward:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="na"&gt;orbs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;alexjs-orb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lauravuo/alexjs-orb@0.0.1&lt;/span&gt;

&lt;span class="na"&gt;workflows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;lint&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="s"&gt;alexjs-orb/lint&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if all goes well, the job succeeds and logs can be observed from the CI output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;...
CHANGELOG.md: no issues found
README.md: no issues found
src/README.md: no issues found
src/commands/README.md: no issues found
src/examples/README.md: no issues found
src/executors/README.md: no issues found
src/jobs/README.md: no issues found
src/scripts/README.md: no issues found
src/tests/README.md: no issues found
src/tests/test.md: no issues found
CircleCI received &lt;span class="nb"&gt;exit &lt;/span&gt;code 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sources for my orb can be found in &lt;a href="https://github.com/lauravuo/alexjs-orb"&gt;GitHub&lt;/a&gt; in case you want to take a closer look. In fact, currently all orbs published to the orb registry are open source. There is a definite need for private orbs as well, so it is interesting to see what happens in this front in the future.&lt;/p&gt;

&lt;p&gt;What about you? Which CI system do you use and does it have similar support for shared functionality?&lt;/p&gt;

&lt;p&gt;&lt;span&gt;Cover image by &lt;a href="https://unsplash.com/@patmcmanaman?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText"&gt;Patrick McManaman&lt;/a&gt; on &lt;a href="https://unsplash.com/s/photos/circle?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText"&gt;Unsplash&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;

</description>
      <category>ci</category>
      <category>circleci</category>
      <category>alex</category>
      <category>hacktoberfest</category>
    </item>
    <item>
      <title>The Best Python Resources for Beginners</title>
      <dc:creator>Laura Vuorenoja</dc:creator>
      <pubDate>Thu, 05 Nov 2020 19:07:14 +0000</pubDate>
      <link>https://dev.to/levelupkoodarit/the-best-python-resources-for-beginners-4pb4</link>
      <guid>https://dev.to/levelupkoodarit/the-best-python-resources-for-beginners-4pb4</guid>
      <description>&lt;p&gt;My friend told me the other day that she would like to learn Python coding for data manipulation and visualization. Even though I know that there are tons of Python tutorials out there, I had no clue where to point her to. In particular, because I haven't ever learned Python properly, I might not be the best judge for evaluating Python tutorials 😄.&lt;/p&gt;

&lt;p&gt;So I decided to phone-a-friend and ask wiser people for some recommendations. As my friend has no previous background in programming, the material needed to be for absolute beginners. Here's a list of the suggestions I got:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resources in English&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.python.org/3/tutorial/"&gt;The Python Tutorial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=rfscVS0vtbw"&gt;FreeCodeCamp Video Course&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.w3schools.com/python/python_intro.asp"&gt;W3Schools tutorial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.codecademy.com/learn/learn-python-3"&gt;CodeAcademy course&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/playlist?list=PLlrxD0HtieHhS8VzuMCfQD4uJ9yne1mE6"&gt;Microsoft - Python for Beginners video course&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Resources in Finnish&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://python-s20.mooc.fi"&gt;Helsinki Uni MOOC&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fitech.io/fi/opinnot/ohjelmointi-pythonilla/"&gt;FITech Python course&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.cs.hut.fi/~ttsirkia/Python.pdf"&gt;Python course material&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cs.helsinki.fi/group/linkki/materiaali/python-perusteet/materiaali.html"&gt;Python course material (for high schools)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.cse.tkk.fi/fi/opinnot/CSE-A1121/2015/yleista/index.html"&gt;Python course material (Aalto)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Is one of your favourite tutorials or learning resources missing? Please suggest more!&lt;/p&gt;

&lt;p&gt;&lt;span&gt;Great sneak photo 🐍 by &lt;a href="https://unsplash.com/@thenightstxlker?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText"&gt;Tamara Gore&lt;/a&gt; on &lt;a href="https://unsplash.com/s/photos/python?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText"&gt;Unsplash&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>python</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Hello dev.to, here’s one more dev too 👋</title>
      <dc:creator>Laura Vuorenoja</dc:creator>
      <pubDate>Mon, 02 Nov 2020 16:23:29 +0000</pubDate>
      <link>https://dev.to/lauravuo/hello-dev-to-here-s-one-more-dev-too-1ghj</link>
      <guid>https://dev.to/lauravuo/hello-dev-to-here-s-one-more-dev-too-1ghj</guid>
      <description>&lt;p&gt;I joined the dev.to community already in the summer. Although I have been thinking about writing my experiences and learnings in tech for a while, I haven’t had the chance to start my author's career at dev until now. I got the final motivational boost to get posts out from the inspirational members of the LevelUp Koodarit community.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you are a woman or member of the gender minorities and seeking a Finnish coding community, I highly recommend you to join &lt;a href="https://www.facebook.com/groups/224556481380051/"&gt;LUK&lt;/a&gt; 😊. It is an excellent place to share information, receive, and provide help.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This literacy journey starts by introducing me. My name is Laura, and I am living in Tampere (work partially in Helsinki), Finland. I started coding when I was in my twenties. At that time I began to study at the university and chose computer studies for my major even though I had not messed around with computers much before. I was not sure what I would want to be “when growing up” and IT seemed to be a suitable choice since it was easy to get in. And of course, we had this company called Nokia that employed all available coders in its years of success.&lt;/p&gt;

&lt;p&gt;The start of my studies was bumpy: I just was not able to get my head round C++. It was the language they used for teaching all newbies back then, at the start of the millennium. Fortunately, I did not give up, and when Java and a teacher with a more pedagogic approach came along, I did not only get the hang of it, but fell in love with solving problems through code.&lt;/p&gt;

&lt;p&gt;I have made my living in coding for 15 years now. It has been an exciting journey filled with a bunch of different technologies, projects, teams, and companies. I have written code for embedded systems as well as extensive web services, from C code to Javascript. And I do like being a rolling stone in this sense, learning new is one of the best things in this job.&lt;/p&gt;

&lt;p&gt;Roughly 1,5 years ago I landed my dream job: I am currently researching emerging technologies in the largest bank and insurance company in Finland. One of my focus areas is &lt;a href="https://en.wikipedia.org/wiki/Self-sovereign_identity"&gt;self-sovereign identity&lt;/a&gt; and technologies enabling &lt;a href="https://trustoverip.org/"&gt;trust-over-IP&lt;/a&gt;. It is super exciting to design and build better ways to construct the digital society of the future.&lt;/p&gt;

&lt;p&gt;I try to be technology agnostic, have a pragmatic approach, and choose the best tool for the job. Thus I do not have strict favorites when it comes to programming languages or platforms. That is also why I tend to acquire skills as I need them: depending on the project phase and requirements, I may be writing code related to infra scripts, database queries, API interfaces, or user interfaces. Recently I have been playing around mostly with web services built with Golang, AWS, and React.&lt;/p&gt;

&lt;p&gt;Coding is part of my free time as well. I often get goofy ideas of little apps or services that ease my other hobbies or everyday life. Hopefully, I can present those projects in my future posts in more detail. Sometimes I also study new programming languages or other tech related topics. For example, this fall I have been trying to learn a little bit of Haskell.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--HxDwsC_M--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/amifqpysxpqyuoe0762j.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--HxDwsC_M--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/amifqpysxpqyuoe0762j.jpeg" alt="Me enjoying the Finnish summer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Otherwise, my free time fills up with music and nature. I love to sing: together with &lt;a href="https://kaupunginnaiset.fi/"&gt;my choir&lt;/a&gt;, with my friends in karaoke bars, and of course, alone in &lt;a href="https://en.wikipedia.org/wiki/Finnish_sauna#Finnish_sauna_customs"&gt;the sauna&lt;/a&gt;. I also love wandering in nature, and I try to do it as often as I can. My yearly highlight is a regular summer vacation trip to the Finnish Lapland to ride with my mountain bike.&lt;/p&gt;

&lt;p&gt;I am glad if you had the time to read this far. I hope I can share more of my experiences and thoughts of my projects in the future with you. It is great to be part of this community!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;P.S. The cover image is from my laptop. What do you think, is the laptop stickers a window to the developer's soul?&lt;/em&gt; 😀&lt;/p&gt;

</description>
      <category>womenintech</category>
      <category>intro</category>
      <category>bio</category>
      <category>helloworld</category>
    </item>
  </channel>
</rss>
