May 18, 2026

Automating .NET Framework Desktop App Releases on GitHub

Setting up Continuous Integration and Continuous Deployment (CI/CD) for modern .NET Core applications is usually straightforward. However, if you are maintaining a legacy .NET Framework desktop application (like WPF or WinForms using .NET 4.5 or 4.8), compiling and releasing it via GitHub Actions requires a specific set of tools: Windows runners, MSBuild, and a bit of PowerShell configuration.

Based on my experience, we will walk through the entire process of setting up a push-to-deploy pipeline that automatically builds your .NET Framework solution, zips the output, and publishes a formal GitHub Release.

The Release Process: From Code to Deployment

Here is how the automated lifecycle works once configured:

  1. Write Code & Push: You make changes to your local application and push the commits to your GitHub repository.
  2. Add the Workflow: You create a YAML configuration file inside a .github/workflows/ directory in your repository. This file acts as the blueprint, telling GitHub's servers exactly how to build your app.
  3. Trigger the Action: You tell GitHub to run the workflow either by pushing a Git Tag (e.g., v1.0) from your terminal, or by manually clicking a "Run workflow" button in the GitHub Actions tab.
  4. Automated Build & Package: GitHub spins up a temporary Windows server. It restores your NuGet packages, compiles your solution using MSBuild, and runs a background script to compress your executable and DLL files into a ZIP archive.
  5. Publish Release: The finished ZIP file is automatically uploaded and attached to a new GitHub Release page, making it instantly available for your users to download.

How to Structure Your GitHub Action

While we won't look at the raw YAML script, understanding the logical steps your action must take is critical for .NET Framework apps.

  • Choose the Right Runner: You must configure the action to run on windows-latest so it has access to Visual Studio build tools.
  • Restore Dependencies: Use the NuGet setup action to restore your packages. It is highly recommended to explicitly tell NuGet to drop the packages into a root packages folder so MSBuild can easily locate them.
  • Targeting Packs: If you are targeting an older framework like .NET 4.5, modern GitHub runners will not have it pre-installed. You will need to use a PowerShell step to download the targeting pack directly from NuGet and place it in the Windows Reference Assemblies folder.
  • Build with MSBuild: Use the MSBuild setup action to compile your solution file. Make sure to specify the Release configuration.
  • Zip and Release: Use a simple PowerShell command (like Compress-Archive) to zip your output folder, and pass that ZIP file to a GitHub Release action.

    
  name: Build and Release FaceSearchVLC 2

  on:
    push:
      tags:
        - 'v*'
    workflow_dispatch:

  jobs:
    build-and-release:
      runs-on: windows-latest

      steps:
        - name: Checkout source code
          uses: actions/checkout@v4

        - name: Setup NuGet
          uses: nuget/setup-nuget@v2

        - name: Restore NuGet packages
          run: nuget restore nfwFolderCheckGithub.sln

        - name: Install .NET 4.5 Targeting Pack
          shell: pwsh
          run: |
            $url = "https://www.nuget.org/api/v2/package/Microsoft.NETFramework.ReferenceAssemblies.net45/1.0.3"
            Invoke-WebRequest -Uri $url -OutFile "net45.zip"
            Expand-Archive -Path "net45.zip" -DestinationPath "net45"
            $target = "C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5"
            New-Item -ItemType Directory -Path $target -Force
            Copy-Item -Path "net45\build\.NETFramework\v4.5\*" -Destination $target -Recurse -Force

        - name: Pre-Build Fixes (HintPaths & Windows.winmd)
          shell: pwsh
          run: |
            # 1. Fix NuGet Paths: Copy root packages into the project folders so MSBuild can find them
            if (Test-Path "packages") {
                Write-Host "Copying packages to project directories..."
                Copy-Item -Path "packages" -Destination "ConsoleToast\packages" -Recurse -Force
                Copy-Item -Path "packages" -Destination "nfwFolderCheck\packages" -Recurse -Force
            }

            # 2. Fix WinRT APIs: Find Windows.winmd on the runner and place it in the output folder
            $winmd = Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\UnionMetadata" -Filter "Windows.winmd" -Recurse | Select-Object -First 1
            if ($winmd) {
                Write-Host "Found Windows.winmd. Copying to build directory..."
                New-Item -ItemType Directory -Force -Path "ConsoleToast\bin\Release"
                Copy-Item $winmd.FullName -Destination "ConsoleToast\bin\Release\Windows.winmd"
            }

        - name: Setup MSBuild
          uses: microsoft/setup-msbuild@v2

        - name: Build the Solution
          run: msbuild nfwFolderCheckGithub.sln /p:Configuration=Release /p:Platform="Any CPU" /p:SignManifests=false

        - name: Prepare and Zip Release Assets
          shell: pwsh
          run: |
            New-Item -ItemType Directory -Force -Path _ReleaseOutput

            if (Test-Path "nfwFolderCheck\bin\Release\*") {
                Copy-Item -Path "nfwFolderCheck\bin\Release\*" -Destination _ReleaseOutput -Recurse -Force
            }

            if (Test-Path "ConsoleToast\bin\Release\*") {
                Copy-Item -Path "ConsoleToast\bin\Release\*" -Destination _ReleaseOutput -Recurse -Force
            }

            Compress-Archive -Path _ReleaseOutput\* -DestinationPath FaceSearchVLC-Release.zip

        - name: Publish GitHub Release
          uses: softprops/action-gh-release@v2
          with:
            files: FaceSearchVLC-Release.zip
            generate_release_notes: true
            # This line checks if a tag triggered the run. 
            # If not (manual run), it creates a tag like "v1.0.4" based on the run number.
            tag_name: ${{ github.ref_type == 'tag' && github.ref_name || format('v1.0.{0}', github.run_number) }}
          env:
            GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    
  

