diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 233c7fb51e5..1c6c65e278c 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -36,6 +36,8 @@ You can of course, as always, ask for help at [#coderbus](irc://irc.rizon.net/co
You need the .NET 8.0 SDK, node>=v20, and npm>=v5.7 (in your PATH) to compile the server. On Linux, you also need the `libgdiplus` package installed to generate icons.
+You need to run `corepack enable` to configure node to correctly build the webpanel.
+
The recommended IDE is Visual Studio 2022 or VSCode.
In order to build the service version and/or the Windows installer you need a to run on Windows.
diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml
index 6cfb58ff5b2..8704b477d8a 100644
--- a/.github/workflows/ci-pipeline.yml
+++ b/.github/workflows/ci-pipeline.yml
@@ -36,6 +36,7 @@ env:
OD_MIN_COMPAT_DOTNET_VERSION: 7
OD_DOTNET_VERSION: 8
TGS_DOTNET_QUALITY: ga
+ TGS_WEBPANEL_NODE_VERSION: 20.x
TGS_TEST_GITHUB_TOKEN: ${{ secrets.LIVE_TESTS_TOKEN }}
TGS_RELEASE_NOTES_TOKEN: ${{ secrets.DEV_PUSH_TOKEN }}
PACKAGING_PRIVATE_KEY_PASSPHRASE: ${{ secrets.PACKAGING_PRIVATE_KEY_PASSPHRASE }}
@@ -52,6 +53,8 @@ jobs:
permissions:
security-events: write
actions: read
+ env:
+ TGS_TELEMETRY_KEY_FILE: /tmp/tgs_telemetry_key.txt
steps:
- name: Setup dotnet
uses: actions/setup-dotnet@v4
@@ -74,9 +77,16 @@ jobs:
with:
languages: csharp
+ - name: Setup Telemetry Key File
+ run: echo "${{ secrets.TGS_TELEMETRY_KEY }}" > ${{ env.TGS_TELEMETRY_KEY_FILE }}
+
- name: Build
run: dotnet build -c ReleaseNoWindows -p:TGS_HOST_NO_WEBPANEL=true
+ - name: Delete Telemetry Key File
+ if: always()
+ run: rm ${{ env.TGS_TELEMETRY_KEY_FILE }}
+
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
@@ -356,6 +366,8 @@ jobs:
docker-build:
name: Build Docker Image
runs-on: ubuntu-latest
+ env:
+ TGS_TELEMETRY_KEY_FILE: tgs_telemetry_key.txt
steps:
- name: Checkout (Branch)
uses: actions/checkout@v4
@@ -367,8 +379,16 @@ jobs:
with:
ref: "refs/pull/${{ github.event.inputs.pull_request_number }}/merge"
+ - name: Setup Telemetry Key File
+ shell: bash
+ run: echo "${{ secrets.TGS_TELEMETRY_KEY }}" > ${{ env.TGS_TELEMETRY_KEY_FILE }}
+
- name: Build Docker Image
- run: docker build . -f build/Dockerfile
+ run: docker build . -f build/Dockerfile --build-arg TGS_TELEMETRY_KEY_FILE=${{ env.TGS_TELEMETRY_KEY_FILE }}
+
+ - name: Delete Telemetry Key File
+ if: always()
+ run: rm ${{ env.TGS_TELEMETRY_KEY_FILE }}
linux-unit-tests:
name: Linux Tests
@@ -379,6 +399,7 @@ jobs:
env:
TGS_TEST_DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
TGS_TEST_IRC_CONNECTION_STRING: ${{ secrets.IRC_CONNECTION_STRING }}
+ TGS_TELEMETRY_KEY_FILE: /tmp/tgs_telemetry_key.txt
runs-on: ubuntu-latest
steps:
- name: Install x86 libc Dependencies
@@ -393,6 +414,11 @@ jobs:
dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x'
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
+ - name: Setup Node.JS
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ env.TGS_WEBPANEL_NODE_VERSION }}
+
- name: Checkout (Branch)
uses: actions/checkout@v4
if: github.event_name == 'push' || github.event_name == 'schedule'
@@ -406,9 +432,19 @@ jobs:
- name: Restore
run: dotnet restore
+ - name: Enable Corepack
+ run: corepack enable
+
+ - name: Setup Telemetry Key File
+ run: echo "${{ secrets.TGS_TELEMETRY_KEY }}" > ${{ env.TGS_TELEMETRY_KEY_FILE }}
+
- name: Build
run: dotnet build -c ${{ matrix.configuration }}NoWindows
+ - name: Delete Telemetry Key File
+ if: always()
+ run: rm ${{ env.TGS_TELEMETRY_KEY_FILE }}
+
- name: Cache BYOND .zips
uses: actions/cache@v4
id: cache-byond
@@ -434,6 +470,7 @@ jobs:
env:
TGS_TEST_DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
TGS_TEST_IRC_CONNECTION_STRING: ${{ secrets.IRC_CONNECTION_STRING }}
+ TGS_TELEMETRY_KEY_FILE: C:/tgs_telemetry_key.txt
runs-on: windows-latest
steps:
- name: Setup dotnet
@@ -442,6 +479,11 @@ jobs:
dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x'
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
+ - name: Setup Node.JS
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ env.TGS_WEBPANEL_NODE_VERSION }}
+
- name: Checkout (Branch)
uses: actions/checkout@v4
if: github.event_name == 'push' || github.event_name == 'schedule'
@@ -455,9 +497,21 @@ jobs:
- name: Restore
run: dotnet restore
+ - name: Enable Corepack
+ run: corepack enable
+
+ - name: Setup Telemetry Key File
+ shell: bash
+ run: echo "${{ secrets.TGS_TELEMETRY_KEY }}" > ${{ env.TGS_TELEMETRY_KEY_FILE }}
+
- name: Build
run: dotnet build -c ${{ matrix.configuration }}NoWix
+ - name: Delete Telemetry Key File
+ shell: bash
+ if: always()
+ run: rm ${{ env.TGS_TELEMETRY_KEY_FILE }}
+
- name: Cache BYOND .zips
uses: actions/cache@v4
id: cache-byond
@@ -483,6 +537,8 @@ jobs:
database-type: [ 'SqlServer', 'Sqlite', 'PostgresSql', 'MariaDB', 'MySql' ]
watchdog-type: [ 'Basic', 'Advanced' ]
configuration: [ 'Debug', 'Release' ]
+ env:
+ TGS_TELEMETRY_KEY_FILE: C:/tgs_telemetry_key.txt
runs-on: windows-latest
steps:
- name: Wait for LocalDB Connection # Do this first because we don't want to find out it's failing later
@@ -501,6 +557,11 @@ jobs:
${{ env.OD_MIN_COMPAT_DOTNET_VERSION }}.0.x
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
+ - name: Setup Node.JS
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ env.TGS_WEBPANEL_NODE_VERSION }}
+
- name: Set TGS_TEST_DUMP_API_SPEC
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Advanced' && matrix.database-type == 'SqlServer' }}
run: echo "TGS_TEST_DUMP_API_SPEC=yes" >> $Env:GITHUB_ENV
@@ -570,9 +631,21 @@ jobs:
- name: Restore
run: dotnet restore
+ - name: Enable Corepack
+ run: corepack enable
+
+ - name: Setup Telemetry Key File
+ shell: bash
+ run: echo "${{ secrets.TGS_TELEMETRY_KEY }}" > ${{ env.TGS_TELEMETRY_KEY_FILE }}
+
- name: Build
run: dotnet build -c ${{ matrix.configuration }} tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj
+ - name: Delete Telemetry Key File
+ shell: bash
+ if: always()
+ run: rm ${{ env.TGS_TELEMETRY_KEY_FILE }}
+
- name: Cache BYOND .zips
uses: actions/cache@v4
id: cache-byond
@@ -713,6 +786,8 @@ jobs:
database-type: [ 'Sqlite', 'PostgresSql', 'MariaDB', 'MySql' ]
watchdog-type: [ 'Basic', 'Advanced' ]
configuration: [ 'Debug', 'Release' ]
+ env:
+ TGS_TELEMETRY_KEY_FILE: /tmp/tgs_telemetry_key.txt
runs-on: ubuntu-latest
steps:
- name: Disable ptrace_scope
@@ -733,6 +808,11 @@ jobs:
${{ env.OD_MIN_COMPAT_DOTNET_VERSION }}.0.x
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
+ - name: Setup Node.JS
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ env.TGS_WEBPANEL_NODE_VERSION }}
+
- name: Set Sqlite Connection Info
if: ${{ matrix.database-type == 'Sqlite' }}
run: |
@@ -775,9 +855,19 @@ jobs:
- name: Restore
run: dotnet restore
+ - name: Enable Corepack
+ run: corepack enable
+
+ - name: Setup Telemetry Key File
+ run: echo "${{ secrets.TGS_TELEMETRY_KEY }}" > ${{ env.TGS_TELEMETRY_KEY_FILE }}
+
- name: Build
run: dotnet build -c ${{ matrix.configuration }}NoWindows tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj
+ - name: Delete Telemetry Key File
+ if: always()
+ run: rm ${{ env.TGS_TELEMETRY_KEY_FILE }}
+
- name: Cache BYOND .zips
uses: actions/cache@v4
id: cache-byond
@@ -1117,12 +1207,14 @@ jobs:
build-deb:
name: Build .deb Package # Can't do i386 due to https://github.com/dotnet/core/issues/4595
runs-on: ubuntu-latest
+ env:
+ TGS_TELEMETRY_KEY_FILE: /tmp/tgs_telemetry_key.txt
steps:
- name: Install Native Dependencies
run: |
sudo dpkg --add-architecture i386
sudo apt-get update
- sudo apt-get install -y -o APT::Immediate-Configure=0 libstdc++6:i386 libgcc-s1:i386 gnupg2 xmlstarlet libgdiplus
+ sudo apt-get install -y -o APT::Immediate-Configure=0 libstdc++6:i386 libgcc-s1:i386
- name: Import GPG Key
if: (github.event_name == 'push' && contains(github.event.head_commit.message, '[TGSDeploy]') && (github.event.ref == 'refs/heads/master' || github.event.ref == 'refs/heads/dev'))
@@ -1160,27 +1252,37 @@ jobs:
with:
ref: "refs/pull/${{ github.event.inputs.pull_request_number }}/merge"
- - name: Parse TGS version
- run: |
- echo "TGS_VERSION=$(xmlstarlet sel -N X="http://schemas.microsoft.com/developer/msbuild/2003" --template --value-of /X:Project/X:PropertyGroup/X:TgsCoreVersion build/Version.props)" >> $GITHUB_ENV
-
- name: Grab Most Recent Changelog
run: curl -L https://raw.githubusercontent.com/tgstation/tgstation-server/gh-pages/changelog.yml -o changelog.yml
+ - name: Setup Telemetry Key File
+ run: echo "${{ secrets.TGS_TELEMETRY_KEY }}" > ${{ env.TGS_TELEMETRY_KEY_FILE }}
+
- name: Execute Build Script (Unsigned)
- if: (!(github.event_name == 'push' && contains(github.event.head_commit.message, '[TGSDeploy]') && (github.event.ref == 'refs/heads/master' || github.event.ref == 'refs/heads/dev')))
+ if: (!(github.event_name == 'push' && contains(github.event.head_commit.message, '[TGSDeploy]') && github.event.ref == 'refs/heads/master'))
run: sudo -E build/package/deb/build_package.sh
- name: Execute Build Script (Signed)
- if: (github.event_name == 'push' && contains(github.event.head_commit.message, '[TGSDeploy]') && (github.event.ref == 'refs/heads/master' || github.event.ref == 'refs/heads/dev'))
+ if: (github.event_name == 'push' && contains(github.event.head_commit.message, '[TGSDeploy]') && github.event.ref == 'refs/heads/master')
env:
PACKAGING_KEYGRIP: ${{ vars.PACKAGING_KEYGRIP }}
+ run: sudo -E build/package/deb/build_package.sh
+
+ - name: Parse TGS version
run: |
- sudo -E build/package/deb/build_package.sh
+ echo "TGS_VERSION=$(xmlstarlet sel -N X="http://schemas.microsoft.com/developer/msbuild/2003" --template --value-of /X:Project/X:PropertyGroup/X:TgsCoreVersion build/Version.props)" >> $GITHUB_ENV
+
+ - name: Verify Package Files are Signed
+ if: (github.event_name == 'push' && contains(github.event.head_commit.message, '[TGSDeploy]') && github.event.ref == 'refs/heads/master')
+ run:
gpg --verify tgstation-server_${{ env.TGS_VERSION }}-1.dsc
gpg --verify tgstation-server_${{ env.TGS_VERSION }}-1_amd64.changes
gpg --verify tgstation-server_${{ env.TGS_VERSION }}-1_amd64.buildinfo
+ - name: Delete Telemetry Key File
+ if: always()
+ run: rm ${{ env.TGS_TELEMETRY_KEY_FILE }}
+
- name: Test Install
run: |
sudo mkdir /etc/tgstation-server
@@ -1219,6 +1321,8 @@ jobs:
build-msi:
name: Build Windows Installer .exe
runs-on: windows-latest
+ env:
+ TGS_TELEMETRY_KEY_FILE: C:/tgs_telemetry_key.txt
steps:
- name: Install winget
uses: Cyberboss/install-winget@v1
@@ -1231,6 +1335,11 @@ jobs:
dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x'
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
+ - name: Setup Node.JS
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ env.TGS_WEBPANEL_NODE_VERSION }}
+
- name: Checkout (Branch)
uses: actions/checkout@v4
if: github.event_name == 'push' || github.event_name == 'schedule'
@@ -1252,9 +1361,21 @@ jobs:
- name: Restore
run: dotnet restore
+ - name: Enable Corepack
+ run: corepack enable
+
+ - name: Setup Telemetry Key File
+ shell: bash
+ run: echo "${{ secrets.TGS_TELEMETRY_KEY }}" > ${{ env.TGS_TELEMETRY_KEY_FILE }}
+
- name: Build Host
run: dotnet build -c Release src/Tgstation.Server.Host/Tgstation.Server.Host.csproj
+ - name: Delete Telemetry Key File
+ shell: bash
+ if: always()
+ run: rm ${{ env.TGS_TELEMETRY_KEY_FILE }}
+
- name: Build Service
run: dotnet build -c Release src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj
@@ -1620,6 +1741,8 @@ jobs:
needs: [deploy-dm, deploy-http, deployment-gate]
runs-on: windows-latest
if: github.event.ref == 'refs/heads/master' && contains(github.event.head_commit.message, '[TGSDeploy]')
+ env:
+ TGS_TELEMETRY_KEY_FILE: C:/tgs_telemetry_key.txt
steps:
- name: Setup dotnet
uses: actions/setup-dotnet@v4
@@ -1627,6 +1750,11 @@ jobs:
dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x'
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
+ - name: Setup Node.JS
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ env.TGS_WEBPANEL_NODE_VERSION }}
+
- name: Checkout
uses: actions/checkout@v4
@@ -1638,9 +1766,21 @@ jobs:
cd build/package/winget
dotnet tool restore
- - name: Build Host # We need to rebuild the installer.exe so it can be properly signed
+ - name: Enable Corepack
+ run: corepack enable
+
+ - name: Setup Telemetry Key File
+ shell: bash
+ run: echo "${{ secrets.TGS_TELEMETRY_KEY }}" > ${{ env.TGS_TELEMETRY_KEY_FILE }}
+
+ - name: Build Host # We need to rebuild the installer.exe so it can be properly signed
run: dotnet build -c Release src/Tgstation.Server.Host/Tgstation.Server.Host.csproj
+ - name: Delete Telemetry Key File
+ shell: bash
+ if: always()
+ run: rm ${{ env.TGS_TELEMETRY_KEY_FILE }}
+
- name: Build Service
run: dotnet build -c Release src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj
diff --git a/README.md b/README.md
index 58b341d571a..ea51ba7f332 100644
--- a/README.md
+++ b/README.md
@@ -313,6 +313,12 @@ The following providers use the `ServerUrl` setting:
- Keycloak
- InvisionCommunity
+- `Telemetry:DisableVersionReporting`: Prevents you installation and the version you're using from being reported on the source repository's deployments list
+
+- `Telemetry:ServerFriendlyName`: Prevents anonymous TGS version usage statistics from being sent to be displayed on the repository.
+
+- `Telemetry:VersionReportingRepositoryId`: The repository telemetry is sent to. For security reasons, this is not the main TGS repo. See the [tgstation-server-deployments](https://github.com/tgstation/tgstation-server-deployments) repository for more information.
+
### Database Configuration
If using a MariaDB/MySQL server, our client library [recommends you set 'utf8mb4' as your default charset](https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql#1-recommended-server-charset) disregard at your own risk.
diff --git a/build/Dockerfile b/build/Dockerfile
index cba330156e2..b301591833e 100644
--- a/build/Dockerfile
+++ b/build/Dockerfile
@@ -1,5 +1,8 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim AS build
+# Set in CI
+ARG TGS_TELEMETRY_KEY_FILE=
+
# install node and npm
# replace shell with bash so we can source files
RUN curl --silent -o- https://raw.githubusercontent.com/creationix/nvm/v0.39.1/install.sh | sh
@@ -17,7 +20,8 @@ RUN . $NVM_DIR/nvm.sh \
&& apt-get install -y \
dos2unix \
libgdiplus \
- && rm -rf /var/lib/apt/lists/*
+ && rm -rf /var/lib/apt/lists/* \
+ && corepack enable
# Build web control panel
WORKDIR /repo/build
@@ -45,7 +49,9 @@ RUN dotnet publish -c Release -o /app \
&& build/RemoveUnsupportedRuntimes.sh /app
WORKDIR /repo/src/Tgstation.Server.Host
-RUN dotnet publish -c Release -o /app/lib/Default \
+
+RUN export TGS_TELEMETRY_KEY_FILE="../../${TGS_TELEMETRY_KEY_FILE}" \
+ && dotnet publish -c Release -o /app/lib/Default \
&& cd ../.. \
&& build/RemoveUnsupportedRuntimes.sh /app/lib/Default \
&& mv /app/lib/Default/appsettings* /app
diff --git a/build/Version.props b/build/Version.props
index 4f6a8945320..f524e2ee699 100644
--- a/build/Version.props
+++ b/build/Version.props
@@ -21,6 +21,5 @@
11.4.2
- 1.22.21
diff --git a/build/WebpanelVersion.props b/build/WebpanelVersion.props
index 806c5ed542d..2613f5510c0 100644
--- a/build/WebpanelVersion.props
+++ b/build/WebpanelVersion.props
@@ -1,6 +1,6 @@
- 5.9.0
+ 6.1.0
diff --git a/build/package/deb/build_package.sh b/build/package/deb/build_package.sh
index 1a25546da88..a48071f2193 100755
--- a/build/package/deb/build_package.sh
+++ b/build/package/deb/build_package.sh
@@ -18,7 +18,7 @@ apt-get install -y \
devscripts \
ca-certificates \
curl \
- gnupg \
+ gnupg2 \
xmlstarlet \
libgdiplus
@@ -35,6 +35,8 @@ echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.co
apt-get update
apt-get install nodejs dotnet-sdk-8.0 -y
+corepack enable
+
CURRENT_COMMIT=$(git rev-parse HEAD)
rm -rf packaging
@@ -67,7 +69,6 @@ cp build/tgstation-server.service debian/
SIGN_COMMAND="$SCRIPT_DIR/wrap_gpg.sh"
rm -f /tmp/tgs_wrap_gpg_output.log
-
set +e
if [[ -z "$PACKAGING_KEYGRIP" ]]; then
diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs
index 4e37e7d008b..9a04c0ac94e 100644
--- a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs
+++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs
@@ -79,13 +79,13 @@ await databaseContextFactory.UseContext(
IGitHubService gitHubService;
if (instanceAuthenticated)
{
- authenticatedGitHubService = gitHubServiceFactory.CreateService(repositorySettings.AccessToken!);
+ authenticatedGitHubService = await gitHubServiceFactory.CreateService(repositorySettings.AccessToken!, cancellationToken);
gitHubService = authenticatedGitHubService;
}
else
{
authenticatedGitHubService = null;
- gitHubService = gitHubServiceFactory.CreateService();
+ gitHubService = await gitHubServiceFactory.CreateService(cancellationToken);
}
var repoOwner = remoteInformation.RemoteRepositoryOwner!;
@@ -175,8 +175,8 @@ public override async ValueTask> RemoveMergedTest
}
var gitHubService = repositorySettings.AccessToken != null
- ? gitHubServiceFactory.CreateService(repositorySettings.AccessToken)
- : gitHubServiceFactory.CreateService();
+ ? await gitHubServiceFactory.CreateService(repositorySettings.AccessToken, cancellationToken)
+ : await gitHubServiceFactory.CreateService(cancellationToken);
var tasks = revisionInformation
.ActiveTestMerges
@@ -255,7 +255,7 @@ protected override async ValueTask CommentOnTestMergeSource(
int testMergeNumber,
CancellationToken cancellationToken)
{
- var gitHubService = gitHubServiceFactory.CreateService(repositorySettings.AccessToken!);
+ var gitHubService = await gitHubServiceFactory.CreateService(repositorySettings.AccessToken!, cancellationToken);
try
{
@@ -343,7 +343,7 @@ await databaseContextFactory.UseContext(
return;
}
- var gitHubService = gitHubServiceFactory.CreateService(gitHubAccessToken);
+ var gitHubService = await gitHubServiceFactory.CreateService(gitHubAccessToken, cancellationToken);
try
{
diff --git a/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs
index 6109f490bb7..69289bb5d40 100644
--- a/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs
+++ b/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs
@@ -49,8 +49,8 @@ public GitHubRemoteFeatures(IGitHubServiceFactory gitHubServiceFactory, ILogger<
CancellationToken cancellationToken)
{
var gitHubService = repositorySettings.AccessToken != null
- ? gitHubServiceFactory.CreateService(repositorySettings.AccessToken)
- : gitHubServiceFactory.CreateService();
+ ? await gitHubServiceFactory.CreateService(repositorySettings.AccessToken, cancellationToken)
+ : await gitHubServiceFactory.CreateService(cancellationToken);
PullRequest? pr = null;
ApiException? exception = null;
diff --git a/src/Tgstation.Server.Host/Configuration/TelemetryConfiguration.cs b/src/Tgstation.Server.Host/Configuration/TelemetryConfiguration.cs
new file mode 100644
index 00000000000..7ec0361759d
--- /dev/null
+++ b/src/Tgstation.Server.Host/Configuration/TelemetryConfiguration.cs
@@ -0,0 +1,33 @@
+namespace Tgstation.Server.Host.Configuration
+{
+ ///
+ /// Configuration options for telemetry.
+ ///
+ public sealed class TelemetryConfiguration
+ {
+ ///
+ /// The key for the the resides in.
+ ///
+ public const string Section = "Telemetry";
+
+ ///
+ /// The default value of .
+ ///
+ private const long DefaultVersionReportingRepositoryId = 841149827; // https://github.com/tgstation/tgstation-server-deployments
+
+ ///
+ /// If version reporting telemetry is disabled.
+ ///
+ public bool DisableVersionReporting { get; set; }
+
+ ///
+ /// The friendly name used on GitHub deployments for version reporting. If only the server will be shown.
+ ///
+ public string? ServerFriendlyName { get; set; }
+
+ ///
+ /// The GitHub repository ID used for version reporting.
+ ///
+ public long? VersionReportingRepositoryId { get; set; } = DefaultVersionReportingRepositoryId;
+ }
+}
diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs
index d4c1335df77..c41113069d2 100644
--- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs
+++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs
@@ -165,7 +165,7 @@ async Task CacheFactory()
Uri? repoUrl = null;
try
{
- var gitHubService = gitHubServiceFactory.CreateService();
+ var gitHubService = await gitHubServiceFactory.CreateService(cancellationToken);
var repositoryUrlTask = gitHubService.GetUpdatesRepositoryUrl(cancellationToken);
var releases = await gitHubService.GetTgsReleases(cancellationToken);
diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs
index 36970000c59..7e981066c0d 100644
--- a/src/Tgstation.Server.Host/Core/Application.cs
+++ b/src/Tgstation.Server.Host/Core/Application.cs
@@ -143,6 +143,7 @@ public void ConfigureServices(
services.UseStandardConfig(Configuration);
services.UseStandardConfig(Configuration);
services.UseStandardConfig(Configuration);
+ services.UseStandardConfig(Configuration);
// enable options which give us config reloading
services.AddOptions();
@@ -423,6 +424,7 @@ void AddTypedContext()
services.AddSingleton();
services.AddSingleton();
services.AddHostedService();
+ services.AddHostedService();
services.AddFileDownloader();
services.AddGitHub();
diff --git a/src/Tgstation.Server.Host/Core/ServerUpdater.cs b/src/Tgstation.Server.Host/Core/ServerUpdater.cs
index 8c9d9681687..1170537a12d 100644
--- a/src/Tgstation.Server.Host/Core/ServerUpdater.cs
+++ b/src/Tgstation.Server.Host/Core/ServerUpdater.cs
@@ -288,7 +288,7 @@ async ValueTask BeginUpdateImpl(
{
logger.LogDebug("Looking for GitHub releases version {version}...", newVersion);
- var gitHubService = gitHubServiceFactory.CreateService();
+ var gitHubService = await gitHubServiceFactory.CreateService(cancellationToken);
var releases = await gitHubService.GetTgsReleases(cancellationToken);
foreach (var kvp in releases)
{
diff --git a/src/Tgstation.Server.Host/Core/VersionReportingService.cs b/src/Tgstation.Server.Host/Core/VersionReportingService.cs
new file mode 100644
index 00000000000..dd0eef6f786
--- /dev/null
+++ b/src/Tgstation.Server.Host/Core/VersionReportingService.cs
@@ -0,0 +1,269 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+using Octokit;
+
+using Tgstation.Server.Common.Extensions;
+using Tgstation.Server.Host.Configuration;
+using Tgstation.Server.Host.Extensions;
+using Tgstation.Server.Host.IO;
+using Tgstation.Server.Host.Properties;
+using Tgstation.Server.Host.System;
+using Tgstation.Server.Host.Utils;
+using Tgstation.Server.Host.Utils.GitHub;
+
+namespace Tgstation.Server.Host.Core
+{
+ ///
+ /// Handles TGS version reporting, if enabled.
+ ///
+ sealed class VersionReportingService : BackgroundService
+ {
+ ///
+ /// The for the .
+ ///
+ readonly IGitHubClientFactory gitHubClientFactory;
+
+ ///
+ /// The for the .
+ ///
+ readonly IIOManager ioManager;
+
+ ///
+ /// The for the .
+ ///
+ readonly IAsyncDelayer asyncDelayer;
+
+ ///
+ /// The for the .
+ ///
+ readonly IAssemblyInformationProvider assemblyInformationProvider;
+
+ ///
+ /// The for the .
+ ///
+ readonly ILogger logger;
+
+ ///
+ /// The for the .
+ ///
+ readonly TelemetryConfiguration telemetryConfiguration;
+
+ ///
+ /// The passed to .
+ ///
+ CancellationToken shutdownCancellationToken;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The value of .
+ /// The value of .
+ /// The value of .
+ /// The value of .
+ /// The containing the value of .
+ /// The value of .
+ public VersionReportingService(
+ IGitHubClientFactory gitHubClientFactory,
+ IIOManager ioManager,
+ IAsyncDelayer asyncDelayer,
+ IAssemblyInformationProvider assemblyInformationProvider,
+ IOptions telemetryConfigurationOptions,
+ ILogger logger)
+ {
+ this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
+ this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
+ this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
+ this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
+ telemetryConfiguration = telemetryConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(telemetryConfigurationOptions));
+ this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ ///
+ public override Task StopAsync(CancellationToken cancellationToken)
+ {
+ shutdownCancellationToken = cancellationToken;
+ return base.StopAsync(cancellationToken);
+ }
+
+ ///
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ if (telemetryConfiguration.DisableVersionReporting)
+ {
+ logger.LogDebug("Version telemetry disabled");
+ return;
+ }
+
+ if (!telemetryConfiguration.VersionReportingRepositoryId.HasValue)
+ {
+ logger.LogError("Version reporting repository is misconfigured. Telemetry cannot be sent!");
+ return;
+ }
+
+ var attribute = TelemetryAppSerializedKeyAttribute.Instance;
+ if (attribute == null)
+ {
+ logger.LogDebug("TGS build configuration does not allow for version telemetry");
+ return;
+ }
+
+ logger.LogDebug("Starting...");
+
+ try
+ {
+ var telemetryIdFile = ioManager.ResolvePath(
+ ioManager.ConcatPath(
+ ioManager.GetPathInLocalDirectory(assemblyInformationProvider),
+ "telemetry.id"));
+
+ Guid telemetryId;
+ if (!await ioManager.FileExists(telemetryIdFile, stoppingToken))
+ {
+ telemetryId = Guid.NewGuid();
+ await ioManager.WriteAllBytes(telemetryIdFile, Encoding.UTF8.GetBytes(telemetryId.ToString()), stoppingToken);
+ logger.LogInformation("Generated telemetry ID {telemetryId} and wrote to {file}", telemetryId, telemetryIdFile);
+ }
+ else
+ {
+ var contents = await ioManager.ReadAllBytes(telemetryIdFile, stoppingToken);
+
+ string guidStr;
+ try
+ {
+ guidStr = Encoding.UTF8.GetString(contents);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Cannot decode telemetry ID from installation file ({path}). Telemetry will not be sent!", telemetryIdFile);
+ return;
+ }
+
+ if (!Guid.TryParse(guidStr, out telemetryId))
+ {
+ logger.LogError("Cannot parse telemetry ID from installation file ({path}). Telemetry will not be sent!", telemetryIdFile);
+ return;
+ }
+ }
+
+ try
+ {
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ var nextDelayHours = await TryReportVersion(
+ telemetryId,
+ attribute.SerializedKey,
+ telemetryConfiguration.VersionReportingRepositoryId.Value,
+ false,
+ stoppingToken)
+ ? 24
+ : 1;
+
+ logger.LogDebug("Next version report in {hours} hours", nextDelayHours);
+ await asyncDelayer.Delay(TimeSpan.FromHours(nextDelayHours), stoppingToken);
+ }
+ }
+ catch (OperationCanceledException ex)
+ {
+ logger.LogTrace(ex, "Inner cancellation");
+ }
+
+ shutdownCancellationToken.ThrowIfCancellationRequested();
+
+ logger.LogDebug("Sending shutdown telemetry");
+ await TryReportVersion(
+ telemetryId,
+ attribute.SerializedKey,
+ telemetryConfiguration.VersionReportingRepositoryId.Value,
+ true,
+ shutdownCancellationToken);
+ }
+ catch (OperationCanceledException ex)
+ {
+ logger.LogTrace(ex, "Exiting due to outer cancellation...");
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Crashed!");
+ }
+ }
+
+ ///
+ /// Make an attempt to report the current to the configured GitHub repository.
+ ///
+ /// The telemetry for the installation.
+ /// The serialized authentication for the .
+ /// The ID of the repository to send telemetry to.
+ /// If this is shutdown telemetry.
+ /// The for the operation.
+ /// A resulting in if telemetry was reported successfully, otherwise.
+ async ValueTask TryReportVersion(Guid telemetryId, string serializedPem, long repositoryId, bool shutdown, CancellationToken cancellationToken)
+ {
+ logger.LogDebug("Sending version telemetry...");
+
+ var serverFriendlyName = telemetryConfiguration.ServerFriendlyName;
+ if (String.IsNullOrWhiteSpace(serverFriendlyName))
+ serverFriendlyName = null;
+
+ logger.LogTrace(
+ "Repository ID: {repoId}, Server friendly name: {friendlyName}",
+ repositoryId,
+ serverFriendlyName == null
+ ? "(null)"
+ : $"\"{serverFriendlyName}\"");
+ try
+ {
+ var gitHubClient = await gitHubClientFactory.CreateInstallationClient(
+ serializedPem,
+ repositoryId,
+ cancellationToken);
+
+ if (gitHubClient == null)
+ {
+ logger.LogWarning("Could not create GitHub client to connect to repository ID {repoId}!", repositoryId);
+ return false;
+ }
+
+ // remove this lookup once https://github.com/octokit/octokit.net/pull/2960 is merged and released
+ var repository = await gitHubClient.Repository.Get(repositoryId);
+
+ logger.LogTrace("Repository ID {id} resolved to {owner}/{name}", repositoryId, repository.Owner.Name, repository.Name);
+
+ var inputs = new Dictionary
+ {
+ { "telemetry_id", telemetryId.ToString() },
+ { "tgs_semver", assemblyInformationProvider.Version.Semver().ToString() },
+ { "shutdown", shutdown ? "true" : "false" },
+ };
+
+ if (serverFriendlyName != null)
+ inputs.Add("server_friendly_name", serverFriendlyName);
+
+ await gitHubClient.Actions.Workflows.CreateDispatch(
+ repository.Owner.Login,
+ repository.Name,
+ ".github/workflows/tgs_deployments_telemetry.yml",
+ new CreateWorkflowDispatch("main")
+ {
+ Inputs = inputs,
+ });
+
+ logger.LogTrace("Telemetry sent successfully");
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ logger.LogWarning(ex, "Failed to report version!");
+ return false;
+ }
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Properties/TelemetryAppSerializedKeyAttribute.cs b/src/Tgstation.Server.Host/Properties/TelemetryAppSerializedKeyAttribute.cs
new file mode 100644
index 00000000000..6d2ff1be5b8
--- /dev/null
+++ b/src/Tgstation.Server.Host/Properties/TelemetryAppSerializedKeyAttribute.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Reflection;
+
+namespace Tgstation.Server.Host.Properties
+{
+ ///
+ /// Attribute for bundling the GitHub App serialized private key used for version telemetry.
+ ///
+ [AttributeUsage(AttributeTargets.Assembly)]
+ sealed class TelemetryAppSerializedKeyAttribute : Attribute
+ {
+ ///
+ /// Return the 's instance of the .
+ ///
+ public static TelemetryAppSerializedKeyAttribute? Instance => Assembly
+ .GetExecutingAssembly()
+ .GetCustomAttribute();
+
+ ///
+ /// The serialized GitHub App Client ID and private key.
+ ///
+ public string SerializedKey { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The value of .
+ public TelemetryAppSerializedKeyAttribute(string serializedKey)
+ {
+ SerializedKey = serializedKey ?? throw new ArgumentNullException(nameof(serializedKey));
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs
index 3f3180f9da2..d535d978843 100644
--- a/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs
+++ b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs
@@ -60,12 +60,12 @@ public GitHubOAuthValidator(
{
logger.LogTrace("Validating response code...");
- var gitHubService = gitHubServiceFactory.CreateService();
+ var gitHubService = await gitHubServiceFactory.CreateService(cancellationToken);
var token = await gitHubService.CreateOAuthAccessToken(oAuthConfiguration, code, cancellationToken);
if (token == null)
return null;
- var authenticatedClient = gitHubServiceFactory.CreateService(token);
+ var authenticatedClient = await gitHubServiceFactory.CreateService(token, cancellationToken);
logger.LogTrace("Getting user details...");
var userId = await authenticatedClient.GetCurrentUserId(cancellationToken);
diff --git a/src/Tgstation.Server.Host/Setup/SetupWizard.cs b/src/Tgstation.Server.Host/Setup/SetupWizard.cs
index 5c77ecffebc..dacc6734107 100644
--- a/src/Tgstation.Server.Host/Setup/SetupWizard.cs
+++ b/src/Tgstation.Server.Host/Setup/SetupWizard.cs
@@ -950,6 +950,31 @@ async ValueTask ParseAddress(string question)
};
}
+ ///
+ /// Prompts the user to create a .
+ ///
+ /// The for the operation.
+ /// A resulting in the new .
+ async ValueTask ConfigureTelemetry(CancellationToken cancellationToken)
+ {
+ bool enableReporting = await PromptYesNo("Enable version telemetry? This anonymously reports the TGS version in use.", true, cancellationToken);
+
+ string? serverFriendlyName = null;
+ if (enableReporting)
+ {
+ await console.WriteAsync("(Optional) Publically associate your reported version with a friendly name:", false, cancellationToken);
+ serverFriendlyName = await console.ReadLineAsync(false, cancellationToken);
+ if (String.IsNullOrWhiteSpace(serverFriendlyName))
+ serverFriendlyName = null;
+ }
+
+ return new TelemetryConfiguration
+ {
+ DisableVersionReporting = !enableReporting,
+ ServerFriendlyName = serverFriendlyName,
+ };
+ }
+
///
/// Saves a given set to .
///
@@ -961,6 +986,7 @@ async ValueTask ParseAddress(string question)
/// The to save.
/// The to save.
/// The to save.
+ /// The to save.
/// The for the operation.
/// A representing the running operation.
async ValueTask SaveConfiguration(
@@ -972,6 +998,7 @@ async ValueTask SaveConfiguration(
ElasticsearchConfiguration? elasticsearchConfiguration,
ControlPanelConfiguration controlPanelConfiguration,
SwarmConfiguration? swarmConfiguration,
+ TelemetryConfiguration? telemetryConfiguration,
CancellationToken cancellationToken)
{
newGeneralConfiguration.ApiPort = hostingPort ?? GeneralConfiguration.DefaultApiPort;
@@ -984,6 +1011,7 @@ async ValueTask SaveConfiguration(
{ ElasticsearchConfiguration.Section, elasticsearchConfiguration },
{ ControlPanelConfiguration.Section, controlPanelConfiguration },
{ SwarmConfiguration.Section, swarmConfiguration },
+ { TelemetryConfiguration.Section, telemetryConfiguration },
};
var versionConverter = new VersionConverter();
@@ -1055,6 +1083,8 @@ async ValueTask RunWizard(string userConfigFileName, CancellationToken cancellat
var swarmConfiguration = await ConfigureSwarm(cancellationToken);
+ var telemetryConfiguration = await ConfigureTelemetry(cancellationToken);
+
await console.WriteAsync(null, true, cancellationToken);
await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Configuration complete! Saving to {0}", userConfigFileName), true, cancellationToken);
@@ -1067,6 +1097,7 @@ await SaveConfiguration(
elasticSearchConfiguration,
controlPanelConfiguration,
swarmConfiguration,
+ telemetryConfiguration,
cancellationToken);
}
@@ -1184,6 +1215,7 @@ await SaveConfiguration(
AllowAnyOrigin = true,
},
null,
+ null,
cancellationToken);
}
else
diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj
index 7e29d1943bd..1c2937a5b21 100644
--- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj
+++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj
@@ -29,13 +29,13 @@
-
+
-
+
@@ -45,21 +45,43 @@
-
+
<_Parameter1>$(TgsConfigVersion)
<_Parameter2>$(TgsInteropVersion)
<_Parameter3>$(TgsWebpanelVersion)
<_Parameter4>$(TgsHostWatchdogVersion)
<_Parameter5>$(TgsMariaDBRedistVersion)
-
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+ <_Parameter1>@(SerializedTelemetryKey)
+
+
+
+
+
+
+
diff --git a/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs b/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs
index e479388b79b..4cf31a5d289 100644
--- a/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs
+++ b/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs
@@ -1,9 +1,16 @@
using System;
using System.Collections.Generic;
+using System.IdentityModel.Tokens.Jwt;
using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Tokens;
+
using Octokit;
using Tgstation.Server.Host.Configuration;
@@ -12,7 +19,7 @@
namespace Tgstation.Server.Host.Utils.GitHub
{
///
- sealed class GitHubClientFactory : IGitHubClientFactory
+ sealed class GitHubClientFactory : IGitHubClientFactory, IDisposable
{
///
/// Limit to the amount of days a can live in the .
@@ -45,6 +52,11 @@ sealed class GitHubClientFactory : IGitHubClientFactory
///
readonly Dictionary clientCache;
+ ///
+ /// The used to guard access to .
+ ///
+ readonly SemaphoreSlim clientCacheSemaphore;
+
///
/// Initializes a new instance of the class.
///
@@ -61,50 +73,139 @@ public GitHubClientFactory(
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
clientCache = new Dictionary();
+ clientCacheSemaphore = new SemaphoreSlim(1, 1);
}
///
- public IGitHubClient CreateClient() => GetOrCreateClient(generalConfiguration.GitHubAccessToken);
+ public void Dispose() => clientCacheSemaphore.Dispose();
+
+ ///
+ public async ValueTask CreateClient(CancellationToken cancellationToken)
+ => (await GetOrCreateClient(
+ generalConfiguration.GitHubAccessToken,
+ null,
+ cancellationToken))!;
///
- public IGitHubClient CreateClient(string accessToken)
- => GetOrCreateClient(
- accessToken ?? throw new ArgumentNullException(nameof(accessToken)));
+ public async ValueTask CreateClient(string accessToken, CancellationToken cancellationToken)
+ => (await GetOrCreateClient(
+ accessToken ?? throw new ArgumentNullException(nameof(accessToken)),
+ null,
+ cancellationToken))!;
+
+ ///
+ public ValueTask CreateInstallationClient(string serializedPem, long repositoryId, CancellationToken cancellationToken)
+ => GetOrCreateClient(serializedPem, repositoryId, cancellationToken);
///
- /// Retrieve a from the or add a new one based on a given .
+ /// Retrieve a from the or add a new one based on a given .
///
- /// Optional access token to use as credentials.
- /// The for the given .
- GitHubClient GetOrCreateClient(string? accessToken)
+ /// Optional access token to use as credentials or GitHub App private key. If using a private key, must be set.
+ /// Setting this specifies is a private key and a GitHub App installation authenticated client will be returned.
+ /// The for the operation.
+ /// A resulting in the for the given or if authentication failed.
+#pragma warning disable CA1506 // TODO: Decomplexify
+ async ValueTask GetOrCreateClient(string? accessTokenOrSerializedPem, long? installationRepositoryId, CancellationToken cancellationToken)
+#pragma warning restore CA1506
{
GitHubClient client;
bool cacheHit;
DateTimeOffset? lastUsed;
- lock (clientCache)
+ using (await SemaphoreSlimContext.Lock(clientCacheSemaphore, cancellationToken))
{
string cacheKey;
- if (String.IsNullOrWhiteSpace(accessToken))
+ if (String.IsNullOrWhiteSpace(accessTokenOrSerializedPem))
{
- accessToken = null;
+ accessTokenOrSerializedPem = null;
cacheKey = DefaultCacheKey;
}
else
- cacheKey = accessToken;
+ cacheKey = accessTokenOrSerializedPem;
cacheHit = clientCache.TryGetValue(cacheKey, out var tuple);
var now = DateTimeOffset.UtcNow;
if (!cacheHit)
{
+ logger.LogTrace("Creating new GitHubClient...");
var product = assemblyInformationProvider.ProductInfoHeaderValue.Product!;
client = new GitHubClient(
new ProductHeaderValue(
product.Name,
product.Version));
- if (accessToken != null)
- client.Credentials = new Credentials(accessToken);
+ if (accessTokenOrSerializedPem != null)
+ {
+ if (installationRepositoryId.HasValue)
+ {
+ logger.LogTrace("Performing GitHub App authentication for installation on repository {installationRepositoryId}", installationRepositoryId.Value);
+ var splits = accessTokenOrSerializedPem.Split(':');
+ if (splits.Length != 2)
+ {
+ logger.LogError("Failed to parse serialized Client ID & PEM! Expected 2 chunks, got {chunkCount}", splits.Length);
+ return null;
+ }
+
+ byte[] pemBytes;
+ try
+ {
+ pemBytes = Convert.FromBase64String(splits[1]);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Failed to parse supposed base64 PEM!");
+ return null;
+ }
+
+ var pem = Encoding.UTF8.GetString(pemBytes);
+
+ using var rsa = RSA.Create();
+ rsa.ImportFromPem(pem);
+
+ var signingCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256);
+ var jwtSecurityTokenHandler = new JwtSecurityTokenHandler { SetDefaultTimesOnTokenCreation = false };
+
+ var nowDateTime = DateTime.UtcNow;
+
+ var jwt = jwtSecurityTokenHandler.CreateToken(new SecurityTokenDescriptor
+ {
+ Issuer = splits[0],
+ Expires = nowDateTime.AddMinutes(10),
+ IssuedAt = nowDateTime,
+ SigningCredentials = signingCredentials,
+ });
+
+ var jwtStr = jwtSecurityTokenHandler.WriteToken(jwt);
+
+ client.Credentials = new Credentials(jwtStr, AuthenticationType.Bearer);
+
+ Installation installation;
+ try
+ {
+ installation = await client.GitHubApps.GetRepositoryInstallationForCurrent(installationRepositoryId.Value);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Failed to perform app authentication!");
+ return null;
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+ try
+ {
+ var installToken = await client.GitHubApps.CreateInstallationToken(installation.Id);
+
+ client.Credentials = new Credentials(installToken.Token);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Failed to perform installation authentication!");
+ return null;
+ }
+ }
+ else
+ client.Credentials = new Credentials(accessTokenOrSerializedPem);
+ }
clientCache.Add(cacheKey, (Client: client, LastUsed: now));
lastUsed = null;
diff --git a/src/Tgstation.Server.Host/Utils/GitHub/GitHubServiceFactory.cs b/src/Tgstation.Server.Host/Utils/GitHub/GitHubServiceFactory.cs
index efa08d13bba..a38b84b271d 100644
--- a/src/Tgstation.Server.Host/Utils/GitHub/GitHubServiceFactory.cs
+++ b/src/Tgstation.Server.Host/Utils/GitHub/GitHubServiceFactory.cs
@@ -1,4 +1,6 @@
using System;
+using System.Threading;
+using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -44,13 +46,16 @@ public GitHubServiceFactory(
}
///
- public IGitHubService CreateService() => CreateServiceImpl(gitHubClientFactory.CreateClient());
+ public async ValueTask CreateService(CancellationToken cancellationToken)
+ => CreateServiceImpl(
+ await gitHubClientFactory.CreateClient(cancellationToken));
///
- public IAuthenticatedGitHubService CreateService(string accessToken)
+ public async ValueTask CreateService(string accessToken, CancellationToken cancellationToken)
=> CreateServiceImpl(
- gitHubClientFactory.CreateClient(
- accessToken ?? throw new ArgumentNullException(nameof(accessToken))));
+ await gitHubClientFactory.CreateClient(
+ accessToken ?? throw new ArgumentNullException(nameof(accessToken)),
+ cancellationToken));
///
/// Create a .
diff --git a/src/Tgstation.Server.Host/Utils/GitHub/IGitHubClientFactory.cs b/src/Tgstation.Server.Host/Utils/GitHub/IGitHubClientFactory.cs
index ec95fd16ab8..1808b4ca3c3 100644
--- a/src/Tgstation.Server.Host/Utils/GitHub/IGitHubClientFactory.cs
+++ b/src/Tgstation.Server.Host/Utils/GitHub/IGitHubClientFactory.cs
@@ -1,4 +1,7 @@
-using Octokit;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Octokit;
namespace Tgstation.Server.Host.Utils.GitHub
{
@@ -10,14 +13,25 @@ public interface IGitHubClientFactory
///
/// Create a client. Low rate limit unless the server's GitHubAccessToken is set to bypass it.
///
+ /// The for the operation.
/// A new .
- IGitHubClient CreateClient();
+ ValueTask CreateClient(CancellationToken cancellationToken);
///
/// Create a client with authentication using a personal access token.
///
/// The GitHub personal access token.
+ /// The for the operation.
/// A new .
- IGitHubClient CreateClient(string accessToken);
+ ValueTask CreateClient(string accessToken, CancellationToken cancellationToken);
+
+ ///
+ /// Creates a GitHub App client for an installation.
+ ///
+ /// The private key .
+ /// The GitHub repository ID.
+ /// The for the operation.
+ /// A resulting in a new for the given or if authentication failed.
+ ValueTask CreateInstallationClient(string pem, long repositoryId, CancellationToken cancellationToken);
}
}
diff --git a/src/Tgstation.Server.Host/Utils/GitHub/IGitHubServiceFactory.cs b/src/Tgstation.Server.Host/Utils/GitHub/IGitHubServiceFactory.cs
index f4e7be70f55..adb0ec88ebd 100644
--- a/src/Tgstation.Server.Host/Utils/GitHub/IGitHubServiceFactory.cs
+++ b/src/Tgstation.Server.Host/Utils/GitHub/IGitHubServiceFactory.cs
@@ -1,4 +1,7 @@
-namespace Tgstation.Server.Host.Utils.GitHub
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Tgstation.Server.Host.Utils.GitHub
{
///
/// Factory for s.
@@ -8,14 +11,16 @@ public interface IGitHubServiceFactory
///
/// Create a .
///
- /// A new .
- public IGitHubService CreateService();
+ /// The for the operation.
+ /// A resulting in a new .
+ public ValueTask CreateService(CancellationToken cancellationToken);
///
/// Create an .
///
/// The access token to use for communication with GitHub.
- /// A new .
- public IAuthenticatedGitHubService CreateService(string accessToken);
+ /// The for the operation.
+ /// A resulting in a new .
+ public ValueTask CreateService(string accessToken, CancellationToken cancellationToken);
}
}
diff --git a/src/Tgstation.Server.Host/appsettings.yml b/src/Tgstation.Server.Host/appsettings.yml
index 732a61e54b2..353104e6de7 100644
--- a/src/Tgstation.Server.Host/appsettings.yml
+++ b/src/Tgstation.Server.Host/appsettings.yml
@@ -78,3 +78,7 @@ Swarm: # Should be left empty if using swarm mode is not desired
# PublicAddress: # The public address of the swarm node
# ControllerAddress: # Required on non-controller nodes. The internal address of the swarm controller's API'. Should be left empty on the controller itself
# UpdateRequiredNodeCount: # The number of nodes expected to be in the swarm before initiating an update. This should count every server irrespective of whether or not they are the controller MINUS 1
+Telemetry:
+ DisableVersionReporting: false # Prevents you installation and the version you're using from being reported on the source repository's deployments list
+ ServerFriendlyName: null # Sets a friendly name for your server in reported telemetry. Must be unique. First come first serve
+ VersionReportingRepositoryId: 841149827 # GitHub repostiory ID where the tgs_version_telemetry workflow can be found
diff --git a/tests/Tgstation.Server.Host.Tests/Setup/TestSetupWizard.cs b/tests/Tgstation.Server.Host.Tests/Setup/TestSetupWizard.cs
index 823036a10b3..cd6e5c86a02 100644
--- a/tests/Tgstation.Server.Host.Tests/Setup/TestSetupWizard.cs
+++ b/tests/Tgstation.Server.Host.Tests/Setup/TestSetupWizard.cs
@@ -191,6 +191,8 @@ public async Task TestWithUserStupidity()
"y",
// swarm config
"n",
+ // telemetry config
+ "n",
//saved, now for second run
//this time use defaults amap
String.Empty,
@@ -230,6 +232,9 @@ public async Task TestWithUserStupidity()
"privatekey",
"n",
"http://controller.com",
+ // telemetry config
+ "y",
+ "telemetry name",
//third run, we already hit all the code coverage so just get through it
String.Empty,
nameof(DatabaseType.MariaDB),
@@ -267,7 +272,9 @@ public async Task TestWithUserStupidity()
"https://controllerinternal.com",
"https://controllerpublic.com",
"privatekey",
- "y"
+ "y",
+ // telemetry config
+ "n",
};
var inputPos = 0;
diff --git a/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubClientFactory.cs b/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubClientFactory.cs
index 6f4fac0ae26..33058be75f4 100644
--- a/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubClientFactory.cs
+++ b/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubClientFactory.cs
@@ -1,5 +1,6 @@
using System;
using System.Net.Http.Headers;
+using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@@ -57,14 +58,14 @@ public async Task TestCreateBasicClient()
mockOptions.SetupGet(x => x.Value).Returns(gc);
var factory = new GitHubClientFactory(mockApp.Object, loggerFactory.CreateLogger(), mockOptions.Object);
- var client = factory.CreateClient();
+ var client = await factory.CreateClient(CancellationToken.None);
Assert.IsNotNull(client);
var credentials = await client.Connection.CredentialStore.GetCredentials();
Assert.AreEqual(AuthenticationType.Anonymous, credentials.AuthenticationType);
gc.GitHubAccessToken = "asdfasdfasdfasdfasdfasdf";
- client = factory.CreateClient();
+ client = await factory.CreateClient(CancellationToken.None);
Assert.IsNotNull(client);
credentials = await client.Connection.CredentialStore.GetCredentials();
@@ -83,9 +84,9 @@ public async Task TestCreateTokenClient()
mockOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration());
var factory = new GitHubClientFactory(mockApp.Object, loggerFactory.CreateLogger(), mockOptions.Object);
- Assert.ThrowsException(() => factory.CreateClient(null));
+ await Assert.ThrowsExceptionAsync(() => factory.CreateClient(null, CancellationToken.None).AsTask());
- var client = factory.CreateClient("asdf");
+ var client = await factory.CreateClient("asdf", CancellationToken.None);
Assert.IsNotNull(client);
var credentials = await client.Connection.CredentialStore.GetCredentials();
@@ -96,7 +97,7 @@ public async Task TestCreateTokenClient()
}
[TestMethod]
- public void TestClientCaching()
+ public async Task TestClientCaching()
{
var mockApp = new Mock();
mockApp.SetupGet(x => x.ProductInfoHeaderValue).Returns(new ProductInfoHeaderValue("TGSTests", "1.2.3")).Verifiable();
@@ -105,10 +106,10 @@ public void TestClientCaching()
mockOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration());
var factory = new GitHubClientFactory(mockApp.Object, loggerFactory.CreateLogger(), mockOptions.Object);
- var client1 = factory.CreateClient();
- var client2 = factory.CreateClient("asdf");
- var client3 = factory.CreateClient();
- var client4 = factory.CreateClient("asdf");
+ var client1 = await factory.CreateClient(CancellationToken.None);
+ var client2 = await factory.CreateClient("asdf", CancellationToken.None);
+ var client3 = await factory.CreateClient(CancellationToken.None);
+ var client4 = await factory.CreateClient("asdf", CancellationToken.None);
Assert.ReferenceEquals(client1, client3);
Assert.ReferenceEquals(client2, client4);
}
diff --git a/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubServiceFactory.cs b/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubServiceFactory.cs
index daa9afdd4c2..24212eaa936 100644
--- a/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubServiceFactory.cs
+++ b/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubServiceFactory.cs
@@ -1,4 +1,6 @@
using System;
+using System.Threading;
+using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -28,27 +30,29 @@ public void TestConstructor()
}
[TestMethod]
- public void TestCreateService()
+ public async Task TestCreateService()
{
var mockFactory = new Mock();
- mockFactory.Setup(x => x.CreateClient()).Returns(Mock.Of()).Verifiable();
+#pragma warning disable CA2012 // Use ValueTasks correctly
+ mockFactory.Setup(x => x.CreateClient(It.IsAny())).Returns(ValueTask.FromResult(Mock.Of())).Verifiable();
var mockToken = "asdf";
- mockFactory.Setup(x => x.CreateClient(mockToken)).Returns(Mock.Of()).Verifiable();
+ mockFactory.Setup(x => x.CreateClient(mockToken, It.IsAny())).Returns(ValueTask.FromResult(Mock.Of())).Verifiable();
+#pragma warning restore CA2012 // Use ValueTasks correctly
var mockOptions = new Mock>();
mockOptions.SetupGet(x => x.Value).Returns(new UpdatesConfiguration());
var factory = new GitHubServiceFactory(mockFactory.Object, Mock.Of(), mockOptions.Object);
- Assert.ThrowsException(() => factory.CreateService(null));
+ await Assert.ThrowsExceptionAsync(() => factory.CreateService(null, CancellationToken.None).AsTask());
Assert.AreEqual(0, mockFactory.Invocations.Count);
- var result1 = factory.CreateService();
+ var result1 = await factory.CreateService(CancellationToken.None);
Assert.IsNotNull(result1);
- var result2 = factory.CreateService(mockToken);
+ var result2 = factory.CreateService(mockToken, CancellationToken.None);
Assert.IsNotNull(result2);
mockFactory.VerifyAll();
diff --git a/tests/Tgstation.Server.Tests/Live/DummyGitHubServiceFactory.cs b/tests/Tgstation.Server.Tests/Live/DummyGitHubServiceFactory.cs
index 2f657577820..d05faed9a65 100644
--- a/tests/Tgstation.Server.Tests/Live/DummyGitHubServiceFactory.cs
+++ b/tests/Tgstation.Server.Tests/Live/DummyGitHubServiceFactory.cs
@@ -1,4 +1,6 @@
using System;
+using System.Threading;
+using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@@ -18,13 +20,13 @@ public DummyGitHubServiceFactory(ICryptographySuite cryptographySuite, ILogger CreateDummyService();
+ public ValueTask CreateService(CancellationToken cancellationToken) => ValueTask.FromResult(CreateDummyService());
- public IAuthenticatedGitHubService CreateService(string accessToken)
+ public ValueTask CreateService(string accessToken, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(accessToken);
- return CreateDummyService();
+ return ValueTask.FromResult(CreateDummyService());
}
TestingGitHubService CreateDummyService() => new TestingGitHubService(cryptographySuite, logger);
diff --git a/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs b/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs
index 645edac147d..f9ed0b926ab 100644
--- a/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs
+++ b/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs
@@ -155,6 +155,7 @@ public LiveTestingServer(SwarmConfiguration swarmConfiguration, bool enableOAuth
$"General:OpenDreamGitUrl={OpenDreamUrl}",
$"Security:TokenExpiryMinutes=120", // timeouts are useless for us
$"General:OpenDreamSuppressInstallOutput={TestingUtils.RunningInGitHubActions}",
+ "Telemetry:DisableVersionReporting=true",
};
swarmArgs = new List();
diff --git a/tests/Tgstation.Server.Tests/Live/TestingGitHubService.cs b/tests/Tgstation.Server.Tests/Live/TestingGitHubService.cs
index d104dbd6d3a..585dd569997 100644
--- a/tests/Tgstation.Server.Tests/Live/TestingGitHubService.cs
+++ b/tests/Tgstation.Server.Tests/Live/TestingGitHubService.cs
@@ -39,7 +39,7 @@ static TestingGitHubService()
});
var gitHubClientFactory = new GitHubClientFactory(new AssemblyInformationProvider(), Mock.Of>(), mockOptions.Object);
- RealClient = gitHubClientFactory.CreateClient();
+ RealClient = gitHubClientFactory.CreateClient(CancellationToken.None).GetAwaiter().GetResult();
}
public static async Task InitializeAndInject(CancellationToken cancellationToken)