Skip to content

Commit 41e920f

Browse files
chore: migrate release flow to @changesets/cli
- Replace commit-and-tag-version with @changesets/cli + @changesets/changelog-github - Add .changeset/config.json for kjldev/purview-telemetry-sourcegenerator - Add pending changeset for AddEmbeddedAttributeDefinition feature (minor bump) - Create release-pr.yml: Version PR bot with GitHub App / PAT dual-token support - Rewrite cd.yml: draft-first release, ERR cleanup trap, immutable-release-compatible - Update ci.yml: add changeset-check job + add to ci-gate dependencies - Update Justfile: remove release-final/release-pre, add changeset/changeset-status - Create docs/release-process.md: comprehensive reusable release guide - Fix dotnet test syntax (positional path, not --solution flag) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2661e82 commit 41e920f

10 files changed

Lines changed: 722 additions & 370 deletions

File tree

.changeset/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Changesets
2+
3+
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4+
with multi-package repos, or single-package repos to help you version and publish your code. You can find the
5+
full documentation for it [in our repository](https://github.com/changesets/changesets).
6+
7+
We have a quick list of common questions to get you started engaging with this project in
8+
[our docs](https://github.com/changesets/changesets/blob/main/docs/common-questions.md). We also have a
9+
[introductory guide](https://github.com/changesets/changesets/blob/main/docs/intro-to-using-changesets.md)
10+
and information about how we [version things](https://github.com/changesets/changesets/blob/main/docs/versioning-policy.md)
11+
for further context.

.changeset/config.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
3+
"changelog": [
4+
"@changesets/changelog-github",
5+
{ "repo": "kjldev/purview-telemetry-sourcegenerator" }
6+
],
7+
"commit": false,
8+
"fixed": [],
9+
"linked": [],
10+
"access": "public",
11+
"baseBranch": "main",
12+
"updateInternalDependencies": "patch",
13+
"ignore": []
14+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"purview-telemetry-sourcegenerator": minor
3+
---
4+
5+
Use `AddEmbeddedAttributeDefinition` to inject `Microsoft.CodeAnalysis.EmbeddedAttribute` via Roslyn's official API, and apply `[global::Microsoft.CodeAnalysis.Embedded]` to all generated marker attribute types.
6+
7+
This ensures generated marker types are invisible to IDE tooling and consumer code, eliminating spurious symbol conflicts. Requires Roslyn 4.14.0+ (ships with Visual Studio 2022 17.14+ and .NET 10 SDK).

.github/workflows/cd.yml

Lines changed: 61 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,7 @@ on:
99
branches:
1010
- main
1111
paths:
12-
- .github/workflows/cd.yml
13-
- Justfile
14-
- global.json
1512
- package.json
16-
- benchmarks/**
17-
- samples/**
18-
- src/**
1913

2014
jobs:
2115
release:
@@ -35,36 +29,8 @@ jobs:
3529
with:
3630
fetch-depth: 0
3731

38-
- id: release-trigger
39-
name: Determine whether to publish a release
40-
env:
41-
EVENT_NAME: ${{ github.event_name }}
42-
BEFORE_SHA: ${{ github.event.before }}
43-
CURRENT_SHA: ${{ github.sha }}
44-
shell: bash
45-
run: |
46-
set -euo pipefail
47-
48-
if [[ "${EVENT_NAME}" == "workflow_dispatch" ]]; then
49-
echo "should_release=true" >> "${GITHUB_OUTPUT}"
50-
exit 0
51-
fi
52-
53-
if [[ "${BEFORE_SHA}" == "0000000000000000000000000000000000000000" ]]; then
54-
changed_files="$(git show --name-only --pretty='' "${CURRENT_SHA}")"
55-
else
56-
changed_files="$(git diff --name-only "${BEFORE_SHA}" "${CURRENT_SHA}")"
57-
fi
58-
59-
if grep -Fxq "package.json" <<< "${changed_files}"; then
60-
echo "should_release=true" >> "${GITHUB_OUTPUT}"
61-
else
62-
echo "should_release=false" >> "${GITHUB_OUTPUT}"
63-
fi
64-
6532
- id: version
6633
name: Read release version
67-
if: ${{ steps.release-trigger.outputs.should_release == 'true' }}
6834
shell: bash
6935
run: |
7036
set -euo pipefail
@@ -79,72 +45,62 @@ jobs:
7945
echo "prerelease=false" >> "${GITHUB_OUTPUT}"
8046
fi
8147
82-
- name: Block duplicate version
83-
if: ${{ steps.release-trigger.outputs.should_release == 'true' }}
48+
- id: release-check
49+
name: Check if release already exists
8450
env:
85-
GITHUB_TOKEN: ${{ github.token }}
51+
GH_TOKEN: ${{ github.token }}
8652
RELEASE_TAG: ${{ steps.version.outputs.tag }}
8753
shell: bash
8854
run: |
8955
set -euo pipefail
9056
91-
# A pre-existing tag is expected: commit-and-tag-version creates the tag
92-
# locally and it may be pushed before this workflow runs. Only an existing
93-
# GitHub Release is a hard error (it means the release was already published).
94-
release_status="$(
95-
curl -sS \
96-
-o release.json \
97-
-w "%{http_code}" \
98-
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
99-
-H "Accept: application/vnd.github+json" \
100-
"https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/tags/${RELEASE_TAG}"
101-
)"
102-
103-
if [[ "${release_status}" == "200" ]]; then
104-
echo "::error::Release ${RELEASE_TAG} already exists. Refusing to replace an existing release version."
105-
cat release.json
106-
exit 1
107-
fi
108-
109-
if [[ "${release_status}" != "404" ]]; then
110-
echo "::error::Unexpected GitHub API response while checking ${RELEASE_TAG} (HTTP ${release_status})."
111-
cat release.json
112-
exit 1
57+
if gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" &>/dev/null; then
58+
echo "::notice::Release ${RELEASE_TAG} already exists — skipping."
59+
echo "should_release=false" >> "${GITHUB_OUTPUT}"
60+
else
61+
echo "should_release=true" >> "${GITHUB_OUTPUT}"
11362
fi
11463
115-
rm -f release.json
116-
11764
- name: Set up .NET
65+
if: ${{ steps.release-check.outputs.should_release == 'true' }}
11866
uses: actions/setup-dotnet@v5
11967
with:
12068
global-json-file: ./global.json
12169

12270
- name: Restore dotnet tools
71+
if: ${{ steps.release-check.outputs.should_release == 'true' }}
12372
run: dotnet tool restore
12473

12574
- name: CSharpier format check
75+
if: ${{ steps.release-check.outputs.should_release == 'true' }}
12676
run: dotnet csharpier check .
12777

12878
- name: Restore main solution
79+
if: ${{ steps.release-check.outputs.should_release == 'true' }}
12980
run: dotnet restore ${{ env.MAIN_SOLUTION }}
13081

13182
- name: Build main solution
83+
if: ${{ steps.release-check.outputs.should_release == 'true' }}
13284
run: dotnet build ${{ env.MAIN_SOLUTION }} --no-restore --configuration ${{ env.CONFIGURATION }}
13385

13486
- name: Test main solution
135-
run: dotnet test --solution ${{ env.MAIN_SOLUTION }} --no-build --configuration ${{ env.CONFIGURATION }} --verbosity normal
87+
if: ${{ steps.release-check.outputs.should_release == 'true' }}
88+
run: dotnet test ${{ env.MAIN_SOLUTION }} --no-build --configuration ${{ env.CONFIGURATION }} --verbosity normal
13689

13790
- name: Restore sample solution
91+
if: ${{ steps.release-check.outputs.should_release == 'true' }}
13892
run: dotnet restore ${{ env.SAMPLE_SOLUTION }}
13993

14094
- name: Build sample solution
95+
if: ${{ steps.release-check.outputs.should_release == 'true' }}
14196
run: dotnet build ${{ env.SAMPLE_SOLUTION }} --no-restore --configuration ${{ env.CONFIGURATION }}
14297

14398
- name: Test sample solution
144-
run: dotnet test --solution ${{ env.SAMPLE_SOLUTION }} --no-build --configuration ${{ env.CONFIGURATION }} --verbosity normal
99+
if: ${{ steps.release-check.outputs.should_release == 'true' }}
100+
run: dotnet test ${{ env.SAMPLE_SOLUTION }} --no-build --configuration ${{ env.CONFIGURATION }} --verbosity normal
145101

146102
- name: Pack NuGet artifacts
147-
if: ${{ steps.release-trigger.outputs.should_release == 'true' }}
103+
if: ${{ steps.release-check.outputs.should_release == 'true' }}
148104
env:
149105
PACKAGE_VERSION: ${{ steps.version.outputs.value }}
150106
shell: bash
@@ -163,20 +119,47 @@ jobs:
163119
"/p:RepositoryCommit=${GITHUB_SHA}" \
164120
"/p:COPYRIGHT_YEAR=$(date -u +%Y)"
165121
166-
- name: Create GitHub release
167-
if: ${{ steps.release-trigger.outputs.should_release == 'true' }}
168-
uses: softprops/action-gh-release@v3
169-
with:
170-
tag_name: ${{ steps.version.outputs.tag }}
171-
name: ${{ steps.version.outputs.tag }}
172-
target_commitish: ${{ github.sha }}
173-
generate_release_notes: true
174-
prerelease: ${{ steps.version.outputs.prerelease }}
175-
fail_on_unmatched_files: true
176-
files: |
177-
./artifacts/nuget/*.nupkg
122+
- name: Publish GitHub Release
123+
if: ${{ steps.release-check.outputs.should_release == 'true' }}
124+
env:
125+
GH_TOKEN: ${{ github.token }}
126+
RELEASE_TAG: ${{ steps.version.outputs.tag }}
127+
PRERELEASE: ${{ steps.version.outputs.prerelease }}
128+
shell: bash
129+
run: |
130+
set -euo pipefail
131+
132+
# On any error: delete the draft release and its tag so the next run starts clean.
133+
# Draft releases are never immutable, so this always succeeds.
134+
cleanup() {
135+
echo "::warning::Release failed — cleaning up draft and tag for ${RELEASE_TAG}..."
136+
gh release delete "${RELEASE_TAG}" --yes --cleanup-tag 2>/dev/null || true
137+
}
138+
trap cleanup ERR
139+
140+
PRERELEASE_FLAG=""
141+
if [[ "${PRERELEASE}" == "true" ]]; then
142+
PRERELEASE_FLAG="--prerelease"
143+
fi
144+
145+
# Create as draft first — draft releases are never immutable, so assets
146+
# can be freely attached. The release becomes immutable only after --draft=false.
147+
gh release create "${RELEASE_TAG}" \
148+
--draft \
149+
--title "${RELEASE_TAG}" \
150+
--generate-notes \
151+
${PRERELEASE_FLAG} \
152+
./artifacts/nuget/*.nupkg \
178153
./artifacts/nuget/*.snupkg
179154
155+
# Publish: marks as latest and becomes immutable. Any retry will clean up above.
156+
gh release edit "${RELEASE_TAG}" \
157+
--draft=false \
158+
--latest
159+
160+
echo "✅ Release ${RELEASE_TAG} published successfully."
161+
180162
- name: Skip release publish
181-
if: ${{ steps.release-trigger.outputs.should_release != 'true' }}
182-
run: echo "package.json did not change on this push, so release publishing was skipped."
163+
if: ${{ steps.release-check.outputs.should_release != 'true' }}
164+
run: echo "Release ${{ steps.version.outputs.tag }} already exists — skipping."
165+

.github/workflows/ci.yml

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ jobs:
5252
run: dotnet build ${{ env.SOLUTION }} --no-restore --configuration ${{ env.CONFIGURATION }}
5353

5454
- name: Run tests
55-
run: dotnet test --solution ${{ env.SOLUTION }} --no-build --verbosity normal --configuration ${{ env.CONFIGURATION }}
55+
run: dotnet test ${{ env.SOLUTION }} --no-build --verbosity normal --configuration ${{ env.CONFIGURATION }}
5656

5757
build-and-test-samples:
5858
name: Build and Test Samples
@@ -78,7 +78,51 @@ jobs:
7878
run: dotnet build ${{ env.SOLUTION }} --no-restore --configuration ${{ env.CONFIGURATION }}
7979

8080
- name: Run tests
81-
run: dotnet test --solution ${{ env.SOLUTION }} --no-build --verbosity normal --configuration ${{ env.CONFIGURATION }}
81+
run: dotnet test ${{ env.SOLUTION }} --no-build --verbosity normal --configuration ${{ env.CONFIGURATION }}
82+
83+
changeset-check:
84+
name: Changeset Check
85+
runs-on: ubuntu-latest
86+
87+
steps:
88+
- name: Checkout code
89+
uses: actions/checkout@v6
90+
with:
91+
fetch-depth: 0
92+
93+
- name: Check for changeset
94+
env:
95+
HEAD_REF: ${{ github.head_ref }}
96+
PR_LABELS: ${{ toJson(github.event.pull_request.labels.*.name) }}
97+
shell: bash
98+
run: |
99+
set -euo pipefail
100+
101+
# Version Packages PRs are created by the release bot — no changeset needed
102+
if [[ "${HEAD_REF}" == changeset-release/* ]]; then
103+
echo "Version Packages PR — changeset check skipped."
104+
exit 0
105+
fi
106+
107+
# PRs labelled skip-changeset are explicitly opted out (docs-only, CI-only, etc.)
108+
if echo "${PR_LABELS}" | grep -q '"skip-changeset"'; then
109+
echo "skip-changeset label present — changeset check skipped."
110+
exit 0
111+
fi
112+
113+
git fetch origin main --depth=1
114+
115+
changed_files="$(git diff --name-only origin/main...HEAD)"
116+
changeset_files="$(echo "${changed_files}" | grep -E '^\.changeset/[^/]+\.md$' | grep -v 'README\.md' || true)"
117+
118+
if [[ -z "${changeset_files}" ]]; then
119+
echo "::error::No changeset file found."
120+
echo "::error::Run 'just changeset' to create one, or add the 'skip-changeset' label if this PR needs no release note."
121+
exit 1
122+
fi
123+
124+
echo "✅ Changeset found:"
125+
echo "${changeset_files}"
82126
83127
ci-gate:
84128
name: CI Gate
@@ -88,13 +132,15 @@ jobs:
88132
- format-check
89133
- build-and-test
90134
- build-and-test-samples
135+
- changeset-check
91136

92137
steps:
93138
- name: Verify upstream jobs succeeded
94139
env:
95140
FORMAT_CHECK_RESULT: ${{ needs['format-check'].result }}
96141
BUILD_AND_TEST_RESULT: ${{ needs['build-and-test'].result }}
97142
BUILD_AND_TEST_SAMPLES_RESULT: ${{ needs['build-and-test-samples'].result }}
143+
CHANGESET_CHECK_RESULT: ${{ needs['changeset-check'].result }}
98144
shell: bash
99145
run: |
100146
set -euo pipefail
@@ -103,6 +149,7 @@ jobs:
103149
["Format Check"]="${FORMAT_CHECK_RESULT}"
104150
["Build and Test"]="${BUILD_AND_TEST_RESULT}"
105151
["Build and Test Samples"]="${BUILD_AND_TEST_SAMPLES_RESULT}"
152+
["Changeset Check"]="${CHANGESET_CHECK_RESULT}"
106153
)
107154
108155
failures=0

.github/workflows/release-pr.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Release PR
2+
3+
permissions:
4+
contents: write
5+
pull-requests: write
6+
7+
on:
8+
push:
9+
branches:
10+
- main
11+
12+
jobs:
13+
release-pr:
14+
name: Create or update Release PR
15+
runs-on: ubuntu-latest
16+
17+
steps:
18+
- name: Generate GitHub App token
19+
id: app-token
20+
if: ${{ secrets.APP_ID != '' }}
21+
uses: actions/create-github-app-token@v1
22+
with:
23+
app-id: ${{ secrets.APP_ID }}
24+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
25+
26+
- name: Checkout code
27+
uses: actions/checkout@v6
28+
with:
29+
fetch-depth: 0
30+
token: ${{ steps.app-token.outputs.token || secrets.CHANGESET_TOKEN }}
31+
32+
- name: Set up Bun
33+
uses: oven-sh/setup-bun@v2
34+
35+
- name: Install dependencies
36+
run: bun install
37+
38+
- name: Create or update Release PR
39+
uses: changesets/action@v1
40+
with:
41+
version: bun run version-packages
42+
createGithubReleases: false
43+
env:
44+
GITHUB_TOKEN: ${{ steps.app-token.outputs.token || secrets.CHANGESET_TOKEN }}

Justfile

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,15 @@ test-s:
3838
@Write-Host "Running tests for {{ BLUE }}{{ sample_solution_file }}{{ NORMAL }} with {{ YELLOW }}{{ configuration }}{{ NORMAL }}..."
3939
@dotnet test --solution "{{ sample_solution_file }}" --configuration "{{ configuration }}"
4040

41-
# Creates a new release (final or prerelease) using bun
41+
# Creates a new changeset to describe the changes in your current branch (requires bun)
4242
[group('Versioning and Release')]
43-
release-final:
44-
@Write-Host "Committing the changes and creating a new release..."
45-
@bun release
43+
changeset:
44+
@bun changeset
4645

47-
# Creates a new prerelease using bun
46+
# Shows pending changesets and what version bump they imply (requires bun)
4847
[group('Versioning and Release')]
49-
release-pre:
50-
@Write-Host "Committing the changes and creating a new release..."
51-
@bun release -- --prerelease prerelease
48+
changeset-status:
49+
@bun changeset status
5250

5351
# Packs the source generator into a NuGet package and outputs it to the specified folder
5452
[group('Build and Test')]

0 commit comments

Comments
 (0)