Redux-inspired UI framework for discord.py
Project description
Build predictable, state-driven interfaces with discord.py.
A flexible, Redux-inspired UI framework that introduces centralized state, composable components, ownership control, and predictable data flow to Discord applications.
Why CascadeUI
Interactive Discord UIs become difficult to manage as they grow. State accumulates across
Viewsubclass attributes, components stop responding after bot restarts, multi-step forms lose data between pages, and sharing data between views requires manualmessage.edit()plumbing in every callback.
CascadeUI introduces structure built on a Redux-inspired core:
- Centralized state instead of scattered view attributes, so every view reads from a single source of truth and stays in sync automatically.
- Predictable updates through dispatched actions, with one way to change state and one way to read it. No callback spaghetti.
- Clear separation between logic and presentation. Reducers handle data, views render it, and neither knows about the other.
- Reusable UI patterns instead of one-off implementations. Menus, pagination, forms, wizards, tabs, and persistent panels are first-class library primitives.
- Built-in interaction control for ownership, instance limits, and navigation. Restrict who can click what, cap how many concurrent instances a user or guild can hold, and push, pop, or replace views without tracking message history by hand.
- Persistence, undo/redo, and lifecycle handling without the boilerplate. Components survive bot restarts, state history is one method call away, and session cleanup happens automatically.
The pattern scales from simple panels to full application-style interfaces.
Architecture
CascadeUI follows a unidirectional data flow model:
User interaction -> dispatch(action)
-> middleware
-> reducer (state update)
-> subscribers notified
-> views re-render
All state lives in a single store. Actions describe what happened. Reducers define how state changes. Views subscribe to relevant state and update automatically.
Coming from Redux or React?
CascadeUI ports Redux's mental model onto Discord. Most core primitives have a closest analogue in frameworks you already know:
| CascadeUI | Closest Redux / React analogue |
|---|---|
StateStore |
Redux store |
@cascade_reducer |
Redux reducer |
@computed |
Reselect / useMemo |
build_ui() |
React component render() |
on_state_changed |
componentDidUpdate + auto re-render |
push() / pop() / replace() |
React Router navigation |
| Middleware chain | applyMiddleware |
PersistenceMiddleware |
redux-persist (opt-in per slot) |
Full treatment: guide/concepts.md walks through each mapping in depth, including where the two diverge - middleware is async, state persists across bot restarts (Discord messages outlive your code), and Discord's platform layer (ephemeral 15-minute wall, webhook tokens, rate limits) has no React/Redux equivalent.
When to Use
Every discord.py view needs ownership control, session cleanup, and interaction safety. CascadeUI handles all of that out of the box with class-level declarations - no boilerplate, no manual checks.
Even a single-view panel benefits from owner_only = True and instance_limit = 1. As your interface grows, the same framework scales to:
- Shared state across multiple views via
StateStore - Real data and message persistence via
PersistenceMiddleware - Cross-view reactivity with
dispatch()andsubscribed_actions - Multi-step flows and validation via
WizardLayoutViewandFormLayoutView - Navigation stacks (
push()/pop()/replace()), session policies, andparticipant_limit - Grid-based game boards with
emoji_grid()andbutton_grid()
Getting Started
pip install pycascadeui
Optional dependencies:
pip install pycascadeui[sqlite]
Requirements:
- Python 3.10+
- discord.py 2.7+
Hello World
A minimal CascadeUI view: per-user counter with ownership, instance replacement, and state-driven rebuilds - in about 20 lines.
import discord
from discord.ui import ActionRow
from cascadeui import StatefulButton, StatefulLayoutView, card
class CounterView(StatefulLayoutView):
# Class-level policy -- ownership and instance control in three lines.
owner_only = True # Only the opener can click
instance_limit = 1 # One live counter per user
instance_policy = "replace" # Second open replaces the first
# Reactivity -- build_ui() re-runs whenever scoped state changes.
subscribed_actions = {"SCOPED_UPDATE"}
state_scope = "user"
def build_ui(self):
self.clear_items()
count = self.scoped_state.get("count", 0)
self.add_item(card(f"Count: **{count}**"))
self.add_item(ActionRow(StatefulButton(
label="+1",
style=discord.ButtonStyle.primary,
callback=self._increment,
)))
async def _increment(self, interaction):
count = self.scoped_state.get("count", 0)
await self.dispatch_scoped({"count": count + 1})
# In a cog command:
# view = CounterView(context=ctx)
# await view.send()
See the Quickstart for the detailed walkthrough and examples/v2_hello_world.py for the full runnable cog.
Features
For full details, see the official documentation.
State and Data Flow
- Centralized store with dispatch and reducer cycle
- Custom reducers via
@cascade_reducerdecorator with automatic deep copy and built-in collision guards - Action batching for grouped, atomic updates; nested batches collapse into one commit and fire a single notification cycle
- Computed state via
@computeddecorator with selector-based cache invalidation and per-store instances that survive singleton resets (≈ Reselect / React'suseMemo) - Selector-based subscriptions for targeted re-renders (similar to React's selective re-render optimization)
- Built-in profiler with exportable markdown + JSON reports for dispatch, subscriber, and refresh timings -- measure before you optimize, attach snapshots to PRs and bug reports
access_slot()/read_slot()/slot_propertyhelpers for auto-vivifying application buckets without hand-rolling the read/write plumbing- Scoped state family:
get_scoped(),get_scoped_from(),iter_scoped(),set_scoped(),merge_scoped()-- one call from inside a reducer, no private key-building required - Cross-view reactivity: dispatch from any view, all subscribers update instantly with automatic coalescing under concurrent access
- Middleware pipeline for logging, persistence, and transformation (Redux-style, async)
- Event hooks for lifecycle observation and side effects
Views and Patterns
- Layout-based V2 system for structured, container-driven interfaces
- Full support for traditional discord.py Views (V1)
- Pre-built patterns: menus, tabs, wizards, forms, pagination, leaderboards, persistent leaderboards
PaginatedView.from_cursor()for lazy cursor-driven pagination with an LRU page cacheDisplayLayoutViewfor one-shot V2 sends from a pre-built container, no subclass required- Automatic state-driven rebuilds: define
build_ui()and the library wires it intoon_state_changed()andrefresh()for you (declarative render, like React components) - One hook for V1 and V2:
build_ui()returnsNone(V2 mutates the tree) or a dict of edit kwargs like{"embed": ...}(V1), and the library splats it intomessage.edit() - Theming with per-view overrides, V2 accent colors, and a
ContextVarthat propagates the active theme throughbuild_ui()so builders likecard()andstats_card()inherit automatically (likeReact.Context)
Interaction Control and Sessions
- Interaction ownership control, owner-only by default and configurable via
allowed_users - Instance limiting per user, guild, user+guild, or globally with replace or reject policies
- Participant-aware views for multi-user scenarios like challenge flows, lobbies, and games
participant_limitwithon_participant_limithook andauto_register_participantsfor automatic slot claiming duringsend()- Navigation stack with
push(),pop(), andreplace(), sharing one Discord message across the chain (akin to React Router's history API) check_instance_available()for fail-fast pre-checks before constructing expensive views- Five-pillar architecture: Access Control, Instance Constraints, View Lifecycle, Session Membership, and Navigation -- each attribute belongs to exactly one pillar
session_continuityopt-in for repeat-open state coalescing; the default isolates every send as its own session- Parent and child view lifecycle via
attach_child()(orparent=kwarg) with automatic cleanup - Automatic interaction acknowledgement via auto-defer (tunable per view), with
respond()/open_modal()/_safe_defer()helpers that transparently route through response or followup - Interaction serialization via an
asyncio.Lockso rapid clicks process sequentially without racingmessage.edit()calls - Refresh throttling: opt-in
refresh_cooldown_msproactively batches edits, and reactive 429 backoff honors Discord'sretry_afterautomatically. Both share a single monotonic cooldown and coalesce on the latest store state at fire time auto_refresh_ephemeralflag: bypass Discord's 15-minute ephemeral editability wall with a user-driven token handoff; armed views freeze state-driven rebuilds so the refresh button cannot be clobbered between T+810s and T+900s- Automatic message re-fetch after
send()so long-lived views are not bound to the interaction webhook's 15-minute token window, with a_webhook_messagedual-reference so embed edits still route through the webhook when the channel endpoint would drop them silently
Components and Composition
- Stateful buttons, selects, and modals with state integration
- Select callbacks can opt into a
valuessecond parameter, no moreinteraction.data["values"][0] - V2 layout builders:
card(),stats_card(),action_section(),toggle_section(),image_section(),link_section(),confirm_section(),button_row(),cycle_button(),toggle_button(),tab_nav(),key_value(),alert(),progress_bar(),divider(),gap(),gallery() - Grid helpers:
emoji_grid()for text-rendered boards with axis labels and mutation API,button_grid()for interactive cell grids with Discord's 5x5 limit enforced - Built-in form system with typed modal fields (
text,integer,float,date), inline selects, per-field validation, and declarativeFormSchema/WizardSchemabase classes - Component wrappers: loading states, confirmation dialogs, cooldowns
Persistence and Infrastructure
- Persistent views that survive bot restarts with automatic message re-attachment
- Two-namespace persistence model (
registryandapplication) with per-namespace windows, max-age ceiling, and retry backoff on backend failure - State persistence backends: built-in SQLite (via
aiosqlite) and an in-memory backend for tests; custom backends plug in through a capability-flagProtocolwith documented copy-on-store and NULL-safe TTL contracts - Opt-in application slots via
persistent_slots = ("scoped",). Only the slots a view declares ride to disk, the rest stay volatile (≈redux-persist, opt-in per slot) - Named scoped buckets via
scoped_slotso each subsystem (e.g."battleship_stats","tictactoe_stats") persists into its own flat bucket instead of one monolithicscopedtree - Debounced
PersistenceMiddlewareinstalled viasetup_middleware, with smart filtering so bookkeeping actions do not hit disk and an identity-diff scan that skips no-op writes - Undo and redo via snapshot-based state history (opt in with
enable_undo); batched dispatches collapse to one undo entry per participating view - Scoped state isolation (
user,guild,user_guild,global) with automatic key derivation and a reducer-sidemerge_scoped()writer DevToolsCogwith a tabbed state inspector and owner-only/cascadeuicommand group for live debugging- Silent snowflake coercion at every public boundary (
Memberwhereintis expected just works) - Class-attribute validation at subclass-definition time. Typos in
instance_policy,participant_limit, and friends fail at import with a clear error
Showcase
Cross-View Reactivity
Dispatch actions from any view and update all subscribers instantly across the interface.
# Any view can dispatch a named action.
await self.dispatch("SETTINGS_UPDATED", {"theme": "dark"})
# Any other open view that subscribes wakes up automatically --
# no manual message.edit(), no cross-view wiring.
class NotificationPanel(StatefulLayoutView):
subscribed_actions = {"SETTINGS_UPDATED"}
# build_ui() re-runs whenever SETTINGS_UPDATED fires anywhere.
Dynamic Rendering
Define
build_ui()once. The library calls it on every relevant state change and ships the edit for you. Noon_state_changed()override, no manualrefresh(), nomessage.edit()plumbing.
class SettingsHub(StatefulLayoutView):
state_scope = "user"
subscribed_actions = {"SETTINGS_UPDATED"}
def build_ui(self):
self.clear_items()
settings = self.user_scoped_state()
self.add_item(card(
"## Settings",
key_value({
"Theme": settings.get("theme", "default").title(),
"Notifications": "On" if settings.get("notify") else "Off",
}),
))
# build_ui() is called automatically on state changes --
# no manual refresh() or on_state_changed() override needed.
Navigation and Flow
Push, pop, and replace views on a shared navigation stack.
MenuLayoutViewhandles the wiring for category-based hubs -- declare your categories and target views, the pattern generates the push callbacks andaction_section()items automatically.
from cascadeui import MenuLayoutView
class SettingsMenu(MenuLayoutView):
instance_limit = 1
instance_scope = "user_guild"
instance_policy = "replace"
def __init__(self, *args, **kwargs):
categories = [
{"label": "Appearance", "emoji": "\N{ARTIST PALETTE}",
"description": "Theme and accent colors", "view": AppearanceView},
{"label": "Notifications", "emoji": "\N{BELL}",
"description": "Alert preferences", "view": NotificationsView},
{"label": "Locale", "emoji": "\N{GLOBE WITH MERIDIANS}",
"description": "Language and timezone", "view": LocaleView},
]
super().__init__(*args, categories=categories, **kwargs)
Ownership Control
Views are owner-only by default - only the user who opened it can interact. For multi-user scenarios,
allowed_usersandparticipant_limitextend that control.
class BattleshipView(StatefulLayoutView):
unauthorized_message = "You're not part of this game."
instance_limit = 1
instance_policy = "reject"
participant_limit = 2
auto_register_participants = True
def __init__(self, *args, opponent_id: int, **kwargs):
super().__init__(*args, **kwargs)
self.allowed_users = {self.user_id, opponent_id}
Lifecycle Control
Control active sessions per user, guild, or globally with automatic cleanup, replacement policies, and view-capacity caps.
class DashboardView(TabLayoutView):
instance_limit = 1 # Only one open at a time
instance_scope = "user_guild" # Per user per guild
instance_policy = "replace" # Exit the old one, open the new one
class GameView(StatefulLayoutView):
participant_limit = 8 # Owner + 7 joiners maximum
auto_register_participants = True # Claim slots from allowed_users on send()
Persistence and Continuity
Persist views and state across restarts with automatic restoration.
from cascadeui import PersistenceMiddleware, SQLiteBackend, setup_middleware
# Install PersistenceMiddleware once in your bot's setup_hook:
async def setup_hook(self):
await setup_middleware(
PersistenceMiddleware(backend=SQLiteBackend("cascadeui.db"), bot=self),
)
Declare persistent_slots on any view that should carry application state to disk. The rest stays volatile:
class BattleshipView(StatefulLayoutView):
scoped_slot = "battleship_stats" # per-subsystem bucket
persistent_slots = ("battleship_stats",) # opt this slot into persistence
State History (Undo/Redo)
Snapshot-based state history per session with built-in undo and redo support.
class SettingsHub(StatefulLayoutView):
enable_undo = True # Every dispatch captures a snapshot
async def _undo(self, interaction):
await self.undo() # Restore previous snapshot
async def _redo(self, interaction):
await self.redo() # Reapply the reverted snapshot
Ephemeral Refresh
Discord ephemeral messages become uneditable after 15 minutes. CascadeUI handles the token handoff automatically.
class FleetView(StatefulLayoutView):
timeout = 3600 # Handoff auto-engages for timeout > 900s
refresh_button_label = "Refresh" # Default: "Continue Session"
Developer Tools
Inspect live state, session activity, and performance timings without leaving your Discord client.
from cascadeui import DevToolsCog
# In your bot's setup_hook:
await bot.add_cog(DevToolsCog(bot))
View Patterns
Category Menu
Category-based navigation hubs with automatic drill-down, themed cards, and declarative per-category styling.
from cascadeui import MenuLayoutView, card, key_value
class ConfigHub(MenuLayoutView):
menu_style = discord.ButtonStyle.primary
auto_exit_button = True
def __init__(self, *args, **kwargs):
categories = [
{"label": "General", "emoji": "\u2699\ufe0f",
"description": "Core settings", "view": GeneralView},
{"label": "Moderation", "emoji": "\U0001f6e1\ufe0f",
"description": "AutoMod and logging", "view": ModerationView},
]
super().__init__(*args, categories=categories, **kwargs)
def _build_header(self):
return [card("## Server Config", key_value(self._summary()))]
Tabbed Dashboard
Structured, multi-section interfaces with tab-based navigation and composable layouts.
Dynamic Pagination
Generate paginated interfaces from raw data with built-in navigation and formatting helpers.
import discord
from cascadeui import PaginatedLayoutView, card, divider
def format_page(items):
lines = [f"**{item['name']}** | {item['rarity']} | {item['value']}g" for item in items]
return [card(
"## Inventory",
divider(),
"\n".join(lines),
color=discord.Color.blue(),
)]
view = await PaginatedLayoutView.from_data(
items=all_items,
per_page=4,
formatter=format_page,
context=ctx,
)
await view.send()
Leaderboards
Paginated ranked displays with cross-page numbering, optional summary stats, and a persistent variant for admin-posted panels that refresh on live data without a bot restart.
from cascadeui import LeaderboardLayoutView, PersistentLeaderboardLayoutView, get_store
class BattleshipLeaderboard(LeaderboardLayoutView):
leaderboard_top_n = 25
leaderboard_per_page = 10
title = "Battleship Rankings"
def format_stats(self, user_id, stats):
wins = stats.get("wins", 0)
games = stats.get("games", 0)
return f"**{wins}W** / {games - wins}L"
def build_summary(self, entries):
# Each game contributes to two player rows, so halve for unique games.
unique_games = sum(s.get("games", 0) for _, s in entries) // 2
return {"Players": str(len(entries)), "Games Played": str(unique_games)}
# One-shot usage: fetch live entries and pass them in.
entries = get_store().computed["battleship_leaderboards"].get(guild_id, [])
view = BattleshipLeaderboard(context=ctx, entries=entries)
await view.send()
# Persistent variant -- admin-posted panel that survives bot restarts
# and re-fetches live data on every restore.
class PersistentBoard(PersistentLeaderboardLayoutView):
persistence_key = "battleship-leaderboard-main"
Forms and Validation
Define structured input flows with declarative fields, native text inputs, and per-field validation.
from cascadeui import FormLayoutView, min_length, regex
class RegistrationForm(FormLayoutView):
instance_limit = 1
instance_policy = "reject"
exit_policy = "delete"
def __init__(self, *args, **kwargs):
fields = [
{
"id": "username", "label": "Username", "type": "text",
"required": True, "min_length": 3, "max_length": 20,
"validators": [
min_length(3),
regex(r"^[a-zA-Z0-9_]+$", "Alphanumeric and underscores only"),
],
},
{
"id": "role", "label": "Role", "type": "select",
"required": True,
"options": [
{"label": "Developer", "value": "dev"},
{"label": "Designer", "value": "design"},
],
},
]
super().__init__(*args, title="Registration", fields=fields, **kwargs)
async def on_submit(self, interaction, values):
await self.respond(
interaction, f"Welcome, {values['username']}!", ephemeral=True,
)
await self.exit()
Multi-Step Wizard
Multi-step flows with back/next/finish navigation, per-step builders and validators, and fully customizable button styling.
from cascadeui import WizardLayoutView
class CharacterCreator(WizardLayoutView):
instance_limit = 1
instance_policy = "replace"
exit_policy = "delete"
back_button_label = "Previous"
next_button_label = "Continue"
finish_button_label = "Create Character"
finish_button_style = discord.ButtonStyle.success
def __init__(self, *args, **kwargs):
steps = [
{"name": "Identity", "builder": self.build_identity},
{"name": "Class", "builder": self.build_class},
{"name": "Stats", "builder": self.build_stats},
{"name": "Review", "builder": self.build_review},
]
super().__init__(*args, steps=steps, **kwargs)
Component Patterns
Emoji Grid
Text-rendered grids with optional axis labels and a mutation API. Plugs directly into
card()andContainer.
from cascadeui import emoji_grid, card
grid = emoji_grid(4, 4, fill="\u2b1c", col_labels="numeric")
grid.fill_rect((1, 0), (1, 3), "\U0001f7e6")
grid[(2, 2)] = "\u2764\ufe0f"
view.add_item(card(grid))
Button Grid
Interactive cell grids packed into
ActionRowcomponents. Discord's 5x5 limit is enforced automatically.
from cascadeui import button_grid, StatefulButton
rows = button_grid(3, 3, lambda r, c: StatefulButton(
label=f"{chr(65 + r)}{c + 1}",
style=discord.ButtonStyle.secondary,
callback=on_cell_click,
))
for row in rows:
view.add_item(row)
V1 Components
CascadeUI supports traditional discord.py Views and embeds.
Use V1 when you need:
- Embed-specific features such as fields or timestamps
- Simpler layouts without containers
All core features such as navigation, persistence, and undo/redo are supported.
from cascadeui import PersistentView, SuccessButton
import discord
class TicketPanel(PersistentView):
persistence_key = "support-ticket-panel"
owner_only = False # Public panel -- anyone can open a ticket
def build_embed(self):
return discord.Embed(
title="Support Tickets",
description="Click below to open a private support thread.",
color=discord.Color.blurple(),
)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.add_item(SuccessButton(
label="Open Ticket",
custom_id="ticket-panel:open",
callback=self._open_ticket,
))
async def _open_ticket(self, interaction):
# ... create private thread, send confirmation ...
await self.respond(interaction, "Ticket created!", ephemeral=True)
Examples
The documentation includes full implementations demonstrating practical usage:
- Dashboards and control panels
- Settings systems
- Pagination
- Forms and wizards
- Persistent views
- Multi-user games with shared state, hidden information, and challenge flows (TicTacToe, Battleship)
- Open-join lobbies with capacity caps and host-vs-participant authority (Werewolf-style)
Documentation
Support
- Discord: https://discord.com/invite/9Xj68BpKRb
- Issues: https://github.com/HollowTheSilver/CascadeUI/issues
Development
git clone https://github.com/HollowTheSilver/CascadeUI.git
cd CascadeUI
pip install -e ".[dev]"
pytest tests/ -v
black cascadeui/
isort cascadeui/
Developer's Note
I built CascadeUI with over ten years of Python experience behind it. All documentation, docstrings, and the entire testing module were written and designed using my custom Anthropic Opus 4.6 sub-agents built on Claude Code. I don't try to hide that. I'm a proponent of efficient and responsible agent application in software design. That experience is what makes these tools effective. They're amplifiers, not substitutes.
-- Hollow
MIT License
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pycascadeui-3.0.0.tar.gz.
File metadata
- Download URL: pycascadeui-3.0.0.tar.gz
- Upload date:
- Size: 372.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0e8eee54f277518db76ab99da14f834160474b6f5726282b613e18a666de74ec
|
|
| MD5 |
82aea6994223b48e7496bf601c1cdb9e
|
|
| BLAKE2b-256 |
2b7ebae89ead6f2898abc0ca7157b3370e8488153d9189216abf3f331c681e04
|
Provenance
The following attestation bundles were made for pycascadeui-3.0.0.tar.gz:
Publisher:
publish.yml on HollowTheSilver/CascadeUI
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pycascadeui-3.0.0.tar.gz -
Subject digest:
0e8eee54f277518db76ab99da14f834160474b6f5726282b613e18a666de74ec - Sigstore transparency entry: 1343596592
- Sigstore integration time:
-
Permalink:
HollowTheSilver/CascadeUI@2b98c0f4d4889d66eba88ca4e5019a2050ee8080 -
Branch / Tag:
refs/tags/v3.0.0 - Owner: https://github.com/HollowTheSilver
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2b98c0f4d4889d66eba88ca4e5019a2050ee8080 -
Trigger Event:
release
-
Statement type:
File details
Details for the file pycascadeui-3.0.0-py3-none-any.whl.
File metadata
- Download URL: pycascadeui-3.0.0-py3-none-any.whl
- Upload date:
- Size: 240.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0972f5e117858c9bacadcf690b31e442b83efa3768e257717db465cb87ad2ede
|
|
| MD5 |
bd7f4fad6f2ee29d2b0ce74441eca112
|
|
| BLAKE2b-256 |
94e4038a03cb4d8046609e27a08550771524c7895d403a780f7787d7061180f6
|
Provenance
The following attestation bundles were made for pycascadeui-3.0.0-py3-none-any.whl:
Publisher:
publish.yml on HollowTheSilver/CascadeUI
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pycascadeui-3.0.0-py3-none-any.whl -
Subject digest:
0972f5e117858c9bacadcf690b31e442b83efa3768e257717db465cb87ad2ede - Sigstore transparency entry: 1343596613
- Sigstore integration time:
-
Permalink:
HollowTheSilver/CascadeUI@2b98c0f4d4889d66eba88ca4e5019a2050ee8080 -
Branch / Tag:
refs/tags/v3.0.0 - Owner: https://github.com/HollowTheSilver
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2b98c0f4d4889d66eba88ca4e5019a2050ee8080 -
Trigger Event:
release
-
Statement type: