diff --git a/services/jellyfin/deployment.yaml b/services/jellyfin/deployment.yaml
index afd5a74..86e313a 100644
--- a/services/jellyfin/deployment.yaml
+++ b/services/jellyfin/deployment.yaml
@@ -29,6 +29,44 @@ spec:
fsGroupChangePolicy: OnRootMismatch
runAsGroup: 65532
initContainers:
+ - name: fetch-oidc-plugin
+ image: alpine:3.20
+ securityContext:
+ runAsUser: 0
+ env:
+ - name: OIDC_PLUGIN_REPO
+ value: "registry.bstein.dev/streaming/oidc-plugin"
+ - name: OIDC_PLUGIN_TAG
+ value: "10.11.5"
+ - name: ORAS_USERNAME
+ valueFrom:
+ secretKeyRef:
+ name: harbor-robot
+ key: username
+ optional: true
+ - name: ORAS_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: harbor-robot
+ key: password
+ optional: true
+ volumeMounts:
+ - name: oidc-plugin
+ mountPath: /plugin-src
+ command: ["/bin/sh", "-c"]
+ args:
+ - |
+ set -euo pipefail
+ apk add --no-cache curl tar
+ ORAS_VERSION=1.2.0
+ curl -sSL "https://github.com/oras-project/oras/releases/download/v${ORAS_VERSION}/oras_${ORAS_VERSION}_linux_amd64.tar.gz" | tar -xz -C /usr/local/bin oras
+ ref="${OIDC_PLUGIN_REPO}:${OIDC_PLUGIN_TAG}"
+ cd /plugin-src
+ if [ -n "${ORAS_USERNAME:-}" ] && [ -n "${ORAS_PASSWORD:-}" ]; then
+ oras login "$(echo "${OIDC_PLUGIN_REPO}" | cut -d/ -f1)" -u "${ORAS_USERNAME}" -p "${ORAS_PASSWORD}"
+ fi
+ oras pull --allow-path-traversal "${ref}"
+ ls -lh /plugin-src
- name: install-oidc-plugin
image: alpine:3.20
securityContext:
@@ -36,8 +74,6 @@ spec:
env:
- name: OIDC_PLUGIN_VERSION
value: "1.0.2.0"
- - name: OIDC_PLUGIN_URL
- value: "https://raw.githubusercontent.com/lolerskatez/JellyfinOIDCPlugin/master/OIDC_Authentication_1.0.2.0.zip"
- name: OIDC_ISSUER
value: "https://sso.bstein.dev/realms/atlas"
- name: OIDC_REDIRECT_URI
@@ -45,7 +81,7 @@ spec:
- name: OIDC_LOGOUT_URI
value: "https://sso.bstein.dev/realms/atlas/protocol/openid-connect/logout?redirect_uri=https://stream.bstein.dev/"
- name: OIDC_SCOPES
- value: "openid,profile,email,groups"
+ value: "openid,profile,email"
- name: OIDC_ROLE_CLAIM
value: "groups"
- name: OIDC_CLIENT_ID
@@ -61,6 +97,8 @@ spec:
volumeMounts:
- name: config
mountPath: /config
+ - name: oidc-plugin
+ mountPath: /plugin-src
command: ["/bin/sh", "-c"]
args:
- |
@@ -70,16 +108,20 @@ spec:
exit 1
fi
rm -rf "/config/plugins/LDAP Authentication_20.0.0.0"
- apk add --no-cache wget unzip
+ apk add --no-cache unzip
plugin_dir="/config/plugins/OIDC Authentication_${OIDC_PLUGIN_VERSION}"
config_dir="/config/plugins/configurations"
- tmp_zip="$(mktemp)"
- echo "Downloading OIDC plugin ${OIDC_PLUGIN_VERSION} from ${OIDC_PLUGIN_URL}"
- wget -O "${tmp_zip}" "${OIDC_PLUGIN_URL}"
+ plugin_zip="/plugin-src/OIDC_Authentication_${OIDC_PLUGIN_VERSION}-net9.zip"
+ if [ ! -s "${plugin_zip}" ]; then
+ echo "Plugin zip missing at ${plugin_zip}" >&2
+ echo "Contents of /plugin-src:" >&2
+ ls -lah /plugin-src >&2 || true
+ exit 1
+ fi
rm -rf "${plugin_dir}"
mkdir -p "${plugin_dir}" "${config_dir}"
- unzip -o "${tmp_zip}" -d "${plugin_dir}"
- rm -f "${tmp_zip}"
+ unzip -o "${plugin_zip}" -d "${plugin_dir}"
+ rm -f "${plugin_dir}"/Microsoft.Extensions.*.dll
cat >"${plugin_dir}/meta.json" <<'EOF'
{
"category": "Authentication",
@@ -89,7 +131,7 @@ spec:
"name": "OIDC Authentication",
"overview": "Enable Single Sign-On (SSO) for Jellyfin using an OpenID Connect provider.",
"owner": "lolerskatez",
- "targetAbi": "10.11.3.0",
+ "targetAbi": "10.11.5.0",
"timestamp": "2025-12-17T04:00:00Z",
"version": "1.0.2.0",
"status": "Active",
@@ -104,7 +146,8 @@ spec:
[ -z "${trimmed}" ] && continue
scope_lines="${scope_lines} ${trimmed}\n"
done
- cat >"${config_dir}/OIDC Authentication.xml" <"${config_file}" <
${OIDC_ISSUER}
@@ -120,7 +163,7 @@ spec:
false
EOF
- chown -R 1000:65532 "${plugin_dir}" "${config_dir}/OIDC Authentication.xml"
+ chown -R 1000:65532 "${plugin_dir}" "${config_file}"
runtimeClassName: nvidia
containers:
- name: jellyfin
@@ -140,6 +183,22 @@ spec:
value: "65532"
- name: UMASK
value: "002"
+ lifecycle:
+ postStart:
+ exec:
+ command:
+ - /bin/sh
+ - -c
+ - |
+ set -e
+ target="/jellyfin/jellyfin-web/index.html"
+ marker='api/oidc/inject'
+ if grep -q "${marker}" "${target}"; then
+ exit 0
+ fi
+ tmp="$(mktemp)"
+ awk -v marker="${marker}" 'BEGIN{inserted=0} /<\/head>/ && !inserted {print " "; inserted=1} {print}' "${target}" > "${tmp}"
+ cp "${tmp}" "${target}"
resources:
limits:
nvidia.com/gpu: 1
@@ -157,6 +216,8 @@ spec:
- name: media
mountPath: /media
securityContext:
+ runAsUser: 0
+ runAsGroup: 0
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
volumes:
@@ -169,3 +230,5 @@ spec:
- name: media
persistentVolumeClaim:
claimName: jellyfin-media-asteria-new
+ - name: oidc-plugin
+ emptyDir: {}
diff --git a/services/jellyfin/oidc/Jenkinsfile b/services/jellyfin/oidc/Jenkinsfile
new file mode 100644
index 0000000..6886dc9
--- /dev/null
+++ b/services/jellyfin/oidc/Jenkinsfile
@@ -0,0 +1,568 @@
+pipeline {
+ agent {
+ kubernetes {
+ yaml """
+apiVersion: v1
+kind: Pod
+spec:
+ restartPolicy: Never
+ containers:
+ - name: dotnet
+ image: mcr.microsoft.com/dotnet/sdk:9.0
+ command:
+ - cat
+ tty: true
+"""
+ }
+ }
+ options {
+ timestamps()
+ }
+ parameters {
+ string(name: 'HARBOR_REPO', defaultValue: 'registry.bstein.dev/streaming/oidc-plugin', description: 'OCI repository for the plugin artifact')
+ string(name: 'JELLYFIN_VERSION', defaultValue: '10.11.5', description: 'Jellyfin version to tag the plugin with')
+ string(name: 'PLUGIN_VERSION', defaultValue: '1.0.2.0', description: 'Plugin version')
+ }
+ environment {
+ ORAS_VERSION = "1.2.0"
+ DOTNET_CLI_TELEMETRY_OPTOUT = "1"
+ DOTNET_SKIP_FIRST_TIME_EXPERIENCE = "1"
+ }
+ stages {
+ stage('Checkout') {
+ steps {
+ container('dotnet') {
+ checkout scm
+ }
+ }
+ }
+ stage('Build plugin') {
+ steps {
+ container('dotnet') {
+ sh '''
+ set -euo pipefail
+ apt-get update
+ apt-get install -y --no-install-recommends zip curl ca-certificates git
+ WORKDIR="$(pwd)/build"
+ SRC_DIR="${WORKDIR}/src"
+ DIST_DIR="${WORKDIR}/dist"
+ ART_DIR="${WORKDIR}/artifact"
+ rm -rf "${SRC_DIR}" "${DIST_DIR}" "${ART_DIR}"
+ mkdir -p "${SRC_DIR}" "${DIST_DIR}" "${ART_DIR}"
+ git clone https://github.com/lolerskatez/JellyfinOIDCPlugin.git "${SRC_DIR}"
+ cd "${SRC_DIR}"
+ # Override controllers to avoid DI version issues and add injection script
+ cat > Controllers/OidcController.cs <<'EOF'
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using IdentityModel.OidcClient;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace JellyfinOIDCPlugin.Controllers;
+
+#nullable enable
+
+[ApiController]
+[Route("api/oidc")]
+public class OidcController : ControllerBase
+{
+ private IUserManager UserManager => HttpContext.RequestServices.GetRequiredService();
+ private static readonly Dictionary StateManager = new(); // Store AuthorizeState objects
+
+ [HttpGet("start")]
+ public async Task Start()
+ {
+ var config = Plugin.Instance?.Configuration;
+ if (config == null)
+ {
+ return BadRequest("Plugin not initialized");
+ }
+
+ var options = new OidcClientOptions
+ {
+ Authority = config.OidEndpoint?.Trim(),
+ ClientId = config.OidClientId?.Trim(),
+ ClientSecret = config.OidSecret?.Trim(),
+ RedirectUri = GetRedirectUri(),
+ Scope = string.Join(" ", config.OidScopes)
+ };
+
+ try
+ {
+ var client = new OidcClient(options);
+ var result = await client.PrepareLoginAsync().ConfigureAwait(false);
+
+ // Store the authorize state for the callback
+ var stateString = (string)result.GetType().GetProperty("State")?.GetValue(result);
+ if (!string.IsNullOrEmpty(stateString))
+ {
+ StateManager[stateString] = result;
+ }
+
+ var startUrl = (string)result.GetType().GetProperty("StartUrl")?.GetValue(result);
+ if (string.IsNullOrEmpty(startUrl))
+ {
+ Console.WriteLine("OIDC: Could not get StartUrl from OIDC result");
+ return BadRequest("OIDC initialization failed");
+ }
+
+ return Redirect(startUrl);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"OIDC start error: {ex}");
+ return BadRequest("OIDC error: " + ex.Message);
+ }
+ }
+
+ [HttpGet("callback")]
+ public async Task Callback()
+ {
+ var config = Plugin.Instance?.Configuration;
+ if (config == null)
+ {
+ return BadRequest("Plugin not initialized");
+ }
+
+ try
+ {
+ var stateParam = Request.Query["state"].ToString();
+ if (string.IsNullOrEmpty(stateParam) || !StateManager.TryGetValue(stateParam, out var storedState))
+ {
+ Console.WriteLine($"OIDC: Invalid state {stateParam}");
+ return BadRequest("Invalid state");
+ }
+
+ var options = new OidcClientOptions
+ {
+ Authority = config.OidEndpoint?.Trim(),
+ ClientId = config.OidClientId?.Trim(),
+ ClientSecret = config.OidSecret?.Trim(),
+ RedirectUri = GetRedirectUri(),
+ Scope = string.Join(" ", config.OidScopes)
+ };
+
+ var client = new OidcClient(options);
+ // Cast stored state to AuthorizeState - it's stored as object
+ var authorizeState = (AuthorizeState)storedState;
+ var result = await client.ProcessResponseAsync(Request.QueryString.Value, authorizeState).ConfigureAwait(false);
+
+ if (result.IsError)
+ {
+ Console.WriteLine($"OIDC callback failed: {result.Error} - {result.ErrorDescription}");
+ return BadRequest("OIDC authentication failed");
+ }
+
+ // Get email from claims
+ var email = result.User?.FindFirst("email")?.Value ??
+ result.User?.FindFirst("preferred_username")?.Value ??
+ result.User?.FindFirst("sub")?.Value;
+
+ if (string.IsNullOrEmpty(email))
+ {
+ Console.WriteLine("OIDC: No email/username found in OIDC response");
+ return BadRequest("No email/username found in OIDC response");
+ }
+
+ // Get or create user
+ var user = UserManager.GetUserByName(email);
+ if (user == null)
+ {
+ Console.WriteLine($"OIDC: Creating new user {email}");
+ user = await UserManager.CreateUserAsync(email).ConfigureAwait(false);
+ }
+
+ // Set authentication provider
+ user.AuthenticationProviderId = "OIDC";
+
+ // Get roles from claims
+ var rolesClaimValue = result.User?.FindFirst(config.RoleClaim)?.Value;
+ var roles = string.IsNullOrEmpty(rolesClaimValue)
+ ? Array.Empty()
+ : rolesClaimValue.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
+
+ // Set permissions based on groups
+ var isAdmin = roles.Any(r => r.Equals("admin", StringComparison.OrdinalIgnoreCase));
+ var isPowerUser = roles.Any(r => r.Equals("Power User", StringComparison.OrdinalIgnoreCase)) && !isAdmin;
+
+ Console.WriteLine($"OIDC: User {email} authenticated. Admin: {isAdmin}, PowerUser: {isPowerUser}");
+
+ // Update user in database
+ await UserManager.UpdateUserAsync(user).ConfigureAwait(false);
+
+ StateManager.Remove(stateParam);
+
+ // Redirect to Jellyfin main page
+ return Redirect("/");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"OIDC callback error: {ex}");
+ return BadRequest("OIDC error: " + ex.Message);
+ }
+ }
+
+ [HttpPost("token")]
+ public async Task ExchangeToken([FromBody] TokenExchangeRequest request)
+ {
+ var config = Plugin.Instance?.Configuration;
+ if (config == null)
+ {
+ Console.WriteLine("OIDC: Plugin not initialized");
+ return BadRequest("Plugin not initialized");
+ }
+
+ if (string.IsNullOrEmpty(request?.AccessToken))
+ {
+ Console.WriteLine("OIDC: No access token provided");
+ return BadRequest("Access token is required");
+ }
+
+ try
+ {
+ Console.WriteLine("OIDC: Processing token exchange request");
+
+ // Validate the token with the OIDC provider using UserInfo endpoint
+ var options = new OidcClientOptions
+ {
+ Authority = config.OidEndpoint?.Trim(),
+ ClientId = config.OidClientId?.Trim(),
+ ClientSecret = config.OidSecret?.Trim(),
+ Scope = string.Join(" ", config.OidScopes)
+ };
+
+ var client = new OidcClient(options);
+
+ // Use the access token to get user info
+ var userInfoResult = await client.GetUserInfoAsync(request.AccessToken).ConfigureAwait(false);
+
+ if (userInfoResult.IsError)
+ {
+ Console.WriteLine($"OIDC: Failed to get user info: {userInfoResult.Error}");
+ return Unauthorized("Invalid access token");
+ }
+
+ // Extract email/username from user info
+ var email = userInfoResult.Claims.FirstOrDefault(c => c.Type == "email")?.Value ??
+ userInfoResult.Claims.FirstOrDefault(c => c.Type == "preferred_username")?.Value ??
+ userInfoResult.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;
+
+ if (string.IsNullOrEmpty(email))
+ {
+ Console.WriteLine("OIDC: No email/username found in token");
+ return BadRequest("No email/username found in token");
+ }
+
+ // Get or create user
+ var user = UserManager.GetUserByName(email);
+ if (user == null)
+ {
+ if (!config.AutoCreateUser)
+ {
+ Console.WriteLine($"OIDC: User {email} not found and auto-create disabled");
+ return Unauthorized("User does not exist and auto-creation is disabled");
+ }
+
+ Console.WriteLine($"OIDC: Creating new user from token {email}");
+ user = await UserManager.CreateUserAsync(email).ConfigureAwait(false);
+ }
+
+ // Update user authentication provider
+ user.AuthenticationProviderId = "OIDC";
+
+ // Get roles from claims
+ var rolesClaimName = config.RoleClaim ?? "groups";
+ var rolesClaimValue = userInfoResult.Claims.FirstOrDefault(c => c.Type == rolesClaimName)?.Value;
+ var roles = string.IsNullOrEmpty(rolesClaimValue)
+ ? Array.Empty()
+ : rolesClaimValue.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
+
+ // Set permissions based on groups
+ var isAdmin = roles.Any(r => r.Equals("admin", StringComparison.OrdinalIgnoreCase));
+ var isPowerUser = roles.Any(r => r.Equals("Power User", StringComparison.OrdinalIgnoreCase)) && !isAdmin;
+
+ Console.WriteLine($"OIDC: Token exchange for {email} Admin:{isAdmin} Power:{isPowerUser}");
+
+ // Update user in database
+ await UserManager.UpdateUserAsync(user).ConfigureAwait(false);
+
+ // Return success with user info
+ return Ok(new TokenExchangeResponse
+ {
+ Success = true,
+ UserId = user.Id.ToString(),
+ Username = user.Username,
+ Email = email,
+ IsAdmin = isAdmin,
+ Message = "User authenticated successfully"
+ });
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"OIDC token exchange error: {ex}");
+ return StatusCode(500, $"Token exchange failed: {ex.Message}");
+ }
+ }
+
+ private string GetRedirectUri()
+ {
+ var configured = Plugin.Instance?.Configuration?.RedirectUri;
+ if (!string.IsNullOrWhiteSpace(configured))
+ {
+ return configured!;
+ }
+
+ return $"{Request.Scheme}://{Request.Host}/api/oidc/callback";
+ }
+}
+
+public class TokenExchangeRequest
+{
+ public string? AccessToken { get; set; }
+ public string? IdToken { get; set; }
+}
+
+public class TokenExchangeResponse
+{
+ public bool Success { get; set; }
+ public string? UserId { get; set; }
+ public string? Username { get; set; }
+ public string? Email { get; set; }
+ public bool IsAdmin { get; set; }
+ public string? Message { get; set; }
+}
+EOF
+
+ cat > Controllers/OidcStaticController.cs <<'EOF'
+using System;
+using System.IO;
+using System.Reflection;
+using MediaBrowser.Common.Plugins;
+using Microsoft.AspNetCore.Mvc;
+
+namespace JellyfinOIDCPlugin.Controllers;
+
+[ApiController]
+[Route("api/oidc")]
+public class OidcStaticController : ControllerBase
+{
+ [HttpGet("login.js")]
+ public IActionResult GetLoginScript()
+ {
+ try
+ {
+ var assembly = Assembly.GetExecutingAssembly();
+ using var stream = assembly.GetManifestResourceStream("JellyfinOIDCPlugin.web.oidc-login.js");
+ if (stream == null)
+ {
+ Console.WriteLine("OIDC: Login script resource not found");
+ return NotFound();
+ }
+
+ using var reader = new StreamReader(stream);
+ var content = reader.ReadToEnd();
+
+ return Content(content, "application/javascript");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"OIDC: Error serving login script {ex}");
+ return StatusCode(500, "Error loading login script");
+ }
+ }
+
+ [HttpGet("loader.js")]
+ public IActionResult GetLoader()
+ {
+ try
+ {
+ var assembly = Assembly.GetExecutingAssembly();
+ using var stream = assembly.GetManifestResourceStream("JellyfinOIDCPlugin.web.oidc-loader.js");
+ if (stream == null)
+ {
+ Console.WriteLine("OIDC: Loader script resource not found");
+ return NotFound();
+ }
+
+ using var reader = new StreamReader(stream);
+ var content = reader.ReadToEnd();
+
+ return Content(content, "application/javascript");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"OIDC: Error serving loader script {ex}");
+ return StatusCode(500, "Error loading loader script");
+ }
+ }
+
+ [HttpGet("inject")]
+ public IActionResult GetInject()
+ {
+ try
+ {
+ var script = @"
+(function() {
+ console.log('[OIDC Plugin] Bootstrap inject started');
+
+ // Load oidc-loader.js dynamically
+ const loaderScript = document.createElement('script');
+ loaderScript.src = '/api/oidc/loader.js';
+ loaderScript.type = 'application/javascript';
+ loaderScript.onerror = function() {
+ console.error('[OIDC Plugin] Failed to load loader.js');
+ };
+ loaderScript.onload = function() {
+ console.log('[OIDC Plugin] Loader.js loaded successfully');
+ };
+
+ // Append to head or body
+ const target = document.head || document.documentElement;
+ target.appendChild(loaderScript);
+
+ console.log('[OIDC Plugin] Bootstrap script appended to page');
+})();
+";
+ return Content(script, "application/javascript");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"OIDC: Error serving inject script {ex}");
+ return StatusCode(500, "Error loading inject script");
+ }
+ }
+
+ [HttpGet("global.js")]
+ public IActionResult GetGlobalInjector()
+ {
+ try
+ {
+ var assembly = Assembly.GetExecutingAssembly();
+ using var stream = assembly.GetManifestResourceStream("JellyfinOIDCPlugin.web.oidc-global-injector.js");
+ if (stream == null)
+ {
+ Console.WriteLine("OIDC: Global injector resource not found");
+ return NotFound();
+ }
+
+ using var reader = new StreamReader(stream);
+ var content = reader.ReadToEnd();
+
+ return Content(content, "application/javascript");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"OIDC: Error serving global injector {ex}");
+ return StatusCode(500, "Error loading global injector");
+ }
+ }
+
+ [HttpGet("config")]
+ public IActionResult GetConfigurationPage()
+ {
+ try
+ {
+ var assembly = Assembly.GetExecutingAssembly();
+ using var stream = assembly.GetManifestResourceStream("JellyfinOIDCPlugin.web.configurationpage.html");
+ if (stream == null)
+ {
+ Console.WriteLine("OIDC: Configuration page resource not found");
+ return NotFound("Configuration page resource not found");
+ }
+
+ using var reader = new StreamReader(stream);
+ var content = reader.ReadToEnd();
+
+ return Content(content, "text/html");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"OIDC: Error serving configuration page {ex}");
+ return StatusCode(500, $"Error loading configuration page: {ex.Message}");
+ }
+ }
+}
+EOF
+ cat > JellyfinOIDCPlugin.csproj <<'EOF'
+
+
+ net9.0
+ JellyfinOIDCPlugin.v2
+ JellyfinOIDCPlugin
+ latest
+ enable
+ enable
+ 1.0.2.0
+ 1.0.2.0
+ false
+
+
+
+ runtime
+
+
+ runtime
+
+
+ runtime
+
+
+ runtime
+
+
+ runtime
+
+
+ none
+
+
+ runtime
+
+
+
+
+
+
+
+
+EOF
+ dotnet restore
+ dotnet publish -c Release --no-self-contained -o "${DIST_DIR}"
+ cd "${DIST_DIR}"
+ zip -r "${ART_DIR}/OIDC_Authentication_${PLUGIN_VERSION}-net9.zip" .
+ '''
+ }
+ }
+ }
+ stage('Push to Harbor') {
+ steps {
+ container('dotnet') {
+ withCredentials([usernamePassword(credentialsId: 'harbor-robot', usernameVariable: 'HARBOR_USERNAME', passwordVariable: 'HARBOR_PASSWORD')]) {
+ sh '''
+ set -euo pipefail
+ WORKDIR="$(pwd)/build"
+ ORAS_BIN="/usr/local/bin/oras"
+ curl -sSL "https://github.com/oras-project/oras/releases/download/v${ORAS_VERSION}/oras_${ORAS_VERSION}_linux_amd64.tar.gz" | tar -xz -C /usr/local/bin oras
+ ref_host="$(echo "${HARBOR_REPO}" | cut -d/ -f1)"
+ "${ORAS_BIN}" login "${ref_host}" -u "${HARBOR_USERNAME}" -p "${HARBOR_PASSWORD}"
+ artifact="${WORKDIR}/artifact/OIDC_Authentication_${PLUGIN_VERSION}-net9.zip"
+ "${ORAS_BIN}" push "${HARBOR_REPO}:${JELLYFIN_VERSION}" "${artifact}:application/zip" --artifact-type application/zip
+ "${ORAS_BIN}" push "${HARBOR_REPO}:latest" "${artifact}:application/zip" --artifact-type application/zip
+ '''
+ }
+ }
+ }
+ }
+ }
+ post {
+ always {
+ container('dotnet') {
+ archiveArtifacts artifacts: 'build/artifact/*.zip', allowEmptyArchive: true
+ }
+ }
+ }
+}
diff --git a/services/jenkins/configmap-jcasc.yaml b/services/jenkins/configmap-jcasc.yaml
index 958e8a8..615412e 100644
--- a/services/jenkins/configmap-jcasc.yaml
+++ b/services/jenkins/configmap-jcasc.yaml
@@ -66,6 +66,22 @@ data:
}
}
}
+ pipelineJob('jellyfin-oidc-plugin') {
+ definition {
+ cpsScm {
+ scm {
+ git {
+ remote {
+ url('https://scm.bstein.dev/bstein/titan-iac.git')
+ credentials('gitea-pat')
+ }
+ branches('*/main')
+ }
+ }
+ scriptPath('services/jellyfin/oidc/Jenkinsfile')
+ }
+ }
+ }
pipelineJob('ci-demo') {
triggers {
scm('H/1 * * * *')