Skip to content

[BRE-1932] Refactor dotnet extensions start-release.yml #280

Open
brandonbiete wants to merge 6 commits into
mainfrom
bre-1932/migrate-dotnet-extensions-release-workflows
Open

[BRE-1932] Refactor dotnet extensions start-release.yml #280
brandonbiete wants to merge 6 commits into
mainfrom
bre-1932/migrate-dotnet-extensions-release-workflows

Conversation

@brandonbiete

@brandonbiete brandonbiete commented May 19, 2026

Copy link
Copy Markdown

🎟️ Tracking

https://bitwarden.atlassian.net/browse/BRE-1932

📔 Objective

This PR migrates dotnet-extensions release workflows from repo-managed to the centralized deploy repo pattern. This enables proper release branch protection by giving only the deploy bot write access to protected branches.

What happens when you trigger a release via start-release, prerelease, and release workflows:

  1. dotnet-extensions repo's workflows (start-release.yml, prerelease.yml, or release.yml) build the package and uploads artifacts

  2. It calls trigger-actions@main (composite action) with task name and data

  3. deploy repo's trigger-actions.yml receives the deployment event and routes to the appropriate workflow

  4. deploy repo's cut-release-branch-dotnet-extensions.yml workflow runs (for start-release), which:

    • Gets bot credentials from Azure Key Vault
    • Checks out dotnet-extensions at the current version
    • Creates release branch release/{package}/{major.minor}
    • Pushes branch to dotnet-extensions (only bot can do this)
  5. deploy repo's release-dotnet-extensions.yml workflow runs (for prerelease/release), which:

    • Downloads the artifacts from step 1 using the run_id
    • Checks out dotnet-extensions at the release branch
    • Creates a GitHub release on dotnet-extensions with the artifacts
    • Publishes the .nupkg to NuGet (or GitHub Packages for prereleases) via OIDC
  6. After deploy completes, dotnet-extensions repo's workflow calls version-bump.yml, which:

    • Calculates the next version (prerelease iteration or hotfix patch bump)
    • Uploads modified .csproj files as artifacts
    • Triggers deploy repo with task: update-repository
  7. deploy repo's update-repository.yml workflow:

    • Downloads the modified files
    • Creates a bot branch with the changes
    • Opens a PR against the target branch (main or release branch)
    • Auto-merges once required checks pass

🚨 Breaking Changes

@brandonbiete brandonbiete requested review from a team as code owners May 19, 2026 19:23
@brandonbiete brandonbiete requested a review from dereknance May 19, 2026 19:23
@codecov

codecov Bot commented May 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 68.58%. Comparing base (53ebbb0) to head (bd7d1cb).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #280   +/-   ##
=======================================
  Coverage   68.58%   68.58%           
=======================================
  Files          51       51           
  Lines        1184     1184           
  Branches      104      104           
=======================================
  Hits          812      812           
  Misses        325      325           
  Partials       47       47           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@withinfocus withinfocus requested a review from justindbaur May 19, 2026 19:26
@withinfocus

Copy link
Copy Markdown
Contributor

@justindbaur to review -- we need to be prepared for this workflow change.

dereknance
dereknance previously approved these changes May 19, 2026
@dereknance dereknance dismissed their stale review May 19, 2026 21:54

Didn't see that Justin was asked to review until after I approved, so I'm dismissing my review so that this PR can't be merged just yet, to give him time.

@justindbaur justindbaur left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workflow doesn't actually do a release, this just starts a code freeze which uses the BW-GHAPP to create release/* branches and make a commit to main for vNext. The release branch action will fail if they can't do it with the Bitwarden bot. If any workflow needs to be updated it is probably pack-and-release.yml. But deploys already defer to the bitwarden/devops repo and requires approval to do the deploy. Right now the approving team has to be Platform or Arch but maybe we could just change the team to BRE.

@brandonbiete

Copy link
Copy Markdown
Author

This workflow doesn't actually do a release, this just starts a code freeze which uses the BW-GHAPP to create release/* branches and make a commit to main for vNext. The release branch action will fail if they can't do it with the Bitwarden bot. If any workflow needs to be updated it is probably pack-and-release.yml. But deploys already defer to the bitwarden/devops repo and requires approval to do the deploy. Right now the approving team has to be Platform or Arch but maybe we could just change the team to BRE.

Thank you for looking into this. I may have over-complicated this migration. Let me check with the team and see if what you've outlined aligns with the new deployment standards.

@brandonbiete brandonbiete marked this pull request as draft May 20, 2026 13:57
@brandonbiete

brandonbiete commented May 20, 2026

Copy link
Copy Markdown
Author

This workflow doesn't actually do a release, this just starts a code freeze which uses the BW-GHAPP to create release/* branches and make a commit to main for vNext. The release branch action will fail if they can't do it with the Bitwarden bot. If any workflow needs to be updated it is probably pack-and-release.yml. But deploys already defer to the bitwarden/devops repo and requires approval to do the deploy. Right now the approving team has to be Platform or Arch but maybe we could just change the team to BRE.

After the start-release workflow has ran which creates a new branch and bumps the version. The wrapper workflows, pre-release and release, are then ran manually. Those leverage the pack-and-release reusable workflow which creates a release in GitHub and then like like you said, kicks off the publish-nuget workflow that exists in the devops repo. I think our concern here is with the releases to GH, that also needs to be owned by BRE.

@brandonbiete brandonbiete requested a review from justindbaur May 20, 2026 15:47
@justindbaur

Copy link
Copy Markdown
Member

@brandonbiete If the concern is the GitHub release creation then honestly we can just delete that, it was largely just done to create a nice commit diff but I can work on something to fill that gap.

- name: Create GitHub Release
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
_PACKAGE: ${{ steps.parse-package.outputs.result }}
_CURRENT_VERSION: ${{ steps.current-version.outputs.VERSION }}
_EVENT_REF: ${{ github.event.ref }}
_INPUT_PRERELEASE: ${{ inputs.prerelease }}
with:
script: |
const package = process.env._PACKAGE;
const currentVersion = process.env._CURRENT_VERSION;
const eventRef = process.env._EVENT_REF;
const preRelease = process.env._INPUT_PRERELEASE === "true";
const currentRef = process.env.GITHUB_SHA;
// Configure Git
await exec.exec(`git config user.name "github-actions"`);
await exec.exec(`git config user.email "github-actions@github.com"`);
// List existing tags so that we could use them to link to the best full changelog
// Debug purposes only right now until there is enough data for me to make this command bullet proof
await exec.exec("git fetch --tags");
await exec.exec(`git --no-pager tag --list "${package}_v*" --no-contains "${currentRef}"`, [], {
listeners: {
stdout: function stdout(data) {
console.log(`Found tags:\n${data}`);
}
},
ignoreErrorCode: true // Just for research purposes right now, it's fine if this fails
});
// Create tag
const tag = `${package}_v${currentVersion}`;
console.log(`Creating tag & release: ${tag}`);
await exec.exec(`git tag "${tag}"`);
await exec.exec(`git push origin --tags`);
// Create release
const { data } = await github.rest.repos.createRelease({
owner: "bitwarden",
repo: "dotnet-extensions",
tag_name: tag,
target_commitish: eventRef,
name: tag,
body: "",
prerelease: preRelease,
generate_release_notes: false, // This creates a link between this and the last tag but that might not be our version
});
const templateMarker = data.upload_url.indexOf("{");
let url = data.upload_url;
if (templateMarker > -1) {
url = url.substring(0, templateMarker);
}
const globber = await glob.create("**/*.nupkg");
const files = await globber.glob();
const fs = require("fs");
const path = require("path");
if (files.length === 0) {
core.setFailed("No files found, cannot create release.");
return;
}
for (const file of files) {
const endpoint = new URL(url);
endpoint.searchParams.append("name", path.basename(file));
const endpointString = endpoint.toString();
console.log(`Uploading file: ${file} to ${endpointString}`);
// do the upload
const uploadResponse = await github.request({
method: "POST",
url: endpointString,
data: fs.readFileSync(file),
});
console.log(`Upload response: ${uploadResponse.status}`);
}
- name: Upload artifacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: packages
path: |
**/*.nupkg
**/*.snupkg

