<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Jesse Houwing</title>
    <description>The latest articles on Forem by Jesse Houwing (@jessehouwing).</description>
    <link>https://forem.com/jessehouwing</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F153902%2F2549a6a6-c745-4d10-b17b-3c8c7651681c.jpg</url>
      <title>Forem: Jesse Houwing</title>
      <link>https://forem.com/jessehouwing</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/jessehouwing"/>
    <language>en</language>
    <item>
      <title>Automatic versioning for GitHub Actions authors</title>
      <dc:creator>Jesse Houwing</dc:creator>
      <pubDate>Sat, 07 Feb 2026 10:34:01 +0000</pubDate>
      <link>https://forem.com/jessehouwing/automatic-versioning-for-github-actions-authors-2e19</link>
      <guid>https://forem.com/jessehouwing/automatic-versioning-for-github-actions-authors-2e19</guid>
      <description>&lt;p&gt;As a maintainer of 3 GitHub Actions, I've always struggled a bit with the versioning of them. GitHub's documentation provides guidance, but no easy way to verify you did everything right. Based on a quick scan of the actions on the marketplace I'm not alone, even some of GitHub's own actions have some inconsistencies when it comes to it.&lt;/p&gt;

&lt;p&gt;This is why I cobbled together v1 of my Actions Semver Checker action a couple of years ago and it has served me well. But as I threw the action together in a short amount of time way back when, it contained a few bugs, was very hard to maintain and I always dreaded making changes to it, since I had fixed my own repos, so it was hard to test whether I broke anything or accidentally introduced unwanted side effects.&lt;/p&gt;

&lt;p&gt;This is why I had Copilot Agent use the existing v1 implementation as a blueprint to build a new version. But with a lot of improvements. I gave Copilot the order to refactor the action with the following constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make sure the old implementation and the new one can be called side-by-side so it's easy to compare the differences.&lt;/li&gt;
&lt;li&gt;Extract the validation rules into separate scripts so they're easier to maintain and test.&lt;/li&gt;
&lt;li&gt;Provide manual remediation options wherever possible.&lt;/li&gt;
&lt;li&gt;Implement an autofix functionality which tries to execute the manual remediation steps.&lt;/li&gt;
&lt;li&gt;Add unit tests for everything.&lt;/li&gt;
&lt;li&gt;Add integration tests for everything.&lt;/li&gt;
&lt;li&gt;Try to remain backwards compatible with v1 as much as possible when it comes to the inputs.&lt;/li&gt;
&lt;li&gt;I gave it links to all the relevant GitHub docs and blogs for:

&lt;ul&gt;
&lt;li&gt;GitHub CLI release commands&lt;/li&gt;
&lt;li&gt;Releases REST API&lt;/li&gt;
&lt;li&gt;Tags REST API&lt;/li&gt;
&lt;li&gt;GitHub's GraphQL API&lt;/li&gt;
&lt;li&gt;Blogs discussing immutable releases&lt;/li&gt;
&lt;li&gt;GitHub Actions versioning &lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;I used Claude Opus to generate a plan for this, since it's a pretty big effort. I went over the plan and made a few tweaks and then had the Copilot Coding Agent spend a good 45 minutes to do the work. And this resulted in my first vibe coded GitHub Action. And the result? It did mostly what it needed to do. It surfaced a couple of gaps in the initial plan and the request to keep both old and new code side-by-side caused some interesting interweaving between the 2 implementations.&lt;/p&gt;

&lt;p&gt;But now that I had a test suite of 90+ tests, I felt much better changing the actual behavior. And after many round trips with the Copilot Coding Agent as well as a few hands-on sessions inside Visual Studio Code with Copilot, I am proud to release &lt;a href="https://github.com/jessehouwing/actions-semver-checker/" rel="noopener noreferrer"&gt;v2 of the GitHub Actions Semver Checker Action&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does it do exactly?
&lt;/h2&gt;

&lt;p&gt;Good question, the action is meant to be installed into the repository which hosts a GitHub action, like &lt;code&gt;actions/checkout&lt;/code&gt; or &lt;code&gt;jessehouwing/actions-semver-checker&lt;/code&gt;. It analyzes all branches, tags and releases and validates them based on a number of rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There should be a vX tag or branch (configurable) which points to the latest vX.y.z tag.&lt;/li&gt;
&lt;li&gt;There should be a vX.Y tag or branch (configurable) which points to the latest vX.Y.z tag.&lt;/li&gt;
&lt;li&gt;There should be a vx.y.z tag&lt;/li&gt;
&lt;li&gt;There should be &lt;em&gt;no&lt;/em&gt; vx.y.z branch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;new -&lt;/strong&gt; There should be a release for every vx.y.z tag

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;new -&lt;/strong&gt; That release should be immutable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;new -&lt;/strong&gt; The release for the highest vx.y.z should be set to "latest"&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;new -&lt;/strong&gt; There should be &lt;em&gt;no&lt;/em&gt; release for vX and vX.Y tags&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;new -&lt;/strong&gt; The action should be published to the marketplace

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;new -&lt;/strong&gt; The action.yml should have the correct metadata elements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;new -&lt;/strong&gt; The latest version of the action should be published to the marketplace&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;new -&lt;/strong&gt; Immutable releases should be turned on&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;And for almost all of these validations, I managed to build an autofix option which automatically corrects any issues found.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's new
&lt;/h2&gt;

&lt;p&gt;As you can see in the list above, v2 adds a lot of new validations and it also adds the autofix feature to automatically resolve most of the issues found. If the action is not able to fix the issue, manual remediation steps are logged to the workflows summary pages.&lt;/p&gt;

&lt;p&gt;Autofixes available:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;delete/update/create branches&lt;/li&gt;
&lt;li&gt;delete/update/create tags&lt;/li&gt;
&lt;li&gt;convert branch to tag&lt;/li&gt;
&lt;li&gt;convert tag to branch&lt;/li&gt;
&lt;li&gt;delete/update/create/publish releases&lt;/li&gt;
&lt;li&gt;automatically set the latest version&lt;/li&gt;
&lt;li&gt;republish releases to make them immutable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are a few other improvements worth noting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fully configurable which suites of rules you want to run&lt;/li&gt;
&lt;li&gt;You no longer need to check out the whole repo to run the action&lt;/li&gt;
&lt;li&gt;Retry logic and handling of rate limits&lt;/li&gt;
&lt;li&gt;Available as module on the PowerShell Gallery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is one unfortunate change to v1 I could not work around&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You should pass the &lt;code&gt;${{ secrets.GUTHUB_TOKEN }}&lt;/code&gt; to the &lt;code&gt;token:&lt;/code&gt; input of the action&lt;/li&gt;
&lt;li&gt;For some validations the token must have &lt;code&gt;contents: write&lt;/code&gt; permissions (to read draft releases)&lt;/li&gt;
&lt;li&gt;For autofix functionality the token must have &lt;code&gt;contents: write&lt;/code&gt; permissions&lt;/li&gt;
&lt;li&gt;To fix certain issues, the token needs permissions which cannot be granted to GitHub Actions, such as &lt;code&gt;workflows: write&lt;/code&gt;. Use a GitHub App or Fine-grained Personal Access Token instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recommended usage
&lt;/h2&gt;

&lt;p&gt;I recommend you install the action into your action repository with its default settings, this will analyze your repo and will detect all rule violations.&lt;/p&gt;

&lt;p&gt;Change the settings of the action to match your desired behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;floating-versions-use&lt;/code&gt; can be set to &lt;code&gt;tags&lt;/code&gt; or &lt;code&gt;branches&lt;/code&gt; depending on how you want to manage the &lt;code&gt;vx&lt;/code&gt; and &lt;code&gt;vx.y&lt;/code&gt; versions.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;check-minor-version&lt;/code&gt; can be set to &lt;code&gt;none&lt;/code&gt;, &lt;code&gt;warning&lt;/code&gt; or &lt;code&gt;error&lt;/code&gt; to validate the existence of &lt;code&gt;vx.y&lt;/code&gt; versions.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;check-releases&lt;/code&gt; can be set to &lt;code&gt;none&lt;/code&gt;, &lt;code&gt;warning&lt;/code&gt; or &lt;code&gt;error&lt;/code&gt; to validate the existence of releases for &lt;code&gt;vx.y.z&lt;/code&gt; tags.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;check-release-immutability&lt;/code&gt; can be set to &lt;code&gt;none&lt;/code&gt;, &lt;code&gt;warning&lt;/code&gt; or &lt;code&gt;error&lt;/code&gt; to validate whether releases are immutable.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ignore-preview-releases&lt;/code&gt; can be set to &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt; to exclude preview releases when checking floating versions (&lt;code&gt;vx&lt;/code&gt; an &lt;code&gt;vx.y&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ignore-versions&lt;/code&gt; can exclude versions from validation completely. This can be used to ignore really old versions or versions that were accidentally made immutable and can no longer be fixed.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: Check SemVer

on:
  push:
    branches: ['main']
    tags:
      - '*'
  workflow_dispatch:

permissions: {}

jobs:
  check-semver:
    permissions:
      contents: write

    concurrency:
      group: '${{ github.workflow }}'
      cancel-in-progress: true

    runs-on: ubuntu-latest

    steps:
      - uses: jessehouwing/actions-semver-checker@v2
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          check-release-immutability: none
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ be extra careful with the &lt;code&gt;check-release-immutability&lt;/code&gt; setting prior to turning on autofix. Most automatic fixes can be easily undone using the git commandline, but once a release is made immutable, there is no way back.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once your existing tags have been cleaned up and you understand the implications, turn on release immutability checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    steps:
      - uses: jessehouwing/actions-semver-checker@v2
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          check-release-immutability: error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the action in read-only mode for a while, manually performing the suggested remediation steps.&lt;/p&gt;

&lt;p&gt;When you're confident the action does what you want it to do, consider turning on &lt;code&gt;autofix: true&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    steps:
      - uses: jessehouwing/actions-semver-checker@v2
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          autofix: true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With autofix turned on, the only thing you need to do to release a new version and update all tags and releases, is to push a new tag to to create a new release with the correct name: &lt;code&gt;vx.y.z&lt;/code&gt;. This will trigger autofix to do the rest on your behalf.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>github</category>
    </item>
    <item>
      <title>Improved Dependency Submission for GitHub Actions</title>
      <dc:creator>Jesse Houwing</dc:creator>
      <pubDate>Sat, 27 Dec 2025 18:47:09 +0000</pubDate>
      <link>https://forem.com/jessehouwing/improved-dependency-submission-for-github-actions-1b29</link>
      <guid>https://forem.com/jessehouwing/improved-dependency-submission-for-github-actions-1b29</guid>
      <description>&lt;p&gt;If you've read some of my earlier blogs about GitHub Actions Security, you may have followed my advice to pin your actions and configure Dependabot. Recently someone brought to my attention that you then Security Advisories for GitHub actions do not show up in the Dependency Graph and Dependabot won't create security updates.&lt;/p&gt;

&lt;p&gt;I decided to solve that problem.&lt;/p&gt;

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

&lt;p&gt;When a security vulnerability is found and a security advisory is created, it will have the action and its affected versions as metadata. &lt;strong&gt;Versions&lt;/strong&gt; , not &lt;strong&gt;SHAs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When Automatic Dependency Submission submits the versions of actions used by a repository it records:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@v1.2.3&lt;/code&gt; as &lt;code&gt;1.2.3&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@v1&lt;/code&gt; as &lt;code&gt;1.*.*&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;8e8c483db84b4bee98b60c0593521ed34d9990e8&lt;/code&gt; as &lt;code&gt;8e8c483db84b4bee98b60c0593521ed34d9990e8&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Security Advisory Database can't match &lt;code&gt;8e8c483db84b4bee98b60c0593521ed34d9990e8&lt;/code&gt; against the vulnerable version range and won't show the vulnerability in the dependency graph.&lt;/p&gt;

&lt;p&gt;Another issue exists when actions are forked to an organization (a common technique to create a private actions marketplace). The dependency is recorded using the organization name that holds the fork, not the upstream.&lt;/p&gt;

&lt;p&gt;The Security Advisory Database can't match &lt;code&gt;myorg/actions_checkout&lt;/code&gt; against the vulnerable action &lt;code&gt;actions/checkout&lt;/code&gt; and won't show the vulnerably in the dependency graph.&lt;/p&gt;

&lt;h2&gt;
  
  
  Impacted features
&lt;/h2&gt;

&lt;p&gt;Without recording the exact version of each action used, a number of features won't work as you might expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dependency Graph won't show which actions you use are vulnerable.&lt;/li&gt;
&lt;li&gt;Dependabot Security Updates won't be created for workflows using vulnerable actions.&lt;/li&gt;
&lt;li&gt;Dependency Review Action won't block pull requests introducing vulnerable actions into your workflows.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;To solve the issue I created a new manual submission action which mimics the automatic submission, but adds a few extra features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It resolves the SHA to the highest, most specific version and records that as an additional dependency.&lt;/li&gt;
&lt;li&gt;It resolves a wildcard version to the current specific version and records that as an additional dependency.&lt;/li&gt;
&lt;li&gt;It can be configured to look up the upstream of a forked action and records that as an additional dependency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With these additional dependencies recorded, the Dependency Graph now contains all the metadata to determine whether the GitHub actions you depend on are vulnerable and all features should now work as expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to
&lt;/h2&gt;

&lt;p&gt;Changing how Actions' dependencies are reported, you simply add my &lt;code&gt;jessehouwing/actions-dependency-submission&lt;/code&gt; action to your workflows (and optionally add &lt;code&gt;actions/dependency-review-action&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: Submit and validate dependencies of GitHub Actions

on:
  push:
    branches:
      - main
  pull_request:
  schedule:
    - cron: '33 4 * * *'

permissions: {}

jobs:
  submit-actions-dependencies:
    runs-on: ubuntu-latest
    permissions:
      # submitting dependencies requires 'contents: write'
      contents: write

    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
      - uses: jessehouwing/actions-dependency-submission@e848a29fd84b874cce3e45ceb00619bc72dbeca3 # 1.0.2
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
      - uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # 4.8.2
        if: github.event_name == 'pull_request'
        with:
          retry-on-snapshot-warnings: true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can test whether the action is working correctly by adding a vulnerable action to your repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: Submit Dependencies
on:
  workflow_dispatch:

jobs:
  submit-dependencies:
    if: false
    runs-on: ubuntu-latest
    permissions: {}
    steps:
      - uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should show as a &lt;strong&gt;high&lt;/strong&gt; vulnerability in the Dependency graph of the repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq1h3mw72qvf0wzlbrf7n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq1h3mw72qvf0wzlbrf7n.png" alt="Improved Dependency Submission for GitHub Actions" width="800" height="275"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Dependency Graph showing &lt;code&gt;actions/download-artifact@v4.1.2&lt;/code&gt; as vulnerable.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And you'll also see the original report using the SHA:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyqx4ncq8tvechctofm9f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyqx4ncq8tvechctofm9f.png" alt="Improved Dependency Submission for GitHub Actions" width="800" height="72"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Dependency Graph showing actions/download-artifact referenced by SHA.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You can &lt;a href="https://github.com/jessehouwing/actions-dependency-submission" rel="noopener noreferrer"&gt;find the action and additional documentation and examples here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>github</category>
      <category>supplychainsecurity</category>
      <category>security</category>
    </item>
    <item>
      <title>GitHub Billing - Let Organization and Repository admins set the Cost Center in GitHub</title>
      <dc:creator>Jesse Houwing</dc:creator>
      <pubDate>Wed, 24 Sep 2025 08:48:31 +0000</pubDate>
      <link>https://forem.com/jessehouwing/github-billing-let-organization-and-repository-admins-set-the-cost-center-in-github-1ad2</link>
      <guid>https://forem.com/jessehouwing/github-billing-let-organization-and-repository-admins-set-the-cost-center-in-github-1ad2</guid>
      <description>&lt;p&gt;We rely on GitHub Cost Centers to split the GitHub bill across the different entities that make up our company. What resource is tied to which cost center is managed in the GitHub Enterprise Settings and for now this can only be done be a user with Enterprise Owner or Billing Administrator permissions.&lt;/p&gt;

&lt;p&gt;I'd been searching for a way to allow Organization Owners and Repository Administrators to set the Cost Center on a repository themselves without having to involve one of these highly privileged accounts. We found a solution using GitHub Actions and Repository Custom Properties.&lt;/p&gt;

&lt;p&gt;Since my last blog post on Cost Centers, a few small improvements were released by GitHub:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;There is now an API to associate a Repository with a Cost Center.&lt;/li&gt;
&lt;li&gt;It's now possible to associate a Repository to a different Cost Center than the Organization that owns it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These two improvements allow us to solve this problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the Custom Property
&lt;/h2&gt;

&lt;p&gt;GitHub has a feature called Custom Properties for repositories which allows you to link arbitrary data to a repo. You can define these properties at the Enterprise, Organization and Repository level.&lt;/p&gt;

&lt;p&gt;Since our cost centers are defined at the Enterprise level, I chose to create this custom property at the same level. To add a new custom property navigate to your enterprise, select the &lt;strong&gt;Policies&lt;/strong&gt; tab and then drill down into &lt;strong&gt;Repository&lt;/strong&gt; and then &lt;strong&gt;Custom Properties&lt;/strong&gt;. Choose &lt;strong&gt;New Property&lt;/strong&gt; to create your new property:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fipkkak8h8owq5ukb6pq6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fipkkak8h8owq5ukb6pq6.png" alt="GitHub Billing - Let Organization and Repository admins set the Cost Center in GitHub"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Navigate to the Custom Properties section and click new property&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Give the property a clear &lt;strong&gt;Name&lt;/strong&gt; and &lt;strong&gt;Description&lt;/strong&gt;. Since our list of cost centers is clearly defined, I've chosen the &lt;strong&gt;Type&lt;/strong&gt;  &lt;strong&gt;single-select&lt;/strong&gt; to allow our users to pick a cost center from a predefined list.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8k5yho61ng6opij6dg4t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8k5yho61ng6opij6dg4t.png" alt="GitHub Billing - Let Organization and Repository admins set the Cost Center in GitHub"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Give the property a unique name and set the type to single select&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Next, add the list of cost centers you want people to be able to select from as options. I've also included an option &lt;strong&gt;inherit&lt;/strong&gt; which I'll explain later:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqkwor8jp2orvquernj95.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqkwor8jp2orvquernj95.png" alt="GitHub Billing - Let Organization and Repository admins set the Cost Center in GitHub"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Add the required cost-center options.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And finally make the field required and set the &lt;strong&gt;default value&lt;/strong&gt; to &lt;strong&gt;inherit&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2i0xyxs9lvv8vars9lu5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2i0xyxs9lvv8vars9lu5.png" alt="GitHub Billing - Let Organization and Repository admins set the Cost Center in GitHub"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Set the permission to allow users with properties permission to edit this field, make it required and set the default value.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now when you create a new repository or navigate to the settings of an existing repository, you can set the cost-center custom field:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn1277a5oqgektmnw0ope.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn1277a5oqgektmnw0ope.png" alt="GitHub Billing - Let Organization and Repository admins set the Cost Center in GitHub"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Users are required to select a cost center when creating a new repository.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And existing repositories will now show the custom property in the repository settings:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe32htzq7z8h3h7xny3g3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe32htzq7z8h3h7xny3g3.png" alt="GitHub Billing - Let Organization and Repository admins set the Cost Center in GitHub"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The reason for Inherit
&lt;/h2&gt;

&lt;p&gt;In the above example we added a special value &lt;strong&gt;inherit&lt;/strong&gt; , when this value is set, we &lt;em&gt;don't actually set any cost center on the repository&lt;/em&gt; and this causes all costs generated by this repository to automatically flow to the cost center associated to the Organization. If the Organization isn't associated to a cost center, the costs will flow to the Enterprise.&lt;/p&gt;
&lt;h2&gt;
  
  
  Making it all work using GitHub Actions
&lt;/h2&gt;

&lt;p&gt;Creating the Custom Property itself doesn't do anything by itself. In order to actually associate the Repository to the Cost Center, we still need to update the configuration in the GitHub Enterprise Billing &amp;amp; Licensing section:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx88z91ra5ytbgim7zg6f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx88z91ra5ytbgim7zg6f.png" alt="GitHub Billing - Let Organization and Repository admins set the Cost Center in GitHub"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Billing Administrators can associate Organizations, Repositories and Users to Cost Centers&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Without additional automation each repository needs to be manually added to a cost center by editing the Cost Center and adding the Repository. This is a tedious manual process.&lt;/p&gt;

&lt;p&gt;Instead, to update the Cost Center configuration I've created a GitHub Actions workflow that runs once a day and compares the Cost Center configuration against the values of the Custom Properties and performs updates to the Cost Center configuration if needed.&lt;/p&gt;

&lt;p&gt;The basic workflow is quite simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Query the current Cost Center configuration&lt;/li&gt;
&lt;li&gt;Query all Organizations in our GitHub Enterprise &lt;/li&gt;
&lt;li&gt;Query all Repositories in each GitHub Organization&lt;/li&gt;
&lt;li&gt;Query the Custom Properties for each Repository&lt;/li&gt;
&lt;li&gt;Update the Cost Center configuration in case it doesn't match the Custom Property&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The workflow relies on the GitHub CLI t0 query the data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$costCenters = (invoke-gh -fromJson -- api /enterprises/$enterprise/settings/billing/cost-centers).costCenters

$orgs = get-allorganizations $enterprise | % { $_.login }

update-repocostcenters
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The &lt;code&gt;update-repocostcenters&lt;/code&gt; function checks which updates are needed:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function update-repocostcenters {
    Write-Output "Updating Costcenters for repositories in $enterprise." 

    foreach ($org in $orgs) {

        Write-Output "Processing organization $org"
        $repos = invoke-gh -slurp api /orgs/$org/repos --paginate --jq '.[] | { name: .name, full_name: .full_name }' | ConvertFrom-Json

        foreach ($repo in $repos) { 

            $customProperties = invoke-gh api /repos/$($repo.full_name)/properties/values --jq '.' | ConvertFrom-Json
            $repoCostCenterProperty = $customProperties | Where-Object { $_.property_name -eq "cost-center" } | Select-Object -ExpandProperty value -ErrorAction Ignore
            $repo | Add-Member -NotePropertyName cost_center -NotePropertyValue $repoCostCenterProperty -Force

            $currentCostCenter = $costCenters | ? { $_.resources | ? { $_.type -eq "Repo" -and $_.name -eq $repo.full_name } }

            if ( $repo.cost_center -eq "inherit" ) {
                $repo.cost_center = $null
            }

            $targetCostCenter = $null

            if ($repo.cost_center) {
                $targetCostCenter = $costCenters | ? { $_.name -eq $repo.cost_center }
                if (-not $targetCostCenter) {
                    Write-Warning "Costcenter for repository $($repo.full_name) not found."
                }
            }

            if ($null -eq $currentCostCenter) {
                if ($null -ne $targetCostCenter) {
                    Write-Output "Updating costcenter for repository $($repo.full_name) from unassigned to $($targetCostCenter.name)."
                    Update-CostCenterResources -Handles $repo.full_name -ResourceType "Repo" -Action "Add" -CostCenter $targetCostCenter -Enterprise $enterprise
                }
                else {
                    Write-Verbose "Repository $($repo.full_name) does not have a cost center assigned."
                }
            }
            else {
                if ($null -eq $targetCostCenter) {
                    Write-Verbose "Repository $($repo.full_name) does not have a cost center assigned."
                    Write-Output "Updating costcenter for repository $($repo.full_name) from $($currentCostCenter.name) to unassigned."
                    Update-CostCenterResources -Handles $repo.full_name -ResourceType "Repo" -Action "Delete" -CostCenter $currentCostCenter -Enterprise $enterprise
                }
                elseif ($currentCostCenter.id -ne $targetCostCenter.id) {
                    Write-Output "Updating costcenter for repository $($repo.full_name) from $($currentCostCenter.name) to $($targetCostCenter.name)."
                    Update-CostCenterResources -Handles $repo.full_name -ResourceType "Repo" -Action "Delete" -CostCenter $currentCostCenter -Enterprise $enterprise
                    Update-CostCenterResources -Handles $repo.full_name -ResourceType "Repo" -Action "Add" -CostCenter $targetCostCenter -Enterprise $enterprise
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Unfortunately, you can't simply update the associated Cost Center in a single call, when changing the current value, an add plus delete is required.&lt;/p&gt;

&lt;p&gt;I shared the &lt;code&gt;Update-CostCenterResources&lt;/code&gt; function in a previous blogpost. GitHub added the ability to set Organizations and Repositories recently, so here is the updated version:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function Update-CostCenterResources {
    param(
        [Parameter(Mandatory = $true)]
        [string[]]$Handles,

        [ValidateSet('User', 'Repo', 'Org')]
        [string]$ResourceType = "User",

        [Parameter(Mandatory = $true)]
        [ValidateSet('Add', 'Delete')]
        [string]$Action,

        [Parameter(Mandatory = $true)]
        $CostCenter,

        [Parameter(Mandatory = $true)]
        [string]$Enterprise
    )

    switch ($Action) {
        'Add' {
            $method = 'POST'
            $Handles = $Handles | Where-Object { 
                $handle = $_
                return (($costCenter.resources | ? { $_.type -eq $ResourceType } | ? { $_.name -eq $handle }).Count -eq 0)
            }
        }
        'Delete' {
            $method = 'DELETE'
            $Handles = $Handles | Where-Object { 
                $handle = $_
                return (($costCenter.resources | ? { $_.type -eq $ResourceType } | ? { $_.name -eq $handle }).Count -gt 0)
            }
        }
    }

    # Call fails when processing too many users at once. Thus batching the calls...
    $count = 0
    do {
        $batch = $Handles | Select-Object -Skip $count -First 30
        $count += $batch.Count

        if ($batch.Count -gt 0) {
            switch ($ResourceType) {
                'User' { 
                    $body = @{
                        users = [string[]]$batch
                    }
                }
                'Org' { 
                    $body = @{
                        organizations = [string[]]$batch
                    }
                }
                'Repo' { 
                    $body = @{
                        repositories = [string[]]$batch
                    }
                }
            }


            $_ = ($body | ConvertTo-Json) | gh api --method $method /enterprises/$Enterprise/settings/billing/cost-centers/$($CostCenter.id)/resource --input -
        }
    } while ($batch.Count -gt 0)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;All of this runs from a simple GitHub workflow which has access to a Personal Access token with the required permissions to access the Cost Center information as well as sufficient permissions to query all Repositories in all Organizations in the enterprise.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: Set Costcenters

on:
  schedule:
    - cron: "5 4 * * *"

jobs:
  update:
    permissions: 
      contents: read

    runs-on: ubuntu-latest

    steps:      
    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
      with:
        fetch-depth: 0

    - name: Assign Costcenters
      run: |
         .\assign-costcenters.ps1
      env:
        GH_TOKEN: ${{ secrets.GH_TOKEN }}
      shell: pwsh 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You can find the whole script here:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;



</description>
      <category>github</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>GitHub Actions learnings from the recent nx hack</title>
      <dc:creator>Jesse Houwing</dc:creator>
      <pubDate>Tue, 02 Sep 2025 21:05:46 +0000</pubDate>
      <link>https://forem.com/jessehouwing/github-actions-learnings-from-the-recent-nx-hack-57l1</link>
      <guid>https://forem.com/jessehouwing/github-actions-learnings-from-the-recent-nx-hack-57l1</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1562813733-b31f71025d54%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDN8fGhhY2tlcnxlbnwwfHx8fDE3NTY3OTQ1ODN8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1562813733-b31f71025d54%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDN8fGhhY2tlcnxlbnwwfHx8fDE3NTY3OTQ1ODN8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" alt="GitHub Actions learnings from the recent nx hack" width="2000" height="1336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You may have seen recent reporting around the compromise of the &lt;code&gt;nx&lt;/code&gt; project. A malicious version of their package was published to npmjs which subsequently published GitHub tokens, crypto wallets and other sensitive materials of 1000s of users.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/nrwl/nx/security/advisories/GHSA-cxm3-wv7p-598c?ref=jessehouwing.net" rel="noopener noreferrer"&gt;The maintainers of the project have spelled out exactly how the project was compromised&lt;/a&gt;. Yet, it doesn't tell exactly what they did to prevent the same issue from happening in the future. Or how &lt;em&gt;you&lt;/em&gt; can protect yourself from these attack vectors.&lt;/p&gt;

&lt;p&gt;I'll walk through the attack chain and will explain what options are available to you to prevent these things from happening.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the vulnerable workflow
&lt;/h2&gt;

&lt;p&gt;Let's look at the main cause of this hack. &lt;a href="https://github.com/nrwl/nx/pull/32458/files?ref=jessehouwing.net" rel="noopener noreferrer"&gt;The introduction of a new GitHub Actions Workflow that had the &lt;code&gt;pull_request_target&lt;/code&gt; trigger enabled&lt;/a&gt;. This trigger is dangerous, since it signals to GitHub that the workflow is safe and has access to secrets. By default only to secrets referenced in the workflow itself, but that &lt;em&gt;always&lt;/em&gt; includes the built-in GitHub Actions token.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;on:
  pull_request_target:
    types: [opened, edited, synchronize, reopened]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second stage of the hack was possible due to script injection in that same workflow. This allowed the injection of arbitrary code in the pipeline through the title of the pull request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      - name: Create PR message file
        run: |
          mkdir -p /tmp
          cat &amp;gt; /tmp/pr-message.txt &amp;lt;&amp;lt; 'EOF'
          ${{ github.event.pull_request.title }}

          ${{ github.event.pull_request.body }}
          EOF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pull request's title and body are written to the script prior to execution, so including &lt;code&gt;EOF&lt;/code&gt; in any of them cause any subsequent text to be interpreted as arbitrary bash commands. And adding &lt;code&gt;$(...)&lt;/code&gt; would also lead to code execution prior to writing the contents to &lt;code&gt;pr-message.txt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Since the workflow can now execute arbitrary code, and the GitHub Hosted Runner runs with sudo by default, &lt;a href="https://karimrahal.com/2023/01/05/github-actions-leaking-secrets/?ref=jessehouwing.net" rel="noopener noreferrer"&gt;it would also be possible to write a memory dump of the runner process to gain access to the Runner's GitHub Token&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The payload was probably hidden in the body of the pull request and could have been similar to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Break the multi-line content redirection by supplying the defined EOF token.
EOF
# Dump the listener process so we can get access to its secrets

sudo apt-get install -y gdb &amp;amp;&amp;amp; \
sudo gcore -o k.dump "$(ps ax | grep 'Runner.Listener' | head -n 1 | awk '{ print $1 }')"

# Then, we grep the memory dump for the format of secret values—per the job data we decrypted in the network analysis:

grep -Eao '"[^"]+":\{"value":"[^"]*","issecret":true\}' k.dump*

# Optionally use CURL to send these secrets to a url you control. 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;This is a well-known way to exfiltrate secrets from the GitHub Hosted Runner and I speculate it was used to gain access to the GITHUB_TOKEN secret.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Since this token has write-all permissions on older repositories by default, it is possible to either write to an unprotected branch on the repo, or to run other workflows. &lt;a href="https://github.com/nrwl/nx/blob/400e3003d72acd75522468d58fdc160e76e13a68/.github/workflows/publish.yml?ref=jessehouwing.net#L8-L13" rel="noopener noreferrer"&gt;The &lt;code&gt;publish.yml&lt;/code&gt; workflow had the ability to be triggered manually&lt;/a&gt; in the &lt;code&gt;master&lt;/code&gt; branch. This means that it can be triggered against any branch in the project repo and will use the contents of the workflow in that specific branch. Because the npm token is stored as a repository secret, it will be available to all workflows in the repository, regardless of the branch it's defined in.&lt;/p&gt;

&lt;p&gt;In this case I could have used the GitHub Token to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a new branch in the project repository&lt;/li&gt;
&lt;li&gt;Overwrite the publish.yml in the new branch to send the npm token to a webserver I control&lt;/li&gt;
&lt;li&gt;Trigger the publish workflow against my branch&lt;/li&gt;
&lt;li&gt;Delete the branch&lt;/li&gt;
&lt;li&gt;Optionally delete the workflow run&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without overwriting the &lt;code&gt;publish.yml&lt;/code&gt;, changes in any of the build scripts executed in the publish job would have had access to the token, since it was added as a job level environment variable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  publish:
    if: ${{ github.repository_owner == 'nrwl' }}
    env:
      GH_TOKEN: ${{ github.token }}
      NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the disclosure the project mentions that they have branch rulesets in place to protect their master branch. Without branch protections this would have been even simpler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the vulnerable (human) process
&lt;/h2&gt;

&lt;p&gt;This attack couldn't have happened if the workflow above wouldn't have been added, or if it had used the standard &lt;code&gt;pull_request&lt;/code&gt; trigger instead. If you read the documentation for &lt;code&gt;pull_request_target&lt;/code&gt;, it calls out that its dangerous.&lt;/p&gt;

&lt;p&gt;It looks like this workflow was generated by AI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🤖 Generated with Claude Code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that none of the humans in the loop were aware of the dangers. Nor was there any tooling in place to scan the workflows and to alert them of the potential problems.&lt;/p&gt;

&lt;p&gt;Once the attacker figured out the attack chain, it would have been possible to execute the attack in seconds, with only minimal observable traces. And because all actions would be performed by the &lt;code&gt;github-actions[bot]&lt;/code&gt; user, none of the changes would stand out and any commits would even be considered verified if done correctly.&lt;/p&gt;

&lt;p&gt;It's trivially easy to setup a repo in which to test this attack without actually having to interact with the nx repo.&lt;/p&gt;

&lt;p&gt;Because the NPM token was stored as a repository secret, access to it would not require an explicit approval.&lt;/p&gt;

&lt;p&gt;Not related to this attack, but worrying nonetheless. The actions referenced by the nx project are pinned to their major version tag. This is against best practices for GitHub Actions in public repos. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      - uses: actions/checkout@v4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Best practices require each action to be pinned to their commit sha. In this case as far as I can tell none of the referenced actions have a known vulnerability, but &lt;a href="https://www.cve.news/cve-2025-30066/?ref=jessehouwing.net" rel="noopener noreferrer"&gt;this leaves the project open to attacks similar to the tj-actions/changed-files hack from a few months back&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remediation
&lt;/h2&gt;

&lt;p&gt;Let's break down the remediations that can be put in place:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Change the default permissions for GitHub Actions to read-only.&lt;/li&gt;
&lt;li&gt;Set workflow permissions at the job level&lt;/li&gt;
&lt;li&gt;Enable branch rulesets to require pull requests for all important branches.&lt;/li&gt;
&lt;li&gt;Require Approval for running fork pull request workflows from contributors&lt;/li&gt;
&lt;li&gt;Do not rely on &lt;code&gt;pull_request_target&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Do not use &lt;code&gt;workflow_dispatch&lt;/code&gt; or &lt;code&gt;repository _dispatch&lt;/code&gt; triggers in workflows that have access to critical secrets.&lt;/li&gt;
&lt;li&gt;Do not mix pull-request validation and continuous integration workflows&lt;/li&gt;
&lt;li&gt;Store critical secrets in environment secrets, instead of repository secrets.&lt;/li&gt;
&lt;li&gt;Pass secrets only to the steps that need them&lt;/li&gt;
&lt;li&gt;Pass data to script steps using the environment&lt;/li&gt;
&lt;li&gt;Pin all actions to their sha.&lt;/li&gt;
&lt;li&gt;Enable GitHub Advanced Security Code Scanning for GitHub Actions and add Branch Ruleset.&lt;/li&gt;
&lt;li&gt;Enable Dependabot for GitHub Actions to automatically update action references to the latest version.&lt;/li&gt;
&lt;li&gt;Consider 3rd party security hardening tools like StepSecurity's Harden Runner.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each of these remediations is discussed in more detail below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Change the default permissions for GitHub Actions to read-only
&lt;/h3&gt;

&lt;p&gt;When GitHub Actions was first released, the default permissions were set to write-all. When GitHub changed the default permission to read-all later, they did not retroactively reset the permission for existing accounts and organizations.&lt;/p&gt;

&lt;p&gt;GitHub Enterprise Owners and GitHub Organization Owners can enforce a default permission of read-all through the Settings:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fakytquk6pl89qniracem.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fakytquk6pl89qniracem.png" alt="GitHub Actions learnings from the recent nx hack" width="800" height="275"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Set the default workflow permissions to: Read repository contents and package permissions.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This will break all workflows that do not explicitly declare their required permissions in case they need more than read-all.&lt;/p&gt;

&lt;p&gt;While you're at it, you should also turn off &lt;strong&gt;Allow GitHub Actions to create and approve pull requests&lt;/strong&gt;. Allowing this setting offers attackers a way to bypass branch rulesets under certain conditions.&lt;/p&gt;
&lt;h3&gt;
  
  
  Set workflow permissions at the job level
&lt;/h3&gt;

&lt;p&gt;A workflow can declare its required permissions at the workflow or at the job level. For best security, it's recommended to deny all permissions at the workflow level and explicitly declare permissions for each job specifically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Opt out of any default permissions
permissions: {}

jobs:
  build:
    # explicitly request permissions
    permissions:
      contents: read

  deploy:
    permissions:
      contents: read
      packages: write
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This also removes write permissions in case the default permissions are set to read and write.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enable branch rulesets to require pull requests for all important branches.
&lt;/h3&gt;

&lt;p&gt;The nx project reports that they had enabled Branch Rulesets to require a pull request for any changes to enter the master branch and other important branches in their repository.&lt;/p&gt;

&lt;p&gt;This prevented attackers from simply pushing a new workflow directly to the main branch to exfiltrate the npm token and other secrets.&lt;/p&gt;

&lt;p&gt;Protecting branches in your repository is a straight forward configuration change.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F86tb5oqj12rz1tudyrzr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F86tb5oqj12rz1tudyrzr.png" alt="GitHub Actions learnings from the recent nx hack" width="731" height="510"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Require a pull request before merging&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is also a prerequisite for other protections, like requiring Code Scanning Results before merging.&lt;/p&gt;

&lt;p&gt;There are quite a few policy settings you can enable through Branch Rulesets the &lt;a href="https://medium.com/boostsecurity/slsa-dip-source-of-the-problem-a1dac46a976?ref=jessehouwing.net" rel="noopener noreferrer"&gt;following research explains why each of these settings is important and how an attacker might abuse a ruleset with a slightly less strict configuration&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Require approval for running fork pull request workflows from contributors
&lt;/h3&gt;

&lt;p&gt;This is a controversial one. As it adds quite a bit of overhead to project teams receiving many 3rd party contributions. But it might have been enough for a maintainer to detect the script injection attack that was used to exfiltrate the GitHub Actions token.&lt;/p&gt;

&lt;p&gt;GitHub Enterprise Owner, Organization Owners and Repository Admins can set a policy to require approval before running any GitHub Actions workflow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiy7lm6xyuoc73qa72vkw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiy7lm6xyuoc73qa72vkw.png" alt="GitHub Actions learnings from the recent nx hack" width="800" height="240"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Require approval for all external contributors&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;While it might feel tempting to set this policy to any of the lighter settings, there are already known cases where a person appears helpful by fixing a few typos. They'll be considered trusted after that.&lt;/p&gt;

&lt;p&gt;Setting this policy will require a maintainer to review the Pull Request and trigger the workflows each time an external contributor pushes new commits.&lt;/p&gt;
&lt;h3&gt;
  
  
  Do not rely on &lt;code&gt;pull_request_target&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;pull_request_target&lt;/code&gt; is inherently dangerous. It opens you up to all kinds of additional attack paths and very few people are completely aware of the implications. &lt;strong&gt;It is very easy to make a mistake when you enable this trigger on a workflow and open your repository up for attacks.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My recommendation is to not rely on the &lt;code&gt;pull_request_target&lt;/code&gt; trigger.&lt;/strong&gt; For most scenarios the &lt;code&gt;pull_request&lt;/code&gt; trigger is sufficient.&lt;/p&gt;

&lt;p&gt;In the enterprises I control we've added a &lt;a href="https://docs.github.com/en/code-security/secret-scanning/using-advanced-secret-scanning-and-push-protection-features/custom-patterns/managing-custom-patterns?ref=jessehouwing.net" rel="noopener noreferrer"&gt;Custom Pattern for Secret Scanning to block all commits that contain &lt;code&gt;pull_request_target&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwwe7f3zuirx4i3spmsbd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwwe7f3zuirx4i3spmsbd.png" alt="GitHub Actions learnings from the recent nx hack" width="800" height="416"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Define a custom pattern to block pushes&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And we enabled this pattern for Push protection:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbz3apoa5193vr0ul4nr3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbz3apoa5193vr0ul4nr3.png" alt="GitHub Actions learnings from the recent nx hack" width="800" height="274"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Enable custom pattern for push protection&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;While there are cases where &lt;code&gt;pull_request_target&lt;/code&gt; can be useful, it's generally not worth the security risks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows?ref=jessehouwing.net#pull_request_target" rel="noopener noreferrer"&gt;From the docs&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Warning&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
For workflows that are triggered by the &lt;code&gt;pull_request_target&lt;/code&gt; event, the &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; is granted read/write repository permission unless the &lt;code&gt;permissions&lt;/code&gt; key is specified and the workflow can access secrets, even when it is triggered from a fork. Although the workflow runs in the context of the base of the pull request, you should make sure that you do not check out, build, or run untrusted code from the pull request with this event. Additionally, any caches share the same scope as the base branch. To help prevent cache poisoning, you should not save the cache if there is a possibility that the cache contents were altered. For more information, see &lt;a href="https://securitylab.github.com/research/github-actions-preventing-pwn-requests?ref=jessehouwing.net" rel="noopener noreferrer"&gt;Keeping your GitHub Actions and workflows secure: Preventing pwn requests&lt;/a&gt; on the GitHub Security Lab website.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Do not use &lt;code&gt;workflow_dispatch&lt;/code&gt; or &lt;code&gt;repository_dispatch&lt;/code&gt; triggers in workflows that have access to critical secrets.
&lt;/h3&gt;

&lt;p&gt;Since &lt;code&gt;workflow_dispatch&lt;/code&gt; and &lt;code&gt;repository_dispatch&lt;/code&gt; allow an attacker to control the branch against the workflow will run, it can be used to exfiltrate repository secrets if the attacker has write access to any other branch on the repository.&lt;/p&gt;

&lt;p&gt;The only way to protect secrets from this attack path, is to store them as an environment secret.&lt;/p&gt;
&lt;h3&gt;
  
  
  Do not mix pull-request validation and continuous integration workflows
&lt;/h3&gt;

&lt;p&gt;Many developers will try to reduce duplication by combining multiple workflows into 1 and defining multiple triggers or conditions. These conditions then control what code is executed depending on the event that triggered the workflow.&lt;/p&gt;

&lt;p&gt;A really common example is to have a single pull request validation workflow that also doubles as the first set of steps of the full CI build.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;publish.yml&lt;/code&gt; in the &lt;code&gt;nx&lt;/code&gt; project has 3 such purposes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Release the packages for the nx project whenever a new tag is created&lt;/li&gt;
&lt;li&gt;A canary release based on a schedule&lt;/li&gt;
&lt;li&gt;Creation of a test-build for pull requests
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: publish

on:
  # Automated schedule - canary releases from master
  schedule:
    - cron: "0 3 * * 2-6" # Tuesdays - Saturdays, at 3am UTC
  # Manual trigger - PR releases or dry-runs (based on workflow inputs)
  workflow_dispatch:
    inputs:
      pr:
        description: "PR Number - If set, a real release will be created for the branch associated with the given PR number. If blank, a dry-run of the currently selected branch will be performed."
        required: false
        type: number
  release:
    types: [published]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The &lt;code&gt;nx&lt;/code&gt; project does this by restricting the build and publish jobs to only run when the triggering repository is owned by &lt;code&gt;nrwl&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    build:
      if: ${{ github.repository_owner == 'nrwl' }}

    publish:
      if: ${{ github.repository_owner == 'nrwl' }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which would prevent the publish job from running against pull requests from a forked repository.&lt;/p&gt;

&lt;p&gt;However, since the leaked token had write-all permission, it would have been easy to publish a new branch which would satisfy the &lt;code&gt;repository_owner&lt;/code&gt; condition and thus trigger the publication jobs.&lt;/p&gt;

&lt;p&gt;More restrictions could have been added, like checking against the actor to see whether it's a maintainer or preventing workflows triggered by GitHub Actions, or restricting jobs to specific branches. But when the attacker has write permissions to the repository, none of these will likely save you.&lt;/p&gt;

&lt;p&gt;A better approach would be to split the workflow into 2 separate workflows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;publish.yaml&lt;/code&gt; - which publishes the production packages&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pr-validation.yaml&lt;/code&gt; - which builds a PR validation package&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These two workflows can use a 3rd &lt;a href="https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows?ref=jessehouwing.net" rel="noopener noreferrer"&gt;callable workflow&lt;/a&gt;, or one or more &lt;a href="https://docs.github.com/en/actions/tutorials/create-actions/create-a-composite-action?ref=jessehouwing.net" rel="noopener noreferrer"&gt;Composite actions&lt;/a&gt; to reduce code duplication while also reducing chances of an attacker calling a workflow they're not meant to call.&lt;/p&gt;

&lt;p&gt;Another advantage of callable workflows and composite actions is that they can be stored in a different repository, limiting an attacker's ability to leverage a leaked Actions Token to change the workflow code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Store critical secrets in environment secrets, instead of repository secrets.
&lt;/h3&gt;

&lt;p&gt;Secrets defined at the repository level are accessible to all workflows. There is no way to limit access to repository level secrets.&lt;/p&gt;

&lt;p&gt;In order to protect a critical secret, such as the NPM token used to publish your package, it should be defined as an Environment Secret.&lt;/p&gt;

&lt;p&gt;In the Environment's policy you can then set a branch policy to limit which branches have access to the secret.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft9y721u5yuqahvewvwfp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft9y721u5yuqahvewvwfp.png" alt="GitHub Actions learnings from the recent nx hack" width="704" height="460"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Set specific branches to limit access to critical secrets.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Optionally add &lt;strong&gt;Required Reviewers&lt;/strong&gt; to prevent access to the secrets without manual approval:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy013g55h7hdodrqhcrjx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy013g55h7hdodrqhcrjx.png" alt="GitHub Actions learnings from the recent nx hack" width="698" height="447"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Add Required Reviewers to prevent access to the secrets without manual approval&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then update the workflow to reference the environment when defining the job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;jobs:
  deploy:
    environment: public
    steps:
      uses: actions/deploy@sha
      with:
        npm_token: ${{ secrets.NPM_TOKEN }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/nrwl/nx/commit/f9293243032b516d8c15fbbf2e4750b46da2d1a5?ref=jessehouwing.net" rel="noopener noreferrer"&gt;It looks like the &lt;code&gt;nx&lt;/code&gt; project has already implemented this in a recent change&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pass secrets only to the steps that need them
&lt;/h3&gt;

&lt;p&gt;In the nx repository &lt;a href="https://github.com/nrwl/nx/blob/400e3003d72acd75522468d58fdc160e76e13a68/.github/workflows/publish.yml?ref=jessehouwing.net#L433-L436" rel="noopener noreferrer"&gt;the &lt;code&gt;NPM_AUTH_TOKEN&lt;/code&gt; is defined at the job level&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  publish:
    env:
      # DO NOT USE THIS!
      GH_TOKEN: ${{ github.token }}
      NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
    steps:
      - ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the secrets available to all steps in the workflow. If the attacker can take control of any of the code executed in these steps, they would be able to exfiltrate the secret.&lt;/p&gt;

&lt;p&gt;In this case the job also calls &lt;code&gt;pnpm build:wasm&lt;/code&gt; which adds a number of additional places to hide exfiltration code.&lt;/p&gt;

&lt;p&gt;It's better to explicitly pass the secrets only to the steps that need them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  publish:
    env:
      # NO SECRETS SET HERE
    steps:
      - ...
      - ...
      - name: Publish
        env:
          VERSION: ${{ needs.resolve-required-data.outputs.version }}
          DRY_RUN: ${{ needs.resolve-required-data.outputs.dry_run_flag }}
          PUBLISH_BRANCH: ${{ needs.resolve-required-data.outputs.publish_branch }}
          NX_VERBOSE_LOGGING: true
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
        run: |
          echo ""
          # Create and check out the publish branch
          git checkout -b $PUBLISH_BRANCH
          echo ""
          echo "Version set to: $VERSION"
          echo "DRY_RUN set to: $DRY_RUN"
          echo ""
          pnpm nx-release --local=false $VERSION $DRY_RUN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pass data to script steps using the environment
&lt;/h3&gt;

&lt;p&gt;The Script Injection attack was possible because variables were in-lined in the body of a script step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      - name: Create PR message file
        run: |
          mkdir -p /tmp
          cat &amp;gt; /tmp/pr-message.txt &amp;lt;&amp;lt; 'EOF'
          ${{ github.event.pull_request.title }} &amp;lt;- unsafe variable reference

          ${{ github.event.pull_request.body }} &amp;lt;- unsafe variable reference
          EOF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To pass data safely to scripts, you should instead pass them through the environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      - name: Create PR message file
        run: |
          mkdir -p /tmp
          echo $PR_TITLE &amp;gt;&amp;gt; /tmp/pr-message.txt
          echo "" &amp;gt;&amp;gt; /tmp/pr-message.txt
          echo $PR_BODY &amp;gt;&amp;gt; /tmp/pr-message.txt
        env:
          PR_TITLE: ${{ github.event.pull_request.title }}
          PR_BODY: ${{ github.event.pull_request.body }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Script Injection can be detected by GitHub Advanced Security Code Scanning for Actions as well as &lt;a href="https://github.com/rhysd/actionlint?ref=jessehouwing.net" rel="noopener noreferrer"&gt;ActionLint&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pin all actions to their sha.
&lt;/h3&gt;

&lt;p&gt;The attack on nx doesn't seem to have relied on this attack vector, but the recent attack on tj-actions/changed-files has shown again that it's dangerous to rely on only the tag to reference actions of 3rd parties.&lt;/p&gt;

&lt;p&gt;GitHub recommends that you always reference actions you do not control using their explicit full commit sha.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      - uses: actions/checkout@v4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Should have used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.blog/changelog/2025-08-15-github-actions-policy-now-supports-blocking-and-sha-pinning-actions/?ref=jessehouwing.net" rel="noopener noreferrer"&gt;There is now a policy which requires all actions to be pinned to their commit sha&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm3lkrcjdsjshsi0woqvu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm3lkrcjdsjshsi0woqvu.png" alt="GitHub Actions learnings from the recent nx hack" width="800" height="420"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Require actions to be pinned to a full-length commit SHA&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because this is a new policy, GitHub Administrators must explicitly opt-in to this policy.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There are several tools available to perform the pinning on your behalf:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/amenocal/gh-pin-actions?ref=jessehouwing.net" rel="noopener noreferrer"&gt;gh-pin-actions extension for the GitHub CLI.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.renovatebot.com/modules/manager/github-actions/?ref=jessehouwing.net#digest-pinning-and-updating" rel="noopener noreferrer"&gt;RenovateBot - Digest Pinning and Updating&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once, pinned, if enabled, Dependabot will automatically submit a pull request with the correct new commit sha. Unfortunately, it doesn't automatically change the tag &lt;code&gt;v4&lt;/code&gt; to its sha.&lt;/p&gt;
&lt;h3&gt;
  
  
  Enable GitHub Advanced Security Code Scanning for GitHub Actions and add Branch Ruleset.
&lt;/h3&gt;

&lt;p&gt;The script injection vulnerability at the root of this attack would have been detected by GitHub Advanced Security Code Scanning for GitHub Actions.&lt;/p&gt;

&lt;p&gt;GitHub Advanced Security is free for all public repos, so there is not really a good reason not to enable it.&lt;/p&gt;

&lt;p&gt;Because support for GitHub Actions was added recently, GitHub Repository Owners must explicitly enable GitHub Actions in the Code Scanning configuration:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgjp1vht2gd7se1dd5goe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgjp1vht2gd7se1dd5goe.png" alt="GitHub Actions learnings from the recent nx hack" width="713" height="195"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Enable GitHub Actions for CodeQL Code Scanning&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Or add &lt;code&gt;actions&lt;/code&gt; to the list of supported languages in their workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;jobs:
  analyze:
    name: Analyze (${{ matrix.language }})
    runs-on: ${{ (matrix.language == 'swift' &amp;amp;&amp;amp; 'macos-latest') || 'ubuntu-latest' }}
    permissions:
      security-events: write
      packages: read
      actions: read
      contents: read

    strategy:
      fail-fast: false
      matrix:
        include:
        # Make sure Actions is added to the list of languages:
        - language: actions
          build-mode: none
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will detect a number of GitHub Actions vulnerabilities, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Script Injection&lt;/li&gt;
&lt;li&gt;Unsafe checkout&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In order to prevent workflows with these vulnerabilities to enter the repository, add a branch ruleset that requires the Code QL workflow to succeed:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flkzba2nxk54oovksdtsn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flkzba2nxk54oovksdtsn.png" alt="GitHub Actions learnings from the recent nx hack" width="675" height="337"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Enable Require status checks to pass and add the CodeQL check explicitly&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And require Code Scanning results:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv8k48tkj852zk444f3dj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv8k48tkj852zk444f3dj.png" alt="GitHub Actions learnings from the recent nx hack" width="680" height="217"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Enable Require code scanning results and add CodeQL explicitly.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;An alternative for GitHub Advanced Security Code Scanning for Actions that would also have detected the Script Injection vulnerability is &lt;a href="https://github.com/rhysd/actionlint/?ref=jessehouwing.net" rel="noopener noreferrer"&gt;ActionLint&lt;/a&gt;. &lt;a href="https://github.com/marketplace/actions/rhysd-actionlint?ref=jessehouwing.net" rel="noopener noreferrer"&gt;Actionlint can also be added as a workflow&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Enable Dependabot for GitHub Actions to automatically update action references to the latest version.
&lt;/h3&gt;

&lt;p&gt;To automatically keep your referenced GitHub Actions up to date, especially when you are pinning to the full commit sha, you can enable Dependabot to automatically create a pull request whenever an action is updated.&lt;/p&gt;

&lt;p&gt;To enable Dependabot for GitHub Actions, explicitly add &lt;code&gt;github-actions&lt;/code&gt; to your &lt;code&gt;.github/dependabot.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/" # Location of package manifests
    schedule:
      interval: "weekly"

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

&lt;/div&gt;



&lt;p&gt;To reduce maintainer burden, &lt;a href="https://docs.github.com/en/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions?ref=jessehouwing.net" rel="noopener noreferrer"&gt;GitHub Actions can be used to automate updates completely&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Consider 3rd party security hardening tools like StepSecurity's Harden Runner.
&lt;/h3&gt;

&lt;p&gt;All of the above remediations are built-in features of the GitHub platform.&lt;/p&gt;

&lt;p&gt;There are 3rd party solutions available to further secure GitHub Actions. Of those StepSecurity's Harden Runner is probably the best-known option. You can add harden-runner to any workflow by adding a single step to your workflow jobs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;steps:
  - name: Harden Runner
    uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
    with:
      egress-policy: audit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Community version of Harden runner offers the following protections to all of workflows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD-Aware Event Correlation:&lt;/strong&gt;  Each outbound network connection, file operation, and process execution is mapped to the exact step, job, and workflow where it occurs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated Baseline Creation:&lt;/strong&gt;  Harden-Runner builds a baseline for each job based on past outbound network connections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anomaly Detection:&lt;/strong&gt;  Once the baseline is created, any future outbound calls not in the baseline trigger a detection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block Network Egress Traffic with Domain Allowlist:&lt;/strong&gt;  Optionally use the automatically created baseline to control outbound network traffic by specifying allowed domains, preventing unauthorized data exfiltration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detect Modification of Source Code:&lt;/strong&gt;  Monitor and alert on unauthorized changes to your source code during the CI/CD pipeline.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;GitHub offers many features to protect your project against attacks that try to abuse GitHub Actions. Unfortunately, many of these features require explicit opt-in or configuration.&lt;/p&gt;

&lt;p&gt;This should not be an excuse to leave your open-source projects open to attack.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;em&gt;Dear GitHub,&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;We love the unlimited free environment and all the tools you provide to keep our open-source projects free and healthy. And while we love your commitment to not break our workflows, it's not helpful to make many of the protections you offer opt-in.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Please, please, please start enabling core security features automatically, at least on popular open-source projects as part of your roll-out strategy.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The new GitHub Advanced Security Code Scanning for GitHub Actions would have alerted the maintainers of the &lt;code&gt;nx&lt;/code&gt; project to the Script Injection vulnerability. But it isn't activated by default, nor are maintainers warned they must opt-in to receive this protection.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The long ago changed default read-only permissions for GitHub Actions would have prevented the triggering of the publish workflow, but this setting is kept to the organization default, and maintainers aren't warned they should change this setting.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Requiring workflows triggered by external contributors to always be approved by a maintainer would have at least triggered a human intervention as part of the attack chain. But this new default has also not been rolled out to existing organizations, nor are maintainers warned they should really change this setting.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Pinning Actions to their full commit sha has been a recommendation for years now. Yet GitHub has not provided any tooling to make this process easier, and while the policy has finally arrived, again it has rolled out with relatively low fanfare and maintainers are not warned they should really change this setting. Nor does Dependabot or GitHub Advanced Security Code Scanning for Actions submit a pull request to pin these actions.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This hasn't been the first attack to leverage the &lt;code&gt;pull_request_target&lt;/code&gt; trigger, I'd wish workflows with this trigger would not automatically enable and would require explicit enablement by an owner through a danger-zone action which clearly spells out the risks.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;So again, please, start enabling these core security features automatically and make such roll-out part of the introduction of new features such as these.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>github</category>
      <category>security</category>
    </item>
    <item>
      <title>Authenticate Connect-MgGraph using OIDC in GitHub Actions</title>
      <dc:creator>Jesse Houwing</dc:creator>
      <pubDate>Tue, 10 Jun 2025 15:08:58 +0000</pubDate>
      <link>https://forem.com/jessehouwing/authenticate-connect-mggraph-using-oidc-in-github-actions-5bkb</link>
      <guid>https://forem.com/jessehouwing/authenticate-connect-mggraph-using-oidc-in-github-actions-5bkb</guid>
      <description>&lt;p&gt;I'm running a number of maintenance scripts against our Azure EntraId to manage GitHub related things. Removing dormant users, asking users to setup their notification email correctly etc.&lt;/p&gt;

&lt;p&gt;For a long time, I ran these scripts with an interactive session, before moving them over to GitHub Actions. Recently I made the move to convert my scripts to a workflow and changed the authentication mechanism to OIDC-connect, in order to remove the need to manage secrets and tokens.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure-openid-connect?ref=jessehouwing.net" rel="noopener noreferrer"&gt;documentation is quite clear on how to set things up&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Unfortunately, while it explains how to setup an Azure PowerShell session using &lt;code&gt;enable-AzPSSession: true&lt;/code&gt;, it doesn't explain how to authenticate using &lt;code&gt;connect-mggraph&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After some experimentation (and of course some help from GitHub Copilot), I figured out I could query the access token from &lt;code&gt;az&lt;/code&gt;, then pass that straight to &lt;code&gt;connect-mggraph&lt;/code&gt;. So now my workflow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Azure CLI Login&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/login@v2&lt;/span&gt;
    &lt;span class="s"&gt;with&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;client-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_CLIENT_ID }}&lt;/span&gt;
      &lt;span class="na"&gt;tenant-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_TENANT_ID }}&lt;/span&gt;
      &lt;span class="na"&gt;allow-no-subscriptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Assign Costcenters&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
       &lt;span class="s"&gt;$accessToken = az account get-access-token --resource https://graph.microsoft.com `&lt;/span&gt;
         &lt;span class="s"&gt;--query accessToken --output tsv&lt;/span&gt;
       &lt;span class="s"&gt;write-host "::add-mask::$accessToken"&lt;/span&gt;
       &lt;span class="s"&gt;$accessToken = $accessToken | ConvertTo-SecureString -AsPlainText -Force&lt;/span&gt;
       &lt;span class="s"&gt;Connect-MgGraph -AccessToken $accessToken -NoWelcome&lt;/span&gt;

       &lt;span class="s"&gt;# rest of the script here ... &lt;/span&gt;
    &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pwsh&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And my scripts can happily query the Microsoft Graph.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>github</category>
      <category>azure</category>
      <category>security</category>
    </item>
    <item>
      <title>Picking the right GitHub Copilot model</title>
      <dc:creator>Jesse Houwing</dc:creator>
      <pubDate>Fri, 25 Apr 2025 09:24:19 +0000</pubDate>
      <link>https://forem.com/jessehouwing/picking-the-right-github-copilot-model-52k6</link>
      <guid>https://forem.com/jessehouwing/picking-the-right-github-copilot-model-52k6</guid>
      <description>&lt;p&gt;GitHub recently released documentation on all the large language models available in GitHub Copilot. The list is ever expanding, especially when you have previews turned on for your account. But it's still hard for many people to pick the right model for their task.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.github.com/en/copilot/using-github-copilot/ai-models/choosing-the-right-ai-model-for-your-task" rel="noopener noreferrer"&gt;You can find the official guidance in the GitHub Documentation.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I wanted to make model selection even easier, so I asked GitHub Copilot to read the doc for me and suggest the right model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#fetch https://docs.github.com/en/copilot/using-github-copilot/ai-models/choosing-the-right-ai-model-for-your-task

What model would I use to plan a large refactoring involving multiple files for which I do not yet have a working example?

Select the best 2 and explain the pros and cons between them
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it actually came back with a pretty good suggestion:&lt;/p&gt;

&lt;p&gt;To plan a large refactoring involving multiple files without a working example, the two best models to consider are  &lt;strong&gt;GPT-4.5&lt;/strong&gt;  and  &lt;strong&gt;GPT-4o&lt;/strong&gt;. Here's a comparison of their pros and cons:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;GPT-4.5&lt;/strong&gt;
&lt;/h3&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Pros:&lt;/strong&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deep Reasoning and Contextual Understanding:&lt;/strong&gt;  GPT-4.5 excels at handling complex logic and multi-step reasoning, making it ideal for large-scale refactoring tasks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-File Context:&lt;/strong&gt;  It can retain and process context across multiple files, which is crucial for understanding dependencies and relationships in a large codebase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliable and Consistent Results:&lt;/strong&gt;  Produces high-quality, structured suggestions for improving maintainability and modularity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nuanced Problem-Solving:&lt;/strong&gt;  Handles ambiguous or incomplete requirements effectively, which is helpful when no working example exists.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Cons:&lt;/strong&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Higher Latency:&lt;/strong&gt;  Slightly slower response times compared to smaller models like GPT-4o.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Higher Cost:&lt;/strong&gt;  More resource-intensive, which may not be ideal for cost-sensitive scenarios.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;GPT-4o&lt;/strong&gt;
&lt;/h3&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Pros:&lt;/strong&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Faster Response Times:&lt;/strong&gt;  Provides quicker iterations, which is beneficial for rapid back-and-forth during refactoring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Balanced Performance:&lt;/strong&gt;  Offers a good mix of reasoning and speed, making it suitable for lightweight tasks or smaller refactoring steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost-Effective:&lt;/strong&gt;  Less expensive than GPT-4.5, making it a better choice for budget-conscious projects.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Cons:&lt;/strong&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Limited Depth for Complex Tasks:&lt;/strong&gt;  While capable, it may struggle with deeply nuanced or highly interconnected refactoring tasks compared to GPT-4.5.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduced Cross-File Context:&lt;/strong&gt;  May not handle dependencies across multiple files as effectively as GPT-4.5.&lt;/li&gt;
&lt;/ul&gt;




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

&lt;ul&gt;
&lt;li&gt;Use  &lt;strong&gt;GPT-4.5&lt;/strong&gt;  if the refactoring involves complex logic, dependencies across multiple files, or requires high-quality, structured suggestions.&lt;/li&gt;
&lt;li&gt;Use  &lt;strong&gt;GPT-4o&lt;/strong&gt;  if you need faster iterations, are working on smaller or less complex refactoring tasks, or are constrained by cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both models are strong candidates, but the choice depends on the complexity of the refactoring and your priorities (speed vs. depth).&lt;/p&gt;

</description>
      <category>githubcopilot</category>
      <category>github</category>
      <category>visualstudiocode</category>
    </item>
    <item>
      <title>Say goodbye to your Personal Access Tokens</title>
      <dc:creator>Jesse Houwing</dc:creator>
      <pubDate>Tue, 04 Mar 2025 12:30:04 +0000</pubDate>
      <link>https://forem.com/jessehouwing/say-goodbye-to-your-personal-access-tokens-4b20</link>
      <guid>https://forem.com/jessehouwing/say-goodbye-to-your-personal-access-tokens-4b20</guid>
      <description>&lt;p&gt;We got rid of all Azure DevOps PAT usage and so should you. &lt;/p&gt;

&lt;p&gt;📢 Big shout out to Jesse! &lt;/p&gt;

&lt;p&gt;Without his blog post and direct help, I was probably still renewing expired PATs manually. But let's start at the beginning.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Michael, author of some of my favorite Azure DevOps Extensions, reached out on the &lt;a href="https://www.azuredevops.club/?ref=jessehouwing.net" rel="noopener noreferrer"&gt;Azure DevOps Club slack channel&lt;/a&gt; for help. He wanted to automatically rotate Personal Access Tokens to integrate with Azure DevOps, which lead to a quest to completely eradicate Personal Access Tokens altogether. After sharing my work to make the Azure DevOps Extension Tasks work without Personal Access Tokens, Michael went to work. I've asked him to share his results in this guest blog, since I feel they're useful for anyone doing automation against Azure DevOps. As you can see he agreed!  &lt;/p&gt;

&lt;p&gt;Jesse Houwing&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  PATs, PATs and more PATs - our 5 scenarios we used PATs for
&lt;/h2&gt;

&lt;p&gt;Building &lt;a href="https://www.bravonotes.com/?ref=jessehouwing.net" rel="noopener noreferrer"&gt;Bravo Notes&lt;/a&gt; here at Agile Extensions we are extensively using the Azure DevOps REST APIs in many ways.&lt;/p&gt;

&lt;p&gt;And I'm not talking about the API calls the Bravo Notes extension makes at runtime e.g. to load work items, publish wiki pages etc.&lt;/p&gt;

&lt;p&gt;We use the Azure DevOps REST APIs in these &lt;strong&gt;5 scenarios&lt;/strong&gt; as well:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;publish multiple versions of the Bravo Notes extension for development, staging and production via the marketplace APIs (in Azure Pipelines)&lt;/li&gt;
&lt;li&gt;retrieve marketplace event data for installs/uninstalls and more on a schedule (in Azure Functions app)&lt;/li&gt;
&lt;li&gt;run integration tests for many Bravo Notes components that need Azure DevOps APIs (in Azure Pipelines)&lt;/li&gt;
&lt;li&gt;run e2e tests of the main Bravo Notes app outside of Azure DevOps (in Azure Pipelines)&lt;/li&gt;
&lt;li&gt;run integration and e2e tests locally&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the past we used personal access tokens (PATs) for all of those scenarios.&lt;/p&gt;

&lt;p&gt;For years our process was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;watch for notifications that a PAT was about to expire&lt;/li&gt;
&lt;li&gt;be annoyed that this manual regular chore was on your plate (AGAIN!)&lt;/li&gt;
&lt;li&gt;regenerate PAT in Azure DevOps&lt;/li&gt;
&lt;li&gt;update pipeline variables and secrets with the new PAT&lt;/li&gt;
&lt;li&gt;feel bad and wonder why there wasn't a more elegant solution&lt;/li&gt;
&lt;li&gt;be done and push responsibility for improving this to future Michael&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  PAT REST APIs to the rescue?
&lt;/h2&gt;

&lt;p&gt;A while I ago a new set of APIs got introduced to manage PATs. That sounded intriguing and a few weeks back I reached out to the community to ask for guidance on how to automatically renew and manage PATs and do away with the manual process entirely.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🤔 Me: Is there a good resource online on how to transition from manually rotating PATs to rotating PATs automatically (a pipeline using PAT REST API, KeyVault)?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Thankfully Jesse pointed me to a better (the right) way:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Don't use PATs&lt;/strong&gt; but use an Azure Service principle with workload Federation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When you are reading this on his blog you probably already know that Jesse is one of those people who uses the internet for what it was build for:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;He shares what he has learned (often the hard way) to make the lives of all of us easier.&lt;/strong&gt; In this case in form of &lt;a href="https://dev.to/jessehouwing/publish-azure-devops-extensions-using-azure-workload-identity-2979"&gt;this blog post on how to publish marketplace extensions without using PATs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I was convinced already that using PATs was less than ideal.&lt;/p&gt;

&lt;p&gt;But at this point I was still hesitant about whether this approach could help us do away with PATs entirely. After all we had 5 different scenarios.&lt;/p&gt;

&lt;p&gt;There was only one way to find out so we started by tackling the scenario that Jesse wrote about in his blog post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scenario 1: Publish marketplace extensions without a PAT
&lt;/h2&gt;

&lt;p&gt;The blog post didn't disappoint as it contained all the pieces to help us &lt;strong&gt;publish marketplace extensions without a PAT&lt;/strong&gt; - whohoo 🥳!&lt;/p&gt;

&lt;p&gt;Below are the steps we needed to take. Please refer to Jesse's blog post for all the datails. Our mileage varied a bit as the ARM service connection now uses an "App registration" in Azure instead of a "Service Principal".&lt;/p&gt;

&lt;p&gt;The process is the still the same in general, though.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create the ARM service connection (the UX changed a bit since Jesse's blog post)

&lt;ul&gt;
&lt;li&gt;Choose "Azure Resource Manager" Connection&lt;/li&gt;
&lt;li&gt;Identity Type: "App registration (automatic)"&lt;/li&gt;
&lt;li&gt;Credential: "Workload identity federation"&lt;/li&gt;
&lt;li&gt;the rest is about the same&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Add the App registration/service principal as a user in Azure DevOps&lt;/li&gt;
&lt;li&gt;Extract the Azure DevOps Identity Id from the Profile API via a pipeline task&lt;/li&gt;
&lt;li&gt;Use that Id to add the App registration as a member to the marketplace publisher&lt;/li&gt;
&lt;li&gt;Update our pipeline to use the ARM service connection&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Note that we could use the newest version of the marketplace extension tasks (v5) that allows you to publish using the ARM service connection directly.&lt;/p&gt;

&lt;p&gt;So there is no need to fetch an access token and overwrite the marketplace service connection credentials anymore. After changing our pipelines we were able to delete our old "Visual Studio Marketplace" service connection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- task: PublishAzureDevOpsExtension@5
  displayName: 'Publish Extension'
  inputs:
    connectTo: 'AzureRM'
    connectedServiceNameAzureRM: 'marketplace-service-connection'
    fileType: vsix
    vsixFile: '$(Pipeline.Workspace)/vsix/production.vsix'
    updateTasksVersion: false

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

&lt;/div&gt;



&lt;p&gt;And that was it. The pipeline worked as before and we could revoke the PAT for publishing extensions to the marketplace.&lt;/p&gt;

&lt;p&gt;Of course we didn't want to stop here and tackle the next scenario and remove one more PAT.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scenario 2: Call Azure DevOps APIs from a Azure Function app
&lt;/h2&gt;

&lt;p&gt;We maintain an Azure Function app to query data about install and uninstall events and pull statistics from the marketplace. Authentication works using - what else could it be - a PAT.&lt;/p&gt;

&lt;p&gt;In Azure Functions of course we don't have the magic of service connections that we can utilize in Azure Pipelines.&lt;/p&gt;

&lt;p&gt;After initially failing to find the code to use for our NodeJS based function app, searching for the magic GUID &lt;code&gt;499b84ac-1321-427f-aa17-267ca6975798&lt;/code&gt; that represents Azure DevOps led me right to what we needed to acquire an access token inside the Function app.&lt;/p&gt;

&lt;p&gt;After finding the right way to acquire the access token, we got this solved in 2 simple steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add the Azure Function app as a user in Azure DevOps&lt;/li&gt;
&lt;li&gt;Use the code below to acquire an access token and authorize Azure DevOps REST API calls
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const { ManagedIdentityCredential } = require("@azure/identity");

async function getMarketplaceAccessToken() {
    const credential = new ManagedIdentityCredential({
        // `AZURE_CLIENT_ID` points to the App registration created via the ARM service connection
        clientId: process.env.AZURE_CLIENT_ID,
    });
    const tokenResponse = await credential.getToken(
        "499b84ac-1321-427f-aa17-267ca6975798/.default",
        {
            tenantId: process.env.AZURE_TENANT_ID,
        }
    );

    return tokenResponse.token;
}

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

&lt;/div&gt;



&lt;p&gt;There was no stopping now - but still 3 PATs to go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scenario 3: Run intergration tests in Azure Pipelines
&lt;/h2&gt;

&lt;p&gt;Next up was our integration testing pipeline tasks that run integration tests using NodeJS and &lt;code&gt;jest&lt;/code&gt;. Until now a PAT was used from a secret pipeline variable. Another one that we didn't want to renew manually every few months.&lt;/p&gt;

&lt;p&gt;As we already acquired an access token in a pipeline via an ARM service connection in scenario 1, this dind't seem to difficult to pull off.&lt;/p&gt;

&lt;p&gt;Following the least priviledge principle we didn't want to use the same service principal from scenario 1. This would have given it access to the marketplace AND Azure DevOps resources that were needed for our integration tests.&lt;/p&gt;

&lt;p&gt;Luckily you can create as many ARM service connections as you want and so we did. To use the newly created App registration/service principal, the pipeline task from Jesse's blog post to retrieve an access token that we dind't need for scenario 1 now came in handy again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- task: AzureCLI@2
  displayName: 'Accquire token for integration testing'
  inputs:
    azureSubscription: 'azure-devops-integration-testing-connection'
    scriptType: 'pscore'
    scriptLocation: 'inlineScript'
    useGlobalConfig: true
    inlineScript: |
      $accessToken = az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --query "accessToken" --output tsv
      write-host "##vso[task.setsecret]$accessToken"
      write-host "##vso[task.setvariable variable=SECRET_INGETRATION_TESTING_TOKEN;issecret=true]$accessToken"

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

&lt;/div&gt;



&lt;p&gt;Now we could use the the token for subsequent integration test tasks. Yay only 2 PATs to go!&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 4: Run e2e tests in Azure Pipelines (against external Azure DevOps organization)
&lt;/h3&gt;

&lt;p&gt;While this scenario seemed to be exactly the same es scenario 3 at first, we had an issue here that stopped us in our tracks at first.&lt;/p&gt;

&lt;p&gt;The e2e tests called APIs in a separate Azure DevOps org that only existed for testing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[OrgA Pipeline] ---- REST API call ----&amp;gt; [OrgB Work item API]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The solution was too simple to be true.&lt;/p&gt;

&lt;p&gt;We were just able to add the App registration/service principal as a user in OrgB and give it access to the resources needed for the e2e tests.&lt;/p&gt;

&lt;p&gt;Another PAT down!&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 5: Running integration and e2e tests locally
&lt;/h3&gt;

&lt;p&gt;This might be my least favorite solution so far, but it was simple and did the trick for now.&lt;/p&gt;

&lt;p&gt;We use &lt;code&gt;jest&lt;/code&gt; to run our tests and in our npm scripts we just stuck in a Azure Developer CLI call to log in prior to starting the test script.&lt;/p&gt;

&lt;p&gt;In our code to set up API authentication we now run some special code to acquire the access token, again with the azure identity SDK.&lt;/p&gt;

&lt;p&gt;So&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Modify testing script to log in - &lt;code&gt;azd auth login &amp;amp;&amp;amp; npx jest --config jest.integration.config.js"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Retrieve the access token from the Azure Developer Cli:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let token = process.env.SECRET_INGETRATION_TESTING_TOKEN;
if (!token &amp;amp;&amp;amp; !process.env.CI) {
    const credential = new AzureDeveloperCliCredential({
        tenantId: process.env.AZURE_TENANT_ID,
    });
    const tokenResult = await credential.getToken(
        '499b84ac-1321-427f-aa17-267ca6975798/.default',
        {
            tenantId: process.env.AZURE_TENANT_ID,
        },
    );
    token = tokenResult.token;
}

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

&lt;/div&gt;



&lt;p&gt;(Would love to hear about a better solution to securly get an access token for running scripts locally in a development environment.)&lt;/p&gt;

&lt;p&gt;All 5 scenarios done! The champaign 🍾 was almost open when I revoked all PATs that were not needed anymore when I discovered that I had forgotten one last PAT.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bonus scenario: the last token - the renovate pipline
&lt;/h3&gt;

&lt;p&gt;We use renovate to update dependencies and automatically create pull requests in yet another pipeline. (You can thank Jesse for his work online here as well).&lt;/p&gt;

&lt;p&gt;Immideately I had a bad feeling and I thought that renovate would only accept PATs as it only accepts &lt;code&gt;RENOVATE_TOKEN&lt;/code&gt; as a means to authenticate. The &lt;a href="https://docs.renovatebot.com/modules/platform/azure/?ref=jessehouwing.net" rel="noopener noreferrer"&gt;docs&lt;/a&gt; make it look that way for sure.&lt;/p&gt;

&lt;p&gt;Looking at the code (yay open source) I could see that renovate automatically detects whether the token is a PAT or a OAuth token and chooses the Authorization header (&lt;code&gt;Basic&lt;/code&gt; or &lt;code&gt;Bearer&lt;/code&gt;) accordingly.&lt;/p&gt;

&lt;p&gt;So we could just use the same AzureCLI pipeline task as in scenarios 3 to acquire and 4 and be done!&lt;/p&gt;

&lt;p&gt;P.S.: Talking to Jesse about that he mentioned that Azure DevOps now accepts OAuth tokens when using the &lt;code&gt;Basic&lt;/code&gt; authentication scheme as well which is great in my opinion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Just reading all the terms involved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ARM service connection&lt;/li&gt;
&lt;li&gt;Open Id Connect&lt;/li&gt;
&lt;li&gt;Workload Identity federation&lt;/li&gt;
&lt;li&gt;service principal&lt;/li&gt;
&lt;li&gt;App registration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;made the whole process seem daunting to me.&lt;/p&gt;

&lt;p&gt;But in the end we got rid of all our PATs within a couple of hours.&lt;/p&gt;

&lt;p&gt;Personal access tokens are great to quickly test an API call, authenticate a throw-away script where you don't have other means of authentication ready.&lt;/p&gt;

&lt;p&gt;However for the long term you really don't want to have to create PATs and renew them until the end of time.&lt;/p&gt;

&lt;p&gt;Thanks again to Jesse and Joost Voskuil for their help.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In case you are facing your own challenge with Azure DevOps or GitHub, don't hesitate to join the &lt;a href="https://www.azuredevops.club/?ref=jessehouwing.net" rel="noopener noreferrer"&gt;Azure DevOps Club&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>azuredevops</category>
      <category>azure</category>
      <category>security</category>
    </item>
    <item>
      <title>GitHub's new Billing - Assigning Cost Centers in Bulk</title>
      <dc:creator>Jesse Houwing</dc:creator>
      <pubDate>Thu, 20 Feb 2025 14:10:17 +0000</pubDate>
      <link>https://forem.com/jessehouwing/githubs-new-billing-assigning-cost-centers-in-bulk-2nel</link>
      <guid>https://forem.com/jessehouwing/githubs-new-billing-assigning-cost-centers-in-bulk-2nel</guid>
      <description>&lt;p&gt;You can assign resources to cost centers, which will in turn charge any costs associated with that resource to that cost center.&lt;/p&gt;

&lt;p&gt;Examples of ressource categories are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repositories - All costs associated with a specific repository. Action Minutes, Codespaces, LFS Storage etc.&lt;/li&gt;
&lt;li&gt;Users - All costs associated with a specific user. Enterprise Seat, Copilot, Advanced Security.&lt;/li&gt;
&lt;li&gt;Organizations - All costs associated with a specific organization. Costs generated by any unassigned repositories in the organization. GitHub Packages storage and network traffic. &lt;/li&gt;
&lt;li&gt;Enterprise - All costs associated with the whole enterprise. Costs associated with users who are not explicitly assigned to a cost center.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By linking organizations, users and repositories to a cost center, you can cause their specific costs to be redirected from the Enterprise to the specific Cost Center.&lt;/p&gt;

&lt;p&gt;Linking Organizations and Repositories can be done through the Cost Center UI in the GitHub Enterprise Admin portal:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fezcl1smzn3tl8pos0dwr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fezcl1smzn3tl8pos0dwr.png" alt="GitHub's new Billing - Assigning Cost Centers in Bulk" width="800" height="504"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;In the Enterprise Admin portal, you can assign Organizations and Repositories to Cost Centers. Members can only be assigned through the API.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you're lucky there are only a few Organizations that need to be assigned, but in our case, Members were a whole other story. We already have 500+ members and external collaborators in our enterprise and these needed to be assigned to specific cost centers. As you can see, this can't be done in the UI, but requires API calls.&lt;/p&gt;

&lt;p&gt;It turns out the API is a bit tricky, there are certain things about the API that aren't (yet) nicely documented:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can only assign users to a cost center in batches of 50 (confirmed with GitHub Support)&lt;/li&gt;
&lt;li&gt;You can only assign a user if it isn't already assigned to another cost center. Changing cost centers requires 2 operations.&lt;/li&gt;
&lt;li&gt;The API is eventually consistent. Changes made may take a couple of seconds to show up in subsequent API calls.&lt;/li&gt;
&lt;li&gt;While you could assign cost centers on a per-user basis, this is a great way to burn through your API Rate Limits quickly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To work around these issues, I've written a small wrapper around the API in PowerShell which uses the existing resource assignments stored in the Cost Centers to reduce the number of required calls to the API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function Update-CostCenterResources {
    param(
        [Parameter(Mandatory=$true)]
        [string[]]$Handles,

        [Parameter(Mandatory=$true)]
        [ValidateSet('Add','Delete')]
        [string]$Action,

        [Parameter(Mandatory=$true)]
        $CostCenter,

        [Parameter(Mandatory=$true)]
        [string]$Enterprise
    )

    switch ($Action)
    {
        'Add' {
            $method = 'POST'
            $Handles = $Handles | Where-Object { 
                $handle = $_
                return (($costCenter.resources | ?{ $_.type -eq "User" } | ?{$_.name -eq $handle }).Count -eq 0)
            }
        }
        'Delete' {
            $method = 'DELETE'
            $Handles = $Handles | Where-Object { 
                $handle = $_
                return (($costCenter.resources | ?{ $_.type -eq "User" } | ?{$_.name -eq $handle }).Count -gt 0)
            }
        }
    }

    # Call fails when processing too many users at once. Thus batching the calls...
    $count = 0
    do {
        $batch = $Handles | Select-Object -Skip $count -First 50
        $count += $batch.Count

        if ($batch.Count -gt 0) {
            $body = @{
                users = [string[]]$batch
            }

            $_ = ($body | ConvertTo-Json) | gh api --method $method /enterprises/$Enterprise/settings/billing/cost-centers/$($CostCenter.id)/resource --input -
        }
    } while ($batch.Count -gt 0)
}

$enterprise = "xebia"
$costCenters = (invoke-gh -fromJson -- api /enterprises/$enterprise/settings/billing/cost-centers).costCenters

$costCenterNL = $costCenters | ?{ $_.name -eq "Netherlands" }

$handles = @("jessehouwing", "jessehouwing-demo")

# First remove the users from their currently assigned cost centers (if any)
$costCenters | 
  ?{ $_.id -ne $costCenterNL.id } | 
  ?{ $_ | ?{ $.resources | ?{ $_.type -eq "User" -and $_.name -in $handles } } | 
  %{ 
    Update-CostCenterResources -handles $handles -action "Delete" -CostCenter $_ -Enterprise $enterprise
  }

# Then assign the users to their new cost center
Update-CostCenterResources -handles $handles -action "Add" -CostCenter $costCenterNL -Enterprise $enterprise
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can find the current cost center for a user in the Cost Center's Resources array, or from the GitHub Enterprise assigned-seats REST API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$handle = "jessehouwing"
$enterprise = "xebia"

# retrieve from Cost Center API
$costCenters = (gh api /enterprises/$enterprise/settings/billing/cost-centers | ConvertFrom-Json).costCenters
$currentCostCenter = $costCenters | ?{ $_.resources | ?{ $_.type -eq "User" -and $_.name -eq $handle } }

# retrieve from Assigned Seats API:
$enterpriseUsers = gh api https://api.github.com/enterprises/$enterprise/consumed-licenses --jq '.users[]' --paginate | ConvertFrom-Json
$currentCostCenter = ($enterpriseUsers | ?{ $_.github_com_login -eq $handle }).github_com_cost_center
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By combining the above logic with some more proprietary magic, we were able to quickly assign all our members to Cost Centers based on their Azure EntraID metadata.&lt;/p&gt;

&lt;p&gt;As you can see in the chart below, when we assigned out cost centers on January 9th, all future costs were no longer associated to the enterprise (in green), but to the respective cost centers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8btwiqcgcmai90dtjrnb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8btwiqcgcmai90dtjrnb.png" alt="GitHub's new Billing - Assigning Cost Centers in Bulk" width="800" height="297"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Costs distributed between Enterprise and a number of Cost Centers as of January 9th.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: It is currently not possible to retroactively assign a resource to a cost center. Otherwise, we'd have assigned all costs as of the 1st of the month.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>github</category>
    </item>
    <item>
      <title>Use inline completions for more answers with GitHub Copilot Free</title>
      <dc:creator>Jesse Houwing</dc:creator>
      <pubDate>Mon, 10 Feb 2025 16:03:31 +0000</pubDate>
      <link>https://forem.com/jessehouwing/use-inline-completions-for-more-answers-with-github-copilot-free-5626</link>
      <guid>https://forem.com/jessehouwing/use-inline-completions-for-more-answers-with-github-copilot-free-5626</guid>
      <description>&lt;p&gt;2 months ago, GitHub gave everybody their taste of GitHub Copilot with the limited free tier. You get 2000 completions and 50 chats.&lt;/p&gt;

&lt;p&gt;This is awesome! But 50 chats aren't that many.&lt;/p&gt;

&lt;p&gt;Did you know you can use your completions for simple chat answers as well? And this is a nice tip for NeoVim users as well who don't have access to Chat in their editor. Inline Suggestions are significantly faster than Chat to boot.&lt;/p&gt;

&lt;p&gt;You can use inline completions to get tips, answer questions and suggest improvements to your code as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q&amp;amp;A
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Q: Explain the key usage scenarios for the Singleton pattern
// A: The singleton pattern |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In most cases typing &lt;code&gt;// A:&lt;/code&gt; will do the trick, but sometimes you'll need to give Copilot a starting sentence to provide you with an answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Q: Explain the key usage scenarios for the Singleton pattern. A detailed description
// A: The Singleton pattern is used when we want to ensure that a class has only one instance and provide a global point of access to that instance. Some key usage scenarios for the Singleton pattern include:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you type &lt;code&gt;// -&lt;/code&gt; on the next line or open the completions panel (&lt;code&gt;ctrl-enter&lt;/code&gt;), you'll get more details:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// 1. Global Access: It provides a single point of access to the instance of the class, allowing other objects to easily access and use its functionality.
// 2. Resource Management: It can be used to manage shared resources, such as database connections, file systems, or configuration settings, ensuring that there is only one instance to avoid conflicts or resource wastage.
// 3. Caching: It can be used to implement a cache that stores frequently used data or objects in memory, reducing the need to recreate or fetch them from external sources.
// 4. Logging: It can be used to create a single logger instance that logs messages from different parts of the application, centralizing the logging functionality.
// 5. Configuration Settings: It can be used to manage application-wide configuration settings, ensuring that changes are reflected consistently across the application.
// 6. State Management: It can be used to maintain a shared state or context across different parts of the application, allowing objects to share and update state information.
// 7. Thread Safety: It can be used to provide a thread-safe way to access and modify shared resources or data, ensuring that concurrent access does
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Suggest test cases
&lt;/h2&gt;

&lt;p&gt;You can also ask questions about the contents of the current file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Suggest additional test cases to add to this test fixture, signatures only:
// |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns the following suggestions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// public void GetById_WhenFlightDoesNotExist_ReturnsNull()
// public void Add_WhenFlightIsNull_DoesNotAddFlightToRepository()
// public void Add_WhenFlightHasNullFlightNumber_ThrowsException()
// public void Add_WhenFlightHasEmptyFlightNumber_ThrowsException()
// public void Add_WhenFlightHasNullOrigin_ThrowsException()
// public void Add_WhenFlightHasEmptyOrigin_ThrowsException()
// public void Add_WhenFlightHasNullDestination_ThrowsException()
// public void Add_WhenFlightHasEmptyDestination_ThrowsException()
// public void Add_WhenFlightHasDepartureTimeInThePast_ThrowsException()
// public void Add_WhenFlightHasArrivalTimeBeforeDepartureTime_ThrowsException()
// public void Add_WhenFlightHasFuelRangeLessThanZero_ThrowsException()
// public void Add_WhenFlightHasFuelTankLeakTrue_ThrowsException()
// public void Add_WhenFlightHasNullFlightLogSignature_ThrowsException()
// public void Add_WhenFlightHasEmptyFlightLogSignature_ThrowsException()
// public void Add_WhenFlightHasNullAerobaticSequenceSignature_ThrowsException()
// public void Add_WhenFlightHasEmptyAerobaticSequenceSignature_ThrowsException()
// public void Add_WhenFlightHasDuplicateId_ThrowsException()
// public void Add_WhenFlightHasDuplicateFlightNumber_ThrowsException()
// public void Add_WhenFlightHasDuplicateOriginAndDestination_ThrowsException()
// public void Add_WhenFlightHasDuplicateFlightLogSignature_ThrowsException()
// public void Add_WhenFlightHasDuplicateAerobaticSequenceSignature_ThrowsException()
// public void Add_WhenFlightHasDuplicateFlightNumberAndOriginAndDestination_ThrowsException()
// public void Add_WhenFlightHasDuplicateFlightNumberAndFlightLogSignature_ThrowsException()
// public void Add_WhenFlightHasDuplicateFlightNumberAndAerobaticSequenceSignature_ThrowsException()
// public void Add_WhenFlightHasDuplicateFlightNumberAndOriginAndDestinationAndFlightLogSignature_ThrowsException()
// public void Add_WhenFlightHasDuplicateFlightNumberAndOriginAndDestinationAndAerobaticSequenceSignature_ThrowsException()
// public void Add_WhenFlightHasDuplicateFlightNumberAndFlightLogSignatureAndAerobaticSequenceSignature_ThrowsException()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Suggest improvements
&lt;/h2&gt;

&lt;p&gt;You can ask for improvements you can make to the current code file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// To improve the security of this REST API I can:
// |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And GitHub Copilot will reply with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// add an authorization filter to this controller
// and require authentication for all actions
// using the [Authorize] attribute
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can tweak the replies by adding &lt;code&gt;// and&lt;/code&gt; or &lt;code&gt;//something else I could do is&lt;/code&gt;&lt;/p&gt;

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

&lt;p&gt;If you run out of Chat messages, you can continue the conversation using inline suggestions. If you run out of those too, you seem to be getting enough value from GitHub Copilot to warrant a subscription ;).&lt;/p&gt;

&lt;p&gt;This also adds a bit of Chat support to NeoVim, Qt Explorer, XCode and other tools that only support inline suggestions for now.&lt;/p&gt;

</description>
      <category>githubcopilot</category>
      <category>github</category>
      <category>visualstudiocode</category>
    </item>
    <item>
      <title>Troubleshooting GitHub Copilot keyboard shortcuts in Jetbrains IDEs</title>
      <dc:creator>Jesse Houwing</dc:creator>
      <pubDate>Wed, 22 Jan 2025 18:39:29 +0000</pubDate>
      <link>https://forem.com/jessehouwing/troubleshooting-github-copilot-keyboard-shortcuts-in-jetbrains-ides-2ho</link>
      <guid>https://forem.com/jessehouwing/troubleshooting-github-copilot-keyboard-shortcuts-in-jetbrains-ides-2ho</guid>
      <description>&lt;p&gt;Over the past months I've delivered 100s of GitHub Copilot workshops. Ranging from 2 hour introduction talks, where I quickly walk through the main features of the product, up to day-long workshops with interactive hands-on.&lt;/p&gt;

&lt;p&gt;For most people it's pretty easy to get started. In Visual Studio Code you install a plugin, in Visual Studio 2022 it's installed by default, but in other IDEs it can be a bit trickier to get GitHub Copilot to work.&lt;/p&gt;

&lt;p&gt;In my experience the different &lt;a href="https://www.jetbrains.com/ides/?ref=jessehouwing.net" rel="noopener noreferrer"&gt;Jetbrains IDEs are among the trickiest. This includes IntelliJ, Rider, Pycharm, Webstorm, RubyMine and more&lt;/a&gt;. So far I've found two main problems that break GitHub Copilot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keyboard shortcut is already assigned to another command
&lt;/h2&gt;

&lt;p&gt;The Jetbrains IDEs, for many users, are an alternative IDE which at some point in their career replaced another product. Because of that there are many predefined keymaps available. For example, Rider offers the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IntelliJ&lt;/li&gt;
&lt;li&gt;Emacs&lt;/li&gt;
&lt;li&gt;Resharper&lt;/li&gt;
&lt;li&gt;Sublime Text&lt;/li&gt;
&lt;li&gt;Visual Assist&lt;/li&gt;
&lt;li&gt;Visual Studio&lt;/li&gt;
&lt;li&gt;Visual Studio 2022&lt;/li&gt;
&lt;li&gt;Visual Studio Code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plugins that install themselves in these IDEs need to find a keyboard shortcut which isn't already in use, and among all of these different layouts that leaves very few easily accessible keys available.&lt;/p&gt;

&lt;p&gt;You can see whether an assigned keyboard shortcut is conflicting with other assigned commands by double-clicking the command in the keymap and picking &lt;strong&gt;Add keyboard shortcut&lt;/strong&gt; from the context menu.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm84dx7ism8bqxib0n6ed.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm84dx7ism8bqxib0n6ed.png" alt="Troubleshooting GitHub Copilot keyboard shortcuts in Jetbrains IDEs" width="800" height="597"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;See whether an assigned keyboard shortcut is conflicting with other assigned commands by double-clicking the command in the keymap and picking Add keyboard shortcut from the context menu&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For example, the default &lt;code&gt;alt&lt;/code&gt;+&lt;code&gt;\&lt;/code&gt; keyboard shortcut is used by GitHub Copilot to Show Completions, but in the Visual Studio 2022 keymap, which is my preference, that shortcut is already assigned to 3 other commands:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8x2cvihx7g2wmltudp03.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8x2cvihx7g2wmltudp03.png" alt="Troubleshooting GitHub Copilot keyboard shortcuts in Jetbrains IDEs" width="460" height="382"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;alt+\ is already assigned to 3 other commands&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In order to use GitHub Copilot to its fullest potential, you need to go through each of its keyboard shortcuts and either assign a free key combination or remove the other assigned actions by clicking &lt;code&gt;ok&lt;/code&gt; and then choosing to &lt;code&gt;remove&lt;/code&gt; the other assigned actions:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqan1mcvf9xzp41itf1ci.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqan1mcvf9xzp41itf1ci.png" alt="Troubleshooting GitHub Copilot keyboard shortcuts in Jetbrains IDEs" width="440" height="150"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Remove other assigned actions to allow GitHub Copilot to use the keyboard shortcut&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For now, I haven't found a better solution. On the plus side, this will allow me to assign the same keyboard shortcuts I use in Visual Studio 2022 or Visual Studio Code so that I don't need to juggle even more shortcuts in my head.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conflict between GitHub Copilot and Jetbrains' bundled line completion
&lt;/h2&gt;

&lt;p&gt;Completing a suggestion using &lt;code&gt;tab&lt;/code&gt; might not work when &lt;strong&gt;GitHub Copilot&lt;/strong&gt; and Jetbrains' bundled &lt;strong&gt;Full line code completion&lt;/strong&gt; are enabled at the same time. Work is clearly underway to let these two coexist and work together.&lt;/p&gt;

&lt;p&gt;Yet I've encountered issues when GitHub Copilot and Jetbrains' Inline completion are both turned on. Under normal circumstances the two features work it out among eachother, but when connecting to a remote IDE using ssh (using GitHub Codespaces for example) the assigned keyboard shortcuts may fail.&lt;/p&gt;

&lt;p&gt;This is either fixed by ensuring &lt;strong&gt;GitHub Copilot&lt;/strong&gt; and &lt;strong&gt;Full line code completion&lt;/strong&gt; are both installed locally as well as remotely and are both up to date. In case &lt;code&gt;tab&lt;/code&gt; completion still fails, disabling the &lt;strong&gt;Full line code completion&lt;/strong&gt; plugin both locally and remotely usually solves the problem.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo3eu9h9jec5650kzmylc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo3eu9h9jec5650kzmylc.png" alt="Troubleshooting GitHub Copilot keyboard shortcuts in Jetbrains IDEs" width="800" height="597"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Disable the "Full line completion" plugin in case GitHub Copilot can't tab complete.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>github</category>
      <category>githubcopilot</category>
      <category>jetbrains</category>
      <category>rider</category>
    </item>
    <item>
      <title>The use or uselessness of signed commits</title>
      <dc:creator>Jesse Houwing</dc:creator>
      <pubDate>Wed, 05 Jun 2024 12:59:00 +0000</pubDate>
      <link>https://forem.com/jessehouwing/the-use-or-uselessness-of-signed-commits-27e9</link>
      <guid>https://forem.com/jessehouwing/the-use-or-uselessness-of-signed-commits-27e9</guid>
      <description>&lt;p&gt;This is why it's possible to sign a commit. Signing a commit adds cryptographic proof of your identity using a public/private key verification. &lt;a href="https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits?ref=jessehouwing.net" rel="noopener noreferrer"&gt;Commits can be signed using GPG, SSH or S/MIME from your workstation&lt;/a&gt; and GitHub can sign commits you make on the web "on your behalf" (it basically signs it with GitHub's own key).&lt;/p&gt;

&lt;p&gt;I'll use GitHub as an example hosting platform for your git repositories, but conceptually this applies to Git in general and all other Git hosting platforms.&lt;/p&gt;

&lt;p&gt;To verify your identity on GitHub, you need to upload your keys, so they can verify you are who you are. If done correctly, you'll see a small badge☑️ / (verified) next to a commit that is signed this way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkk84uqf4cc5ujsyhc1me.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkk84uqf4cc5ujsyhc1me.png" alt="The use or uselessness of signed commits" width="800" height="91"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The cryptographic signature is part of the commit, so when a repository is transferred from one machine to another or one hosting provider to another, you should still be able to verify the repo is "intact" if you have a copy of all the public keys of the contributors. This is why commit-signing works agnostic to the repository host. &lt;strong&gt;This is also why it's so hard to do it right.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Great!" You might say. And many experts will confirm this on the web, in conference talks and books, including a few of my own colleagues. But if you read through these blogs, you'll see immediately that it isn't simple to set up.&lt;/p&gt;

&lt;p&gt;And, yes, it's great... In a way. And it's also not great. Let me explain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the &lt;code&gt;verified&lt;/code&gt; badge mean?
&lt;/h2&gt;

&lt;p&gt;A commit that shows the verified badge will tell you the following:&lt;/p&gt;

&lt;p&gt;✅ The commit was signed&lt;br&gt;&lt;br&gt;
✅ The signature is correct&lt;br&gt;&lt;br&gt;
✅ The signature matches the email used in the commit&lt;br&gt;&lt;br&gt;
✅ The key used to sign is known by GitHub&lt;br&gt;&lt;br&gt;
✅ They key and email are owned by the account that's shown (in this case &lt;code&gt;octocat&lt;/code&gt;)&lt;br&gt;&lt;br&gt;
✅ Whether the contents of the commit (still) match the signature&lt;br&gt;&lt;br&gt;
✅ Potentially: That the key is owned/derived/trusted by an organization or corporation.&lt;br&gt;&lt;br&gt;
✅ &lt;em&gt;The commit was likely made by the person you think made the commit&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Great!&lt;/p&gt;
&lt;h2&gt;
  
  
  What does the &lt;code&gt;verified&lt;/code&gt; badge &lt;em&gt;not&lt;/em&gt; mean?
&lt;/h2&gt;

&lt;p&gt;A commit that shows the verified badge doesn't tell you the following:&lt;/p&gt;

&lt;p&gt;⛔ Whether the signer inspected and approves the content of the commit&lt;br&gt;&lt;br&gt;
⛔ Whether the contents of the commit were altered between staging and committing&lt;br&gt;&lt;br&gt;
⛔ Whether the signer made the commit in person (or had automation sign on their behalf)&lt;br&gt;&lt;br&gt;
⛔ Whether the account is compromised or not&lt;br&gt;&lt;br&gt;
⛔ Whether the account owner has securely stored their private key&lt;br&gt;&lt;br&gt;
⛔ Whether the account is actually the account you were expecting. This may be difficult for you to distinguish. Is it &lt;code&gt;Octocat&lt;/code&gt; or &lt;code&gt;0ctocat&lt;/code&gt;?&lt;br&gt;&lt;br&gt;
⛔ Whether the account is stil owned by the same person last time you encountered their profile (&lt;a href="https://gitprotect.io/blog/github-repojacking-are-you-sure-your-github-is-safe/?ref=jessehouwing.net" rel="noopener noreferrer"&gt;it might have been repo-jacked&lt;/a&gt;)&lt;br&gt;&lt;br&gt;
⛔ &lt;em&gt;In essence: Whether the commit was actually made by the person you think made the commit&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So, the &lt;code&gt;verified&lt;/code&gt; badge isn't a seal of approval, nor a universal signal of trust&lt;/p&gt;
&lt;h2&gt;
  
  
  What does the &lt;code&gt;unverified&lt;/code&gt; badge mean?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq27dvyso4iyfccma29nt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq27dvyso4iyfccma29nt.png" alt="The use or uselessness of signed commits" width="800" height="297"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That leaves us with the &lt;code&gt;unverified&lt;/code&gt; badge. Which might be the most useful signal that commit signing may give. it signals that:&lt;/p&gt;

&lt;p&gt;🟠 The commit was signed with a key that doesn't belong to the account that made the commit.&lt;br&gt;&lt;br&gt;
🟠 The commit was signed with a key that doesn't match a known email for the account that made the commit&lt;br&gt;&lt;br&gt;
🟠 The commit wasn't signed and &lt;a href="https://github.blog/changelog/2021-04-28-flag-unsigned-commits-with-vigilant-mode?ref=jessehouwing.net" rel="noopener noreferrer"&gt;the account has enabled &lt;strong&gt;vigilant mode&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
🟠 &lt;em&gt;You cannot trust the commit was made by the person you think made the commit.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  How can this all go wrong
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Generating and storing your keys
&lt;/h3&gt;

&lt;p&gt;To perform commit signing correctly, users must take several steps.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They need to acquire a keypair. 

&lt;ul&gt;
&lt;li&gt;Most people will generate a ssh key or a GPG key on their box.&lt;/li&gt;
&lt;li&gt;Some companies will generate a key for you and hand it over to you.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;You need to protect the private keys in some way

&lt;ul&gt;
&lt;li&gt;Encrypt them using a passphrase&lt;/li&gt;
&lt;li&gt;Protect them with a 2FA token (like a YubiKey)&lt;/li&gt;
&lt;li&gt;Set the file system permissions&lt;/li&gt;
&lt;li&gt;Store them in a software key-vault (for example: windows credential manager or 1password)&lt;/li&gt;
&lt;li&gt;Or put them on a smartcard&lt;/li&gt;
&lt;li&gt;Back up the key somewhere safe&lt;/li&gt;
&lt;li&gt;Delete the unprotected key thoroughly&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;You need to put the private key on every device, VM, container, user profile you want to use to sign commits. (Or generate a new key for each device, but then, how will people know they should trust your keys?).&lt;/li&gt;
&lt;li&gt;You need to upload your keys to GitHub and other platforms where you want your identity to be known.&lt;/li&gt;
&lt;li&gt;You need to make sure your keys are "locked" most of the time to prevent malware on your machine to intercept the key during development.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you search for instructions to start signing your commits, you'll find the &lt;a href="https://docs.github.com/en/authentication/managing-commit-signature-verification/generating-a-new-gpg-key?ref=jessehouwing.net" rel="noopener noreferrer"&gt;generic instructions to generate the key and to setup git to use the key&lt;/a&gt;, but these generally won't tell you how to then store the private key securely and how to ensure it is locked most of the time.&lt;/p&gt;

&lt;p&gt;I've seen a lot of people that have stored their key material in their user's home directory, without a passphrase. I've made that mistake myself at least once using my career.&lt;/p&gt;

&lt;p&gt;Recently &lt;a href="https://blog.1password.com/git-commit-signing/?ref=jessehouwing.net" rel="noopener noreferrer"&gt;vendors like 1Password have taken notice of how hard it is to actually do this right, and they're now offering features to handle most of the key generation and secure storage for you&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Which is great, they're making commit signing easy. Like how Let's Encrypt made it easy for everyone to secure their website.&lt;/p&gt;

&lt;p&gt;Of course, key generation and distribution could be handled by your IT department. And they might hand you 2 Yubikeys on your first day at work. Unfortunately, for the vast majority of maintainers and contributors there is no IT department to take care of all the nitty gritty details.&lt;/p&gt;

&lt;p&gt;Or if you're contributing to the Linux kernel and submit your patch over email using a signed patch file, it's the only way to have your work integrated into the repository:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://www.kernel.org/doc/html/latest/process/submitting-patches.html?ref=jessehouwing.net" rel="noopener noreferrer"&gt;Submitting patches: the essential guide to getting your code into the kernel — The Linux Kernel documentation&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Verifying the identity of a commit author
&lt;/h3&gt;

&lt;p&gt;It still doesn't solve the issue of distributing your keys to your peers. Your commit may show &lt;code&gt;verified&lt;/code&gt; on GitHub, but you won't be able to verify the signature of a commit on your local machine, build agent or in the repo of a different vendor, like GitLab. Unlike the browsers, which are preloaded with trusted root certificates, the git client and hosts don't have a built-in system to share public keys.&lt;/p&gt;

&lt;p&gt;So, to provide true protection, not just on GitHub, you'll need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A way to distribute your public key.&lt;/li&gt;
&lt;li&gt;Be able to revoke a key&lt;/li&gt;
&lt;li&gt;Be able to rotate a key&lt;/li&gt;
&lt;li&gt;A way to receive the keys of the people you work with&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In a corporate environment, your IT or security department may be able to set this up for you, but for most contributors in the world, this is something they have to do themselves. And it is something many have no clue how to do well.&lt;/p&gt;

&lt;p&gt;What makes this even harder is that identity verification in this case is hard. Many people use an email address from &lt;code&gt;gmail.com&lt;/code&gt; or &lt;code&gt;outlook.com&lt;/code&gt;, so it would be trivial to create an email address that's similar, but different. For example, I own: &lt;code&gt;jesse.houwing@gmail.com&lt;/code&gt;, but nothing blocks you from creating &lt;code&gt;jhouwing@gmail.com&lt;/code&gt; or &lt;code&gt;jesse.h0uwing@gmail.com&lt;/code&gt; or &lt;code&gt;jesse.houw1ng@gmail.com&lt;/code&gt;, generating a key for that and registering it with github. GitHub will show &lt;code&gt;verified&lt;/code&gt; in the commit and it's up to you to inspect the identity of the underlying user. The same is true for GitHub profiles. It's not hard to create a new account, mirror a bunch or repos and then create a pull request that will look authentic at first glance.&lt;/p&gt;

&lt;p&gt;Another issue is the fact that when a GitHub account is renamed or deleted, it will become available to new users after 90 days. So &lt;code&gt;octocat&lt;/code&gt; from last year's excellent pull request may be someone completely different than &lt;code&gt;octocat&lt;/code&gt; today. Since the new owner of the account can add new key material, any commit they make will show as &lt;code&gt;verified&lt;/code&gt;. Not every repo host currently prevents you from creating highly similar usernames or warns their users a highly similar username was created. &lt;a href="https://docs.github.com/en/site-policy/other-site-policies/github-username-policy?ref=jessehouwing.net#what-if-the-username-i-want-is-already-taken" rel="noopener noreferrer"&gt;GitHub even suggests to slightly change your desired username if the account you want is already taken&lt;/a&gt;, and won't prevent you from creating an account that looks a lot like an existing account.&lt;/p&gt;

&lt;p&gt;And then there is one another issue: &lt;em&gt;privacy&lt;/em&gt;. &lt;a href="https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/setting-your-commit-email-address?ref=jessehouwing.net#about-commit-email-addresses" rel="noopener noreferrer"&gt;GitHub and other platforms allow users to make their true email address private&lt;/a&gt;. Instead github will use an address in the following format &lt;code&gt;ID+USERNAME@users.noreply.github.com&lt;/code&gt; for your commits. Users opting into this privacy feature will be even harder to verify, as you won't be able to see their true email address.&lt;/p&gt;
&lt;h3&gt;
  
  
  Validating the contents of a commit
&lt;/h3&gt;

&lt;p&gt;Even if the commit was signed by the correct person, there is no guarantee the contents of the commit were written by them.&lt;/p&gt;

&lt;p&gt;When you want to add new code to your repository you'd go through a few steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# change a set of files
echo "// small change" &amp;gt;&amp;gt; thefile.cs

# stage the changes
git add thefile.cs

# commit &amp;amp; sign
git commit -m "committing my stuff" -s

# push the commit to the remote
git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The thing is, malware could inject itself between any of these steps. So, a diligent contributor would run &lt;code&gt;git status&lt;/code&gt; and do a diff to verify the contents of the staging area before committing and the results of the commit prior to pushing. A slightly more paranoid maintainer will have to type their SSH passphrase to initiate the push. But let's be honest, most of us don't.&lt;/p&gt;

&lt;p&gt;A pre-commit hook or a git filter or an executable running in the background could change the contents of the file between &lt;code&gt;git add&lt;/code&gt; and &lt;code&gt;git commit&lt;/code&gt; and most contributors would be none the wiser. Or it might intercept your passphrase for your private key and exfiltrate it from your machine.&lt;/p&gt;

&lt;p&gt;An even simpler attack is possible: a malicious npm package may have slipped onto your machine and has nestled itself into the &lt;code&gt;package-lock.json&lt;/code&gt;, for which most diffs are hidden by default anyway:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgpo05hlv3atii100qw2u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgpo05hlv3atii100qw2u.png" alt="The use or uselessness of signed commits" width="800" height="281"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Contents of generated and large files are hidden in diff view by default.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Hopefully any changes to the code are detected during the pull request or by some tool that runs a pull request check. It wouldn't be the first time such changes slip by undetected.&lt;/p&gt;

&lt;p&gt;Unfortunately, very few IDEs and Git clients will show you the contents you are about to sign or the contents you have just signed. And even if they did, if you're committing a large change to a file, you are unlikely to notice any code that was injected.&lt;/p&gt;

&lt;p&gt;If a contributor hasn't properly protected their key material, it would be even more trivial for an attacker to take the key and send it somewhere to use it at any later point in time, without you ever noticing.&lt;/p&gt;

&lt;p&gt;And even when a user has properly set up a passphrase or 2-factor authentication, they may just leave their YubiKey in their usb port and tap it whenever prompted. Unless a user knows exactly when to expect a prompt, they may just enter their passphrase into every prompt that asks for it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A lot of the "secure" aspect of commit-signing depends on the contributor being fully aware of how many "obscure" utilities work, and when it's safe to enter their password in a prompt. I suspect that it would be much safer if GitHub would prompt the user through GitHub Mobile when they receive a push, than expecting a user to know which prompts to trust.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;verified&lt;/code&gt; badge is like the 🛡️ we removed from the browser bar.
&lt;/h2&gt;

&lt;p&gt;Which brings me to the reason I wrote this article. Currently, most contributors and maintainers that have properly set up their signing infrastructure are power users who (hopefully) know how to do this well. But pushing the general population to set up commit-signing will only degrade the trust I have in the &lt;code&gt;verified&lt;/code&gt; badge. I currently do not believe that the whole population of GitHub users will be able to set up their environment correctly or will know how to protect their private keys.&lt;/p&gt;

&lt;p&gt;In its own way, the &lt;code&gt;verified&lt;/code&gt; badge is a lot like the 🛡️ shield we used to show in the URL bar or the browser. Banks would even tell you that the shield was a beacon of trust. Websites would proudly show a badge to boast about their certificates:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyvpnbto32ankgsjk90r1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyvpnbto32ankgsjk90r1.jpg" alt="The use or uselessness of signed commits" width="485" height="323"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The '90s seal of trust&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Until &lt;code&gt;https&lt;/code&gt; became the standard and now everybody uses it. Bad actors have equipped their phishing sites with the same quality SSL certificates and the value of the shield has completely evaporated. It's only when a site truly tries to impersonate someone, that the browser now shows that you're entering dangerous territory. All major browsers now highlight when a connection is &lt;strong&gt;unsafe&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To me there is little value in the &lt;code&gt;verified&lt;/code&gt; badge, but I do see a lot of value in the &lt;code&gt;unverified&lt;/code&gt; or &lt;code&gt;invalid&lt;/code&gt; status. Those are a clear signal that something is wrong. Unfortunately, with the current mess in setting things up and the complete lack of a ubiquitous infrastructure to share public keys and verify identity effectively the presence of the &lt;code&gt;verified&lt;/code&gt; badge just won't be enough.&lt;/p&gt;

&lt;p&gt;I predict that the &lt;code&gt;verified&lt;/code&gt; badge will go the way of the &lt;code&gt;https&lt;/code&gt; shield. Once most people are verified, vendors like GitHub will not show it like a mark of trust, but they'll only warn you when something's off.&lt;/p&gt;

&lt;p&gt;There are already &lt;a href="https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches?ref=jessehouwing.net#require-signed-commits" rel="noopener noreferrer"&gt;policies you can enable on your repo to force all commits to be signed&lt;/a&gt; and for &lt;a href="https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/managing-the-commit-signoff-policy-for-your-repository?ref=jessehouwing.net" rel="noopener noreferrer"&gt;all commits made from github.com to be signed&lt;/a&gt;. These will verify that the commits weren't signed incorrectly or not signed at all. But it cannot raelly verify the commits were made by people you should trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do we need this anyway
&lt;/h2&gt;

&lt;p&gt;To make things worse, git hosting platforms like GitHub, Azure Repos, GitLab already have better ways to establish who you are. Before I push to GitHub, I'm signed in with my GitHub account, performed SSO with my corporate identity and provided multiple 2nd factors along the way. GitHub knows my email addresses, because I have verified these with GitHub already. Other hosting platforms can already do the same and they have an account identity store for all the users on their platforms. The current way of dealing with signing commits is of course there because Git doesn't need a hosting platform. It's a distributed version control system and it might be used by people who don't ever connect to GitHub or Azure Repos or GitLab. But a large portion of git's users do.&lt;/p&gt;

&lt;p&gt;GitHub won't need me to generate any keys myself to verify that &lt;strong&gt;I&lt;/strong&gt; pushed a commit to GitHub. But we need the whole signing infrastructure to ensure you can verify I authored a commit when you're not looking at it on GitHub. And to me that's probably more important information than the commit author's name and email address they had configured in their git client.&lt;/p&gt;

&lt;p&gt;I suspect that, like the Git Credential Manager which securely stores your logon credentials on your system, we will have a Git Signing Manager in the future that will be able to sign your commits with your authentication token. GitHub basically already does that when you commit changes from GitHub Codespaces or when you edit files directly on the web. I wouldn't be surprised when &lt;a href="https://gushi.org/make-dns-cert/HOWTO.html?ref=jessehouwing.net" rel="noopener noreferrer"&gt;DNS will play a central role in public key distribution&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We'll end up in a state where everyone signs their commits, but we won't see this anywhere unless there is something wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do you think?
&lt;/h2&gt;

&lt;p&gt;I've had several lengthy discussions with colleagues, friends and random people at conferences about this topic. What do you think? What does a &lt;code&gt;verified&lt;/code&gt; badge mean to you?&lt;/p&gt;

</description>
      <category>git</category>
      <category>github</category>
      <category>azurerepos</category>
      <category>security</category>
    </item>
    <item>
      <title>Don't get lost in your plans</title>
      <dc:creator>Jesse Houwing</dc:creator>
      <pubDate>Wed, 14 Feb 2024 12:18:15 +0000</pubDate>
      <link>https://forem.com/jessehouwing/dont-get-lost-in-your-plans-4g8</link>
      <guid>https://forem.com/jessehouwing/dont-get-lost-in-your-plans-4g8</guid>
      <description>&lt;p&gt;Today I got a call from a customer who is trying to understand what's going on in their teams and is getting lost among the many different Features and Product Backlog Items on their boards.&lt;/p&gt;

&lt;p&gt;There are 2 things going on at this customer.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;They're changing how they use their tools to better fit their needs.&lt;/li&gt;
&lt;li&gt;They're changing their delivery process to improve collaboration in and among teams.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;They're currently using Azure Boards to visualize their work, but the tool doesn't really matter. If they were using GitHub Projects or Jira, they would have similar issues.&lt;/p&gt;

&lt;p&gt;Let me quickly describe their setup. There are 9 teams, they're loosely connected at the moment. Teams are working on their own Product backlog Items day-to-day, but those deliver a set of working functionality across the teams in the form of a Feature. Value is currently delivered at the Feature level.&lt;/p&gt;

&lt;p&gt;In the past each team had their own backlog of Epics and Features and Product Backlog Items and this caused a lot of problems around ordering, setting priority, crafting common goals, managing dependencies.&lt;/p&gt;

&lt;p&gt;One of the configuration changes made at the tool level is to show all Epics and Features on a single Backlog and a single Board. With this change there is now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a single view on the common order&lt;/li&gt;
&lt;li&gt;a single place to visualize work with high priority&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With this change a few other things were also made transparant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There are only a few "Epics"&lt;/li&gt;
&lt;li&gt;There are a LOT of "Features"&lt;/li&gt;
&lt;li&gt;But worse, there are a LOT of "Features" in progress&lt;/li&gt;
&lt;li&gt;Their laptops have relatively small screens 😉.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's why I got that call today. The client knows they need to change their processes, but that's taking its sweet time. So, for now, they were looking for ways to not get lost in their work.&lt;/p&gt;

&lt;h2&gt;
  
  
  6 tips to not get lost in your plans that won't require you to change your process.
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reduce the amount of data you show on your cards -&lt;/strong&gt; The default settings of tools show quite a bit of data on every card. An issue number, title, who the card is assigned to, the parent item. and on top of that people tend to add more data: estimate of size, estimate of value, subtasks etc. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adjust the board's settings based on your current needs&lt;/strong&gt; - The settings of your Backlog and Kanban board are not static, you can tweak the fields based on the activity you're using the board for. For example, in refinement: show the estimates. In a scrum-of-scrums, hide most fields, except the Title. Some tools will let you quickly clone a board to make adjustments (GitHub Projects), in others you will need to manually tweak the settings each time (Azure Boards). Many tools will allow you to hide or collapse columns on your boards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Naming is important&lt;/strong&gt; - Instead of "&lt;em&gt;As a grumpy project manager I want to improve the visualization of my work in progress in order to not accidentally miss important visual signals that would help me reduce risks&lt;/em&gt;" you could use a much simpler title such as "&lt;em&gt;Reduce visual clutter&lt;/em&gt;". Then add all of the details in the card's description field.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filtering&lt;/strong&gt; - Every tool I've ever used offers filtering features. You can craft a query or a filter to show a subset of the cards on your board. In order to filter you may need to add some additional metadata to your cards in the form of a custom field or a tag. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blow up &amp;amp; zoom out&lt;/strong&gt; - Most tools either offer a built-in full screen feature as well as the ability to zoom in and out. If you're looking at the plan from a browser, hitting F11 will also remove the browser's chrome. While it may only give you a little more space, it's the small things that count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the right visualization&lt;/strong&gt; - Most tools offer multiple ways to visualize the work. Backlogs, boards, at every level, for a single team or across all teams. It's important to know which visualization is right for the activity you're doing. Doing cross-team-refinement to reduce dependencies? Use a view that visualizes your dependencies (Delivery Plans in Azure Boards for example). &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;While these tricks to tweak the tools can provide some relief, they simply hide the actual problem: &lt;strong&gt;there is too much work on the board at the same time.&lt;/strong&gt; It's well known that having a lot of Work in progress (WIP) causes work to take longer (longer cycle time). It increases context switching. It reduces collaboration between individuals and teams. &lt;strong&gt;Usually the end result is: fewer things get delivered and they all take longer.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6 tips to change your process
&lt;/h2&gt;

&lt;p&gt;Instead of hiding the problem, here are a few tricks to fix things for good.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Don't scale&lt;/strong&gt; - As my colleague Laurens Bonnema puts it: &lt;em&gt;the first rule of scaling: don't&lt;/em&gt;. As my friend Simon Reindl says in his Scrum classes: &lt;em&gt;Nail it before you scale it&lt;/em&gt;. Or as I often bluntly put it: &lt;strong&gt;If you scale shit, you get heaps of it.&lt;/strong&gt; Try to form one or a few teams around smaller objectives and coordinate each of these collectives with their own board. Improve your tools, techniques, architecture and collaboration patterns before trying to scale up further, if you even want to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improve in-team and cross-team collaboration&lt;/strong&gt; - Reducing WIP and refining as you go will greatly reduce the number of things your teams are working on. There are examples from organizations practicing Mob Programming (aka Ensemble Programming or Whole Team Programming) where each team works only on a single item at a time. In that case, if you have 9 teams, you only have 9 cards in progress. Possibly those 9 cards all contribute to only 1 or 2 "Bigger items" such as a feature. To achieve this your teams will need to be composed of the "right people" to deliver a piece of valuable work. It may require that those people are capable of doing more than 1 element in your work process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduce WIP&lt;/strong&gt; - All of the tools offer setting a Work-in-progress limit on each column of your board. If you set a limit and actually stick to that limit, you'll have fewer items on your board and it will be easier to keep an overview of the total work in progress. Having a Kanban board or a Sprint board with 30 cards in a column and a scrollbar is a recipe for reduced transparency. Ensure you do not only reduce WIP at the Product backlog Item or Task level. Reducing WIP works best if also applied at the "Feature" and "Epic" level, if that's what you're using. Or at the "Project", "Theme" or "Saga" level. Try to do as few things in parallel as you can.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimize effectiveness, not busy-ness&lt;/strong&gt; - The goalkeeper in soccer has one important job, and that is to protect their team's goal. Yet, if the ball is on the other side of the field, they don't suddenly start reading their email, coach the kiddy-league or walk over to another field to protect another goal. They remain engaged in the soccer match, even if they aren't actively handling the ball. The same principle should apply to the people on your team. While the team is working on delivering their Sprint goal, if an individual can't immediately contribute, there is really no need to find less important work to keep them busy. Busy-ness is an epidemic in modern organizations. In increases WIP, it reduces collaboration and it reduces motivation and commitment over the long term.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refine &amp;amp; plan just-in-time&lt;/strong&gt; - Many organizations practice a Quarterly planning cycle on top of their bi-weekly sprint cycle. As does this customer. If, during this quarterly cycle, work is planned for the whole quarter and most of that work is immediately broken down into actionable Product Backlog Items, then each quarter starts with a completely overloaded board with way too many cards. Instead, try to make a rough selection of "large" items that may need to be delivered that quarter, order them and then start decomposing the top 1, 2, maybe 3 of these into smaller items as you go. This will greatly reduce the clutter on your boards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the right "levels"&lt;/strong&gt; - &lt;em&gt;If you're using a product Backlog decomposition across multiple levels.&lt;/em&gt; &lt;strong&gt;I personally prefer a single, short, flat backlog over a complex tree with dependencies between branches.&lt;/strong&gt; But my customer has chosen to decompose their work across Epics, Features, Product Backlog Items and Tasks. Their biggest problem is currently at the "Feature" level. That's where work from multiple teams comes together and that's where they're working on a lot of things at the same time. The big question of course is, "should this have been a feature"? They have very few Epics they're working on, a LOT of features and Product Backlog Items. There is a possibility that redefining what constitutes a Feature may simplify things. Maybe multiple features can be combined? Maybe some could be relegated to a Product Backlog Item and be moved out of this overcrowded visualization?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improve the definition of Workflow&lt;/strong&gt; - The process this customer follows has a few issues where large batches of work may occur: they plan a large batch of items at a quarterly level, decompose work far in advance and it may take them a while to get work accepted and moved to Done. Yet their workflow is relatively simple and only has a few columns with a pretty generic definition like "development". As a result, as soon as work starts on any item (at any level), cards are pulled into "development". And even though the column was defined with a WIP limit of 10, last week they had about 30 cards in that column in various sub-states. There are 2 things one may do in such a case. &lt;strong&gt;reinforce the WIP limits&lt;/strong&gt; , or in order to better understand where work is "stuck", &lt;strong&gt;further decompose these columns&lt;/strong&gt; into 2 or more (sub-)columns. This may help visualize any bottlenecks (and would reduce the need for vertical scrollbars 😉) even if it adds opportunity to set a total higher WIP limit.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  2 tips in case of emergency😜
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk0xt3b38wqsow2n5jopa.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk0xt3b38wqsow2n5jopa.jpg" alt="Don't get lost in your plans" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Buy a bigger screen&lt;/strong&gt; - Instead of trying to squeeze your whole plan into a 13" HD laptop screen, connect an external 8K 80" screen or a decent projector. Heck, a 46" ultra-wide screen can visualize a LOT of columns on your Kanban board.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buy multiple screens&lt;/strong&gt; - If a bigger screen isn't an option, maybe using multiple smaller ones may help you better understand what work is in progress,&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Knowing you have a problem is half the solution. There is only so much you can tweak with tools. Instead of trying to work around the problem and possibly hiding it for a bit longer, try making the real underlying issues transparant and addressing those.&lt;/p&gt;

</description>
      <category>scrum</category>
      <category>kanban</category>
      <category>githubprojects</category>
      <category>azureboards</category>
    </item>
  </channel>
</rss>
