<?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: Sheng-Lin Yang</title>
    <description>The latest articles on DEV Community by Sheng-Lin Yang (@slyang08).</description>
    <link>https://dev.to/slyang08</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%2F3482487%2Fe76ff177-dc2a-4c36-9c8c-bd903cc08474.png</url>
      <title>DEV Community: Sheng-Lin Yang</title>
      <link>https://dev.to/slyang08</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/slyang08"/>
    <language>en</language>
    <item>
      <title>Release my PR for the project Bifrost</title>
      <dc:creator>Sheng-Lin Yang</dc:creator>
      <pubDate>Sat, 13 Dec 2025 00:26:56 +0000</pubDate>
      <link>https://dev.to/slyang08/release-my-pr-for-the-project-bifrost-3pj8</link>
      <guid>https://dev.to/slyang08/release-my-pr-for-the-project-bifrost-3pj8</guid>
      <description>&lt;p&gt;&lt;strong&gt;Final Reflection – Implementing the "Enable/Disable API Key" Feature in &lt;a href="https://github.com/maximhq/bifrost/pull/992" rel="noopener noreferrer"&gt;Bifrost&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When I first started working on this issue, I believed it would be a simple front-end modification — just adding a small component and a few backend adjustments. However, as I explored the project more deeply, I realized it was actually a full-stack feature that involved the frontend, backend, and database layers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/maximhq/bifrost" rel="noopener noreferrer"&gt;Bifrost&lt;/a&gt; is a large-scale, high-performance AI gateway written primarily in Go, without using any backend web frameworks. This gave me a great opportunity to learn how a production-grade system is architected directly in Go. On the frontend side, the project uses &lt;strong&gt;Next.js&lt;/strong&gt; with &lt;strong&gt;Shadcn UI&lt;/strong&gt;, which were both relatively new technologies for me and became an excellent learning experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  Technical Implementation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Frontend:&lt;/strong&gt;&lt;br&gt;
In &lt;code&gt;ui/app/workspace/providers/views/modelProviderKeysTableView.tsx&lt;/code&gt;, I implemented the feature using the existing &lt;code&gt;Switch&lt;/code&gt; component to let users enable or disable their API keys instead of deleting them. This small UI change significantly improves user experience by allowing temporary control over keys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend:&lt;/strong&gt;&lt;br&gt;
I updated multiple backend files to extend the data schema and handle the new &lt;code&gt;enabled&lt;/code&gt; field in logic.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Core:&lt;/strong&gt;&lt;br&gt;
In &lt;code&gt;core/bifrost.go&lt;/code&gt;, I added logic to ensure requests only use enabled keys, skipping any that are disabled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Database:&lt;/strong&gt;&lt;br&gt;
In &lt;code&gt;framework/configstore/migrations.go&lt;/code&gt;, I wrote a manual migration script to add the new &lt;code&gt;enabled&lt;/code&gt; column to the &lt;code&gt;keys&lt;/code&gt; table and set its default value to &lt;code&gt;true&lt;/code&gt;. This ensured data consistency without relying on any framework-level ORM migration tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  Collaboration and Review Process
&lt;/h3&gt;

&lt;p&gt;After pushing my pull request, I followed the automated feedback from &lt;strong&gt;CodeRabbitAI&lt;/strong&gt; and the advice from other contributors. Their suggestions helped me spot missing pieces, follow the repository's coding conventions, and refine my migration logic. These automated tools and community interactions reduced communication delays and improved both my workflow and code quality.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Throughout this contribution, I learned how powerful Go can be for backend, CLI, and microservice development. Working on Bifrost taught me how to design and implement web application features without relying on frameworks — managing SQL migrations manually and integrating logic across the full stack.  &lt;/p&gt;

&lt;p&gt;This was a valuable experience that strengthened my understanding of &lt;strong&gt;full-stack development&lt;/strong&gt;, open-source collaboration, and how modern teams maintain high-quality production systems.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>llm</category>
      <category>osd600</category>
      <category>release0dot4</category>
    </item>
    <item>
      <title>Progressing in Bifrost project</title>
      <dc:creator>Sheng-Lin Yang</dc:creator>
      <pubDate>Fri, 12 Dec 2025 23:23:33 +0000</pubDate>
      <link>https://dev.to/slyang08/progressing-in-bifrost-project-862</link>
      <guid>https://dev.to/slyang08/progressing-in-bifrost-project-862</guid>
      <description>&lt;p&gt;In the &lt;a href="https://github.com/maximhq/bifrost/issues/831" rel="noopener noreferrer"&gt;issue 831&lt;/a&gt; of &lt;a href="https://github.com/maximhq/bifrost" rel="noopener noreferrer"&gt;Bifrost&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;make dev&lt;/code&gt; starts full stack (frontend:3000 + backend:8080)
&lt;/li&gt;
&lt;li&gt;
&lt;a href="http://localhost:3000/workspace/providers" rel="noopener noreferrer"&gt;http://localhost:3000/workspace/providers&lt;/a&gt; verifies API Key page
&lt;/li&gt;
&lt;li&gt;Fork + feature branch: &lt;code&gt;git checkout -b 11-17-feat-option-disable-key&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;ui/app/workspace/providers/views/modelProviderKeysTableView.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TableCell&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Switch&lt;/span&gt;
    &lt;span class="nx"&gt;checked&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isKeyEnabled&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hasUpdateProviderAccess&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;onCheckedChange&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;updateProvider&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
          &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;index&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="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;checked&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;k&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="nf"&gt;unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Key &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;checked&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;enabled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;disabled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; successfully`&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="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Failed to update key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getErrorMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&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="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/TableCell&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;→ This is a table for the API Key. I have done about the UI and the type of schema in front-end.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;framework/configstore/tables/key.go&lt;/code&gt;:&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;TableKey&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;Enabled&lt;/span&gt;    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;     &lt;span class="s"&gt;`gorm:"default:true" json:"enabled,omitempty"`&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;k&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Enabled&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;enabled&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt; &lt;span class="c"&gt;// DB default&lt;/span&gt;
  &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Enabled&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;enabled&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;BeforeSave&lt;/span&gt; &lt;span class="n"&gt;and&lt;/span&gt; &lt;span class="n"&gt;AfterFind&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;→ SQLite schema，no enabled column; migration needed.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;core/bifrost.go&lt;/code&gt;:&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="c"&gt;// Skip disabled keys (default enabled when nil)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Enabled&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!*&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Enabled&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;→ The logic of provider selector&lt;/p&gt;

&lt;p&gt;The code in back-end seems that it could be able to set up as enabled or disabled for front-end.&lt;/p&gt;

&lt;p&gt;However, the workflow and the codeRabbit on GitHub was kept telling me there were something better to improve(like: migration the data column manually, add log in files &lt;code&gt;core/changelog.md&lt;/code&gt; and &lt;code&gt;transports/changelog.md&lt;/code&gt;) for the issue in the project. Also, I got the message about the data should migrate manually so I have to write the code in &lt;code&gt;framework/configstore/migrations.go&lt;/code&gt;.&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="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;migrationAddPluginVersionColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&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="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;// migrationAddEnabledColumnToKeyTable adds the enabled column to the config_keys table&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;migrationAddEnabledColumnToKeyTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&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;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;gorm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;migrator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;migrator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;migrator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;
        &lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"add_enabled_column_to_key_table"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Migrate&lt;/span&gt;&lt;span class="o"&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;tx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;gorm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;mg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Migrator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="c"&gt;// Check if column already exists&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;mg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasColumn&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;tables&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TableKey&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="s"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c"&gt;// Add the column&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;mg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddColumn&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;tables&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TableKey&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="s"&gt;"enabled"&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="k"&gt;return&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 add enabled column: %w"&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="c"&gt;// Set default = true for existing rows&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;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"UPDATE config_keys SET enabled = TRUE WHERE enabled IS NULL"&lt;/span&gt;&lt;span class="p"&gt;)&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="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;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 backfill enabled column: %w"&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="k"&gt;return&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;Rollback&lt;/span&gt;&lt;span class="o"&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;tx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;gorm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;mg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Migrator&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;mg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasColumn&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;tables&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TableKey&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="s"&gt;"enabled"&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="n"&gt;mg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DropColumn&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;tables&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TableKey&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="s"&gt;"enabled"&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="k"&gt;return&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 drop enabled column: %w"&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="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&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="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Migrate&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="k"&gt;return&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;"error running enabled column migration: %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="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;:&lt;br&gt;
Since working on this project that I have been followed &lt;code&gt;Coderabbitai&lt;/code&gt; and with other contributors in something that I may miss in the whole progressing.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>llm</category>
      <category>osd600</category>
      <category>release0dot4</category>
    </item>
    <item>
      <title>Planning to solve a new larger project</title>
      <dc:creator>Sheng-Lin Yang</dc:creator>
      <pubDate>Fri, 12 Dec 2025 14:09:30 +0000</pubDate>
      <link>https://dev.to/slyang08/planning-to-solve-a-new-larger-project-1m0g</link>
      <guid>https://dev.to/slyang08/planning-to-solve-a-new-larger-project-1m0g</guid>
      <description>&lt;p&gt;I chose the project with &lt;a href="https://github.com/maximhq/bifrost" rel="noopener noreferrer"&gt;Bifrost&lt;/a&gt; because the description as &lt;code&gt;Fastest LLM gateway (50x faster than LiteLLM) with adaptive load balancer, cluster mode, guardrails, 1000+ models support &amp;amp; &amp;lt;100 µs overhead at 5k RPS.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The current issue: &lt;br&gt;
In the Bifrost Web UI, API key management only supports "add/delete." When users want to pause a key (keep the configuration but stop using it), they can only delete and recreate it, which leads to weak User Experience.&lt;/p&gt;

&lt;p&gt;In this issue that the solution may be:&lt;br&gt;
&lt;strong&gt;Frontend&lt;/strong&gt;: Toggle Switch + Disabled status display (&lt;code&gt;ui/&lt;/code&gt;)&lt;br&gt;
&lt;strong&gt;Backend&lt;/strong&gt;: Add &lt;code&gt;Enabled: *bool&lt;/code&gt; field, HTTP handler filtering (&lt;code&gt;transports/bifrost-http/&lt;/code&gt;)&lt;br&gt;
&lt;strong&gt;Core&lt;/strong&gt;: Provider selector skips disabled keys (&lt;code&gt;core/bifrost.go&lt;/code&gt;)&lt;br&gt;
&lt;strong&gt;Database&lt;/strong&gt;: Configstore schema migration (&lt;code&gt;framework/configstore/&lt;/code&gt;)&lt;br&gt;
&lt;strong&gt;Real-time sync&lt;/strong&gt;: Toggle takes effect immediately, no restart needed&lt;/p&gt;

&lt;p&gt;I have worked on multiple open-source CLI tools (including LLM integrations) and WebSocket websites, but this is my first challenge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High-performance Go + Next.js architecture (68% Go + 23% TypeScript)&lt;/li&gt;
&lt;li&gt;Production-grade LLM gateway (1300+ stars, actively maintained)&lt;/li&gt;
&lt;li&gt;Starting from the frontend and then gradually going deeper into the backend core.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Learning Value:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go pure HTTP handler implementation (core/)&lt;/li&gt;
&lt;li&gt;Next.js + Go backend integration patterns&lt;/li&gt;
&lt;li&gt;Bifrost configstore architecture (framework/configstore)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;:&lt;br&gt;
It is not only adjust the User Interface on the web page but also need to modify the core code.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>osd600</category>
      <category>release0dot4</category>
    </item>
    <item>
      <title>Release my cli tool on npm</title>
      <dc:creator>Sheng-Lin Yang</dc:creator>
      <pubDate>Fri, 21 Nov 2025 21:16:09 +0000</pubDate>
      <link>https://dev.to/slyang08/release-my-cli-tool-on-npm-3laa</link>
      <guid>https://dev.to/slyang08/release-my-cli-tool-on-npm-3laa</guid>
      <description>&lt;p&gt;For my TypeScript CLI project using &lt;code&gt;pnpm&lt;/code&gt;, I chose &lt;strong&gt;npm&lt;/strong&gt; as the package registry and release tool.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reason:&lt;/strong&gt; npm is the standard package registry for Node.js and TypeScript projects, widely used and supported.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Link:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/repo-snapshot?activeTab=readme" rel="noopener noreferrer"&gt;npm repo for repo-snapshot&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By releasing via npm, my CLI tool becomes easily installable by developers globally, either with &lt;code&gt;npm&lt;/code&gt; or &lt;code&gt;pnpm&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Before publishing, I made several changes to ensure the project would work smoothly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;README.md&lt;/strong&gt; – Updated installation instructions, usage examples, and added both &lt;code&gt;npm&lt;/code&gt; and &lt;code&gt;pnpm&lt;/code&gt; instructions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;package.json&lt;/strong&gt; –&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Added &lt;code&gt;"main"&lt;/code&gt; field for entry point&lt;/li&gt;