@brandonbiete

Copy link
Copy Markdown
Author

@brandonbiete If the concern is the GitHub release creation then honestly we can just delete that, it was largely just done to create a nice commit diff but I can work on something to fill that gap.

- name: Create GitHub Release
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
_PACKAGE: ${{ steps.parse-package.outputs.result }}
_CURRENT_VERSION: ${{ steps.current-version.outputs.VERSION }}
_EVENT_REF: ${{ github.event.ref }}
_INPUT_PRERELEASE: ${{ inputs.prerelease }}
with:
script: |
const package = process.env._PACKAGE;
const currentVersion = process.env._CURRENT_VERSION;
const eventRef = process.env._EVENT_REF;
const preRelease = process.env._INPUT_PRERELEASE === "true";
const currentRef = process.env.GITHUB_SHA;
// Configure Git
await exec.exec(`git config user.name "github-actions"`);
await exec.exec(`git config user.email "github-actions@github.com"`);
// List existing tags so that we could use them to link to the best full changelog
// Debug purposes only right now until there is enough data for me to make this command bullet proof
await exec.exec("git fetch --tags");
await exec.exec(`git --no-pager tag --list "${package}_v*" --no-contains "${currentRef}"`, [], {
listeners: {
stdout: function stdout(data) {
console.log(`Found tags:\n${data}`);
}
},
ignoreErrorCode: true // Just for research purposes right now, it's fine if this fails
});
// Create tag
const tag = `${package}_v${currentVersion}`;
console.log(`Creating tag & release: ${tag}`);
await exec.exec(`git tag "${tag}"`);
await exec.exec(`git push origin --tags`);
// Create release
const { data } = await github.rest.repos.createRelease({
owner: "bitwarden",
repo: "dotnet-extensions",
tag_name: tag,
target_commitish: eventRef,
name: tag,
body: "",
prerelease: preRelease,
generate_release_notes: false, // This creates a link between this and the last tag but that might not be our version
});
const templateMarker = data.upload_url.indexOf("{");
let url = data.upload_url;
if (templateMarker > -1) {
url = url.substring(0, templateMarker);
}
const globber = await glob.create("**/*.nupkg");
const files = await globber.glob();
const fs = require("fs");
const path = require("path");
if (files.length === 0) {
core.setFailed("No files found, cannot create release.");
return;
}
for (const file of files) {
const endpoint = new URL(url);
endpoint.searchParams.append("name", path.basename(file));
const endpointString = endpoint.toString();
console.log(`Uploading file: ${file} to ${endpointString}`);
// do the upload
const uploadResponse = await github.request({
method: "POST",
url: endpointString,
data: fs.readFileSync(file),
});
console.log(`Upload response: ${uploadResponse.status}`);
}
- name: Upload artifacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: packages
path: |
**/*.nupkg
**/*.snupkg

Just had a thorough discussion around this with @pixman20 and here's what we plan to do. We are going to pull the logic out from the workflows that handle version bumping and GH releases from this repo to the deploy repo. And also migrate the nuget publishing workflow from the devops repo to the deploy repo. Everything from a developer's perspective should work exactly the same as far as being able to trigger this logic from the dotnet-extensions repo. Let me know if you have any questions.

@withinfocus

Copy link
Copy Markdown
Contributor

For what it's worth, we've been enjoying GitHub Environments for a while now and it helps the team deploy what they need via the approval process built in there. Will that be moving to the repo as well? Would be cool to see Environments scaled out for other releases there too.

@brandonbiete

Copy link
Copy Markdown
Author

For what it's worth, we've been enjoying GitHub Environments for a while now and it helps the team deploy what they need via the approval process built in there. Will that be moving to the repo as well? Would be cool to see Environments scaled out for other releases there too.

Because the deploy repo is locked down to only BRE we are not currently using GH environments there as an approval gate. We do however use it as part of trust publishing for certain products such as this. So for dotnext-extensions anyone will be able to fire off a release just as before and we are planning to lock down the release* branches so that only our deploy bot can write to them.

@brandonbiete brandonbiete force-pushed the bre-1932/migrate-dotnet-extensions-release-workflows branch from 125a540 to 74882b0 Compare May 21, 2026 15:49
@brandonbiete brandonbiete marked this pull request as ready for review May 21, 2026 21:03
@brandonbiete brandonbiete requested a review from pixman20 May 21, 2026 21:05
@brandonbiete brandonbiete requested a review from dereknance June 9, 2026 13:07
@brandonbiete

Copy link
Copy Markdown
Author

@justindbaur @dereknance could i have this re-reviewed when you have a chance, thank you!

@justindbaur justindbaur left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, took me a while to fully understand the flow between this and the deploy PR but I think this is pretty similar and should work. Just a couple minor questions about why some actions were downgraded but otherwise looks good to me.

Comment thread .github/workflows/pack-and-release.yml Outdated
Comment thread .github/workflows/pack-and-release.yml Outdated
@brandonbiete brandonbiete force-pushed the bre-1932/migrate-dotnet-extensions-release-workflows branch from 74882b0 to acfaa33 Compare June 9, 2026 19:10
@brandonbiete brandonbiete requested a review from justindbaur June 9, 2026 19:11
Updates start-release and pack-and-release workflows to use trigger-actions
pattern instead of performing operations directly. This centralizes release
control in the deploy repo.

- Modify start-release.yml to trigger deploy repo via trigger-actions
- Modify pack-and-release.yml to trigger deploy repo for release + publish
- Remove GitHub release creation (moved to deploy repo)
- Remove devops dispatch (replaced with deploy repo trigger)
- Remove version bump job (now handled by deploy repo)
@brandonbiete brandonbiete force-pushed the bre-1932/migrate-dotnet-extensions-release-workflows branch from acfaa33 to 0c8d80f Compare June 9, 2026 19:13
justindbaur
justindbaur previously approved these changes Jun 9, 2026
@brandonbiete

Copy link
Copy Markdown
Author

Will merge once https://github.com/bitwarden/deploy/pull/132 has been approved as they depend on one another.

@pixman20 pixman20 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR looks good, but will need version-bump.yml to be updated first

Comment thread .github/workflows/pack-and-release.yml
Comment thread .github/workflows/start-release.yml
Security improvement: dotnet-extensions workflows no longer access GitHub App credentials.

Changes:
- Restored bump-version jobs to start-release.yml and pack-and-release.yml
- Refactored version-bump.yml to only calculate changes (no credential access)
  - Removed Azure/Key Vault/GitHub App token steps
  - Added TODO (BRE-2027) for upload-modified-files action
  - Added trigger to deploy repo for version-bump-dotnet-extensions task
- Updated architecture documentation with new flow

Related PRs in deploy repo will add version-bump-dotnet-extensions.yml workflow.
This doc was added for context during development but should not be committed to the repo.
Uses the new update-repository workflow (BRE-2003) instead of custom
version-bump-dotnet-extensions workflow.

Changes:
- Changed task from 'version-bump-dotnet-extensions' to 'update-repository'
- Simplified data payload to just artifact_name and target_branch
- Removed package and new_version from data (not needed by generic workflow)
- update-repository.yml will handle PR creation and auto-merge

This leverages the shared protected-branch update mechanism that:
- Opens PR against target branch
- Waits for required checks to pass
- Auto-merges when checks complete
- Works for both main and release/* branches
Comment thread .github/workflows/version-bump.yml Outdated
Comment thread .github/workflows/version-bump.yml Outdated
@sonarqubecloud

Copy link
Copy Markdown

@brandonbiete

Copy link
Copy Markdown
Author

@justindbaur Can you please review this PR again. This pipeline has been re-arch'd since you last reviewed. The objective outlines how it flows end-to-end. Let me know if you have any questions.

@justindbaur justindbaur left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets go for it, we can do some releases to make sure it all works as expected and figure out any issues.

@pixman20 pixman20 added the ai-review Request a Claude code review label Jun 25, 2026
@pixman20

Copy link
Copy Markdown
Contributor

@claude review this

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

Reviewed the migration of the start-release, prerelease/release (pack-and-release), and version-bump workflows from repo-managed publishing to the centralized deploy repo pattern via trigger-actions. The change removes direct write credentials and Azure Key Vault GH App token usage from this repo, downgrades job permissions to contents: read, and sets persist-credentials: false — a least-privilege improvement that aligns with the stated goal of restricting protected-branch writes to the deploy bot. The data payloads, folded JSON scalars, and the explicit prerelease boolean flag all render as valid JSON given both callers pass explicit values, and the version-bump output now correctly consumes steps.upload.outputs.artifact_name.

Code Review Details

No blocking findings. The substantive review items (restoring removed steps, switching to the upload-modified-files output, and the update-repository pattern) were already resolved in prior review threads and are reflected in the current state.

@claude

claude Bot commented Jun 25, 2026

Copy link
Copy Markdown

Claude finished @pixman20's task in 1m 21s —— View job


Code Review: PR #280 — Refactor release workflows

  • Gather context (read diff and changed workflows)
  • Analyze changes for correctness & best practices
  • Post review feedback

I reviewed the three changed workflow files against origin/main. This is a clean, well-scoped migration to the centralized trigger-actions / deploy repo pattern. The security posture is meaningfully improved and the logic is sound. Already approved by @justindbaur — I agree it's good to merge. A few minor, non-blocking observations below.


✅ Strengths

  • Reduced privilege everywhere. contents: writecontents: read and persist-credentials: truefalse across all three jobs. The repo's workflows no longer mint a GH App token or hold write credentials — that responsibility moves to the deploy bot. This is exactly the branch-protection goal the PR set out to achieve.
  • Removed Azure KV → GH App token plumbing from all three files, replaced by a single trigger-actions@main call that handles auth via OIDC. Much less surface area in this repo.
  • Pinned action SHAs retained (checkout, setup-dotnet, github-script, upload-artifact) — good supply-chain hygiene. The checkout bump to v6.0.2 is consistent across files.

🟡 Minor / non-blocking

1. Unused job outputs — a couple of declared outputs are never consumed by any caller:

  • version-bump.yml:21-22package and new-version outputs. No workflow references bump-version.outputs.* (confirmed via grep).
  • pack-and-release.yml:16version: ${{ steps.current-version.outputs.VERSION }} on the release job. No workflow references release.outputs.version; only release.outputs.package is used by bump-version.

These are harmless but are dead declarations. If they're placeholders for a follow-up, a quick comment would help; otherwise they can be dropped.

2. Inconsistent runner versionstart-release.yml and pack-and-release.yml were bumped to ubuntu-24.04, but version-bump.yml:18 is still on ubuntu-22.04. Worth aligning for consistency (and 22.04 images are nearing end-of-support on hosted runners).

3. prerelease interpolated as a bare JSON value — in pack-and-release.yml:

"prerelease": ${{ inputs.prerelease }}

This relies on the workflow_call boolean input rendering to literal true/false (unquoted) to stay valid JSON. That holds for type: boolean inputs, so this is correct as written — just flagging that it's load-bearing. The prerelease input has no explicit default, so it defaults to false when a caller omits it, which keeps the JSON valid. No change required.


Note on data payload quoting

The string-interpolated data JSON (package, version, branch, run_id, target_branch, artifact_name) all draw from controlled sources (workflow_dispatch choice input, dotnet msbuild version, github.* contexts, action outputs), so there's no realistic injection/quote-breaking concern here. Fine as-is.


Nothing blocking — the unused outputs and runner version are the only cleanups I'd suggest, and they can be deferred. 👍
• Branch: bre-1932/migrate-dotnet-extensions-release-workflows

@pixman20

Copy link
Copy Markdown
Contributor

Approved, see Claude comments for potential improvements separate that could be done separately from this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Request a Claude code review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants