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

AGL 481: Rob Snyder and The Power of PULL

1 Share

About Rob

The-Power-of-PullRob Snyder is a serial startup founder and a fellow at Harvard Innovation Labs. He graduated from Harvard Business School and previously worked at McKinsey & Company. He is also an entrepreneur-in-residence and venture partner for early-stage venture capital funds. Snyder lives with his wife and daughter in New Hampshire. 


Today We Talked About

  • Rob’s background
  • Entrepreneurship is hard
  • PULL Framework
    • P = Problem (the customer has a real problem or project they need to accomplish)
    • U = Unavoidable (they can’t simply ignore it or postpone it indefinitely)
    • L = Limitations (existing solutions have meaningful shortcomings)
    • L = Looking (the customer is actively looking for a better solution)
  • Painpoints and Problems
  • Start-up Journey
  • What is real demand?
  • Demand exsist out there… you just have to find it
  • What to do if your product isn’t gaining traction?
  • Bottom-up Model
  • Guide to staying sane as a founder
  • Serve people that are stuck

Connect with Rob


Leave me a tip $
Click here to Donate to the show


I hope you enjoyed this show, please head over to Apple Podcasts and subscribe and leave me a rating and review, even one sentence will help spread the word.  Thanks again!





Download audio: https://media.blubrry.com/a_geek_leader_podcast__/mc.blubrry.com/a_geek_leader_podcast__/AGL_481_Rob_Snyder_and_The_Power_of_PULL.mp3?awCollectionId=300549&awEpisodeId=12125592&aw_0_azn.pgenre=Business&aw_0_1st.ri=blubrry&aw_0_azn.pcountry=US&aw_0_azn.planguage=en&cat_exclude=IAB1-8%2CIAB1-9%2CIAB7-41%2CIAB8-5%2CIAB8-18%2CIAB11-4%2CIAB25%2CIAB26&aw_0_cnt.rss=https%3A%2F%2Fwww.ageekleader.com%2Ffeed%2Fpodcast
Read the whole story
alvinashcraft
12 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Episode 584: It’s All God Throwing Dice

1 Share

This week, we discuss the Situational Awareness meltdown, Airtable getting sold, and review the Java documentary. Plus, what you should actually buy at IKEA.

Watch the YouTube Live Recording of Episode 584

Runner-up Titles

  • Duffle bag of leftovers
  • 5Are you dead in this scenario?
  • Bury me with my IKEA bags
  • Don’t automate the world, nobody really wants that
  • AttentionIsAllYouNeed.blog
  • Take some money off the table
  • No more great ideas

Rundown

Relevant to your Interests

Sponsors

Conferences

SDT News & Community

Recommendations

Sponsored By:





Download audio: https://aphid.fireside.fm/d/1437767933/9b74150b-3553-49dc-8332-f89bbbba9f92/26662984-45b4-4089-88dc-d2c543005e99.mp3
Read the whole story
alvinashcraft
22 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Writing Effective Copilot Instructions for Complex Codebases

1 Share

GitHub Copilot's custom instructions feature lets you give the AI context about your project. For a small repository, a single copilot-instructions.md file with build commands and a few conventions is enough. But what happens when your codebase has 500,000+ lines of code, 19 projects, four target frameworks, two code generation pipelines, and domain-specific patterns that span everything from zero-allocation buffer management to ECMA-262 regex translation?

We hit that wall with Corvus.Text.Json. A flat instructions file couldn't capture the depth of knowledge an AI assistant needs to work effectively across the whole codebase. We ended up building something more structured: a library of 20 modular skill files alongside a main instructions document. In total, that gave us 2,438 lines of AI-consumable context.

This post walks through what we built, the design principles behind it, and how you can apply the same approach to your own projects.

If you're interested in the broader landscape of AI-assisted coding tools, my colleague Mike Evans-Larah wrote an excellent overview: AI-assisted coding is four decisions, not one. This post goes deep on one of those decisions - the "Capabilities" layer, specifically how you structure the instructions and knowledge that shape what your AI assistant can do.

The building blocks

Before diving into what we built, here's a quick summary of what GitHub gives you to work with. There are three official instruction types:

Type Location Scope
Repository-wide .github/copilot-instructions.md Every request in the repo
Path-specific .github/instructions/*.instructions.md Requests involving files matching a glob pattern
Agent instructions AGENTS.md (anywhere in the repo) Nearest file in directory tree takes precedence

Path-specific instructions need an applyTo field in their YAML frontmatter to specify which files they apply to:

---
applyTo: "**/*.cs"
---
Use 4-space indentation and file-scoped namespaces.

Separately from these three instruction types, VS Code and Copilot CLI support skills. These are markdown files in .github/skills/<name>/SKILL.md that the assistant can discover and invoke by name. Each skill has a YAML frontmatter block with a name and description, and the assistant decides which skills are relevant to the current task. Skills are not the same as repository custom instructions. They're a distinct mechanism, and not all Copilot surfaces support them equally.

Skills are the key building block for what we've done. They let you decompose a large codebase's knowledge into modular, independently-addressable units.

What we built

Our Corvus.Text.Json repository has this structure:

.github/
├── copilot-instructions.md          (383 lines - the essentials)
└── skills/
    ├── corvus-analyzers/SKILL.md          (68 lines)
    ├── corvus-benchmarks/SKILL.md         (89 lines)
    ├── corvus-bowtie-testing/SKILL.md    (121 lines)
    ├── corvus-buffer-and-pooling/SKILL.md (163 lines)
    ├── corvus-build-and-test/SKILL.md     (98 lines)
    ├── corvus-codegen/SKILL.md           (111 lines)
    ├── corvus-docs-website/SKILL.md       (87 lines)
    ├── corvus-ecma-regex/SKILL.md         (74 lines)
    ├── corvus-keywords-and-validation/SKILL.md (119 lines)
    ├── corvus-low-alloc-data-structures/SKILL.md (190 lines)
    ├── corvus-mutable-documents/SKILL.md  (89 lines)
    ├── corvus-numeric-types/SKILL.md      (74 lines)
    ├── corvus-parsed-documents-and-memory/SKILL.md (99 lines)
    ├── corvus-query-languages/SKILL.md   (138 lines)
    ├── corvus-standalone-evaluator/SKILL.md (108 lines)
    ├── corvus-test-suite-regeneration/SKILL.md (65 lines)
    ├── corvus-v4-migration/SKILL.md      (103 lines)
    ├── corvus-yaml/SKILL.md               (71 lines)
    ├── ref-struct-delegates/SKILL.md     (112 lines)
    └── reviewing-skills/SKILL.md          (76 lines)

The main copilot-instructions.md covers the essentials that apply to every task: build commands, project overview, architecture overview, key conventions, and the stackalloc/ArrayPool rent pattern. It's what every interaction needs.

The 20 skill files cover specific concerns. When a developer asks the assistant to help with mutable documents, the assistant loads the corvus-mutable-documents skill. When they're debugging benchmarks, it loads corvus-benchmarks. The assistant only pays attention to the knowledge relevant to the current task.

What a skill looks like

Here's our corvus-mutable-documents skill, condensed to show the structure (the full file is on GitHub):

---
name: corvus-mutable-documents
description: >
  Create and manipulate mutable JSON documents using JsonWorkspace,
  JsonDocumentBuilder, and the builder pattern. Covers workspace creation
  (rented vs unrented), the canonical parse-build-mutate-serialize pattern,
  deep property mutation, array operations, cloning, and RFC 6902 JSON Patch
  via PatchBuilder. USE FOR: writing code that creates or modifies JSON,
  understanding the V5 mutation model, implementing JSON Patch operations,
  working with JsonWorkspace. DO NOT USE FOR: read-only parsing
  (use corvus-parsed-documents-and-memory), V4 mutation patterns
  (use corvus-v4-migration).
---

# Mutable Documents

## JsonWorkspace

A scoped container for pooled memory used during mutable JSON operations.

```csharp
// Preferred - rents from thread-local cache
using JsonWorkspace workspace = JsonWorkspace.Create();

// When you need explicit lifetime control
JsonWorkspace workspace = JsonWorkspace.CreateUnrented();
```

Always use a `using` block. `Dispose()` returns the workspace to the
thread-local cache (rented) or disposes all child documents (unrented).

## Canonical Mutation Pattern

```csharp
using JsonWorkspace workspace = JsonWorkspace.Create();
using ParsedJsonDocument<JsonElement> sourceDoc =
    ParsedJsonDocument<JsonElement>.Parse(json);

// Convert immutable → mutable
using JsonDocumentBuilder<JsonElement.Mutable> builder =
    sourceDoc.RootElement.CreateBuilder(workspace);

JsonElement.Mutable root = builder.RootElement;

// Mutate
root.SetProperty("name"u8, "new value"u8);
root.RemoveProperty("oldProp"u8);

// Serialize
string result = root.ToString();
```

## Multiple Builders Per Workspace
## Cloning
## JSON Patch (RFC 6902)

## Common Pitfalls
- Forgetting to dispose workspace/builder
- Using stale element references after mutation
- Not disposing BeginPatch() via GetPatchAndDispose()

## Cross-References
- For read-only parsing, see `corvus-parsed-documents-and-memory`
- For dispose analyzers (CTJ004-006), see `corvus-analyzers`
- For V4→V5 mutation model changes, see `corvus-v4-migration`

Every section is self-contained, with copy-paste-ready code blocks, a pitfalls section that catches common mistakes before they happen, and cross-references that tell the assistant where to look next if the task spans multiple concerns.

Design principles

These principles emerged through months of iterating on the instructions as we used them. They weren't designed upfront. They're patterns we noticed working well and then applied consistently.

Modular by concern

Each skill covers exactly one concern. corvus-buffer-and-pooling covers the stackalloc/ArrayPool pattern. corvus-ecma-regex covers regex translation. corvus-mutable-documents covers the mutation model. No skill tries to do everything.

This matters because AI context windows are finite. When the assistant loads a skill, it gets 55–190 lines of focused, relevant knowledge. It does not get a 2,300-line wall of text where the relevant paragraph is buried somewhere in the middle.

Scope boundaries

Every skill has explicit "USE FOR" and "DO NOT USE FOR" fields in its YAML description:

description: >
  Build, test, and run the Corvus.JsonSchema solution correctly. ...
  USE FOR: building the solution, running tests, diagnosing test failures,
  understanding TFM targeting, finding the right test project for a feature area.
  DO NOT USE FOR: benchmark execution (use corvus-benchmarks), code generation
  (use corvus-codegen), test suite regeneration (use corvus-test-suite-regeneration).

The "DO NOT USE FOR" entries are arguably more important than the "USE FOR" entries. They prevent the assistant from hallucinating advice about benchmarks when it's loaded the build-and-test skill. And each redirect points to the correct skill by name, so the assistant can load the right one instead.

Cross-reference network

Skills cross-reference each other, forming a network. Some are hub skills referenced by many others:

  • corvus-build-and-test - referenced from benchmarks, Bowtie testing, codegen, docs, query languages
  • corvus-parsed-documents-and-memory - referenced from buffers, low-alloc structures, mutable documents, YAML, numerics
  • corvus-keywords-and-validation - referenced from codegen, standalone evaluator, regex, numerics, test suite regeneration

Other skills are leaves, focused on narrow concerns. The network prevents duplication: the buffer pooling pattern is defined once in corvus-buffer-and-pooling and referenced from everywhere that uses it. Some pairs reference each other bidirectionally (for instance, corvus-parsed-documents-and-memory and corvus-mutable-documents), which is fine - they're complementary concerns that often come up together.

Code-first examples

Every skill includes copy-paste-ready code blocks with real syntax and real file paths. They are not pseudocode or abstract descriptions, but actual commands that work in the repository as-is:

# Run all tests (standard)
dotnet test Corvus.Text.Json.Test.slnx \
  --filter "category!=failing&category!=outerloop"