Troubleshooting Common .NET Framework Build Errors

When compiling older desktop apps on cloud runners, you will likely run into a few specific MSBuild errors. Here is exactly how to fix them.

Error: MSB1009: Project file does not exist

The Problem: MSBuild throws an error referencing a placeholder name like your-solution-name. This happens if you copy-pasted a template and left placeholder variables intact.

The Fix: Hardcode your actual solution file name (e.g., MyApplication.sln) directly into the MSBuild run command inside your workflow.

Error: MSB3644: Reference assemblies for .NETFramework,Version=v4.5 were not found

The Problem: GitHub updated their modern Windows runners to Visual Studio 2022, which dropped built-in support for .NET 4.5. Do not try to downgrade to older runners (like windows-2019), as they have been permanently retired and will cause your build to freeze.

The Fix: Keep the runner on the latest version and add a PowerShell step right before MSBuild to manually download the 4.5 targeting pack from NuGet and copy it into your runner's Reference Assemblies directory.

Error: MSB3323: Unable to find manifest signing certificate

The Problem: If your project uses ClickOnce deployment, Visual Studio generated a local certificate (a .pfx file) on your personal computer. GitHub's runner does not have this file, so the build crashes.

The Fix: Append /p:SignManifests=false to your MSBuild command to tell the compiler to ignore the missing local certificate during automated cloud builds.

Error: Missing Namespaces (CS0234 or CS0246)

The Problem: You get compilation errors saying external types (like ToastNotification or WindowsAPICodePack) could not be found. This happens because GitHub restores NuGet packages to the repository root, but older project files often look for them in specific local subfolders or hardcoded C:\ drive paths.

The Fix: Add a "Pre-Build" PowerShell step to dynamically copy the restored packages folder into the exact directory your project expects. If you rely on native Windows 10 APIs, use PowerShell to search the runner's UnionMetadata folder for Windows.winmd and copy it directly into your build folder before compiling.

Error: GitHub Releases requires a tag

The Problem: The final release step fails with a warning when you manually click "Run workflow". This happens because a formal GitHub Release cannot be created without a Git tag attached to it.

The Fix: You can either trigger the workflow strictly by pushing Git tags from your terminal, or update your release action configuration to automatically generate a fallback tag (like formatting the run number into a version string) whenever a tag isn't detected.

Apr 8, 2026

Automated Code Review Tools for Developers

Manual code reviews are essential, but they can be time-consuming and prone to human oversight. Automated tools like Qodo/Coderabbit, DeepScan, DeepSource, SonarQube, and Semgrep help developers catch bugs early, enforce coding standards, and maintain high-quality codebases.

Why Use Automated Code Review Tools?

  • Save time: Automate repetitive checks so reviewers can focus on design and architecture.
  • Improve quality: Catch subtle bugs and vulnerabilities before they reach production.
  • Consistency: Enforce coding standards across teams and projects.
  • Security: Detect insecure patterns and prevent vulnerabilities.

Qodo / Coderabbit

AI-powered assistants that integrate directly into pull requests. They provide human-like review comments on readability, maintainability, and potential issues. Example: spotting unnecessary complexity in a function and suggesting a cleaner approach.

DeepScan

Specializes in JavaScript and TypeScript. It detects runtime errors and code quality issues that traditional linters might miss. Example: identifying a potential null reference in a React component.

DeepSource

A continuous analysis platform supporting multiple languages. It finds bugs, performance issues, and security vulnerabilities. Example: flagging inefficient database queries or unsafe string concatenations.

SonarQube

A widely used platform for tracking bugs, vulnerabilities, and code smells. It provides dashboards to monitor maintainability and technical debt. Example: highlighting duplicated code across modules.

Semgrep

A lightweight static analysis tool that allows custom rules. Popular for security scanning. Example: writing a rule to detect unsafe use of eval() in JavaScript.

Feb 7, 2026

How to Use Cloudflare SSL Certificate with Plesk Hosting

Many website owners see a "Not Secure" warning when they first move DNS to Cloudflare. Cloudflare provides a free Universal SSL certificate for visitors connecting to your site, but you also need a certificate on your origin server (like Plesk) to complete the handshake.

Step 1: Generate Origin Certificate in Cloudflare

In Cloudflare dashboard, go to SSL/TLS → Origin Server and click Create Certificate. Cloudflare will give you two text blocks: the Origin Certificate and the Private Key. These are only valid between Cloudflare and your server.

Step 2: Install Certificate in Plesk

  • Log in to Plesk and navigate to Websites & Domains → SSL/TLS Certificates.
  • Create a new certificate entry and paste the Private Key into the "Private key" field.
  • Paste the Origin Certificate into the "Certificate (*.crt)" field.
  • Leave the "CA certificate (*-ca.crt)" field empty (Cloudflare does not provide a CA chain).
  • Save and assign this certificate to your domain under Hosting Settings.

Step 3: Configure Cloudflare SSL Mode

In Cloudflare → SSL/TLS → Overview, set the mode to Full (Strict). This ensures Cloudflare will only connect to your server if the certificate is valid.

Step 4: Redirect All Traffic to HTTPS

Enable Always Use HTTPS and Automatic HTTPS Rewrites in Cloudflare. This forces visitors to use the secure version of your site and fixes mixed content issues.

Result

After these steps, visiting https://yourdomain.com will load securely without handshake errors. Cloudflare protects the edge, and your Plesk server is secured with the Origin Certificate.

Jan 14, 2026

Custom JSON Health Check Responses in ASP.NET Core

When building modern APIs, health checks are a common way to expose the status of your application. By default, ASP.NET Core health checks return a very minimal response (often just HTTP status codes). But what if you want to return a clean JSON payload that can easily be consumed by monitoring systems?

Step 1: Create a Custom JSON Response Writer

ASP.NET Core’s HealthCheckOptions allows us to plug in a custom ResponseWriter. Here’s one that outputs a JSON response containing the health status:

private async Task JsonResponseWriter(HttpContext context, HealthReport report)
{
    context.Response.ContentType = "application/json";
    await JsonSerializer.SerializeAsync(
        context.Response.Body,
        new { Status = report.Status.ToString() },
        new JsonSerializerOptions 
        { 
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase 
        }
    );
}

What this does:

  • Sets the response type to application/json.

  • Serializes an anonymous object containing the health check Status.

  • Uses camelCase naming to keep JSON consistent with API conventions.


Step 2: Hook the Writer into Health Checks

Now, wire up the health check middleware in your pipeline:

app.UseHealthChecks("/health", new HealthCheckOptions 
{ 
    ResponseWriter = JsonResponseWriter 
});

Explanation:

  • Requests to /health will trigger your health checks.

  • Instead of the default plain-text response, your JsonResponseWriter will return JSON like:

{
  "status": "Healthy"
}

Step 3: Configure Global JSON Options

To keep your entire API’s JSON responses consistent, configure the JsonSerializerOptions in Startup.cs (or Program.cs for minimal hosting):

services.AddControllers()
    .AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.IgnoreNullValues = true;
        options.JsonSerializerOptions.WriteIndented = true;
    });


  • IgnoreNullValues = true → Null properties won’t clutter your responses.

  • WriteIndented = true → JSON output is more readable (great for debugging).

With this setup:

  • Hitting /health gives a clean JSON response.

  • All controllers return JSON using consistent formatting.

  • Monitoring tools can easily parse your app’s health status.

Example response:

{
  "status": "Healthy"
}

Conclusion

By adding a custom ResponseWriter and configuring global JSON options, you can make your ASP.NET Core health checks more API-friendly. This approach is simple, clean, and integrates seamlessly into modern monitoring setups like Kubernetes, Prometheus, or cloud load balancers.


Jan 12, 2026

Fixing Google Analytics Integration in a Next.js App

When I tried to add Google Analytics to my React/Next.js project, I used the GoogleTagManager React component with my GA4 tag ID:

    
<GoogleTagManager gtmId="G-XXXXXXXXXX" />
    
  

The build succeeded, but Analytics showed “No data received in past 48 hours.”

The Problem

Using the GoogleTagManager component with a GA4 ID (G-XXXXXXXXXX) only created a preload link in the <head>:

    
<link rel="preload" href="https://www.googletagmanager.com/gtm.js?id=G-XXXXXXXXXX" as="script"/>
    
  

This preload line fetched the script but did not initialize GA4 tracking. As a result, Google Analytics never received events.

The Solution

Instead of relying on the preload link, I injected the full GA4 snippet using Next.js’s next/script. This ensures the script loads after hydration and initializes correctly.

Sample Code

    
import Script from "next/script";

export default function Layout({ children }) {
  return (
    <html lang="en">
      <head>
        {/* Load GA4 script */}
        <Script
          src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"
          strategy="afterInteractive"
        />

        {/* Initialize GA4 */}
        <Script id="ga4-init" strategy="afterInteractive">
          {`
            window.dataLayer = window.dataLayer || [];
            function gtag(){dataLayer.push(arguments);}
            gtag('js', new Date());
            gtag('config', 'G-XXXXXXXXXX');
          `}
        </Script>
      </head>
      <body>{children}</body>
    </html>
  );
}
    
  

Replace G-XXXXXXXXXX with your own GA4 Measurement ID.

Key Takeaways

  • The GoogleTagManager React component with a GA4 ID only creates a preload link — it doesn’t initialize Analytics.
  • GA4 requires the full gtag.js snippet with initialization code.
  • Use next/script in Next.js for safe script injection.
  • Verify events in Analytics → Realtime after deployment.

๐Ÿ‘‰ This adjustment solved the issue and got my Analytics dashboard flowing with data again.

Nov 5, 2025

PowerShell Script to Create and Bind a Self-Signed SSL Certificate

The script creates a new self-signed certificate, generates a unique App ID, and binds it to port 5555 — automatically.

Copy and run this in PowerShell (Run as Administrator):

# Get computer hostname
$hostname = hostname
Write-Host "Hostname: $hostname"

# Create a self-signed cert for localhost + hostname (valid 1 year)
$cert = New-SelfSignedCertificate -DnsName "localhost", $hostname `
  -CertStoreLocation "Cert:\LocalMachine\My" `
  -NotAfter (Get-Date).AddYears(1)
$thumb = $cert.Thumbprint
Write-Host "Cert Thumbprint: $thumb"

# Create new Application ID (GUID)
$appid = "{" + ([guid]::NewGuid().ToString()) + "}"
Write-Host "App ID: $appid"

# Bind certificate to all interfaces on port 5555
$ipport = "0.0.0.0:5555"
netsh http add sslcert ipport=$ipport certhash=$thumb appid=$appid

Write-Host "✅ Done! SSL binding created on port 5555."

๐Ÿ” Why This Helps

When hosting a local web API or service (e.g., on Kestrel or IIS Express), HTTPS often fails due to:

This script fixes all of it in one run — clean, fast, repeatable.


๐Ÿงน Optional: Clean Old Bindings

If you want to clear any previous SSL settings before running the script:

netsh http delete sslcert ipport=0.0.0.0:5555
netsh http delete sslcert ipport=localhost:5555
netsh http delete sslcert ipport=192.168.1.25:5555

Ignore “file not found” messages — that just means no old bindings exist.


✅ Verify

After running the script, check:

netsh http show sslcert

You should see your new certificate bound to port 5555.

Sep 25, 2025

Firebase Storage: A Simple Guide to CORS Configuration

Cross-Origin Resource Sharing (CORS) is a critical part of web development, especially when working with cloud storage solutions like Firebase Storage. This post will walk you through a simple, yet powerful, CORS configuration to get your Firebase Storage bucket talking to your web app.

What's CORS and Why Does It Matter?

CORS is a security feature built into modern web browsers. It's a mechanism that allows a web page to request resources from a different domain than the one that served the web page. Imagine you have your website at `www.mywebsite.com` and you're trying to display an image stored in your Firebase Storage bucket at `my-project.appspot.com`. Without a proper CORS policy, your browser would block the request, seeing it as a potential security risk.

Configuring Your `cors.json`

To configure your CORS policy for a Firebase Storage bucket, you need to create a JSON file. 


[
  {
    "origin": ["*"],
    "method": ["GET", "HEAD", "PUT", "POST", "DELETE"],
    "responseHeader": [
      "Content-Type",
      "Access-Control-Allow-Origin"
    ],
    "maxAgeSeconds": 3600
  }
]

origin: This is the most crucial part. The `*` acts as a wildcard, allowing any domain to access your resources. While convenient for development and public assets, for production, it's best practice to replace `*` with your specific domain, e.g., `["https://www.yourdomain.com"]`, for enhanced security.

method: This specifies the HTTP methods (like `GET`, `POST`, `PUT`, `DELETE`) that are allowed. By including all of them, you're granting full control to your allowed origins. This is helpful if you need to perform actions beyond just fetching data, like uploading or deleting files.

responseHeader: These are the headers that the browser is allowed to access. Content-Type is a standard one, and Access-Control-Allow-Origin is the header that confirms the origin is allowed, which is essential for CORS to work correctly.

maxAgeSeconds: This tells the browser how long it can cache the CORS preflight response for. The value 3600 means one hour, which helps reduce the number of preflight requests for the same resource, making your application a bit faster.

Applying the Policy with gsutil

Once your cors.json file is ready, you need to apply it to your Firebase Storage bucket. You've already done this with the `gsutil` command in the Firebase Studio terminal:


studio-45:~/studio{main}$ gsutil cors set cors.json gs://studio-45-e8723.firebasestorage.app
Setting CORS on gs://studio-45-e8723.firebasestorage.app/...

  * gsutil cors set: This is the command to set a new CORS configuration.

  * cors.json: This is the path to your configuration file.

  * gs://studio-45-e8723.firebasestorage.app: This is the URI for your specific Firebase Storage bucket.

After running this command, your Firebase Storage bucket will now be configured with the CORS policy you defined. This means your web application, running on any origin, can now access the files in your bucket using the specified methods and headers\!


Sep 1, 2025

Connect Firebase Project & Workspace in Firebase Studio

It’s a common setup mistake: you create a Firebase project, then a Firebase Studio workspace but they’re not connected. 

  • A Firebase project in the Firebase Console
  • A Firebase workspace in Firebase Studio


๐Ÿ› ️ The Fix: Link Your Workspace to the Firebase Project

1. Open the Firebase Studio Terminal

In Firebase Studio, navigate to your workspace and open the terminal window. This is where you’ll run CLI commands.

2. Run firebase init

firebase init

This command starts the setup wizard. You’ll be prompted to:

  • Grant access to Google Cloud resources

  • Select a Google Cloud project
    Choose the correct Firebase project from the list — this links your workspace to it.

  • Choose Firebase features
    Continue with terminal window. Pick the services you plan to use (e.g. Firestore, Functions, Hosting).

This step creates two key files in your workspace:

  • .firebaserc — maps your workspace to the selected Firebase project
  • firebase.json — configures Firebase services

