Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
146098 stories
·
33 followers

SwiftUI Editor Architecture (Text, Media, Forms at Scale)

1 Share

Editors are the hardest category of apps to build.

Examples:

  • note apps
  • document editors
  • form builders
  • drawing tools
  • media annotators

They combine:

  • large mutable state
  • frequent updates
  • undo/redo
  • validation
  • autosave
  • performance constraints

This post shows how to design a scalable editor architecture in SwiftUI that stays:

  • fast
  • correct
  • testable
  • maintainable

🧠 The Core Principle

An editor is a state machine with commands.

If you mutate models directly, undo, autosave, and validation become impossible to reason about.

🧱 1. Document Model as the Single Source of Truth

struct Document {
    var title: String
    var blocks: [Block]
    var metadata: Metadata
}

Never bind the UI directly to persistence.

🧬 2. Command-Based Mutations

Every edit becomes a command:

protocol EditorCommand {
    func apply(to doc: inout Document)
    func undo(on doc: inout Document)
}

This enables:

  • undo/redo
  • change tracking
  • batching
  • autosave

🔁 3. Editor Engine

final class EditorEngine: ObservableObject {
    @Published private(set) var document: Document
    let undoStack = UndoStack()

    func perform(_ command: EditorCommand) {
        undoStack.perform(command)
        objectWillChange.send()
    }
}

The engine owns all changes.

🧭 4. Validation Pipeline

enum ValidationState {
    case valid
    case invalid([FieldError])
}

Validate:

  • on commit
  • on autosave
  • on publish

Never inline validation into views.

💾 5. Autosave System

func scheduleAutosave() {
    autosaveTask?.cancel()
    autosaveTask = Task {
        try await Task.sleep(nanoseconds: 2_000_000_000)
        await save()
    }
}

Triggered by:

  • command execution
  • focus loss
  • background entry

🧠 6. Partial Rendering for Performance

Large documents must render lazily:

LazyVStack {
    ForEach(document.blocks) { block in
        BlockView(block: block)
    }
}

Never render entire documents at once.

🧪 7. Testing Editor Behavior

Test commands, not UI:

func testInsertBlock() {
    var doc = Document(...)
    let cmd = InsertBlock(...)
    cmd.apply(to: &doc)

    XCTAssertEqual(doc.blocks.count, 2)
}

The UI becomes trivial.

⚠️ 8. Avoid Direct Bindings

Bad:

TextEditor(text: $document.title)

Good:

TextEditor(text: Binding(
    get: { engine.document.title },
    set: { engine.perform(UpdateTitleCommand($0)) }
))

This enforces consistency.

❌ 9. Common Editor Anti-Patterns

Avoid:

  • direct state mutation
  • UI-driven persistence
  • no command layer
  • no undo
  • no validation
  • full document re-rendering

These don’t scale.

🧠 Mental Model

Think:

User Action
 → Command
   → Editor Engine
     → Document State
       → UI Render

Not:

“The view changes the model”

🚀 Final Thoughts

Editor apps fail when:

  • state is mutated freely
  • undo is bolted on
  • performance is ignored

Command-driven editors:

  • scale
  • remain correct
  • are testable
  • feel professional
Read the whole story
alvinashcraft
25 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Edith Wharton On Writing Fiction

1 Share

We’re writing about the award-winning author, Edith Wharton. In this post, we read about Edith Wharton on writing fiction.

Edith Wharton was born 24 January 1862, and died 11 August 1937

Wharton was an American literary author who wrote novels of manners. She was the author of The Age of InnocenceEthan Frome, The House of Mirth, and The Custom of the Country. She wrote more than 50 books in 40 years, including fiction, short stories, historical novels, travel books, and criticism. Wharton’s stories often included social satire and comedy.

She was known for her stories about upper crust society. In her writing, Wharton was influenced by Henry James, and her themes included social change, ethical issues, and the constraints of upper-class society. In The Age of Innocence, she tells of a romance doomed by duty in 1870s ‘Old New York’. Like many of her other novels, The Age of Innocence has been adapted for film.