&lt;li&gt;Added &lt;code&gt;"files"&lt;/code&gt; field to include only compiled files&lt;/li&gt;
&lt;li&gt;Added &lt;code&gt;"repository"&lt;/code&gt; information

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;tsconfig.json&lt;/strong&gt; – Adjusted output settings to generate proper compiled JavaScript for npm.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These changes made the project more user-friendly and compatible with npm’s publishing requirements.&lt;/p&gt;

&lt;p&gt;Here is the detailed step-by-step process I followed:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 – Register npm Account
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Created an account on &lt;a href="https://www.npmjs.com/signup" rel="noopener noreferrer"&gt;npm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Verified my email&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2 – Prepare Git Repository
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Pushed the latest code to GitHub&lt;/li&gt;
&lt;li&gt;Used &lt;code&gt;git tag&lt;/code&gt; to mark release versions, e.g., &lt;code&gt;git tag v0.9.0&lt;/code&gt; and &lt;code&gt;git push --tags&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3 – Publish to npm
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Log in to npm&lt;/span&gt;
npm login

&lt;span class="c"&gt;# Publish the package&lt;/span&gt;
npm publish &lt;span class="nt"&gt;--access&lt;/span&gt; public
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this, my CLI tool was publicly available.&lt;/p&gt;

&lt;p&gt;However, I wanted every new release to automatically publish to npm. I implemented CD using GitHub Actions:&lt;/p&gt;

&lt;h3&gt;
  
  
  Steps:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Generate npm Access Token&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://docs.npmjs.com/creating-and-viewing-access-tokens" rel="noopener noreferrer"&gt;npm Access Tokens&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Add Secret in GitHub&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Repository → Settings → Secrets and variables → Actions → New Repository Secret&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Name: &lt;code&gt;NPM_TOKEN&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Secret: Copy the npm token&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Create GitHub Actions Workflow&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Path: &lt;code&gt;.github/workflows/npm-publish.yml&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use example code from &lt;a href="https://docs.npmjs.com/generating-provenance-statements#example-github-actions-workflow" rel="noopener noreferrer"&gt;npm documentation&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Customize &lt;code&gt;branch&lt;/code&gt;, &lt;code&gt;version&lt;/code&gt;, and other settings&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example snippet of workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm publish&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;v*.*.*'&lt;/span&gt;  &lt;span class="c1"&gt;# or 'v*'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&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/setup-node@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;node-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;18'&lt;/span&gt;
          &lt;span class="na"&gt;registry-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://registry.npmjs.org'&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="s"&gt;npm ci&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="s"&gt;npm run build&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="s"&gt;npm publish&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;NODE_AUTH_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.NPM_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This setup ensures that any new tag automatically triggers a release.&lt;/p&gt;

&lt;p&gt;Even though I tested the installation myself, I wanted a &lt;strong&gt;fresh perspective&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I asked a partner unfamiliar with my project to follow the README instructions.&lt;/li&gt;
&lt;li&gt;Observations:&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Initially, they missed the difference between &lt;code&gt;npm&lt;/code&gt; and &lt;code&gt;pnpm&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;They tried installing without &lt;code&gt;nvm&lt;/code&gt; (required for npm but not for pnpm).

&lt;ul&gt;
&lt;li&gt;Fixes:&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Clarified installation instructions in README&lt;/li&gt;
&lt;li&gt;Added separate examples for &lt;code&gt;npm&lt;/code&gt; and &lt;code&gt;pnpm&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Ensured users understand how to run the CLI immediately after install&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This session was very useful to make the tool more &lt;strong&gt;beginner-friendly&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So the users can now install the tool globally:&lt;/p&gt;

&lt;h3&gt;
  
  
  Using npm:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; repo-snapshot
repo-snapshot &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Using pnpm:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add &lt;span class="nt"&gt;-g&lt;/span&gt; repo-snapshot
repo-snapshot &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Publishing &lt;code&gt;repo-snapshot&lt;/code&gt; taught me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to prepare a TypeScript CLI for npm release&lt;/li&gt;
&lt;li&gt;How to automate releases with GitHub Actions&lt;/li&gt;
&lt;li&gt;How to test usability for external users&lt;/li&gt;
&lt;li&gt;How to document clear installation instructions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Seeing my project live on npm and available for anyone to download was &lt;strong&gt;a rewarding experience&lt;/strong&gt; and a major milestone for my CLI development journey.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>cli</category>
      <category>osd600lab9</category>
      <category>release</category>
    </item>
    <item>
      <title>Combine retry feature and logging in second issue</title>
      <dc:creator>Sheng-Lin Yang</dc:creator>
      <pubDate>Fri, 21 Nov 2025 19:56:10 +0000</pubDate>
      <link>https://dev.to/slyang08/combine-retry-feature-and-logging-in-second-issue-3hc</link>
      <guid>https://dev.to/slyang08/combine-retry-feature-and-logging-in-second-issue-3hc</guid>
      <description>&lt;p&gt;Repository: &lt;a href="https://github.com/scinac/CLImanga" rel="noopener noreferrer"&gt;CLImanga&lt;/a&gt;&lt;br&gt;
