From fcc16f2c14c9df78f47bb0640e3b1980258dcb82 Mon Sep 17 00:00:00 2001
From: Daniel Cazzulino
Date: Tue, 25 Jun 2024 05:21:39 -0300
Subject: [PATCH 01/13] Bump to .net6/8 for the CLI
Fixes #146
---
src/Config.Tool/Config.Tool.csproj | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Config.Tool/Config.Tool.csproj b/src/Config.Tool/Config.Tool.csproj
index 2f6bff4..9f232ee 100644
--- a/src/Config.Tool/Config.Tool.csproj
+++ b/src/Config.Tool/Config.Tool.csproj
@@ -34,7 +34,7 @@ Other
Exe
- netcoreapp3.1;net6.0
+ net6.0;net8.0
win-x64;linux-x64
dotnet-config
From c662931c0e5b102fed5c8095a8ea9c1a7c5ff4d3 Mon Sep 17 00:00:00 2001
From: Daniel Cazzulino
Date: Tue, 25 Jun 2024 05:30:41 -0300
Subject: [PATCH 02/13] Bump dependencies
---
src/CommandLine/CommandLine.csproj | 1 -
src/Config.Tests/Config.Tests.csproj | 14 +++++++-------
src/Config.Tool/Config.Tool.csproj | 3 +--
src/Config/Config.csproj | 3 +--
src/Configuration/Configuration.csproj | 3 +--
5 files changed, 10 insertions(+), 14 deletions(-)
diff --git a/src/CommandLine/CommandLine.csproj b/src/CommandLine/CommandLine.csproj
index 552b08b..230d4f4 100644
--- a/src/CommandLine/CommandLine.csproj
+++ b/src/CommandLine/CommandLine.csproj
@@ -35,7 +35,6 @@ The following heuristics are applied when providing default values:
-
diff --git a/src/Config.Tests/Config.Tests.csproj b/src/Config.Tests/Config.Tests.csproj
index d4559ed..1df97ce 100644
--- a/src/Config.Tests/Config.Tests.csproj
+++ b/src/Config.Tests/Config.Tests.csproj
@@ -1,4 +1,4 @@
-
+
net6.0
@@ -23,15 +23,15 @@
-
+
-
-
-
-
-
+
+
+
+
+
diff --git a/src/Config.Tool/Config.Tool.csproj b/src/Config.Tool/Config.Tool.csproj
index 9f232ee..de63a7d 100644
--- a/src/Config.Tool/Config.Tool.csproj
+++ b/src/Config.Tool/Config.Tool.csproj
@@ -52,9 +52,8 @@ Other
-
-
+
diff --git a/src/Config/Config.csproj b/src/Config/Config.csproj
index df047b8..049f8ed 100644
--- a/src/Config/Config.csproj
+++ b/src/Config/Config.csproj
@@ -33,9 +33,8 @@ Usage:
-
-
+
diff --git a/src/Configuration/Configuration.csproj b/src/Configuration/Configuration.csproj
index a877461..ace4925 100644
--- a/src/Configuration/Configuration.csproj
+++ b/src/Configuration/Configuration.csproj
@@ -20,8 +20,7 @@ Note: section is required and subsection is optional, just like in dotnet-config
-
-
+
From 071f704c27f7be0ac0bd2cfafe76e65936133fb9 Mon Sep 17 00:00:00 2001
From: kzu
Date: Tue, 25 Jun 2024 08:39:22 +0000
Subject: [PATCH 03/13] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Bump=20files=20with?=
=?UTF-8?q?=20dotnet-file=20sync=20#=20devlooped/oss?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Integrate more seamlessly with the existing workflows https://github.com/devlooped/oss/commit/e732f6a
- Add our implementation of JWT manifest reading and reporting https://github.com/devlooped/oss/commit/a0ae727
- Fix path to jwk.ps1 alongside the SponsorLink.targets https://github.com/devlooped/oss/commit/c4830fc
- Minor code simplification https://github.com/devlooped/oss/commit/cf154d5
- Rename sample assemblies for nicer display https://github.com/devlooped/oss/commit/93df7c7
- Minimal docs on consuming https://github.com/devlooped/oss/commit/827a1d1
- Remove dependency on ThisAssembly https://github.com/devlooped/oss/commit/c879f25
- Add nullable and generated code annotations https://github.com/devlooped/oss/commit/b2a11fa
- Whitespace and formatting https://github.com/devlooped/oss/commit/d74f511
- Improve versioning of sample package https://github.com/devlooped/oss/commit/3b943f5
- Dynamically fetch devlooped JWK from github https://github.com/devlooped/oss/commit/55124bc
- Fix scenario where multiple packages share product name https://github.com/devlooped/oss/commit/23f83bd
- Add targets for inclusion from tests https://github.com/devlooped/oss/commit/81ba912
- Simplify and unify manifest reading implementation https://github.com/devlooped/oss/commit/4fca946
- SponsorLink-enabled analyzers need copylocal https://github.com/devlooped/oss/commit/7593657
- Update dependabot.yml with some default groupings https://github.com/devlooped/oss/commit/cba10bb
- Add System.IdentityModel group https://github.com/devlooped/oss/commit/e7d18ae
- Add MS.IdentityModel to identity group https://github.com/devlooped/oss/commit/14d1868
- Exclude System.IdentityModel from System group https://github.com/devlooped/oss/commit/35ca3f3
- Fix dependabot group for tests https://github.com/devlooped/oss/commit/49661db
- Make sure build runs before pack https://github.com/devlooped/oss/commit/ede013a
- Switch to PackOnBuild=true and remove pack step https://github.com/devlooped/oss/commit/6e7a3ab
- Upload binlog artifact on debug runs https://github.com/devlooped/oss/commit/a67ae78
- Set env:gh_token if present as secret https://github.com/devlooped/oss/commit/97ebd18
- Update to checkout@v4 https://github.com/devlooped/oss/commit/5fb1723
- Cleanup build and publish to use VersionLabel https://github.com/devlooped/oss/commit/14deaea
- Add .sass-cache to ignores https://github.com/devlooped/oss/commit/d65f9c7
- Move .sass-cache down alongside other jekyll folders https://github.com/devlooped/oss/commit/551d4e0
- Ignore azure functions local settings https://github.com/devlooped/oss/commit/4bd7025
- Update .gitignore with BenchmarkDotNet artifacts default path https://github.com/devlooped/oss/commit/e20e906
- Remove whitespace and add results to ignore https://github.com/devlooped/oss/commit/ef852e7
- Only ignore App folder directly under the root https://github.com/devlooped/oss/commit/02811fa
- Honor the PackReadme=false property https://github.com/devlooped/oss/commit/1bf1eac
- NoTargets/Traversal SDKs now support central package versions too https://github.com/devlooped/oss/commit/afca922
- Enable floating versions for central packages by default https://github.com/devlooped/oss/commit/b1d14c6
- Add static usings to allow unprefixed ThrowXxxx https://github.com/devlooped/oss/commit/6dfe21f
- Add compatibility for non-SDK projects without InitializeSourceControlInformation target https://github.com/devlooped/oss/commit/6e96c59
- Set Version from VersionLabel if it's a refs/tags/ https://github.com/devlooped/oss/commit/57653a2
- Append missing trailing path to directory for icon/readme https://github.com/devlooped/oss/commit/5cec43d
- Add common sponsors metadata to assemblies https://github.com/devlooped/oss/commit/0789bf0
- Update assembly metadata format for Funding.GitHub https://github.com/devlooped/oss/commit/5801de0
- SponsorLink metadata will be opt-in only by analyzer projects https://github.com/devlooped/oss/commit/c618ea8
- Update dotnet-file.yml with fix to create pull request action https://github.com/devlooped/oss/commit/11a331d
- Don't add random wait on manual dotnet-file runs https://github.com/devlooped/oss/commit/7afe350
- Bump create-pr dependency to avoid error with existing PRs https://github.com/devlooped/oss/commit/11a8757
- Only commit markdown files when resolving includes https://github.com/devlooped/oss/commit/2c10a83
---
.github/dependabot.yml | 31 ++
.github/workflows/build.yml | 21 +-
.github/workflows/changelog.yml | 2 +-
.github/workflows/dotnet-file.yml | 5 +-
.github/workflows/includes.yml | 5 +-
.github/workflows/publish.yml | 18 +-
.github/workflows/sponsor.yml | 2 +-
.gitignore | 6 +-
.netconfig | 235 +++++++++++--
SponsorLink.sln | 43 +++
src/Directory.Build.props | 23 +-
src/Directory.Build.targets | 6 +-
src/SponsorLink/Analyzer/Analyzer.csproj | 37 +++
.../Analyzer/Properties/launchSettings.json | 11 +
.../Analyzer/StatusReportingAnalyzer.cs | 25 ++
.../buildTransitive/SponsorableLib.targets | 3 +
src/SponsorLink/Directory.Build.props | 47 +++
src/SponsorLink/Directory.Build.targets | 8 +
src/SponsorLink/Library/Library.csproj | 21 ++
src/SponsorLink/Library/MyClass.cs | 5 +
src/SponsorLink/Library/Resources.resx | 123 +++++++
src/SponsorLink/Library/readme.md | 5 +
src/SponsorLink/SponsorLink.Tests.targets | 38 +++
src/SponsorLink/SponsorLink.targets | 186 +++++++++++
.../SponsorLink/AppDomainDictionary.cs | 36 ++
.../SponsorLink/DiagnosticsManager.cs | 137 ++++++++
src/SponsorLink/SponsorLink/ManifestStatus.cs | 25 ++
src/SponsorLink/SponsorLink/Resources.es.resx | 163 +++++++++
src/SponsorLink/SponsorLink/Resources.resx | 164 ++++++++++
src/SponsorLink/SponsorLink/SponsorLink.cs | 169 ++++++++++
.../SponsorLink/SponsorLink.csproj | 79 +++++
.../SponsorLink/SponsorLinkAnalyzer.cs | 119 +++++++
src/SponsorLink/SponsorLink/SponsorStatus.cs | 25 ++
.../SponsorLink/SponsorableLib.targets | 60 ++++
src/SponsorLink/SponsorLink/Tracing.cs | 53 +++
.../Devlooped.Sponsors.targets | 102 ++++++
src/SponsorLink/SponsorLink/sponsorable.md | 5 +
src/SponsorLink/SponsorLinkAnalyzer.sln | 43 +++
src/SponsorLink/Tests/.netconfig | 15 +
src/SponsorLink/Tests/Attributes.cs | 59 ++++
src/SponsorLink/Tests/Extensions.cs | 43 +++
src/SponsorLink/Tests/JsonOptions.cs | 72 ++++
src/SponsorLink/Tests/Resources.Designer.cs | 63 ++++
src/SponsorLink/Tests/Resources.resx | 101 ++++++
src/SponsorLink/Tests/Sample.cs | 59 ++++
src/SponsorLink/Tests/SponsorLinkTests.cs | 126 +++++++
src/SponsorLink/Tests/SponsorableManifest.cs | 309 ++++++++++++++++++
src/SponsorLink/Tests/Tests.csproj | 57 ++++
src/SponsorLink/jwk.ps1 | 1 +
src/SponsorLink/readme.md | 34 ++
50 files changed, 2979 insertions(+), 46 deletions(-)
create mode 100644 SponsorLink.sln
create mode 100644 src/SponsorLink/Analyzer/Analyzer.csproj
create mode 100644 src/SponsorLink/Analyzer/Properties/launchSettings.json
create mode 100644 src/SponsorLink/Analyzer/StatusReportingAnalyzer.cs
create mode 100644 src/SponsorLink/Analyzer/buildTransitive/SponsorableLib.targets
create mode 100644 src/SponsorLink/Directory.Build.props
create mode 100644 src/SponsorLink/Directory.Build.targets
create mode 100644 src/SponsorLink/Library/Library.csproj
create mode 100644 src/SponsorLink/Library/MyClass.cs
create mode 100644 src/SponsorLink/Library/Resources.resx
create mode 100644 src/SponsorLink/Library/readme.md
create mode 100644 src/SponsorLink/SponsorLink.Tests.targets
create mode 100644 src/SponsorLink/SponsorLink.targets
create mode 100644 src/SponsorLink/SponsorLink/AppDomainDictionary.cs
create mode 100644 src/SponsorLink/SponsorLink/DiagnosticsManager.cs
create mode 100644 src/SponsorLink/SponsorLink/ManifestStatus.cs
create mode 100644 src/SponsorLink/SponsorLink/Resources.es.resx
create mode 100644 src/SponsorLink/SponsorLink/Resources.resx
create mode 100644 src/SponsorLink/SponsorLink/SponsorLink.cs
create mode 100644 src/SponsorLink/SponsorLink/SponsorLink.csproj
create mode 100644 src/SponsorLink/SponsorLink/SponsorLinkAnalyzer.cs
create mode 100644 src/SponsorLink/SponsorLink/SponsorStatus.cs
create mode 100644 src/SponsorLink/SponsorLink/SponsorableLib.targets
create mode 100644 src/SponsorLink/SponsorLink/Tracing.cs
create mode 100644 src/SponsorLink/SponsorLink/buildTransitive/Devlooped.Sponsors.targets
create mode 100644 src/SponsorLink/SponsorLink/sponsorable.md
create mode 100644 src/SponsorLink/SponsorLinkAnalyzer.sln
create mode 100644 src/SponsorLink/Tests/.netconfig
create mode 100644 src/SponsorLink/Tests/Attributes.cs
create mode 100644 src/SponsorLink/Tests/Extensions.cs
create mode 100644 src/SponsorLink/Tests/JsonOptions.cs
create mode 100644 src/SponsorLink/Tests/Resources.Designer.cs
create mode 100644 src/SponsorLink/Tests/Resources.resx
create mode 100644 src/SponsorLink/Tests/Sample.cs
create mode 100644 src/SponsorLink/Tests/SponsorLinkTests.cs
create mode 100644 src/SponsorLink/Tests/SponsorableManifest.cs
create mode 100644 src/SponsorLink/Tests/Tests.csproj
create mode 100644 src/SponsorLink/jwk.ps1
create mode 100644 src/SponsorLink/readme.md
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index f551596..c95eb73 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -7,3 +7,34 @@ updates:
directory: /
schedule:
interval: daily
+ groups:
+ Azure:
+ patterns:
+ - "Azure*"
+ - "Microsoft.Azure*"
+ Identity:
+ patterns:
+ - "System.IdentityModel*"
+ - "Microsoft.IdentityModel*"
+ System:
+ patterns:
+ - "System*"
+ exclude-patterns:
+ - "System.IdentityModel*"
+ Extensions:
+ patterns:
+ - "Microsoft.Extensions*"
+ Web:
+ patterns:
+ - "Microsoft.AspNetCore*"
+ Tests:
+ patterns:
+ - "Microsoft.NET.Test*"
+ - "xunit*"
+ - "coverlet*"
+ ThisAssembly:
+ patterns:
+ - "ThisAssembly*"
+ ProtoBuf:
+ patterns:
+ - "protobuf-*"
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 0480456..c671ecc 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -17,9 +17,12 @@ on:
env:
DOTNET_NOLOGO: true
+ PackOnBuild: true
+ GeneratePackageOnBuild: true
VersionPrefix: 42.42.${{ github.run_number }}
VersionLabel: ${{ github.ref }}
-
+ GH_TOKEN: ${{ secrets.GH_TOKEN }}
+
defaults:
run:
shell: bash
@@ -31,7 +34,7 @@ jobs:
matrix: ${{ steps.lookup.outputs.matrix }}
steps:
- name: 🤘 checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: 🔎 lookup
id: lookup
@@ -50,13 +53,13 @@ jobs:
os: ${{ fromJSON(needs.os-matrix.outputs.matrix) }}
steps:
- name: 🤘 checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: 🙏 build
- run: dotnet build -m:1
+ run: dotnet build -m:1 -bl:build.binlog
- name: ⚙ GNU grep
if: matrix.os == 'macOS-latest'
@@ -67,8 +70,12 @@ jobs:
- name: 🧪 test
uses: ./.github/workflows/test
- - name: 📦 pack
- run: dotnet pack -m:1
+ - name: 🐛 logs
+ uses: actions/upload-artifact@v3
+ if: runner.debug && always()
+ with:
+ name: logs
+ path: '*.binlog'
# Only push CI package to sleet feed if building on ubuntu (fastest)
- name: 🚀 sleet
@@ -83,7 +90,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 🤘 checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml
index b120b73..ca50e5a 100644
--- a/.github/workflows/changelog.yml
+++ b/.github/workflows/changelog.yml
@@ -17,7 +17,7 @@ jobs:
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: 🤘 checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
diff --git a/.github/workflows/dotnet-file.yml b/.github/workflows/dotnet-file.yml
index 818aa2c..95f6228 100644
--- a/.github/workflows/dotnet-file.yml
+++ b/.github/workflows/dotnet-file.yml
@@ -24,7 +24,7 @@ jobs:
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: 🤘 checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
@@ -32,6 +32,7 @@ jobs:
- name: ⌛ rate
shell: pwsh
+ if: github.event_name != 'workflow_dispatch'
run: |
# add random sleep since we run on fixed schedule
sleep (get-random -max 60)
@@ -70,7 +71,7 @@ jobs:
validate: false
- name: ✍ pull request
- uses: peter-evans/create-pull-request@v4
+ uses: peter-evans/create-pull-request@v6
with:
base: main
branch: dotnet-file-sync
diff --git a/.github/workflows/includes.yml b/.github/workflows/includes.yml
index bb1a90b..9cdae21 100644
--- a/.github/workflows/includes.yml
+++ b/.github/workflows/includes.yml
@@ -21,7 +21,7 @@ jobs:
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: 🤘 checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
with:
token: ${{ env.GH_TOKEN }}
@@ -29,8 +29,9 @@ jobs:
uses: devlooped/actions-includes@v1
- name: ✍ pull request
- uses: peter-evans/create-pull-request@v4
+ uses: peter-evans/create-pull-request@v6
with:
+ add-paths: '**/*.md'
base: main
branch: markdown-includes
delete-branch: true
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index bd83ada..a086072 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -10,25 +10,33 @@ on:
env:
DOTNET_NOLOGO: true
Configuration: Release
-
+ PackOnBuild: true
+ GeneratePackageOnBuild: true
+ VersionLabel: ${{ github.ref }}
+ GH_TOKEN: ${{ secrets.GH_TOKEN }}
+
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: 🤘 checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: 🙏 build
- run: dotnet build -m:1 -p:version=${GITHUB_REF#refs/*/v}
+ run: dotnet build -m:1 -bl:build.binlog
- name: 🧪 test
uses: ./.github/workflows/test
- - name: 📦 pack
- run: dotnet pack -m:1 -p:version=${GITHUB_REF#refs/*/v}
+ - name: 🐛 logs
+ uses: actions/upload-artifact@v3
+ if: runner.debug && always()
+ with:
+ name: logs
+ path: '*.binlog'
- name: 🚀 nuget
run: dotnet nuget push ./bin/**/*.nupkg -s https://api.nuget.org/v3/index.json -k ${{secrets.NUGET_API_KEY}} --skip-duplicate
diff --git a/.github/workflows/sponsor.yml b/.github/workflows/sponsor.yml
index 9e47191..1d484d3 100644
--- a/.github/workflows/sponsor.yml
+++ b/.github/workflows/sponsor.yml
@@ -15,7 +15,7 @@ jobs:
steps:
- name: 🤘 checkout
if: env.token != ''
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: 💜 sponsor
if: env.token != ''
diff --git a/.gitignore b/.gitignore
index 0c18de7..6639458 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,12 +1,15 @@
bin
-app
obj
artifacts
pack
TestResults
+results
+BenchmarkDotNet.Artifacts
+/app
.vs
.vscode
.idea
+local.settings.json
*.suo
*.sdf
@@ -31,5 +34,6 @@ node_modules
_site
.jekyll-metadata
.jekyll-cache
+.sass-cache
Gemfile.lock
package-lock.json
diff --git a/.netconfig b/.netconfig
index 9e30a9e..ebe97f5 100644
--- a/.netconfig
+++ b/.netconfig
@@ -22,18 +22,18 @@
weak
[file ".github/dependabot.yml"]
url = https://github.com/devlooped/oss/blob/main/.github/dependabot.yml
- sha = 4f070a477b4162a280f02722ae666376ae4fcc71
- etag = 35f2134fff3b0235ff8dac8618a76198c8ef533ad2f29628bbb435cd1134d638
+ sha = 49661dbf0720cde93eb5569be7523b5912351560
+ etag = c147ea2f3431ca0338c315c4a45b56ee233c4d30f8d6ab698d0e1980a257fd6a
weak
[file ".github/workflows/build.yml"]
url = https://github.com/devlooped/oss/blob/main/.github/workflows/build.yml
- sha = 13d67e2cf3f786c8189364fd29332aaa7dc575dc
- etag = c616df0877fba60002ccfc0397e9f731ddb22acbbb195a0598fedd4cac5f3135
+ sha = 14deaea5cecc64df51781d29891a2f67caf8be16
+ etag = d9fa5d91dc601f10d19099abb55c86df065cd1c23b1f6fab98ad883cb443bf5c
weak
[file ".gitignore"]
url = https://github.com/devlooped/oss/blob/main/.gitignore
- sha = b87a8a795a4c2b6830602225c066c11108552a99
- etag = 96e0860052044780f1fc9e3bdfbee09d82d5dddb8b1217d67460fc7330a64dd8
+ sha = 02811fa23b0a102b9b33048335d41e515bf75737
+ etag = a9c37ae312afac14b78436a7d018af4483d88736b5f780576f2c5a0b3f14998c
weak
[file "Directory.Build.rsp"]
url = https://github.com/devlooped/oss/blob/main/Directory.Build.rsp
@@ -52,23 +52,23 @@
weak
[file "src/Directory.Build.props"]
url = https://github.com/devlooped/oss/blob/main/src/Directory.Build.props
- sha = 6ae80a175a8f926ac5d9ffb0f6afd55d85cc9320
- etag = 69d4b16c14d5047b3ed812dbf556b0b8d77deb86f73af04b9bd3640220056fa8
+ sha = 14deaea5cecc64df51781d29891a2f67caf8be16
+ etag = f177eb767aaa6a347da43ff7ff419c9a0736c562cb171e17ded8007a1945a8b0
weak
[file "src/Directory.Build.targets"]
url = https://github.com/devlooped/oss/blob/main/src/Directory.Build.targets
- sha = 1514d15399a7d545ad92a0e9d57dc8295fdd6af8
- etag = 428f80b0786ff17b836c7a5b0640948724855d17933e958642b22849ac00dadb
+ sha = c618ea86d94402a12c7d7d10fe2b5cb8a21c3eea
+ etag = 7cb1421f00d9f6f4c00f0ca98e485dcadb927cfa6b3f0b5d4fb212525d2ce9c0
weak
[file ".github/workflows/changelog.yml"]
url = https://github.com/devlooped/oss/blob/main/.github/workflows/changelog.yml
- sha = a4b66eb5f4dfb9704502f19f59ba33cb4855188c
- etag = 54c0b571648b1055beb3ddac180b34e93a9869b9f0277de306901b2c1dbe0b2c
+ sha = 5fb172362c767bef7c36478f1a6bdc264723f8f9
+ etag = ad1efa56d6024ee1add2bcda81a7e4e38d0e9069473c6ff70374d5ce06af1f5a
weak
[file ".github/workflows/publish.yml"]
url = https://github.com/devlooped/oss/blob/main/.github/workflows/publish.yml
- sha = d3022567c9ef2bc9461511e53b8abe065afdf03b
- etag = 58601b5a71c805647ab26e84053acdfb8d174eaa93330487af8a5503753c5707
+ sha = 14deaea5cecc64df51781d29891a2f67caf8be16
+ etag = 4e9a9885a28ce867fd6139e1ae23735ad0073775145af96ff7d96d047d750973
weak
[file "assets/css/style.scss"]
url = https://github.com/devlooped/oss/blob/main/assets/css/style.scss
@@ -85,8 +85,8 @@
weak
[file ".github/workflows/dotnet-file.yml"]
url = https://github.com/devlooped/oss/blob/main/.github/workflows/dotnet-file.yml
- sha = f08c3f28e46e28eb31e70846d65e57aa9553ce56
- etag = 567444486383d032c1c5fbc538f07e860f92b1d08c66ac6ffb1db64ca539251c
+ sha = 7afe350f7e80a230e922db026d4e1198ba15cae1
+ etag = 65e9794df6caff779eb989c8f71ddf4d4109b24a75af79e4f8d0fe6ba7bd9702
weak
[file ".github/workflows/test/action.yml"]
url = https://github.com/devlooped/oss/blob/main/.github/workflows/test/action.yml
@@ -98,13 +98,13 @@
skip
[file ".github/workflows/includes.yml"]
url = https://github.com/devlooped/oss/blob/main/.github/workflows/includes.yml
- sha = ac753b791d03997eb655efb26ae141b51addd1c0
- etag = fcd94a08ac9ebc0e8351deac4e7f085cf8ef67816cc50006e068f44166096eb8
+ sha = 5fb172362c767bef7c36478f1a6bdc264723f8f9
+ etag = e5ee22e115c925fb85ec931cda3ac811fcc453c03904554fa3f573935b221d34
weak
[file ".github/workflows/sponsor.yml"]
url = https://github.com/devlooped/oss/blob/main/.github/workflows/sponsor.yml
- sha = 8990ebb36199046e0b8098bad9e46dcef739c56e
- etag = e1dc114d2e8b57d50649989d32dbf0c9080ec77da3738a4cc79e9256d6ca5d3e
+ sha = 5fb172362c767bef7c36478f1a6bdc264723f8f9
+ etag = 0849ee61af6daee29615f9632173b4e82da5bfa9d78ff28907e9408bd5acde4d
weak
[file ".github/release.yml"]
url = https://github.com/devlooped/oss/blob/main/.github/release.yml
@@ -121,3 +121,198 @@
sha = c1610886eba42cb250e3894aed40c0a258cd383d
etag = 598ee294649a44d4c5d5934416c01183597d08aa7db7938453fd2bbf52a4e64d
weak
+[file "SponsorLink.sln"]
+ url = https://github.com/devlooped/oss/blob/main/SponsorLink.sln
+ sha = e732f6a2c44a2f7940a1868a92cd66523f74ed24
+ etag = 5f4cdd4e73a4ac03a41b6f11ec5c576f7a0e7ecef95fcdae04006abc39ea729b
+ weak
+[file "src/SponsorLink/Analyzer/Analyzer.csproj"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Analyzer/Analyzer.csproj
+ sha = 93df7c7ec34f83ae58efbf213624d5ea31fe3c41
+ etag = f76e33fde812244a275b95c8815101f6f87d144a5305a2c1f0f631f770d91920
+ weak
+[file "src/SponsorLink/Analyzer/Properties/launchSettings.json"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Analyzer/Properties/launchSettings.json
+ sha = a0ae7272f31c766ebb129ea38c11c01df93b6b5d
+ etag = 6c59ab4d008e3221e316c9e3b6e0da155b892680d48cdc400a39d53cb9a12aac
+ weak
+[file "src/SponsorLink/Analyzer/StatusReportingAnalyzer.cs"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Analyzer/StatusReportingAnalyzer.cs
+ sha = c879f25bf483086725c8a29f104555644e6ee542
+ etag = cde10b763b87a3987e86cca2292c9afc7637d2113b9921e79492b6a31620bbb4
+ weak
+[file "src/SponsorLink/Analyzer/buildTransitive/SponsorableLib.targets"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Analyzer/buildTransitive/SponsorableLib.targets
+ sha = a0ae7272f31c766ebb129ea38c11c01df93b6b5d
+ etag = 332060de0945590d7c41cd237c250b8186acd6fc2045cc85a890368c74fdf473
+ weak
+[file "src/SponsorLink/Directory.Build.props"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Directory.Build.props
+ sha = 3b943f5aa59f33141d1c0fffcb215446d594ad53
+ etag = 0c7737411744012078642dbfc174af3f2ac7dc9f7b8ea4423981ae38753a5be4
+ weak
+[file "src/SponsorLink/Directory.Build.targets"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Directory.Build.targets
+ sha = a0ae7272f31c766ebb129ea38c11c01df93b6b5d
+ etag = 9938f29c3573bf8bdb9686e1d9884dee177256b1d5dd7ee41472dd64bfbdd92d
+ weak
+[file "src/SponsorLink/Library/Library.csproj"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Library/Library.csproj
+ sha = 93df7c7ec34f83ae58efbf213624d5ea31fe3c41
+ etag = 56233a536fb38edd75f66f6a9a9e6044eb227a0b58fb791495ff88e43649feb7
+ weak
+[file "src/SponsorLink/Library/MyClass.cs"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Library/MyClass.cs
+ sha = a0ae7272f31c766ebb129ea38c11c01df93b6b5d
+ etag = b5b3ccd6cd14bb90dd9702b9d7e52cc22c11e601c039617738d688f9fd45d49b
+ weak
+[file "src/SponsorLink/Library/Resources.resx"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Library/Resources.resx
+ sha = a0ae7272f31c766ebb129ea38c11c01df93b6b5d
+ etag = aff6051733d22982e761f2b414173aafeab40e0a76a142e2b33025dced213eb2
+ weak
+[file "src/SponsorLink/Library/readme.md"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Library/readme.md
+ sha = 55124bc610b2dcad9efb343bdffc79c959170593
+ etag = 5002ac8c5bbeee60c13937a32c1b6c1a5dbf0065617c8f2550e6eca6fded256d
+ weak
+[file "src/SponsorLink/SponsorLink.Tests.targets"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink.Tests.targets
+ sha = 81ba912310dd4b723c7a0103a76cb71b183983b1
+ etag = cf6deba5b5cdadb5b2ea6b8533331da49afd3c841db2932a45618627ffc4ff9a
+ weak
+[file "src/SponsorLink/SponsorLink.targets"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink.targets
+ sha = 759365751e6529049a3df5701f85aecb51189289
+ etag = 6e3955b7233c5c2000b9adf1bb281e74e7fb08813e17b3ef10fd8b5d50f9fb4d
+ weak
+[file "src/SponsorLink/SponsorLink/AppDomainDictionary.cs"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/AppDomainDictionary.cs
+ sha = a0ae7272f31c766ebb129ea38c11c01df93b6b5d
+ etag = 4a70f86e73f951bca95618c221d821e38a31ef9092af4ac61447eab845671a28
+ weak
+[file "src/SponsorLink/SponsorLink/DiagnosticsManager.cs"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/DiagnosticsManager.cs
+ sha = cf154d5d9c2ac3dad56e95da04effdad64409471
+ etag = 7ac9738f71cafd15dbb347bc9d83468b0691d0b0888cc82e35c161fd1f2d48eb
+ weak
+[file "src/SponsorLink/SponsorLink/ManifestStatus.cs"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/ManifestStatus.cs
+ sha = b2a11faac6c1c300bce8c1d45f95b585c19f2953
+ etag = e46848f83c0436ba33a1c09a4060ad627a74db41bab66bb37ca40fce8a6532a7
+ weak
+[file "src/SponsorLink/SponsorLink/Resources.es.resx"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/Resources.es.resx
+ sha = c879f25bf483086725c8a29f104555644e6ee542
+ etag = c0a05bb5efedf8e30a73ab96678579ad33832e4a4aec75d3b596b47f248c23f5
+ weak
+[file "src/SponsorLink/SponsorLink/Resources.resx"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/Resources.resx
+ sha = c879f25bf483086725c8a29f104555644e6ee542
+ etag = fcb46a86511cb7996e8dcd1f4e283cea9cd51610b094ac49a7396301730814b0
+ weak
+[file "src/SponsorLink/SponsorLink/SponsorLink.cs"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/SponsorLink.cs
+ sha = 55124bc610b2dcad9efb343bdffc79c959170593
+ etag = 28178198489bf9b72f8a400563950194a06f7ce55ff4a016535eb1be35fa70b8
+ weak
+[file "src/SponsorLink/SponsorLink/SponsorLink.csproj"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/SponsorLink.csproj
+ sha = c879f25bf483086725c8a29f104555644e6ee542
+ etag = 997b08082f85a491be7a68805d7811e65e1474a6e7d49cbe927617f7035d21e1
+ weak
+[file "src/SponsorLink/SponsorLink/SponsorLinkAnalyzer.cs"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/SponsorLinkAnalyzer.cs
+ sha = 23f83bd6b1f0fe13ac02bf464377f576652fec97
+ etag = 5f9823d1bf83f7d28e5809e0a08d942fb2c444f4653ca5b035d500ebef2ead15
+ weak
+[file "src/SponsorLink/SponsorLink/SponsorStatus.cs"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/SponsorStatus.cs
+ sha = 4fca946c3201d90d30e2183f699c850dcc1bf8d5
+ etag = 9a5f6f35c38c34b77796925d80addc998e204bc112fcd5fc124030060390e7c2
+ weak
+[file "src/SponsorLink/SponsorLink/SponsorableLib.targets"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/SponsorableLib.targets
+ sha = a0ae7272f31c766ebb129ea38c11c01df93b6b5d
+ etag = 2f923a97081481a6a264d63c8ff70ce5ba65c3dbaf7ea078cbe1388fb0868e1c
+ weak
+[file "src/SponsorLink/SponsorLink/Tracing.cs"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/Tracing.cs
+ sha = a0ae7272f31c766ebb129ea38c11c01df93b6b5d
+ etag = 22e32872cafd080bcd5ac9084355578ef70910c8e494602ead365139dcbf40c0
+ weak
+[file "src/SponsorLink/SponsorLink/buildTransitive/Devlooped.Sponsors.targets"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/buildTransitive/Devlooped.Sponsors.targets
+ sha = 55124bc610b2dcad9efb343bdffc79c959170593
+ etag = 46842d44ece3d55285bc30a6b22ac21c1c35d3b0c451aa5285d4ca4564b8698c
+ weak
+[file "src/SponsorLink/SponsorLink/sponsorable.md"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/sponsorable.md
+ sha = a0ae7272f31c766ebb129ea38c11c01df93b6b5d
+ etag = 9c275d50705a2e661f0f86f1ae5e555c0033a05e86e12f936283a5b5ef47ae77
+ weak
+[file "src/SponsorLink/SponsorLinkAnalyzer.sln"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLinkAnalyzer.sln
+ sha = a0ae7272f31c766ebb129ea38c11c01df93b6b5d
+ etag = fc2928c9b303d81ff23891ee791a859b794d9f2d4b9f4e81b9ed15e5b74db487
+ weak
+[file "src/SponsorLink/Tests/.netconfig"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Tests/.netconfig
+ sha = a0ae7272f31c766ebb129ea38c11c01df93b6b5d
+ etag = 089a26cdb722d57014c8b8104cc6f4e770868efdc49ae3119eebc873f00a316e
+ weak
+[file "src/SponsorLink/Tests/Attributes.cs"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Tests/Attributes.cs
+ sha = a0ae7272f31c766ebb129ea38c11c01df93b6b5d
+ etag = 1d7c17a2c9424db73746112c338a39e0000134ac878b398e2aa88f7ea5c0c488
+ weak
+[file "src/SponsorLink/Tests/Extensions.cs"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Tests/Extensions.cs
+ sha = a0ae7272f31c766ebb129ea38c11c01df93b6b5d
+ etag = f68e11894103f8748ce290c29927bf1e4f749e743ae33d5350e72ed22c15d245
+ weak
+[file "src/SponsorLink/Tests/JsonOptions.cs"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Tests/JsonOptions.cs
+ sha = a0ae7272f31c766ebb129ea38c11c01df93b6b5d
+ etag = 6e9a1b12757a97491441b9534ced4e5dac6d9d6334008fa0cd20575650bbd935
+ weak
+[file "src/SponsorLink/Tests/Resources.Designer.cs"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Tests/Resources.Designer.cs
+ sha = c879f25bf483086725c8a29f104555644e6ee542
+ etag = 69404ac09238930893fdbc225ae7839b14957e129b4c05f1ef0e7afcc4c91d63
+ weak
+[file "src/SponsorLink/Tests/Resources.resx"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Tests/Resources.resx
+ sha = c879f25bf483086725c8a29f104555644e6ee542
+ etag = 13d1bb8b0de32a8c9b5dbdc806a036ed89d423cd7c0be187b8c56055c9bf7783
+ weak
+[file "src/SponsorLink/Tests/Sample.cs"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Tests/Sample.cs
+ sha = c879f25bf483086725c8a29f104555644e6ee542
+ etag = c4ed1e041d1ec816710757790aaa6688e3756870cfd98ba7e6c7b5103ce3a9ba
+ weak
+[file "src/SponsorLink/Tests/SponsorLinkTests.cs"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Tests/SponsorLinkTests.cs
+ sha = d74f5111504a0fae6e5a1e68ca92bf7afddb3254
+ etag = 1fa41250bd984e8aa840a966d34ce0e94f2111d1422d7f50b864c38364fcf4a4
+ weak
+[file "src/SponsorLink/Tests/SponsorableManifest.cs"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Tests/SponsorableManifest.cs
+ sha = a0ae7272f31c766ebb129ea38c11c01df93b6b5d
+ etag = e0c95e7fc6c0499dbc8c5cd28aa9a6a5a49c9d0ad41fe028a5a085aca7e00eaf
+ weak
+[file "src/SponsorLink/Tests/Tests.csproj"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Tests/Tests.csproj
+ sha = c879f25bf483086725c8a29f104555644e6ee542
+ etag = 31d33feb5860cd6df71ee2d6f3ca6d8fdc9e6535bea8caa97300421c0502246e
+ weak
+[file "src/SponsorLink/jwk.ps1"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/jwk.ps1
+ sha = c4830fc3b1aa78ec98d1d2ea4fed86ef0b7b803c
+ etag = f399e05ecb56adaf41d2545171f299a319142b17dd09fc38e452ca8c5d13bd0d
+ weak
+[file "src/SponsorLink/readme.md"]
+ url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/readme.md
+ sha = 827a1d18bf0245978d81bcd3d52e9e6f1584d1ef
+ etag = 079b4aedba2aa9851e609b569f25c55db8d5922e3dbb1adc22611ce4d6cfe465
+ weak
diff --git a/SponsorLink.sln b/SponsorLink.sln
new file mode 100644
index 0000000..d4eab56
--- /dev/null
+++ b/SponsorLink.sln
@@ -0,0 +1,43 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.11.34909.67
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SponsorLink", "src\SponsorLink\SponsorLink\SponsorLink.csproj", "{1E1D01A2-D202-4FAB-B21B-AF21B1C37163}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Analyzer", "src\SponsorLink\Analyzer\Analyzer.csproj", "{87B3A42C-FFA7-49CF-8F3A-656A6D213246}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Library", "src\SponsorLink\Library\Library.csproj", "{23371E8B-2401-42A1-9A01-4720D8388105}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "src\SponsorLink\Tests\Tests.csproj", "{A86B253A-340E-4B82-8207-336BF65F36C8}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {1E1D01A2-D202-4FAB-B21B-AF21B1C37163}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1E1D01A2-D202-4FAB-B21B-AF21B1C37163}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1E1D01A2-D202-4FAB-B21B-AF21B1C37163}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1E1D01A2-D202-4FAB-B21B-AF21B1C37163}.Release|Any CPU.Build.0 = Release|Any CPU
+ {87B3A42C-FFA7-49CF-8F3A-656A6D213246}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {87B3A42C-FFA7-49CF-8F3A-656A6D213246}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {87B3A42C-FFA7-49CF-8F3A-656A6D213246}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {87B3A42C-FFA7-49CF-8F3A-656A6D213246}.Release|Any CPU.Build.0 = Release|Any CPU
+ {23371E8B-2401-42A1-9A01-4720D8388105}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {23371E8B-2401-42A1-9A01-4720D8388105}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {23371E8B-2401-42A1-9A01-4720D8388105}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {23371E8B-2401-42A1-9A01-4720D8388105}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A86B253A-340E-4B82-8207-336BF65F36C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A86B253A-340E-4B82-8207-336BF65F36C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A86B253A-340E-4B82-8207-336BF65F36C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A86B253A-340E-4B82-8207-336BF65F36C8}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {36BC3C24-D4E3-4EB0-A910-4BE4BD8FE01F}
+ EndGlobalSection
+EndGlobal
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 6b9a668..1648dcd 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -26,10 +26,10 @@
icon.png
- readme.md
+ readme.md
icon.png
- readme.md
+ readme.md
true
true
@@ -37,7 +37,8 @@
$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\bin'))
- true
+ true
+ true
true
@@ -117,6 +118,8 @@
<_VersionLabel>$(VersionLabel.Replace('refs/heads/', ''))
+ <_VersionLabel>$(_VersionLabel.Replace('refs/tags/v', ''))
+
<_VersionLabel Condition="$(_VersionLabel.Contains('refs/pull/'))">$(VersionLabel.TrimEnd('.0123456789'))
@@ -127,7 +130,9 @@
<_VersionLabel>$(_VersionLabel.Replace('/', '-'))
- $(_VersionLabel)
+ $(_VersionLabel)
+
+ $(_VersionLabel)
@@ -141,6 +146,16 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets
index 5bd4019..0cb1e4e 100644
--- a/src/Directory.Build.targets
+++ b/src/Directory.Build.targets
@@ -35,16 +35,16 @@
+ Condition="'$(PackReadme)' != 'false' and '$(PackageReadmeFile)' != ''" />
+ Condition="Exists('$(MSBuildThisFileDirectory)icon.png') and !Exists('$(MSBuildProjectDirectory)\icon.png')" />
+ Condition="'$(PackReadme)' != 'false' and Exists('$(MSBuildThisFileDirectory)readme.md') and !Exists('$(MSBuildProjectDirectory)\readme.md')" />
+ false
+ $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)bin'))
+
+ https://pkg.kzu.app/index.json;https://api.nuget.org/v3/index.json
+ $(PackageOutputPath);$(RestoreSources)
+
+
+ $([System.DateTime]::Parse("2024-03-15"))
+ $([System.DateTime]::UtcNow.Subtract($(Epoc)).TotalDays)
+ $([System.Math]::Truncate($(TotalDays)))
+ $([System.Math]::Floor($([MSBuild]::Divide($([System.DateTime]::UtcNow.TimeOfDay.TotalSeconds), 10))))
+ 42.$(Days).$(Seconds)
+
+ SponsorableLib
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/SponsorLink/Directory.Build.targets b/src/SponsorLink/Directory.Build.targets
new file mode 100644
index 0000000..4ce4c80
--- /dev/null
+++ b/src/SponsorLink/Directory.Build.targets
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SponsorLink/Library/Library.csproj b/src/SponsorLink/Library/Library.csproj
new file mode 100644
index 0000000..f363648
--- /dev/null
+++ b/src/SponsorLink/Library/Library.csproj
@@ -0,0 +1,21 @@
+
+
+
+ SponsorableLib
+ netstandard2.0
+ true
+ SponsorableLib
+ Sample library incorporating SponsorLink checks
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/SponsorLink/Library/MyClass.cs b/src/SponsorLink/Library/MyClass.cs
new file mode 100644
index 0000000..7b7f6f5
--- /dev/null
+++ b/src/SponsorLink/Library/MyClass.cs
@@ -0,0 +1,5 @@
+namespace SponsorableLib;
+
+public class MyClass
+{
+}
diff --git a/src/SponsorLink/Library/Resources.resx b/src/SponsorLink/Library/Resources.resx
new file mode 100644
index 0000000..636fedc
--- /dev/null
+++ b/src/SponsorLink/Library/Resources.resx
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Bar
+
+
\ No newline at end of file
diff --git a/src/SponsorLink/Library/readme.md b/src/SponsorLink/Library/readme.md
new file mode 100644
index 0000000..ba4ce37
--- /dev/null
+++ b/src/SponsorLink/Library/readme.md
@@ -0,0 +1,5 @@
+# Sponsorable Library
+
+Example of a library that is available for sponsorship and leverages
+[SponsorLink](https://github.com/devlooped/SponsorLink) to remind users
+in an IDE (VS/Rider).
diff --git a/src/SponsorLink/SponsorLink.Tests.targets b/src/SponsorLink/SponsorLink.Tests.targets
new file mode 100644
index 0000000..ffc7586
--- /dev/null
+++ b/src/SponsorLink/SponsorLink.Tests.targets
@@ -0,0 +1,38 @@
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SponsorLink/SponsorLink.targets b/src/SponsorLink/SponsorLink.targets
new file mode 100644
index 0000000..6de86fb
--- /dev/null
+++ b/src/SponsorLink/SponsorLink.targets
@@ -0,0 +1,186 @@
+
+
+
+
+
+
+ true
+
+ true
+
+ true
+
+ CoreResGen;$(CoreCompileDependsOn)
+
+
+ $(Product)
+
+ $([System.Text.RegularExpressions.Regex]::Replace("$(FundingProduct)", "[^A-Z]", ""))
+
+ 21
+
+
+
+
+
+
+
+
+
+
+
+ SponsorLink\%(RecursiveDir)%(Filename)%(Extension)
+
+
+ SponsorLink\%(RecursiveDir)%(Filename)%(Extension)
+
+
+ SponsorLink\%(RecursiveDir)%(Filename)%(Extension)
+
+
+ SponsorLink\%(PackagePath)
+
+
+
+
+
+ false
+
+
+ false
+
+
+ false
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ namespace Devlooped.Sponsors%3B
+
+partial class SponsorLink
+{
+ public partial class Funding
+ {
+ public const string Product = "$(FundingProduct)"%3B
+ public const string Prefix = "$(FundingPrefix)"%3B
+ public const int Grace = $(FundingGrace)%3B
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $([System.IO.Path]::GetFullPath($([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','$(AssemblyOriginatorKeyFile)'))))
+ /keyfile:"$(AbsoluteAssemblyOriginatorKeyFile)" /delaysign
+ $(ILRepackArgs) /internalize
+ $(ILRepackArgs) /union
+
+ $(ILRepackArgs) @(LibDir -> '/lib:"%(Identity)."', ' ')
+ $(ILRepackArgs) /out:"@(IntermediateAssembly -> '%(FullPath)')"
+ $(ILRepackArgs) "@(IntermediateAssembly -> '%(FullPath)')"
+ $(ILRepackArgs) @(MergedAssemblies -> '"%(FullPath)"', ' ')
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $([System.IO.File]::ReadAllText('$(MSBuildProjectDirectory)\$(BaseIntermediateOutputPath)devlooped.jwk'))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SponsorLink/SponsorLink/AppDomainDictionary.cs b/src/SponsorLink/SponsorLink/AppDomainDictionary.cs
new file mode 100644
index 0000000..05cc949
--- /dev/null
+++ b/src/SponsorLink/SponsorLink/AppDomainDictionary.cs
@@ -0,0 +1,36 @@
+//
+#nullable enable
+using System;
+
+namespace Devlooped.Sponsors;
+
+///
+/// A helper class to store and retrieve values from the current
+/// as typed named values.
+///
+///
+/// This allows tools that run within the same app domain to share state, such as
+/// MSBuild tasks or Roslyn analyzers.
+///
+static class AppDomainDictionary
+{
+ ///
+ /// Gets the value associated with the specified name, or creates a new one if it doesn't exist.
+ ///
+ public static TValue Get(string name) where TValue : notnull, new()
+ {
+ var data = AppDomain.CurrentDomain.GetData(name);
+ if (data is TValue firstTry)
+ return firstTry;
+
+ lock (AppDomain.CurrentDomain)
+ {
+ if (AppDomain.CurrentDomain.GetData(name) is TValue secondTry)
+ return secondTry;
+
+ var newValue = new TValue();
+ AppDomain.CurrentDomain.SetData(name, newValue);
+ return newValue;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/SponsorLink/SponsorLink/DiagnosticsManager.cs b/src/SponsorLink/SponsorLink/DiagnosticsManager.cs
new file mode 100644
index 0000000..c22ecc8
--- /dev/null
+++ b/src/SponsorLink/SponsorLink/DiagnosticsManager.cs
@@ -0,0 +1,137 @@
+//
+#nullable enable
+using System;
+using System.Collections.Concurrent;
+using System.Globalization;
+using Humanizer;
+using Microsoft.CodeAnalysis;
+
+namespace Devlooped.Sponsors;
+
+///
+/// Manages diagnostics for the SponsorLink analyzer so that there are no duplicates
+/// when multiple projects share the same product name (i.e. ThisAssembly).
+///
+class DiagnosticsManager
+{
+ ///
+ /// Acceses the diagnostics dictionary for the current .
+ ///
+ ConcurrentDictionary Diagnostics
+ => AppDomainDictionary.Get>(nameof(Diagnostics));
+
+ ///
+ /// Creates a descriptor from well-known diagnostic kinds.
+ ///
+ /// The names of the sponsorable accounts that can be funded for the given product.
+ /// The product or project developed by the sponsorable(s).
+ /// Custom prefix to use for diagnostic IDs.
+ /// The kind of status diagnostic to create.
+ /// The given .
+ /// The is not one of the known ones.
+ public DiagnosticDescriptor GetDescriptor(string[] sponsorable, string product, string prefix, SponsorStatus status) => status switch
+ {
+ SponsorStatus.Unknown => CreateUnknown(sponsorable, product, prefix),
+ SponsorStatus.Sponsor => CreateSponsor(sponsorable, prefix),
+ SponsorStatus.Expiring => CreateExpiring(sponsorable, prefix),
+ SponsorStatus.Expired => CreateExpired(sponsorable, prefix),
+ _ => throw new NotImplementedException(),
+ };
+
+ ///
+ /// Pushes a diagnostic for the given product. If an existing one exists, it is replaced.
+ ///
+ /// The same diagnostic that was pushed, for chained invocations.
+ public Diagnostic Push(string product, Diagnostic diagnostic)
+ {
+ // Directly sets, since we only expect to get one warning per sponsorable+product
+ // combination.
+ Diagnostics[product] = diagnostic;
+ return diagnostic;
+ }
+
+ ///
+ /// Attemps to remove a diagnostic for the given product.
+ ///
+ /// The product diagnostic that might have been pushed previously.
+ /// The removed diagnostic, or if none was previously pushed.
+ public Diagnostic? Pop(string product)
+ {
+ Diagnostics.TryRemove(product, out var diagnostic);
+ return diagnostic;
+ }
+
+ ///
+ /// Gets the status of the given product based on a previously stored diagnostic.
+ ///
+ /// The product to check status for.
+ /// Optional that was reported, if any.
+ public SponsorStatus? GetStatus(string product)
+ {
+ // NOTE: the SponsorLinkAnalyzer.SetStatus uses diagnostic properties to store the
+ // kind of diagnostic as a simple string instead of the enum. We do this so that
+ // multiple analyzers or versions even across multiple products, which all would
+ // have their own enum, can still share the same diagnostic kind.
+ if (Diagnostics.TryGetValue(product, out var diagnostic) &&
+ diagnostic.Properties.TryGetValue(nameof(SponsorStatus), out var value))
+ {
+ // Switch on value matching DiagnosticKind names
+ return value switch
+ {
+ nameof(SponsorStatus.Unknown) => SponsorStatus.Unknown,
+ nameof(SponsorStatus.Sponsor) => SponsorStatus.Sponsor,
+ nameof(SponsorStatus.Expiring) => SponsorStatus.Expiring,
+ nameof(SponsorStatus.Expired) => SponsorStatus.Expired,
+ _ => null,
+ };
+ }
+
+ return null;
+ }
+
+ static DiagnosticDescriptor CreateSponsor(string[] sponsorable, string prefix) => new(
+ $"{prefix}100",
+ Resources.Sponsor_Title,
+ Resources.Sponsor_Message,
+ "SponsorLink",
+ DiagnosticSeverity.Info,
+ isEnabledByDefault: true,
+ description: Resources.Sponsor_Description,
+ helpLinkUri: "https://github.com/devlooped#sponsorlink",
+ "DoesNotSupportF1Help");
+
+ static DiagnosticDescriptor CreateUnknown(string[] sponsorable, string product, string prefix) => new(
+ $"{prefix}101",
+ Resources.Unknown_Title,
+ Resources.Unknown_Message,
+ "SponsorLink",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: string.Format(CultureInfo.CurrentCulture, Resources.Unknown_Description,
+ sponsorable.Humanize(x => $"https://github.com/sponsors/{x}"),
+ string.Join(" ", sponsorable)),
+ helpLinkUri: "https://github.com/devlooped#sponsorlink",
+ WellKnownDiagnosticTags.NotConfigurable);
+
+ static DiagnosticDescriptor CreateExpiring(string[] sponsorable, string prefix) => new(
+ $"{prefix}103",
+ Resources.Expiring_Title,
+ Resources.Expiring_Message,
+ "SponsorLink",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: string.Format(CultureInfo.CurrentCulture, Resources.Expiring_Description, string.Join(" ", sponsorable)),
+ helpLinkUri: "https://github.com/devlooped#autosync",
+ "DoesNotSupportF1Help", WellKnownDiagnosticTags.NotConfigurable);
+
+ static DiagnosticDescriptor CreateExpired(string[] sponsorable, string prefix) => new(
+ $"{prefix}104",
+ Resources.Expired_Title,
+ Resources.Expired_Message,
+ "SponsorLink",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: string.Format(CultureInfo.CurrentCulture, Resources.Expired_Description, string.Join(" ", sponsorable)),
+ helpLinkUri: "https://github.com/devlooped#autosync",
+ "DoesNotSupportF1Help", WellKnownDiagnosticTags.NotConfigurable);
+}
diff --git a/src/SponsorLink/SponsorLink/ManifestStatus.cs b/src/SponsorLink/SponsorLink/ManifestStatus.cs
new file mode 100644
index 0000000..0960e5a
--- /dev/null
+++ b/src/SponsorLink/SponsorLink/ManifestStatus.cs
@@ -0,0 +1,25 @@
+//
+namespace Devlooped.Sponsors;
+
+///
+/// The resulting status from validation.
+///
+public enum ManifestStatus
+{
+ ///
+ /// The manifest couldn't be read at all.
+ ///
+ Unknown,
+ ///
+ /// The manifest was read and is valid (not expired and properly signed).
+ ///
+ Valid,
+ ///
+ /// The manifest was read but has expired.
+ ///
+ Expired,
+ ///
+ /// The manifest was read, but its signature is invalid.
+ ///
+ Invalid,
+}
diff --git a/src/SponsorLink/SponsorLink/Resources.es.resx b/src/SponsorLink/SponsorLink/Resources.es.resx
new file mode 100644
index 0000000..ec1b5c1
--- /dev/null
+++ b/src/SponsorLink/SponsorLink/Resources.es.resx
@@ -0,0 +1,163 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Patrocinar los proyectos en que dependes asegura que se mantengan activos, y que recibas el apoyo que necesitas. También es muy económico y está disponible en todo el mundo!
+Por favor considera apoyar el proyecto patrocinando en {0} y ejecutando posteriormente 'sponsor sync {1}'.
+
+
+ Por favor considere apoyar {0} patrocinando @{1} 🙏
+
+
+ Estado de patrocinio desconocido
+
+
+ Funcionalidades exclusivas para patrocinadores pueden no estar disponibles. Ejecuta 'sponsor sync {0}' y, opcionalmente, habilita la sincronización automática.
+
+
+ El estado de patrocino ha expirado y la sincronización automática no está habilitada.
+
+
+ El estado de patrocino ha expirado
+
+
+ Eres un verdadero héroe. Tu patrocinio ayuda a mantener el proyecto vivo y próspero 🙏.
+
+
+ Gracias por apoyar a {0} con tu patrocinio 💟!
+
+
+ Eres un patrocinador del proyecto, eres lo máximo 💟!
+
+
+ El estado de patrocino ha expirado y estás en un período de gracia. Ejecuta 'sponsor sync {0}' y, opcionalmente, habilita la sincronización automática.
+
+
+ El estado de patrocino necesita actualización periódica y la sincronización automática no está habilitada.
+
+
+ El estado de patrocino ha expirado y el período de gracia terminará pronto
+
+
+ y
+
+
+ o
+
+
\ No newline at end of file
diff --git a/src/SponsorLink/SponsorLink/Resources.resx b/src/SponsorLink/SponsorLink/Resources.resx
new file mode 100644
index 0000000..e12a0e5
--- /dev/null
+++ b/src/SponsorLink/SponsorLink/Resources.resx
@@ -0,0 +1,164 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Sponsoring projects you depend on ensures they remain active, and that you get the support you need. It's also super affordable and available worldwide!
+Please consider supporting the project by sponsoring at {0} and running 'sponsor sync {1}' afterwards.
+ Unknown sponsor description
+
+
+ Please consider supporting {0} by sponsoring @{1} 🙏
+
+
+ Unknown sponsor status
+
+
+ Sponsor-only features may be disabled. Please run 'sponsor sync {0}' and optionally enable automatic sync.
+
+
+ Sponsor status has expired and automatic sync has not been enabled.
+
+
+ Sponsor status expired
+
+
+ You are a true hero. Your sponsorship helps keep the project alive and thriving 🙏.
+
+
+ Thank you for supporting {0} with your sponsorship 💟!
+
+
+ You are a sponsor of the project, you rock 💟!
+
+
+ Sponsor status has expired and you are in the grace period. Please run 'sponsor sync {0}' and optionally enable automatic sync.
+
+
+ Sponsor status needs periodic updating and automatic sync has not been enabled.
+
+
+ Sponsor status expired, grace period ending soon
+
+
+ and
+
+
+ or
+
+
\ No newline at end of file
diff --git a/src/SponsorLink/SponsorLink/SponsorLink.cs b/src/SponsorLink/SponsorLink/SponsorLink.cs
new file mode 100644
index 0000000..f3d8328
--- /dev/null
+++ b/src/SponsorLink/SponsorLink/SponsorLink.cs
@@ -0,0 +1,169 @@
+//
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IdentityModel.Tokens.Jwt;
+using System.Linq;
+using System.Reflection;
+using System.Security.Claims;
+using Microsoft.IdentityModel.Tokens;
+
+namespace Devlooped.Sponsors;
+
+static partial class SponsorLink
+{
+ public static Dictionary Sponsorables { get; } = typeof(SponsorLink).Assembly
+ .GetCustomAttributes()
+ .Where(x => x.Key.StartsWith("Funding.GitHub."))
+ .Select(x => new { Key = x.Key[15..], x.Value })
+ .ToDictionary(x => x.Key, x => x.Value);
+
+ ///
+ /// Whether the current process is running in an IDE, either
+ /// or .
+ ///
+ public static bool IsEditor => IsVisualStudio || IsRider;
+
+ ///
+ /// Whether the current process is running as part of an active Visual Studio instance.
+ ///
+ public static bool IsVisualStudio =>
+ Environment.GetEnvironmentVariable("ServiceHubLogSessionKey") != null ||
+ Environment.GetEnvironmentVariable("VSAPPIDNAME") != null;
+
+ ///
+ /// Whether the current process is running as part of an active Rider instance.
+ ///
+ public static bool IsRider =>
+ Environment.GetEnvironmentVariable("RESHARPER_FUS_SESSION") != null ||
+ Environment.GetEnvironmentVariable("IDEA_INITIAL_DIRECTORY") != null;
+
+ ///
+ /// Manages the sharing and reporting of diagnostics across the source generator
+ /// and the diagnostic analyzer, to avoid doing the online check more than once.
+ ///
+ public static DiagnosticsManager Diagnostics { get; } = new();
+
+ ///
+ /// Gets the expiration date from the principal, if any.
+ ///
+ ///
+ /// Whichever "exp" claim is the latest, or if none found.
+ ///
+ public static DateTime? GetExpiration(this ClaimsPrincipal principal)
+ // get all "exp" claims, parse them and return the latest one or null if none found
+ => principal.FindAll("exp")
+ .Select(c => c.Value)
+ .Select(long.Parse)
+ .Select(DateTimeOffset.FromUnixTimeSeconds)
+ .Max().DateTime is var exp && exp == DateTime.MinValue ? null : exp;
+
+ ///
+ /// Reads all manifests, validating their signatures.
+ ///
+ /// The combined principal with all identities (and their claims) from each provided and valid JWT
+ /// The tokens to read and their corresponding JWK for signature verification.
+ /// if at least one manifest can be successfully read and is valid.
+ /// otherwise.
+ public static bool TryRead([NotNullWhen(true)] out ClaimsPrincipal? principal, params (string jwt, string jwk)[] values)
+ => TryRead(out principal, values.AsEnumerable());
+
+ ///
+ /// Reads all manifests, validating their signatures.
+ ///
+ /// The combined principal with all identities (and their claims) from each provided and valid JWT
+ /// The tokens to read and their corresponding JWK for signature verification.
+ /// if at least one manifest can be successfully read and is valid.
+ /// otherwise.
+ public static bool TryRead([NotNullWhen(true)] out ClaimsPrincipal? principal, IEnumerable<(string jwt, string jwk)> values)
+ {
+ principal = null;
+
+ foreach (var value in values)
+ {
+ if (string.IsNullOrWhiteSpace(value.jwt) || string.IsNullOrEmpty(value.jwk))
+ continue;
+
+ if (Validate(value.jwt, value.jwk, out var token, out var claims, false) == ManifestStatus.Valid && claims != null)
+ {
+ if (principal == null)
+ principal = claims;
+ else
+ principal.AddIdentities(claims.Identities);
+ }
+ }
+
+ return principal != null;
+ }
+
+ ///
+ /// Validates the manifest signature and optional expiration.
+ ///
+ /// The JWT to validate.
+ /// The key to validate the manifest signature with.
+ /// Except when returning , returns the security token read from the JWT, even if signature check failed.
+ /// The associated claims, only when return value is not .
+ /// Whether to check for expiration.
+ /// The status of the validation.
+ public static ManifestStatus Validate(string jwt, string jwk, out SecurityToken? token, out ClaimsPrincipal? principal, bool validateExpiration)
+ {
+ token = default;
+ principal = default;
+
+ SecurityKey key;
+ try
+ {
+ key = JsonWebKey.Create(jwk);
+ }
+ catch (ArgumentException)
+ {
+ return ManifestStatus.Unknown;
+ }
+
+ var handler = new JwtSecurityTokenHandler { MapInboundClaims = false };
+
+ if (!handler.CanReadToken(jwt))
+ return ManifestStatus.Unknown;
+
+ var validation = new TokenValidationParameters
+ {
+ RequireExpirationTime = false,
+ ValidateLifetime = false,
+ ValidateAudience = false,
+ ValidateIssuer = false,
+ ValidateIssuerSigningKey = true,
+ IssuerSigningKey = key,
+ RoleClaimType = "roles",
+ NameClaimType = "sub",
+ };
+
+ try
+ {
+ principal = handler.ValidateToken(jwt, validation, out token);
+ if (validateExpiration && token.ValidTo == DateTime.MinValue)
+ return ManifestStatus.Invalid;
+
+ // The sponsorable manifest does not have an expiration time.
+ if (validateExpiration && token.ValidTo < DateTimeOffset.UtcNow)
+ return ManifestStatus.Expired;
+
+ return ManifestStatus.Valid;
+ }
+ catch (SecurityTokenInvalidSignatureException)
+ {
+ var jwtToken = handler.ReadJwtToken(jwt);
+ token = jwtToken;
+ principal = new ClaimsPrincipal(new ClaimsIdentity(jwtToken.Claims));
+ return ManifestStatus.Invalid;
+ }
+ catch (SecurityTokenException)
+ {
+ var jwtToken = handler.ReadJwtToken(jwt);
+ token = jwtToken;
+ principal = new ClaimsPrincipal(new ClaimsIdentity(jwtToken.Claims));
+ return ManifestStatus.Invalid;
+ }
+ }
+
+}
diff --git a/src/SponsorLink/SponsorLink/SponsorLink.csproj b/src/SponsorLink/SponsorLink/SponsorLink.csproj
new file mode 100644
index 0000000..824353d
--- /dev/null
+++ b/src/SponsorLink/SponsorLink/SponsorLink.csproj
@@ -0,0 +1,79 @@
+
+
+
+ netstandard2.0
+ SponsorLink
+ disable
+ false
+ CoreResGen;$(CoreCompileDependsOn)
+
+
+
+
+ $(Product)
+
+ $([System.Text.RegularExpressions.Regex]::Replace("$(FundingProduct)", "[^A-Z]", ""))
+
+ 21
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ namespace Devlooped.Sponsors%3B
+
+partial class SponsorLink
+{
+ public partial class Funding
+ {
+ public const string Product = "$(FundingProduct)"%3B
+ public const string Prefix = "$(FundingPrefix)"%3B
+ public const int Grace = $(FundingGrace)%3B
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $([System.IO.File]::ReadAllText('$(MSBuildProjectDirectory)\$(BaseIntermediateOutputPath)devlooped.jwk'))
+
+
+
+
+
+
+
diff --git a/src/SponsorLink/SponsorLink/SponsorLinkAnalyzer.cs b/src/SponsorLink/SponsorLink/SponsorLinkAnalyzer.cs
new file mode 100644
index 0000000..2bf1783
--- /dev/null
+++ b/src/SponsorLink/SponsorLink/SponsorLinkAnalyzer.cs
@@ -0,0 +1,119 @@
+//
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using Humanizer;
+using Humanizer.Localisation;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using static Devlooped.Sponsors.SponsorLink;
+
+namespace Devlooped.Sponsors;
+
+///
+/// Links the sponsor status for the current compilation.
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
+public class SponsorLinkAnalyzer : DiagnosticAnalyzer
+{
+ static readonly Dictionary descriptors = new()
+ {
+ // Requires:
+ //
+ //
+ { SponsorStatus.Unknown, Diagnostics.GetDescriptor([.. Sponsorables.Keys], Funding.Product, Funding.Prefix, SponsorStatus.Unknown) },
+ { SponsorStatus.Sponsor, Diagnostics.GetDescriptor([.. Sponsorables.Keys], Funding.Product, Funding.Prefix, SponsorStatus.Sponsor) },
+ { SponsorStatus.Expiring, Diagnostics.GetDescriptor([.. Sponsorables.Keys], Funding.Product, Funding.Prefix, SponsorStatus.Expiring) },
+ { SponsorStatus.Expired, Diagnostics.GetDescriptor([.. Sponsorables.Keys], Funding.Product, Funding.Prefix, SponsorStatus.Expired) },
+ };
+
+ public override ImmutableArray SupportedDiagnostics { get; } = descriptors.Values.ToImmutableArray();
+
+#pragma warning disable RS1026 // Enable concurrent execution
+ public override void Initialize(AnalysisContext context)
+#pragma warning restore RS1026 // Enable concurrent execution
+ {
+#if !DEBUG
+ // Only enable concurrent execution in release builds, otherwise debugging is quite annoying.
+ context.EnableConcurrentExecution();
+#endif
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+
+#pragma warning disable RS1013 // Start action has no registered non-end actions
+ // We do this so that the status is set at compilation start so we can use it
+ // across all other analyzers. We report only on finish because multiple
+ // analyzers can report the same diagnostic and we want to avoid duplicates.
+ context.RegisterCompilationStartAction(ctx =>
+ {
+ var manifests = ctx.Options.AdditionalFiles
+ .Where(x =>
+ ctx.Options.AnalyzerConfigOptionsProvider.GetOptions(x).TryGetValue("build_metadata.AdditionalFiles.SourceItemType", out var itemType) &&
+ itemType == "SponsorManifest" &&
+ Sponsorables.ContainsKey(Path.GetFileNameWithoutExtension(x.Path)))
+ .ToImmutableArray();
+
+ // Setting the status early allows other analyzers to potentially check for it.
+ var status = SetStatus(manifests);
+ // Never report any diagnostic unless we're in an editor.
+ if (IsEditor)
+ {
+ // NOTE: even if we don't report the diagnostic, we still set the status so other analyzers can use it.
+ ctx.RegisterCompilationEndAction(ctx =>
+ {
+ // NOTE: for multiple projects with the same product name, we only report one diagnostic,
+ // so it's expected to NOT get a diagnostic back. Also, we don't want to report
+ // multiple diagnostics for each project in a solution that uses the same product.
+ if (Diagnostics.Pop(Funding.Product) is Diagnostic diagnostic)
+ {
+ ctx.ReportDiagnostic(diagnostic);
+ }
+ });
+ }
+ });
+#pragma warning restore RS1013 // Start action has no registered non-end actions
+ }
+
+ SponsorStatus SetStatus(ImmutableArray manifests)
+ {
+ if (!SponsorLink.TryRead(out var claims, manifests.Select(text =>
+ (text.GetText()?.ToString() ?? "", Sponsorables[Path.GetFileNameWithoutExtension(text.Path)]))) ||
+ claims.GetExpiration() is not DateTime exp)
+ {
+ // report unknown, either unparsed manifest or one with no expiration (which we never emit).
+ Diagnostics.Push(Funding.Product, Diagnostic.Create(descriptors[SponsorStatus.Unknown], null,
+ properties: ImmutableDictionary.Create().Add(nameof(SponsorStatus), nameof(SponsorStatus.Unknown)),
+ Funding.Product, Sponsorables.Keys.Humanize(Resources.Or)));
+ return SponsorStatus.Unknown;
+ }
+ else if (exp < DateTime.Now)
+ {
+ // report expired or expiring soon if still within the configured days of grace period
+ if (exp.AddDays(Funding.Grace) < DateTime.Now)
+ {
+ // report expiring soon
+ Diagnostics.Push(Funding.Product, Diagnostic.Create(descriptors[SponsorStatus.Expiring], null,
+ properties: ImmutableDictionary.Create().Add(nameof(SponsorStatus), nameof(SponsorStatus.Expiring))));
+ return SponsorStatus.Expiring;
+ }
+ else
+ {
+ // report expired
+ Diagnostics.Push(Funding.Product, Diagnostic.Create(descriptors[SponsorStatus.Expired], null,
+ properties: ImmutableDictionary.Create().Add(nameof(SponsorStatus), nameof(SponsorStatus.Expired))));
+ return SponsorStatus.Expired;
+ }
+ }
+ else
+ {
+ // report sponsor
+ Diagnostics.Push(Funding.Product, Diagnostic.Create(descriptors[SponsorStatus.Sponsor], null,
+ properties: ImmutableDictionary.Create().Add(nameof(SponsorStatus), nameof(SponsorStatus.Sponsor)),
+ Funding.Product));
+ return SponsorStatus.Sponsor;
+ }
+ }
+}
diff --git a/src/SponsorLink/SponsorLink/SponsorStatus.cs b/src/SponsorLink/SponsorLink/SponsorStatus.cs
new file mode 100644
index 0000000..6cdbc90
--- /dev/null
+++ b/src/SponsorLink/SponsorLink/SponsorStatus.cs
@@ -0,0 +1,25 @@
+//
+namespace Devlooped.Sponsors;
+
+///
+/// The determined sponsoring status.
+///
+public enum SponsorStatus
+{
+ ///
+ /// Sponsorship status is unknown.
+ ///
+ Unknown,
+ ///
+ /// The sponsors manifest is expired but within the grace period.
+ ///
+ Expiring,
+ ///
+ /// The sponsors manifest is expired and outside the grace period.
+ ///
+ Expired,
+ ///
+ /// The user is sponsoring.
+ ///
+ Sponsor,
+}
diff --git a/src/SponsorLink/SponsorLink/SponsorableLib.targets b/src/SponsorLink/SponsorLink/SponsorableLib.targets
new file mode 100644
index 0000000..8311ca6
--- /dev/null
+++ b/src/SponsorLink/SponsorLink/SponsorableLib.targets
@@ -0,0 +1,60 @@
+
+
+
+
+ $([System.IO.Path]::GetFullPath($(MSBuildThisFileDirectory)sponsorable.md))
+
+
+
+
+
+
+
+
+
+ $(WarningsNotAsErrors);LIB001;LIB002;LIB003;LIB004;LIB005
+
+ $(BaseIntermediateOutputPath)autosync.stamp
+
+ $(HOME)
+ $(USERPROFILE)
+
+ true
+ $([System.IO.Path]::GetFullPath('$(UserProfileHome)/.sponsorlink'))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ %(GitRoot.FullPath)
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SponsorLink/SponsorLink/Tracing.cs b/src/SponsorLink/SponsorLink/Tracing.cs
new file mode 100644
index 0000000..9201796
--- /dev/null
+++ b/src/SponsorLink/SponsorLink/Tracing.cs
@@ -0,0 +1,53 @@
+//
+#nullable enable
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Text;
+
+namespace Devlooped.Sponsors;
+
+static class Tracing
+{
+ public static void Trace(string message, object? value, [CallerArgumentExpression("value")] string? expression = null, [CallerFilePath] string? filePath = null, [CallerLineNumber] int lineNumber = 0)
+ => Trace($"{message}: {value} ({expression})", filePath, lineNumber);
+
+ public static void Trace(object? value, [CallerArgumentExpression("value")] string? expression = null, [CallerFilePath] string? filePath = null, [CallerLineNumber] int lineNumber = 0)
+ => Trace($"{value} ({expression})", filePath, lineNumber);
+
+ public static void Trace([CallerMemberName] string? message = null, [CallerFilePath] string? filePath = null, [CallerLineNumber] int lineNumber = 0)
+ {
+ var trace = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("SPONSORLINK_TRACE"));
+#if DEBUG
+ trace = true;
+#endif
+
+ if (!trace)
+ return;
+
+ var line = new StringBuilder()
+ .Append($"[{DateTime.Now:O}]")
+ .Append($"[{Process.GetCurrentProcess().ProcessName}:{Process.GetCurrentProcess().Id}]")
+ .Append($" {message} ")
+ .AppendLine($" -> {filePath}({lineNumber})")
+ .ToString();
+
+ var dir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData, Environment.SpecialFolderOption.Create);
+ var tries = 0;
+ // Best-effort only
+ while (tries < 10)
+ {
+ try
+ {
+ File.AppendAllText(Path.Combine(dir, "SponsorLink.log"), line);
+ Debugger.Log(0, "SponsorLink", line);
+ return;
+ }
+ catch (IOException)
+ {
+ tries++;
+ }
+ }
+ }
+}
diff --git a/src/SponsorLink/SponsorLink/buildTransitive/Devlooped.Sponsors.targets b/src/SponsorLink/SponsorLink/buildTransitive/Devlooped.Sponsors.targets
new file mode 100644
index 0000000..de0563e
--- /dev/null
+++ b/src/SponsorLink/SponsorLink/buildTransitive/Devlooped.Sponsors.targets
@@ -0,0 +1,102 @@
+
+
+
+
+ $([System.DateTime]::Now.ToString("yyyy-MM-yy"))
+
+ $(BaseIntermediateOutputPath)autosync-$(Today).stamp
+
+ $(BaseIntermediateOutputPath)autosync.stamp
+
+ $(HOME)
+ $(USERPROFILE)
+
+ $([System.IO.Path]::GetFullPath('$(UserProfileHome)/.sponsorlink'))
+
+ $([System.IO.Path]::Combine('$(SponsorLinkHome)', '.netconfig'))
+
+
+
+
+
+
+
+
+
+
+
+
+ SL_CollectDependencies
+ $(SLDependsOn);SL_CheckAutoSync;SL_ReadAutoSyncEnabled;SL_SyncSponsors
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ %(SLConfigAutoSync.Identity)
+ true
+ false
+
+
+
+
+
+
+
+ $([System.IO.File]::ReadAllText($(AutoSyncStampFile)).Trim())
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SponsorLink/SponsorLink/sponsorable.md b/src/SponsorLink/SponsorLink/sponsorable.md
new file mode 100644
index 0000000..c023c25
--- /dev/null
+++ b/src/SponsorLink/SponsorLink/sponsorable.md
@@ -0,0 +1,5 @@
+# Why Sponsor
+
+Well, why not? It's super cheap :)
+
+This could even be partially auto-generated from FUNDING.yml and what-not.
\ No newline at end of file
diff --git a/src/SponsorLink/SponsorLinkAnalyzer.sln b/src/SponsorLink/SponsorLinkAnalyzer.sln
new file mode 100644
index 0000000..be206b1
--- /dev/null
+++ b/src/SponsorLink/SponsorLinkAnalyzer.sln
@@ -0,0 +1,43 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.10.34928.147
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Analyzer", "Analyzer\Analyzer.csproj", "{584984D6-926B-423D-9416-519613423BAE}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Library", "Library\Library.csproj", "{598CD398-A172-492C-8367-827D43276029}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Tests\Tests.csproj", "{EA02494C-6ED4-47A0-8D43-20F50BE8554F}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SponsorLink", "SponsorLink\SponsorLink.csproj", "{B91C7E99-3D2E-4FDF-B017-9123E810197F}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {584984D6-926B-423D-9416-519613423BAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {584984D6-926B-423D-9416-519613423BAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {584984D6-926B-423D-9416-519613423BAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {584984D6-926B-423D-9416-519613423BAE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {598CD398-A172-492C-8367-827D43276029}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {598CD398-A172-492C-8367-827D43276029}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {598CD398-A172-492C-8367-827D43276029}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {598CD398-A172-492C-8367-827D43276029}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EA02494C-6ED4-47A0-8D43-20F50BE8554F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EA02494C-6ED4-47A0-8D43-20F50BE8554F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EA02494C-6ED4-47A0-8D43-20F50BE8554F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EA02494C-6ED4-47A0-8D43-20F50BE8554F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B91C7E99-3D2E-4FDF-B017-9123E810197F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B91C7E99-3D2E-4FDF-B017-9123E810197F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B91C7E99-3D2E-4FDF-B017-9123E810197F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B91C7E99-3D2E-4FDF-B017-9123E810197F}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {1DDA0EFF-BEF6-49BB-8AA8-D71FE1CD3E6F}
+ EndGlobalSection
+EndGlobal
diff --git a/src/SponsorLink/Tests/.netconfig b/src/SponsorLink/Tests/.netconfig
new file mode 100644
index 0000000..3b3bd0d
--- /dev/null
+++ b/src/SponsorLink/Tests/.netconfig
@@ -0,0 +1,15 @@
+[file "SponsorableManifest.cs"]
+ url = https://github.com/devlooped/SponsorLink/blob/main/src/Core/SponsorableManifest.cs
+ sha = 976ecefc44d87217e04933d9cd7f6b950468410b
+ etag = e0c95e7fc6c0499dbc8c5cd28aa9a6a5a49c9d0ad41fe028a5a085aca7e00eaf
+ weak
+[file "JsonOptions.cs"]
+ url = https://github.com/devlooped/SponsorLink/blob/main/src/Core/JsonOptions.cs
+ sha = 79dc56ce45fc36df49e1c4f8875e93c297edc383
+ etag = 6e9a1b12757a97491441b9534ced4e5dac6d9d6334008fa0cd20575650bbd935
+ weak
+[file "Extensions.cs"]
+ url = https://github.com/devlooped/SponsorLink/blob/main/src/Core/Extensions.cs
+ sha = d204b667eace818934c49e09b5b08ea82aef87fa
+ etag = f68e11894103f8748ce290c29927bf1e4f749e743ae33d5350e72ed22c15d245
+ weak
diff --git a/src/SponsorLink/Tests/Attributes.cs b/src/SponsorLink/Tests/Attributes.cs
new file mode 100644
index 0000000..aa5f48d
--- /dev/null
+++ b/src/SponsorLink/Tests/Attributes.cs
@@ -0,0 +1,59 @@
+using Microsoft.Extensions.Configuration;
+using Xunit;
+
+public class SecretsFactAttribute : FactAttribute
+{
+ public SecretsFactAttribute(params string[] secrets)
+ {
+ var configuration = new ConfigurationBuilder()
+ .AddUserSecrets()
+ .Build();
+
+ var missing = new HashSet();
+
+ foreach (var secret in secrets)
+ {
+ if (string.IsNullOrEmpty(configuration[secret]))
+ missing.Add(secret);
+ }
+
+ if (missing.Count > 0)
+ Skip = "Missing user secrets: " + string.Join(',', missing);
+ }
+}
+
+public class LocalFactAttribute : SecretsFactAttribute
+{
+ public LocalFactAttribute(params string[] secrets) : base(secrets)
+ {
+ if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI")))
+ Skip = "Non-CI test";
+ }
+}
+
+public class CIFactAttribute : FactAttribute
+{
+ public CIFactAttribute()
+ {
+ if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI")))
+ Skip = "CI-only test";
+ }
+}
+
+public class LocalTheoryAttribute : TheoryAttribute
+{
+ public LocalTheoryAttribute()
+ {
+ if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI")))
+ Skip = "Non-CI test";
+ }
+}
+
+public class CITheoryAttribute : TheoryAttribute
+{
+ public CITheoryAttribute()
+ {
+ if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI")))
+ Skip = "CI-only test";
+ }
+}
\ No newline at end of file
diff --git a/src/SponsorLink/Tests/Extensions.cs b/src/SponsorLink/Tests/Extensions.cs
new file mode 100644
index 0000000..75a78b4
--- /dev/null
+++ b/src/SponsorLink/Tests/Extensions.cs
@@ -0,0 +1,43 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using Microsoft.Extensions.Logging;
+
+namespace Devlooped.Sponsors;
+
+static class Extensions
+{
+ public static HashCode Add(this HashCode hash, params object[] items)
+ {
+ foreach (var item in items)
+ hash.Add(item);
+
+ return hash;
+ }
+
+
+ public static HashCode AddRange(this HashCode hash, IEnumerable items)
+ {
+ foreach (var item in items)
+ hash.Add(item);
+
+ return hash;
+ }
+
+ public static Array Cast(this Array array, Type elementType)
+ {
+ //Convert the object list to the destination array type.
+ var result = Array.CreateInstance(elementType, array.Length);
+ Array.Copy(array, result, array.Length);
+ return result;
+ }
+
+ public static void Assert(this ILogger logger, [DoesNotReturnIf(false)] bool condition, [CallerArgumentExpression(nameof(condition))] string? message = default, params object?[] args)
+ {
+ if (!condition)
+ {
+ //Debug.Assert(condition, message);
+ logger.LogError(message, args);
+ throw new InvalidOperationException(message);
+ }
+ }
+}
diff --git a/src/SponsorLink/Tests/JsonOptions.cs b/src/SponsorLink/Tests/JsonOptions.cs
new file mode 100644
index 0000000..c816eba
--- /dev/null
+++ b/src/SponsorLink/Tests/JsonOptions.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
+using Microsoft.IdentityModel.Tokens;
+
+namespace Devlooped.Sponsors;
+
+static partial class JsonOptions
+{
+ public static JsonSerializerOptions Default { get; } =
+#if NET6_0_OR_GREATER
+ new(JsonSerializerDefaults.Web)
+#else
+ new()
+#endif
+ {
+ AllowTrailingCommas = true,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ ReadCommentHandling = JsonCommentHandling.Skip,
+#if NET6_0_OR_GREATER
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault | JsonIgnoreCondition.WhenWritingNull,
+#endif
+ WriteIndented = true,
+ Converters =
+ {
+ new JsonStringEnumConverter(allowIntegerValues: false),
+#if NET6_0_OR_GREATER
+ new DateOnlyJsonConverter()
+#endif
+ }
+ };
+
+ public static JsonSerializerOptions JsonWebKey { get; } = new(JsonSerializerOptions.Default)
+ {
+ WriteIndented = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault | JsonIgnoreCondition.WhenWritingNull,
+ TypeInfoResolver = new DefaultJsonTypeInfoResolver
+ {
+ Modifiers =
+ {
+ info =>
+ {
+ if (info.Type != typeof(JsonWebKey))
+ return;
+
+ foreach (var prop in info.Properties)
+ {
+ // Don't serialize empty lists, makes for more concise JWKs
+ prop.ShouldSerialize = (obj, value) =>
+ value is not null &&
+ (value is not IList list || list.Count > 0);
+ }
+ }
+ }
+ }
+ };
+
+
+#if NET6_0_OR_GREATER
+ public class DateOnlyJsonConverter : JsonConverter
+ {
+ public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ => DateOnly.Parse(reader.GetString()?[..10] ?? "", CultureInfo.InvariantCulture);
+
+ public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options)
+ => writer.WriteStringValue(value.ToString("O", CultureInfo.InvariantCulture));
+ }
+#endif
+}
diff --git a/src/SponsorLink/Tests/Resources.Designer.cs b/src/SponsorLink/Tests/Resources.Designer.cs
new file mode 100644
index 0000000..7824a60
--- /dev/null
+++ b/src/SponsorLink/Tests/Resources.Designer.cs
@@ -0,0 +1,63 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace Tests {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Tests.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+ }
+}
diff --git a/src/SponsorLink/Tests/Resources.resx b/src/SponsorLink/Tests/Resources.resx
new file mode 100644
index 0000000..4fdb1b6
--- /dev/null
+++ b/src/SponsorLink/Tests/Resources.resx
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/src/SponsorLink/Tests/Sample.cs b/src/SponsorLink/Tests/Sample.cs
new file mode 100644
index 0000000..897c91c
--- /dev/null
+++ b/src/SponsorLink/Tests/Sample.cs
@@ -0,0 +1,59 @@
+extern alias Analyzer;
+using System;
+using System.Globalization;
+using System.Runtime.CompilerServices;
+using System.Security.Cryptography;
+using Analyzer::Devlooped.Sponsors;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Tests;
+
+public class Sample(ITestOutputHelper output)
+{
+ [Theory]
+ [InlineData("es-AR", SponsorStatus.Unknown)]
+ [InlineData("es-AR", SponsorStatus.Expiring)]
+ [InlineData("es-AR", SponsorStatus.Expired)]
+ [InlineData("es-AR", SponsorStatus.Sponsor)]
+ [InlineData("en", SponsorStatus.Unknown)]
+ [InlineData("en", SponsorStatus.Expiring)]
+ [InlineData("en", SponsorStatus.Expired)]
+ [InlineData("en", SponsorStatus.Sponsor)]
+ [InlineData("", SponsorStatus.Unknown)]
+ [InlineData("", SponsorStatus.Expiring)]
+ [InlineData("", SponsorStatus.Expired)]
+ [InlineData("", SponsorStatus.Sponsor)]
+ public void Test(string culture, SponsorStatus kind)
+ {
+ Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture =
+ culture == "" ? CultureInfo.InvariantCulture : CultureInfo.GetCultureInfo(culture);
+
+ var diag = new DiagnosticsManager().GetDescriptor(["foo"], "bar", "FB", kind);
+
+ output.WriteLine(diag.Title.ToString());
+ output.WriteLine(diag.MessageFormat.ToString());
+ output.WriteLine(diag.Description.ToString());
+ }
+
+ [Fact]
+ public void RenderSponsorables()
+ {
+ Assert.NotEmpty(SponsorLink.Sponsorables);
+
+ foreach (var pair in SponsorLink.Sponsorables)
+ {
+ output.WriteLine($"{pair.Key} = {pair.Value}");
+ // Read the JWK
+ var jsonWebKey = Microsoft.IdentityModel.Tokens.JsonWebKey.Create(pair.Value);
+
+ Assert.NotNull(jsonWebKey);
+
+ using var key = RSA.Create(new RSAParameters
+ {
+ Modulus = Microsoft.IdentityModel.Tokens.Base64UrlEncoder.DecodeBytes(jsonWebKey.N),
+ Exponent = Microsoft.IdentityModel.Tokens.Base64UrlEncoder.DecodeBytes(jsonWebKey.E),
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/SponsorLink/Tests/SponsorLinkTests.cs b/src/SponsorLink/Tests/SponsorLinkTests.cs
new file mode 100644
index 0000000..7625e2c
--- /dev/null
+++ b/src/SponsorLink/Tests/SponsorLinkTests.cs
@@ -0,0 +1,126 @@
+extern alias Analyzer;
+using System.Security.Cryptography;
+using System.Text.Json;
+using Analyzer::Devlooped.Sponsors;
+using Devlooped.Sponsors;
+using Microsoft.IdentityModel.Tokens;
+using Xunit;
+
+namespace Devlooped.Tests;
+
+public class SponsorLinkTests
+{
+ // We need to convert to jwk string since the analyzer project has merged the JWT assembly and types.
+ public static string ToJwk(SecurityKey key)
+ => JsonSerializer.Serialize(
+ JsonWebKeyConverter.ConvertFromSecurityKey(key),
+ JsonOptions.JsonWebKey);
+
+ [Fact]
+ public void ValidateSponsorable()
+ {
+ var manifest = SponsorableManifest.Create(new Uri("https://foo.com"), [new Uri("https://github.com/sponsors/bar")], "ASDF1234");
+ var jwt = manifest.ToJwt();
+ var jwk = ToJwk(manifest.SecurityKey);
+
+ // NOTE: sponsorable manifest doesn't have expiration date.
+ var status = SponsorLink.Validate(jwt, jwk, out var token, out var principal, false);
+
+ Assert.Equal(ManifestStatus.Valid, status);
+ }
+
+ [Fact]
+ public void ValidateWrongKey()
+ {
+ var manifest = SponsorableManifest.Create(new Uri("https://foo.com"), [new Uri("https://github.com/sponsors/bar")], "ASDF1234");
+ var jwt = manifest.ToJwt();
+ var jwk = ToJwk(new RsaSecurityKey(RSA.Create()));
+
+ var status = SponsorLink.Validate(jwt, jwk, out var token, out var principal, false);
+
+ Assert.Equal(ManifestStatus.Invalid, status);
+
+ // We should still be a able to read the data, knowing it may have been tampered with.
+ Assert.NotNull(principal);
+ Assert.NotNull(token);
+ }
+
+ [Fact]
+ public void ValidateExpiredSponsor()
+ {
+ var manifest = SponsorableManifest.Create(new Uri("https://foo.com"), [new Uri("https://github.com/sponsors/bar")], "ASDF1234");
+ var jwk = ToJwk(manifest.SecurityKey);
+ var sponsor = manifest.Sign([], expiration: TimeSpan.Zero);
+
+ // Will be expired after this.
+ Thread.Sleep(1000);
+
+ var status = SponsorLink.Validate(sponsor, jwk, out var token, out var principal, true);
+
+ Assert.Equal(ManifestStatus.Expired, status);
+
+ // We should still be a able to read the data, even if expired (but not tampered with).
+ Assert.NotNull(principal);
+ Assert.NotNull(token);
+ }
+
+ [Fact]
+ public void ValidateUnknownFormat()
+ {
+ var manifest = SponsorableManifest.Create(new Uri("https://foo.com"), [new Uri("https://github.com/sponsors/bar")], "ASDF1234");
+ var jwk = ToJwk(manifest.SecurityKey);
+
+ var status = SponsorLink.Validate("asdfasdf", jwk, out var token, out var principal, false);
+
+ Assert.Equal(ManifestStatus.Unknown, status);
+
+ // Nothing could be read at all.
+ Assert.Null(principal);
+ Assert.Null(token);
+ }
+
+ [Fact]
+ public void TryRead()
+ {
+ var fooSponsorable = SponsorableManifest.Create(new Uri("https://foo.com"), [new Uri("https://github.com/sponsors/foo")], "ASDF1234");
+ var barSponsorable = SponsorableManifest.Create(new Uri("https://bar.com"), [new Uri("https://github.com/sponsors/bar")], "GHJK5678");
+
+ // Org sponsor and member of team
+ var fooSponsor = fooSponsorable.Sign([new("sub", "kzu"), new("email", "me@foo.com"), new("roles", "org"), new("roles", "team")], expiration: TimeSpan.FromDays(30));
+ // Org + personal sponsor
+ var barSponsor = barSponsorable.Sign([new("sub", "kzu"), new("email", "me@bar.com"), new("roles", "org"), new("roles", "user")], expiration: TimeSpan.FromDays(30));
+
+ Assert.True(SponsorLink.TryRead(out var principal, [(fooSponsor, ToJwk(fooSponsorable.SecurityKey)), (barSponsor, ToJwk(barSponsorable.SecurityKey))]));
+
+ // Can check role across both JWTs
+ Assert.True(principal.IsInRole("org"));
+ Assert.True(principal.IsInRole("team"));
+ Assert.True(principal.IsInRole("user"));
+
+ Assert.True(principal.HasClaim("sub", "kzu"));
+ Assert.True(principal.HasClaim("email", "me@foo.com"));
+ Assert.True(principal.HasClaim("email", "me@bar.com"));
+ }
+
+ [LocalFact]
+ public void ValidateCachedManifest()
+ {
+ var path = Environment.ExpandEnvironmentVariables("%userprofile%\\.sponsorlink\\github\\devlooped.jwt");
+ if (!File.Exists(path))
+ return;
+
+ var jwt = File.ReadAllText(path);
+
+ var status = SponsorLink.Validate(jwt,
+ """
+ {
+ "e": "AQAB",
+ "kty": "RSA",
+ "n": "5inhv8QymaDBOihNi1eY-6-hcIB5qSONFZxbxxXAyOtxAdjFCPM-94gIZqM9CDrX3pyg1lTJfml_a_FZSU9dB1ii5mSX_mNHBFXn1_l_gi1ErdbkIF5YbW6oxWFxf3G5mwVXwnPfxHTyQdmWQ3YJR-A3EB4kaFwLqA6Ha5lb2ObGpMTQJNakD4oTAGDhqHMGhu6PupGq5ie4qZcQ7N8ANw8xH7nicTkbqEhQABHWOTmLBWq5f5F6RYGF8P7cl0IWl_w4YcIZkGm2vX2fi26F9F60cU1v13GZEVDTXpJ9kzvYeM9sYk6fWaoyY2jhE51qbv0B0u6hScZiLREtm3n7ClJbIGXhkUppFS2JlNaX3rgQ6t-4LK8gUTyLt3zDs2H8OZyCwlCpfmGmdsUMkm1xX6t2r-95U3zywynxoWZfjBCJf41leM9OMKYwNWZ6LQMyo83HWw1PBIrX4ZLClFwqBcSYsXDyT8_ZLd1cdYmPfmtllIXxZhLClwT5qbCWv73V"
+ }
+ """
+ , out var token, out var principal, false);
+
+ Assert.Equal(ManifestStatus.Valid, status);
+ }
+}
diff --git a/src/SponsorLink/Tests/SponsorableManifest.cs b/src/SponsorLink/Tests/SponsorableManifest.cs
new file mode 100644
index 0000000..5ae6e3f
--- /dev/null
+++ b/src/SponsorLink/Tests/SponsorableManifest.cs
@@ -0,0 +1,309 @@
+using System.Diagnostics.CodeAnalysis;
+using System.IdentityModel.Tokens.Jwt;
+using System.Security.Claims;
+using System.Security.Cryptography;
+using System.Text.Json;
+using Microsoft.IdentityModel.Tokens;
+
+namespace Devlooped.Sponsors;
+
+///
+/// The serializable manifest of a sponsorable user, as persisted
+/// in the .github/sponsorlink.jwt file.
+///
+public class SponsorableManifest
+{
+ ///
+ /// Overall manifest status.
+ ///
+ public enum Status
+ {
+ ///
+ /// SponsorLink manifest is invalid.
+ ///
+ Invalid,
+ ///
+ /// The manifest has an audience that doesn't match the sponsorable account.
+ ///
+ AccountMismatch,
+ ///
+ /// SponsorLink manifest not found for the given account, so it's not supported.
+ ///
+ NotFound,
+ ///
+ /// Manifest was successfully fetched and validated.
+ ///
+ OK,
+ }
+
+ ///
+ /// Creates a new manifest with a new RSA key pair.
+ ///
+ public static SponsorableManifest Create(Uri issuer, Uri[] audience, string clientId)
+ {
+ var rsa = RSA.Create(3072);
+ var pub = Convert.ToBase64String(rsa.ExportRSAPublicKey());
+
+ return new SponsorableManifest(issuer, audience, clientId, new RsaSecurityKey(rsa), pub);
+ }
+
+ public static async Task<(Status, SponsorableManifest?)> FetchAsync(string sponsorable, string? branch, HttpClient? http = default)
+ {
+ // Try to detect sponsorlink manifest in the sponsorable .github repo
+ var url = $"https://github.com/{sponsorable}/.github/raw/{branch ?? "main"}/sponsorlink.jwt";
+
+ // Manifest should be public, so no need for any special HTTP client.
+ using (http ??= new HttpClient())
+ {
+ var response = await http.GetAsync(url);
+ if (!response.IsSuccessStatusCode)
+ return (Status.NotFound, default);
+
+ var jwt = await response.Content.ReadAsStringAsync();
+ if (!TryRead(jwt, out var manifest, out var missingClaim))
+ return (Status.Invalid, default);
+
+ // Manifest audience should match the sponsorable account to avoid weird issues?
+ if (sponsorable != manifest.Sponsorable)
+ return (Status.AccountMismatch, default);
+
+ return (Status.OK, manifest);
+ }
+ }
+
+ ///
+ /// Parses a JWT into a .
+ ///
+ /// The JWT containing the sponsorable information.
+ /// The parsed manifest, if not required claims are missing.
+ /// The missing required claim, if any.
+ /// A validated manifest.
+ public static bool TryRead(string jwt, [NotNullWhen(true)] out SponsorableManifest? manifest, out string? missingClaim)
+ {
+ var handler = new JwtSecurityTokenHandler { MapInboundClaims = false };
+ missingClaim = null;
+ manifest = default;
+
+ if (!handler.CanReadToken(jwt))
+ return false;
+
+ var token = handler.ReadJwtToken(jwt);
+ var issuer = token.Issuer;
+
+ if (token.Audiences.FirstOrDefault(x => x.StartsWith("https://github.com/")) is null)
+ {
+ missingClaim = "aud";
+ return false;
+ }
+
+ if (token.Claims.FirstOrDefault(c => c.Type == "client_id")?.Value is not string clientId)
+ {
+ missingClaim = "client_id";
+ return false;
+ }
+
+ if (token.Claims.FirstOrDefault(c => c.Type == "pub")?.Value is not string pub)
+ {
+ missingClaim = "pub";
+ return false;
+ }
+
+ if (token.Claims.FirstOrDefault(c => c.Type == "sub_jwk")?.Value is not string jwk)
+ {
+ missingClaim = "sub_jwk";
+ return false;
+ }
+
+ var key = new JsonWebKeySet { Keys = { JsonWebKey.Create(jwk) } }.GetSigningKeys().First();
+ manifest = new SponsorableManifest(new Uri(issuer), token.Audiences.Select(x => new Uri(x)).ToArray(), clientId, key, pub);
+
+ return true;
+ }
+
+ public SponsorableManifest(Uri issuer, Uri[] audience, string clientId, SecurityKey publicKey, string publicRsaKey)
+ {
+ Issuer = issuer.AbsoluteUri;
+ Audience = audience.Select(a => a.AbsoluteUri.TrimEnd('/')).ToArray();
+ ClientId = clientId;
+ SecurityKey = publicKey;
+ PublicKey = publicRsaKey;
+ Sponsorable = audience.Where(x => x.Host == "github.com").Select(x => x.Segments.LastOrDefault()?.TrimEnd('/')).FirstOrDefault() ??
+ throw new ArgumentException("At least one of the intended audience must be a GitHub sponsors URL.");
+ }
+
+ ///
+ /// Converts (and optionally signs) the manifest into a JWT. Never exports the private key.
+ ///
+ /// Optional credentials when signing the resulting manifest. Defaults to the if it has a private key.
+ /// The JWT manifest.
+ public string ToJwt(SigningCredentials? signing = default)
+ {
+ var jwk = JsonWebKeyConverter.ConvertFromSecurityKey(SecurityKey);
+
+ // Automatically sign if the manifest was created with a private key
+ if (SecurityKey is RsaSecurityKey rsa && rsa.PrivateKeyStatus == PrivateKeyStatus.Exists)
+ {
+ signing ??= new SigningCredentials(rsa, SecurityAlgorithms.RsaSha256);
+
+ // Ensure we never serialize the private key
+ jwk = JsonWebKeyConverter.ConvertFromRSASecurityKey(new RsaSecurityKey(rsa.Rsa.ExportParameters(false)));
+ }
+
+ var token = new JwtSecurityToken(
+ claims:
+ new[] { new Claim(JwtRegisteredClaimNames.Iss, Issuer) }
+ .Concat(Audience.Select(x => new Claim(JwtRegisteredClaimNames.Aud, x)))
+ .Concat(
+ [
+ // See https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.6
+ new(JwtRegisteredClaimNames.Iat, Math.Truncate((DateTime.UtcNow - DateTime.UnixEpoch).TotalSeconds).ToString()),
+ new("client_id", ClientId),
+ // non-standard claim containing the base64-encoded public key
+ new("pub", PublicKey),
+ // standard claim, serialized as a JSON string, not an encoded JSON object
+ new("sub_jwk", JsonSerializer.Serialize(jwk, JsonOptions.JsonWebKey), JsonClaimValueTypes.Json),
+ ]),
+ signingCredentials: signing);
+
+ return new JwtSecurityTokenHandler().WriteToken(token);
+ }
+
+ ///
+ /// Sign the JWT claims with the provided RSA key.
+ ///
+ public string Sign(IEnumerable claims, RSA rsa, TimeSpan? expiration = default)
+ => Sign(claims, new RsaSecurityKey(rsa), expiration);
+
+ public string Sign(IEnumerable claims, RsaSecurityKey? key = default, TimeSpan? expiration = default)
+ {
+ var rsa = key ?? SecurityKey as RsaSecurityKey;
+ if (rsa?.PrivateKeyStatus != PrivateKeyStatus.Exists)
+ throw new NotSupportedException("No private key found to sign the manifest.");
+
+ var signing = new SigningCredentials(rsa, SecurityAlgorithms.RsaSha256);
+
+ var expirationDate = expiration != null ?
+ DateTime.UtcNow.Add(expiration.Value) :
+ // Expire the first day of the next month
+ new DateTime(
+ DateTime.UtcNow.AddMonths(1).Year,
+ DateTime.UtcNow.AddMonths(1).Month, 1,
+ // Use current time so they don't expire all at the same time
+ DateTime.UtcNow.Hour,
+ DateTime.UtcNow.Minute,
+ DateTime.UtcNow.Second,
+ DateTime.UtcNow.Millisecond,
+ DateTimeKind.Utc);
+
+ var tokenClaims = claims.Where(x => x.Type != JwtRegisteredClaimNames.Iat && x.Type != JwtRegisteredClaimNames.Exp).ToList();
+
+ // See https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.6
+ tokenClaims.Add(new(JwtRegisteredClaimNames.Iat, Math.Truncate((DateTime.UtcNow - DateTime.UnixEpoch).TotalSeconds).ToString()));
+
+ if (tokenClaims.Find(c => c.Type == JwtRegisteredClaimNames.Iss) is { } issuer)
+ {
+ if (issuer.Value != Issuer)
+ throw new ArgumentException($"The received claims contain an incompatible 'iss' claim. If present, the claim must contain the value '{Issuer}' but was '{issuer.Value}'.");
+ }
+ else
+ {
+ tokenClaims.Insert(0, new(JwtRegisteredClaimNames.Iss, Issuer));
+ }
+
+ if (tokenClaims.Find(c => c.Type == "client_id") is { } clientId)
+ {
+ if (clientId.Value != ClientId)
+ throw new ArgumentException($"The received claims contain an incompatible 'client_id' claim. If present, the claim must contain the value '{ClientId}' but was '{clientId.Value}'.");
+ }
+ else
+ {
+ tokenClaims.Add(new("client_id", ClientId));
+ }
+
+ // Avoid duplicating audience claims
+ foreach (var audience in Audience)
+ {
+ // Always compare ignoring trailing /
+ if (tokenClaims.Find(c => c.Type == JwtRegisteredClaimNames.Aud && c.Value.TrimEnd('/') == audience.TrimEnd('/')) == null)
+ tokenClaims.Insert(1, new(JwtRegisteredClaimNames.Aud, audience));
+ }
+
+ // The other claims (client_id, pub, sub_jwk) claims are mostly for the SL manifest itself,
+ // not for the user, so for now we don't add them.
+
+ // Don't allow mismatches of public manifest key and the one used to sign, to avoid
+ // weird run-time errors verifiying manifests that were signed with a different key.
+ var pubKey = Convert.ToBase64String(rsa.Rsa.ExportRSAPublicKey());
+ if (pubKey != PublicKey)
+ throw new ArgumentException($"Cannot sign with a private key that does not match the manifest public key.");
+
+ var jwt = new JwtSecurityTokenHandler().WriteToken(new JwtSecurityToken(
+ claims: tokenClaims,
+ expires: expirationDate,
+ signingCredentials: signing
+ ));
+
+ return jwt;
+ }
+
+ public ClaimsPrincipal Validate(string jwt, out SecurityToken? token) => new JwtSecurityTokenHandler().ValidateToken(jwt, new TokenValidationParameters
+ {
+ RequireExpirationTime = true,
+ // NOTE: setting this to false allows checking sponsorships even when the manifest is expired.
+ // This might be useful if package authors want to extend the manifest lifetime beyond the default
+ // 30 days and issue a warning on expiration, rather than an error and a forced sync.
+ // If this is not set (or true), a SecurityTokenExpiredException exception will be thrown.
+ ValidateLifetime = false,
+ RequireAudience = true,
+ // At least one of the audiences must match the manifest audiences
+ AudienceValidator = (audiences, _, _) => Audience.Intersect(audiences.Select(x => x.TrimEnd('/'))).Any(),
+ ValidIssuer = Issuer,
+ IssuerSigningKey = SecurityKey,
+ }, out token);
+
+ ///
+ /// Gets the GitHub sponsorable account.
+ ///
+ public string Sponsorable { get; }
+
+ ///
+ /// The web endpoint that issues signed JWT to authenticated users.
+ ///
+ ///
+ /// See https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.1
+ ///
+ public string Issuer { get; }
+
+ ///
+ /// The audience for the JWT, which includes the sponsorable account and potentially other sponsoring platforms.
+ ///
+ ///
+ /// See https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.3
+ ///
+ public string[] Audience { get; }
+
+ ///
+ /// The OAuth client ID (i.e. GitHub OAuth App ID) that is used to
+ /// authenticate the user.
+ ///
+ ///
+ /// See https://www.rfc-editor.org/rfc/rfc8693.html#name-client_id-client-identifier
+ ///
+ public string ClientId { get; internal set; }
+
+ ///
+ /// Public key that can be used to verify JWT signatures.
+ ///
+ public string PublicKey { get; }
+
+ ///
+ /// Public key in a format that can be used to verify JWT signatures.
+ ///
+ public SecurityKey SecurityKey { get; }
+
+ ///
+ public override int GetHashCode() => new HashCode().Add(Issuer, ClientId, PublicKey).AddRange(Audience).ToHashCode();
+
+ ///
+ public override bool Equals(object? obj) => obj is SponsorableManifest other && GetHashCode() == other.GetHashCode();
+}
diff --git a/src/SponsorLink/Tests/Tests.csproj b/src/SponsorLink/Tests/Tests.csproj
new file mode 100644
index 0000000..0585911
--- /dev/null
+++ b/src/SponsorLink/Tests/Tests.csproj
@@ -0,0 +1,57 @@
+
+
+
+ net8.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ True
+ True
+ Resources.resx
+
+
+
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
+
+
+
+
+
+
+ %(GitRoot.FullPath)
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SponsorLink/jwk.ps1 b/src/SponsorLink/jwk.ps1
new file mode 100644
index 0000000..c66f56f
--- /dev/null
+++ b/src/SponsorLink/jwk.ps1
@@ -0,0 +1 @@
+curl https://raw.githubusercontent.com/devlooped/.github/main/sponsorlink.jwt --silent | jq -R 'split(".") | .[1] | @base64d | fromjson' | jq '.sub_jwk'
\ No newline at end of file
diff --git a/src/SponsorLink/readme.md b/src/SponsorLink/readme.md
new file mode 100644
index 0000000..cb651a1
--- /dev/null
+++ b/src/SponsorLink/readme.md
@@ -0,0 +1,34 @@
+# SponsorLink .NET Analyzer
+
+This is one opinionated implementation of [SponsorLink](https://devlooped.com/SponsorLink)
+for .NET projects leveraging Roslyn analyzers.
+
+It is intended for use by [devlooped](https://github.com/devlooped) projects, but can be
+used as a template for other sponsorables as well. Supporting arbitrary sponsoring scenarios
+is out of scope though, since we just use GitHub sponsors for now.
+
+## Usage
+
+A project initializing from this template repo via [dotnet-file](https://github.com/devlooped/dotnet-file)
+will have all the sources cloned under `src\SponsorLink`.
+
+Including the analyzer and targets in a project involves two steps.
+
+1. Create an analyzer project and add the following property:
+
+```xml
+
+ ...
+ $(MSBuildThisFileDirectory)..\SponsorLink\SponsorLink.targets
+
+```
+
+2. Add a `buildTransitive\[PackageId].targets` file with the following import:
+
+```xml
+
+
+
+```
+
+As long as NuGetizer is used, the right packaging will be done automatically.
\ No newline at end of file
From 7f2f4a9b1fd7dbd3213a50e103916a070965e55d Mon Sep 17 00:00:00 2001
From: Daniel Cazzulino
Date: Tue, 25 Jun 2024 05:43:40 -0300
Subject: [PATCH 04/13] Remove incoming SL.sln from dotnet-sync
---
.netconfig | 3 +++
SponsorLink.sln | 43 -------------------------------------------
2 files changed, 3 insertions(+), 43 deletions(-)
delete mode 100644 SponsorLink.sln
diff --git a/.netconfig b/.netconfig
index ebe97f5..c105c99 100644
--- a/.netconfig
+++ b/.netconfig
@@ -50,6 +50,9 @@
sha = 0683ee777d7d878d4bf013d7deea352685135a05
etag = 2c6335b37e4ae05eea7c01f5d0c9d82b49c488f868a8b5ba7bff7c6ff01f3994
weak
+[file "SponsorLink.sln"]
+ url = https://github.com/devlooped/oss/blob/main/SponsorLink.sln
+ skip
[file "src/Directory.Build.props"]
url = https://github.com/devlooped/oss/blob/main/src/Directory.Build.props
sha = 14deaea5cecc64df51781d29891a2f67caf8be16
diff --git a/SponsorLink.sln b/SponsorLink.sln
deleted file mode 100644
index d4eab56..0000000
--- a/SponsorLink.sln
+++ /dev/null
@@ -1,43 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.11.34909.67
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SponsorLink", "src\SponsorLink\SponsorLink\SponsorLink.csproj", "{1E1D01A2-D202-4FAB-B21B-AF21B1C37163}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Analyzer", "src\SponsorLink\Analyzer\Analyzer.csproj", "{87B3A42C-FFA7-49CF-8F3A-656A6D213246}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Library", "src\SponsorLink\Library\Library.csproj", "{23371E8B-2401-42A1-9A01-4720D8388105}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "src\SponsorLink\Tests\Tests.csproj", "{A86B253A-340E-4B82-8207-336BF65F36C8}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {1E1D01A2-D202-4FAB-B21B-AF21B1C37163}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {1E1D01A2-D202-4FAB-B21B-AF21B1C37163}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {1E1D01A2-D202-4FAB-B21B-AF21B1C37163}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {1E1D01A2-D202-4FAB-B21B-AF21B1C37163}.Release|Any CPU.Build.0 = Release|Any CPU
- {87B3A42C-FFA7-49CF-8F3A-656A6D213246}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {87B3A42C-FFA7-49CF-8F3A-656A6D213246}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {87B3A42C-FFA7-49CF-8F3A-656A6D213246}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {87B3A42C-FFA7-49CF-8F3A-656A6D213246}.Release|Any CPU.Build.0 = Release|Any CPU
- {23371E8B-2401-42A1-9A01-4720D8388105}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {23371E8B-2401-42A1-9A01-4720D8388105}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {23371E8B-2401-42A1-9A01-4720D8388105}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {23371E8B-2401-42A1-9A01-4720D8388105}.Release|Any CPU.Build.0 = Release|Any CPU
- {A86B253A-340E-4B82-8207-336BF65F36C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {A86B253A-340E-4B82-8207-336BF65F36C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {A86B253A-340E-4B82-8207-336BF65F36C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {A86B253A-340E-4B82-8207-336BF65F36C8}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {36BC3C24-D4E3-4EB0-A910-4BE4BD8FE01F}
- EndGlobalSection
-EndGlobal
From a79ec58d17622cba89c7f993f12349e0ac508d2d Mon Sep 17 00:00:00 2001
From: Daniel Cazzulino
Date: Tue, 25 Jun 2024 05:43:56 -0300
Subject: [PATCH 05/13] Remove unnecessary RIDs
---
src/Config.Tool/Config.Tool.csproj | 4 ----
1 file changed, 4 deletions(-)
diff --git a/src/Config.Tool/Config.Tool.csproj b/src/Config.Tool/Config.Tool.csproj
index de63a7d..2a2ef77 100644
--- a/src/Config.Tool/Config.Tool.csproj
+++ b/src/Config.Tool/Config.Tool.csproj
@@ -35,7 +35,6 @@ Other
Exe
net6.0;net8.0
- win-x64;linux-x64
dotnet-config
DotNetConfig
@@ -56,9 +55,6 @@ Other
-
-
-
From 92a6d187ce2ad15aa3519ce825abb47581267b8c Mon Sep 17 00:00:00 2001
From: Daniel Cazzulino
Date: Tue, 25 Jun 2024 05:55:08 -0300
Subject: [PATCH 06/13] Use nugetizer for packing
---
readme.md | 4 ++++
src/CommandLine/CommandLine.csproj | 4 ++++
src/Config.Tool/Config.Tool.csproj | 4 ++++
src/Config.Tool/readme.md | 3 +++
src/Config/Config.csproj | 4 ++++
src/Configuration/Configuration.csproj | 4 ++++
6 files changed, 23 insertions(+)
create mode 100644 src/Config.Tool/readme.md
diff --git a/readme.md b/readme.md
index 9efaee4..f7b4b24 100644
--- a/readme.md
+++ b/readme.md
@@ -478,6 +478,7 @@ For numbers, the argument/option can be either `long` or `int`. Keep in mind tha
[](https://www.nuget.org/packages/dotnet-config) [](https://www.nuget.org/packages/dotnet-config)
+
The command line tool allows you to inspect and modify configuration files used by your dotnet tools.
Installation is the same as for any other dotnet tool:
@@ -533,3 +534,6 @@ Command line parsing is done with [Mono.Options](https://www.nuget.org/packages/
all the following variants for arguments are supported: `-flag`, `--flag`, `/flag`, `-flag=value`,
`--flag=value`, `/flag=value`, `-flag:value`, `--flag:value`, `/flag:value`, `-flag value`,
`--flag value`, `/flag value`.
+
+
+
\ No newline at end of file
diff --git a/src/CommandLine/CommandLine.csproj b/src/CommandLine/CommandLine.csproj
index 230d4f4..be505d5 100644
--- a/src/CommandLine/CommandLine.csproj
+++ b/src/CommandLine/CommandLine.csproj
@@ -35,6 +35,10 @@ The following heuristics are applied when providing default values:
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/src/Config.Tool/Config.Tool.csproj b/src/Config.Tool/Config.Tool.csproj
index 2a2ef77..29bb93d 100644
--- a/src/Config.Tool/Config.Tool.csproj
+++ b/src/Config.Tool/Config.Tool.csproj
@@ -52,6 +52,10 @@ Other
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/src/Config.Tool/readme.md b/src/Config.Tool/readme.md
new file mode 100644
index 0000000..f7599d8
--- /dev/null
+++ b/src/Config.Tool/readme.md
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/src/Config/Config.csproj b/src/Config/Config.csproj
index 049f8ed..9c5c84b 100644
--- a/src/Config/Config.csproj
+++ b/src/Config/Config.csproj
@@ -33,6 +33,10 @@ Usage:
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/src/Configuration/Configuration.csproj b/src/Configuration/Configuration.csproj
index ace4925..2166517 100644
--- a/src/Configuration/Configuration.csproj
+++ b/src/Configuration/Configuration.csproj
@@ -21,6 +21,10 @@ Note: section is required and subsection is optional, just like in dotnet-config
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
From e47facef91c72143dbf5219d9952ed7055230665 Mon Sep 17 00:00:00 2001
From: kzu
Date: Tue, 25 Jun 2024 09:05:59 +0000
Subject: [PATCH 07/13] =?UTF-8?q?=F0=9F=96=89=20Update=20changelog=20with?=
=?UTF-8?q?=20v1.1.0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
changelog.md | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/changelog.md b/changelog.md
index 08f7a40..cb05fc6 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,4 +1,22 @@
+# Changelog
+## [v1.1.0](https://github.com/dotnetconfig/dotnet-config/tree/v1.1.0) (2024-06-25)
+
+[Full Changelog](https://github.com/dotnetconfig/dotnet-config/compare/v1.0.6...v1.1.0)
+
+:sparkles: Implemented enhancements:
+
+- Use nugetizer for packing [\#150](https://github.com/dotnetconfig/dotnet-config/pull/150) (@kzu)
+- Bump to .net6/8 for the CLI [\#148](https://github.com/dotnetconfig/dotnet-config/pull/148) (@kzu)
+
+:hammer: Other:
+
+- .Net 5 reached EOL, please upgrade this tool to use .Net 6 or 8 [\#146](https://github.com/dotnetconfig/dotnet-config/issues/146)
+- Can't save empty/blank values [\#145](https://github.com/dotnetconfig/dotnet-config/issues/145)
+
+:twisted_rightwards_arrows: Merged:
+
+- Add how to work with array of complex objects [\#98](https://github.com/dotnetconfig/dotnet-config/pull/98) (@PadreSVK)
## [v1.0.6](https://github.com/dotnetconfig/dotnet-config/tree/v1.0.6) (2021-07-30)
From 90333f192b75bb5eea5377aaedb6f8a17c907f2d Mon Sep 17 00:00:00 2001
From: Daniel Cazzulino
Date: Tue, 25 Jun 2024 14:52:21 -0300
Subject: [PATCH 08/13] Make options and note high compat level with git config
---
readme.md | 19 +++++++++++++------
src/Config.Tool/Program.cs | 23 ++++++++++++++++-------
2 files changed, 29 insertions(+), 13 deletions(-)
diff --git a/readme.md b/readme.md
index f7b4b24..ff540e5 100644
--- a/readme.md
+++ b/readme.md
@@ -486,6 +486,11 @@ Installation is the same as for any other dotnet tool:
> dotnet tool install -g dotnet-config
```
+The available options and actions are (for the most part) compatible with the behavior of `git config`.
+
+> When reading and writing from a single file, you can for the most part just use `git config`
+> along with the compatibility option `-f|--file` specifying the file to read/write from.
+
Reading and writing variables don't require any special options. The following lines first
write a variable value and then retrieve its value:
@@ -504,29 +509,31 @@ All current options from running `dotnet config -?` are:
Usage: dotnet config [options]
Location (uses all locations by default)
- --local use .netconfig.user file
--global use global config file
--system use system config file
+ --local use .netconfig.user file
+ -f, --file use given config file (git config compat)
--path[=VALUE] use given config file or directory
Action
--get get value: name [value-regex]
--get-all get all values: key [value-regex]
--get-regexp get values for regexp: name-regex [value-regex]
- --set set value: name value [value-regex]
- --set-all set all matches: name value [value-regex]
--add add a new variable: name value
--unset remove a variable: name [value-regex]
--unset-all remove all matches: name [value-regex]
- --remove-section remove a section: name
+ --set set value: name value [value-regex]
+ --set-all set all matches: name value [value-regex]
--rename-section rename section: old-name new-name
+ --remove-section remove a section: name
-l, --list list all
-e, --edit edit the config file in an editor
Other
- --default[=VALUE] with --get, use default value when missing entry
--name-only show variable names only
- --type[=VALUE] value is given this type, can be 'boolean', 'datetime' or 'number'
+ --default[=VALUE] with --get, use default value when missing entry
+ --type[=VALUE] value is given this type, can be 'boolean', '
+ datetime' or 'number'
-?, -h, --help Display this help
```
diff --git a/src/Config.Tool/Program.cs b/src/Config.Tool/Program.cs
index 2f2eef9..0a65dd0 100644
--- a/src/Config.Tool/Program.cs
+++ b/src/Config.Tool/Program.cs
@@ -30,7 +30,8 @@ static int Run(string[] args)
var useSystem = false;
var useGlobal = false;
var useLocal = false;
- var path = Directory.GetCurrentDirectory();
+ string? path = default;
+ string? file = default;
var nameOnly = false;
string? defaultValue = default;
string? type = default;
@@ -41,18 +42,17 @@ static int Run(string[] args)
{ Environment.NewLine },
{ Environment.NewLine },
{ "Location (uses all locations by default)" },
- { "local", "use .netconfig.user file", _ => useLocal = true },
{ "global", "use global config file", _ => useGlobal = true },
{ "system", "use system config file", _ => useSystem = true },
- { "path:", "use given config file or directory", f => path = f },
+ { "local", "use .netconfig.user file", _ => useLocal = true },
+ { "f|file", "use given config file (git config compat)", f => file = f },
+ { "path:", "use given config file or directory", p => path = p },
{ Environment.NewLine },
{ "Action" },
{ "get", "get value: name [value-regex]", _ => action = ConfigAction.Get },
{ "get-all", "get all values: key [value-regex]", _ => action = ConfigAction.GetAll },
{ "get-regexp", "get values for regexp: name-regex [value-regex]", _ => action = ConfigAction.GetRegexp },
- { "set", "set value: name value [value-regex]", _ => action = ConfigAction.Set },
- { "set-all", "set all matches: name value [value-regex]", _ => action = ConfigAction.SetAll },
{ "replace-all", "replace all matches: name value [value-regex]", _ => action = ConfigAction.SetAll, true },
//{ "get-urlmatch", "get value specific for the URL: section[.var] URL", _ => action = ConfigAction.Get },
@@ -60,16 +60,19 @@ static int Run(string[] args)
{ "unset", "remove a variable: name [value-regex]", _ => action = ConfigAction.Unset },
{ "unset-all", "remove all matches: name [value-regex]", _ => action = ConfigAction.UnsetAll },
- { "remove-section", "remove a section: name", _ => action = ConfigAction.RemoveSection },
+ { "set", "set value: name value [value-regex]", _ => action = ConfigAction.Set },
+ { "set-all", "set all matches: name value [value-regex]", _ => action = ConfigAction.SetAll },
+
{ "rename-section", "rename section: old-name new-name", _ => action = ConfigAction.RenameSection },
+ { "remove-section", "remove a section: name", _ => action = ConfigAction.RemoveSection },
{ "l|list", "list all", _ => action = ConfigAction.List },
{ "e|edit", "edit the config file in an editor", _ => action = ConfigAction.Edit },
{ Environment.NewLine },
{ "Other" },
- { "default:", "with --get, use default value when missing entry", v => defaultValue = v },
{ "name-only", "show variable names only", _ => nameOnly = true },
+ { "default:", "with --get, use default value when missing entry", v => defaultValue = v },
{ "type:", "value is given this type, can be 'boolean', 'datetime' or 'number'", t => type = t },
{ "debug", "add some extra logging for troubleshooting purposes", _ => debug = true, true },
@@ -95,6 +98,12 @@ static int Run(string[] args)
return ShowError("Can only specify one config location.");
}
+ if (file != null && !File.Exists(file))
+ return ShowError($"Specified config file {file} not found.");
+
+ // --file overrides --path since it's more specific.
+ path = file ?? path ?? Directory.GetCurrentDirectory();
+
ConfigLevel? level = null;
Config config;
if (useGlobal)
From c0d76dbff31b3cee031b06dc8b5e2c718ff71d58 Mon Sep 17 00:00:00 2001
From: Daniel Cazzulino
Date: Tue, 25 Jun 2024 14:54:58 -0300
Subject: [PATCH 09/13] Improve docs, add package readme for extensions
---
readme.md | 5 ++++
src/CommandLine/CommandLine.csproj | 5 +---
src/Config.Tool/Config.Tool.csproj | 37 ++------------------------
src/Config/Config.csproj | 10 +------
src/Config/readme.md | 3 +++
src/Configuration/Configuration.csproj | 11 +++++---
src/Configuration/readme.md | 3 +++
7 files changed, 22 insertions(+), 52 deletions(-)
create mode 100644 src/Config/readme.md
create mode 100644 src/Configuration/readme.md
diff --git a/readme.md b/readme.md
index ff540e5..109c1d0 100644
--- a/readme.md
+++ b/readme.md
@@ -21,6 +21,7 @@
CLI
+
# Why
`dotnet-config` (or `.netconfig`) provides a uniform mechanism for
@@ -339,6 +340,7 @@ You can explore the entire API in the [docs site](https://dotnetconfig.org/api/)
PM> Install-Package DotNetConfig.Configuration
```
+
Usage (in this example, also chaining other providers):
```csharp
@@ -366,6 +368,8 @@ string port = config["serve:port"]; // == "8080";
string timeout = config["security:admin:timeout"]; // == "60";
```
+
+
### System.CommandLine
[](https://www.nuget.org/packages/DotNetConfig.CommandLine) [](https://www.nuget.org/packages/DotNetConfig.CommandLine)
@@ -543,4 +547,5 @@ all the following variants for arguments are supported: `-flag`, `--flag`, `/fla
`--flag value`, `/flag value`.
+
\ No newline at end of file
diff --git a/src/CommandLine/CommandLine.csproj b/src/CommandLine/CommandLine.csproj
index be505d5..2855a1a 100644
--- a/src/CommandLine/CommandLine.csproj
+++ b/src/CommandLine/CommandLine.csproj
@@ -35,10 +35,7 @@ The following heuristics are applied when providing default values:
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/src/Config.Tool/Config.Tool.csproj b/src/Config.Tool/Config.Tool.csproj
index 29bb93d..2093a67 100644
--- a/src/Config.Tool/Config.Tool.csproj
+++ b/src/Config.Tool/Config.Tool.csproj
@@ -1,37 +1,7 @@
- A global tool for managing hierarchical configurations for dotnet tools, using git config format.
-
-Usage: dotnet config [options]
-
-Location (uses all locations by default)
- --local use .netconfig.user file
- --global use global config file
- --system use system config file
- --path[=VALUE] use given config file or directory
-
-Action
- --get get value: name [value-regex]
- --get-all get all values: key [value-regex]
- --get-regexp get values for regexp: name-regex [value-regex]
- --set set value: name value [value-regex]
- --set-all set all matches: name value [value-regex]
- --add add a new variable: name value
- --unset remove a variable: name [value-regex]
- --unset-all remove all matches: name [value-regex]
- --remove-section remove a section: name
- --rename-section rename section: old-name new-name
- -l, --list list all
- -e, --edit edit the config file in an editor
-
-Other
- --default[=VALUE] with --get, use default value when missing entry
- --name-only show variable names only
- --type[=VALUE] value is given this type, can be 'boolean', '
- datetime' or 'number'
- -?, -h, --help Display this help
-
+ A global tool for managing hierarchical configurations for dotnet tools, using git config format.
Exe
net6.0;net8.0
@@ -52,10 +22,7 @@ Other
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/src/Config/Config.csproj b/src/Config/Config.csproj
index 9c5c84b..1f243e1 100644
--- a/src/Config/Config.csproj
+++ b/src/Config/Config.csproj
@@ -20,7 +20,6 @@ Usage:
true
true
true
- readme.md
@@ -33,18 +32,11 @@ Usage:
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
-
-
-
-
diff --git a/src/Config/readme.md b/src/Config/readme.md
new file mode 100644
index 0000000..2bf6101
--- /dev/null
+++ b/src/Config/readme.md
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/src/Configuration/Configuration.csproj b/src/Configuration/Configuration.csproj
index 2166517..c32bc37 100644
--- a/src/Configuration/Configuration.csproj
+++ b/src/Configuration/Configuration.csproj
@@ -21,14 +21,17 @@ Note: section is required and subsection is optional, just like in dotnet-config
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
+
+
+ %(Filename)%(Extension)
+
+
+
diff --git a/src/Configuration/readme.md b/src/Configuration/readme.md
new file mode 100644
index 0000000..7c486fd
--- /dev/null
+++ b/src/Configuration/readme.md
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
From d6bd67810157e8483dc3963e2fb9275b1a14b66b Mon Sep 17 00:00:00 2001
From: kzu
Date: Tue, 25 Jun 2024 17:58:54 +0000
Subject: [PATCH 10/13] =?UTF-8?q?=F0=9F=96=89=20Update=20changelog=20with?=
=?UTF-8?q?=20v1.1.1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
changelog.md | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/changelog.md b/changelog.md
index cb05fc6..8b10f3e 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,18 @@
# Changelog
+## [v1.1.1](https://github.com/dotnetconfig/dotnet-config/tree/v1.1.1) (2024-06-25)
+
+[Full Changelog](https://github.com/dotnetconfig/dotnet-config/compare/v1.1.0...v1.1.1)
+
+:sparkles: Implemented enhancements:
+
+- Improve docs, add package readme for extensions [\#152](https://github.com/dotnetconfig/dotnet-config/pull/152) (@kzu)
+- Make options and note high compat level with git config [\#151](https://github.com/dotnetconfig/dotnet-config/pull/151) (@kzu)
+
+:bug: Fixed bugs:
+
+- DotNetConfig.CommandLine not compatible with latest prerelease of System.Commandline [\#105](https://github.com/dotnetconfig/dotnet-config/issues/105)
+
## [v1.1.0](https://github.com/dotnetconfig/dotnet-config/tree/v1.1.0) (2024-06-25)
[Full Changelog](https://github.com/dotnetconfig/dotnet-config/compare/v1.0.6...v1.1.0)
From 24bd9714e7ee11725ee3bcf59d00fd21958664c2 Mon Sep 17 00:00:00 2001
From: Daniel Cazzulino
Date: Tue, 25 Jun 2024 15:12:31 -0300
Subject: [PATCH 11/13] Add test that ensures current tab-based behavior
To ensure compatibility with git config.
Closes #122
---
src/Config.Tests/ConfigTests.cs | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/src/Config.Tests/ConfigTests.cs b/src/Config.Tests/ConfigTests.cs
index a3a23ee..bc910d5 100644
--- a/src/Config.Tests/ConfigTests.cs
+++ b/src/Config.Tests/ConfigTests.cs
@@ -249,6 +249,19 @@ public void can_roundtrip()
Assert.Equal("bar", value);
}
+ [Fact]
+ public void when_setting_variable_then_uses_tab()
+ {
+ var file = Path.GetTempFileName();
+ var config = Config.Build(file);
+
+ config.SetString("section", "subsection", "foo", "bar");
+
+ var line = File.ReadAllLines(file).SkipWhile(x => x[0] == '#' || x[0] == '[').First();
+
+ Assert.Equal('\t', line[0]);
+ }
+
[Fact]
public void when_setting_global_variable_then_writes_global_file()
{
From 5bf47f78893133c09e5fed0885d65774efc5452f Mon Sep 17 00:00:00 2001
From: Daniel Cazzulino
Date: Sun, 7 Jul 2024 18:19:11 -0300
Subject: [PATCH 12/13] Drop immutability which doesn't add any value
Config writing is such a rare occurrence (compared with reading) that the additional complexity of making the config/document immutable doesn't bring much value. Quite the contrary, it's a source of hard to detect bugs.
The pattern of returning the same object for chaining is preserved (so the API is backs-compat), and the mutability is compatible with how ServiceCollection works, for example. The chaining is just for convenience.
---
docs/api/index.md | 5 +-
readme.md | 5 +-
src/Config.Tests/ConfigTests.cs | 15 ++++
src/Config/Config.cs | 6 +-
src/Config/Config.csproj | 4 +-
src/Config/ConfigDocument.cs | 28 +++++---
src/Config/FileConfig.cs | 121 +++++++++++++++++---------------
7 files changed, 105 insertions(+), 79 deletions(-)
diff --git a/docs/api/index.md b/docs/api/index.md
index 9b1b0f9..bd4acc1 100644
--- a/docs/api/index.md
+++ b/docs/api/index.md
@@ -96,12 +96,9 @@ configuration level to use for persisting the value, by passing a `ConfigLevel`:
//[vs "alias"]
// comexp = run|community|exp
-config = config.AddString("vs", "alias", "comexp", "run|community|exp", ConfigLevel.Global);
+config.AddString("vs", "alias", "comexp", "run|community|exp", ConfigLevel.Global);
```
-> IMPORTANT: the Config API is immutable, so if you make changes, you should update your reference
-> to the newly updated Config, otherwise, subsequent changes would override prior ones.
-
You can explore the entire API in the [docs site](https://dotnetconfig.org/api/).
### Microsoft.Extensions.Configuration
diff --git a/readme.md b/readme.md
index 109c1d0..533f274 100644
--- a/readme.md
+++ b/readme.md
@@ -324,12 +324,9 @@ configuration level to use for persisting the value, by passing a `ConfigLevel`:
//[vs "alias"]
// comexp = run|community|exp
-config = config.AddString("vs", "alias", "comexp", "run|community|exp", ConfigLevel.Global);
+config.AddString("vs", "alias", "comexp", "run|community|exp", ConfigLevel.Global);
```
-> IMPORTANT: the Config API is immutable, so if you make changes, you should update your reference
-> to the newly updated Config, otherwise, subsequent changes would override prior ones.
-
You can explore the entire API in the [docs site](https://dotnetconfig.org/api/).
### Microsoft.Extensions.Configuration
diff --git a/src/Config.Tests/ConfigTests.cs b/src/Config.Tests/ConfigTests.cs
index bc910d5..669c54b 100644
--- a/src/Config.Tests/ConfigTests.cs
+++ b/src/Config.Tests/ConfigTests.cs
@@ -262,6 +262,21 @@ public void when_setting_variable_then_uses_tab()
Assert.Equal('\t', line[0]);
}
+ [Fact]
+ public void when_setting_muliplevariables_then_can_reuse_instance()
+ {
+ var file = Path.GetTempFileName();
+ var config = Config.Build(file);
+
+ config.SetString("section", "subsection", "foo", "bar");
+ config.SetString("section", "subsection", "bar", "baz");
+
+ var saved = Config.Build(file);
+
+ Assert.Equal("bar", saved.GetString("section", "subsection", "foo"));
+ Assert.Equal("baz", saved.GetString("section", "subsection", "bar"));
+ }
+
[Fact]
public void when_setting_global_variable_then_writes_global_file()
{
diff --git a/src/Config/Config.cs b/src/Config/Config.cs
index 4e00dbf..627da43 100644
--- a/src/Config/Config.cs
+++ b/src/Config/Config.cs
@@ -159,9 +159,9 @@ public static Config Build(ConfigLevel store) =>
///
public ConfigLevel? Level =>
this is AggregateConfig ? null :
- FilePath == GlobalLocation ? (ConfigLevel?)ConfigLevel.Global :
- FilePath == SystemLocation ? (ConfigLevel?)ConfigLevel.System :
- FilePath.EndsWith(UserExtension) ? (ConfigLevel?)ConfigLevel.Local : null;
+ FilePath == GlobalLocation ? ConfigLevel.Global :
+ FilePath == SystemLocation ? ConfigLevel.System :
+ FilePath.EndsWith(UserExtension) ? ConfigLevel.Local : null;
///
/// Gets the section and optional subsection from the configuration.
diff --git a/src/Config/Config.csproj b/src/Config/Config.csproj
index 1f243e1..23842e5 100644
--- a/src/Config/Config.csproj
+++ b/src/Config/Config.csproj
@@ -7,8 +7,8 @@ Usage:
var config = Config.Build();
var value = config.GetString("section", "subsection", "variable");
- // Setting values, Config is immutable, so chain calls and update var
- config = config
+ // Setting values
+ config
.SetString("section", "subsection", "variable", value)
.SetBoolean("section", "subsection", "enabled", true);
diff --git a/src/Config/ConfigDocument.cs b/src/Config/ConfigDocument.cs
index 9c92551..7870c27 100644
--- a/src/Config/ConfigDocument.cs
+++ b/src/Config/ConfigDocument.cs
@@ -38,7 +38,7 @@ record ConfigDocument : IEnumerable
public ConfigLevel? Level { get; }
- internal ImmutableList Lines { get; init; } = ImmutableList.Empty;
+ internal ImmutableList Lines { get; private set; } = ImmutableList.Empty;
public ConfigDocument Save()
{
@@ -99,7 +99,8 @@ public ConfigDocument Add(string section, string? subsection, string name, strin
lines = lines.Insert(index, Line.CreateVariable(filePath, index, sectionLine.Section, sectionLine.Subsection, name, value));
}
- return this with { Lines = lines };
+ Lines = lines;
+ return this;
}
public ConfigDocument Set(string section, string? subsection, string name, string? value = null, ValueMatcher? valueMatcher = null)
@@ -119,7 +120,8 @@ public ConfigDocument Set(string section, string? subsection, string name, strin
if (variable != null)
{
- return this with { Lines = Lines.Replace(variable, variable.WithValue(value)) };
+ Lines = Lines.Replace(variable, variable.WithValue(value));
+ return this;
}
else
{
@@ -143,7 +145,9 @@ public ConfigDocument Unset(string section, string? subsection, string name)
var variable = variables.FirstOrDefault();
if (variable != null)
{
- return (this with { Lines = Lines.Remove(variable) }).CleanupSection(section, subsection);
+ Lines = Lines.Remove(variable);
+ CleanupSection(section, subsection);
+ return this;
}
return this;
@@ -164,7 +168,8 @@ public ConfigDocument SetAll(string section, string? subsection, string name, st
lines = lines.Replace(variable, variable.WithValue(value));
}
- return this with { Lines = lines };
+ Lines = lines;
+ return this;
}
public ConfigDocument UnsetAll(string section, string? subsection, string name, ValueMatcher? valueMatcher = null)
@@ -185,7 +190,9 @@ public ConfigDocument UnsetAll(string section, string? subsection, string name,
lines = lines.Remove(variable);
}
- return (this with { Lines = lines }).CleanupSection(section, subsection);
+ Lines = lines;
+ CleanupSection(section, subsection);
+ return this;
}
public ConfigDocument RemoveSection(string section, string? subsection = null)
@@ -216,7 +223,8 @@ public ConfigDocument RemoveSection(string section, string? subsection = null)
while (lines.Count > 0 && lines[^1].Kind == LineKind.None)
lines = lines.RemoveAt(lines.Count - 1);
- return this with { Lines = lines };
+ Lines = lines;
+ return this;
}
public ConfigDocument RenameSection(string oldSection, string? oldSubsection, string newSection, string? newSubsection)
@@ -246,7 +254,8 @@ public ConfigDocument RenameSection(string oldSection, string? oldSubsection, st
}
}
- return this with { Lines = lines };
+ Lines = lines;
+ return this;
}
///
@@ -266,7 +275,8 @@ ConfigDocument CleanupSection(string section, string? subsection)
while (index < lines.Count && lines[index].Kind == LineKind.None)
lines = lines.RemoveAt(index);
- return this with { Lines = lines };
+ Lines = lines;
+ return this;
}
}
diff --git a/src/Config/FileConfig.cs b/src/Config/FileConfig.cs
index a167566..6d09378 100644
--- a/src/Config/FileConfig.cs
+++ b/src/Config/FileConfig.cs
@@ -20,32 +20,33 @@ public override Config AddBoolean(string section, string? subsection, string var
if (value)
{
// Shortcut notation.
- return new FileConfig(FilePath,
- document.Add(section, subsection, variable, null)
- .Save());
+ document.Add(section, subsection, variable, null).Save();
}
else
{
- return new FileConfig(FilePath,
- document.Add(section, subsection, variable, "false")
- .Save());
+ document.Add(section, subsection, variable, "false").Save();
}
+
+ return this;
}
public override Config AddDateTime(string section, string? subsection, string variable, DateTime value)
- => new FileConfig(FilePath, document
- .Add(section, subsection, variable, value.ToString("O"))
- .Save());
+ {
+ document.Add(section, subsection, variable, value.ToString("O")).Save();
+ return this;
+ }
public override Config AddNumber(string section, string? subsection, string variable, long value)
- => new FileConfig(FilePath, document
- .Add(section, subsection, variable, value.ToString())
- .Save());
+ {
+ document.Add(section, subsection, variable, value.ToString()).Save();
+ return this;
+ }
public override Config AddString(string section, string? subsection, string variable, string value)
- => new FileConfig(FilePath, document
- .Add(section, subsection, variable, value)
- .Save());
+ {
+ document.Add(section, subsection, variable, value).Save();
+ return this;
+ }
public override IEnumerable GetAll(string section, string? subsection, string variable, string? valueRegex)
=> document.GetAll(section, subsection, variable, valueRegex);
@@ -69,78 +70,82 @@ public override IEnumerable GetRegex(string nameRegex, string? valu
}
public override Config RemoveSection(string section, string? subsection)
- => new FileConfig(FilePath, document
- .RemoveSection(section, subsection)
- .Save());
+ {
+ document.RemoveSection(section, subsection).Save();
+ return this;
+ }
public override Config RenameSection(string oldSection, string? oldSubsection, string newSection, string? newSubsection)
- => new FileConfig(FilePath, document
- .RenameSection(oldSection, oldSubsection, newSection, newSubsection)
- .Save());
+ {
+ document.RenameSection(oldSection, oldSubsection, newSection, newSubsection).Save();
+ return this;
+ }
public override Config SetAllBoolean(string section, string? subsection, string variable, bool value, string? valueRegex)
{
if (value)
{
// Shortcut notation.
- return new FileConfig(FilePath, document
- .SetAll(section, subsection, variable, null, valueRegex)
- .Save());
+ document.SetAll(section, subsection, variable, null, valueRegex).Save();
}
else
{
- return new FileConfig(FilePath, document
- .SetAll(section, subsection, variable, "false", valueRegex)
- .Save());
+ document.SetAll(section, subsection, variable, "false", valueRegex).Save();
}
+
+ return this;
}
public override Config SetAllDateTime(string section, string? subsection, string variable, DateTime value, string? valueRegex)
- => new FileConfig(FilePath, document
- .SetAll(section, subsection, variable, value.ToString("O"), valueRegex)
- .Save());
+ {
+ document.SetAll(section, subsection, variable, value.ToString("O"), valueRegex).Save();
+ return this;
+ }
public override Config SetAllNumber(string section, string? subsection, string variable, long value, string? valueRegex)
- => new FileConfig(FilePath, document
- .SetAll(section, subsection, variable, value.ToString(), valueRegex)
- .Save());
+ {
+ document.SetAll(section, subsection, variable, value.ToString(), valueRegex).Save();
+ return this;
+ }
public override Config SetAllString(string section, string? subsection, string variable, string value, string? valueRegex)
- => new FileConfig(FilePath, document
- .SetAll(section, subsection, variable, value, valueRegex)
- .Save());
+ {
+ document.SetAll(section, subsection, variable, value, valueRegex).Save();
+ return this;
+ }
public override Config SetBoolean(string section, string? subsection, string variable, bool value, string? valueRegex)
{
if (value)
{
// Shortcut notation.
- return new FileConfig(FilePath, document
- .Set(section, subsection, variable, null, valueRegex)
- .Save());
+ document.Set(section, subsection, variable, null, valueRegex).Save();
}
else
{
- return new FileConfig(FilePath, document
- .Set(section, subsection, variable, "false", valueRegex)
- .Save());
+ document.Set(section, subsection, variable, "false", valueRegex).Save();
}
+
+ return this;
}
public override Config SetDateTime(string section, string? subsection, string variable, DateTime value, string? valueRegex)
- => new FileConfig(FilePath, document
- .Set(section, subsection, variable, value.ToString("O"), valueRegex)
- .Save());
+ {
+ document.Set(section, subsection, variable, value.ToString("O"), valueRegex).Save();
+ return this;
+ }
public override Config SetNumber(string section, string? subsection, string variable, long value, string? valueRegex)
- => new FileConfig(FilePath, document
- .Set(section, subsection, variable, value.ToString(), valueRegex)
- .Save());
+ {
+ document.Set(section, subsection, variable, value.ToString(), valueRegex).Save();
+ return this;
+ }
public override Config SetString(string section, string? subsection, string variable, string value, string? valueRegex)
- => new FileConfig(FilePath, document
- .Set(section, subsection, variable, value, valueRegex)
- .Save());
+ {
+ document.Set(section, subsection, variable, value, valueRegex).Save();
+ return this;
+ }
public override bool TryGetBoolean(string section, string? subsection, string variable, out bool value)
{
@@ -211,14 +216,16 @@ public override bool TryGetString(string section, string? subsection, string var
}
public override Config Unset(string section, string? subsection, string variable)
- => new FileConfig(FilePath, document
- .Unset(section, subsection, variable)
- .Save());
+ {
+ document.Unset(section, subsection, variable).Save();
+ return this;
+ }
public override Config UnsetAll(string section, string? subsection, string variable, string? valueMatcher)
- => new FileConfig(FilePath, document
- .UnsetAll(section, subsection, variable, valueMatcher)
- .Save());
+ {
+ document.UnsetAll(section, subsection, variable, valueMatcher).Save();
+ return this;
+ }
protected override IEnumerable GetEntries() => document;
From a33573daafc4168d147e5730323e5f3e589570b2 Mon Sep 17 00:00:00 2001
From: kzu
Date: Sun, 7 Jul 2024 21:36:59 +0000
Subject: [PATCH 13/13] =?UTF-8?q?=F0=9F=96=89=20Update=20changelog=20with?=
=?UTF-8?q?=20main?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
changelog.md | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/changelog.md b/changelog.md
index 8b10f3e..fb92fb1 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,21 @@
# Changelog
+## [v1.2.0](https://github.com/dotnetconfig/dotnet-config/tree/v1.2.0) (2024-07-07)
+
+[Full Changelog](https://github.com/dotnetconfig/dotnet-config/compare/v1.1.1...v1.2.0)
+
+:sparkles: Implemented enhancements:
+
+- Drop immutability which doesn't add any value [\#155](https://github.com/dotnetconfig/dotnet-config/pull/155) (@kzu)
+
+:hammer: Other:
+
+- Tabs vs spaces [\#122](https://github.com/dotnetconfig/dotnet-config/issues/122)
+
+:twisted_rightwards_arrows: Merged:
+
+- Add test that ensures current tab-based behavior [\#153](https://github.com/dotnetconfig/dotnet-config/pull/153) (@kzu)
+
## [v1.1.1](https://github.com/dotnetconfig/dotnet-config/tree/v1.1.1) (2024-06-25)
[Full Changelog](https://github.com/dotnetconfig/dotnet-config/compare/v1.1.0...v1.1.1)