Edith Wharton was also a significant writer of ghost stories, drawing from her fear of the genre as a child to create chilling tales like ‘Pomegranate Seed‘, ‘Afterward‘, and ‘The Lady’s Maid’s Bell‘.

Wharton was thrilled to make friends with luminaries such as Henry James, Sinclair Lewis, Jean Cocteau, André Gide, Paul Bourget, and Percy Lubbock.

She was the first woman to receive the Pulitzer Prize for Fiction; an honorary Doctorate from Yale University; and full membership in the Academy of Arts and Letters. She was nominated for the Nobel Prize in Literature more than once.

She also wrote an autobiography, A Backward Glance, and penned The Writing of Fiction. ‘In The Writing of Fiction, Wharton provides general comments on the roots of modern fiction, the various approaches to writing a piece of fiction, and the development of form and style. She also devotes entire chapters to the telling of a short story, the construction of a novel, and the importance of character and situation in the novel.’ (via)

In this post, I will share some of her best writing advice.

Edith Wharton On Writing Fiction

1. The Middle Is The Hardest Part

‘What is writing a novel like?
1. The beginning: A ride through a spring wood.
2. The middle: The Gobi desert.
3. The end: A night with a lover.
I am now in the Gobi desert.’ (Diary entry from 1934, when working on The Buccaneers via Penguin Random House)

2. Have A New Vision

‘True originality consists not in a new manner but in a new vision. That new, that personal, vision is attained only by looking long enough at the object represented to make it the writer’s own.’ (The Writing of Fiction by Edith Wharton)

3. Don’t Be Afraid To Use The Same Plots

‘Another unsettling element in modern art is that common symptom of immaturity, the dread of doing what has been done before.’ (The Writing of Fiction by Edith Wharton)

‘Every dawning talent has to go through a phase of imitation and subjection to influences, and the great object of the young writer should be not to fear those influences, but to seek only the greatest, and to assimilate them so they become [her] stock-in-trade.’ (From a letter, 1918)

4. Immerse The Reader In The Story

‘The least touch of irrelevance, the least chill of inattention, will instantly undo the spell, and it will take as long to weave again as to get Humpty Dumpty back on his wall.’ (The Writing of Fiction by Edith Wharton)

5. Believe In Yourself

‘At last I had groped my way through to my vocation, and thereafter never questioned that storytelling was my job … The Land of Letters was hence forth to be my country and I gloried in my new citizenship.’ (From her autobiography, A Backward Glance. She describes how she felt at the publication of her first collection of short stories.)

‘My literary success puzzled and embarrassed my old friends far more than it impressed them, and in my own family it created a kind of constraint which increased with the years. None of my relations ever spoke to me of my books, either to praise or to blame — they simply ignored them …
At first I felt this indifference acutely; but now I no longer cared, for my recognition as a writer had transformed my life.’ (From her autobiography, A Backward Glance)

‘I have often wondered, in looking back at the slow stammering beginnings of my literary life, whether or not it is a good thing for the creative artist to grow up in an atmosphere where the arts are simply non-existent. Violent opposition might be a stimulus–but was it helpful or the reverse to have every aspiration ignored, or looked at askance?’ (From her autobiography, A Backward Glance)

6. Find Your Tribe

‘My long experimenting had resulted in two or three books which brought me more encouragement than I had ever dreamed of obtaining, and were the means of my making some of the happiest friendships of my life.
The reception of my books gave me the self-confidence I had so long lacked, and in the company of people who shared my tastes, and treated me as their equal, I ceased to suffer from the agonizing shyness which used to rob such encounters of all pleasure.’ (From her autobiography, A Backward Glance)

7. Create Your Characters For The Format

‘No subject in itself, however fruitful, appears to be able to keep a novel alive; only the characters in it can. Of the short story the same cannot be said. Some of the greatest short stories owe their vitality entirely to the dramatic rendering of a situation.’ (The Writing of Fiction by Edith Wharton)

8. Take The Critics With A Pinch Of Salt

‘After all, one knows one’s weak points so well, that it’s rather bewildering to have the critics overlook them & invent others that (one is fairly sure) don’t exist — or exist in a less measure.’ (Letter to Robert Grant, 19 November 1907)

9. Prevent Author Intrusion

‘I have never known a novel that was good enough to be good in spite of its being adapted to the author’s political views.’ (Letter to Upton Sinclair, 19 August 1927)

10. Let Your Characters’ Changes Seem Natural

‘The other difficulty is that of communicating the effect of the gradual passage of time in such a way that the modifying and maturing of the characters shall seem not an arbitrary sleight-of-hand but the natural result of growth in age and experience. This is the great mystery of the art of fiction. The secret seems incommunicable; one can only conjecture that it has to do with the novelist’s own deep belief in his characters and what he is telling about them.’ (The Writing of Fiction by Edith Wharton)

11. Work With A Mentor

‘He [Walter Berry] alone not only encouraged me to write, as others had already done, but had the patience and the intelligence to teach me how. Others praised, some flattered–he alone took the trouble to analyze and criticise. The instinct to write had always been there; it was he who drew it forth, shaped it and set it free.’ (From her autobiography, A Backward Glance)

12. Have Something Important To Say

‘A good subject, then, must contain in itself something that sheds a light on our moral experience. If it is incapable of this expansion, this vital radiation, it remains, however showy a surface it presents, a mere irrelevant happening, a meaningless scrap of fact torn out of its context.’ (The Writing of Fiction by Edith Wharton)

13. Embrace Ideas

‘Ah, good conversation – there’s nothing like it, is there? The air of ideas is the only air worth breathing.’ (The Age of Innocence by Edith Wharton)

Source for photograph: E. F. Cooper, Newport, Rhode Island, Public domain, via Wikimedia Commons

Amanda Patterson

by Amanda Patterson

If you enjoyed this post, you will love:

  1. Elizabeth Strout’s Writing Process
  2. Harlan Coben On Writing Suspense
  3. Brandon Sanderson’s 3 Rules For Magic
  4. 10 Bits Of Writing Advice From Colson Whitehead
  5. 9 Bits Of Writing Advice From Naomi Alderman
  6. 9 Bits Of Writing Advice From Guillermo del Toro
  7. Douglas Adams On The Difficulties Of Writing
  8. 6 Bits Of Writing Advice From Richard Osman
  9. 5 Bits Of Writing Advice From Kathy Reichs
  10. Writing Advice From The World’s Most Famous Authors

Top Tip: Find out more about our workbooks and online courses in our shop.

The post Edith Wharton On Writing Fiction appeared first on Writers Write.

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete

White House caught altering press release photos

1 Share
Image via @WhiteHouse on Twitter

With the Trump administration trying so hard to alter reality by destroying information on once useful government websites, holding a line of denialist bullshit on the murder and detainment of American citizens by overzealous Gravy SEALs, and insisting that tariffs are good for the American people, it's not surprising to see the greasy buggers caught in another lie. — Read the rest

The post White House caught altering press release photos appeared first on Boing Boing.

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete

A Practical Demo of Zero-Downtime Migrations Using Password Hashing

1 Share

Push updates live faster with RunPod
Spin up compute and storage in seconds with RunPod. Deploy serverless inference that scales automatically, with no manual tuning or capacity planning. Push updates live, iterate fast, and monitor everything in one place with logs, and metrics built in. RunPod is infrastructure for AI startups and technical teams who want to stay in flow, move fast, and spend less time on cloud plumbing and more time shipping real products. Explore Runpod

Data Engineering Design Patterns - Book Giveaway from Buf
Buf Schema Registry brings type safety to data pipelines with Protobuf. Manage schemas centrally, validate changes, and generate Python code automatically. Plus: Buf is giving away an O'Reilly Media book, Data Engineering Design Patterns with proven solutions to idempotency, error handling, observability challenges and more. Get your copy now.

Security requirements evolve. What was considered "secure enough" five years ago might not pass a security audit today.

You need to upgrade to a modern algorithm like Argon2 or Bcrypt. But here is the problem: hashing is a one-way operation. You cannot reverse-engineer the existing hashes to "upgrade" them.

If you simply swap your IPasswordHasher implementation, you break the application. Every single existing user who tries to log in will fail authentication because your new hasher doesn't understand the old format.

In this article, I want to demo a zero-downtime migration concept in practice.

Real systems have more constraints (and you should not build auth from scratch). But this is a clean example of a pattern you can reuse for database migrations:

  • Move from old format to new format
  • Keep existing behavior working
  • Gradually migrate data
  • Delete legacy only when you are done

Let's dive in.

The Naive Approach and Why It Fails

Let's imagine you have a simple authentication system. You want to replace your legacy PBKDF2 hasher with a standard Argon2 implementation.

You might think, "I'll just register the new implementation in the dependency injection container."

// Switching from LegacyHasher to ModernHasher
builder.Services.AddSingleton<IPasswordHasher, ModernHasher>();

Here is the failure scenario:

  1. New Users: They register and log in perfectly. Their passwords are hashed with Argon2 from day one.
  2. Existing Users: A user enters their correct password. The system fetches the old PBKDF2 hash from the database.
  3. The Crash: The ModernHasher tries to verify the PBKDF2 hash. It fails immediately, returning 401 Unauthorized.

You have inadvertently locked out your entire user base. We need a way to support both algorithms simultaneously without making the login code a mess.

The Solution: Migration on Login

The strategy is simple: we don't migrate the database in a batch job. We migrate users lazily when they prove their identity.

The flow looks like this:

  1. Attempt 1: Try to verify the password using the New algorithm.
  2. Attempt 2 (Fallback): If that fails, check if the Legacy algorithm can verify it.
  3. The Migration: If the Legacy verification succeeds:
    • Log the user in (Success).
    • Immediately re-hash their password using the New algorithm.
    • Update the database record.

Future logins for this user will now succeed via the standard flow.

A sequence diagram showing the migration on login flow, with attempts to verify using new and legacy hashers.

Implementation with .NET Keyed Services

In .NET 8, Microsoft introduced Keyed Services, which are perfect for this scenario. They allow us to register multiple implementations of the same interface and retrieve them by name.

1. Registering the Services

We register both hashers in our Program.cs, assigning them unique keys:

// Register the implementations with specific keys
builder.Services.AddKeyedSingleton<IPasswordHasher, Pbdkf2PasswordHasher>("legacy");
builder.Services.AddKeyedSingleton<IPasswordHasher, Argon2PasswordHasher>("modern");

// (Optional) Register the modern one as the default for other services
builder.Services.AddSingleton<IPasswordHasher, Argon2PasswordHasher>();

2. The Login Command Handler

Now we implement the migration logic. We inject both hashers using the [FromKeyedServices] attribute.

public class LoginCommandHandler(
    IUserRepository userRepository,
    [FromKeyedServices("modern")] IPasswordHasher newHasher,
    [FromKeyedServices("legacy")] IPasswordHasher legacyHasher)
{
    public async Task<AuthenticationResult> Handle(LoginCommand command)
    {
        var user = await userRepository.GetByEmailAsync(command.Email);
        if (user is null)
        {
            return AuthenticationResult.Fail();
        }

        // 1. Try the new algorithm first (Happy Path)
        if (newHasher.Verify(user.PasswordHash, command.Password))
        {
            return AuthenticationResult.Success(user);
        }

        // 2. Fallback: Check if it's a legacy hash
        if (legacyHasher.Verify(user.PasswordHash, command.Password))
        {
            // 3. MIGRATION STEP: Re-hash and save
            var newHash = newHasher.Hash(command.Password);

            user.UpdatePasswordHash(newHash);
            await userRepository.SaveChangesAsync();

            return AuthenticationResult.Success(user);
        }

        return AuthenticationResult.Fail();
    }
}

This code ensures that active users are automatically upgraded. After a few months, the vast majority of your user base will be on the new algorithm.

Real-World Improvements

While the implementation above works, here are two improvements to make it production-ready.

1. Algorithm Prefixes

Relying on "trial and error" verification works, but it's cleaner to know exactly which algorithm was used to create a hash.

Standard algorithms often include a prefix (e.g., Bcrypt starts with $2a$ or $2b$). You can use this to route the request efficiently:

public bool IsLegacyHash(string hash)
{
    // This assumes we're storing a prefix for PBKDF2 hashes. Something to consider.
    return hash.StartsWith("pbkdf2$");
}

Another benefit this unlocks is being able to query the database for users still on the legacy format.

2. Feature Flags

Performing a database write during a login request adds latency. If you have high traffic, you might want to control this roll-out.

By wrapping the migration logic behind a Feature Flag, you can disable the "write" step if your database comes under load, while still allowing users to log in via the read-only fallback.

if (await featureManager.IsEnabledAsync(FeatureFlags.MigratePasswords) &&
    legacyHasher.Verify(user.PasswordHash, command.Password))
{
    // Perform migration...
}

Finishing the Migration

After you run this for a while (usually a few months), most active accounts will be upgraded. You can then run a cleanup script to identify any remaining legacy hashes and force those users to reset their passwords on the next login attempt.

At that point, you can remove:

  • The legacy hasher registration
  • The legacy verification code path
  • The feature flag

And the migration is complete.

Summary

A "simple" hashing upgrade is really a data migration. This article is about the migration pattern. Not about reinventing auth.

The zero-downtime pattern looks like this:

  1. New format for new writes
  2. Support both formats for reads
  3. Migrate old data gradually (migrate-on-login is a great trick)
  4. Put it behind a feature flag
  5. Delete legacy when you are done

By allowing the old and new formats to coexist for a period of time, you achieve a seamless transition. Once your monitoring shows that 99% of active users have migrated, you can identify the users on the legacy format and force a password reset on their next attempt.

If you want to see a practical demo of this, check out this video I made.

Hope this was helpful!




Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete

DiffLog - The Laziest Way to Write Professional Changelogs

1 Share
<p>Just shipped a new version - code is deployed, tests passed, and now, time to look at the diff from the last release. Time to write a changelog that consists of more than just “fix stuff”, “wip”, “actually fix it this time”, “fixed the fixes” and “please work”. Now turn that into professional release notes. For developers. And end users. And maybe a tweet. And your boss wants an executive summary too. We all know how this ends - you either spend an hour crafting the perfect changelog, or you just dump the git log and call it a day.</p> <h1 id="tl;dr">TL;DR<a title="#tl;dr" href="#tl;dr"></a></h1> <p>I built DiffLog - a CLI tool that turns your git history into polished release notes using LLMs. It generates changelogs for different audiences and formats automatically. DiffLog open source and available on GitHub: <a href="https://github.com/nor0x/difflog" target="_blank">github.com/nor0x/difflog</a> and can be installed via <a href="https://www.nuget.org/packages/difflog" target="_blank">NuGet</a></p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dotnet tool install -g DiffLog</span><br></pre></td></tr></table></figure> <p><img src="2026/01/DiffLog-The-Laziest-Way-to-Write-Professional-Changelogs/screenshot.jpg" alt="" loading="lazy" class="φbp"></p> <h1 id="because-your-git-log-isn't-a-changelog">Because Your Git Log Isn’t a Changelog<a title="#because-your-git-log-isn't-a-changelog" href="#because-your-git-log-isn't-a-changelog"></a></h1> <p>I’m currently building several new products and apps, and they all need proper changelogs. But here’s the thing - different people care about different things. Developers want to know about API changes and breaking changes. End users want to know about new features and bug fixes in plain language. Marketing wants something snappy for social media. And executives want a high-level summary without the technical jargon.</p> <p>Writing the same changelog four times? Nope! That’s where LLMs come in - guess what, they’re really good at taking a bunch of commit messages and turning them into coherent, audience-appropriate release notes. I decided to build a tool that automates this entire process.</p> <h1 id="introducing-difflog">Introducing DiffLog<a title="#introducing-difflog" href="#introducing-difflog"></a></h1> <p>DiffLog is a CLI tool that scans your git history, groups changes into clear sections, and uses AI to format the result for different audiences. The key idea is simple: your commits already contain all the information needed for a changelog - it just needs to be transformed into something readable.</p> <h2 id="generate-from-anywhere-in-your-history">Generate from anywhere in your history<a title="#generate-from-anywhere-in-your-history" href="#generate-from-anywhere-in-your-history"></a></h2> <p>DiffLog can work with tags, commit ranges, or date filters:</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">difflog generate --from v1.0.0 --to v1.1.0 <span class="comment"># between tags</span></span><br><span class="line">difflog generate --from HEAD~20 <span class="comment"># last 20 commits</span></span><br><span class="line">difflog generate --from-date 2026-01-01 <span class="comment"># since a date</span></span><br><span class="line">difflog generate -i <span class="comment"># interactive mode</span></span><br></pre></td></tr></table></figure> <p>the interactive mode is particularly nice for exploring your options - it guides you through selecting references and output settings.</p> <h2 id="multiple-audiences,-one-command">Multiple audiences, one command<a title="#multiple-audiences,-one-command" href="#multiple-audiences,-one-command"></a></h2> <p>This is where DiffLog really shines. You can generate notes tailored to specific audiences:</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">difflog generate -a Developers --format Markdown</span><br><span class="line">difflog generate -a EndUsers --format Html -o release-notes.html</span><br><span class="line">difflog generate -a SocialMedia --format PlainText</span><br><span class="line">difflog generate -a Executive --format Markdown</span><br></pre></td></tr></table></figure> <p>each audience gets a different perspective on the same changes:</p> <ul> <li><strong>Developers</strong>: Technical details, API changes, breaking changes, migration notes</li> <li><strong>EndUsers</strong>: User-facing features and improvements in plain language</li> <li><strong>SocialMedia</strong>: Snappy, engaging summaries perfect for tweets or announcements</li> <li><strong>Executive</strong>: High-level business impact without the technical jargon</li> </ul> <h2 id="output-formats">Output formats<a title="#output-formats" href="#output-formats"></a></h2> <p>DiffLog supports multiple output formats to fit your workflow:</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">difflog generate --format Markdown <span class="comment"># great for GitHub releases</span></span><br><span class="line">difflog generate --format Html <span class="comment"># for documentation sites</span></span><br><span class="line">difflog generate --format PlainText <span class="comment"># for emails or simple READMEs</span></span><br><span class="line">difflog generate --format Json <span class="comment"># for programmatic processing</span></span><br></pre></td></tr></table></figure> <h2 id="extra-goodies">Extra goodies<a title="#extra-goodies" href="#extra-goodies"></a></h2> <p>By default, DiffLog includes links to issues/PRs and a contributor list. You can toggle these off if you prefer a cleaner output:</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">difflog generate --no-links <span class="comment"># skip issue/PR links</span></span><br><span class="line">difflog generate --no-contributors <span class="comment"># skip contributor list</span></span><br><span class="line">difflog generate --exclude <span class="string">&quot;chore,docs&quot;</span> <span class="comment"># exclude certain categories</span></span><br></pre></td></tr></table></figure> <h1 id="custom-system-prompts">Custom system prompts<a title="#custom-system-prompts" href="#custom-system-prompts"></a></h1> <p>The audience and format options are built-in and should be sufficient for most general use cases. But this information can be fully customized using your own system prompts. You can override the prompt by either passing it directly on the command line or loading it from a file:</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># store a custom prompt for an audience</span></span><br><span class="line">difflog config --audience Developers --system-prompt-file prompts/dev.txt</span><br><span class="line"></span><br><span class="line"><span class="comment"># or override just for this run</span></span><br><span class="line">difflog generate --system-prompt <span class="string">&quot;Focus on security improvements and performance gains&quot;</span></span><br><span class="line">difflog generate --system-prompt-file prompts/social.txt</span><br></pre></td></tr></table></figure> <p>this can be incredibly useful when you have specific formatting requirements or want to emphasize certain aspects of your release. The prompt resolution order is:</p> <ol> <li>Command-line <code>--system-prompt</code> or <code>--system-prompt-file</code></li> <li>Stored prompt for the selected audience</li> <li>Built-in defaults</li> </ol> <h1 id="github-actions-integration">GitHub Actions integration<a title="#github-actions-integration" href="#github-actions-integration"></a></h1> <p>This is where I have integrated DiffLog into my workflow. I use it in my CI/CD pipelines to automatically generate release notes whenever i push a new tag:</p> <figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">Generate</span> <span class="string">Release</span> <span class="string">Notes</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line"> <span class="attr">push:</span></span><br><span class="line"> <span class="attr">tags:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">&#x27;v*&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line"> <span class="attr">release-notes:</span></span><br><span class="line"> <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line"> <span class="attr">steps:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v4</span></span><br><span class="line"> <span class="attr">with:</span></span><br><span class="line"> <span class="attr">fetch-depth:</span> <span class="number">0</span> <span class="comment"># Required for full git history</span></span><br><span class="line"></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Setup</span> <span class="string">.NET</span></span><br><span class="line"> <span class="attr">uses:</span> <span class="string">actions/setup-dotnet@v4</span></span><br><span class="line"> <span class="attr">with:</span></span><br><span class="line"> <span class="attr">dotnet-version:</span> <span class="string">&#x27;10.0.x&#x27;</span></span><br><span class="line"></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Install</span> <span class="string">DiffLog</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">dotnet</span> <span class="string">tool</span> <span class="string">install</span> <span class="string">-g</span> <span class="string">DiffLog</span></span><br><span class="line"></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Generate</span> <span class="string">Release</span> <span class="string">Notes</span></span><br><span class="line"> <span class="attr">env:</span></span><br><span class="line"> <span class="attr">OPENAI_API_KEY:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.OPENAI_API_KEY</span> <span class="string">&#125;&#125;</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string"> PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2&gt;/dev/null || echo &quot;&quot;)</span></span><br><span class="line"><span class="string"> CURRENT_TAG=$&#123;GITHUB_REF#refs/tags/&#125;</span></span><br><span class="line"><span class="string"></span> </span><br><span class="line"> <span class="string">if</span> [ <span class="string">-n</span> <span class="string">&quot;$PREV_TAG&quot;</span> ]<span class="string">;</span> <span class="string">then</span></span><br><span class="line"> <span class="string">difflog</span> <span class="string">generate</span> <span class="string">--from</span> <span class="string">$PREV_TAG</span> <span class="string">--to</span> <span class="string">$CURRENT_TAG</span> <span class="string">--format</span> <span class="string">Markdown</span> <span class="string">-o</span> <span class="string">release-notes.md</span></span><br><span class="line"> <span class="string">else</span></span><br><span class="line"> <span class="string">difflog</span> <span class="string">generate</span> <span class="string">--to</span> <span class="string">$CURRENT_TAG</span> <span class="string">--format</span> <span class="string">Markdown</span> <span class="string">-o</span> <span class="string">release-notes.md</span></span><br><span class="line"> <span class="string">fi</span></span><br><span class="line"></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Create</span> <span class="string">GitHub</span> <span class="string">Release</span></span><br><span class="line"> <span class="attr">uses:</span> <span class="string">softprops/action-gh-release@v2</span></span><br><span class="line"> <span class="attr">with:</span></span><br><span class="line"> <span class="attr">body_path:</span> <span class="string">release-notes.md</span></span><br></pre></td></tr></table></figure> <p>you can also generate weekly changelogs or PR-based summaries:</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">difflog generate --from-date $(<span class="built_in">date</span> -d <span class="string">&#x27;7 days ago&#x27;</span> +%Y-%m-%d) --format Markdown -o changelog.md</span><br></pre></td></tr></table></figure> <p>You can check out the <a href="https://github.com/nor0x/difflog" target="_blank">DiffLog GitHub repository</a> for the actions and how the tool is used in the CI and publish workflows.</p> <h1 id="configuration">configuration<a title="#configuration" href="#configuration"></a></h1> <p>DiffLog reads configuration from environment variables or a saved config file:</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">difflog config --api-key sk-... --model gpt-4o</span><br><span class="line">difflog config -i <span class="comment"># interactive setup</span></span><br></pre></td></tr></table></figure> <p>environment variables take precedence, which is perfect for CI/CD:</p> <ul> <li><code>OPENAI_API_KEY</code></li> <li><code>OPENAI_BASE_URL</code> (for Azure OpenAI or compatible APIs)</li> <li><code>OPENAI_MODEL</code></li> </ul> <p>you can also list available tags in any repository:</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">difflog tags --path .</span><br><span class="line"></span><br><span class="line">── 📝 Tags ───────────────────────────────────────────</span><br><span class="line"></span><br><span class="line">v1.0.0</span><br><span class="line">v1.1.0</span><br><span class="line">v1.2.0</span><br><span class="line">v2.0.0</span><br></pre></td></tr></table></figure> <h1 id="tech-stack">tech stack<a title="#tech-stack" href="#tech-stack"></a></h1> <p>Building DiffLog was a fun exercise in combining CLI development with AI integration. Here’s what i used:</p> <ul> <li><strong>.NET 10</strong> - the core runtime and NuGet packaging</li> <li><strong>Spectre.Console</strong> - for the terminal UI and formatted tables</li> <li><strong>Spectre.Console.Cli</strong> - for command-line parsing</li> <li><strong><a href="http://Microsoft.Extensions.AI">Microsoft.Extensions.AI</a></strong> - for the AI abstraction layer</li> <li><strong>OpenAI client</strong> - for LLM integration (might add support for more providers later)</li> <li><strong>OpenCode</strong> - Claude Opus 4.5 was my coding partner throughout the project</li> </ul> <p><a href="https://spectreconsole.net/" target="_blank">Spectre.Console</a> continues to be my go-to for building CLI applications in .NET. The interactive prompts and formatted output make the tool feel polished and professional.</p> <h1 id="start-generating!-📝">Start generating! 📝<a title="#start-generating!-📝" href="#start-generating!-📝"></a></h1> <p>If you’re tired of writing changelogs manually (or not writing them at all), give DiffLog a try:</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">dotnet tool install -g DiffLog</span><br><span class="line">difflog generate -i</span><br></pre></td></tr></table></figure> <h1 id="improvements">Improvements<a title="#improvements" href="#improvements"></a></h1> <p>I might add support for other LLM providers such as Anthropic or Google in the future - just tried to keep the initial version focused and tailored for my own needs first. Feedback and contributions are very welcome! The source code is on GitHub at <a href="https://github.com/nor0x/difflog" target="_blank">github.com/nor0x/difflog</a> - feel free to play with it, fork it, and contribute. I’m actively using this for all my projects now, and it’s been a huge time saver.</p>
Read the whole story
alvinashcraft
2 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Agentic AI Foundation Announced

1 Share
Read the whole story
alvinashcraft
2 minutes ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories