This commit is contained in:
James Read 2026-05-31 23:55:11 +00:00 committed by GitHub
commit b2096d4a8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 1803 additions and 826 deletions

132
.github/dependabot.yml vendored
View File

@ -1,132 +0,0 @@
version: 2
updates:
# npm updates for frontend - targeting "next" branch
- package-ecosystem: "npm"
directory: "/frontend"
schedule:
interval: "weekly"
target-branch: "next"
open-pull-requests-limit: 10
labels:
- "3k"
- "dependencies"
cooldown:
default-days: 7
# npm updates for frontend - targeting "release/2k" branch (security updates only)
- package-ecosystem: "npm"
directory: "/frontend"
schedule:
interval: "weekly"
target-branch: "release/2k"
open-pull-requests-limit: 0
labels:
- "2k"
- "dependencies"
cooldown:
default-days: 7
# npm updates for integration-tests - targeting "next" branch
- package-ecosystem: "npm"
directory: "/integration-tests"
schedule:
interval: "weekly"
target-branch: "next"
open-pull-requests-limit: 10
labels:
- "3k"
- "dependencies"
cooldown:
default-days: 7
# npm updates for integration-tests - targeting "release/2k" branch (security updates only)
- package-ecosystem: "npm"
directory: "/integration-tests"
schedule:
interval: "weekly"
target-branch: "release/2k"
open-pull-requests-limit: 0
labels:
- "2k"
- "dependencies"
cooldown:
default-days: 7
# Go modules updates for service - targeting "next" branch
- package-ecosystem: "gomod"
directory: "/service"
schedule:
interval: "weekly"
target-branch: "next"
open-pull-requests-limit: 10
labels:
- "3k"
- "dependencies"
cooldown:
default-days: 7
# Go modules updates for service - targeting "release/2k" branch (security updates only)
- package-ecosystem: "gomod"
directory: "/service"
schedule:
interval: "weekly"
target-branch: "release/2k"
open-pull-requests-limit: 0
labels:
- "2k"
- "dependencies"
cooldown:
default-days: 7
# Go modules updates for lang - targeting "next" branch
- package-ecosystem: "gomod"
directory: "/lang"
schedule:
interval: "weekly"
target-branch: "next"
open-pull-requests-limit: 10
labels:
- "3k"
- "dependencies"
cooldown:
default-days: 7
# Go modules updates for lang - targeting "release/2k" branch (security updates only)
- package-ecosystem: "gomod"
directory: "/lang"
schedule:
interval: "weekly"
target-branch: "release/2k"
open-pull-requests-limit: 0
labels:
- "2k"
- "dependencies"
cooldown:
default-days: 7
# Docker updates - targeting "next" branch
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
target-branch: "next"
open-pull-requests-limit: 10
labels:
- "3k"
- "dependencies"
cooldown:
default-days: 7
# Docker updates - targeting "release/2k" branch (security updates only)
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
target-branch: "release/2k"
open-pull-requests-limit: 0
labels:
- "2k"
- "dependencies"
cooldown:
default-days: 7

View File

@ -14,6 +14,10 @@ logLevel: "INFO"
#
# Docs: https://docs.olivetin.app/action_execution/create_your_first.html
actions:
# Every action can still be run on demand from the web UI or API. The keys
# below are optional *additional* triggers (see each action and
# https://docs.olivetin.app/action_execution/ ).
#
# This is the most simple action, it just runs the command and flashes the
# button to indicate status.
#
@ -23,6 +27,8 @@ actions:
shell: ping -c 3 1.1.1.1
icon: ping
popupOnStart: execution-dialog-stdout-only
# https://docs.olivetin.app/action_execution/onstartup.html
execOnStartup: true
# This uses `popupOnStart: execution-dialog-stdout-only` to simply show just
# the command output.
@ -30,6 +36,10 @@ actions:
icon: disk
shell: df -h /media
popupOnStart: execution-dialog-stdout-only
# https://docs.olivetin.app/action_execution/onfilechanged.html
# Create the directory first, e.g. mkdir -p /tmp/olivetin-demo-file-changed
execOnFileChangedInDir:
- /tmp/olivetin-demo-file-changed
# This uses `popupOnStart: execution-dialog` to show a dialog with more
# information about the command that was run.
@ -37,6 +47,10 @@ actions:
shell: dmesg | tail
icon: logs
popupOnStart: execution-dialog
# https://docs.olivetin.app/action_execution/oncron.html — second example;
# the "date" action uses @hourly elsewhere in this file.
execOnCron:
- "0 3 * * 0"
# This uses `popupOnStart: execution-button` to display a mini button that
# links to the logs.
@ -51,6 +65,8 @@ actions:
maxRate:
- limit: 3
duration: 1m
execOnCron:
- "@hourly"
# You are not limited to operating system commands, and of course you can run
# your own scripts. Here `maxConcurrent` stops the script running multiple
@ -63,6 +79,8 @@ actions:
timeout: 10
icon: backup
popupOnStart: execution-dialog
# https://docs.olivetin.app/action_execution/oncalendar.html
execOnCalendarFile: examples/demo-olivetin-calendar.yaml
# When you want to prompt users for input, that is when you should use
# `arguments` - this presents a popup dialog and asks for argument values.
@ -74,6 +92,11 @@ actions:
icon: ping
timeout: 100
popupOnStart: execution-dialog-stdout-only
# https://docs.olivetin.app/action_execution/onwebhook.html — POST to /webhooks
# with header X-OliveTin-Demo: ping-host (path and payload rules are documented).
execOnWebhook:
- matchHeaders:
X-OliveTin-Demo: ping-host
arguments:
- name: host
title: Host
@ -149,6 +172,10 @@ actions:
icon: ssh
shell: olivetin-setup-easy-ssh
popupOnStart: execution-dialog
# Second webhook example: POST /webhooks?demo=setup-ssh
execOnWebhook:
- matchQuery:
demo: setup-ssh
# Here's how to use SSH with the "easy" config, to restart a service on
# another server.
@ -215,6 +242,10 @@ actions:
- title: Ping All Servers
shell: "echo 'Ping all servers'"
icon: ping
# https://docs.olivetin.app/action_execution/onfilecreated.html
# mkdir -p /tmp/olivetin-demo-file-created
execOnFileCreatedInDir:
- /tmp/olivetin-demo-file-created
- title: Start {{ .CurrentEntity.Names }}
icon: box

View File

@ -39,6 +39,22 @@ And you should get something that looks like this;
image::../action-button-iconify.png[]
== Default Icon (bundled HugeIcon)
OliveTin used to use a default emoji smiley face as the default icon for actions, but that was a bit too "emoji" and not everyone liked it. Now, OliveTin uses a simple "command line" icon from the HugeIcons set as the default icon for actions. This is a simple and neutral icon that should work well for most actions.
If you need to reset a default icon for some reason, this is how you can do it;
.`config.yaml`
----
actions:
- title: Action with the bundled CLI HugeIcon
icon: hugeicons:CommandLineIcon
shell: echo hello
----
If you want to use other icons from the HugeIcons set, you need to use the Iconify method described above, not with the "hugeicons:" prefix - that only works for the default icon.
== Unicode icons ("emoji")
Using simple emoji (unicode) icons from your browser's font is extremely fast, and can look good on some platforms. However, the icons are platform specific, which mean's they'll look different between browsers and between operating systems.
@ -56,7 +72,7 @@ actions:
shell: echo "You are awesome"
----
=== Unicode alises
=== Unicode aliases
OliveTin has hard-coded aliases for a few commonly used icons, so you don't have to type out the full unicode codes. A list of those hard coded icons is;
@ -140,5 +156,3 @@ examples;
shell: echo "I like purple"
----
////

View File

@ -10,6 +10,7 @@ A full list of argument types are below;
| (default) | xref:args/input.adoc[Textbox] | If a `type:` is not set, and `choices:` is empty, then ascii will be used, and a warning will be logged. It is recommended that you set the type explicitly, rather than relying on defaults.
| ascii | xref:args/input.adoc[Textbox] | a-z (case insensitive), 0-9, but no spaces or punctuation
| ascii_identifier | xref:args/input.adoc[Textbox] | Like a DNS name, a-Z (case insensitive), 0-9, `-`, `.`, and `_`.
| shell_safe_identifier | xref:args/input.adoc[Textbox] | Like an ascii identifier, but also allows `@` and `+`. Useful for shell-safe usernames and email-style identifiers.
| ascii_sentence | xref:args/input.adoc[Textbox] | a-z (case insensitive), 0-9, with spaces, `.` and `,`.
| unicode_identifier | xref:args/input.adoc[Textbox] | Like an ascii identifier, but allows unicode characters. This is useful for languages that use non-ascii characters, such as Chinese, Japanese, etc.
| email | xref:args/input.adoc[Textbox] | An email address.

View File

@ -18,7 +18,7 @@ actions:
shell: echo 'Hello World!'
----
The configuration does not really get more complicated than that. You can of course add more actions, and customize more, but the syntax otherwise extremely simple.
The configuration does not really get more complicated than that. You can of course add more actions, and customize more, but the syntax is otherwise extremely simple.
For building up from here, look at the following resources;
@ -54,7 +54,7 @@ All configuration options are covered in the solution sections
| `showNavigateOnStartIcons` | Show (or hide) the small icons on action buttons that indicate popup/argument/background behavior on start. | `true` | Live reloadable | xref:advanced_configuration/webui.adoc[Customize the web UI].
| `sectionNavigationStyle` | The style of the section navigation. `sidebar`, `topbar` | `sidebar` | Live reloadable | xref:advanced_configuration/webui.adoc[Customize the web UI].
| `defaultPopupOnStart` | The default popup to show on start. | `none` | Live reloadable | xref:action_customization/popuponstart.adoc[Popup On Start].
| `defaultIconForActions` | The default icon to use for actions. | `smile` | Requires Restart | -
| `defaultIconForActions` | The default icon string for actions (Unicode aliases such as `smile`, `hugeicons:NeutralIcon`, HTML, Iconify snippets, images, etc.). See xref:action_customization/icons.adoc[Icons]. | `hugeicons:CommandLineIcon` | Requires Restart | -
| `defaultIconForDirectories` | The default icon to use for directories. | `directory` | Requires Restart | -
| `defaultIconForBack` | The default icon to use for back (from directories). | `«` | Requires Restart | -
| `enableCustomJs` | Enable custom JavaScript. | `false` | Live Reloadable, but refreshing the web browser is required. | xref:advanced_configuration/webui.adoc[Custom JS].

View File

@ -18,7 +18,7 @@ Note that OliveTin will load `/theme.css` depending on `themeName:` in your conf
== Understanding theme URLs
When you create a theme, OliveTin will serve your theme's CSS at `/theme.css` and any other assets at `/custom-webui/themes/mytheme/`. This might be a little strange at first, as your theme.css file wil be in the `/custom-webui/themes/mytheme/` directory, but OliveTin will still serve it at `/theme.css`. Let's explain why this happens;
When you create a theme, OliveTin will serve your theme's CSS at `/theme.css` and any other assets at `/custom-webui/themes/mytheme/`. This might be a little strange at first, as your theme.css file will be in the `/custom-webui/themes/mytheme/` directory, but OliveTin will still serve it at `/theme.css`. Let's explain why this happens;
OliveTin wants to make it easy for your reverse proxy, cache server, or browser, to cache as much content as possible. This means that if OliveTin had to inject a new CSS file into the HTML every time you changed your theme, then your reverse proxy, cache server, or browser would have to re-download the HTML every time you changed your theme. This is not ideal.
@ -26,7 +26,7 @@ It is possible that OliveTin's initial webUiSettings.json (that is loaded to set
To make things fast, OliveTin will copy the content of your `/custom-webui/themes/mytheme/theme.css` file into memory when it starts, and then requests for `/theme.css` will load this file.
What this means for you, is that to get to files like `backgrond.png` from your CSS, you must write your CSS to point to the file in the `/custom-webui/themes/mytheme/` directory;
What this means for you, is that to get to files like `background.png` from your CSS, you must write your CSS to point to the file in the `/custom-webui/themes/mytheme/` directory;
.Correct example
```
@ -48,6 +48,4 @@ The OliveTin themes page is here; https://olivetin.app/themes
When you are done with your theme, fork https://github.com/OliveTin/themes on GitHub and create a new page under the "content" directory for your new theme. Commit that to GitHub and then raise a pull request.
If you meed more help, please jump on our discord server!
If you need more help, please jump on our Discord server!

View File

@ -20,15 +20,15 @@
"iconify-icon": "^3.0.2",
"picocrank": "^1.15.0",
"standard": "^17.1.2",
"unplugin-vue-components": "^32.0.0",
"vite": "^8.0.13",
"unplugin-vue-components": "^32.1.0",
"vite": "^8.0.14",
"vue": "^3.5.34",
"vue-i18n": "^11.4.4",
"vue-router": "^5.0.7"
},
"devDependencies": {
"process": "^0.11.10",
"stylelint": "^17.11.1",
"stylelint": "^17.12.0",
"stylelint-config-standard": "^40.0.0"
}
},
@ -164,16 +164,16 @@
"peer": true
},
"node_modules/@cacheable/memory": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.7.tgz",
"integrity": "sha512-RbxnxAMf89Tp1dLhXMS7ceft/PGsDl1Ip7T20z5nZ+pwIAsQ1p2izPjVG69oCLv/jfQ7HDPHTWK0c9rcAWXN3A==",
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.9.tgz",
"integrity": "sha512-HdMx6DoGywB30vacDbBsITbIX4pgFqj1zsrV58jZBUw3klzkNoXhj7qOqAgledhxG7YZI5rBSJg7Zp8/VG0DuA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@cacheable/utils": "^2.3.3",
"@keyv/bigmap": "^1.3.0",
"hookified": "^1.14.0",
"keyv": "^5.5.5"
"@cacheable/utils": "^2.4.1",
"@keyv/bigmap": "^1.3.1",
"hookified": "^1.15.1",
"keyv": "^5.6.0"
}
},
"node_modules/@cacheable/memory/node_modules/@keyv/bigmap": {
@ -204,13 +204,13 @@
}
},
"node_modules/@cacheable/utils": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.3.4.tgz",
"integrity": "sha512-knwKUJEYgIfwShABS1BX6JyJJTglAFcEU7EXqzTdiGCXur4voqkiJkdgZIQtWNFhynzDWERcTYv/sETMu3uJWA==",
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.4.1.tgz",
"integrity": "sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==",
"dev": true,
"license": "MIT",
"dependencies": {
"hashery": "^1.3.0",
"hashery": "^1.5.1",
"keyv": "^5.6.0"
}
},
@ -1157,18 +1157,18 @@
}
},
"node_modules/@oxc-project/types": {
"version": "0.130.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz",
"integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==",
"version": "0.132.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz",
"integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/Boshen"
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz",
"integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz",
"integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==",
"cpu": [
"arm64"
],
@ -1182,9 +1182,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz",
"integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz",
"integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==",
"cpu": [
"arm64"
],
@ -1198,9 +1198,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz",
"integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz",
"integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==",
"cpu": [
"x64"
],
@ -1214,9 +1214,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz",
"integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz",
"integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==",
"cpu": [
"x64"
],
@ -1230,9 +1230,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz",
"integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz",
"integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==",
"cpu": [
"arm"
],
@ -1246,9 +1246,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz",
"integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz",
"integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==",
"cpu": [
"arm64"
],
@ -1262,9 +1262,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz",
"integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz",
"integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==",
"cpu": [
"arm64"
],
@ -1278,9 +1278,9 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz",
"integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz",
"integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==",
"cpu": [
"ppc64"
],
@ -1294,9 +1294,9 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz",
"integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz",
"integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==",
"cpu": [
"s390x"
],
@ -1310,9 +1310,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz",
"integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz",
"integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==",
"cpu": [
"x64"
],
@ -1326,9 +1326,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz",
"integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz",
"integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==",
"cpu": [
"x64"
],
@ -1342,9 +1342,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz",
"integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz",
"integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==",
"cpu": [
"arm64"
],
@ -1358,9 +1358,9 @@
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz",
"integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz",
"integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==",
"cpu": [
"wasm32"
],
@ -1376,9 +1376,9 @@
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz",
"integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz",
"integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==",
"cpu": [
"arm64"
],
@ -1392,9 +1392,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz",
"integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz",
"integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==",
"cpu": [
"x64"
],
@ -1990,17 +1990,17 @@
}
},
"node_modules/cacheable": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.2.tgz",
"integrity": "sha512-w+ZuRNmex9c1TR9RcsxbfTKCjSL0rh1WA5SABbrWprIHeNBdmyQLSYonlDy9gpD+63XT8DgZ/wNh1Smvc9WnJA==",
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.5.tgz",
"integrity": "sha512-EQfaKe09tl615iNvq/TBRWTFf1AKJNXYQSsMx0Z3EI0nA+pVsVPS8wJhnRlkbdacKPh1d0qVIhwTc2zsQNFEEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@cacheable/memory": "^2.0.7",
"@cacheable/utils": "^2.3.3",
"@cacheable/memory": "^2.0.8",
"@cacheable/utils": "^2.4.1",
"hookified": "^1.15.0",
"keyv": "^5.5.5",
"qified": "^0.6.0"
"keyv": "^5.6.0",
"qified": "^0.10.1"
}
},
"node_modules/cacheable/node_modules/keyv": {
@ -3672,13 +3672,13 @@
}
},
"node_modules/hashery": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/hashery/-/hashery-1.4.0.tgz",
"integrity": "sha512-Wn2i1In6XFxl8Az55kkgnFRiAlIAushzh26PTjL2AKtQcEfXrcLa7Hn5QOWGZEf3LU057P9TwwZjFyxfS1VuvQ==",
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz",
"integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"hookified": "^1.14.0"
"hookified": "^1.15.0"
},
"engines": {
"node": ">=20"
@ -4645,9 +4645,9 @@
}
},
"node_modules/local-pkg": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
"integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz",
"integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==",
"license": "MIT",
"dependencies": {
"mlly": "^1.7.4",
@ -4851,9 +4851,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"funding": [
{
"type": "github",
@ -5304,9 +5304,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"funding": [
{
"type": "opencollective",
@ -5323,7 +5323,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"nanoid": "^3.3.12",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@ -5416,18 +5416,25 @@
}
},
"node_modules/qified": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/qified/-/qified-0.6.0.tgz",
"integrity": "sha512-tsSGN1x3h569ZSU1u6diwhltLyfUWDp3YbFHedapTmpBl0B3P6U3+Qptg7xu+v+1io1EwhdPyyRHYbEw0KN2FA==",
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz",
"integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==",
"dev": true,
"license": "MIT",
"dependencies": {
"hookified": "^1.14.0"
"hookified": "^2.1.1"
},
"engines": {
"node": ">=20"
}
},
"node_modules/qified/node_modules/hookified": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz",
"integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==",
"dev": true,
"license": "MIT"
},
"node_modules/quansync": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
@ -5599,12 +5606,12 @@
}
},
"node_modules/rolldown": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz",
"integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz",
"integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==",
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.130.0",
"@oxc-project/types": "=0.132.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
@ -5614,21 +5621,21 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.1",
"@rolldown/binding-darwin-arm64": "1.0.1",
"@rolldown/binding-darwin-x64": "1.0.1",
"@rolldown/binding-freebsd-x64": "1.0.1",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.1",
"@rolldown/binding-linux-arm64-gnu": "1.0.1",
"@rolldown/binding-linux-arm64-musl": "1.0.1",
"@rolldown/binding-linux-ppc64-gnu": "1.0.1",
"@rolldown/binding-linux-s390x-gnu": "1.0.1",
"@rolldown/binding-linux-x64-gnu": "1.0.1",
"@rolldown/binding-linux-x64-musl": "1.0.1",
"@rolldown/binding-openharmony-arm64": "1.0.1",
"@rolldown/binding-wasm32-wasi": "1.0.1",
"@rolldown/binding-win32-arm64-msvc": "1.0.1",
"@rolldown/binding-win32-x64-msvc": "1.0.1"
"@rolldown/binding-android-arm64": "1.0.2",
"@rolldown/binding-darwin-arm64": "1.0.2",
"@rolldown/binding-darwin-x64": "1.0.2",
"@rolldown/binding-freebsd-x64": "1.0.2",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.2",
"@rolldown/binding-linux-arm64-gnu": "1.0.2",
"@rolldown/binding-linux-arm64-musl": "1.0.2",
"@rolldown/binding-linux-ppc64-gnu": "1.0.2",
"@rolldown/binding-linux-s390x-gnu": "1.0.2",
"@rolldown/binding-linux-x64-gnu": "1.0.2",
"@rolldown/binding-linux-x64-musl": "1.0.2",
"@rolldown/binding-openharmony-arm64": "1.0.2",
"@rolldown/binding-wasm32-wasi": "1.0.2",
"@rolldown/binding-win32-arm64-msvc": "1.0.2",
"@rolldown/binding-win32-x64-msvc": "1.0.2"
}
},
"node_modules/run-parallel": {
@ -6130,9 +6137,9 @@
}
},
"node_modules/stylelint": {
"version": "17.11.1",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.11.1.tgz",
"integrity": "sha512-+smN/HqVTggUx3iuAzOi9fPh8SrH+cJWlZrYVldXoJ06orWBhZ4Ue/QEp64oei6pVrAh4w3tG+Y12Vw7MbCFRQ==",
"version": "17.12.0",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.12.0.tgz",
"integrity": "sha512-KIlzWXMHUvgfPUR0R7TK3H80yCIi0uoivUwf+6Az4yrHJD1Q3c1qIkh/H5Z0i/K3QXgtq/UMEkWyBUSUwnpnOg==",
"dev": true,
"funding": [
{
@ -6160,7 +6167,7 @@
"debug": "^4.4.3",
"fast-glob": "^3.3.3",
"fastest-levenshtein": "^1.0.16",
"file-entry-cache": "^11.1.2",
"file-entry-cache": "^11.1.3",
"global-modules": "^2.0.0",
"globby": "^16.2.0",
"globjoin": "^0.1.4",
@ -6252,24 +6259,24 @@
}
},
"node_modules/stylelint/node_modules/file-entry-cache": {
"version": "11.1.2",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.2.tgz",
"integrity": "sha512-N2WFfK12gmrK1c1GXOqiAJ1tc5YE+R53zvQ+t5P8S5XhnmKYVB5eZEiLNZKDSmoG8wqqbF9EXYBBW/nef19log==",
"version": "11.1.3",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.3.tgz",
"integrity": "sha512-oMbq0PD6VIiIwMF6LIa7MEwd/l9huKwmqRKXqmrkqIZv8CvRbfowL+L0ryAl8h//HfAS0zS+4SbYoRyAoA6BJA==",
"dev": true,
"license": "MIT",
"dependencies": {
"flat-cache": "^6.1.20"
"flat-cache": "^6.1.22"
}
},
"node_modules/stylelint/node_modules/flat-cache": {
"version": "6.1.20",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.20.tgz",
"integrity": "sha512-AhHYqwvN62NVLp4lObVXGVluiABTHapoB57EyegZVmazN+hhGhLTn3uZbOofoTw4DSDvVCadzzyChXhOAvy8uQ==",
"version": "6.1.22",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.22.tgz",
"integrity": "sha512-N2dnzVJIphnNsjHcrxGW7DePckJ6haPrSFqpsBUhHYgwtKGVq4JrBGielEGD2fCVnsGm1zlBVZ8wGhkyuetgug==",
"dev": true,
"license": "MIT",
"dependencies": {
"cacheable": "^2.3.2",
"flatted": "^3.3.3",
"cacheable": "^2.3.4",
"flatted": "^3.4.2",
"hookified": "^1.15.0"
}
},
@ -6689,18 +6696,18 @@
}
},
"node_modules/unplugin-vue-components": {
"version": "32.0.0",
"resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-32.0.0.tgz",
"integrity": "sha512-uLdccgS7mf3pv1bCCP20y/hm+u1eOjAmygVkh+Oa70MPkzgl1eQv1L0CwdHNM3gscO8/GDMGIET98Ja47CBbZg==",
"version": "32.1.0",
"resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-32.1.0.tgz",
"integrity": "sha512-YiUkSxuRjab18XFOrX5VsIxXzccrfmHVGsGeJgSgklb829DQmCy9E4vvDUE4tuvZZdxyFJZX0Oc4TPnnxiiMyg==",
"license": "MIT",
"dependencies": {
"chokidar": "^5.0.0",
"local-pkg": "^1.1.2",
"local-pkg": "^1.2.0",
"magic-string": "^0.30.21",
"mlly": "^1.8.2",
"obug": "^2.1.1",
"picomatch": "^4.0.3",
"tinyglobby": "^0.2.15",
"picomatch": "^4.0.4",
"tinyglobby": "^0.2.16",
"unplugin": "^3.0.0",
"unplugin-utils": "^0.3.1"
},
@ -6769,15 +6776,15 @@
}
},
"node_modules/vite": {
"version": "8.0.13",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz",
"integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==",
"version": "8.0.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz",
"integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==",
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
"postcss": "^8.5.14",
"rolldown": "1.0.1",
"postcss": "^8.5.15",
"rolldown": "1.0.2",
"tinyglobby": "^0.2.16"
},
"bin": {

View File

@ -6,11 +6,11 @@
"source": "index.html",
"devDependencies": {
"process": "^0.11.10",
"stylelint": "^17.11.1",
"stylelint": "^17.12.0",
"stylelint-config-standard": "^40.0.0"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "node --test resources/vue/components/*.test.mjs"
},
"author": "",
"parcelIgnore": [
@ -33,8 +33,8 @@
"iconify-icon": "^3.0.2",
"picocrank": "^1.15.0",
"standard": "^17.1.2",
"unplugin-vue-components": "^32.0.0",
"vite": "^8.0.13",
"unplugin-vue-components": "^32.1.0",
"vite": "^8.0.14",
"vue": "^3.5.34",
"vue-i18n": "^11.4.4",
"vue-router": "^5.0.7"

View File

@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.11.0
// @generated by protoc-gen-es v2.12.0
// @generated from file olivetin/api/v1/olivetin.proto (package olivetin.api.v1, syntax proto3)
/* eslint-disable */
@ -60,6 +60,36 @@ export declare type Action = Message<"olivetin.api.v1.Action"> & {
* @generated from field: string datetime_rate_limit_expires = 9;
*/
datetimeRateLimitExpires: string;
/**
* @generated from field: bool exec_on_startup = 10;
*/
execOnStartup: boolean;
/**
* @generated from field: repeated string exec_on_cron = 11;
*/
execOnCron: string[];
/**
* @generated from field: repeated string exec_on_file_created_in_dir = 12;
*/
execOnFileCreatedInDir: string[];
/**
* @generated from field: repeated string exec_on_file_changed_in_dir = 13;
*/
execOnFileChangedInDir: string[];
/**
* @generated from field: string exec_on_calendar_file = 14;
*/
execOnCalendarFile: string;
/**
* @generated from field: repeated olivetin.api.v1.ActionWebhookExecHint exec_on_webhooks = 15;
*/
execOnWebhooks: ActionWebhookExecHint[];
};
/**
@ -68,6 +98,37 @@ export declare type Action = Message<"olivetin.api.v1.Action"> & {
*/
export declare const ActionSchema: GenMessage<Action>;
/**
* @generated from message olivetin.api.v1.ActionWebhookExecHint
*/
export declare type ActionWebhookExecHint = Message<"olivetin.api.v1.ActionWebhookExecHint"> & {
/**
* @generated from field: string template = 1;
*/
template: string;
/**
* @generated from field: string match_path = 2;
*/
matchPath: string;
/**
* @generated from field: map<string, string> match_headers = 3;
*/
matchHeaders: { [key: string]: string };
/**
* @generated from field: map<string, string> match_query = 4;
*/
matchQuery: { [key: string]: string };
};
/**
* Describes the message olivetin.api.v1.ActionWebhookExecHint.
* Use `create(ActionWebhookExecHintSchema)` to create a new message.
*/
export declare const ActionWebhookExecHintSchema: GenMessage<ActionWebhookExecHint>;
/**
* @generated from message olivetin.api.v1.ActionArgument
*/
@ -188,7 +249,7 @@ export declare type GetDashboardResponse = Message<"olivetin.api.v1.GetDashboard
/**
* @generated from field: olivetin.api.v1.Dashboard dashboard = 4;
*/
dashboard?: Dashboard;
dashboard?: Dashboard | undefined;
};
/**
@ -302,7 +363,7 @@ export declare type DashboardComponent = Message<"olivetin.api.v1.DashboardCompo
/**
* @generated from field: olivetin.api.v1.Action action = 6;
*/
action?: Action;
action?: Action | undefined;
/**
* @generated from field: string entity_type = 7;
@ -412,7 +473,7 @@ export declare type StartActionAndWaitResponse = Message<"olivetin.api.v1.StartA
/**
* @generated from field: olivetin.api.v1.LogEntry log_entry = 1;
*/
logEntry?: LogEntry;
logEntry?: LogEntry | undefined;
};
/**
@ -476,7 +537,7 @@ export declare type StartActionByGetAndWaitResponse = Message<"olivetin.api.v1.S
/**
* @generated from field: olivetin.api.v1.LogEntry log_entry = 1;
*/
logEntry?: LogEntry;
logEntry?: LogEntry | undefined;
};
/**
@ -825,7 +886,7 @@ export declare type ExecutionStatusResponse = Message<"olivetin.api.v1.Execution
/**
* @generated from field: olivetin.api.v1.LogEntry log_entry = 1;
*/
logEntry?: LogEntry;
logEntry?: LogEntry | undefined;
};
/**
@ -1135,7 +1196,7 @@ export declare type EventExecutionFinished = Message<"olivetin.api.v1.EventExecu
/**
* @generated from field: olivetin.api.v1.LogEntry log_entry = 1;
*/
logEntry?: LogEntry;
logEntry?: LogEntry | undefined;
};
/**
@ -1151,7 +1212,7 @@ export declare type EventExecutionStarted = Message<"olivetin.api.v1.EventExecut
/**
* @generated from field: olivetin.api.v1.LogEntry log_entry = 1;
*/
logEntry?: LogEntry;
logEntry?: LogEntry | undefined;
};
/**
@ -1437,7 +1498,7 @@ export declare type InitResponse = Message<"olivetin.api.v1.InitResponse"> & {
/**
* @generated from field: olivetin.api.v1.EffectivePolicy effective_policy = 18;
*/
effectivePolicy?: EffectivePolicy;
effectivePolicy?: EffectivePolicy | undefined;
/**
* @generated from field: string banner_message = 19;
@ -1553,7 +1614,7 @@ export declare type GetActionBindingResponse = Message<"olivetin.api.v1.GetActio
/**
* @generated from field: olivetin.api.v1.Action action = 1;
*/
action?: Action;
action?: Action | undefined;
};
/**
@ -1858,3 +1919,4 @@ export declare const OliveTinApiService: GenService<{
output: typeof EntitySchema;
},
}>;

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
<template>
<div :id="`actionButton-${bindingId}`" role="none" class="action-button">
<div :id="`actionButton-${bindingId}`" role="none" class="action-button" @contextmenu.prevent="openActionDetails">
<button :id="`actionButtonInner-${bindingId}`" :title="title" :disabled="!canExec || isDisabled"
:class="combinedClasses" @click="handleClick">
@ -18,7 +18,7 @@
</div>
</div>
<span class="icon" v-html="unicodeIcon"></span>
<ActionIconGlyph class="icon" :glyph="actionGlyph" />
<span class="title" aria-live="polite">{{ displayTitle }}
</span>
<span v-if="rateLimitMessage" class="rate-limit-message">{{ rateLimitMessage }}</span>
@ -33,7 +33,9 @@ import { useRouter } from 'vue-router'
import { HugeiconsIcon } from '@hugeicons/vue'
import { WorkoutRunIcon, TypeCursorIcon, ComputerTerminal01Icon, WorkHistoryIcon } from '@hugeicons/core-free-icons'
import { ref, watch, onMounted, onUnmounted, inject, computed } from 'vue'
import ActionIconGlyph from './components/ActionIconGlyph.vue'
import { ref, watch, onMounted, onUnmounted, computed } from 'vue'
const router = useRouter()
const navigateOnStart = ref('')
@ -56,7 +58,6 @@ const canExec = ref(true)
const popupOnStart = ref('')
// Display properties
const unicodeIcon = ref('&#x1f4a9;')
const displayTitle = ref('')
// State
@ -77,6 +78,9 @@ const showNavigateOnStartIcons = computed(() => {
return window.initResponse?.showNavigateOnStartIcons ?? true
})
const actionGlyph = computed(() => props.actionData?.icon ?? '')
const glyph = ref('')
// Combined classes including custom cssClass
const combinedClasses = computed(() => {
const classes = [...buttonClasses.value]
@ -89,16 +93,6 @@ const combinedClasses = computed(() => {
// Timestamps
const updateIterationTimestamp = ref(0)
function getUnicodeIcon(icon) {
if (icon === '') {
console.log('icon not found ', icon)
return '&#x1f4a9;'
} else {
return unescape(icon)
}
}
function constructFromJson(json) {
updateIterationTimestamp.value = 0
@ -119,8 +113,7 @@ function constructFromJson(json) {
isDisabled.value = !json.canExec
displayTitle.value = title.value
unicodeIcon.value = getUnicodeIcon(json.icon)
glyph.value = json.icon ?? ''
// Initialize rate limit from action data (parse datetime string)
if (json.datetimeRateLimitExpires) {
const date = new Date(json.datetimeRateLimitExpires.replace(' ', 'T'))
@ -139,8 +132,6 @@ function updateFromJson(json) {
// Fields that should not be updated
// title - as the callback URL relies on it
unicodeIcon.value = getUnicodeIcon(json.icon)
// Update rate limiting if changed (parse datetime string)
if (json.datetimeRateLimitExpires) {
const date = new Date(json.datetimeRateLimitExpires.replace(' ', 'T'))
@ -191,7 +182,19 @@ function updateRateLimitStatus() {
}
}
function openActionDetails() {
const id = props.actionData?.bindingId
if (!id) {
return
}
router.push(`/action/${id}`)
}
async function handleClick() {
if (popupOnStart.value === 'history') {
openActionDetails()
return
}
if (props.actionData.arguments && props.actionData.arguments.length > 0) {
router.push(`/actionBinding/${props.actionData.bindingId}/argumentForm`)
} else {
@ -249,8 +252,6 @@ function onLogEntryChanged(logEntry) {
function onExecutionStarted(logEntry) {
if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) {
router.push(`/logs/${logEntry.executionTrackingId}`)
} else if (popupOnStart.value === 'history') {
router.push(`/action/${bindingId.value}`)
}
isDisabled.value = true
@ -324,10 +325,17 @@ watch(
() => props.actionData,
(newData) => {
updateFromJson(newData)
if (newData?.icon !== undefined) {
glyph.value = newData.icon ?? ''
}
},
{ deep: true }
)
defineExpose({
glyph
})
</script>
<style>

View File

@ -0,0 +1,73 @@
<template>
<span class="action-icon-glyph">
<HugeiconsIcon
v-if="hugeiconsModel"
:icon="hugeiconsModel"
width="1em"
height="1em"
class="action-icon-glyph-svg"
/>
<span v-else-if="decodedTextGlyphIsHtml" v-html="decodedTextGlyph"></span>
<span v-else v-text="decodedTextGlyph"></span>
</span>
</template>
<script setup>
import { computed } from 'vue'
import { HugeiconsIcon } from '@hugeicons/vue'
import { CommandLineIcon } from '@hugeicons/core-free-icons'
import { decodeHtmlEntities, glyphLooksLikeHtml } from './actionIconGlyphHelpers.mjs'
const hugeiconsPrefix = 'hugeicons:'
/** Maps config values like hugeicons:CommandLineIcon to Hugeicons icon definitions. */
const hugeiconsRegistry = {
CommandLineIcon,
}
const props = defineProps({
glyph: {
type: String,
required: false,
default: '',
},
})
const hugeiconsModel = computed(() => {
if (!props.glyph) {
return CommandLineIcon
}
if (!props.glyph.startsWith(hugeiconsPrefix)) {
return null
}
const name = props.glyph.slice(hugeiconsPrefix.length)
const iconModel = hugeiconsRegistry[name]
return iconModel ?? CommandLineIcon
})
const decodedTextGlyph = computed(() => {
if (hugeiconsModel.value) {
return ''
}
return decodeHtmlEntities(props.glyph)
})
const decodedTextGlyphIsHtml = computed(() => glyphLooksLikeHtml(decodedTextGlyph.value))
</script>
<style scoped>
.action-icon-glyph {
display: inline-flex;
vertical-align: middle;
align-items: center;
justify-content: center;
}
.action-icon-glyph-svg {
vertical-align: middle;
}
</style>

View File

@ -0,0 +1,38 @@
const fallbackNamedHtmlEntities = {
amp: '&',
apos: "'",
darr: '\u2193',
gt: '>',
laquo: '\u00ab',
larr: '\u2190',
nbsp: '\u00a0',
quot: '"',
raquo: '\u00bb',
rarr: '\u2192',
uarr: '\u2191',
}
export function decodeHtmlEntities(text) {
if (typeof document !== 'undefined') {
const textarea = document.createElement('textarea')
textarea.innerHTML = text
return textarea.value
}
return text.replace(/&#x([0-9a-fA-F]+);?/g, (_, hex) => {
const codePoint = Number.parseInt(hex, 16)
return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : ''
}).replace(/&#(\d+);?/g, (_, decimal) => {
const codePoint = Number.parseInt(decimal, 10)
return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : ''
}).replace(/&([a-zA-Z][a-zA-Z0-9]+);?/g, (entity, name) => {
return fallbackNamedHtmlEntities[name] ?? entity
})
}
export function glyphLooksLikeHtml(text) {
const trimmedText = text.trim()
return trimmedText.startsWith('<') || /<img\b/i.test(text) || /\/custom-webui\//i.test(text)
}

View File

@ -0,0 +1,20 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { decodeHtmlEntities, glyphLooksLikeHtml } from './actionIconGlyphHelpers.mjs'
test('decodeHtmlEntities decodes named entity icons as plain glyph text', () => {
assert.equal(decodeHtmlEntities('&laquo;'), '\u00ab')
assert.equal(decodeHtmlEntities('&rarr;'), '\u2192')
assert.equal(decodeHtmlEntities('&laquo; next &rarr;'), '\u00ab next \u2192')
})
test('decoded named entity icons are not treated as HTML markup', () => {
const decodedGlyph = decodeHtmlEntities('&rarr;')
assert.equal(glyphLooksLikeHtml(decodedGlyph), false)
})
test('decodeHtmlEntities keeps existing numeric entity icon support', () => {
assert.equal(decodeHtmlEntities('&#x1f4a9;'), '\ud83d\udca9')
assert.equal(decodeHtmlEntities('&#128190;'), '\ud83d\udcbe')
})

View File

@ -95,6 +95,19 @@ const routes = [
]
}
},
{
path: '/action/:actionId/actionexecconditions',
name: 'ActionExecConditions',
component: () => import('./views/ActionExecConditionsView.vue'),
props: true,
meta: {
title: 'Execution conditions',
breadcrumb: [
{ name: "Actions", href: "/" },
{ name: "Execution conditions" },
]
}
},
{
path: '/diagnostics',
name: 'Diagnostics',

View File

@ -1,12 +1,22 @@
<template>
<Section :title="'Action Details: ' + actionTitle" :padding="false">
<template #toolbar>
<div class="action-details-toolbar">
<button v-if="action" @click="startAction" title="Start this action" class="button neutral">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path fill="currentColor" d="M8 6v12l8-6z" />
</svg>
Start
</button>
<router-link
v-if="action"
:to="{ name: 'ActionExecConditions', params: { actionId: route.params.actionId } }"
class="button neutral"
title="View configured automatic triggers and on-demand execution"
>
Execution conditions
</router-link>
</div>
</template>
<div class = "flex-row padding" v-if="action">
@ -22,7 +32,7 @@
</p>
</div>
<div style = "align-self: start; text-align: right;">
<span class="icon" v-html="action.icon"></span>
<ActionIconGlyph class="icon" :glyph="action.icon" />
<div class="filter-container">
<label class="input-with-icons">
@ -94,6 +104,7 @@ import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import Pagination from 'picocrank/vue/components/Pagination.vue'
import Section from 'picocrank/vue/components/Section.vue'
import ActionIconGlyph from '../components/ActionIconGlyph.vue'
import ActionStatusDisplay from '../components/ActionStatusDisplay.vue'
const route = useRoute()
@ -428,5 +439,11 @@ watch(
.padding {
padding: 1rem;
}
</style>
.action-details-toolbar {
display: inline-flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
}
</style>

View File

@ -0,0 +1,219 @@
<template>
<Section :title="'Execution conditions: ' + actionTitle" :padding="false">
<template #toolbar>
<router-link :to="{ name: 'ActionDetails', params: { actionId: route.params.actionId } }" class="button neutral">
Back to action details
</router-link>
</template>
<div v-if="action" class="padding content">
<p>
These entries mirror the automatic triggers from your OliveTin configuration for this action.
You can always run the action manually as well.
</p>
<h3 class="exec-type-heading">
On demand
<a class="doc-link" :href="execConditionDocs.onDemand" target="_blank" rel="noopener noreferrer">Documentation</a>
</h3>
<p>
Manual execution from the web UI (dashboard or action details), or via the API (for example StartAction),
is always available when your user is allowed to execute the action.
</p>
<template v-if="action.execOnStartup">
<h3 class="exec-type-heading">
<code>execOnStartup</code>
<a class="doc-link" :href="execConditionDocs.startup" target="_blank" rel="noopener noreferrer">Documentation</a>
</h3>
<p>Runs once when OliveTin starts.</p>
</template>
<template v-if="nonEmptyList(action.execOnCron)">
<h3 class="exec-type-heading">
<code>execOnCron</code>
<a class="doc-link" :href="execConditionDocs.cron" target="_blank" rel="noopener noreferrer">Documentation</a>
</h3>
<ul>
<li v-for="(line, idx) in action.execOnCron" :key="'cron-' + idx"><code>{{ line }}</code></li>
</ul>
</template>
<template v-if="nonEmptyList(action.execOnFileCreatedInDir)">
<h3 class="exec-type-heading">
<code>execOnFileCreatedInDir</code>
<a class="doc-link" :href="execConditionDocs.fileCreated" target="_blank" rel="noopener noreferrer">Documentation</a>
</h3>
<ul>
<li v-for="(dir, idx) in action.execOnFileCreatedInDir" :key="'created-' + idx"><code>{{ dir }}</code></li>
</ul>
</template>
<template v-if="nonEmptyList(action.execOnFileChangedInDir)">
<h3 class="exec-type-heading">
<code>execOnFileChangedInDir</code>
<a class="doc-link" :href="execConditionDocs.fileChanged" target="_blank" rel="noopener noreferrer">Documentation</a>
</h3>
<ul>
<li v-for="(dir, idx) in action.execOnFileChangedInDir" :key="'changed-' + idx"><code>{{ dir }}</code></li>
</ul>
</template>
<template v-if="action.execOnCalendarFile">
<h3 class="exec-type-heading">
<code>execOnCalendarFile</code>
<a class="doc-link" :href="execConditionDocs.calendar" target="_blank" rel="noopener noreferrer">Documentation</a>
</h3>
<p><code>{{ action.execOnCalendarFile }}</code></p>
</template>
<template v-if="nonEmptyList(action.execOnWebhooks)">
<h3 class="exec-type-heading">
<code>execOnWebhook</code>
<a class="doc-link" :href="execConditionDocs.webhook" target="_blank" rel="noopener noreferrer">Documentation</a>
</h3>
<ul class="webhook-list">
<li v-for="(wh, idx) in action.execOnWebhooks" :key="'wh-' + idx">
<span v-if="wh.template">template: <code>{{ wh.template }}</code></span>
<span v-if="wh.matchPath"> · matchPath: <code>{{ wh.matchPath }}</code></span>
<span v-if="nonEmptyObject(wh.matchHeaders)"> · matchHeaders: <code>{{ wh.matchHeaders }}</code></span>
<span v-if="nonEmptyObject(wh.matchQuery)"> · matchQuery: <code>{{ wh.matchQuery }}</code></span>
<span v-if="!webhookHasCondition(wh)">Webhook trigger (no conditions in response)</span>
</li>
</ul>
</template>
<p v-if="!hasConfiguredTriggers" class="muted">
This action has no automatic triggers in configuration besides on-demand execution.
</p>
</div>
<div v-else-if="!loading" class="padding empty-state">
<p>Could not load this action.</p>
<router-link :to="{ name: 'Actions' }">Return to index</router-link>
</div>
</Section>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import Section from 'picocrank/vue/components/Section.vue'
const route = useRoute()
const action = ref(null)
const actionTitle = ref('Action')
const loading = ref(true)
const execConditionDocs = {
onDemand: 'https://docs.olivetin.app/action_execution/ondemand.html',
startup: 'https://docs.olivetin.app/action_execution/onstartup.html',
cron: 'https://docs.olivetin.app/action_execution/oncron.html',
fileCreated: 'https://docs.olivetin.app/action_execution/onfilecreated.html',
fileChanged: 'https://docs.olivetin.app/action_execution/onfilechanged.html',
calendar: 'https://docs.olivetin.app/action_execution/oncalendar.html',
webhook: 'https://docs.olivetin.app/action_execution/onwebhook.html',
}
function nonEmptyList(list) {
return Array.isArray(list) && list.length > 0
}
function nonEmptyObject(object) {
return object && Object.keys(object).length > 0
}
function webhookHasCondition(webhook) {
return webhook.template || webhook.matchPath || nonEmptyObject(webhook.matchHeaders) || nonEmptyObject(webhook.matchQuery)
}
const hasConfiguredTriggers = computed(() => {
const a = action.value
if (!a) {
return false
}
if (a.execOnStartup) {
return true
}
if (nonEmptyList(a.execOnCron) || nonEmptyList(a.execOnFileCreatedInDir) || nonEmptyList(a.execOnFileChangedInDir)) {
return true
}
if (a.execOnCalendarFile) {
return true
}
if (nonEmptyList(a.execOnWebhooks)) {
return true
}
return false
})
async function fetchAction() {
loading.value = true
try {
const actionId = route.params.actionId
const response = await window.client.getActionBinding({ bindingId: actionId })
action.value = response.action
actionTitle.value = response.action?.title || 'Action'
} catch (err) {
console.error('Failed to fetch action:', err)
window.showBigError('fetch-action-exec-conditions', 'getting action', err, false)
action.value = null
} finally {
loading.value = false
}
}
onMounted(fetchAction)
watch(
() => route.params.actionId,
() => {
action.value = null
actionTitle.value = 'Action'
fetchAction()
}
)
</script>
<style scoped>
.content h3 {
margin-top: 1.25rem;
margin-bottom: 0.35rem;
font-size: 1rem;
}
.exec-type-heading {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.35rem 0.75rem;
}
.exec-type-heading .doc-link {
font-size: 0.85rem;
font-weight: normal;
}
.content p,
.content ul {
margin: 0.35rem 0 0;
}
.webhook-list li {
margin-bottom: 0.35rem;
}
.muted {
color: var(--text-secondary);
margin-top: 1.5rem;
}
.empty-state {
text-align: center;
color: var(--text-secondary);
}
.padding {
padding: 1rem;
}
</style>

View File

@ -174,7 +174,7 @@ function getInputType(arg) {
return 'checkbox'
}
if (arg.type === 'ascii_identifier' || arg.type === 'ascii' || arg.type === 'ascii_sentence') {
if (arg.type === 'ascii_identifier' || arg.type === 'shell_safe_identifier' || arg.type === 'ascii' || arg.type === 'ascii_sentence') {
return 'text'
}
@ -392,6 +392,11 @@ async function startAction(actionArgs) {
async function handleSubmit(event) {
event.preventDefault()
if (popupOnStart.value === 'history') {
router.push(`/action/${props.bindingId}`)
return
}
// Set custom validity for required fields
for (const arg of actionArguments.value) {
const value = argValues.value[arg.name]
@ -422,8 +427,6 @@ async function handleSubmit(event) {
const response = await startAction(argvs)
if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) {
router.push(`/logs/${response.executionTrackingId}`)
} else if (popupOnStart.value === 'history') {
router.push(`/action/${props.bindingId}`)
} else {
router.back()
}

View File

@ -25,7 +25,7 @@
<ActionStatusDisplay :log-entry="logEntry" id = "execution-dialog-status" />
</dd>
</dl>
<span class="icon" role="img" v-html="icon" style = "align-self: start"></span>
<ActionIconGlyph class="icon" role="img" :glyph="icon" style="align-self: start" />
</div>
<div v-if="notFound" class="error-message padded-content">
@ -62,6 +62,7 @@
<script setup>
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
import ActionIconGlyph from '../components/ActionIconGlyph.vue'
import ActionStatusDisplay from '../components/ActionStatusDisplay.vue'
import Section from 'picocrank/vue/components/Section.vue'
import { OutputTerminal } from '../../../js/OutputTerminal.js'
@ -151,7 +152,7 @@ async function reset() {
function show(actionButton) {
if (actionButton) {
icon.value = actionButton.domIcon.innerText
icon.value = actionButton.glyph ?? ''
}
canKill.value = true

View File

@ -47,7 +47,7 @@
<tr v-for="log in filteredLogs" :key="log.executionTrackingId" class="log-row" :title="log.actionTitle">
<td class="timestamp">{{ formatTimestamp(log.datetimeStarted) }}</td>
<td>
<span class="icon" v-html="log.actionIcon"></span>
<ActionIconGlyph class="icon" :glyph="log.actionIcon" />
<router-link :to="`/logs/${log.executionTrackingId}`">
{{ log.actionTitle }}
</router-link>
@ -93,6 +93,7 @@ import Pagination from 'picocrank/vue/components/Pagination.vue'
import Section from 'picocrank/vue/components/Section.vue'
import { useI18n } from 'vue-i18n'
import ActionStatusDisplay from '../components/ActionStatusDisplay.vue'
import ActionIconGlyph from '../components/ActionIconGlyph.vue'
const route = useRoute()
const router = useRouter()

View File

@ -14,7 +14,7 @@
"devDependencies": {
"chai": "^6.2.2",
"eslint": "^10.4.0",
"mocha": "^11.7.5",
"mocha": "^11.7.6",
"selenium-webdriver": "^4.44.0"
}
},
@ -1575,9 +1575,9 @@
}
},
"node_modules/mocha": {
"version": "11.7.5",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz",
"integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==",
"version": "11.7.6",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.6.tgz",
"integrity": "sha512-nS9xOGbw2I3cjCpxwZAEJ9xK9lmJ08vEkQvLtz4du9ZrF9UrjRpeJGiIgl2Z+Qs++pmB4ecDe48Fwsh+j+j7xA==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@ -13,7 +13,7 @@
"devDependencies": {
"chai": "^6.2.2",
"eslint": "^10.4.0",
"mocha": "^11.7.5",
"mocha": "^11.7.6",
"selenium-webdriver": "^4.44.0"
},
"dependencies": {

View File

@ -14,6 +14,19 @@ message Action {
int32 order = 7;
int32 timeout = 8;
string datetime_rate_limit_expires = 9; // Datetime when rate limit expires (empty string if not rate limited), format: "2006-01-02 15:04:05"
bool exec_on_startup = 10;
repeated string exec_on_cron = 11;
repeated string exec_on_file_created_in_dir = 12;
repeated string exec_on_file_changed_in_dir = 13;
string exec_on_calendar_file = 14;
repeated ActionWebhookExecHint exec_on_webhooks = 15;
}
message ActionWebhookExecHint {
string template = 1;
string match_path = 2;
map<string, string> match_headers = 3;
map<string, string> match_query = 4;
}
message ActionArgument {

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,16 @@
module github.com/OliveTin/OliveTin
go 1.25.7
go 1.25.10
exclude google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884
require (
connectrpc.com/connect v1.19.2
connectrpc.com/connect v1.20.0
github.com/Masterminds/semver v1.5.0
github.com/MicahParks/keyfunc/v3 v3.8.0
github.com/PaesslerAG/jsonpath v0.1.1
github.com/alexedwards/argon2id v1.0.0
github.com/bufbuild/buf v1.69.0
github.com/bufbuild/buf v1.70.0
github.com/fsnotify/fsnotify v1.10.1
github.com/fzipp/gocyclo v0.6.0
github.com/go-critic/go-critic v0.14.3
@ -29,7 +29,7 @@ require (
go.akshayshah.org/connectproto v0.6.0
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a
golang.org/x/oauth2 v0.36.0
golang.org/x/sys v0.44.0
golang.org/x/sys v0.45.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
)
@ -38,7 +38,7 @@ require (
buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20250718181942-e35f9b667443.1 // indirect
buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1 // indirect
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1 // indirect
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.2-20260507063250-43b0c5a6cd08.1 // indirect
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260507063250-43b0c5a6cd08.1 // indirect
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260507063250-43b0c5a6cd08.1 // indirect
buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1 // indirect
buf.build/go/app v0.2.1-0.20260407195847-833f8f978cda // indirect
@ -57,7 +57,7 @@ require (
github.com/PaesslerAG/gval v1.2.4 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bufbuild/protocompile v0.14.2-0.20260429155904-12ef1ef2ce91 // indirect
github.com/bufbuild/protocompile v0.14.2-0.20260522222248-64e6ad034132 // indirect
github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cli/browser v1.3.0 // indirect
@ -68,7 +68,7 @@ require (
github.com/cristalhq/acmd v0.12.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/cli v29.5.1+incompatible // indirect
github.com/docker/cli v29.5.2+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker v28.5.2+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.7 // indirect
@ -148,17 +148,17 @@ require (
go.uber.org/zap v1.28.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/crypto v0.52.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20260508232706-74f9aab9d74a // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/net v0.54.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/term v0.43.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.45.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260519071638-aa98bba5eb94 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260523011958-0a33c5d7ca68 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 // indirect
google.golang.org/grpc v1.79.3 // indirect
mvdan.cc/xurls/v2 v2.6.0 // indirect
pluginrpc.com/pluginrpc v0.5.0 // indirect

View File

@ -16,6 +16,8 @@ buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260126144947-81958296
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260126144947-819582968857.2/go.mod h1:mpsjeEaxOYPIJV2cz4IagLghZufRvx+NPVtInjEeoQ8=
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.2-20260507063250-43b0c5a6cd08.1 h1:DcwtSdaY9CwXwPSOneDxJ/B0OCAgNPQQaQxAr/pTHvc=
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.2-20260507063250-43b0c5a6cd08.1/go.mod h1:WjOwVG7wzFSwEkjCjHVRWEOdGYyON/TQYPabl7N2VGI=
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260507063250-43b0c5a6cd08.1 h1:f8pa4iy1Bs+hQ16f3jg22rV/StDKIRj1rNNWX5rLwZ8=
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260507063250-43b0c5a6cd08.1/go.mod h1:7MNigA51XJjPKrLnbcE61BmgW+pAp3mLW61gUqLBBRY=
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20251202164234-62b14f0b533c.1 h1:PdfIJUbUVKdajMVYuMdvr2Wvo+wmzGnlPEYA4bhFaWI=
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20251202164234-62b14f0b533c.1/go.mod h1:1JJi9jvOqRxSMa+JxiZSm57doB+db/1WYCIa2lHfc40=
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260122161138-ab4e39a3c3bc.1 h1:yWmrELGX6l1GphG9kPVcrMQLjWfXGI5bLDxwE+SfbDw=
@ -64,6 +66,8 @@ connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=
connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
connectrpc.com/connect v1.19.2 h1:McQ83FGdzL+t60peksi0gXC7MQ/iLKgLduAnThbM0mo=
connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
connectrpc.com/otelconnect v0.8.0 h1:a4qrN4H8aEE2jAoCxheZYYfEjXMgVPyL9OzPQLBEFXU=
connectrpc.com/otelconnect v0.8.0/go.mod h1:AEkVLjCPXra+ObGFCOClcJkNjS7zPaQSqvO0lCyjfZc=
connectrpc.com/otelconnect v0.9.0 h1:NggB3pzRC3pukQWaYbRHJulxuXvmCKCKkQ9hbrHAWoA=
@ -112,6 +116,8 @@ github.com/bufbuild/buf v1.66.1 h1:wqmmU+6uoxB/eYDOmXq2To4qEXvOJN7gR6L9AxrPL1E=
github.com/bufbuild/buf v1.66.1/go.mod h1:Vd3ELm8IePWaDJaS9FLy94FFOnLrjLi4mDxmXtw9Xio=
github.com/bufbuild/buf v1.69.0 h1:q1YTnHJISHuoeUdmsuC9u+nb9rV8glM/TOsPNEteEzg=
github.com/bufbuild/buf v1.69.0/go.mod h1:Q3KRCXSanDCMFs2zL/MqUwUQV0OUqs23P2sy58CW0nc=
github.com/bufbuild/buf v1.70.0 h1:rGL4TGoy8F1DbQa4BSlMOVBBR7lWblfnKxUdOxmeFns=
github.com/bufbuild/buf v1.70.0/go.mod h1:5gCCIpDmBzhiSJwqmxmbdN5aRZQYGmFSGnOBE7seP8c=
github.com/bufbuild/protocompile v0.14.2-0.20251223142729-db46c1b9d34e h1:LQA+1MyiPkolGHJGC2GMDC5Xu+0RDVH6jGMKech7Exs=
github.com/bufbuild/protocompile v0.14.2-0.20251223142729-db46c1b9d34e/go.mod h1:5UUj46Eu+U+C59C5N6YilaMI7WWfP2bW9xGcOkme2DI=
github.com/bufbuild/protocompile v0.14.2-0.20260105175043-4d8d90b1c6b8 h1:cQYwUyAzyMmYr7AyJU1C6pVCpUrJJBkmx7UunZosxxs=
@ -126,6 +132,8 @@ github.com/bufbuild/protocompile v0.14.2-0.20260306221011-519528254156 h1:XOfIIn
github.com/bufbuild/protocompile v0.14.2-0.20260306221011-519528254156/go.mod h1:cxhE8h+14t0Yxq2H9MV/UggzQ1L0gh0t2tJobITWsBE=
github.com/bufbuild/protocompile v0.14.2-0.20260429155904-12ef1ef2ce91 h1:RPIMBLTMx/CRy0NVyb6yJDlGx2Vo84FsU+kAh46zqIA=
github.com/bufbuild/protocompile v0.14.2-0.20260429155904-12ef1ef2ce91/go.mod h1:DhgqsRznX/F0sGkUYtTQJRP+q8xMReQRQ3qr+n1opWU=
github.com/bufbuild/protocompile v0.14.2-0.20260522222248-64e6ad034132 h1:f4T4k/41jHHhp2Otl6ZShDedr4wF9b+NdqIfLezx4R4=
github.com/bufbuild/protocompile v0.14.2-0.20260522222248-64e6ad034132/go.mod h1:jPUiZUFWc8E3Kc2Y4SRlGAdjde4amGkHY0BUACNS43E=
github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 h1:V1xulAoqLqVg44rY97xOR+mQpD2N+GzhMHVwJ030WEU=
github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ=
github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b h1:b7wvo9ZhjLzCp7tGbOUMvgtYTnd33zGSAmMxcdxMnhQ=
@ -170,6 +178,8 @@ github.com/docker/cli v29.4.3+incompatible h1:u+UliYm2J/rYrIh2FqHQg32neRG8GjbvNu
github.com/docker/cli v29.4.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.5.1+incompatible h1:NiufLAJoRcPauFoBNYthfuM4REFwM8H2h9xnLABNHGs=
github.com/docker/cli v29.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.5.2+incompatible h1:ubykJ1Y8LmNRGJ2BuMQ0kHOt/RO1YzGNswqWMJgivuQ=
github.com/docker/cli v29.5.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
@ -531,6 +541,8 @@ golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
@ -582,6 +594,8 @@ golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
@ -616,6 +630,8 @@ golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -683,6 +699,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348 h1:
google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348/go.mod h1:Yzdzr5OOZFgSsEV2D/Xi9NL3bszpXFAg0hFJiRohcD8=
google.golang.org/genproto/googleapis/api v0.0.0-20260519071638-aa98bba5eb94 h1:DddG61lE5LkX6144z22i0gma9BMBs5aZ9B8lZLobxyw=
google.golang.org/genproto/googleapis/api v0.0.0-20260519071638-aa98bba5eb94/go.mod h1:1dCETSCY2YKZNXQE3h4fun3TYwF5p8jejRKZgfWAgAY=
google.golang.org/genproto/googleapis/api v0.0.0-20260523011958-0a33c5d7ca68 h1:WVVw1Nl19li0fMX++FJ3ye1z9+S1N35QODDy5qpnaXw=
google.golang.org/genproto/googleapis/api v0.0.0-20260523011958-0a33c5d7ca68/go.mod h1:1dCETSCY2YKZNXQE3h4fun3TYwF5p8jejRKZgfWAgAY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
@ -699,6 +717,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348 h1:
google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94 h1:eZCjr/aAF8c5ccm5pb6T4EXgIei5MlAAPWPJk+5ArfY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 h1:PvEgGJf9C/1u5CHkInMg7UFYYUoiaQmW2LbtH0pjB78=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=

View File

@ -0,0 +1,27 @@
package api
import (
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
config "github.com/OliveTin/OliveTin/internal/config"
)
func applyActionExecTriggers(pb *apiv1.Action, cfg *config.Action) {
if cfg == nil {
return
}
pb.ExecOnStartup = cfg.ExecOnStartup
pb.ExecOnCron = append([]string(nil), cfg.ExecOnCron...)
pb.ExecOnFileCreatedInDir = append([]string(nil), cfg.ExecOnFileCreatedInDir...)
pb.ExecOnFileChangedInDir = append([]string(nil), cfg.ExecOnFileChangedInDir...)
pb.ExecOnCalendarFile = cfg.ExecOnCalendarFile
for _, wh := range cfg.ExecOnWebhook {
pb.ExecOnWebhooks = append(pb.ExecOnWebhooks, &apiv1.ActionWebhookExecHint{
Template: wh.Template,
MatchPath: wh.MatchPath,
MatchHeaders: wh.MatchHeaders,
MatchQuery: wh.MatchQuery,
})
}
}

View File

@ -156,6 +156,8 @@ func buildAction(actionBinding *executor.ActionBinding, rr *DashboardRenderReque
DatetimeRateLimitExpires: datetimeRateLimitExpires,
}
applyActionExecTriggers(&btn, action)
for _, cfgArg := range action.Arguments {
pbArg := apiv1.ActionArgument{
Name: cfgArg.Name,

View File

@ -52,6 +52,24 @@ func getNewTestServerAndClient(injectedConfig *config.Config) (*httptest.Server,
return ts, client
}
func TestApplyActionExecTriggersIncludesWebhookHeaderAndQueryMatches(t *testing.T) {
cfg := &config.Action{
ExecOnWebhook: []config.WebhookConfig{
{
MatchHeaders: map[string]string{"X-GitHub-Event": "push"},
MatchQuery: map[string]string{"source": "github"},
},
},
}
pb := &apiv1.Action{}
applyActionExecTriggers(pb, cfg)
require.Len(t, pb.ExecOnWebhooks, 1)
assert.Equal(t, cfg.ExecOnWebhook[0].MatchHeaders, pb.ExecOnWebhooks[0].MatchHeaders)
assert.Equal(t, cfg.ExecOnWebhook[0].MatchQuery, pb.ExecOnWebhooks[0].MatchQuery)
}
func TestGetActionsAndStart(t *testing.T) {
cfg := config.DefaultConfig()

View File

@ -4,6 +4,9 @@ import (
"fmt"
)
// ReservedArgumentNamePrefix is reserved for OliveTin-injected system arguments.
const ReservedArgumentNamePrefix = "ot_"
// Action represents the core functionality of OliveTin - commands that show up
// as buttons in the UI.
type Action struct {
@ -286,7 +289,7 @@ func DefaultConfigWithBasePort(basePort int) *Config {
config.Security.HeaderXContentTypeOptions = true
config.Security.HeaderXFrameOptions = true
config.Security.XFrameOptions = "DENY"
config.DefaultIconForActions = "&#x1F600;"
config.DefaultIconForActions = "hugeicons:CommandLineIcon"
config.DefaultIconForDirectories = "&#128193"
config.DefaultIconForBack = "&laquo;"
config.ThemeCacheDisabled = false

View File

@ -26,6 +26,34 @@ func (cfg *Config) Sanitize() {
}
cfg.sanitizeDashboardsForInlineActions()
if err := cfg.validateReservedActionArgumentNames(); err != nil {
log.Fatalf("%v", err)
}
}
func (cfg *Config) validateReservedActionArgumentNames() error {
for _, action := range cfg.Actions {
if err := action.validateReservedArgumentNames(); err != nil {
return err
}
}
return nil
}
func (action *Action) validateReservedArgumentNames() error {
if action == nil {
return nil
}
for _, arg := range action.Arguments {
if strings.HasPrefix(arg.Name, ReservedArgumentNamePrefix) {
return fmt.Errorf("action %q argument %q uses reserved prefix %q", action.Title, arg.Name, ReservedArgumentNamePrefix)
}
}
return nil
}
func (cfg *Config) sanitizeDashboardsForInlineActions() {

View File

@ -34,7 +34,7 @@ func TestSanitizeConfig(t *testing.T) {
assert.NotNil(t, a2, "Found action after adding it")
assert.Equal(t, 3, a2.Timeout, "Default timeout is set")
assert.Equal(t, "&#x1F600;", a2.Icon, "Default icon is a smiley")
assert.Equal(t, "hugeicons:CommandLineIcon", a2.Icon, "Default icon is the neutral CLI glyph")
assert.Equal(t, "Carrots", a2.Arguments[0].Title, "Arg title is set to name")
assert.Equal(t, "Waffle", a2.Arguments[0].Choices[0].Title, "Choice title is set to name")
}
@ -92,6 +92,59 @@ func TestSanitizeConfigInlineDashboardActions(t *testing.T) {
}
}
func TestValidateReservedActionArgumentNames(t *testing.T) {
c := DefaultConfig()
c.Actions = append(c.Actions, &Action{
Title: "Reserved arg",
Arguments: []ActionArgument{
{Name: "ot_custom", Type: "ascii"},
},
})
err := c.validateReservedActionArgumentNames()
require.Error(t, err)
assert.Contains(t, err.Error(), `action "Reserved arg" argument "ot_custom" uses reserved prefix "ot_"`)
}
func TestValidateReservedActionArgumentNamesAllowsNonReserved(t *testing.T) {
c := DefaultConfig()
c.Actions = append(c.Actions, &Action{
Title: "Allowed arg",
Arguments: []ActionArgument{
{Name: "target", Type: "ascii"},
},
})
require.NoError(t, c.validateReservedActionArgumentNames())
}
func TestValidateReservedActionArgumentNamesChecksInlineActions(t *testing.T) {
c := DefaultConfig()
c.Dashboards = []*DashboardComponent{
{
Title: "Dashboard",
Contents: []*DashboardComponent{
{
Title: "Inline reserved arg",
InlineAction: &Action{
Shell: "echo test",
Arguments: []ActionArgument{
{Name: "ot_custom", Type: "ascii"},
},
},
},
},
},
}
c.sanitizeDashboardsForInlineActions()
err := c.validateReservedActionArgumentNames()
require.Error(t, err)
assert.Contains(t, err.Error(), `action "Inline reserved arg" argument "ot_custom" uses reserved prefix "ot_"`)
}
func TestValidateUniqueLocalUserAPIKeys(t *testing.T) {
t.Parallel()

View File

@ -21,6 +21,7 @@ var (
"unicode_identifier": `^[\w\-\.\_\d]+$`,
"ascii": `^[a-zA-Z0-9]+$`,
"ascii_identifier": `^[a-zA-Z0-9\-\._]+$`,
"shell_safe_identifier": `^[a-zA-Z0-9@\.\_\+\-]+$`,
"ascii_sentence": `^[a-zA-Z0-9\-\._, ]+$`,
}
)

View File

@ -576,6 +576,38 @@ func TestTypeSafetyCheckAsciiIdentifier(t *testing.T) {
}
}
func TestTypeSafetyCheckShellSafeIdentifier(t *testing.T) {
tests := []struct {
name string
value string
hasError bool
}{
{"Simple username", "alice123", false},
{"Email username", "alice@example.com", false},
{"Plus addressing", "alice+test@example.com", false},
{"Hyphen underscore dot", "alice-test_user.example", false},
{"Invalid space", "alice example", true},
{"Invalid shell substitution", "$(whoami)", true},
{"Invalid backtick", "`whoami`", true},
{"Invalid semicolon", "alice;id", true},
{"Invalid ampersand", "alice&id", true},
{"Invalid pipe", "alice|id", true},
{"Invalid quote", "alice'example", true},
{"Invalid slash", "alice/example", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := TypeSafetyCheck("username", tt.value, "shell_safe_identifier")
if tt.hasError {
assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
} else {
assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
}
})
}
}
func TestTypeSafetyCheckAsciiSentence(t *testing.T) {
tests := []struct {
name string

View File

@ -666,13 +666,15 @@ func stepACLCheck(req *ExecutionRequest) bool {
func stepParseArgs(req *ExecutionRequest) bool {
ensureArgumentMap(req)
injectSystemArgs(req)
if !hasBindingAndAction(req) {
return fail(req, fmt.Errorf("cannot parse arguments: Binding or Action is nil"))
}
filterToDefinedArgumentsOnly(req)
if err := injectSystemArgs(req); err != nil {
return fail(req, err)
}
mangleInvalidArgumentValues(req)
if hasExec(req) {
@ -735,7 +737,7 @@ func filterToDefinedArgumentsOnly(req *ExecutionRequest) {
func keepArgument(name string, definedNames map[string]struct{}) bool {
_, ok := definedNames[name]
return ok || strings.HasPrefix(name, "ot_")
return ok
}
func hasWebhookTag(req *ExecutionRequest) bool {
@ -747,9 +749,38 @@ func hasWebhookTag(req *ExecutionRequest) bool {
return false
}
func injectSystemArgs(req *ExecutionRequest) {
req.Arguments["ot_executionTrackingId"] = req.TrackingID
req.Arguments["ot_username"] = req.AuthenticatedUser.Username
var systemArgumentDefinitions = []config.ActionArgument{
{Name: "ot_executionTrackingId", Type: "ascii_identifier", RejectNull: true},
{Name: "ot_username", Type: "shell_safe_identifier", RejectNull: true},
}
func injectSystemArgs(req *ExecutionRequest) error {
args, err := validatedSystemArgs(req)
if err != nil {
return err
}
for name, value := range args {
req.Arguments[name] = value
}
return nil
}
func validatedSystemArgs(req *ExecutionRequest) (map[string]string, error) {
values := map[string]string{
"ot_executionTrackingId": req.TrackingID,
"ot_username": req.AuthenticatedUser.Username,
}
for i := range systemArgumentDefinitions {
arg := &systemArgumentDefinitions[i]
if err := ValidateArgument(arg, values[arg.Name], req.Binding.Action); err != nil {
return nil, fmt.Errorf("system argument %q failed validation: %w", arg.Name, err)
}
}
return values, nil
}
func hasBindingAndAction(req *ExecutionRequest) bool {
@ -939,36 +970,20 @@ func prepareCommand(cmd *exec.Cmd, streamer *OutputStreamer, req *ExecutionReque
}
func stepExecAfter(req *ExecutionRequest) bool {
if req.Binding.Action.ShellAfterCompleted == "" {
return true
}
ctx, cancel := newTimeoutContext(context.Background(), time.Duration(req.Binding.Action.Timeout)*time.Second, req.executor)
defer cancel()
var stdout bytes.Buffer
var stderr bytes.Buffer
args := map[string]string{
"output": req.logEntry.Output,
"exitCode": fmt.Sprintf("%v", req.logEntry.ExitCode),
"ot_executionTrackingId": req.TrackingID,
"ot_username": req.AuthenticatedUser.Username,
}
finalParsedCommand, err := tpl.ParseTemplateWithActionContext(req.Binding.Action.ShellAfterCompleted, req.Binding.Entity, args)
cmd, args, err := buildShellAfterCommand(ctx, req, &stdout, &stderr)
if err != nil {
msg := "Could not prepare shellAfterCompleted command: " + err.Error() + "\n"
req.logEntry.Output += msg
log.Warn(msg)
return fail(req, err)
}
if cmd == nil {
return true
}
cmd := wrapCommandInShell(ctx, finalParsedCommand)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
cmd.Env = buildEnv(args)
runerr := cmd.Start()
@ -998,6 +1013,43 @@ func stepExecAfter(req *ExecutionRequest) bool {
return true
}
func buildShellAfterCommand(ctx context.Context, req *ExecutionRequest, stdout, stderr *bytes.Buffer) (*exec.Cmd, map[string]string, error) {
if req.Binding.Action.ShellAfterCompleted == "" {
return nil, nil, nil
}
args, err := buildShellAfterArgs(req)
if err != nil {
return nil, nil, err
}
finalParsedCommand, err := tpl.ParseTemplateWithActionContext(req.Binding.Action.ShellAfterCompleted, req.Binding.Entity, args)
if err != nil {
msg := "Could not prepare shellAfterCompleted command: " + err.Error() + "\n"
req.logEntry.Output += msg
log.Warn(msg)
return nil, nil, nil
}
cmd := wrapCommandInShell(ctx, finalParsedCommand)
cmd.Stdout = stdout
cmd.Stderr = stderr
return cmd, args, nil
}
func buildShellAfterArgs(req *ExecutionRequest) (map[string]string, error) {
args, err := validatedSystemArgs(req)
if err != nil {
return nil, err
}
args["output"] = req.logEntry.Output
args["exitCode"] = fmt.Sprintf("%v", req.logEntry.ExitCode)
return args, nil
}
//gocyclo:ignore
func stepTrigger(req *ExecutionRequest) bool {
if req.Binding.Action.Triggers == nil {

View File

@ -1,6 +1,7 @@
package executor
import (
"strings"
"testing"
"time"
@ -37,7 +38,7 @@ func TestCreateExecutorAndExec(t *testing.T) {
e, cfg := testingExecutor()
req := ExecutionRequest{
AuthenticatedUser: &authpublic.AuthenticatedUser{Username: "Mr Tickle"},
AuthenticatedUser: &authpublic.AuthenticatedUser{Username: "MrTickle"},
Cfg: cfg,
Arguments: map[string]string{
"person": "yourself",
@ -379,7 +380,7 @@ func TestFilterToDefinedArgumentsOnly(t *testing.T) {
assert.Empty(t, req.Arguments["extra_undefined"])
}
func TestFilterToDefinedArgumentsPreservesSystemArgs(t *testing.T) {
func TestFilterToDefinedArgumentsDropsReservedPrefixArgs(t *testing.T) {
req := newExecRequest()
req.Binding.Action = &config.Action{
Title: "Filter test",
@ -393,8 +394,209 @@ func TestFilterToDefinedArgumentsPreservesSystemArgs(t *testing.T) {
filterToDefinedArgumentsOnly(req)
assert.Equal(t, "track-123", req.Arguments["ot_executionTrackingId"])
assert.Equal(t, "webhook", req.Arguments["ot_username"])
assert.Empty(t, req.Arguments["ot_executionTrackingId"])
assert.Empty(t, req.Arguments["ot_username"])
}
func TestStepParseArgsInjectsSystemArgsAfterFiltering(t *testing.T) {
req := newExecRequest()
req.TrackingID = "server-track-456"
req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice"}
req.Binding.Action = &config.Action{
Title: "Filter then inject",
Shell: "echo test",
Arguments: []config.ActionArgument{
{Name: "name", Type: "ascii"},
},
}
req.Arguments = map[string]string{
"name": "Alice",
"ot_executionTrackingId": "attacker-track",
"ot_username": "mallory",
"ot_custom": "polluted",
}
assert.True(t, stepParseArgs(req))
assert.Equal(t, "Alice", req.Arguments["name"])
assert.Equal(t, "server-track-456", req.Arguments["ot_executionTrackingId"])
assert.Equal(t, "alice", req.Arguments["ot_username"])
assert.Empty(t, req.Arguments["ot_custom"])
}
func TestStepParseArgsDropsReservedPrefixArgsFromEnvironment(t *testing.T) {
req := newExecRequest()
req.TrackingID = "server-track-456"
req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice@example.com"}
req.Binding.Action = &config.Action{
Title: "No reserved prefix pollution",
Shell: "echo test",
Arguments: []config.ActionArgument{},
}
req.Arguments = map[string]string{
"ot_custom": "polluted",
}
assert.True(t, stepParseArgs(req))
env := buildEnv(req.Arguments)
assert.False(t, containsEnvPrefix(env, "OT_CUSTOM="))
assert.True(t, containsEnvPrefix(env, "OT_USERNAME=alice@example.com"))
assert.True(t, containsEnvPrefix(env, "OT_EXECUTIONTRACKINGID=server-track-456"))
}
func TestSystemArgumentDefinitionsAreReservedAndShellSafe(t *testing.T) {
unsafeTypes := map[string]struct{}{
"email": {},
"password": {},
"raw_string_multiline": {},
"url": {},
"very_dangerous_raw_string": {},
}
seen := map[string]struct{}{}
for _, arg := range systemArgumentDefinitions {
assert.True(t, strings.HasPrefix(arg.Name, config.ReservedArgumentNamePrefix))
assert.NotEmpty(t, arg.Type)
assert.True(t, arg.RejectNull)
_, duplicate := seen[arg.Name]
assert.False(t, duplicate, "duplicate system argument definition %q", arg.Name)
seen[arg.Name] = struct{}{}
_, unsafe := unsafeTypes[arg.Type]
assert.False(t, unsafe, "system argument %q uses unsafe type %q", arg.Name, arg.Type)
}
}
func TestValidatedSystemArgsMatchesSystemArgumentDefinitions(t *testing.T) {
req := newExecRequest()
req.TrackingID = "server-track-456"
req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice@example.com"}
args, err := validatedSystemArgs(req)
assert.Nil(t, err)
assert.Len(t, args, len(systemArgumentDefinitions))
for _, arg := range systemArgumentDefinitions {
assert.Contains(t, args, arg.Name)
}
}
func TestBuildShellAfterArgsOnlyAddsExpectedNonSystemArgs(t *testing.T) {
req := newExecRequest()
req.logEntry = &InternalLogEntry{
Output: "hello",
ExitCode: 7,
}
req.TrackingID = "server-track-456"
req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice@example.com"}
req.Binding.Action = &config.Action{ShellAfterCompleted: "echo test"}
args, err := buildShellAfterArgs(req)
assert.Nil(t, err)
assert.Len(t, args, len(systemArgumentDefinitions)+2)
assert.Contains(t, args, "output")
assert.Contains(t, args, "exitCode")
for _, arg := range systemArgumentDefinitions {
assert.Contains(t, args, arg.Name)
}
}
func TestStepParseArgsAllowsEmailUsernameSystemArg(t *testing.T) {
req := newExecRequest()
req.logEntry = &InternalLogEntry{}
req.TrackingID = "server-track-456"
req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice@example.com"}
req.Binding.Action = &config.Action{
Title: "Email username",
Shell: "echo test",
Arguments: []config.ActionArgument{},
}
assert.True(t, stepParseArgs(req))
assert.Equal(t, "alice@example.com", req.Arguments["ot_username"])
}
func TestStepParseArgsFailsWhenUsernameSystemArgIsInvalid(t *testing.T) {
req := newExecRequest()
req.logEntry = &InternalLogEntry{}
req.TrackingID = "server-track-456"
req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice;id"}
req.Binding.Action = &config.Action{
Title: "Invalid system arg",
Shell: "echo test",
Arguments: []config.ActionArgument{},
}
assert.False(t, stepParseArgs(req))
assert.Contains(t, req.logEntry.Output, `system argument "ot_username" failed validation`)
assert.Empty(t, req.Arguments["ot_username"])
}
func TestStepParseArgsFailsWhenTrackingIDSystemArgIsInvalid(t *testing.T) {
req := newExecRequest()
req.logEntry = &InternalLogEntry{}
req.TrackingID = "track/../../bad"
req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice"}
req.Binding.Action = &config.Action{
Title: "Invalid tracking ID",
Shell: "echo test",
Arguments: []config.ActionArgument{},
}
assert.False(t, stepParseArgs(req))
assert.Contains(t, req.logEntry.Output, `system argument "ot_executionTrackingId" failed validation`)
assert.Empty(t, req.Arguments["ot_executionTrackingId"])
}
func TestBuildShellAfterArgsUsesValidatedSystemArgs(t *testing.T) {
req := newExecRequest()
req.logEntry = &InternalLogEntry{
Output: "hello",
ExitCode: 7,
}
req.TrackingID = "server-track-456"
req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice@example.com"}
req.Binding.Action = &config.Action{
Title: "Shell after",
ShellAfterCompleted: "echo test",
}
args, err := buildShellAfterArgs(req)
assert.Nil(t, err)
assert.Equal(t, "alice@example.com", args["ot_username"])
assert.Equal(t, "server-track-456", args["ot_executionTrackingId"])
assert.Equal(t, "hello", args["output"])
assert.Equal(t, "7", args["exitCode"])
}
func TestBuildShellAfterArgsFailsWhenSystemArgIsInvalid(t *testing.T) {
req := newExecRequest()
req.logEntry = &InternalLogEntry{}
req.TrackingID = "server-track-456"
req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice;id"}
req.Binding.Action = &config.Action{
Title: "Shell after invalid username",
ShellAfterCompleted: "echo test",
}
args, err := buildShellAfterArgs(req)
assert.Nil(t, args)
assert.NotNil(t, err)
assert.Contains(t, err.Error(), `system argument "ot_username" failed validation`)
}
func containsEnvPrefix(env []string, prefix string) bool {
for _, item := range env {
if strings.HasPrefix(item, prefix) {
return true
}
}
return false
}
func TestTriggerExecutesTriggeredAction(t *testing.T) {

View File

@ -13,6 +13,13 @@ import (
func WatchFilesInDirectory(cfg *config.Config, ex *executor.Executor) {
for _, action := range cfg.Actions {
for _, dirname := range action.ExecOnFileCreatedInDir {
go func(act *config.Action, dir string) {
filehelper.WatchDirectoryCreate(dir, func(filename string) {
scheduleExec(act, cfg, ex, filename)
})
}(action, dirname)
}
for _, dirname := range action.ExecOnFileChangedInDir {
// Pass values into anonymous function because of this issue
// https://github.com/OliveTin/OliveTin/issues/503
@ -22,12 +29,6 @@ func WatchFilesInDirectory(cfg *config.Config, ex *executor.Executor) {
scheduleExec(act, cfg, ex, filename)
})
}(action, dirname)
go func(act *config.Action, dir string) {
filehelper.WatchDirectoryCreate(dir, func(filename string) {
scheduleExec(act, cfg, ex, filename)
})
}(action, dirname)
}
}
}