Issue: &lt;a href="https://github.com/scinac/CLImanga/issues/3" rel="noopener noreferrer"&gt;Check chapterImage download response for CLImanga&lt;/a&gt;&lt;br&gt;
PR: &lt;a href="https://github.com/scinac/CLImanga/pull/19" rel="noopener noreferrer"&gt;fix: retry download if failed&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;During the development of CLImanga, I noticed a small but impactful issue:&lt;br&gt;
When the program tried to download a manga image, if the download failed (for example due to network issues or a temporary server problem), the program would &lt;strong&gt;stop immediately&lt;/strong&gt; and not attempt to retry.&lt;/p&gt;

&lt;p&gt;This made the download process unreliable, especially when handling multiple chapters or large manga series.&lt;/p&gt;

&lt;p&gt;The goal of this PR was to &lt;strong&gt;implement a retry mechanism&lt;/strong&gt; to improve reliability and provide visibility into download attempts using the custom logging system I implemented in the previous PR.&lt;/p&gt;

&lt;p&gt;The retry mechanism needed to satisfy the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Attempt downloads multiple times (set a &lt;strong&gt;maximum of 5 retries&lt;/strong&gt;) if an error occurs.&lt;/li&gt;
&lt;li&gt;Log each failed attempt with details such as attempt number, URL, and error reason.&lt;/li&gt;
&lt;li&gt;Include a small delay between retries to avoid overwhelming the server (&lt;code&gt;time.Sleep&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Fail gracefully if all attempts are exhausted, returning a descriptive error.&lt;/li&gt;
&lt;li&gt;Integrate with the existing logging system so all attempts are persisted in &lt;code&gt;logs/latest.log&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I improved the &lt;code&gt;downloadFile&lt;/code&gt; function handles the download logic with retry support:&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="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;downloadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;maxRetries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&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;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="o"&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="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;resp&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;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="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="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&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;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&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;StatusOK&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;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;"bad status: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="n"&gt;outFile&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;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Create&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="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="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;outFile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&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;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&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="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="no"&gt;nil&lt;/span&gt; &lt;span class="c"&gt;// success&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&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;"Attempt %d: Failed to download %s: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt&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="n"&gt;url&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="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// If failed, wait and retry&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// If all attempts fail, return the last error&lt;/span&gt;
    &lt;span class="k"&gt;return&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 download file from %s after %d attempts: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxRetries&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key Design Decisions&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Anonymous function for each attempt&lt;/strong&gt;: Ensures that each retry has its own scope for error handling and resource cleanup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logging with attempt number&lt;/strong&gt;: Makes it easy to track how many retries occurred and why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delay using &lt;code&gt;time.Sleep&lt;/code&gt;&lt;/strong&gt;: Reduces risk of overwhelming the server or triggering rate limits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MaxRetries constant&lt;/strong&gt;: Prevents infinite loops and allows easy adjustment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After I improved the &lt;code&gt;downloadFile&lt;/code&gt; function, I would like to validate the retry mechanism, so I created a unit test:&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="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestDownloadFileRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&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;Init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;"https://example.com/nonexistentfile.jpg"&lt;/span&gt;
  &lt;span class="n"&gt;savePath&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;"test_image.jpg"&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;downloadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;savePath&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;t&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="s"&gt;"It was expected to fail, but it succeeded, which may indicate that the test was invalid."&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This point that I did:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uses a URL that is guaranteed to fail (nonexistentfile.jpg)&lt;/li&gt;
&lt;li&gt;Checks that the function returns an error after retry attempts&lt;/li&gt;
&lt;li&gt;Confirms that the retry loop is working as expected and logging messages are written&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;This small PR made a tangible improvement in &lt;strong&gt;robustness&lt;/strong&gt; of CLImanga downloads.&lt;br&gt;
Although the feature is simple, it taught me &lt;strong&gt;important lessons about error handling, retries, and structured logging&lt;/strong&gt;, while allowing me to &lt;strong&gt;apply the log system&lt;/strong&gt; I created in the first PR.&lt;/p&gt;

&lt;p&gt;The combination of retry logic and logging ensures users get reliable downloads and developers have clear insight into what went wrong, if anything.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>cli</category>
      <category>osd600</category>
      <category>release0dot3</category>
    </item>
    <item>
      <title>A log system for the CLI tool</title>
      <dc:creator>Sheng-Lin Yang</dc:creator>
      <pubDate>Fri, 21 Nov 2025 18:27:29 +0000</pubDate>
      <link>https://dev.to/slyang08/a-log-system-for-the-cli-tool-2ee2</link>
      <guid>https://dev.to/slyang08/a-log-system-for-the-cli-tool-2ee2</guid>
      <description>&lt;p&gt;Repository: &lt;a href="https://github.com/scinac/CLImanga" rel="noopener noreferrer"&gt;CLImanga&lt;/a&gt;&lt;br&gt;
Issue: &lt;a href="https://github.com/scinac/CLImanga/issues/14" rel="noopener noreferrer"&gt;create LOG system (file based) for CLImanga&lt;/a&gt;&lt;br&gt;
PR: &lt;a href="https://github.com/scinac/CLImanga/pull/17" rel="noopener noreferrer"&gt;feat: add log system&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first issue I chose to work on for CLImanga was surprisingly simple at first glance:&lt;br&gt;
&lt;strong&gt;"Create a logging system for the project."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before opening this PR, I had never built a logging system in Go.&lt;br&gt;
I mainly relied on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;printf&lt;/code&gt; in C&lt;/li&gt;
&lt;li&gt;&lt;code&gt;console.log&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;console.error&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And honestly, I believed those were enough for most programs.&lt;/p&gt;

&lt;p&gt;But as I began exploring the issue, I realized a real project requires far more like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Persistent logs&lt;/li&gt;
&lt;li&gt;Structured messages&lt;/li&gt;
&lt;li&gt;Error-level separation&lt;/li&gt;
&lt;li&gt;Execution tracing&lt;/li&gt;
&lt;li&gt;Debug-friendly information (file, line, timestamp)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This PR became my first deep dive into designing a &lt;strong&gt;custom, project-specific logging system&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Based on the issue and discussions with the project owner, the logging system needed to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Write logs to a &lt;strong&gt;fixed file&lt;/strong&gt;: &lt;code&gt;logs/latest.log&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Provide &lt;strong&gt;INFO&lt;/strong&gt; and &lt;strong&gt;ERROR&lt;/strong&gt; loggers&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Include metadata:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;date&lt;/li&gt;
&lt;li&gt;time&lt;/li&gt;
&lt;li&gt;file name&lt;/li&gt;
&lt;li&gt;line number&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Support helper utilities to easily log:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the current function name&lt;/li&gt;
&lt;li&gt;the start/end of wrapped functions&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Overwrite the log file on each program run to avoid unlimited growth&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These requirements guided my design choices.&lt;/p&gt;

&lt;p&gt;The final implementation is located in the log package and uses four Go standard libraries:&lt;br&gt;
I used several libraries like: &lt;code&gt;log&lt;/code&gt;, &lt;code&gt;os&lt;/code&gt;, &lt;code&gt;runtime&lt;/code&gt;, and &lt;code&gt;time&lt;/code&gt;:&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;log&lt;/strong&gt;:
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;Info&lt;/span&gt;  &lt;span class="o"&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;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"INFO: "&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;Ldate&lt;/span&gt;&lt;span class="o"&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;Ltime&lt;/span&gt;&lt;span class="o"&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;Lshortfile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Error&lt;/span&gt; &lt;span class="o"&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;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ERROR: "&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;Ldate&lt;/span&gt;&lt;span class="o"&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;Ltime&lt;/span&gt;&lt;span class="o"&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;Lshortfile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The flags ensure each log entry includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;date&lt;/li&gt;
&lt;li&gt;time&lt;/li&gt;
&lt;li&gt;short file name + line number&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;os&lt;/strong&gt;:
&lt;/h3&gt;

&lt;p&gt;Used to manage file and directory operations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create logs/ directory&lt;/li&gt;
&lt;li&gt;Open or create latest.log&lt;/li&gt;
&lt;li&gt;Truncate the file at every startup so the log doesn’t grow forever
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;os.MkdirAll("logs", 0755)
os.OpenFile("logs/latest.log", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;runtime&lt;/strong&gt;:
&lt;/h3&gt;

&lt;p&gt;This was the most interesting part.&lt;/p&gt;

&lt;p&gt;I learned to use &lt;code&gt;runtime.Caller&lt;/code&gt; to retrieve information about the function that called the logger helper:&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="n"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Caller&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FuncForPC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;tip: if switch the number 1 as different number, it means how deeper layer want to know&lt;/p&gt;

&lt;p&gt;This allowed me to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dynamically capture function names&lt;/li&gt;
&lt;li&gt;Generate messages like:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Starting&lt;/span&gt; &lt;span class="n"&gt;function&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DownloadManga&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;time&lt;/strong&gt;:
&lt;/h3&gt;

&lt;p&gt;Used to mark program start time in the log:&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="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RFC3339&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes every run easy to identify when debugging.&lt;/p&gt;

&lt;p&gt;When implementation, the following is the overall workflow of the logging system:&lt;/p&gt;

&lt;h4&gt;
  
  
  Initialization (Init())
&lt;/h4&gt;

&lt;p&gt;When the program starts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ensure the log directory exists&lt;/li&gt;
&lt;li&gt;Create or overwrite latest.log&lt;/li&gt;
&lt;li&gt;Create two loggers: Info and Error&lt;/li&gt;
&lt;li&gt;Write a startup banner with timestamp
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&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;Init&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;Info&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;"Application Started."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;catchProgramExit&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;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Welcome to CLImanga!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Function Wrapping (WrapFunction())
&lt;/h4&gt;

&lt;p&gt;This helper automatically logs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When a function starts&lt;/li&gt;
&lt;li&gt;When it finishes
Example:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;WrapFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="k"&gt;func&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="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&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;Info&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;"Starting function: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;functionName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;Info&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;"Finished function: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;functionName&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This instantly improved debugging readability.&lt;/p&gt;

&lt;h4&gt;
  
  
  Logging caller function name (LogFunctionName())
&lt;/h4&gt;

&lt;p&gt;This helper logs whichever function invoked it like:&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="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;readChapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mangaName&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;selectedChapter&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;manga&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChapterSelect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chapterList&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;manga&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChapterSelect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;appInstance&lt;/span&gt; &lt;span class="n"&gt;fyne&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;App&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;appWindow&lt;/span&gt; &lt;span class="n"&gt;fyne&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Window&lt;/span&gt;&lt;span class="p"&gt;)&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;LogFunctionName&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;ui&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DisplayChapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;appWindow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sc"&gt;'r'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;mangaName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;selectedChapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chapterList&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;appInstance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&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;This was especially helpful during development, when tracing flow across multiple files.&lt;/p&gt;

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

&lt;p&gt;Although this was a &lt;strong&gt;"small"&lt;/strong&gt; feature, it became one of the most educational PRs I have worked on.&lt;/p&gt;

&lt;p&gt;It taught me real-world logging techniques, improved my Go skills, and helped me contribute meaningful infrastructure to the CLImanga project.&lt;/p&gt;

&lt;p&gt;This logging system will support future development and debugging — including my own future PRs.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>cli</category>
      <category>osd600</category>
      <category>release0dot3</category>
    </item>
    <item>
      <title>Setting up CI and automated testing</title>
      <dc:creator>Sheng-Lin Yang</dc:creator>
      <pubDate>Fri, 14 Nov 2025 22:28:38 +0000</pubDate>
      <link>https://dev.to/slyang08/setting-up-ci-and-automated-testing-cf0</link>
      <guid>https://dev.to/slyang08/setting-up-ci-and-automated-testing-cf0</guid>
      <description>&lt;p&gt;In my repository &lt;a href="https://github.com/slyang08/repo-snapshot" rel="noopener noreferrer"&gt;Repo-snapshot&lt;/a&gt; I set Continuous Integration (CI) workflow with GitHub Actions by creating two folders: &lt;code&gt;.github/&lt;/code&gt;, &lt;code&gt;workflows/&lt;/code&gt;, and a file named &lt;code&gt;ci.yml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Structure

repo-snapshot/
├── .github/
│   └── workflows/
│       └── ci.yml
├── src/
│   ├── cli.ts
│   ├── file-collector.ts
│   ├── file-utils.ts
│   ├── git-info.ts
│   ├── index.ts
│   ├── output-builder.ts
│   ├── toml-config.ts
│   └── tree-structure.ts
└── tests/
    ├── file-utils.test.ts
    ├── index.test.ts
    └── output-builder.test.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The workflow configuration looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: CI
on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup pnpm
        uses: pnpm/action-setup@v2
        with:
          version: 10
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "pnpm"
      - name: Clean install
        run: pnpm install --frozen-lockfile --prefer-offline
      - name: Check formatting
        run: pnpm format:check
      - name: Lint
        run: pnpm lint
      - name: Test
        run: pnpm test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I set the CI to trigger on any &lt;code&gt;Pull Request&lt;/code&gt; or &lt;code&gt;Push&lt;/code&gt; to the &lt;code&gt;main&lt;/code&gt; branch, which runs checks for the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;pnpm setup&lt;/li&gt;
&lt;li&gt;Node.js setup&lt;/li&gt;
&lt;li&gt;dependencies installation&lt;/li&gt;
&lt;li&gt;code formatting&lt;/li&gt;
&lt;li&gt;ESLint checks&lt;/li&gt;
&lt;li&gt;testing&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Since I use &lt;code&gt;pnpm&lt;/code&gt; to manage dependencies, it is necessary to set up pnpm first to ensure all dependencies are installed correctly. Next, I set up &lt;code&gt;Node.js&lt;/code&gt; to make sure dependencies are installed for the correct Node version defined in &lt;code&gt;package.json&lt;/code&gt;. Then, the workflow checks whether formatting, linting, and tests run successfully.&lt;/p&gt;

&lt;p&gt;By automating formatting, linting, and testing in CI, I learned how much time and effort can be saved by automatically ensuring code quality on every pull request or push. This approach helps catch errors and maintain standards without manual intervention and reassures me that even if I or collaborators forget a step, CI will enforce it. After setting this up, I became confident that future projects would benefit from the same workflow—resulting in more consistent code, fewer bugs, and faster feedback. Overall, incorporating these checks into CI reinforced the importance of automation in development pipelines and convinced me this is a vital strategy for every project.&lt;/p&gt;

&lt;p&gt;Since completing the CI workflow, I feel it greatly benefits the project. Even for different or new projects, if anyone forgets to format, lint, or properly test code, CI reminds the coder of problems in the pull request and blocks any code that could cause side effects.&lt;/p&gt;

&lt;p&gt;After setting up the CI, I contributed to another project, &lt;a href="https://github.com/whyang9701/repopal/pull/19/files" rel="noopener noreferrer"&gt;Repopal&lt;/a&gt;, by creating unit tests.&lt;/p&gt;

&lt;p&gt;Its testing structure differs from mine in folder naming, directory layout, and configuration files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Structure

repopal/
├── __tests__/
│   ├── unit/
│   │   └── file.test.ts
│   └── vitest.config.ts
└── src/
    ├── file.ts
    ├── fileMap.ts
    ├── git.ts
    ├── loadConfig.ts
    ├── main.ts
    └── output.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the project differs somewhat, I first read and understood its structure. Then I chose a relevant part to create unit tests. If the testing framework had been different from mine, I would have had to learn a new one. Fortunately, the project also uses &lt;code&gt;vitest&lt;/code&gt;, so I was able to write tests more easily than if it had used another framework like &lt;code&gt;Jest&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>ci</category>
      <category>cli</category>
      <category>osd600lab8</category>
    </item>
    <item>
      <title>Unit testing for my CLI tool</title>
      <dc:creator>Sheng-Lin Yang</dc:creator>
      <pubDate>Sat, 08 Nov 2025 03:22:27 +0000</pubDate>
      <link>https://dev.to/slyang08/unit-testing-for-my-cli-tool-59al</link>
      <guid>https://dev.to/slyang08/unit-testing-for-my-cli-tool-59al</guid>
      <description>&lt;p&gt;In my CLI tool, &lt;a href="https://github.com/slyang08/repo-snapshot" rel="noopener noreferrer"&gt;Repo-snapshot&lt;/a&gt; which is built using TypeScript and Vite, I decided to implement unit tests to ensure my functions work correctly and handle edge cases. For testing, I chose &lt;code&gt;Vitest&lt;/code&gt; along with &lt;code&gt;@vitest/coverage-v8&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why I Chose These Tools
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://vitest.dev/guide/" rel="noopener noreferrer"&gt;Vitest&lt;/a&gt;: Vitest is a modern unit testing framework specifically designed for Vite projects. It integrates seamlessly with TypeScript, is fast, and supports features like mocking, snapshots, and coverage.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/vitest-dev/vitest/tree/main/packages/coverage-v8#readme" rel="noopener noreferrer"&gt;@vitest/coverage-v8&lt;/a&gt;: This plugin provides detailed code coverage reporting using V8’s built-in coverage tool. It helped me identify which parts of my code were fully tested and which were not.&lt;/p&gt;

&lt;p&gt;I chose these tools because they fit perfectly with the TypeScript + Vite environment of my CLI tool and allowed me to write tests efficiently without extra configuration overhead.&lt;/p&gt;

&lt;h4&gt;
  
  
  Setting Up Vitest in My Project
&lt;/h4&gt;

&lt;p&gt;Here’s how I set up Vitest and coverage in my project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install dependencies&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pnpm add -D vitest @vitest/coverage-v8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Configure Vitest&lt;/strong&gt;:
I created a &lt;code&gt;vitest.config.ts&lt;/code&gt; file in my project root:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    coverage: {
      enabled: true,
      provider: "v8",
      include: ["src/**/*.ts"],
      exclude: ["tests/**", "node_modules/**"],
    },
  },
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Update TypeScript config&lt;/strong&gt;:
I modified &lt;code&gt;tsconfig.json&lt;/code&gt; to include test files and support ES modules for tests:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  // Visit https://aka.ms/tsconfig to read more about this file
  "compilerOptions": {
    // File Layout
    "rootDir": "./",
    "outDir": "./dist",

    // Environment Settings
    // See also https://aka.ms/tsconfig/module
    "module": "nodenext",
    "target": "ES2020",
    // For nodejs:
    "types": ["node", "vitest", "vitest/globals"],

    // Other Outputs
    "sourceMap": true,
    "declaration": true,
    "declarationMap": true,

    // Stricter Typechecking Options
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,

    // Recommended Options
    "esModuleInterop": true,
    "strict": true,
    "jsx": "react-jsx",
    "verbatimModuleSyntax": false,
    "isolatedModules": true,
    "noUncheckedSideEffectImports": true,
    "moduleDetection": "force",
    "skipLibCheck": true,
  },
  "include": ["src", "tests"],
  // Ignore tests and node_modules
  "exclude": ["node_modules", "dist"]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add test scripts in&lt;/strong&gt; &lt;code&gt;package.json&lt;/code&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest --watch",
    "coverage": "vitest run --coverage"
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this setup, I could run all tests with:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Or test a specific file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pnpm test tests/file-utils.test.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Writing test cases for my CLI tool was a learning experience. Some functions were straightforward, but others had tricky edge cases. For example, one function that parsed repository paths could fail if a user entered an unexpected format. I had to think carefully about input validation, which was an “aha!” moment—writing tests made me anticipate situations I hadn’t considered.&lt;/p&gt;

&lt;p&gt;There were times I got stuck on mocking parts of the CLI output. Stepping away and revisiting the problem later helped me find simpler, cleaner solutions. This taught me that unit testing isn’t just about catching bugs—it also improves your design and code clarity.&lt;/p&gt;

&lt;p&gt;During testing, I uncovered a few interesting scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Projects containing a &lt;strong&gt;TOML config file&lt;/strong&gt; required careful handling in tests to avoid errors.&lt;/li&gt;
&lt;li&gt;Edge cases for file paths on different operating systems required me to handle both forward slashes (&lt;code&gt;/&lt;/code&gt;) and backward slashes (&lt;code&gt;\&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These discoveries proved the value of testing even small utility functions—tests caught potential runtime errors before users did.&lt;/p&gt;

&lt;p&gt;Before this project, I had very little hands-on experience with unit testing. Through this process, I learned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unit testing helps catch bugs early and improves the reliability of your code.&lt;/li&gt;
&lt;li&gt;Even small functions deserve tests to handle unexpected input or edge cases.&lt;/li&gt;
&lt;li&gt;Testing encourages better design and foresight in programming.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Going forward, I plan to write unit tests for almost every function or class I contribute to, as it saves time and effort in the long run and makes projects more robust.&lt;/p&gt;

&lt;h4&gt;
  
  
  Conclusion:
&lt;/h4&gt;

&lt;p&gt;This experience with Vitest showed me that testing is not just an optional step—it is an essential part of software development. I now feel much more confident incorporating tests into my future projects, and I understand how they can prevent subtle bugs and improve overall code quality.&lt;/p&gt;

</description>
      <category>github</category>
      <category>cli</category>
      <category>testing</category>
      <category>osd600lab7</category>
    </item>
    <item>
      <title>Experiences in Hacktoberfest 2025</title>
      <dc:creator>Sheng-Lin Yang</dc:creator>
      <pubDate>Sat, 01 Nov 2025 02:53:22 +0000</pubDate>
      <link>https://dev.to/slyang08/experiences-in-hacktoberfest-2025-pal</link>
      <guid>https://dev.to/slyang08/experiences-in-hacktoberfest-2025-pal</guid>
      <description>&lt;p&gt;From my first to my fourth issue, I used two programming languages: Python and Golang.&lt;/p&gt;

&lt;p&gt;Starting with the first issue, which was just a try, to the last one, which was a real challenge for me. I moved from a small Python issue to a well-structured Golang project. From working on an algorithm to developing a CLI tool with LLM integration and a Python project analyzer.&lt;/p&gt;

&lt;p&gt;I feel contributing to open-source projects is not as difficult as I once thought. There are so many projects across diverse topics that we might not even imagine, and many of them are creative and useful. Different projects require different languages and skills, offering many opportunities to contribute.&lt;/p&gt;

&lt;p&gt;During Hacktoberfest 2025, I experienced projects that helped me get involved in different fields. I learned Golang for backend development, WebSocket, and CLI tools, mostly working on the Python project analyzer.&lt;/p&gt;

&lt;p&gt;Therefore, I believe that if I keep this enthusiasm for open source, I can continue to learn more. Moreover, after this event, I am confident about continuing to contribute to open-source projects.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>hacktoberfest</category>
      <category>release0dot2</category>
      <category>osd600</category>
    </item>
    <item>
      <title>Circular dependency check in Python</title>
      <dc:creator>Sheng-Lin Yang</dc:creator>
      <pubDate>Fri, 31 Oct 2025 22:54:50 +0000</pubDate>
      <link>https://dev.to/slyang08/circular-dependency-check-in-python-58p2</link>
      <guid>https://dev.to/slyang08/circular-dependency-check-in-python-58p2</guid>
      <description>&lt;p&gt;I worked on my fourth issue, &lt;a href="https://github.com/ludo-technologies/pyscn/issues/175" rel="noopener noreferrer"&gt;Add circular dependency&lt;/a&gt;,&lt;br&gt;
and created my fourth Pull Request (PR): &lt;a href="https://github.com/ludo-technologies/pyscn/pull/213" rel="noopener noreferrer"&gt;New feature with circular dependency in Python project&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is my last issue in Hacktoberfest.&lt;br&gt;
I have challenged myself three times, and this one is the biggest one to me.&lt;/p&gt;

&lt;p&gt;This project implements many features and functions. Until now, I had never contributed to a project that uses one language to analyze or check for bugs and issues in another—like this project, which uses Go to check Python projects, whether they are small, medium, or large.&lt;/p&gt;

&lt;p&gt;For this feature, I implemented three options under the &lt;code&gt;check&lt;/code&gt; command: &lt;code&gt;--select deps&lt;/code&gt; or &lt;code&gt;--select circular&lt;/code&gt;, &lt;code&gt;--allow-circular-deps&lt;/code&gt;, and &lt;code&gt;--max-cycles&lt;/code&gt;. The main objective was to detect circular dependencies between the files in a project. If any were found, the tool would display the files involved. With &lt;code&gt;--max-cycles&lt;/code&gt;, users can specify how many circular dependencies are allowed in the entire project.&lt;/p&gt;

&lt;p&gt;While working on this project, I spent time communicating with the project owner about the problems I encountered, hoping to get useful feedback. One tip I learned from my open-source course was to use &lt;code&gt;git grep&lt;/code&gt; to efficiently find files containing specific keywords.&lt;/p&gt;

&lt;p&gt;I implemented the core functionality for detecting circular dependencies, but I am still working on how to display the exact line and column numbers in the output.&lt;/p&gt;

&lt;p&gt;Through this challenge, I learned the importance of clear communication between developers and how to use tools like &lt;code&gt;git grep&lt;/code&gt; to search for information efficiently.&lt;/p&gt;

&lt;p&gt;Even though this challenge is not completely finished yet, I am committed to seeing it through because it is a great opportunity to work on a well-structured, meaningful project.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>cli</category>
      <category>python</category>
      <category>release0dot2</category>
    </item>
    <item>
      <title>Open Source Contribution: CLI Project with LLM</title>
      <dc:creator>Sheng-Lin Yang</dc:creator>
      <pubDate>Fri, 31 Oct 2025 22:16:11 +0000</pubDate>
      <link>https://dev.to/slyang08/open-source-contribution-cli-project-with-llm-4p3c</link>
      <guid>https://dev.to/slyang08/open-source-contribution-cli-project-with-llm-4p3c</guid>
      <description>&lt;p&gt;I worked on my third issue, &lt;a href="https://github.com/DFanso/commit-msg/issues/85" rel="noopener noreferrer"&gt;No rate limiting for LLM API calls&lt;/a&gt;,&lt;br&gt;
and created my third Pull Request (PR): &lt;a href="https://github.com/DFanso/commit-msg/pull/126" rel="noopener noreferrer"&gt;Fix no rate limiting if llm api calls&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After my first Go open-source project, I wanted my next contribution to involve a command-line tool (CLI). I found a CLI project that used an LLM, and I was interested because I had not realized before that a CLI tool could make use of a large language model (LLM). This sparked my curiosity, so I left a comment expressing my interest in fixing the bug.&lt;/p&gt;

&lt;p&gt;The issue was that the CLI only implemented LLM usage but did not handle scenarios where many users might issue API requests in a very short time. This could cause crashes or provider rate-limit errors. I saw this as a great opportunity to join and solve the problem.&lt;/p&gt;

&lt;p&gt;For this PR, I used the Go package "golang.org/x/time/rate," which provides a robust rate limiter to control how frequently certain actions can occur. It allowed me to regulate API calls and prevent exceeding the provider's rate limits by limiting the request frequency and handling bursts efficiently.​&lt;/p&gt;

&lt;p&gt;I set an API rate limit as a variable and improved a function so that when the CLI makes an API call, it obeys the set rate; if requests exceed the limit, it returns an error and an empty result. After updating the function, I wrote a unit test to make sure everything was working as expected. I was pleased to see it perform correctly.&lt;/p&gt;

&lt;p&gt;Before opening my PR, I left a comment for the repository owner because I was not sure if the test file's location in the project structure would be acceptable, and there was no documentation about it. Once my unit test was approved, I submitted the PR and received a prompt response.&lt;/p&gt;

&lt;p&gt;This experience made me feel ready to challenge myself with even bigger CLI projects in the future.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>cli</category>
      <category>llm</category>
      <category>release0dot2</category>
    </item>
    <item>
      <title>First Golang project I contributed</title>
      <dc:creator>Sheng-Lin Yang</dc:creator>
      <pubDate>Fri, 31 Oct 2025 21:21:54 +0000</pubDate>
      <link>https://dev.to/slyang08/first-golang-project-i-contributed-aa1</link>
      <guid>https://dev.to/slyang08/first-golang-project-i-contributed-aa1</guid>
      <description>&lt;p&gt;I worked on my second issue, &lt;a href="https://github.com/arya2004/gobanter/issues/6" rel="noopener noreferrer"&gt;Typing indicators for users&lt;/a&gt;,&lt;br&gt;
and created my second Pull Request (PR): &lt;a href="https://github.com/arya2004/gobanter/pull/25" rel="noopener noreferrer"&gt;Improve UX for users&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For this second PR, I worked on a WebSocket project using Go (Golang) that allows users to communicate on a web page and send private messages to each other. In this PR, I made a simple improvement by adding a feature to show when "someone is typing" on the website. Although it is a small feature, it helps enhance the user experience for everyone using the chat.&lt;/p&gt;

&lt;p&gt;The code changes for this PR were not very extensive, but this was my first open-source project using Go (Golang), and I wanted to challenge myself and gain confidence with this new language. I made several changes across four files: home.html (HTML), styles.css (CSS), a type definition (Golang), and a function (JavaScript). When I ran the program and saw it working in the browser, I felt really proud to have contributed to an open-source project in Go.&lt;/p&gt;

&lt;p&gt;After creating this PR, I began considering what kinds of topics or projects are especially well-suited for Go. For example, I already have ideas for CLI tools and am interested in contributing to CLI projects using Go. I am excited to keep learning and look forward to making more contributions!&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>websocket</category>
      <category>release0dot2</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
