Publish current app and server
This commit is contained in:
+321
-15
@@ -3,8 +3,10 @@
|
||||
Deploys the complete Memby Docker Compose stack to MATT-NAS.
|
||||
|
||||
.DESCRIPTION
|
||||
Packages the local server build context, Docker Compose file and .env.example,
|
||||
then streams them to the NAS over one SSH connection.
|
||||
Builds and verifies a signed Memby APK, packages it with the local server build
|
||||
context, Docker Compose file and .env.example, then streams everything to the NAS
|
||||
over one SSH connection. Once the new gateway is healthy it publishes the APK
|
||||
through the gateway's release endpoint, making it available to older TVs.
|
||||
|
||||
The remote deployment:
|
||||
- checks Docker and Docker Compose;
|
||||
@@ -28,6 +30,12 @@ This deploys the current local working tree, including uncommitted server change
|
||||
|
||||
.EXAMPLE
|
||||
.\deploy-server.ps1 -Destination /share/Docker/Memby-test -HealthTimeoutSeconds 180
|
||||
|
||||
.EXAMPLE
|
||||
.\deploy-server.ps1 -AppVersion 0.1.70 -ReleaseNotes 'Reliable in-app updates'
|
||||
|
||||
.EXAMPLE
|
||||
.\deploy-server.ps1 -ReleaseNotes 'Required security update' --m
|
||||
#>
|
||||
|
||||
#Requires -Version 7.2
|
||||
@@ -52,20 +60,52 @@ param(
|
||||
|
||||
[Parameter()]
|
||||
[ValidateRange(30, 600)]
|
||||
[int] $HealthTimeoutSeconds = 120
|
||||
[int] $HealthTimeoutSeconds = 120,
|
||||
|
||||
[Parameter()]
|
||||
[ValidatePattern('^\d+\.\d+\.\d+$')]
|
||||
[string] $AppVersion,
|
||||
|
||||
[Parameter()]
|
||||
[string] $ReleaseNotes = '',
|
||||
|
||||
[Parameter()]
|
||||
[switch] $SkipAppTests,
|
||||
|
||||
[Parameter()]
|
||||
[switch] $SkipAppRelease
|
||||
|
||||
,
|
||||
[Parameter()]
|
||||
[Alias('m')]
|
||||
[switch] $MandatoryUpdate,
|
||||
|
||||
# PowerShell advanced scripts do not bind GNU-style double-dash switches. Accept
|
||||
# --m explicitly as the sole positional argument so it can still sit at the end.
|
||||
[Parameter(Position = 0)]
|
||||
[ValidateSet('--m')]
|
||||
[string] $MandatoryFlag
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$mandatoryRelease = $MandatoryUpdate -or $MandatoryFlag -eq '--m'
|
||||
|
||||
if ($mandatoryRelease -and $SkipAppRelease) {
|
||||
throw '--m cannot be combined with -SkipAppRelease because no update would be published.'
|
||||
}
|
||||
|
||||
$script:CurrentStep = 0
|
||||
$script:TotalSteps = 5
|
||||
$script:TotalSteps = if ($SkipAppRelease) { 5 } else { 6 }
|
||||
|
||||
function Write-Banner {
|
||||
Write-Host ''
|
||||
Write-Host '╭─ Memby deployment' -ForegroundColor Magenta
|
||||
Write-Host "│ Source $SourceDirectory (local working tree)" -ForegroundColor DarkGray
|
||||
Write-Host "│ Target ${RemoteUser}@${RemoteHost}:$Destination" -ForegroundColor DarkGray
|
||||
if ($mandatoryRelease) {
|
||||
Write-Host '│ Update mandatory (viewers cannot skip it)' -ForegroundColor Yellow
|
||||
}
|
||||
Write-Host '╰─' -ForegroundColor Magenta
|
||||
Write-Host ''
|
||||
}
|
||||
@@ -114,6 +154,91 @@ function Get-RequiredCommand {
|
||||
return $command.Source
|
||||
}
|
||||
|
||||
function Invoke-Checked {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string] $FilePath,
|
||||
|
||||
[Parameter()]
|
||||
[string[]] $Arguments = @()
|
||||
)
|
||||
|
||||
& $FilePath @Arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "'$FilePath' failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
}
|
||||
|
||||
function Get-AndroidSdk {
|
||||
param([Parameter(Mandatory)][string] $RepositoryDirectory)
|
||||
|
||||
if ($env:ANDROID_HOME -and (Test-Path -LiteralPath $env:ANDROID_HOME)) {
|
||||
return $env:ANDROID_HOME
|
||||
}
|
||||
$propertiesPath = Join-Path $RepositoryDirectory 'local.properties'
|
||||
if (Test-Path -LiteralPath $propertiesPath) {
|
||||
$sdkLine = Get-Content -LiteralPath $propertiesPath |
|
||||
Where-Object { $_ -match '^sdk\.dir=' } |
|
||||
Select-Object -First 1
|
||||
if ($sdkLine) {
|
||||
$sdkPath = $sdkLine.Substring($sdkLine.IndexOf('=') + 1).
|
||||
Replace('\:', ':').
|
||||
Replace('\\', '\')
|
||||
if (Test-Path -LiteralPath $sdkPath) {
|
||||
return $sdkPath
|
||||
}
|
||||
}
|
||||
}
|
||||
throw 'Android SDK not found. Set ANDROID_HOME or sdk.dir in local.properties.'
|
||||
}
|
||||
|
||||
function Import-UserSigningEnvironment {
|
||||
foreach ($name in @(
|
||||
'MEMBY_KEYSTORE',
|
||||
'MEMBY_KEYSTORE_PASSWORD',
|
||||
'MEMBY_KEY_ALIAS',
|
||||
'MEMBY_KEY_PASSWORD'
|
||||
)) {
|
||||
$value = [Environment]::GetEnvironmentVariable($name, 'Process')
|
||||
if ([string]::IsNullOrWhiteSpace($value)) {
|
||||
$value = [Environment]::GetEnvironmentVariable($name, 'User')
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($value)) {
|
||||
throw "Release signing variable $name is not configured."
|
||||
}
|
||||
[Environment]::SetEnvironmentVariable($name, $value, 'Process')
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $env:MEMBY_KEYSTORE -PathType Leaf)) {
|
||||
throw "Release keystore not found: $env:MEMBY_KEYSTORE"
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ConfiguredAppVersion {
|
||||
param([Parameter(Mandatory)][string] $RepositoryDirectory)
|
||||
|
||||
if ($AppVersion) {
|
||||
return $AppVersion
|
||||
}
|
||||
$gradle = Get-Content -LiteralPath (Join-Path $RepositoryDirectory 'app/build.gradle.kts') -Raw
|
||||
if ($gradle -notmatch 'val defaultVersionName = "(\d+\.\d+\.\d+)"') {
|
||||
throw 'Could not read defaultVersionName from app/build.gradle.kts.'
|
||||
}
|
||||
return $Matches[1]
|
||||
}
|
||||
|
||||
function Get-DotEnvValue {
|
||||
param(
|
||||
[Parameter(Mandatory)][string] $Path,
|
||||
[Parameter(Mandatory)][string] $Name
|
||||
)
|
||||
|
||||
$line = Get-Content -LiteralPath $Path |
|
||||
Where-Object { $_ -match ("^" + [regex]::Escape($Name) + "=") } |
|
||||
Select-Object -First 1
|
||||
if (-not $line) { return '' }
|
||||
return $line.Substring($line.IndexOf('=') + 1).Trim()
|
||||
}
|
||||
|
||||
function Assert-SafeRemoteSettings {
|
||||
if ($RemoteHost -notmatch '^[A-Za-z0-9._:-]+$') {
|
||||
throw "RemoteHost contains unsupported characters: '$RemoteHost'."
|
||||
@@ -148,10 +273,24 @@ function New-DeploymentArchive {
|
||||
[string] $RepositoryDirectory,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string] $ArchivePath
|
||||
[string] $ArchivePath,
|
||||
|
||||
[Parameter()]
|
||||
[string] $ReleaseDirectory
|
||||
)
|
||||
|
||||
& $script:TarCommand -cf $ArchivePath -C $RepositoryDirectory server docker-compose.yml .env.example
|
||||
$arguments = @(
|
||||
'-cf', $ArchivePath,
|
||||
'-C', $RepositoryDirectory,
|
||||
'server', 'docker-compose.yml', '.env.example'
|
||||
)
|
||||
if ($ReleaseDirectory) {
|
||||
$arguments += @(
|
||||
'-C', (Split-Path -Parent $ReleaseDirectory),
|
||||
(Split-Path -Leaf $ReleaseDirectory)
|
||||
)
|
||||
}
|
||||
& $script:TarCommand @arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Unable to create the deployment archive (tar exit code $LASTEXITCODE)."
|
||||
}
|
||||
@@ -270,11 +409,106 @@ try {
|
||||
Write-Detail 'server/ build context'
|
||||
Write-Detail 'docker-compose.yml'
|
||||
Write-Detail '.env.example'
|
||||
$releaseDirectory = ''
|
||||
$releaseVersion = ''
|
||||
$releaseSHA256 = ''
|
||||
if (-not $SkipAppRelease) {
|
||||
foreach ($appPath in @(
|
||||
(Join-Path $checkoutDirectory 'app/build.gradle.kts'),
|
||||
(Join-Path $checkoutDirectory 'gradlew.bat')
|
||||
)) {
|
||||
if (-not (Test-Path -LiteralPath $appPath -PathType Leaf)) {
|
||||
throw "Required Android build file is missing: $appPath"
|
||||
}
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace(
|
||||
(Get-DotEnvValue -Path (Join-Path $checkoutDirectory '.env.example') `
|
||||
-Name 'MEMBY_RELEASE_PUBLISH_TOKEN')
|
||||
)) {
|
||||
throw 'MEMBY_RELEASE_PUBLISH_TOKEN is empty in .env.example; a signed APK cannot be published.'
|
||||
}
|
||||
}
|
||||
Write-Success 'Deployment payload is complete'
|
||||
Write-Host ''
|
||||
|
||||
if (-not $SkipAppRelease) {
|
||||
Write-Step 'Building and verifying the signed Android update'
|
||||
Import-UserSigningEnvironment
|
||||
$releaseVersion = Get-ConfiguredAppVersion -RepositoryDirectory $checkoutDirectory
|
||||
$sdk = Get-AndroidSdk -RepositoryDirectory $checkoutDirectory
|
||||
$buildTools = Get-ChildItem -LiteralPath (Join-Path $sdk 'build-tools') -Directory |
|
||||
Sort-Object { [version]$_.Name } -Descending |
|
||||
Select-Object -First 1
|
||||
if (-not $buildTools) {
|
||||
throw 'Android SDK Build Tools are not installed.'
|
||||
}
|
||||
$apkSigner = Join-Path $buildTools.FullName 'apksigner.bat'
|
||||
if (-not (Test-Path -LiteralPath $apkSigner -PathType Leaf)) {
|
||||
throw "APK signer not found: $apkSigner"
|
||||
}
|
||||
|
||||
$jdkHome = 'C:\Program Files\Android\Android Studio\jbr'
|
||||
if (Test-Path -LiteralPath $jdkHome) {
|
||||
$env:JAVA_HOME = $jdkHome
|
||||
}
|
||||
$gradleArguments = @('--console=plain')
|
||||
if (-not $SkipAppTests) {
|
||||
$gradleArguments += 'testDebugUnitTest'
|
||||
}
|
||||
$gradleArguments += @(
|
||||
'assembleRelease',
|
||||
"-Pmemby.versionName=$releaseVersion"
|
||||
)
|
||||
Invoke-Checked -FilePath (Join-Path $checkoutDirectory 'gradlew.bat') `
|
||||
-Arguments $gradleArguments
|
||||
|
||||
$apk = Join-Path $checkoutDirectory 'app/build/outputs/apk/release/app-release.apk'
|
||||
if (-not (Test-Path -LiteralPath $apk -PathType Leaf)) {
|
||||
$unsigned = Join-Path $checkoutDirectory `
|
||||
'app/build/outputs/apk/release/app-release-unsigned.apk'
|
||||
if (Test-Path -LiteralPath $unsigned -PathType Leaf) {
|
||||
throw 'Gradle produced an unsigned APK; deployment was stopped.'
|
||||
}
|
||||
throw "Signed release APK not found: $apk"
|
||||
}
|
||||
Invoke-Checked -FilePath $apkSigner -Arguments @(
|
||||
'verify', '--verbose', '--print-certs', $apk
|
||||
)
|
||||
|
||||
$metadataPath = Join-Path $checkoutDirectory `
|
||||
'app/build/outputs/apk/release/output-metadata.json'
|
||||
$metadata = Get-Content -LiteralPath $metadataPath -Raw | ConvertFrom-Json
|
||||
$builtVersion = [string]$metadata.elements[0].versionName
|
||||
if ($builtVersion -ne $releaseVersion) {
|
||||
throw "Built APK version is $builtVersion, expected $releaseVersion."
|
||||
}
|
||||
|
||||
$releaseDirectory = Join-Path $workDirectory 'release'
|
||||
[void] (New-Item -ItemType Directory -Path $releaseDirectory)
|
||||
$apkName = "memby-$releaseVersion.apk"
|
||||
Copy-Item -LiteralPath $apk -Destination (Join-Path $releaseDirectory $apkName)
|
||||
$releaseSHA256 = (
|
||||
Get-FileHash -LiteralPath (Join-Path $releaseDirectory $apkName) -Algorithm SHA256
|
||||
).Hash.ToLowerInvariant()
|
||||
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
|
||||
[System.IO.File]::WriteAllText(
|
||||
(Join-Path $releaseDirectory 'version.txt'), $releaseVersion, $utf8NoBom
|
||||
)
|
||||
[System.IO.File]::WriteAllText(
|
||||
(Join-Path $releaseDirectory 'sha256.txt'), $releaseSHA256, $utf8NoBom
|
||||
)
|
||||
[System.IO.File]::WriteAllText(
|
||||
(Join-Path $releaseDirectory 'notes.txt'), $ReleaseNotes, $utf8NoBom
|
||||
)
|
||||
Write-Detail "Version $releaseVersion"
|
||||
Write-Detail "SHA-256 $releaseSHA256"
|
||||
Write-Success 'Signed Android update is ready'
|
||||
Write-Host ''
|
||||
}
|
||||
|
||||
Write-Step 'Packaging the release'
|
||||
New-DeploymentArchive -RepositoryDirectory $checkoutDirectory -ArchivePath $archivePath
|
||||
New-DeploymentArchive -RepositoryDirectory $checkoutDirectory -ArchivePath $archivePath `
|
||||
-ReleaseDirectory $releaseDirectory
|
||||
$archiveSize = (Get-Item -LiteralPath $archivePath).Length
|
||||
Write-Detail ("Archive size {0:N1} MiB" -f ($archiveSize / 1MB))
|
||||
Write-Success 'Release archive is ready'
|
||||
@@ -287,6 +521,8 @@ set -eu
|
||||
|
||||
destination='__DESTINATION__'
|
||||
health_timeout=__HEALTH_TIMEOUT__
|
||||
publish_release=__PUBLISH_RELEASE__
|
||||
mandatory_update=__MANDATORY_UPDATE__
|
||||
parent=$(dirname "$destination")
|
||||
staging="${destination}.new.$$"
|
||||
backup="${destination}.previous.$$"
|
||||
@@ -386,7 +622,7 @@ wait_for_service() {
|
||||
trap rollback EXIT
|
||||
trap 'exit 130' INT TERM
|
||||
|
||||
step '[remote 1/8] Checking Docker'
|
||||
step '[remote 1/9] Checking Docker'
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
failure 'Docker is not installed on the NAS'
|
||||
exit 1
|
||||
@@ -399,10 +635,14 @@ docker compose version >/dev/null 2>&1 || {
|
||||
failure 'The Docker Compose v2 plugin is not installed'
|
||||
exit 1
|
||||
}
|
||||
if [ "$publish_release" -eq 1 ] && ! command -v curl >/dev/null 2>&1; then
|
||||
failure 'curl is required on the NAS to publish the signed Android update'
|
||||
exit 1
|
||||
fi
|
||||
success "$(docker --version)"
|
||||
success "$(docker compose version)"
|
||||
|
||||
step '[remote 2/8] Extracting the release'
|
||||
step '[remote 2/9] Extracting the release'
|
||||
# Checked before anything is created: the staging directory, the swap and the backup all
|
||||
# need write access to the parent, and "can't create directory" from BusyBox halfway
|
||||
# through a deployment is a poor way to learn the account cannot write there.
|
||||
@@ -424,9 +664,13 @@ mkdir -- "$staging"
|
||||
tar -xf - -C "$staging"
|
||||
test -f "$staging/docker-compose.yml"
|
||||
test -f "$staging/server/Dockerfile"
|
||||
if [ "$publish_release" -eq 1 ]; then
|
||||
test -f "$staging/release/version.txt"
|
||||
test -f "$staging/release/sha256.txt"
|
||||
fi
|
||||
success 'Release extracted'
|
||||
|
||||
step '[remote 3/8] Installing configuration from .env.example'
|
||||
step '[remote 3/9] Installing configuration from .env.example'
|
||||
# The local .env.example is the single source of truth for configuration and carries
|
||||
# real values rather than placeholders. Every deployment overwrites the deployed .env
|
||||
# with it, so neither a Git push nor an SSH edit is needed.
|
||||
@@ -445,6 +689,7 @@ new_password=$(sed -n 's/^POSTGRES_PASSWORD=//p' "$staging/.env" | head -n 1 | t
|
||||
admin_token=$(sed -n 's/^MEMBY_ADMIN_TOKEN=//p' "$staging/.env" | head -n 1 | tr -d '\r')
|
||||
emby_url=$(sed -n 's/^MEMBY_EMBY_URL=//p' "$staging/.env" | head -n 1 | tr -d '\r')
|
||||
configured_port=$(sed -n 's/^MEMBY_PORT=//p' "$staging/.env" | head -n 1 | tr -d '\r')
|
||||
release_token=$(sed -n 's/^MEMBY_RELEASE_PUBLISH_TOKEN=//p' "$staging/.env" | head -n 1 | tr -d '\r')
|
||||
if [ -z "$new_password" ]; then
|
||||
failure 'POSTGRES_PASSWORD is empty in .env.example; Compose will refuse to start'
|
||||
exit 1
|
||||
@@ -461,6 +706,10 @@ if [ "$configured_port" != '32768' ]; then
|
||||
failure "MEMBY_PORT must be 32768 for the mserver.sublogue.com reverse proxy (found: ${configured_port:-unset})"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$publish_release" -eq 1 ] && [ -z "$release_token" ]; then
|
||||
failure 'MEMBY_RELEASE_PUBLISH_TOKEN is empty; the signed APK cannot be published'
|
||||
exit 1
|
||||
fi
|
||||
success 'Required gateway configuration is present'
|
||||
if [ -n "$previous_password" ] && [ "$previous_password" != "$new_password" ]; then
|
||||
failure 'POSTGRES_PASSWORD differs from the deployed value'
|
||||
@@ -475,21 +724,21 @@ fi
|
||||
)
|
||||
success 'Compose configuration is valid'
|
||||
|
||||
step '[remote 4/8] Pulling PostgreSQL and Redis'
|
||||
step '[remote 4/9] Pulling PostgreSQL and Redis'
|
||||
(
|
||||
cd "$staging"
|
||||
docker compose pull postgres redis
|
||||
)
|
||||
success 'Dependency images are ready'
|
||||
|
||||
step '[remote 5/8] Building memby-server'
|
||||
step '[remote 5/9] Building memby-server'
|
||||
(
|
||||
cd "$staging"
|
||||
docker compose build --pull server
|
||||
)
|
||||
success 'Server image built'
|
||||
|
||||
step '[remote 6/8] Activating the release'
|
||||
step '[remote 6/9] Activating the release'
|
||||
rm -rf -- "$backup"
|
||||
if [ -e "$destination" ] || [ -L "$destination" ]; then
|
||||
if [ -f "$destination/docker-compose.yml" ]; then
|
||||
@@ -511,12 +760,12 @@ mv -- "$staging" "$destination"
|
||||
activated=1
|
||||
success 'Release activated'
|
||||
|
||||
step '[remote 7/8] Starting the Compose stack'
|
||||
step '[remote 7/9] Starting the Compose stack'
|
||||
cd "$destination"
|
||||
docker compose up -d --remove-orphans
|
||||
success 'Compose start command completed'
|
||||
|
||||
step '[remote 8/8] Waiting for healthy services'
|
||||
step '[remote 8/9] Waiting for healthy services'
|
||||
wait_for_service postgres
|
||||
wait_for_service redis
|
||||
wait_for_service server
|
||||
@@ -536,6 +785,52 @@ if ! docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "$serve
|
||||
fi
|
||||
success 'Runtime configuration includes the admin token'
|
||||
|
||||
step '[remote 9/9] Publishing the signed Android update'
|
||||
if [ "$publish_release" -eq 1 ]; then
|
||||
release_version=$(tr -d '\r\n' < "$destination/release/version.txt")
|
||||
release_sha256=$(tr -d '\r\n' < "$destination/release/sha256.txt")
|
||||
if ! printf '%s' "$release_version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
failure 'The packaged Android version is invalid'
|
||||
exit 1
|
||||
fi
|
||||
if ! printf '%s' "$release_sha256" | grep -Eq '^[0-9a-f]{64}$'; then
|
||||
failure 'The packaged Android checksum is invalid'
|
||||
exit 1
|
||||
fi
|
||||
release_apk="$destination/release/memby-${release_version}.apk"
|
||||
if [ ! -f "$release_apk" ]; then
|
||||
failure "Signed APK is missing: $release_apk"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
release_response="$destination/release/publish-response.json"
|
||||
release_status=$(curl --show-error --silent \
|
||||
--output "$release_response" \
|
||||
--write-out '%{http_code}' \
|
||||
-X POST \
|
||||
-H "Authorization: Bearer $release_token" \
|
||||
-F "version=$release_version" \
|
||||
-F "sha256=$release_sha256" \
|
||||
-F "mandatory=$mandatory_update" \
|
||||
-F "notes=<$destination/release/notes.txt" \
|
||||
-F "apk=@$release_apk;type=application/vnd.android.package-archive" \
|
||||
http://127.0.0.1:32768/admin/api/release)
|
||||
case "$release_status" in
|
||||
2??) ;;
|
||||
*)
|
||||
failure "Gateway rejected the Android update (HTTP ${release_status:-unknown})"
|
||||
if [ -s "$release_response" ]; then
|
||||
detail "$(tr -d '\r\n' < "$release_response")"
|
||||
fi
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
rm -rf -- "$destination/release"
|
||||
success "Memby $release_version is available to older TVs"
|
||||
else
|
||||
detail 'Skipped by -SkipAppRelease'
|
||||
fi
|
||||
|
||||
printf '\n'
|
||||
docker compose ps
|
||||
printf '\n'
|
||||
@@ -549,6 +844,14 @@ success 'Memby gateway: https://mserver.sublogue.com'
|
||||
|
||||
$remoteCommand = $remoteCommand.Replace('__DESTINATION__', $Destination)
|
||||
$remoteCommand = $remoteCommand.Replace('__HEALTH_TIMEOUT__', $HealthTimeoutSeconds.ToString())
|
||||
$remoteCommand = $remoteCommand.Replace(
|
||||
'__PUBLISH_RELEASE__',
|
||||
$(if ($SkipAppRelease) { '0' } else { '1' })
|
||||
)
|
||||
$remoteCommand = $remoteCommand.Replace(
|
||||
'__MANDATORY_UPDATE__',
|
||||
$(if ($mandatoryRelease) { '1' } else { '0' })
|
||||
)
|
||||
# This file is edited on Windows, so the here-string above arrives with whatever line
|
||||
# endings it was saved with. A remote shell reads a trailing carriage return as part
|
||||
# of the token — 'set -eu\r' fails with "illegal option" before anything runs — so the
|
||||
@@ -567,6 +870,9 @@ success 'Memby gateway: https://mserver.sublogue.com'
|
||||
Write-Host ' Memby gateway: https://mserver.sublogue.com' -ForegroundColor White
|
||||
Write-Host " NAS endpoint: http://${RemoteHost}:32768" -ForegroundColor DarkGray
|
||||
Write-Host " Install path: ${RemoteHost}:$Destination" -ForegroundColor DarkGray
|
||||
if (-not $SkipAppRelease) {
|
||||
Write-Host " TV update: Memby $releaseVersion (signed and published)" -ForegroundColor White
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$deploymentTimer.Stop()
|
||||
|
||||
Reference in New Issue
Block a user