# Run a single test class
dotnet test Corvus.Text.Json.Test.slnx \
  --filter "FullyQualifiedName~ParsedJsonDocumentTests&category!=failing&category!=outerloop"

This is critical. An assistant that generates a test command missing the mandatory category filters will watch the build fail for minutes before figuring out what went wrong. The skill gives it the exact command that works.

Truth-seeking instructions

The codebase has areas where the surface API has changed over time, and documentation can drift. Rather than trying to keep instructions perfectly synchronised, we tell the assistant to verify:

IMPORTANT: When writing documentation, examples, or instructions that reference Source Generator attributes or CLI tool options, always verify the exact parameter names and types by checking the source code.

And in the codegen skill:

IMPORTANT: Never invent option names. Verify against GenerateCommand.cs.

This is a different philosophy from "document everything perfectly." Instead, we tell the assistant: "here's approximately what exists, and here's where to check the ground truth." It's more resilient to drift than trying to maintain a perfect mirror of every API surface.

Canonical patterns

For operations that have multiple valid approaches, we document the one way we want the assistant to use. Our mutable documents skill shows a single canonical parse→build→mutate→serialize pattern. The buffer-and-pooling skill shows exactly one rent/return pattern with try/finally.

This reduces decision fatigue. Without a canonical pattern, the assistant might generate three different approaches across three files, all technically correct but inconsistent. With one, every generated code block follows the same shape.

Configuration tables

We use tables to centralise reference data:

Solution Purpose
Corvus.Text.Json.slnx Main V5 solution - libraries + tests (use for dotnet build)
Corvus.Text.Json.Test.slnx Tests only (use for dotnet test)
Corvus.Text.Json.Benchmarks.slnx Benchmark projects only

Tables are efficient in context windows and easy to scan. They also force you to be precise. You can't hand-wave in a table cell.

How instructions evolve

These instructions aren't write-once. They evolve as we work with the codebase, through three feedback loops.

The memory system

Copilot CLI stores facts it learns during sessions. When it discovers something the hard way, such as "always use FullyQualifiedName not ClassName in test filters", it stores that as a memory. Next session, it checks those memories before acting. Over time, important facts bubble up into the instructions or skills themselves.

The verification loop

The most effective feedback loop is simple: use the assistant and watch what it does, then fix the instructions when it gets something wrong.

When an assistant generates a test command without mandatory category filters, that's a sign the build-and-test skill needs a more prominent warning. When it uses ParseValue instead of Parse in a code example, that's a convention that needs to be stated explicitly. When it tries to add a file to a project without an explicit <Compile Include> entry, that tells you the "no glob includes" convention isn't landing.

Each failure is a signal. The fix isn't just to correct the output. It's to update the instruction or skill so the same mistake doesn't happen again. Over time, the instructions accumulate the hard-won knowledge of what actually trips the assistant up, rather than what you thought might be important in the abstract.

We also run automated checks where we can. A code sample catalog system tracks every code block in our documentation and skill files, running as part of CI to catch examples that stop compiling. But the human feedback loop matters more. It is the part where we notice patterns of failure and promote fixes into instructions.

Discovery through use

The best conventions aren't designed upfront. They're discovered through bugs. We originally didn't have a convention about Parse vs ParseValue. Then we found the same confusion appearing in documentation examples, in generated code samples, and in AI-assisted edits. After fixing it in several places, we wrote it into the instructions as a convention: "prefer Parse + using in examples." The instruction emerged from the pattern of fixes, not from a design meeting.

Making review a habit

After enough cycles of "finish work, discover a skill has drifted, fix it," we noticed the review step itself needed codifying. So we wrote a skill for it: reviewing-skills documents when to review (after API changes, build infrastructure changes, architecture changes), what to check (code examples against real signatures, numeric values against source constants, scope boundaries, cross-references), and how to run a full audit across all skills using parallel agents.

This is the system becoming self-maintaining. When the assistant finishes a piece of work that changes a public API, it can load the reviewing-skills skill and check whether any other skills reference the old signature. The review checklist catches the kinds of drift we kept finding manually. That includes a constructor that gained a parameter, a file path that moved, or a pipeline step count that changed. Turning that checklist into a skill means the assistant applies it consistently, rather than relying on us to remember.

When an instruction fails to trigger

Sometimes the instruction exists but still doesn't fire. We had a CI gate that checked whether our documentation catalog was in sync. When it failed, the assistant regenerated the catalog and committed. It did so without following the documented verification workflow that the gate was designed to enforce. The instruction was there; it just didn't activate at the right moment.

The root cause was structural. The verification workflow was framed as "when you edit a documentation file, do these steps". But the assistant was in "fix the CI build" mode, not "I edited documentation files" mode. The trigger was attached to the wrong entry point. The fix was to promote the check to a standalone pre-commit gate with its own heading, anchored to the commit point rather than to individual file edits. Commits are where all changes converge regardless of how they were made, so they're the natural place to enforce verification.

This kind of root-cause analysis matters enough that we added it to the reviewing-skills skill itself. When a skill or instruction exists but fails to trigger, we now ask: was it framed for the wrong trigger? Was it buried inside a larger workflow? Did it only describe the happy path? Did multi-turn conversation obscure the activation point? Each question points to a different structural fix. Each fix makes the instruction more resilient to the specific failure mode that exposed it.

Applying this to your codebase

You don't need 20 skills to start. Here's a progression that works:

Start with copilot-instructions.md. Cover the basics that every task needs:

  • How to build the project (exact commands, not "run the build script")
  • How to run tests (with any mandatory filters or exclusions)
  • Key architectural patterns (where code lives, how it's organised)
  • Conventions that are hard to infer from code alone (naming, style, patterns)

Identify natural skill boundaries. Look for areas where:

  • You find yourself re-explaining the same thing to the assistant
  • Different tasks need deep knowledge of different subsystems
  • The assistant makes the same mistake repeatedly in a specific area

Each of these is a candidate for a skill file.

Add scope boundaries from the start. Every skill should have "USE FOR" and "DO NOT USE FOR" in its description. This prevents the assistant from applying the wrong knowledge to a task.

Let instructions evolve. Don't try to document everything upfront. Write instructions for the areas where the assistant struggles, then update them when bugs appear and prune information that turns out not to matter.

Prompts to get started

If you want to use an AI assistant to help bootstrap your own instructions, here are some prompts:

Explore this codebase and write a .github/copilot-instructions.md that covers: the build system (exact commands that work, with any prerequisites), the test suite (how to run it, any mandatory filters), the project structure (where source, tests, and config live), and any conventions that aren't obvious from reading a single file.

Look at the areas of this codebase where different concerns require deep, specialised knowledge. Suggest a set of skill files that would help an AI assistant work effectively in each area. For each, explain what it would cover and why it deserves its own file rather than a section in the main instructions.

Review my existing .github/copilot-instructions.md and identify: instructions that are too vague to be actionable, areas where the assistant would need to search the codebase to fill in gaps, and conventions that are mentioned but not demonstrated with concrete examples.

And when things go wrong despite having instructions in place:

You just [describe what happened - e.g., "regenerated the catalog without verifying the code blocks"]. There is an instruction that should have prevented this. Root-cause why it didn't trigger: was it framed for the wrong situation? Was it buried inside a longer workflow? Did it only cover the happy path? Propose a specific change to the instruction that would have caught this.

Follow the instructions in our [skill/instruction file] and review our skills and instructions. Tell me what you find.

Look at the last three mistakes you made in this session. For each one, check whether an instruction or skill exists that should have prevented it. If it exists, explain why it failed to activate. If it doesn't exist, draft one.

The instructions will improve fastest through actual use. When the assistant struggles, fix the root cause in the instructions and try again.

One important caveat: instruction files are committed to your repository and visible to anyone with access. Never put secrets, API keys, internal URLs, or other sensitive information in instruction or skill files. If the assistant needs access to something sensitive, use environment variables or MCP server integrations instead.

The numbers

For Corvus.Text.Json, we ended up with:

Count Lines
Main instructions 1 383
Skill files 20 2,055
Total 21 2,438

That's a significant investment. But each skill was written to solve a real problem. That might be a pattern the assistant kept getting wrong, a subsystem it couldn't navigate, or a convention it couldn't infer. None of them exist for the sake of completeness; every one earns its keep through daily use.

The result is an assistant that can build the project on the first try, run tests with the right filters, generate code that follows our conventions, navigate the code generation pipeline, and work with our mutable document model. It can do all of this without lengthy back-and-forth to establish context. That's a meaningful productivity gain when you're working in a codebase this size.



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

Octopus Easy Mode - Progressive Rollout

1 Share

Progressive rollouts allow DevOps teams to deploy a release to a small subset of production users before rolling it out to the entire user base. This approach reduces risk by allowing teams to validate the release in production and catch any issues before they affect all users. Typically, the rollout automatically promotes a new version of an application to increasingly larger percentages of production users, like 10%, 50%, and finally 100%. If there is an error, the rollout is halted.

The AWS Well-Architected framework recommends staggered deployments, noting that:

These techniques contribute to safer and more reliable software deployment and release processes.

In the previous post, you created a project that used a Claude agent step to categorize commits.

In this post, you will create a sample project that demonstrates a progressive rollout through multiple production environments.

Return to the series index.

Prerequisites

  • An Octopus Cloud account. If you don't have one, you can sign up for a free trial.
  • The Octopus AI Assistant Chrome extension. You can install it from the Chrome Web Store.

:::div{.hint} The Octopus AI Assistant will work with an on-premises Octopus instance, but it requires more configuration. The cloud-hosted version of Octopus doesn't need extra configuration. This means the cloud-hosted version is the easiest way to get started. :::

Creating the project

Paste the following prompt into the Octopus AI Assistant and run it to create a sample project with a progressive rollout:

Create a new progressive deployment project called "18. Progressive rollout".

The resulting project models a gradual production rollout by promoting the same release through progressively larger slices of production.

The AI Assistant creates a lifecycle with the environments Prod 10, Prod 50, and Prod 100. The lifecycle captures the different stages of the rollout as a percentage of production traffic, enforces the deployment order, and has the project deploy a release to each environment in turn.

How the progressive rollout works

The project creates a custom lifecycle called Progressive with four phases:

  • Development
  • Prod 10
  • Prod 50
  • Prod 100

Each lifecycle phase targets a single environment, and the project uses a runbook to explicitly trigger the next deployment after the current one succeeds.

The deployment process starts with a Deploy App step that simulates deploying an application by printing Deploying app to the task log. It is followed by a Simulate Failure step that acts as a validation gate. This step checks the prompted variable Project.SimulateFail, and if it is set to True, the deployment exits with an error and the rollout stops.

If the validation step succeeds, the process runs a community step template called Run Octopus Deploy Runbook. This step starts a runbook named Deploy Release to promote the current release to the next environment. This works around a limitation where Octopus prevents a deployment to the next environment until the current one is complete, so you cannot trigger a deployment to Prod 50 while the Prod 10 deployment is still running. By having a runbook trigger the deployment after a short delay, we can be sure the current deployment has completed before the next one starts.

The Run Octopus Deploy Runbook step is configured to run in the Prod 10 and Prod 50 environments. It dynamically chooses the next environment with the following logic:

  • When the current environment is Prod 10, it triggers a deployment to Prod 50
  • When the current environment is Prod 50, it triggers a deployment to Prod 100

The step also passes the current release ID into the runbook as the prompted variable Project.Release.Id, ensuring the same release is promoted through each stage of the rollout.

The runbook itself contains a single Sleep step that waits for 60 seconds before using the Octopus API to create the next deployment. This pause allows the current deployment to complete before the next rollout stage begins.

In practice, the rollout looks like this:

  • You create a release and deploy it to Development
  • You promote the release to Prod 10
  • The Run Octopus Deploy Runbook step automatically starts the Deploy Release runbook
  • The runbook waits 60 seconds and then creates a deployment of the same release to Prod 50
  • When the Prod 50 deployment succeeds, the same pattern is used to create the final deployment to Prod 100
  • If there are any failures, the rollout stops

Customizing the rollout

The Deploy Release runbook initiates a deployment to the next production environment after a short delay. This may be customized to instead schedule a deployment at a specific time, which allows the rollout to be paused for a longer period of time before continuing. You could, for example, only roll out to 100% of production traffic during off-peak hours, or after the release has been validated in Prod 50 for a full day.

You may also consider preventing release progression if a deployment fails. This ensures that a failed release cannot be promoted to the next environment until the issue is resolved. A blocked release will also prevent any scheduled deployments from taking place.

Comparing tenants and environments

This example used environments to represent progressive rollouts. It is also possible to use tenants to represent progressive rollouts. However, there are benefits to using environments:

  • Environments are easier to visualize in the Octopus UI
  • Lifecycles enforce the progression of releases through environments, which in turn progressively advance the rollout
  • The ability to block release progression after a successful deployment is only available for environments, not tenants

For these reasons, environments are the recommended approach for modeling progressive rollouts in Octopus.

What just happened?

You created a sample project with:

  • A custom lifecycle called Progressive that promotes releases through Development, Prod 10, Prod 50, and Prod 100
  • A scripted deployment process that simulates an application deployment and then validates the result before continuing
  • A prompted variable that can intentionally fail the validation step to stop the rollout
  • A community step template that runs a Deploy Release runbook to promote the same release to the next production environment
  • A runbook with a delayed API call that chains the rollout from Prod 10 to Prod 50, and then from Prod 50 to Prod 100

What's next?

The next step is an example of blue/green deployments.

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

How to Harden GitHub Actions Permissions with Least Privilege by Default

1 Share

When a workflow has more permissions than it needs, a simple build job can become a path to repository changes, token misuse, or a wider blast radius than the team intended.

The problem is easy to miss because the workflow still passes, so the extra access often goes unnoticed until a review, incident, or failed release exposes it.

This matters because GitHub Actions sits in the middle of code, secrets, releases, and deployment automation. If you tighten permission scope at the workflow and job level, you reduce what an attacker can do if a step, action, or dependency is compromised.

In this tutorial, you'll learn how to identify the permissions a workflow actually needs, reduce those permissions to the minimum practical scope, and verify that the workflow still works when access is intentionally constrained.

Start with one existing workflow, make its permission model explicit, and then remove access that the workflow never uses.

What We'll Cover:

Prerequisites

You should already have a GitHub repository with at least one workflow file and permission to edit repository settings and workflow YAML.

You'll also need a basic understanding of GitHub Actions jobs, permissions, and pull request workflows.

Key Points

  • Start with the smallest workflow permission set that still lets the job run.

  • Give write access only to the job that needs it.

  • Use OIDC for cloud access instead of long-lived credentials when the workflow reaches AWS or another provider.

  • Verify that the workflow still succeeds after you remove excess permissions.

What You'll Build

You'll take one existing GitHub Actions workflow and turn it into a tighter version that gives each job only the access it needs.

That usually means a read-only test job, a separate release job with write access, and cloud deployment jobs that use short-lived OIDC credentials instead of stored secrets.

Why the Default Approach Fails

A common pattern is to let a workflow inherit broad repository token access and only think about security after the pipeline is already working. That feels convenient, but it makes every job look more trusted than it really is.

The safer approach is to treat each job as a separate boundary. A build job usually needs read access, while a release job may need a narrow write scope. The goal is not to make every workflow restrictive for its own sake, but to make each permission explicit and easy to review.

For example, a test workflow should usually read code and upload artifacts. It shouldn't automatically be able to push tags, publish releases, or write to package registries.

How to Implement This Safely

Step 1: Inventory What the Workflow Actually Does

Before you edit YAML, list the actions the workflow performs. For example, a test job may only need to clone code and upload test artifacts, while a release job may need to create a release or push a package.

That split matters because GitHub Actions permissions can be different at the workflow level and the job level.

If you're not sure where to start, inspect the workflow and ask one question: does this job need to read, write, or do both?

Here's a simple permission audit you can apply to most workflows:

Workflow action Usually needs
Clone repository and run tests contents: read
Upload build or test artifacts usually no extra write scope on the repository token
Create a release contents: write
Publish a package packages: write
Request cloud credentials through OIDC id-token: write

Use the table as a starting point, then reduce access further if your workflow is even simpler.

Step 2: Set a Minimal Default Permission Scope

Start with the workflow permission block and grant only what the majority of jobs need.

If the job only checks code or runs tests, contents: read is usually enough.

name: ci

on:
  pull_request:
  push:
    branches:
      - main

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm test

With this baseline, the workflow can read repository content, but it doesn't get extra write access by default.

Step 3: Add Write Access Only Where the Job Needs it

If one job creates a release or publishes a package, give that job a narrower permission set instead of widening the whole workflow.

Keep the write scope on the smallest job that needs it. That keeps the rest of the pipeline easier to audit.

  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    needs: test
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/release.sh

This keeps your build path narrow while still allowing the one job that genuinely needs write access to complete its work.

Step 4: Use OIDC for Cloud Access When the Workflow Leaves GitHub

If the workflow needs cloud access, configure OIDC instead of storing long-lived credentials in secrets. That way, the job requests a temporary token at runtime, and you avoid keeping static cloud keys in the repository.

permissions:
  contents: read
  id-token: write

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Configure cloud credentials
        run: echo "Configure OIDC-based credentials here"

Step 5: Verify That Unnecessary Access is Gone

Run the workflow after removing excess permissions and confirm two things.

First, the intended job still succeeds. Second, a step that tries to use a permission you removed should fail clearly.

A good review signal is that the workflow logs show the exact permission scope you set, and any blocked write attempt fails instead of silently succeeding.

If the workflow only works when you widen permissions again, split the job or move the write action into a smaller release workflow.

You can also prove the change by trying one deliberate failure case. For example, run a release step in a branch where the job only has contents: read. The step should fail instead of silently publishing a release.

How to Verify This Works

Use a small test branch and push a commit that triggers the workflow.

Then confirm that:

  • The read-only job still passes.

  • The release job only works when it has explicit write permission.

  • A blocked action fails fast instead of receiving a token with broader access.

If you use a security scanner or linting step, keep it in the workflow so the permission change doesn't break normal delivery.

Check the workflow run summary, not just the final status. The logs should show that the job requested only the permissions it actually needs, and the denied action should fail because the token can't write.

That gives you a clear audit trail and a simpler review path for every future change to the workflow.

The strongest signal is a workflow that still passes with the smallest reasonable permission set and fails only when you intentionally remove a needed permission.

If you want an extra confidence check, compare the YAML before and after the change. The new version should show a narrow default at the workflow level and only a small number of job-level exceptions.

When This Breaks Down

This approach isn't magic. Some repositories have workflows that do many different things, and those workflows may need to be split before permission scoping becomes clean.

It can also feel slower at first because every permission change becomes explicit. That is the trade-off: a little more setup now in exchange for a much clearer security boundary later.

Finally, least privilege doesn't replace code review or secret scanning. It only reduces what a compromised workflow can do.

It also has limits when third-party actions need wider access than you expected. In that case, the next step is to review the action itself, not just the workflow that calls it.

Conclusion

In this tutorial, you learned how to narrow GitHub Actions permissions to the minimum needed, split read and write responsibilities across jobs, and verify that the workflow still works after the access model is tightened.

As a next step, you can apply the same pattern to release pipelines, package publishing, or cloud deployment workflows that still rely on broader permissions than they should.

References



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

Uno Platform 6.6 Adds Native AOT, Vulkan Rendering, and Broader Accessibility Support

1 Share

Uno Platform 6.6 introduces Native AOT publishing across five target platforms, an optional Vulkan rendering backend, and automatic registration for the framework’s Model Context Protocol servers. The release also reduces XAML boilerplate, expands cross-platform WinUI API coverage, and improves accessibility and multilingual text handling.

By Edin Kapić
Read the whole story
alvinashcraft
2 minutes ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories