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" +}