diff --git a/.github/pack/action.yml b/.github/pack/action.yml
new file mode 100644
index 0000000000..c4cc05247c
--- /dev/null
+++ b/.github/pack/action.yml
@@ -0,0 +1,26 @@
+name: setup
+description: Packs a .NET project
+
+inputs:
+ project:
+ description: Path to a .NET project
+ required: true
+
+runs:
+ using: composite
+ steps:
+ - run: dotnet build --nologo -c Release --no-restore --no-incremental "${{ inputs.project }}"
+ shell: pwsh
+ env:
+ DOTVVM_ROOT: ${{ github.workspace }}
+ DOTNET_NOLOGO: "1"
+ DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "1"
+ DOTNET_CLI_TELEMETRY_OPTOUT: "1"
+
+ - run: dotnet pack -c Release --no-build "${{ inputs.project }}"
+ shell: pwsh
+ env:
+ DOTVVM_ROOT: ${{ github.workspace }}
+ DOTNET_NOLOGO: "1"
+ DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "1"
+ DOTNET_CLI_TELEMETRY_OPTOUT: "1"
diff --git a/.github/workflows/publish-internal.yml b/.github/workflows/publish-internal.yml
new file mode 100644
index 0000000000..2a8e8224c2
--- /dev/null
+++ b/.github/workflows/publish-internal.yml
@@ -0,0 +1,143 @@
+name: publish-internal
+
+on:
+ workflow_dispatch:
+ inputs:
+ release-type:
+ type: choice
+ options:
+ - InternalPreview
+ - PublicPreview
+ - Stable
+ default: InternalPreview
+ description: The type of release (determines version format)
+ required: false
+ version-core:
+ type: string
+ default: "4.0.0"
+ description: The core part of the version string
+ required: false
+ prerelease-version:
+ type: string
+ default: preview01
+ description: The prerelease suffix appended after the core version
+ required: false
+ prerelease-suffix:
+ type: string
+ default: ""
+ description: Additional prerelease suffix appended after the build number
+ required: false
+ signature-type:
+ type: choice
+ options:
+ - DotNetFoundation
+ - Riganti
+ default: DotNetFoundation
+ description: The signature to be used to sign the packages.
+ required: false
+
+jobs:
+ read-input:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - id: set-version
+ run: >
+ if [[ "${{ inputs.release-type }}" == 'InternalPreview' ]]; then
+ VERSION="${{ inputs.version-core}}-${{ inputs.prerelease-version }}-${{ github.run_id }}";
+ elif [[ "${{ inputs.release-type }}" == 'PublicPreview' ]]; then
+ VERSION="${{ inputs.version-core}}-${{ inputs.prerelease-version }}-final";
+ elif [[ "${{ inputs.release-type }}" == 'Stable' ]]; then
+ VERSION="${{ inputs.version-core}}";
+ else
+ echo "Unknown release type '${{ inputs.release-type }}'.";
+ exit 1;
+ fi;
+
+ if [[ ( "${{ inputs.release-type }}" == 'InternalPreview' || "${{ inputs.release-type }}" == 'PublicPreview' ) && -n "${{ inputs.prerelease-suffix }}" ]]; then
+ VERSION="${VERSION}-${{ inputs.prerelease-suffix }}";
+ fi;
+
+ echo "$VERSION";
+ echo "version=$VERSION" >> $GITHUB_OUTPUT;
+ outputs:
+ version: ${{ steps.set-version.outputs.version }}
+
+ publish-nuget-packages:
+ runs-on: windows-2022
+ needs: read-input
+ steps:
+
+ - uses: actions/checkout@v3
+
+ - name: Set up
+ uses: ./.github/setup
+
+ - name: Add internal NuGet feed
+ run: ./ci/scripts/Add-InternalNuGetFeed.ps1 `
+ -internalFeed "${{ secrets.AZURE_ARTIFACTS_FEED }}" `
+ -internalFeedUser "${{ secrets.AZURE_ARTIFACTS_USERNAME }}" `
+ -internalFeedPat "${{ secrets.AZURE_ARTIFACTS_PAT }}"
+
+ - name: Publish NuGet packages (.NET Foundation)
+ if: ${{ inputs.signature-type == 'DotNetFoundation' }}
+ run: ./ci/scripts/Publish-NuGetPackages.ps1 `
+ -root "${{ github.workspace }}" `
+ -version "${{ needs.read-input.outputs.version }}" `
+ -signatureType "DotNetFoundation" `
+ -dnfUrl "${{ secrets.SIGN_DNF_KEYVAULT_URL }}" `
+ -dnfClientId "${{ secrets.SIGN_DNF_CLIENT_ID }}" `
+ -dnfTenantId "${{ secrets.SIGN_DNF_TENANT_ID }}" `
+ -dnfSecret "${{ secrets.SIGN_DNF_SECRET }}" `
+ -dnfCertificate "${{ secrets.SIGN_DNF_CERTIFICATE_NAME }}"
+
+ - name: Publish NuGet packages (Riganti)
+ if: ${{ inputs.signature-type == 'Riganti' }}
+ run: ./ci/scripts/Publish-NuGetPackages.ps1 `
+ -root "${{ github.workspace }}" `
+ -version "${{ needs.read-input.outputs.version }}" `
+ -signatureType "Riganti" `
+ -rigantiUrl "${{ secrets.SIGN_RIGANTI_KEYVAULT_URL }}" `
+ -rigantiClientId "${{ secrets.SIGN_RIGANTI_CLIENT_ID }}" `
+ -rigantiTenantId "${{ secrets.SIGN_RIGANTI_TENANT_ID }}" `
+ -rigantiSecret "${{ secrets.SIGN_RIGANTI_SECRET }}" `
+ -rigantiCertificate "${{ secrets.SIGN_RIGANTI_CERTIFICATE_NAME }}"
+
+ publish-dotvvm-types:
+ runs-on: windows-2022
+ needs: read-input
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set up
+ uses: ./.github/setup
+
+ - name: Build Framework
+ uses: ./.github/pack
+ with:
+ project: src/Framework/Framework
+
+ - name: Build dotvvm-types
+ run: npm run tsc-types
+ working-directory: src/Framework/Framework
+
+ - name: Compose dotvvm-types
+ run: >
+ mkdir types;
+ cp "${{ github.workspace }}/src/Framework/Framework/obj/typescript-types/dotvvm.d.ts" types/index.d.ts;
+ npm version "${{ needs.read-input.outputs.version }}" --no-git-tag-version;
+ cat "${{ github.workspace }}/ci/scripts/npm/dotvvm-types/package.json";
+ working-directory: ci/scripts/npm/dotvvm-types
+
+ - name: Set internal npm registry
+ run: >
+ ./ci/scripts/Set-NpmRegistry.ps1 `
+ -targetDirectory "./ci/scripts/npm/dotvvm-types" `
+ -registry "${{ secrets.INTERNAL_NPM_REGISTRY }}" `
+ -pat "${{ secrets.INTERNAL_NPM_PAT }}" `
+ -username "${{ secrets.INTERNAL_NPM_USERNAME }}" `
+ -email "${{ secrets.INTERNAL_NPM_EMAIL }}"
+
+ - name: Publish dotvvm-types
+ run: npm publish
+ working-directory: ci/scripts/npm/dotvvm-types
diff --git a/ci/scripts/Add-InternalNuGetFeed.ps1 b/ci/scripts/Add-InternalNuGetFeed.ps1
new file mode 100644
index 0000000000..fdb2741883
--- /dev/null
+++ b/ci/scripts/Add-InternalNuGetFeed.ps1
@@ -0,0 +1,13 @@
+param(
+ [string][parameter(Mandatory = $true)]$internalFeed,
+ [string][parameter(Mandatory = $true)]$internalFeedUser,
+ [string][parameter(Mandatory = $true)]$internalFeedPat,
+ [string]$internalFeedName = "riganti"
+)
+
+nuget sources add `
+ -Username "$internalFeedUser" `
+ -Password "$internalFeedPat" `
+ -StorePasswordInClearText `
+ -Name "$internalFeedName" `
+ -Source "$internalFeed"
diff --git a/ci/scripts/Copy-NuGetPackages.ps1 b/ci/scripts/Copy-NuGetPackages.ps1
new file mode 100644
index 0000000000..62578697ec
--- /dev/null
+++ b/ci/scripts/Copy-NuGetPackages.ps1
@@ -0,0 +1,83 @@
+param(
+ [string][parameter(Mandatory = $true)]$root,
+ [string][parameter(Mandatory = $true)]$nuGetOrgApiKey,
+ [string]$internalNuGetFeedName = "riganti",
+ [string]$include = "*",
+ [string]$exclude = "",
+ [string]$version = ""
+)
+
+if (-not (Test-Path "$root")) {
+ throw "The '$root' directory must exist."
+}
+
+$root = Resolve-Path "$root"
+
+$packagesDir = Join-Path "$root" "./artifacts/packages"
+
+if (Test-Path "$packagesDir") {
+ Remove-Item -Recurse "$packagesDir"
+}
+New-Item -ItemType Directory -Force "$packagesDir"
+
+$packages = . "$PSScriptRoot/Get-PublicProjects.ps1"
+
+$filteredPackages = $packages
+if ("$include" -ne "*") {
+ $filteredPackages = $filteredPackages `
+ | Where-Object { $_.Name -match "$include" }
+}
+
+if (-not ([string]::IsNullOrWhiteSpace($exclude))) {
+ $filteredPackages = $filteredPackages `
+ | Where-Object { $_.Name -notmatch "$exclude" }
+}
+
+if ([string]::IsNullOrWhiteSpace($version)) {
+ $latestVersion = nuget list "DotVVM" -NonInteractive -PreRelease -Source "$internalNuGetFeedName" `
+ | Select-String -Pattern "^DotVVM ([\d\w\.-]+)$" `
+ | Select-Object -First 1
+ $version = $latestVersion.Matches.Groups[1].Value
+ Write-Host "Version '$version' selected."
+}
+
+$packagesConfig = @"
+
+
+ $($filteredPackages `
+ | Foreach-Object { return "" } `
+ | Join-String -Separator "`n")
+
+"@
+
+$packagesConfig | Out-File (Join-Path "$packagesDir" "packages.config")
+
+Write-Host "::group::Downloading NuGet packages from internal feed"
+
+$oldCwd = Get-Location
+Set-Location "$packagesDir"
+
+try {
+ nuget restore `
+ -DirectDownload `
+ -NonInteractive `
+ -NoCache `
+ -PackageSaveMode nupkg `
+ -PackagesDirectory "$packagesDir" `
+ -Source "$internalNuGetFeedName"
+} finally {
+ Set-Location "$oldCwd"
+ Write-Host "::endgroup::"
+}
+
+Write-Host "::group::Pushing packages to NuGet.org"
+try {
+ foreach ($package in (Get-ChildItem "$packagesDir/**/*.nupkg")) {
+ nuget push "$($package.FullName)" `
+ -Source "nuget.org" `
+ -ApiKey "$nuGetOrgApiKey" `
+ -NonInteractive
+ }
+} finally {
+ Write-Host "::endgroup::"
+}
diff --git a/ci/scripts/Get-PublicProjects.ps1 b/ci/scripts/Get-PublicProjects.ps1
new file mode 100644
index 0000000000..f1adf33d67
--- /dev/null
+++ b/ci/scripts/Get-PublicProjects.ps1
@@ -0,0 +1,117 @@
+return @(
+ [PSCustomObject]@{
+ Name = "DotVVM.Core";
+ Path = "src/Framework/Core";
+ Type = "standard"
+ },
+ [PSCustomObject]@{
+ Name = "DotVVM";
+ Path = "src/Framework/Framework";
+ Type = "standard"
+ },
+ [PSCustomObject]@{
+ Name = "DotVVM.Owin";
+ Path = "src/Framework/Hosting.Owin";
+ Type = "standard"
+ },
+ [PSCustomObject]@{
+ Name = "DotVVM.AspNetCore";
+ Path = "src/Framework/Hosting.AspNetCore";
+ Type = "standard"
+ },
+ [PSCustomObject]@{
+ Name = "DotVVM.CommandLine";
+ Path = "src/Tools/CommandLine";
+ Type = "tool"
+ },
+ [PSCustomObject]@{
+ Name = "DotVVM.Templates";
+ Path = "src/Templates/DotVVM.Templates.nuspec";
+ Type = "template"
+ },
+ [PSCustomObject]@{
+ Name = "DotVVM.Tools.StartupPerf";
+ Path = "src/Tools/StartupPerfTester";
+ Type = "tool"
+ },
+ [PSCustomObject]@{
+ Name = "DotVVM.Api.Swashbuckle.AspNetCore";
+ Path = "src/Api/Swashbuckle.AspNetCore";
+ Type = "standard"
+ },
+ [PSCustomObject]@{
+ Name = "DotVVM.Api.Swashbuckle.Owin";
+ Path = "src/Api/Swashbuckle.Owin";
+ Type = "standard"
+ },
+ [PSCustomObject]@{
+ Name = "DotVVM.HotReload";
+ Path = "src/Tools/HotReload/Common";
+ Type = "standard"
+ },
+ [PSCustomObject]@{
+ Name = "DotVVM.HotReload.AspNetCore";
+ Path = "src/Tools/HotReload/AspNetCore";
+ Type = "standard"
+ },
+ [PSCustomObject]@{
+ Name = "DotVVM.HotReload.Owin";
+ Path = "src/Tools/HotReload/Owin";
+ Type = "standard"
+ }
+ [PSCustomObject]@{
+ Name = "DotVVM.Testing";
+ Path = "src/Framework/Testing";
+ Type = "standard"
+ },
+ [PSCustomObject]@{
+ Name = "DotVVM.DynamicData";
+ Path = "src/DynamicData/DynamicData";
+ Type = "standard"
+ },
+ [PSCustomObject]@{
+ Name = "DotVVM.DynamicData.Annotations";
+ Path = "src/DynamicData/Annotations";
+ Type = "standard"
+ },
+ [PSCustomObject]@{
+ Name = "DotVVM.AutoUI";
+ Path = "src/AutoUI/Core";
+ Type = "standard"
+ },
+ [PSCustomObject]@{
+ Name = "DotVVM.AutoUI.Annotations";
+ Path = "src/AutoUI/Annotations";
+ Type = "standard"
+ },
+ [PSCustomObject]@{
+ Name = "DotVVM.Tracing.ApplicationInsights";
+ Path = "src/Tracing/ApplicationInsights";
+ Type = "standard"
+ },
+ [PSCustomObject]@{
+ Name = "DotVVM.Tracing.ApplicationInsights.AspNetCore";
+ Path = "src/Tracing/ApplicationInsights.AspNetCore";
+ Type = "standard"
+ },
+ [PSCustomObject]@{
+ Name = "DotVVM.Tracing.ApplicationInsights.Owin";
+ Path = "src/Tracing/ApplicationInsights.Owin";
+ Type = "standard"
+ }
+ [PSCustomObject]@{
+ Name = "DotVVM.Tracing.MiniProfiler.AspNetCore";
+ Path = "src/Tracing/MiniProfiler.AspNetCore";
+ Type = "standard"
+ },
+ [PSCustomObject]@{
+ Name = "DotVVM.Tracing.MiniProfiler.Owin";
+ Path = "src/Tracing/MiniProfiler.Owin";
+ Type = "standard"
+ },
+ [PSCustomObject]@{
+ Name = "DotVVM.Adapters.WebForms";
+ Path = "src/Adapters/WebForms";
+ Type = "standard"
+ }
+)
diff --git a/ci/scripts/Publish-NuGetPackages.ps1 b/ci/scripts/Publish-NuGetPackages.ps1
new file mode 100644
index 0000000000..dc152f3780
--- /dev/null
+++ b/ci/scripts/Publish-NuGetPackages.ps1
@@ -0,0 +1,137 @@
+param(
+ [string][parameter(Mandatory = $true)]$root,
+ [string][parameter(Mandatory = $true)]$version,
+ [string]$internalFeedName = "riganti",
+ [string]$signatureType = "DotNetFoundation",
+ [string]$dnfUrl,
+ [string]$dnfClientId,
+ [string]$dnfTenantId,
+ [string]$dnfSecret,
+ [string]$dnfCertificate,
+ [string]$rigantiUrl,
+ [string]$rigantiClientId,
+ [string]$rigantiTenantId,
+ [string]$rigantiSecret,
+ [string]$rigantiCertificate
+)
+
+$root = Resolve-Path "$root"
+
+if ("$signatureType" -eq "DotNetFoundation") {
+ if ([string]::IsNullOrEmpty($dnfUrl) `
+ -or [string]::IsNullOrEmpty($dnfClientId) `
+ -or [string]::IsNullOrEmpty($dnfTenantId) `
+ -or [string]::IsNullOrEmpty($dnfSecret) `
+ -or [string]::IsNullOrEmpty($dnfCertificate)) {
+ throw "-dnfUrl, -dnfClientId, -dnfTenantId, -dnfSecret, and -dnfCertificate when signing using dotnet sign"
+ }
+} elseif ("$signatureType" -eq "Riganti") {
+ if ([string]::IsNullOrEmpty($rigantiUrl) `
+ -or [string]::IsNullOrEmpty($rigantiClientId) `
+ -or [string]::IsNullOrEmpty($rigantiTenantId) `
+ -or [string]::IsNullOrEmpty($rigantiSecret) `
+ -or [string]::IsNullOrEmpty($rigantiCertificate)) {
+ throw "-rigantiUrl, -rigantiClientId, -rigantiTenantId, -rigantiSecret, and -rigantiCertificate are required when signing using NuGetKeyVaultSignTool"
+ }
+} else {
+ throw "$signatureType is not a valid signature type"
+}
+
+function Get-TemplateProjects {
+ return . "$PSScriptRoot/Get-PublicProjects.ps1" | Where-Object { $_.Type -eq "template" }
+}
+
+function Build-PublicProjectPackages {
+ $packages = . "$PSScriptRoot/Get-PublicProjects.ps1" | Where-Object { $_.Type -ne "template" }
+ foreach ($package in $packages) {
+ Write-Host "::group::$($package.Name)"
+ $dir = Join-Path "$root" "$($package.Path)"
+ try {
+ dotnet build --nologo -c Release --no-restore --no-incremental -p:DOTVVM_ROOT="$root" -p:DOTVVM_VERSION="$version" "$dir"
+ dotnet pack -c Release --no-build -p:DOTVVM_ROOT="$root" -p:DOTVVM_VERSION="$version" "$dir"
+ }
+ finally {
+ Write-Host "::endgroup::"
+ }
+ }
+}
+
+function Build-TemplatePackages {
+ $packages = Get-TemplateProjects
+ foreach ($package in $packages) {
+ Write-Host "::group::$($package.Name)"
+ $path = Join-Path "$root" "$($package.Path)"
+ try {
+ $file = [System.IO.File]::ReadAllText($path, [System.Text.Encoding]::UTF8)
+ $file = [System.Text.RegularExpressions.Regex]::Replace($file, "\([^<]+)\", "" + $version + "")
+ [System.IO.File]::WriteAllText($path, $file, [System.Text.Encoding]::UTF8)
+
+ nuget pack $path -outputdirectory "$root/artifacts/packages" | Out-Host
+ }
+ finally {
+ Write-Host "::endgroup::"
+ }
+ }
+}
+
+function Remove-AllPackages {
+ Remove-Item -Recurse -Force (Join-Path "$root" "artifacts/packages") -ErrorAction SilentlyContinue
+}
+
+function Set-AllPackageSignatures {
+ $oldCwd = Get-Location
+ Set-Location "$root/src"
+
+ try {
+ foreach ($package in (Get-Item "$root/artifacts/packages/*.nupkg")) {
+ $packageName = [System.IO.Path]::GetFileNameWithoutExtension($package);
+
+ if ($signatureType -eq "DotNetFoundation") {
+ dotnet sign code azure-key-vault `
+ "$package" `
+ --base-directory "$root/artifacts/packages" `
+ --publisher-name "DotVVM" `
+ --description "$("$packageName" + " " + $env:DOTVVM_VERSION)" `
+ --description-url "https://github.com/riganti/dotvvm" `
+ --azure-key-vault-url "$dnfUrl" `
+ --azure-key-vault-client-id "$dnfClientId" `
+ --azure-key-vault-tenant-id "$dnfTenantId" `
+ --azure-key-vault-client-secret "$dnfSecret" `
+ --azure-key-vault-certificate "$dnfCertificate"
+ }
+ elseif ($signatureType -eq "Riganti") {
+ dotnet NuGetKeyVaultSignTool sign `
+ --file-digest "sha256" `
+ --timestamp-rfc3161 "http://timestamp.digicert.com" `
+ --timestamp-digest "sha256" `
+ --azure-key-vault-url "$rigantiUrl" `
+ --azure-key-vault-client-id "$rigantiClientId" `
+ --azure-key-vault-tenant-id "$rigantiTenantId" `
+ --azure-key-vault-client-secret "$rigantiSecret" `
+ --azure-key-vault-certificate "$rigantiCertificate" `
+ "$package"
+ }
+ else {
+ throw "$signatureType is not a valid signature type"
+ }
+ if ($LastExitCode -ne 0) {
+ throw "Signing failed"
+ }
+ }
+ }
+ finally {
+ Set-Location "$oldCwd"
+ }
+}
+
+function Publish-AllPackages {
+ foreach ($package in (Get-Item "$root/artifacts/packages/*.nupkg")) {
+ dotnet nuget push --api-key az --source "$internalFeedName" "$package"
+ }
+}
+
+Remove-AllPackages
+Build-PublicProjectPackages
+Build-TemplatePackages
+Set-AllPackageSignatures
+Publish-AllPackages
diff --git a/ci/scripts/Set-NpmRegistry.ps1 b/ci/scripts/Set-NpmRegistry.ps1
new file mode 100644
index 0000000000..9ec925ff2e
--- /dev/null
+++ b/ci/scripts/Set-NpmRegistry.ps1
@@ -0,0 +1,38 @@
+param(
+ [string]$targetDirectory,
+ [string][parameter(Mandatory = $true)]$registry,
+ [string][parameter(Mandatory = $false)]$pat,
+ [string][parameter(Mandatory = $false)]$authToken,
+ [string][parameter(Mandatory = $false)]$username,
+ [string][parameter(Mandatory = $false)]$email
+)
+
+$oldCwd = Get-Location
+
+if (-not ([string]::IsNullOrWhiteSpace("$targetDirectory"))) {
+ if (-not (Test-Path -PathType Container "$targetDirectory")) {
+ throw "Target directory '$targetDirectory' does not exist."
+ }
+ Set-Location $targetDirectory
+}
+
+try {
+ $feed = "$registry".Trim("https:");
+ npm config set --location project "registry=$registry"
+ if ($username) {
+ npm config set --location project "${feed}:username=$username"
+ }
+ if ($email) {
+ npm config set --location project "${feed}:email=$email"
+ }
+ if ($pat) {
+ $password = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("$pat"));
+ npm config set --location project "${feed}:_password=$password"
+ }
+ if ($authToken) {
+ npm config set --location project "${feed}:_authToken=$authToken"
+ }
+}
+finally {
+ Set-Location "$oldCwd"
+}