3. Verify the Connection

Run:

firebase projects:list

Your selected project should appear with a * next to it, indicating it’s active.


studio-77:~/studio{main}$ firebase projects:list
✔ Preparing the list of your Firebase projects

┌──────────────────────┬──────────────────────────┬────────────────┬──────────────────────┐
│ Project Display Name │ Project ID               │ Project Number │ Resource Location ID │
├──────────────────────┼──────────────────────────┼────────────────┼──────────────────────┤
│ Asset                │ asset-90d38 (current)    │ XXXXXXXXXXXX   │ [Not specified]      │
├──────────────────────┼──────────────────────────┼────────────────┼──────────────────────┤

✅ Result

Your Firebase Studio workspace is now properly connected to your Firebase project. All CLI commands will target the correct environment, and you can deploy, emulate, or manage resources without errors.


๐Ÿง  Pro Tip

If you work across multiple Firebase projects, use:

firebase use --add

This lets you assign aliases and switch between them easily:

firebase use dev
firebase use prod

Firebase init output (Web app hosting option)

✔ Please select an option: Use an existing project
✔ Select a default Firebase project for this directory: asset-90d38 (asset)
i  Using project asset-90d38 (asset)

=== App Hosting Setup
i  This command links your local project to Firebase App Hosting. You will be able to deploy your web app with `firebase deploy` after setup.
✔ Please select an option Create a new backend
i  === Set up your backend
✔ Select a primary region to host your backend:
 us-central1
✔  Location set to us-central1.

✔ Provide a name for your backend [1-30 characters] asset-backend
✔  Name set to asset-backend

✔  Created a new Firebase web app named "asset-backend"
✔ Successfully created backend!
        projects/asset-90d38/locations/us-central1/backends/asset-backend

i  === Deploy local source setup
✔ Specify your app''s root directory relative to your firebase.json directory /
✔  Wrote configuration info to firebase.json
i  Writing default settings to apphosting.yaml...
✔ File /home/user/studio/apphosting.yaml already exists. Overwrite? Yes
✔  Wrote /home/user/studio/apphosting.yaml
✔  Firebase initialization complete!

✔  Wrote configuration info to firebase.json
✔  Wrote project information to .firebaserc

✔  Firebase initialization complete!

Sync Firebase Workspace & Project

studio-77:~/studio{main}$ firebase use --add
✔ Which project do you want to add? asset-90d38
✔ What alias do you want to use for this project? (e.g. staging) asset-dev

Created alias asset-dev for asset-90d38.
Now using alias asset-dev (asset-90d38)

studio-77:~/studio{main}$ firebase use asset-dev
Updating Studio Workspace active project to match Firebase CLI 'asset-90d38'
Now using alias asset-dev (asset-90d38)

May 6, 2024

Mar 15, 2024

AI means everyone can now be a programmer(?)

Is it that easy?

Assuming you are not a developer and want to create an app to take screenshots on macOS.

You started to ask questions to AI and installed developer tools. Now you are ready for development.

  1. Gemini
  2. How to take a screenshot on macos xcode using swift?

    You paste the code to XCode and see an error!

    Ask Gemini to provide another solution.
    The code you provided in swift didn't work. Suggest another swift way to capture a screenshot.

    Gemini gives an apology and suggests another solution.

    Let's see in XCode.

    Can you see a strange code that contains Arabic letters?

    guard let contentFilter = SCC ู…ุญุชูˆู‰ ุงู„ุดุงุดุฉ(sharingMode: .none) else { return nil }

    You want to switch to ChatGPT to accomplish this easy task with the same prompt. ๐Ÿ‘‡

  3. ChatGPT
  4. You paste the code provided by ChatGPT. Again, there is a problem in the code!

    context.makeCurrent() didn't work. Is there any other way?

    You pasted this code, and it worked! After a few trials, now think you are a programmer.

    What will you do if you see CGDisplayCreateImage(_:) is deprecated?

    So you keep asking for alternative ways, and you learn that there is a shiny framework named ScreenCaptureKit.

    I recommend trying to ask both Gemini and ChatGPT to take a screenshot using ScreenCaptureKit. I tried, on several attempts, AI suggested wrong code each time.

    Then you will decide how easy programming is!