website/docs: background tasks: add more detail about "next run" (cherry-pick #18660 to version-2025.10)
Cherry-pick of #18660 to version-2025.10 branch.
Original PR: #18660 Original Author: @dominic-r Cherry-picked commit: 888733a32cdfae95411e8a714ac6569ced9f7775
Deploy Preview for authentik-integrations canceled.
| Name | Link |
|---|---|
| Latest commit | 1af67b6be9de8bb65d6aaabdd18bb9e71a4f2a11 |
| Latest deploy log | https://app.netlify.com/projects/authentik-integrations/deploys/693a4f5dce868d0007782bbd |
Deploy Preview for authentik-docs canceled.
| Name | Link |
|---|---|
| Latest commit | 1af67b6be9de8bb65d6aaabdd18bb9e71a4f2a11 |
| Latest deploy log | https://app.netlify.com/projects/authentik-docs/deploys/693a4f5d9dfec40008369d81 |
:x: 1 Tests Failed:
| Tests completed | Failed | Passed | Skipped |
|---|---|---|---|
| 2203 | 1 | 2202 | 2 |
View the top 1 failed test(s) by shortest run time
tests.e2e.test_provider_saml.TestProviderSAML::test_sp_initiated_explicit_postStack Traces | 51.7s run time
self = <tests.e2e.test_provider_saml.TestProviderSAML testMethod=test_sp_initiated_explicit_post> args = (), kwargs = {} @wraps(func) def wrapper(self: TransactionTestCase, *args, **kwargs): """Run test again if we're below max_retries, including tearDown and setUp. Otherwise raise the error""" nonlocal count try: > return func(self, *args, **kwargs) tests/e2e/utils.py:324: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ args = (<tests.e2e.test_provider_saml.TestProviderSAML testMethod=test_sp_initiated_explicit_post>,) kwargs = {}, file = 'default/flow-default-invalidation-flow.yaml' content = 'version: 1\nmetadata:\n name: Default - Invalidation flow\nentries:\n- attrs:\n designation: invalidation\n na...0\n stage: !KeyOf default-invalidation-logout\n target: !KeyOf flow\n model: authentik_flows.flowstagebinding\n' @wraps(func) def wrapper(*args, **kwargs): for file in files: content = BlueprintInstance(path=file).retrieve() Importer.from_string(content).apply() > return func(*args, **kwargs) .../blueprints/tests/__init__.py:25: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ args = (<tests.e2e.test_provider_saml.TestProviderSAML testMethod=test_sp_initiated_explicit_post>,) kwargs = {} file = 'default/flow-default-provider-authorization-explicit-consent.yaml' content = 'version: 1\nmetadata:\n name: Default - Provider authorization flow (explicit consent)\nentries:\n- attrs:\n desi...e: !KeyOf default-provider-authorization-consent\n target: !KeyOf flow\n model: authentik_flows.flowstagebinding\n' @wraps(func) def wrapper(*args, **kwargs): for file in files: content = BlueprintInstance(path=file).retrieve() Importer.from_string(content).apply() > return func(*args, **kwargs) .../blueprints/tests/__init__.py:25: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ args = (<tests.e2e.test_provider_saml.TestProviderSAML testMethod=test_sp_initiated_explicit_post>,) kwargs = {}, file = 'system/providers-saml.yaml' content = 'version: 1\nmetadata:\n labels:\n blueprints.goauthentik.io/system: "true"\n name: System - SAML Provider - Mapp...rosoft..../identity/claims/windowsaccountname"\n expression: |\n return request.user.username\n' @wraps(func) def wrapper(*args, **kwargs): for file in files: content = BlueprintInstance(path=file).retrieve() Importer.from_string(content).apply() > return func(*args, **kwargs) .../blueprints/tests/__init__.py:25: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ args = (<tests.e2e.test_provider_saml.TestProviderSAML testMethod=test_sp_initiated_explicit_post>,) kwargs = {}, config = <AuthentikCryptoConfig: authentik_crypto> @wraps(func) def wrapper(*args, **kwargs): config = apps.get_app_config(app_name) if isinstance(config, ManagedAppConfig): config._on_startup_callback(None) > return func(*args, **kwargs) .../blueprints/tests/__init__.py:43: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <tests.e2e.test_provider_saml.TestProviderSAML testMethod=test_sp_initiated_explicit_post> @retry() @apply_blueprint( "default/flow-default-authentication-flow.yaml", "default/flow-default-invalidation-flow.yaml", ) @apply_blueprint( "default/flow-default-provider-authorization-explicit-consent.yaml", ) @apply_blueprint( "system/providers-saml.yaml", ) @reconcile_app("authentik_crypto") def test_sp_initiated_explicit_post(self): """test SAML Provider flow SP-initiated flow (explicit consent) (POST binding)""" # Bootstrap all needed objects authorization_flow = Flow.objects.get( slug="default-provider-authorization-explicit-consent" ) provider: SAMLProvider = SAMLProvider.objects.create( name=generate_id(), acs_url="http://localhost:9009/saml/acs", audience="authentik-e2e", issuer="authentik-e2e", sp_binding=SAMLBindings.POST, authorization_flow=authorization_flow, signing_kp=create_test_cert(), ) provider.property_mappings.set(SAMLPropertyMapping.objects.all()) provider.save() app = Application.objects.create( name="SAML", slug=generate_id(), provider=provider, ) self.setup_client(provider, True) self.driver.get("http://localhost:9009") self.login() self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "ak-flow-executor"))) flow_executor = self.get_shadow_root("ak-flow-executor") > consent_stage = self.get_shadow_root("ak-stage-consent", flow_executor) tests/e2e/test_provider_saml.py:310: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <tests.e2e.test_provider_saml.TestProviderSAML testMethod=test_sp_initiated_explicit_post> selector = 'ak-stage-consent' container = <selenium.webdriver.remote.shadowroot.ShadowRoot (session="6ac61bdb2a7967b816228b1e97f32a18", element="f.B7FC72B33C0E31B39CC115AE141E3309.d.580AA7F50B2C926C533B4E05AEE4580C.e.12")> def get_shadow_root( self, selector: str, container: WebElement | WebDriver | None = None ) -> WebElement: """Get shadow root element's inner shadowRoot""" if not container: container = self.driver > el = container.find_element(By.CSS_SELECTOR, selector) tests/e2e/utils.py:236: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <selenium.webdriver.remote.shadowroot.ShadowRoot (session="6ac61bdb2a7967b816228b1e97f32a18", element="f.B7FC72B33C0E31B39CC115AE141E3309.d.580AA7F50B2C926C533B4E05AEE4580C.e.12")> by = 'css selector', value = 'ak-stage-consent' def find_element(self, by: str = By.ID, value: str = None): """Find an element inside a shadow root given a By strategy and locator. Parameters: ----------- by : selenium.webdriver.common.by.By The locating strategy to use. Default is `By.ID`. Supported values include: - By.ID: Locate by element ID. - By.NAME: Locate by the `name` attribute. - By.XPATH: Locate by an XPath expression. - By.CSS_SELECTOR: Locate by a CSS selector. - By.CLASS_NAME: Locate by the `class` attribute. - By.TAG_NAME: Locate by the tag name (e.g., "input", "button"). - By.LINK_TEXT: Locate a link element by its exact text. - By.PARTIAL_LINK_TEXT: Locate a link element by partial text match. - RelativeBy: Locate elements relative to a specified root element. Example: -------- element = driver.find_element(By.ID, 'foo') Returns: ------- WebElement The first matching `WebElement` found on the page. """ if by == By.ID: by = By.CSS_SELECTOR value = f'[id="{value}"]' elif by == By.CLASS_NAME: by = By.CSS_SELECTOR value = f".{value}" elif by == By.NAME: by = By.CSS_SELECTOR value = f'[name="{value}"]' > return self._execute(Command.FIND_ELEMENT_FROM_SHADOW_ROOT, {"using": by, "value": value})["value"] .venv/lib/python3.13.../webdriver/remote/shadowroot.py:79: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <selenium.webdriver.remote.shadowroot.ShadowRoot (session="6ac61bdb2a7967b816228b1e97f32a18", element="f.B7FC72B33C0E31B39CC115AE141E3309.d.580AA7F50B2C926C533B4E05AEE4580C.e.12")> command = 'findElementFromShadowRoot' params = {'shadowId': 'f.B7FC72B33C0E31B39CC115AE141E3309.d.580AA7F50B2C926C533B4E05AEE4580C.e.12', 'using': 'css selector', 'value': 'ak-stage-consent'} def _execute(self, command, params=None): """Executes a command against the underlying HTML element. Args: command: The name of the command to _execute as a string. params: A dictionary of named parameters to send with the command. Returns: The command's JSON response loaded into a dictionary object. """ if not params: params = {} params["shadowId"] = self._id > return self.session.execute(command, params) .venv/lib/python3.13.../webdriver/remote/shadowroot.py:133: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <selenium.webdriver.remote.webdriver.WebDriver (session="6ac61bdb2a7967b816228b1e97f32a18")> driver_command = 'findElementFromShadowRoot' params = {'using': 'css selector', 'value': 'ak-stage-consent'} def execute(self, driver_command: str, params: dict = None) -> dict: """Sends a command to be executed by a command.CommandExecutor. Parameters: ----------- driver_command : str - The name of the command to execute as a string. params : dict - A dictionary of named Parameters to send with the command. Returns: -------- dict - The command's JSON response loaded into a dictionary object. """ params = self._wrap_value(params) if self.session_id: if not params: params = {"sessionId": self.session_id} elif "sessionId" not in params: params["sessionId"] = self.session_id response = self.command_executor.execute(driver_command, params) if response: > self.error_handler.check_response(response) .venv/lib/python3.13.../webdriver/remote/webdriver.py:448: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <selenium.webdriver.remote.errorhandler.ErrorHandler object at 0x7fcbe40ed480> response = {'status': 404, 'value': '{"value":{"error":"detached shadow root","message":"detached shadow root: detached shadow ro...unknown>\\n#28 0x559cea660269 \\u003Cunknown>\\n#29 0x7f01168ffaa4 \\u003Cunknown>\\n#30 0x7f011698ca64 __clone\\n"}}'} def check_response(self, response: Dict[str, Any]) -> None: """Checks that a JSON response from the WebDriver does not have an error. :Args: - response - The JSON response from the WebDriver server as a dictionary object. :Raises: If the response contains an error message. """ status = response.get("status", None) if not status or status == ErrorCode.SUCCESS: return value = None message = response.get("message", "") screen: str = response.get("screen", "") stacktrace = None if isinstance(status, int): value_json = response.get("value", None) if value_json and isinstance(value_json, str): import json try: value = json.loads(value_json) if len(value) == 1: value = value["value"] status = value.get("error", None) if not status: status = value.get("status", ErrorCode.UNKNOWN_ERROR) message = value.get("value") or value.get("message") if not isinstance(message, str): value = message message = message.get("message") else: message = value.get("message", None) except ValueError: pass exception_class: Type[WebDriverException] e = ErrorCode() error_codes = [item for item in dir(e) if not item.startswith("__")] for error_code in error_codes: error_info = getattr(ErrorCode, error_code) if isinstance(error_info, list) and status in error_info: exception_class = getattr(ExceptionMapping, error_code, WebDriverException) break else: exception_class = WebDriverException if not value: value = response["value"] if isinstance(value, str): raise exception_class(value) if message == "" and "message" in value: message = value["message"] screen = None # type: ignore[assignment] if "screen" in value: screen = value["screen"] stacktrace = None st_value = value.get("stackTrace") or value.get("stacktrace") if st_value: if isinstance(st_value, str): stacktrace = st_value.split("\n") else: stacktrace = [] try: for frame in st_value: line = frame.get("lineNumber", "") file = frame.get("fileName", "<anonymous>") if line: file = f"{file}:{line}" meth = frame.get("methodName", "<anonymous>") if "className" in frame: meth = f"{frame['className']}.{meth}" msg = " at %s (%s)" msg = msg % (meth, file) stacktrace.append(msg) except TypeError: pass if exception_class == UnexpectedAlertPresentException: alert_text = None if "data" in value: alert_text = value["data"].get("text") elif "alert" in value: alert_text = value["alert"].get("text") raise exception_class(message, screen, stacktrace, alert_text) # type: ignore[call-arg] # mypy is not smart enough here > raise exception_class(message, screen, stacktrace) E selenium.common.exceptions.DetachedShadowRootException: Message: detached shadow root: detached shadow root not found E (Session info: chrome=141.0.7390.122) E Stacktrace: E #0 0x559cea6618b2 <unknown> E #1 0x559cea0d1abb <unknown> E #2 0x559cea0e58b1 <unknown> E #3 0x559cea0e452c <unknown> E #4 0x559cea0e5dc9 <unknown> E #5 0x559cea0d90d4 <unknown> E #6 0x559cea0d7c9e <unknown> E #7 0x559cea0db1e0 <unknown> E #8 0x559cea0db2c5 <unknown> E #9 0x559cea121dfe <unknown> E #10 0x559cea122825 <unknown> E #11 0x559cea116a0a <unknown> E #12 0x559cea147321 <unknown> E #13 0x559cea1168e1 <unknown> E #14 0x559cea1474e2 <unknown> E #15 0x559cea169296 <unknown> E #16 0x559cea1470a7 <unknown> E #17 0x559cea114dc1 <unknown> E #18 0x559cea115ba5 <unknown> E #19 0x559cea62b82e <unknown> E #20 0x559cea62ec8f <unknown> E #21 0x559cea62e72c <unknown> E #22 0x559cea62f139 <unknown> E #23 0x559cea61520b <unknown> E #24 0x559cea62f4c4 <unknown> E #25 0x559cea5fea9d <unknown> E #26 0x559cea64e549 <unknown> E #27 0x559cea64e73f <unknown> E #28 0x559cea660269 <unknown> E #29 0x7f01168ffaa4 <unknown> E #30 0x7f011698ca64 __clone .venv/lib/python3.13.../webdriver/remote/errorhandler.py:232: DetachedShadowRootException During handling of the above exception, another exception occurred: self = <tests.e2e.test_provider_saml.TestProviderSAML testMethod=test_sp_initiated_explicit_post> args = (), kwargs = {} @wraps(func) def wrapper(self: TransactionTestCase, *args, **kwargs): """Run test again if we're below max_retries, including tearDown and setUp. Otherwise raise the error""" nonlocal count try: > return func(self, *args, **kwargs) tests/e2e/utils.py:324: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ args = (<tests.e2e.test_provider_saml.TestProviderSAML testMethod=test_sp_initiated_explicit_post>,) kwargs = {}, file = 'default/flow-default-invalidation-flow.yaml' content = 'version: 1\nmetadata:\n name: Default - Invalidation flow\nentries:\n- attrs:\n designation: invalidation\n na...0\n stage: !KeyOf default-invalidation-logout\n target: !KeyOf flow\n model: authentik_flows.flowstagebinding\n' @wraps(func) def wrapper(*args, **kwargs): for file in files: content = BlueprintInstance(path=file).retrieve() Importer.from_string(content).apply() > return func(*args, **kwargs) .../blueprints/tests/__init__.py:25: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ args = (<tests.e2e.test_provider_saml.TestProviderSAML testMethod=test_sp_initiated_explicit_post>,) kwargs = {} file = 'default/flow-default-provider-authorization-explicit-consent.yaml' content = 'version: 1\nmetadata:\n name: Default - Provider authorization flow (explicit consent)\nentries:\n- attrs:\n desi...e: !KeyOf default-provider-authorization-consent\n target: !KeyOf flow\n model: authentik_flows.flowstagebinding\n' @wraps(func) def wrapper(*args, **kwargs): for file in files: content = BlueprintInstance(path=file).retrieve() Importer.from_string(content).apply() > return func(*args, **kwargs) .../blueprints/tests/__init__.py:25: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ args = (<tests.e2e.test_provider_saml.TestProviderSAML testMethod=test_sp_initiated_explicit_post>,) kwargs = {}, file = 'system/providers-saml.yaml' content = 'version: 1\nmetadata:\n labels:\n blueprints.goauthentik.io/system: "true"\n name: System - SAML Provider - Mapp...rosoft..../identity/claims/windowsaccountname"\n expression: |\n return request.user.username\n' @wraps(func) def wrapper(*args, **kwargs): for file in files: content = BlueprintInstance(path=file).retrieve() Importer.from_string(content).apply() > return func(*args, **kwargs) .../blueprints/tests/__init__.py:25: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ args = (<tests.e2e.test_provider_saml.TestProviderSAML testMethod=test_sp_initiated_explicit_post>,) kwargs = {}, config = <AuthentikCryptoConfig: authentik_crypto> @wraps(func) def wrapper(*args, **kwargs): config = apps.get_app_config(app_name) if isinstance(config, ManagedAppConfig): config._on_startup_callback(None) > return func(*args, **kwargs) .../blueprints/tests/__init__.py:43: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <tests.e2e.test_provider_saml.TestProviderSAML testMethod=test_sp_initiated_explicit_post> @retry() @apply_blueprint( "default/flow-default-authentication-flow.yaml", "default/flow-default-invalidation-flow.yaml", ) @apply_blueprint( "default/flow-default-provider-authorization-explicit-consent.yaml", ) @apply_blueprint( "system/providers-saml.yaml", ) @reconcile_app("authentik_crypto") def test_sp_initiated_explicit_post(self): """test SAML Provider flow SP-initiated flow (explicit consent) (POST binding)""" # Bootstrap all needed objects authorization_flow = Flow.objects.get( slug="default-provider-authorization-explicit-consent" ) provider: SAMLProvider = SAMLProvider.objects.create( name=generate_id(), acs_url="http://localhost:9009/saml/acs", audience="authentik-e2e", issuer="authentik-e2e", sp_binding=SAMLBindings.POST, authorization_flow=authorization_flow, signing_kp=create_test_cert(), ) provider.property_mappings.set(SAMLPropertyMapping.objects.all()) provider.save() app = Application.objects.create( name="SAML", slug=generate_id(), provider=provider, ) self.setup_client(provider, True) self.driver.get("http://localhost:9009") self.login() self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "ak-flow-executor"))) > flow_executor = self.get_shadow_root("ak-flow-executor") tests/e2e/test_provider_saml.py:309: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <tests.e2e.test_provider_saml.TestProviderSAML testMethod=test_sp_initiated_explicit_post> selector = 'ak-flow-executor' container = <selenium.webdriver.remote.webdriver.WebDriver (session="9e438d304f3d5fee53df277c7760f856")> def get_shadow_root( self, selector: str, container: WebElement | WebDriver | None = None ) -> WebElement: """Get shadow root element's inner shadowRoot""" if not container: container = self.driver el = container.find_element(By.CSS_SELECTOR, selector) > return el.shadow_root tests/e2e/utils.py:237: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <selenium.webdriver.remote.webelement.WebElement (session="9e438d304f3d5fee53df277c7760f856", element="f.B799BB078E1934CD7BBA13FAC7A11690.d.3555550DF635DD5D82AE1C4A021ED586.e.11")> @property def shadow_root(self) -> ShadowRoot: """Returns a shadow root of the element if there is one or an error. Only works from Chromium 96, Firefox 96, and Safari 16.4 onwards. Returns: ------- ShadowRoot : object Raises: ------- NoSuchShadowRoot - if no shadow root was attached to element Example: -------- >>> try: ... shadow_root = element.shadow_root >>> except NoSuchShadowRoot: ... print("No shadow root attached to element") """ > return self._execute(Command.GET_SHADOW_ROOT)["value"] .venv/lib/python3.13.../webdriver/remote/webelement.py:327: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <selenium.webdriver.remote.webelement.WebElement (session="9e438d304f3d5fee53df277c7760f856", element="f.B799BB078E1934CD7BBA13FAC7A11690.d.3555550DF635DD5D82AE1C4A021ED586.e.11")> command = 'getShadowRoot' params = {'id': 'f.B799BB078E1934CD7BBA13FAC7A11690.d.3555550DF635DD5D82AE1C4A021ED586.e.11'} def _execute(self, command, params=None): """Executes a command against the underlying HTML element. Parameters: ----------- command : any The name of the command to _execute as a string. params : dict A dictionary of named Parameters to send with the command. Returns: ------- The command's JSON response loaded into a dictionary object. """ if not params: params = {} params["id"] = self._id > return self._parent.execute(command, params) .venv/lib/python3.13.../webdriver/remote/webelement.py:572: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <selenium.webdriver.remote.webdriver.WebDriver (session="9e438d304f3d5fee53df277c7760f856")> driver_command = 'getShadowRoot', params = {} def execute(self, driver_command: str, params: dict = None) -> dict: """Sends a command to be executed by a command.CommandExecutor. Parameters: ----------- driver_command : str - The name of the command to execute as a string. params : dict - A dictionary of named Parameters to send with the command. Returns: -------- dict - The command's JSON response loaded into a dictionary object. """ params = self._wrap_value(params) if self.session_id: if not params: params = {"sessionId": self.session_id} elif "sessionId" not in params: params["sessionId"] = self.session_id response = self.command_executor.execute(driver_command, params) if response: > self.error_handler.check_response(response) .venv/lib/python3.13.../webdriver/remote/webdriver.py:448: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <selenium.webdriver.remote.errorhandler.ErrorHandler object at 0x7fcbe4312030> response = {'status': 404, 'value': '{"value":{"error":"stale element reference","message":"stale element reference: stale elemen...unknown>\\n#25 0x5639350f1269 \\u003Cunknown>\\n#26 0x7f0ded519aa4 \\u003Cunknown>\\n#27 0x7f0ded5a6a64 __clone\\n"}}'} def check_response(self, response: Dict[str, Any]) -> None: """Checks that a JSON response from the WebDriver does not have an error. :Args: - response - The JSON response from the WebDriver server as a dictionary object. :Raises: If the response contains an error message. """ status = response.get("status", None) if not status or status == ErrorCode.SUCCESS: return value = None message = response.get("message", "") screen: str = response.get("screen", "") stacktrace = None if isinstance(status, int): value_json = response.get("value", None) if value_json and isinstance(value_json, str): import json try: value = json.loads(value_json) if len(value) == 1: value = value["value"] status = value.get("error", None) if not status: status = value.get("status", ErrorCode.UNKNOWN_ERROR) message = value.get("value") or value.get("message") if not isinstance(message, str): value = message message = message.get("message") else: message = value.get("message", None) except ValueError: pass exception_class: Type[WebDriverException] e = ErrorCode() error_codes = [item for item in dir(e) if not item.startswith("__")] for error_code in error_codes: error_info = getattr(ErrorCode, error_code) if isinstance(error_info, list) and status in error_info: exception_class = getattr(ExceptionMapping, error_code, WebDriverException) break else: exception_class = WebDriverException if not value: value = response["value"] if isinstance(value, str): raise exception_class(value) if message == "" and "message" in value: message = value["message"] screen = None # type: ignore[assignment] if "screen" in value: screen = value["screen"] stacktrace = None st_value = value.get("stackTrace") or value.get("stacktrace") if st_value: if isinstance(st_value, str): stacktrace = st_value.split("\n") else: stacktrace = [] try: for frame in st_value: line = frame.get("lineNumber", "") file = frame.get("fileName", "<anonymous>") if line: file = f"{file}:{line}" meth = frame.get("methodName", "<anonymous>") if "className" in frame: meth = f"{frame['className']}.{meth}" msg = " at %s (%s)" msg = msg % (meth, file) stacktrace.append(msg) except TypeError: pass if exception_class == UnexpectedAlertPresentException: alert_text = None if "data" in value: alert_text = value["data"].get("text") elif "alert" in value: alert_text = value["alert"].get("text") raise exception_class(message, screen, stacktrace, alert_text) # type: ignore[call-arg] # mypy is not smart enough here > raise exception_class(message, screen, stacktrace) E selenium.common.exceptions.StaleElementReferenceException: Message: stale element reference: stale element not found E (Session info: chrome=141.0.7390.122); For documentation on this error, please visit: https://www.selenium..../webdriver/troubleshooting/errors#stale-element-reference-exception E Stacktrace: E #0 0x5639350f28b2 <unknown> E #1 0x563934b62abb <unknown> E #2 0x563934b768b1 <unknown> E #3 0x563934b753f3 <unknown> E #4 0x563934b76dc9 <unknown> E #5 0x563934b6a0d4 <unknown> E #6 0x563934b68c9e <unknown> E #7 0x563934b6c1e0 <unknown> E #8 0x563934ba7bb3 <unknown> E #9 0x563934bd82f6 <unknown> E #10 0x563934ba78e1 <unknown> E #11 0x563934bd84e2 <unknown> E #12 0x563934bfa296 <unknown> E #13 0x563934bd80a7 <unknown> E #14 0x563934ba5dc1 <unknown> E #15 0x563934ba6ba5 <unknown> E #16 0x5639350bc82e <unknown> E #17 0x5639350bfc8f <unknown> E #18 0x5639350bf72c <unknown> E #19 0x5639350c0139 <unknown> E #20 0x5639350a620b <unknown> E #21 0x5639350c04c4 <unknown> E #22 0x56393508fa9d <unknown> E #23 0x5639350df549 <unknown> E #24 0x5639350df73f <unknown> E #25 0x5639350f1269 <unknown> E #26 0x7f0ded519aa4 <unknown> E #27 0x7f0ded5a6a64 __clone .venv/lib/python3.13.../webdriver/remote/errorhandler.py:232: StaleElementReferenceException During handling of the above exception, another exception occurred: self = <unittest.case._Outcome object at 0x7fcbe40ef130> test_case = <tests.e2e.test_provider_saml.TestProviderSAML testMethod=test_sp_initiated_explicit_post> subTest = False @contextlib.contextmanager def testPartExecutor(self, test_case, subTest=False): old_success = self.success self.success = True try: > yield .../hostedtoolcache/Python/3.13.9........./x64/lib/python3.13/unittest/case.py:58: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <tests.e2e.test_provider_saml.TestProviderSAML testMethod=test_sp_initiated_explicit_post> result = <TestCaseFunction test_sp_initiated_explicit_post> def run(self, result=None): if result is None: result = self.defaultTestResult() startTestRun = getattr(result, 'startTestRun', None) stopTestRun = getattr(result, 'stopTestRun', None) if startTestRun is not None: startTestRun() else: stopTestRun = None result.startTest(self) try: testMethod = getattr(self, self._testMethodName) if (getattr(self.__class__, "__unittest_skip__", False) or getattr(testMethod, "__unittest_skip__", False)): # If the class or method was skipped. skip_why = (getattr(self.__class__, '__unittest_skip_why__', '') or getattr(testMethod, '__unittest_skip_why__', '')) _addSkip(result, self, skip_why) return result expecting_failure = ( getattr(self, "__unittest_expecting_failure__", False) or getattr(testMethod, "__unittest_expecting_failure__", False) ) outcome = _Outcome(result) start_time = time.perf_counter() try: self._outcome = outcome with outcome.testPartExecutor(self): self._callSetUp() if outcome.success: outcome.expecting_failure = expecting_failure with outcome.testPartExecutor(self): > self._callTestMethod(testMethod) .../hostedtoolcache/Python/3.13.9........./x64/lib/python3.13/unittest/case.py:651: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <tests.e2e.test_provider_saml.TestProviderSAML testMethod=test_sp_initiated_explicit_post> method = <bound method TestProviderSAML.test_sp_initiated_explicit_post of <tests.e2e.test_provider_saml.TestProviderSAML testMethod=test_sp_initiated_explicit_post>> def _callTestMethod(self, method): > if method() is not None: .../hostedtoolcache/Python/3.13.9........./x64/lib/python3.13/unittest/case.py:606: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <tests.e2e.test_provider_saml.TestProviderSAML testMethod=test_sp_initiated_explicit_post> args = (), kwargs = {} @wraps(func) def wrapper(self: TransactionTestCase, *args, **kwargs): """Run test again if we're below max_retries, including tearDown and setUp. Otherwise raise the error""" nonlocal count try: return func(self, *args, **kwargs) except tuple(exceptions) as exc: count += 1 if count > max_retires: logger.debug("Exceeded retry count", exc=exc, test=self) raise exc logger.debug("Retrying on error", exc=exc, test=self) self.tearDown() self._post_teardown() self._pre_setup() self.setUp() > return wrapper(self, *args, **kwargs) tests/e2e/utils.py:337: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <tests.e2e.test_provider_saml.TestProviderSAML testMethod=test_sp_initiated_explicit_post> args = (), kwargs = {} @wraps(func) def wrapper(self: TransactionTestCase, *args, **kwargs): """Run test again if we're below max_retries, including tearDown and setUp. Otherwise raise the error""" nonlocal count try: return func(self, *args, **kwargs) except tuple(exceptions) as exc: count += 1 if count > max_retires: logger.debug("Exceeded retry count", exc=exc, test=self) raise exc logger.debug("Retrying on error", exc=exc, test=self) self.tearDown() self._post_teardown() self._pre_setup() self.setUp() > return wrapper(self, *args, **kwargs) tests/e2e/utils.py:337: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <tests.e2e.test_provider_saml.TestProviderSAML testMethod=test_sp_initiated_explicit_post> args = (), kwargs = {} @wraps(func) def wrapper(self: TransactionTestCase, *args, **kwargs): """Run test again if we're below max_retries, including tearDown and setUp. Otherwise raise the error""" nonlocal count try: return func(self, *args, **kwargs) except tuple(exceptions) as exc: count += 1 if count > max_retires: logger.debug("Exceeded retry count", exc=exc, test=self) > raise exc tests/e2e/utils.py:331: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <tests.e2e.test_provider_saml.TestProviderSAML testMethod=test_sp_initiated_explicit_post> args = (), kwargs = {} @wraps(func) def wrapper(self: TransactionTestCase, *args, **kwargs): """Run test again if we're below max_retries, including tearDown and setUp. Otherwise raise the error""" nonlocal count try: > return func(self, *args, **kwargs) tests/e2e/utils.py:324: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ args = (<tests.e2e.test_provider_saml.TestProviderSAML testMethod=test_sp_initiated_explicit_post>,) kwargs = {}, file = 'default/flow-default-invalidation-flow.yaml' content = 'version: 1\nmetadata:\n name: Default - Invalidation flow\nentries:\n- attrs:\n designation: invalidation\n na...0\n stage: !KeyOf default-invalidation-logout\n target: !KeyOf flow\n model: authentik_flows.flowstagebinding\n' @wraps(func) def wrapper(*args, **kwargs): for file in files: content = BlueprintInstance(path=file).retrieve() Importer.from_string(content).apply() > return func(*args, **kwargs) .../blueprints/tests/__init__.py:25: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ args = (<tests.e2e.test_provider_saml.TestProviderSAML testMethod=test_sp_initiated_explicit_post>,) kwargs = {} file = 'default/flow-default-provider-authorization-explicit-consent.yaml' content = 'version: 1\nmetadata:\n name: Default - Provider authorization flow (explicit consent)\nentries:\n- attrs:\n desi...e: !KeyOf default-provider-authorization-consent\n target: !KeyOf flow\n model: authentik_flows.flowstagebinding\n' @wraps(func) def wrapper(*args, **kwargs): for file in files: content = BlueprintInstance(path=file).retrieve() Importer.from_string(content).apply() > return func(*args, **kwargs) .../blueprints/tests/__init__.py:25: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ args = (<tests.e2e.test_provider_saml.TestProviderSAML testMethod=test_sp_initiated_explicit_post>,) kwargs = {}, file = 'system/providers-saml.yaml' content = 'version: 1\nmetadata:\n labels:\n blueprints.goauthentik.io/system: "true"\n name: System - SAML Provider - Mapp...rosoft..../identity/claims/windowsaccountname"\n expression: |\n return request.user.username\n' @wraps(func) def wrapper(*args, **kwargs): for file in files: content = BlueprintInstance(path=file).retrieve() Importer.from_string(content).apply() > return func(*args, **kwargs) .../blueprints/tests/__init__.py:25: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ args = (<tests.e2e.test_provider_saml.TestProviderSAML testMethod=test_sp_initiated_explicit_post>,) kwargs = {}, config = <AuthentikCryptoConfig: authentik_crypto> @wraps(func) def wrapper(*args, **kwargs): config = apps.get_app_config(app_name) if isinstance(config, ManagedAppConfig): config._on_startup_callback(None) > return func(*args, **kwargs) .../blueprints/tests/__init__.py:43: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <tests.e2e.test_provider_saml.TestProviderSAML testMethod=test_sp_initiated_explicit_post> @retry() @apply_blueprint( "default/flow-default-authentication-flow.yaml", "default/flow-default-invalidation-flow.yaml", ) @apply_blueprint( "default/flow-default-provider-authorization-explicit-consent.yaml", ) @apply_blueprint( "system/providers-saml.yaml", ) @reconcile_app("authentik_crypto") def test_sp_initiated_explicit_post(self): """test SAML Provider flow SP-initiated flow (explicit consent) (POST binding)""" # Bootstrap all needed objects authorization_flow = Flow.objects.get( slug="default-provider-authorization-explicit-consent" ) provider: SAMLProvider = SAMLProvider.objects.create( name=generate_id(), acs_url="http://localhost:9009/saml/acs", audience="authentik-e2e", issuer="authentik-e2e", sp_binding=SAMLBindings.POST, authorization_flow=authorization_flow, signing_kp=create_test_cert(), ) provider.property_mappings.set(SAMLPropertyMapping.objects.all()) provider.save() app = Application.objects.create( name="SAML", slug=generate_id(), provider=provider, ) self.setup_client(provider, True) self.driver.get("http://localhost:9009") self.login() self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "ak-flow-executor"))) flow_executor = self.get_shadow_root("ak-flow-executor") > consent_stage = self.get_shadow_root("ak-stage-consent", flow_executor) tests/e2e/test_provider_saml.py:310: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <tests.e2e.test_provider_saml.TestProviderSAML testMethod=test_sp_initiated_explicit_post> selector = 'ak-stage-consent' container = <selenium.webdriver.remote.shadowroot.ShadowRoot (session="9d03a57d00207f5889718dd05bf33e06", element="f.BB645C3576A3C57141A85297DFCAC976.d.1C2F95DB672F64EDE632B4954C12FF23.e.14")> def get_shadow_root( self, selector: str, container: WebElement | WebDriver | None = None ) -> WebElement: """Get shadow root element's inner shadowRoot""" if not container: container = self.driver > el = container.find_element(By.CSS_SELECTOR, selector) tests/e2e/utils.py:236: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <selenium.webdriver.remote.shadowroot.ShadowRoot (session="9d03a57d00207f5889718dd05bf33e06", element="f.BB645C3576A3C57141A85297DFCAC976.d.1C2F95DB672F64EDE632B4954C12FF23.e.14")> by = 'css selector', value = 'ak-stage-consent' def find_element(self, by: str = By.ID, value: str = None): """Find an element inside a shadow root given a By strategy and locator. Parameters: ----------- by : selenium.webdriver.common.by.By The locating strategy to use. Default is `By.ID`. Supported values include: - By.ID: Locate by element ID. - By.NAME: Locate by the `name` attribute. - By.XPATH: Locate by an XPath expression. - By.CSS_SELECTOR: Locate by a CSS selector. - By.CLASS_NAME: Locate by the `class` attribute. - By.TAG_NAME: Locate by the tag name (e.g., "input", "button"). - By.LINK_TEXT: Locate a link element by its exact text. - By.PARTIAL_LINK_TEXT: Locate a link element by partial text match. - RelativeBy: Locate elements relative to a specified root element. Example: -------- element = driver.find_element(By.ID, 'foo') Returns: ------- WebElement The first matching `WebElement` found on the page. """ if by == By.ID: by = By.CSS_SELECTOR value = f'[id="{value}"]' elif by == By.CLASS_NAME: by = By.CSS_SELECTOR value = f".{value}" elif by == By.NAME: by = By.CSS_SELECTOR value = f'[name="{value}"]' > return self._execute(Command.FIND_ELEMENT_FROM_SHADOW_ROOT, {"using": by, "value": value})["value"] .venv/lib/python3.13.../webdriver/remote/shadowroot.py:79: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <selenium.webdriver.remote.shadowroot.ShadowRoot (session="9d03a57d00207f5889718dd05bf33e06", element="f.BB645C3576A3C57141A85297DFCAC976.d.1C2F95DB672F64EDE632B4954C12FF23.e.14")> command = 'findElementFromShadowRoot' params = {'shadowId': 'f.BB645C3576A3C57141A85297DFCAC976.d.1C2F95DB672F64EDE632B4954C12FF23.e.14', 'using': 'css selector', 'value': 'ak-stage-consent'} def _execute(self, command, params=None): """Executes a command against the underlying HTML element. Args: command: The name of the command to _execute as a string. params: A dictionary of named parameters to send with the command. Returns: The command's JSON response loaded into a dictionary object. """ if not params: params = {} params["shadowId"] = self._id > return self.session.execute(command, params) .venv/lib/python3.13.../webdriver/remote/shadowroot.py:133: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <selenium.webdriver.remote.webdriver.WebDriver (session="9d03a57d00207f5889718dd05bf33e06")> driver_command = 'findElementFromShadowRoot' params = {'using': 'css selector', 'value': 'ak-stage-consent'} def execute(self, driver_command: str, params: dict = None) -> dict: """Sends a command to be executed by a command.CommandExecutor. Parameters: ----------- driver_command : str - The name of the command to execute as a string. params : dict - A dictionary of named Parameters to send with the command. Returns: -------- dict - The command's JSON response loaded into a dictionary object. """ params = self._wrap_value(params) if self.session_id: if not params: params = {"sessionId": self.session_id} elif "sessionId" not in params: params["sessionId"] = self.session_id response = self.command_executor.execute(driver_command, params) if response: > self.error_handler.check_response(response) .venv/lib/python3.13.../webdriver/remote/webdriver.py:448: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <selenium.webdriver.remote.errorhandler.ErrorHandler object at 0x7fcbe427c450> response = {'status': 404, 'value': '{"value":{"error":"detached shadow root","message":"detached shadow root: detached shadow ro...unknown>\\n#28 0x558a262b6269 \\u003Cunknown>\\n#29 0x7f5452041aa4 \\u003Cunknown>\\n#30 0x7f54520cea64 __clone\\n"}}'} def check_response(self, response: Dict[str, Any]) -> None: """Checks that a JSON response from the WebDriver does not have an error. :Args: - response - The JSON response from the WebDriver server as a dictionary object. :Raises: If the response contains an error message. """ status = response.get("status", None) if not status or status == ErrorCode.SUCCESS: return value = None message = response.get("message", "") screen: str = response.get("screen", "") stacktrace = None if isinstance(status, int): value_json = response.get("value", None) if value_json and isinstance(value_json, str): import json try: value = json.loads(value_json) if len(value) == 1: value = value["value"] status = value.get("error", None) if not status: status = value.get("status", ErrorCode.UNKNOWN_ERROR) message = value.get("value") or value.get("message") if not isinstance(message, str): value = message message = message.get("message") else: message = value.get("message", None) except ValueError: pass exception_class: Type[WebDriverException] e = ErrorCode() error_codes = [item for item in dir(e) if not item.startswith("__")] for error_code in error_codes: error_info = getattr(ErrorCode, error_code) if isinstance(error_info, list) and status in error_info: exception_class = getattr(ExceptionMapping, error_code, WebDriverException) break else: exception_class = WebDriverException if not value: value = response["value"] if isinstance(value, str): raise exception_class(value) if message == "" and "message" in value: message = value["message"] screen = None # type: ignore[assignment] if "screen" in value: screen = value["screen"] stacktrace = None st_value = value.get("stackTrace") or value.get("stacktrace") if st_value: if isinstance(st_value, str): stacktrace = st_value.split("\n") else: stacktrace = [] try: for frame in st_value: line = frame.get("lineNumber", "") file = frame.get("fileName", "<anonymous>") if line: file = f"{file}:{line}" meth = frame.get("methodName", "<anonymous>") if "className" in frame: meth = f"{frame['className']}.{meth}" msg = " at %s (%s)" msg = msg % (meth, file) stacktrace.append(msg) except TypeError: pass if exception_class == UnexpectedAlertPresentException: alert_text = None if "data" in value: alert_text = value["data"].get("text") elif "alert" in value: alert_text = value["alert"].get("text") raise exception_class(message, screen, stacktrace, alert_text) # type: ignore[call-arg] # mypy is not smart enough here > raise exception_class(message, screen, stacktrace) E selenium.common.exceptions.DetachedShadowRootException: Message: detached shadow root: detached shadow root not found E (Session info: chrome=141.0.7390.122) E Stacktrace: E #0 0x558a262b78b2 <unknown> E #1 0x558a25d27abb <unknown> E #2 0x558a25d3b8b1 <unknown> E #3 0x558a25d3a52c <unknown> E #4 0x558a25d3bdc9 <unknown> E #5 0x558a25d2f0d4 <unknown> E #6 0x558a25d2dc9e <unknown> E #7 0x558a25d311e0 <unknown> E #8 0x558a25d312c5 <unknown> E #9 0x558a25d77dfe <unknown> E #10 0x558a25d78825 <unknown> E #11 0x558a25d6ca0a <unknown> E #12 0x558a25d9d321 <unknown> E #13 0x558a25d6c8e1 <unknown> E #14 0x558a25d9d4e2 <unknown> E #15 0x558a25dbf296 <unknown> E #16 0x558a25d9d0a7 <unknown> E #17 0x558a25d6adc1 <unknown> E #18 0x558a25d6bba5 <unknown> E #19 0x558a2628182e <unknown> E #20 0x558a26284c8f <unknown> E #21 0x558a2628472c <unknown> E #22 0x558a26285139 <unknown> E #23 0x558a2626b20b <unknown> E #24 0x558a262854c4 <unknown> E #25 0x558a26254a9d <unknown> E #26 0x558a262a4549 <unknown> E #27 0x558a262a473f <unknown> E #28 0x558a262b6269 <unknown> E #29 0x7f5452041aa4 <unknown> E #30 0x7f54520cea64 __clone .venv/lib/python3.13.../webdriver/remote/errorhandler.py:232: DetachedShadowRootException
To view more test analytics, go to the Test Analytics Dashboard 📋 Got 3 mins? Take this short survey to help us improve Test Analytics.
authentik PR Installation instructions
Instructions for docker-compose
Add the following block to your .env file:
AUTHENTIK_IMAGE=ghcr.io/goauthentik/dev-server
AUTHENTIK_TAG=gh-39744a0891b80f366b185a38408a937f9640da62
AUTHENTIK_OUTPOSTS__CONTAINER_IMAGE_BASE=ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s
Afterwards, run the upgrade commands from the latest release notes.
Instructions for Kubernetes
Add the following block to your values.yml file:
authentik:
outposts:
container_image_base: ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s
global:
image:
repository: ghcr.io/goauthentik/dev-server
tag: gh-39744a0891b80f366b185a38408a937f9640da62
Afterwards, run the upgrade commands from the latest release notes.