diff --git a/.editorconfig b/.editorconfig index e17d14e..4cab270 100644 --- a/.editorconfig +++ b/.editorconfig @@ -107,6 +107,3 @@ dotnet_analyzer_diagnostic.category-Style.severity = none # VSTHRD200: Use "Async" suffix for async methods dotnet_diagnostic.VSTHRD200.severity = none - -[**/*SponsorLink*/**] -generated_code = true \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c9a0364..de732f2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,25 +4,31 @@ name: build on: workflow_dispatch: + inputs: + configuration: + type: choice + description: Configuration + options: + - Release + - Debug push: branches: [ main, dev, 'dev/*', 'feature/*', 'rel/*' ] paths-ignore: - changelog.md - - code-of-conduct.md - - security.md - - support.md - readme.md pull_request: types: [opened, synchronize, reopened] env: DOTNET_NOLOGO: true - VersionPrefix: 42.42.${{ github.run_number }} - VersionLabel: ${{ github.ref }} PackOnBuild: true GeneratePackageOnBuild: true + VersionPrefix: 42.42.${{ github.run_number }} + VersionLabel: ${{ github.ref }} GH_TOKEN: ${{ secrets.GH_TOKEN }} - + MSBUILDTERMINALLOGGER: auto + Configuration: ${{ github.event.inputs.configuration || 'Release' }} + defaults: run: shell: bash diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1c2833b..82ef81b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,15 +5,17 @@ name: publish on: release: - types: [released] + types: [prereleased, released] env: DOTNET_NOLOGO: true Configuration: Release PackOnBuild: true GeneratePackageOnBuild: true + VersionLabel: ${{ github.ref }} GH_TOKEN: ${{ secrets.GH_TOKEN }} - + MSBUILDTERMINALLOGGER: auto + jobs: publish: runs-on: ubuntu-latest @@ -25,7 +27,7 @@ jobs: fetch-depth: 0 - name: 🙏 build - run: dotnet build -m:1 -p:version=${GITHUB_REF#refs/*/v} -bl:build.binlog + run: dotnet build -m:1 -bl:build.binlog - name: 🧪 test uses: ./.github/workflows/test @@ -38,4 +40,15 @@ jobs: path: '*.binlog' - name: 🚀 nuget + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + if: env.NUGET_API_KEY != '' run: dotnet nuget push ./bin/**/*.nupkg -s https://api.nuget.org/v3/index.json -k ${{secrets.NUGET_API_KEY}} --skip-duplicate + + - name: 🚀 sleet + env: + SLEET_CONNECTION: ${{ secrets.SLEET_CONNECTION }} + if: env.SLEET_CONNECTION != '' + run: | + dotnet tool install -g --version 4.0.18 sleet + sleet push bin --config none -f --verbose -p "SLEET_FEED_CONTAINER=nuget" -p "SLEET_FEED_CONNECTIONSTRING=${{ secrets.SLEET_CONNECTION }}" -p "SLEET_FEED_TYPE=azure" || echo "No packages found" \ No newline at end of file diff --git a/.github/workflows/sponsor.yml b/.github/workflows/sponsor.yml deleted file mode 100644 index 1d484d3..0000000 --- a/.github/workflows/sponsor.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: sponsor 💜 -on: - issues: - types: [opened, edited, reopened] - pull_request: - types: [opened, edited, synchronize, reopened] - -jobs: - sponsor: - runs-on: ubuntu-latest - continue-on-error: true - env: - token: ${{ secrets.GH_TOKEN }} - if: ${{ !endsWith(github.event.sender.login, '[bot]') && !endsWith(github.event.sender.login, 'bot') }} - steps: - - name: 🤘 checkout - if: env.token != '' - uses: actions/checkout@v4 - - - name: 💜 sponsor - if: env.token != '' - uses: devlooped/actions-sponsor@main - with: - token: ${{ env.token }} diff --git a/.netconfig b/.netconfig index bc055fd..7abe85f 100644 --- a/.netconfig +++ b/.netconfig @@ -15,8 +15,8 @@ skip [file ".editorconfig"] url = https://github.com/devlooped/oss/blob/main/.editorconfig - sha = f571a42eac3cad554810dad15139ff390db5e1db - etag = ba2655b8b3ce5491b1c0eea5e0af201a085c48e07542bb9ec2c928084944ea86 + sha = e81ab754b366d52d92bd69b24bef1d5b1c610634 + etag = 7298c6450967975a8782b5c74f3071e1910fc59686e48f9c9d5cd7c68213cf59 weak [file ".gitattributes"] url = https://github.com/devlooped/oss/blob/main/.gitattributes @@ -30,8 +30,8 @@ weak [file ".github/workflows/build.yml"] url = https://github.com/devlooped/oss/blob/main/.github/workflows/build.yml - sha = 5fb172362c767bef7c36478f1a6bdc264723f8f9 - etag = 6efc7d096b25bb4bbeffe7960a1194f1ceb5d21abeda85d28b55594b648ab44a + sha = 65f89e06ed7b2ac33ed3b729adc865b7076840d8 + etag = 20ef46203325f5b3d06a991f8b38c24bcea49fd74d39062a62003448e2dd8b0a weak [file ".github/workflows/changelog.yml"] url = https://github.com/devlooped/oss/blob/main/.github/workflows/changelog.yml @@ -45,8 +45,8 @@ weak [file ".github/workflows/publish.yml"] url = https://github.com/devlooped/oss/blob/main/.github/workflows/publish.yml - sha = 5fb172362c767bef7c36478f1a6bdc264723f8f9 - etag = 9ea4083894308a610742488923d2a44778ebba6ca73fb13424647d9a82c918b4 + sha = 40ee833a83d843c242914599e2635ca9a05aa837 + etag = 105db682c332b2bb0873e60a7d2da45b00fe7d83da0f0e7dbf9a84bc5be77c25 weak [file ".gitignore"] url = https://github.com/devlooped/oss/blob/main/.gitignore @@ -75,13 +75,13 @@ weak [file "src/Directory.Build.props"] url = https://github.com/devlooped/oss/blob/main/src/Directory.Build.props - sha = 6e96c592c7b44bfda10404b9f90e4b8fab299249 - etag = a4925eb815bbcecc022de8d3245db069573d96ac5ecdf5f0e604f06b5577b01e + sha = b76de49afb376aa48eb172963ed70663b59b31d3 + etag = c8b56f3860cc7ccb8773b7bd6189f5c7a6e3a2c27e9104c1ee201fbdc5af9873 weak [file "src/Directory.Build.targets"] url = https://github.com/devlooped/oss/blob/main/src/Directory.Build.targets - sha = c618ea86d94402a12c7d7d10fe2b5cb8a21c3eea - etag = 7cb1421f00d9f6f4c00f0ca98e485dcadb927cfa6b3f0b5d4fb212525d2ce9c0 + sha = 33a20db26e47589769284817b271ce67ea9ccfd8 + etag = 1a3a0151b5771ee97ed8351254ff4c18a0ff568e0df5c33c6830f069bfbb067b weak [file "src/kzu.snk"] url = https://github.com/devlooped/oss/blob/main/src/kzu.snk @@ -95,8 +95,8 @@ weak [file "Gemfile"] url = https://github.com/clarius/pages/blob/main/Gemfile - sha = 565a77f40db0863cb47ceb36f88790259a697c91 - etag = 24e482e91192e292b633e3c17c4f095286ffb5a041d299d761b2e6ef99ee7669 + sha = 90fa16ed0e7300a78a38ee1d23c34a7e875aab27 + etag = 3dd7febc8ae6760f19abfe787711f469c288cd803a6f1c545edec34264d48e71 weak [file ".github/workflows/includes.yml"] url = https://github.com/devlooped/oss/blob/main/.github/workflows/includes.yml @@ -116,11 +116,6 @@ sha = c1610886eba42cb250e3894aed40c0a258cd383d etag = 598ee294649a44d4c5d5934416c01183597d08aa7db7938453fd2bbf52a4e64d weak -[file ".github/workflows/sponsor.yml"] - url = https://github.com/devlooped/oss/blob/main/.github/workflows/sponsor.yml - sha = 5fb172362c767bef7c36478f1a6bdc264723f8f9 - etag = 0849ee61af6daee29615f9632173b4e82da5bfa9d78ff28907e9408bd5acde4d - weak [file ".github/release.yml"] url = https://github.com/devlooped/oss/blob/main/.github/release.yml sha = 1afd173fe8f81b510c597737b0d271218e81fa73 @@ -131,178 +126,3 @@ sha = 055a8b7c94b74ae139cce919d60b83976d2a9942 etag = ddb17acb5872e9e69a76f9dec0ca590f25382caa2ccf750df058dcabb674db2b weak -[file "src/SponsorLink/Analyzer/Analyzer.csproj"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Analyzer/Analyzer.csproj - sha = 7cda4a18313b0b38b26c0152e1007cdbb9b6ba3a - etag = d9444fa36daa8f4ff8f06fc2f9f600dbd8032f25ff58542d3b96676e0305677e - 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 = a0ae7272f31c766ebb129ea38c11c01df93b6b5d - etag = 23d4cd16294974d85164fc26d6a7e2ae52698f23a62463db5025d69d9c166dc5 - 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 = a0ae7272f31c766ebb129ea38c11c01df93b6b5d - etag = 6823e1e914ecedd174276e3d53517cc0b332bb47c56402a9512cfa6aeeeb067e - 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 = a0ae7272f31c766ebb129ea38c11c01df93b6b5d - etag = 3720f8ae0605aa64df8f6c1d9769969162175b79c93a21024653f210a42348e6 - 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/SponsorLink.targets"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink.targets - sha = 7cda4a18313b0b38b26c0152e1007cdbb9b6ba3a - etag = d725bd9cfa33f35224e91748f64237e4dc66270f7e5ec7c835b78164531ae3db - 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 = b2a11faac6c1c300bce8c1d45f95b585c19f2953 - etag = 9f289f45169f35916fff1857840d4118ed134215639d6dae9016dc62004291a5 - 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/SponsorLink.cs"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/SponsorLink.cs - sha = 4fca946c3201d90d30e2183f699c850dcc1bf8d5 - etag = 96e1b1b28bfb2372bd5ffcc6bdef65ee926822b3489ce65be4e5a400884dce21 - weak -[file "src/SponsorLink/SponsorLink/SponsorLink.csproj"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/SponsorLink.csproj - sha = a0ae7272f31c766ebb129ea38c11c01df93b6b5d - etag = ffaea0b580d8dccd672e749a5efd11fda318c484ca4a34428ff81524ec80ec4b - weak -[file "src/SponsorLink/SponsorLink/SponsorLink.es.resx"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/SponsorLink.es.resx - sha = a0ae7272f31c766ebb129ea38c11c01df93b6b5d - etag = ded7de7a2624b335beb462763e3580413da21e80c8b40b4c773ca46c7af4e859 - weak -[file "src/SponsorLink/SponsorLink/SponsorLink.resx"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/SponsorLink.resx - sha = a0ae7272f31c766ebb129ea38c11c01df93b6b5d - etag = 7d9e89ef2cf762a6119c9c6c2ed2517b71a546838151c005400301fde8def266 - weak -[file "src/SponsorLink/SponsorLink/SponsorLinkAnalyzer.cs"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/SponsorLinkAnalyzer.cs - sha = b2a11faac6c1c300bce8c1d45f95b585c19f2953 - etag = fc96f7f5642cbf69b35b7e8de1756822580315f0cee61e47da3b2b1b03f68e1a - 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/ThisAssembly.cs"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/ThisAssembly.cs - sha = b2a11faac6c1c300bce8c1d45f95b585c19f2953 - etag = 978269025f58e2bae872af25fdfd94659e234e8365e3014c18b1b20fdcd155bf - 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 = a0ae7272f31c766ebb129ea38c11c01df93b6b5d - etag = 72ec691a085dc34f946627f7038a82569e44f0b63a9f4a7bd60f0f7b52fd198f - weak -[file "src/SponsorLink/SponsorLink/devlooped.pub.jwk"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/devlooped.pub.jwk - sha = a0ae7272f31c766ebb129ea38c11c01df93b6b5d - etag = cf884781ff88b4d096841e3169282762a898b2050c9b5dac0013bc15bdbee267 - 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/Sample.cs"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Tests/Sample.cs - sha = e732f6a2c44a2f7940a1868a92cd66523f74ed24 - etag = db968d1d665b77a17e13bc7ca3d43ea65ed05cbebc18669f1b607ebe0e38a59a - 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 = a0ae7272f31c766ebb129ea38c11c01df93b6b5d - etag = 237409e155202ec1b845593195d30057a949b2b18ae46a575e4cf480e4e2c8fe - 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/Gemfile b/Gemfile index ed99566..fd95539 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,3 @@ source 'https://rubygems.org' -gem 'github-pages', '~> 209', group: :jekyll_plugins +gem 'github-pages', '~> 231', group: :jekyll_plugins diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 50fc169..381c383 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -46,8 +46,6 @@ Release - true - false Latest @@ -118,6 +116,8 @@ <_VersionLabel>$(VersionLabel.Replace('refs/heads/', '')) + <_VersionLabel>$(_VersionLabel.Replace('refs/tags/v', '')) + <_VersionLabel Condition="$(_VersionLabel.Contains('refs/pull/'))">$(VersionLabel.TrimEnd('.0123456789')) @@ -128,7 +128,9 @@ <_VersionLabel>$(_VersionLabel.Replace('/', '-')) - $(_VersionLabel) + $(_VersionLabel) + + $(_VersionLabel) diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 0cb1e4e..20d7f0b 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -4,6 +4,13 @@ CI;$(DefineConstants) + + + + false + false + true + true diff --git a/src/SponsorLink/Analyzer/Analyzer.csproj b/src/SponsorLink/Analyzer/Analyzer.csproj deleted file mode 100644 index 963c77b..0000000 --- a/src/SponsorLink/Analyzer/Analyzer.csproj +++ /dev/null @@ -1,32 +0,0 @@ - - - - netstandard2.0 - true - analyzers/dotnet/roslyn4.0 - true - $(MSBuildThisFileDirectory)..\SponsorLink.targets - true - disable - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/SponsorLink/Analyzer/Properties/launchSettings.json b/src/SponsorLink/Analyzer/Properties/launchSettings.json deleted file mode 100644 index de45107..0000000 --- a/src/SponsorLink/Analyzer/Properties/launchSettings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "profiles": { - "SponsorableLib": { - "commandName": "DebugRoslynComponent", - "targetProject": "..\\Tests\\Tests.csproj", - "environmentVariables": { - "SPONSORLINK_TRACE": "true" - } - } - } -} \ No newline at end of file diff --git a/src/SponsorLink/Analyzer/StatusReportingAnalyzer.cs b/src/SponsorLink/Analyzer/StatusReportingAnalyzer.cs deleted file mode 100644 index e21acb7..0000000 --- a/src/SponsorLink/Analyzer/StatusReportingAnalyzer.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Collections.Immutable; -using Devlooped.Sponsors; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using static Devlooped.Sponsors.SponsorLink; -using static ThisAssembly.Constants; - -namespace Analyzer; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class StatusReportingAnalyzer : DiagnosticAnalyzer -{ - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Empty; - - public override void Initialize(AnalysisContext context) - { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - - context.RegisterCodeBlockAction(c => - { - var status = Diagnostics.GetStatus(Funding.Product); - Tracing.Trace($"Status: {status}"); - }); - } -} \ No newline at end of file diff --git a/src/SponsorLink/Analyzer/buildTransitive/SponsorableLib.targets b/src/SponsorLink/Analyzer/buildTransitive/SponsorableLib.targets deleted file mode 100644 index fd1e6e4..0000000 --- a/src/SponsorLink/Analyzer/buildTransitive/SponsorableLib.targets +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/src/SponsorLink/Directory.Build.props b/src/SponsorLink/Directory.Build.props deleted file mode 100644 index c0a3e42..0000000 --- a/src/SponsorLink/Directory.Build.props +++ /dev/null @@ -1,43 +0,0 @@ - - - - false - latest - true - annotations - true - - false - $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)bin')) - - https://pkg.kzu.app/index.json;https://api.nuget.org/v3/index.json - $(PackageOutputPath);$(RestoreSources) - - - 42.42.$([System.Math]::Floor($([MSBuild]::Divide($([System.DateTime]::Now.TimeOfDay.TotalSeconds), 10)))) - - SponsorableLib - - - - - - - - - - - - diff --git a/src/SponsorLink/Directory.Build.targets b/src/SponsorLink/Directory.Build.targets deleted file mode 100644 index 4ce4c80..0000000 --- a/src/SponsorLink/Directory.Build.targets +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/SponsorLink/Library/Library.csproj b/src/SponsorLink/Library/Library.csproj deleted file mode 100644 index f351273..0000000 --- a/src/SponsorLink/Library/Library.csproj +++ /dev/null @@ -1,31 +0,0 @@ - - - - netstandard2.0 - true - SponsorableLib - Sample library incorporating SponsorLink checks - true - - - - - - - - - - - - - - MSBuild:Compile - $(IntermediateOutputPath)\$([MSBuild]::ValueOrDefault('%(RelativeDir)', '').Replace('\', '.').Replace('/', '.'))%(Filename).g$(DefaultLanguageSourceExtension) - $(Language) - $(RootNamespace) - $(RootNamespace).$([MSBuild]::ValueOrDefault('%(RelativeDir)', '').Replace('\', '.').Replace('/', '.').TrimEnd('.')) - %(Filename) - - - - diff --git a/src/SponsorLink/Library/MyClass.cs b/src/SponsorLink/Library/MyClass.cs deleted file mode 100644 index 7b7f6f5..0000000 --- a/src/SponsorLink/Library/MyClass.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace SponsorableLib; - -public class MyClass -{ -} diff --git a/src/SponsorLink/Library/Resources.resx b/src/SponsorLink/Library/Resources.resx deleted file mode 100644 index 636fedc..0000000 --- a/src/SponsorLink/Library/Resources.resx +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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/SponsorLink.targets b/src/SponsorLink/SponsorLink.targets deleted file mode 100644 index de93845..0000000 --- a/src/SponsorLink/SponsorLink.targets +++ /dev/null @@ -1,141 +0,0 @@ - - - - - - - true - - true - - true - - - $([System.IO.File]::ReadAllText('$(MSBuildThisFileDirectory)SponsorLink/devlooped.pub.jwk')) - - - $(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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - $([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)"', ' ') - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/SponsorLink/SponsorLink/AppDomainDictionary.cs b/src/SponsorLink/SponsorLink/AppDomainDictionary.cs deleted file mode 100644 index 05cc949..0000000 --- a/src/SponsorLink/SponsorLink/AppDomainDictionary.cs +++ /dev/null @@ -1,36 +0,0 @@ -// -#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 deleted file mode 100644 index 49143d9..0000000 --- a/src/SponsorLink/SponsorLink/DiagnosticsManager.cs +++ /dev/null @@ -1,138 +0,0 @@ -// -#nullable enable -using System; -using System.Collections.Concurrent; -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 - { - get => 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", - ThisAssembly.Strings.Sponsor.Title, - ThisAssembly.Strings.Sponsor.MessageFormat, - "SponsorLink", - DiagnosticSeverity.Info, - isEnabledByDefault: true, - description: ThisAssembly.Strings.Sponsor.Description, - helpLinkUri: "https://github.com/devlooped#sponsorlink", - "DoesNotSupportF1Help"); - - static DiagnosticDescriptor CreateUnknown(string[] sponsorable, string product, string prefix) => new( - $"{prefix}101", - ThisAssembly.Strings.Unknown.Title, - ThisAssembly.Strings.Unknown.MessageFormat, - "SponsorLink", - DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: ThisAssembly.Strings.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", - ThisAssembly.Strings.Expiring.Title, - ThisAssembly.Strings.Expiring.MessageFormat, - "SponsorLink", - DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: ThisAssembly.Strings.Expiring.Description(string.Join(" ", sponsorable)), - helpLinkUri: "https://github.com/devlooped#autosync", - "DoesNotSupportF1Help", WellKnownDiagnosticTags.NotConfigurable); - - static DiagnosticDescriptor CreateExpired(string[] sponsorable, string prefix) => new( - $"{prefix}104", - ThisAssembly.Strings.Expired.Title, - ThisAssembly.Strings.Expired.MessageFormat, - "SponsorLink", - DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: ThisAssembly.Strings.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 deleted file mode 100644 index 0960e5a..0000000 --- a/src/SponsorLink/SponsorLink/ManifestStatus.cs +++ /dev/null @@ -1,25 +0,0 @@ -// -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/SponsorLink.cs b/src/SponsorLink/SponsorLink/SponsorLink.cs deleted file mode 100644 index a5e5beb..0000000 --- a/src/SponsorLink/SponsorLink/SponsorLink.cs +++ /dev/null @@ -1,169 +0,0 @@ -// -#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.jwk) || 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 deleted file mode 100644 index 4b00feb..0000000 --- a/src/SponsorLink/SponsorLink/SponsorLink.csproj +++ /dev/null @@ -1,46 +0,0 @@ - - - - netstandard2.0 - SponsorLink - disable - false - - - - $([System.IO.File]::ReadAllText('$(MSBuildThisFileDirectory)devlooped.pub.jwk')) - - $(Product) - - $([System.Text.RegularExpressions.Regex]::Replace("$(FundingProduct)", "[^A-Z]", "")) - - 21 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/SponsorLink/SponsorLink/SponsorLink.es.resx b/src/SponsorLink/SponsorLink/SponsorLink.es.resx deleted file mode 100644 index d8794ca..0000000 --- a/src/SponsorLink/SponsorLink/SponsorLink.es.resx +++ /dev/null @@ -1,163 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 {links} y ejecutando posteriormente 'gh sponsors sync {spaced}'. - - - No se pudo determinar el estado de su patrocinio. Funcionalidades exclusivas para patrocinadores pueden no estar disponibles. - - - Estado de patrocinio desconocido - - - Funcionalidades exclusivas para patrocinadores pueden no estar disponibles. Ejecuta 'gh sponsors sync {spaced}' 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 de {1} 💟! - - - 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 'gh sponsors sync {spaced}' 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/SponsorLink.resx b/src/SponsorLink/SponsorLink/SponsorLink.resx deleted file mode 100644 index b8cdd5e..0000000 --- a/src/SponsorLink/SponsorLink/SponsorLink.resx +++ /dev/null @@ -1,164 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 {links} and running 'gh sponsors sync {spaced}' afterwards. - Unknown sponsor description - - - Please consider supporting {0} by sponsoring {1} 🙏 - - - Unknown sponsor status - - - Sponsor-only features may be disabled. Please run 'gh sponsors sync {spaced}' 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 'gh sponsors sync {spaced}' 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/SponsorLinkAnalyzer.cs b/src/SponsorLink/SponsorLink/SponsorLinkAnalyzer.cs deleted file mode 100644 index 2e97528..0000000 --- a/src/SponsorLink/SponsorLink/SponsorLinkAnalyzer.cs +++ /dev/null @@ -1,126 +0,0 @@ -// -#nullable enable -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.IO; -using System.Linq; -using Humanizer; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using static Devlooped.Sponsors.SponsorLink; -using static ThisAssembly.Constants; - -namespace Devlooped.Sponsors; - -/// -/// Links the sponsor status for the current compilation. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] -public class SponsorLinkAnalyzer : DiagnosticAnalyzer -{ - static readonly int graceDays = int.Parse(Funding.Grace); - 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 => - { - if (Diagnostics.Pop(Funding.Product) is Diagnostic diagnostic) - { - ctx.ReportDiagnostic(diagnostic); - } - else - { - // This should never happen and would be a bug. - Debug.Assert(true, "We should have provided a diagnostic of some kind for " + Funding.Product); - // We'll report it as unknown as a fallback for now. - ctx.ReportDiagnostic(Diagnostic.Create(descriptors[SponsorStatus.Unknown], null, - properties: ImmutableDictionary.Create().Add(nameof(SponsorStatus), nameof(SponsorStatus.Unknown)), - Funding.Product, Sponsorables.Keys.Humanize(ThisAssembly.Strings.Or))); - } - }); - } - }); -#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(ThisAssembly.Strings.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(graceDays) < 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 deleted file mode 100644 index 6cdbc90..0000000 --- a/src/SponsorLink/SponsorLink/SponsorStatus.cs +++ /dev/null @@ -1,25 +0,0 @@ -// -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 deleted file mode 100644 index 8311ca6..0000000 --- a/src/SponsorLink/SponsorLink/SponsorableLib.targets +++ /dev/null @@ -1,60 +0,0 @@ - - - - - $([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/ThisAssembly.cs b/src/SponsorLink/SponsorLink/ThisAssembly.cs deleted file mode 100644 index 89f2316..0000000 --- a/src/SponsorLink/SponsorLink/ThisAssembly.cs +++ /dev/null @@ -1,31 +0,0 @@ -// -partial class ThisAssembly -{ - partial class Strings - { - partial class Unknown - { - public static string MessageFormat => GetResourceManager("Devlooped.SponsorLink").GetString("Unknown_Message"); - } - - partial class Expiring - { - public static string MessageFormat => GetResourceManager("Devlooped.SponsorLink").GetString("Expiring_Message"); - } - - partial class Expired - { - public static string MessageFormat => GetResourceManager("Devlooped.SponsorLink").GetString("Expired_Message"); - } - - partial class Grace - { - public static string MessageFormat => GetResourceManager("Devlooped.SponsorLink").GetString("Grace_Message"); - } - - partial class Sponsor - { - public static string MessageFormat => GetResourceManager("Devlooped.SponsorLink").GetString("Sponsor_Message"); - } - } -} \ No newline at end of file diff --git a/src/SponsorLink/SponsorLink/Tracing.cs b/src/SponsorLink/SponsorLink/Tracing.cs deleted file mode 100644 index 9201796..0000000 --- a/src/SponsorLink/SponsorLink/Tracing.cs +++ /dev/null @@ -1,53 +0,0 @@ -// -#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 deleted file mode 100644 index 471f37f..0000000 --- a/src/SponsorLink/SponsorLink/buildTransitive/Devlooped.Sponsors.targets +++ /dev/null @@ -1,99 +0,0 @@ - - - - - $([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/devlooped.pub.jwk b/src/SponsorLink/SponsorLink/devlooped.pub.jwk deleted file mode 100644 index cdf45c2..0000000 --- a/src/SponsorLink/SponsorLink/devlooped.pub.jwk +++ /dev/null @@ -1,5 +0,0 @@ -{ - "e": "AQAB", - "kty": "RSA", - "n": "5inhv8QymaDBOihNi1eY-6-hcIB5qSONFZxbxxXAyOtxAdjFCPM-94gIZqM9CDrX3pyg1lTJfml_a_FZSU9dB1ii5mSX_mNHBFXn1_l_gi1ErdbkIF5YbW6oxWFxf3G5mwVXwnPfxHTyQdmWQ3YJR-A3EB4kaFwLqA6Ha5lb2ObGpMTQJNakD4oTAGDhqHMGhu6PupGq5ie4qZcQ7N8ANw8xH7nicTkbqEhQABHWOTmLBWq5f5F6RYGF8P7cl0IWl_w4YcIZkGm2vX2fi26F9F60cU1v13GZEVDTXpJ9kzvYeM9sYk6fWaoyY2jhE51qbv0B0u6hScZiLREtm3n7ClJbIGXhkUppFS2JlNaX3rgQ6t-4LK8gUTyLt3zDs2H8OZyCwlCpfmGmdsUMkm1xX6t2r-95U3zywynxoWZfjBCJf41leM9OMKYwNWZ6LQMyo83HWw1PBIrX4ZLClFwqBcSYsXDyT8_ZLd1cdYmPfmtllIXxZhLClwT5qbCWv73V" -} \ No newline at end of file diff --git a/src/SponsorLink/SponsorLink/sponsorable.md b/src/SponsorLink/SponsorLink/sponsorable.md deleted file mode 100644 index c023c25..0000000 --- a/src/SponsorLink/SponsorLink/sponsorable.md +++ /dev/null @@ -1,5 +0,0 @@ -# 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 deleted file mode 100644 index be206b1..0000000 --- a/src/SponsorLink/SponsorLinkAnalyzer.sln +++ /dev/null @@ -1,43 +0,0 @@ - -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 deleted file mode 100644 index 3b3bd0d..0000000 --- a/src/SponsorLink/Tests/.netconfig +++ /dev/null @@ -1,15 +0,0 @@ -[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 deleted file mode 100644 index aa5f48d..0000000 --- a/src/SponsorLink/Tests/Attributes.cs +++ /dev/null @@ -1,59 +0,0 @@ -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 deleted file mode 100644 index 75a78b4..0000000 --- a/src/SponsorLink/Tests/Extensions.cs +++ /dev/null @@ -1,43 +0,0 @@ -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 deleted file mode 100644 index c816eba..0000000 --- a/src/SponsorLink/Tests/JsonOptions.cs +++ /dev/null @@ -1,72 +0,0 @@ -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/Sample.cs b/src/SponsorLink/Tests/Sample.cs deleted file mode 100644 index 6249e62..0000000 --- a/src/SponsorLink/Tests/Sample.cs +++ /dev/null @@ -1,59 +0,0 @@ -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 deleted file mode 100644 index 7625e2c..0000000 --- a/src/SponsorLink/Tests/SponsorLinkTests.cs +++ /dev/null @@ -1,126 +0,0 @@ -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 deleted file mode 100644 index 5ae6e3f..0000000 --- a/src/SponsorLink/Tests/SponsorableManifest.cs +++ /dev/null @@ -1,309 +0,0 @@ -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 deleted file mode 100644 index f753aad..0000000 --- a/src/SponsorLink/Tests/Tests.csproj +++ /dev/null @@ -1,42 +0,0 @@ - - - - net8.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - %(GitRoot.FullPath) - - - - - - - - - - \ No newline at end of file diff --git a/src/SponsorLink/readme.md b/src/SponsorLink/readme.md deleted file mode 100644 index cb651a1..0000000 --- a/src/SponsorLink/readme.md +++ /dev/null @@ -1,34 +0,0 @@ -# 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