Share the target filter, fix cd-status branch, record deploy status
- enabled_targets/names_for: one implementation of 'a host acts only on its own targets'; --host s5 and --host s5.fisher.hu now select the same targets - shell_value: JSON booleans reach shell consumers as true/false - cd-status: capture before eval, so the unconfigured-host branch is reachable - cd-deploy writes a .status record; cd-status reads it instead of grepping the log's prose - cd-render-hooks --list-urls replaces two inline JSON readers
This commit is contained in:
+11
-1
@@ -107,6 +107,7 @@ eval "$resolved"
|
|||||||
|
|
||||||
mkdir -p "$STATE_DIR"
|
mkdir -p "$STATE_DIR"
|
||||||
readonly LOG_FILE="${STATE_DIR}/${CD_NAME}.log"
|
readonly LOG_FILE="${STATE_DIR}/${CD_NAME}.log"
|
||||||
|
readonly STATUS_FILE="${STATE_DIR}/${CD_NAME}.status"
|
||||||
exec > >(stdbuf -oL tee -a "$LOG_FILE") 2>&1
|
exec > >(stdbuf -oL tee -a "$LOG_FILE") 2>&1
|
||||||
|
|
||||||
log "=============================================================="
|
log "=============================================================="
|
||||||
@@ -129,6 +130,15 @@ notify() {
|
|||||||
fail) icon="❌" ;;
|
fail) icon="❌" ;;
|
||||||
*) icon="ℹ️" ;;
|
*) icon="ℹ️" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
# Every terminal outcome notifies, so this is also where the machine-readable
|
||||||
|
# record of that outcome belongs. cd-status reads this file rather than
|
||||||
|
# grepping the log's prose, which quietly reported "no completed deploy"
|
||||||
|
# for a healthy target whenever a log line was reworded.
|
||||||
|
printf '%s\t%s\t%s\t%s\n' \
|
||||||
|
"$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$status" "${SHA:0:12}" "$message" \
|
||||||
|
> "$STATUS_FILE"
|
||||||
|
|
||||||
topic="${REPO//\//-}"
|
topic="${REPO//\//-}"
|
||||||
curl -fsS --max-time 10 \
|
curl -fsS --max-time 10 \
|
||||||
-H "Title: ${CD_NAME} deploy" \
|
-H "Title: ${CD_NAME} deploy" \
|
||||||
@@ -299,7 +309,7 @@ fi
|
|||||||
|
|
||||||
# ---------------------------------------------------------------- rollback ---
|
# ---------------------------------------------------------------- rollback ---
|
||||||
|
|
||||||
if [[ "$CD_ROLLBACK_ON_FAILURE" != "True" && "$CD_ROLLBACK_ON_FAILURE" != "true" ]]; then
|
if [[ "$CD_ROLLBACK_ON_FAILURE" != "true" ]]; then
|
||||||
notify fail "health check failed for ${SHA:0:12}; rollback disabled, stack left on the new image"
|
notify fail "health check failed for ${SHA:0:12}; rollback disabled, stack left on the new image"
|
||||||
die "health check failed and rollback is disabled for this target"
|
die "health check failed and rollback is disabled for this target"
|
||||||
fi
|
fi
|
||||||
|
|||||||
+33
-7
@@ -16,6 +16,7 @@ would accept a deploy request from anyone who can reach the port.
|
|||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
cd-render-hooks [--host HOST] [--output PATH] [--redact]
|
cd-render-hooks [--host HOST] [--output PATH] [--redact]
|
||||||
|
cd-render-hooks --list-urls [--output PATH] [--base-url URL]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -52,7 +53,8 @@ def _load_cd_target():
|
|||||||
_cd_target = _load_cd_target()
|
_cd_target = _load_cd_target()
|
||||||
load = _cd_target.load
|
load = _cd_target.load
|
||||||
pick_host = _cd_target.pick_host
|
pick_host = _cd_target.pick_host
|
||||||
host_matches = _cd_target.host_matches
|
names_for = _cd_target.names_for
|
||||||
|
enabled_targets = _cd_target.enabled_targets
|
||||||
|
|
||||||
DEFAULT_CONFIG_DIR = Path(
|
DEFAULT_CONFIG_DIR = Path(
|
||||||
os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")
|
os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")
|
||||||
@@ -167,6 +169,22 @@ def build_hook(repo: str, branches: list[str], secret: str) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def list_urls(path: Path, base_url: str) -> int:
|
||||||
|
"""Print one endpoint URL per hook in an already-rendered hooks file.
|
||||||
|
|
||||||
|
Reads the rendered file rather than the manifest so that what is printed is
|
||||||
|
what the daemon is actually serving. Both the installer and cd-status use
|
||||||
|
this instead of reaching into the file's schema themselves.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
hooks = json.loads(path.read_text())
|
||||||
|
except FileNotFoundError:
|
||||||
|
sys.exit(f"cd-render-hooks: no rendered hooks file at {path}")
|
||||||
|
for hook in hooks:
|
||||||
|
print(f"{base_url}/hooks/{hook['id']}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||||
parser.add_argument("--host")
|
parser.add_argument("--host")
|
||||||
@@ -177,19 +195,27 @@ def main() -> int:
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="print to stdout with secrets masked, and write nothing",
|
help="print to stdout with secrets masked, and write nothing",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--list-urls",
|
||||||
|
action="store_true",
|
||||||
|
help="print the endpoint URL of each hook in an existing hooks file",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--base-url",
|
||||||
|
default="",
|
||||||
|
help="prefix for --list-urls, e.g. http://10.255.255.1:21600",
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.list_urls:
|
||||||
|
return list_urls(args.output, args.base_url.rstrip("/"))
|
||||||
|
|
||||||
manifest = load()
|
manifest = load()
|
||||||
host = pick_host(manifest, args.host)
|
host = pick_host(manifest, args.host)
|
||||||
names = {host.lower(), host.split(".", 1)[0].lower()}
|
|
||||||
|
|
||||||
# repo -> branches, preserving manifest order for a stable diff.
|
# repo -> branches, preserving manifest order for a stable diff.
|
||||||
by_repo: dict[str, list[str]] = {}
|
by_repo: dict[str, list[str]] = {}
|
||||||
for target in manifest.get("targets", []):
|
for target in enabled_targets(manifest, names_for(host)):
|
||||||
if not target.get("enabled", True):
|
|
||||||
continue
|
|
||||||
if not host_matches(target["host"], names):
|
|
||||||
continue
|
|
||||||
by_repo.setdefault(target["repo"], []).append(target["branch"])
|
by_repo.setdefault(target["repo"], []).append(target["branch"])
|
||||||
|
|
||||||
if not by_repo:
|
if not by_repo:
|
||||||
|
|||||||
+13
-11
@@ -17,7 +17,10 @@ readonly UNIT_NAME="cd-webhook.service"
|
|||||||
bold() { printf '\n\033[1m%s\033[0m\n' "$*"; }
|
bold() { printf '\n\033[1m%s\033[0m\n' "$*"; }
|
||||||
|
|
||||||
bold "Host"
|
bold "Host"
|
||||||
if eval "$("${REPO_ROOT}/bin/cd-target" host-config 2>/dev/null)"; then
|
# Capture first: `eval ""` succeeds, so evaluating the failed call inline made
|
||||||
|
# the branch below unreachable and tripped `set -u` instead.
|
||||||
|
if host_config="$("${REPO_ROOT}/bin/cd-target" host-config 2>/dev/null)"; then
|
||||||
|
eval "$host_config"
|
||||||
printf ' %s, receiver on %s:%s\n' "$CD_HOST" "$CD_BIND" "$CD_PORT"
|
printf ' %s, receiver on %s:%s\n' "$CD_HOST" "$CD_BIND" "$CD_PORT"
|
||||||
else
|
else
|
||||||
printf ' %s is not configured in targets.json\n' "$(hostname)"
|
printf ' %s is not configured in targets.json\n' "$(hostname)"
|
||||||
@@ -41,12 +44,8 @@ fi
|
|||||||
|
|
||||||
if [[ -f "${CONFIG_DIR}/hooks.json" ]]; then
|
if [[ -f "${CONFIG_DIR}/hooks.json" ]]; then
|
||||||
bold "Hook endpoints"
|
bold "Hook endpoints"
|
||||||
python3 - "${CONFIG_DIR}/hooks.json" <<'PY'
|
"${REPO_ROOT}/bin/cd-render-hooks" --list-urls \
|
||||||
import json, sys
|
--output "${CONFIG_DIR}/hooks.json" | sed 's/^/ /'
|
||||||
with open(sys.argv[1]) as fh:
|
|
||||||
for hook in json.load(fh):
|
|
||||||
print(f" /hooks/{hook['id']}")
|
|
||||||
PY
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
bold "Targets on this host"
|
bold "Targets on this host"
|
||||||
@@ -55,10 +54,13 @@ if [[ ${#targets[@]} -eq 0 ]]; then
|
|||||||
printf ' none\n'
|
printf ' none\n'
|
||||||
else
|
else
|
||||||
for target in "${targets[@]}"; do
|
for target in "${targets[@]}"; do
|
||||||
log="${STATE_DIR}/${target}.log"
|
# cd-deploy writes this on every terminal outcome. Reading a record
|
||||||
if [[ -f "$log" ]]; then
|
# beats grepping the log for phrases the log is free to reword.
|
||||||
last="$(grep -E '=== deploy succeeded|ERROR|WARN health' "$log" | tail -n1 || true)"
|
status_file="${STATE_DIR}/${target}.status"
|
||||||
printf ' %-22s %s\n' "$target" "${last:-no completed deploy recorded}"
|
if [[ -f "$status_file" ]]; then
|
||||||
|
IFS=$'\t' read -r when result sha message < "$status_file" || true
|
||||||
|
printf ' %-22s %s %-4s %s %s\n' \
|
||||||
|
"$target" "$when" "$result" "$sha" "$message"
|
||||||
else
|
else
|
||||||
printf ' %-22s %s\n' "$target" "never deployed from this host"
|
printf ' %-22s %s\n' "$target" "never deployed from this host"
|
||||||
fi
|
fi
|
||||||
|
|||||||
+50
-31
@@ -15,9 +15,6 @@ Subcommands:
|
|||||||
list [--host HOST]
|
list [--host HOST]
|
||||||
Print the name of every enabled target owned by HOST, one per line.
|
Print the name of every enabled target owned by HOST, one per line.
|
||||||
|
|
||||||
repos [--host HOST]
|
|
||||||
Print the distinct repositories HOST deploys, one per line.
|
|
||||||
|
|
||||||
host-config [--host HOST]
|
host-config [--host HOST]
|
||||||
Print shell-quoted BIND/PORT for HOST.
|
Print shell-quoted BIND/PORT for HOST.
|
||||||
|
|
||||||
@@ -30,11 +27,13 @@ HOST defaults to the local machine in every subcommand.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import functools
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import shlex
|
import shlex
|
||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
|
from collections.abc import Iterator
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||||
@@ -47,13 +46,17 @@ MANIFEST = Path(os.environ.get("CD_TARGETS_FILE", REPO_ROOT / "targets.json"))
|
|||||||
NO_MATCH = 3
|
NO_MATCH = 3
|
||||||
|
|
||||||
|
|
||||||
def local_hostnames() -> set[str]:
|
@functools.cache
|
||||||
|
def local_hostnames() -> frozenset[str]:
|
||||||
"""Every name this machine plausibly answers to.
|
"""Every name this machine plausibly answers to.
|
||||||
|
|
||||||
Hosts in this fleet are referred to by FQDN in the manifest but report a
|
Hosts in this fleet are referred to by FQDN in the manifest but report a
|
||||||
short name from `hostname`, so both forms -- and the leading label of any
|
short name from `hostname`, so both forms -- and the leading label of any
|
||||||
FQDN -- have to count as a match. Getting this wrong is how the old
|
FQDN -- have to count as a match. Getting this wrong is how the old
|
||||||
per-project scripts ended up with a dead `hostname != s2` guard.
|
per-project scripts ended up with a dead `hostname != s2` guard.
|
||||||
|
|
||||||
|
Cached: `getfqdn` can block on a slow resolver, and this sits on the path of
|
||||||
|
every webhook delivery. The answer cannot change within one process.
|
||||||
"""
|
"""
|
||||||
names = {socket.gethostname()}
|
names = {socket.gethostname()}
|
||||||
try:
|
try:
|
||||||
@@ -63,14 +66,39 @@ def local_hostnames() -> set[str]:
|
|||||||
for name in list(names):
|
for name in list(names):
|
||||||
if "." in name:
|
if "." in name:
|
||||||
names.add(name.split(".", 1)[0])
|
names.add(name.split(".", 1)[0])
|
||||||
return {n.lower() for n in names if n}
|
return frozenset(n.lower() for n in names if n)
|
||||||
|
|
||||||
|
|
||||||
def host_matches(target_host: str, names: set[str]) -> bool:
|
def host_matches(target_host: str, names: frozenset[str]) -> bool:
|
||||||
target_host = target_host.lower()
|
target_host = target_host.lower()
|
||||||
return target_host in names or target_host.split(".", 1)[0] in names
|
return target_host in names or target_host.split(".", 1)[0] in names
|
||||||
|
|
||||||
|
|
||||||
|
def names_for(explicit: str | None) -> frozenset[str]:
|
||||||
|
"""The names a target's `host` field is matched against.
|
||||||
|
|
||||||
|
Defaults to this machine's own names. An explicit --host is expanded to both
|
||||||
|
its FQDN and short forms, the same way `local_hostnames` expands the local
|
||||||
|
ones, so `--host s5` and `--host s5.fisher.hu` select the same targets.
|
||||||
|
"""
|
||||||
|
if not explicit:
|
||||||
|
return local_hostnames()
|
||||||
|
name = explicit.lower()
|
||||||
|
return frozenset({name, name.split(".", 1)[0]})
|
||||||
|
|
||||||
|
|
||||||
|
def enabled_targets(manifest: dict, names: frozenset[str]) -> Iterator[dict]:
|
||||||
|
"""Every enabled target owned by `names`, in manifest order.
|
||||||
|
|
||||||
|
The single implementation of "a host acts only on its own targets" -- the
|
||||||
|
rule the whole design rests on. cd-render-hooks imports this rather than
|
||||||
|
repeating the filter, so a new filtering dimension is one edit, not four.
|
||||||
|
"""
|
||||||
|
for target in manifest.get("targets", []):
|
||||||
|
if target.get("enabled", True) and host_matches(target["host"], names):
|
||||||
|
yield target
|
||||||
|
|
||||||
|
|
||||||
def load() -> dict:
|
def load() -> dict:
|
||||||
try:
|
try:
|
||||||
return json.loads(MANIFEST.read_text())
|
return json.loads(MANIFEST.read_text())
|
||||||
@@ -93,23 +121,29 @@ def pick_host(manifest: dict, explicit: str | None) -> str:
|
|||||||
return socket.gethostname()
|
return socket.gethostname()
|
||||||
|
|
||||||
|
|
||||||
|
def shell_value(value: object) -> str:
|
||||||
|
"""Render a manifest value the way a shell consumer expects it.
|
||||||
|
|
||||||
|
JSON booleans would otherwise arrive as Python's `True`/`False`, forcing
|
||||||
|
every consumer to test for both spellings of every boolean field.
|
||||||
|
"""
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return "true" if value else "false"
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
def emit(pairs: dict[str, object]) -> None:
|
def emit(pairs: dict[str, object]) -> None:
|
||||||
for key, value in pairs.items():
|
for key, value in pairs.items():
|
||||||
print(f"{key}={shlex.quote(str(value))}")
|
print(f"{key}={shlex.quote(shell_value(value))}")
|
||||||
|
|
||||||
|
|
||||||
def cmd_resolve(args, manifest: dict) -> int:
|
def cmd_resolve(args, manifest: dict) -> int:
|
||||||
host = pick_host(manifest, args.host)
|
host = pick_host(manifest, args.host)
|
||||||
names = local_hostnames() if not args.host else {args.host.lower()}
|
|
||||||
defaults = manifest.get("defaults", {})
|
defaults = manifest.get("defaults", {})
|
||||||
|
|
||||||
for target in manifest.get("targets", []):
|
for target in enabled_targets(manifest, names_for(args.host)):
|
||||||
if not target.get("enabled", True):
|
|
||||||
continue
|
|
||||||
if target["repo"] != args.repo or target["branch"] != args.branch:
|
if target["repo"] != args.repo or target["branch"] != args.branch:
|
||||||
continue
|
continue
|
||||||
if not host_matches(target["host"], names):
|
|
||||||
continue
|
|
||||||
|
|
||||||
merged = {**defaults, **target}
|
merged = {**defaults, **target}
|
||||||
merged["resolved_host"] = host
|
merged["resolved_host"] = host
|
||||||
@@ -120,22 +154,8 @@ def cmd_resolve(args, manifest: dict) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def cmd_list(args, manifest: dict) -> int:
|
def cmd_list(args, manifest: dict) -> int:
|
||||||
names = local_hostnames() if not args.host else {args.host.lower()}
|
for target in enabled_targets(manifest, names_for(args.host)):
|
||||||
for target in manifest.get("targets", []):
|
print(target["name"])
|
||||||
if target.get("enabled", True) and host_matches(target["host"], names):
|
|
||||||
print(target["name"])
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_repos(args, manifest: dict) -> int:
|
|
||||||
names = local_hostnames() if not args.host else {args.host.lower()}
|
|
||||||
seen: list[str] = []
|
|
||||||
for target in manifest.get("targets", []):
|
|
||||||
if not target.get("enabled", True):
|
|
||||||
continue
|
|
||||||
if host_matches(target["host"], names) and target["repo"] not in seen:
|
|
||||||
seen.append(target["repo"])
|
|
||||||
print("\n".join(seen))
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@@ -166,8 +186,7 @@ def main() -> int:
|
|||||||
p.add_argument("--host")
|
p.add_argument("--host")
|
||||||
p.set_defaults(func=cmd_resolve)
|
p.set_defaults(func=cmd_resolve)
|
||||||
|
|
||||||
for name, func in (("list", cmd_list), ("repos", cmd_repos),
|
for name, func in (("list", cmd_list), ("host-config", cmd_host_config)):
|
||||||
("host-config", cmd_host_config)):
|
|
||||||
p = sub.add_parser(name)
|
p = sub.add_parser(name)
|
||||||
p.add_argument("--host")
|
p.add_argument("--host")
|
||||||
p.set_defaults(func=func)
|
p.set_defaults(func=func)
|
||||||
|
|||||||
+2
-2
@@ -21,7 +21,7 @@ deploy ended.
|
|||||||
| This repository | `~/S/my-cd-webhook` |
|
| This repository | `~/S/my-cd-webhook` |
|
||||||
| Secrets (0600) | `~/.config/cd-webhook/secrets.env` |
|
| Secrets (0600) | `~/.config/cd-webhook/secrets.env` |
|
||||||
| Rendered hooks (0600, generated) | `~/.config/cd-webhook/hooks.json` |
|
| Rendered hooks (0600, generated) | `~/.config/cd-webhook/hooks.json` |
|
||||||
| Deploy logs and locks | `~/.local/state/cd-webhook/<target>.log` |
|
| Deploy logs, locks, last-deploy records | `~/.local/state/cd-webhook/<target>.{log,lock,status}` |
|
||||||
| systemd unit | `~/.config/systemd/user/cd-webhook.service` |
|
| systemd unit | `~/.config/systemd/user/cd-webhook.service` |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -74,7 +74,7 @@ missed deploy, not a wrong one.
|
|||||||
## Adding a host
|
## Adding a host
|
||||||
|
|
||||||
1. Add the host to `"hosts"` in `targets.json` with its WireGuard address and
|
1. Add the host to `"hosts"` in `targets.json` with its WireGuard address and
|
||||||
port `21600`.
|
the same port the other hosts there use.
|
||||||
2. Add its targets.
|
2. Add its targets.
|
||||||
3. Commit and push this repository.
|
3. Commit and push this repository.
|
||||||
4. On the host: clone, `./install/install.sh`, fill in `secrets.env`, re-run.
|
4. On the host: clone, `./install/install.sh`, fill in `secrets.env`, re-run.
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ Short notes on things that were not obvious. Prune stale ones.
|
|||||||
`importlib.machinery.SourceFileLoader`. Importing `importlib.util` alone is
|
`importlib.machinery.SourceFileLoader`. Importing `importlib.util` alone is
|
||||||
not enough — `importlib.machinery` needs its own import.
|
not enough — `importlib.machinery` needs its own import.
|
||||||
|
|
||||||
|
- **`if eval "$(cmd)"` tests the wrong thing.** `eval` reports the status of the
|
||||||
|
string it evaluates, and an empty string is success — so a failing `cmd` makes
|
||||||
|
the `if` take the *true* branch with nothing assigned, and `set -u` then aborts
|
||||||
|
on the first variable the branch reads. Capture first
|
||||||
|
(`if out="$(cmd)"; then eval "$out"`). This had made `cd-status`'s "host is not
|
||||||
|
configured" message unreachable.
|
||||||
|
|
||||||
- **`docker/metadata-action`'s `type=sha,format=short` produces 7 characters.**
|
- **`docker/metadata-action`'s `type=sha,format=short` produces 7 characters.**
|
||||||
That is why `image_tag_template` uses `{short7}`. domaindingo's `build.yml`
|
That is why `image_tag_template` uses `{short7}`. domaindingo's `build.yml`
|
||||||
prefixes it with the branch, giving `test-sha-abc1234` / `prod-sha-abc1234`.
|
prefixes it with the branch, giving `test-sha-abc1234` / `prod-sha-abc1234`.
|
||||||
|
|||||||
+3
-9
@@ -119,15 +119,9 @@ printf ' %-28s %s\n' "Trigger On" "Push Events"
|
|||||||
printf ' %-28s %s\n' "Secret" "the matching value from ${SECRETS_FILE}"
|
printf ' %-28s %s\n' "Secret" "the matching value from ${SECRETS_FILE}"
|
||||||
echo
|
echo
|
||||||
|
|
||||||
"${REPO_ROOT}/bin/cd-render-hooks" --redact >/dev/null 2>&1 || true
|
"${REPO_ROOT}/bin/cd-render-hooks" --list-urls \
|
||||||
python3 - "$HOOKS_FILE" "$CD_BIND" "$CD_PORT" <<'PY'
|
--output "$HOOKS_FILE" --base-url "http://${CD_BIND}:${CD_PORT}" \
|
||||||
import json, sys
|
| sed 's/^/ /'
|
||||||
hooks_file, bind, port = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
||||||
with open(hooks_file) as fh:
|
|
||||||
hooks = json.load(fh)
|
|
||||||
for hook in hooks:
|
|
||||||
print(f" http://{bind}:{port}/hooks/{hook['id']}")
|
|
||||||
PY
|
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo " Logs: journalctl --user -u ${UNIT_NAME} -f"
|
echo " Logs: journalctl --user -u ${UNIT_NAME} -f"
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
],
|
],
|
||||||
|
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"registry": "gitea.fisher.hu",
|
|
||||||
"ntfy_base_url": "https://ntfy.fisher.hu",
|
"ntfy_base_url": "https://ntfy.fisher.hu",
|
||||||
|
|
||||||
"image_wait_seconds": 900,
|
"image_wait_seconds": 900,
|
||||||
|
|||||||
Reference in New Issue
Block a user