Compare commits
13 Commits
v0.1
..
588a8af231
| Author | SHA1 | Date | |
|---|---|---|---|
| 588a8af231 | |||
| 8ffb4659f7 | |||
| cc3cac5ee2 | |||
| cfa4fc005b | |||
| f68297d231 | |||
| 62680a0321 | |||
| dd81fcff6f | |||
| 3799c3df33 | |||
| 985837bc2c | |||
| 5216703423 | |||
| 23d2e1da33 | |||
| 55cb32ed6d | |||
| d06bed555c |
@@ -1,10 +1,9 @@
|
|||||||
name: Build and Push Docker Image
|
name: Build and Push Docker Image
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
push:
|
||||||
|
branches:
|
||||||
permissions:
|
- "**"
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -14,6 +13,20 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# - name: Run Trivy vulnerability scanner in repo mode
|
||||||
|
# uses: aquasecurity/trivy-action@v0.36.0
|
||||||
|
# with:
|
||||||
|
# scan-type: 'fs'
|
||||||
|
# ignore-unfixed: true
|
||||||
|
# format: 'sarif'
|
||||||
|
# output: 'trivy-results.sarif'
|
||||||
|
# severity: 'CRITICAL'
|
||||||
|
|
||||||
|
# - name: Upload Trivy scan results to GitHub Security tab
|
||||||
|
# uses: github/codeql-action/upload-sarif@v4
|
||||||
|
# with:
|
||||||
|
# sarif_file: 'trivy-results.sarif'
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
@@ -36,8 +49,7 @@ jobs:
|
|||||||
TAG="test"
|
TAG="test"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "tag=$TAG" >> "$GITEA_OUTPUT"
|
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||||
echo "branch=$BRANCH" >> "$GITEA_OUTPUT"
|
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
@@ -54,7 +66,7 @@ jobs:
|
|||||||
-d "{
|
-d "{
|
||||||
\"tags\": \"all\",
|
\"tags\": \"all\",
|
||||||
\"title\": \"Gitea Build Succeeded\",
|
\"title\": \"Gitea Build Succeeded\",
|
||||||
\"body\": \"Repo: ${{ gitea.repository }}\\nBranch: ${{ steps.tag.outputs.branch }}\\nImage tag built successfully\"
|
\"body\": \"Repo: ${{ gitea.repository }}\\nBranch: ${{ gitea.ref_name }}\\nImage tag built successfully\"
|
||||||
}" \
|
}" \
|
||||||
${{ secrets.APPRISE_URL }}
|
${{ secrets.APPRISE_URL }}
|
||||||
|
|
||||||
@@ -66,6 +78,6 @@ jobs:
|
|||||||
-d "{
|
-d "{
|
||||||
\"tags\": \"all\",
|
\"tags\": \"all\",
|
||||||
\"title\": \"Gitea Build Failed\",
|
\"title\": \"Gitea Build Failed\",
|
||||||
\"body\": \"Repo: ${{ gitea.repository }}\\nBranch: ${{ steps.tag.outputs.branch }}\\nCheck logs in Gitea\"
|
\"body\": \"Repo: ${{ gitea.repository }}\\nBranch: ${{ gitea.ref_name }}\\nCheck logs in Gitea\"
|
||||||
}" \
|
}" \
|
||||||
${{ secrets.APPRISE_URL }}
|
${{ secrets.APPRISE_URL }}
|
||||||
@@ -1,400 +0,0 @@
|
|||||||
name: Security
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- "**"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
issues: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
security:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Prepare security reports
|
|
||||||
run: |
|
|
||||||
mkdir -p security-results
|
|
||||||
|
|
||||||
- name: Compute image tag
|
|
||||||
id: tag
|
|
||||||
run: |
|
|
||||||
BRANCH="${{ gitea.ref_name }}"
|
|
||||||
|
|
||||||
if [ "$BRANCH" = "main" ]; then
|
|
||||||
TAG="latest"
|
|
||||||
elif [[ "$BRANCH" == v* ]]; then
|
|
||||||
TAG="$BRANCH"
|
|
||||||
else
|
|
||||||
TAG="test"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "tag=$TAG" >> "$GITEA_OUTPUT"
|
|
||||||
|
|
||||||
###########################################################
|
|
||||||
# GITLEAKS
|
|
||||||
###########################################################
|
|
||||||
|
|
||||||
- name: Gitleaks
|
|
||||||
run: |
|
|
||||||
set +e
|
|
||||||
cid="$(docker create \
|
|
||||||
ghcr.io/gitleaks/gitleaks:latest \
|
|
||||||
detect \
|
|
||||||
--source /repo \
|
|
||||||
--report-format json \
|
|
||||||
--report-path /repo/security-results/gitleaks.json \
|
|
||||||
--exit-code 1)"
|
|
||||||
docker cp . "$cid:/repo"
|
|
||||||
docker start -a "$cid"
|
|
||||||
status="$?"
|
|
||||||
docker cp "$cid:/repo/security-results/gitleaks.json" security-results/gitleaks.json || true
|
|
||||||
docker rm -f "$cid" >/dev/null 2>&1 || true
|
|
||||||
echo "$status" > security-results/gitleaks.exit
|
|
||||||
|
|
||||||
###########################################################
|
|
||||||
# SEMGREP
|
|
||||||
###########################################################
|
|
||||||
|
|
||||||
- name: Semgrep
|
|
||||||
run: |
|
|
||||||
set +e
|
|
||||||
cid="$(docker create \
|
|
||||||
--workdir /src \
|
|
||||||
semgrep/semgrep \
|
|
||||||
semgrep scan \
|
|
||||||
--config auto \
|
|
||||||
--json \
|
|
||||||
--output /src/security-results/semgrep.json \
|
|
||||||
--error \
|
|
||||||
/src)"
|
|
||||||
docker cp . "$cid:/src"
|
|
||||||
docker start -a "$cid"
|
|
||||||
status="$?"
|
|
||||||
docker cp "$cid:/src/security-results/semgrep.json" security-results/semgrep.json || true
|
|
||||||
docker rm -f "$cid" >/dev/null 2>&1 || true
|
|
||||||
echo "$status" > security-results/semgrep.exit
|
|
||||||
|
|
||||||
###########################################################
|
|
||||||
# TRIVY FS
|
|
||||||
###########################################################
|
|
||||||
|
|
||||||
- name: Trivy Filesystem
|
|
||||||
run: |
|
|
||||||
set +e
|
|
||||||
cid="$(docker create \
|
|
||||||
aquasec/trivy:latest \
|
|
||||||
fs \
|
|
||||||
--scanners vuln,secret,misconfig \
|
|
||||||
--severity HIGH,CRITICAL \
|
|
||||||
--format json \
|
|
||||||
--output /workspace/security-results/trivy-fs.json \
|
|
||||||
--exit-code 1 \
|
|
||||||
/workspace)"
|
|
||||||
docker cp . "$cid:/workspace"
|
|
||||||
docker start -a "$cid"
|
|
||||||
status="$?"
|
|
||||||
docker cp "$cid:/workspace/security-results/trivy-fs.json" security-results/trivy-fs.json || true
|
|
||||||
docker rm -f "$cid" >/dev/null 2>&1 || true
|
|
||||||
echo "$status" > security-results/trivy-fs.exit
|
|
||||||
|
|
||||||
###########################################################
|
|
||||||
# DOCKER IMAGE BUILD
|
|
||||||
###########################################################
|
|
||||||
|
|
||||||
- name: Build Image for scan
|
|
||||||
run: |
|
|
||||||
set +e
|
|
||||||
docker build \
|
|
||||||
-t app:${{ gitea.sha }} \
|
|
||||||
-t blinkfink182/donetick-notifier:${{ steps.tag.outputs.tag }} \
|
|
||||||
.
|
|
||||||
echo "$?" > security-results/docker-build.exit
|
|
||||||
|
|
||||||
###########################################################
|
|
||||||
# TRIVY IMAGE
|
|
||||||
###########################################################
|
|
||||||
|
|
||||||
- name: Trivy Image Scan
|
|
||||||
run: |
|
|
||||||
set +e
|
|
||||||
if [ "$(cat security-results/docker-build.exit)" != "0" ]; then
|
|
||||||
echo "Skipping image scan because the image build failed."
|
|
||||||
echo "1" > security-results/trivy-image.exit
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
cid="$(docker create \
|
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
||||||
aquasec/trivy:latest \
|
|
||||||
image \
|
|
||||||
--severity HIGH,CRITICAL \
|
|
||||||
--format json \
|
|
||||||
--output /tmp/trivy-image.json \
|
|
||||||
--exit-code 1 \
|
|
||||||
app:${{ gitea.sha }})"
|
|
||||||
docker start -a "$cid"
|
|
||||||
status="$?"
|
|
||||||
docker cp "$cid:/tmp/trivy-image.json" security-results/trivy-image.json || true
|
|
||||||
docker rm -f "$cid" >/dev/null 2>&1 || true
|
|
||||||
echo "$status" > security-results/trivy-image.exit
|
|
||||||
|
|
||||||
- name: Create Gitea issues for security findings
|
|
||||||
if: always()
|
|
||||||
env:
|
|
||||||
GITEA_API_URL: ${{ gitea.api_url }}
|
|
||||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
|
||||||
GITEA_REF_NAME: ${{ gitea.ref_name }}
|
|
||||||
GITEA_RUN_ID: ${{ gitea.run_id }}
|
|
||||||
GITEA_SERVER_URL: ${{ gitea.server_url }}
|
|
||||||
GITEA_SHA: ${{ gitea.sha }}
|
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
|
||||||
run: |
|
|
||||||
python3 <<'PY'
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import urllib.parse
|
|
||||||
import urllib.request
|
|
||||||
|
|
||||||
token = os.environ.get("GITEA_TOKEN", "")
|
|
||||||
api_url = os.environ["GITEA_API_URL"].rstrip("/")
|
|
||||||
repo = os.environ["GITEA_REPOSITORY"]
|
|
||||||
ref_name = os.environ.get("GITEA_REF_NAME", "")
|
|
||||||
sha = os.environ.get("GITEA_SHA", "")
|
|
||||||
run_id = os.environ.get("GITEA_RUN_ID", "")
|
|
||||||
server_url = os.environ.get("GITEA_SERVER_URL", "").rstrip("/")
|
|
||||||
|
|
||||||
if not token:
|
|
||||||
print("GITEA_TOKEN is unavailable; skipping issue creation.")
|
|
||||||
raise SystemExit(0)
|
|
||||||
|
|
||||||
owner, name = repo.split("/", 1)
|
|
||||||
issues_url = f"{api_url}/repos/{urllib.parse.quote(owner)}/{urllib.parse.quote(name)}/issues"
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"token {token}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
def load_json(path, fallback):
|
|
||||||
try:
|
|
||||||
with open(path, "r", encoding="utf-8") as handle:
|
|
||||||
return json.load(handle)
|
|
||||||
except FileNotFoundError:
|
|
||||||
return fallback
|
|
||||||
except json.JSONDecodeError as exc:
|
|
||||||
print(f"Could not parse {path}: {exc}")
|
|
||||||
return fallback
|
|
||||||
|
|
||||||
def trim(value, limit):
|
|
||||||
value = str(value or "unknown")
|
|
||||||
return value if len(value) <= limit else value[: limit - 3] + "..."
|
|
||||||
|
|
||||||
def find_existing(title):
|
|
||||||
query = urllib.parse.urlencode({"state": "open", "type": "issues", "q": title})
|
|
||||||
request = urllib.request.Request(f"{issues_url}?{query}", headers=headers)
|
|
||||||
with urllib.request.urlopen(request, timeout=30) as response:
|
|
||||||
issues = json.load(response)
|
|
||||||
return any(issue.get("title") == title for issue in issues)
|
|
||||||
|
|
||||||
def create_issue(title, body):
|
|
||||||
if find_existing(title):
|
|
||||||
print(f"Open issue already exists: {title}")
|
|
||||||
return
|
|
||||||
payload = json.dumps({"title": title, "body": body}).encode("utf-8")
|
|
||||||
request = urllib.request.Request(issues_url, data=payload, headers=headers, method="POST")
|
|
||||||
with urllib.request.urlopen(request, timeout=30) as response:
|
|
||||||
created = json.load(response)
|
|
||||||
print(f"Created issue #{created.get('number')}: {title}")
|
|
||||||
|
|
||||||
def body(scanner, summary, details):
|
|
||||||
run_url = f"{server_url}/{repo}/actions/runs/{run_id}" if server_url and run_id else "Unavailable"
|
|
||||||
lines = [
|
|
||||||
f"Security scanner: `{scanner}`",
|
|
||||||
f"Summary: {summary}",
|
|
||||||
f"Repository: `{repo}`",
|
|
||||||
f"Branch/ref: `{ref_name}`",
|
|
||||||
f"Commit: `{sha}`",
|
|
||||||
f"Action run: {run_url}",
|
|
||||||
"",
|
|
||||||
"Details:",
|
|
||||||
]
|
|
||||||
lines.extend(f"- {key}: {value}" for key, value in details if value not in (None, ""))
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
findings = []
|
|
||||||
|
|
||||||
for finding in load_json("security-results/gitleaks.json", []):
|
|
||||||
rule = finding.get("RuleID") or finding.get("Description") or "secret"
|
|
||||||
file_path = finding.get("File", "unknown")
|
|
||||||
line = finding.get("StartLine", "unknown")
|
|
||||||
title = trim(f"[security][gitleaks] {rule} in {file_path}:{line}", 255)
|
|
||||||
findings.append((
|
|
||||||
title,
|
|
||||||
body("gitleaks", f"{rule} in `{file_path}:{line}`", [
|
|
||||||
("Rule", rule),
|
|
||||||
("Description", finding.get("Description")),
|
|
||||||
("File", file_path),
|
|
||||||
("Line", line),
|
|
||||||
("Fingerprint", finding.get("Fingerprint")),
|
|
||||||
]),
|
|
||||||
))
|
|
||||||
|
|
||||||
semgrep = load_json("security-results/semgrep.json", {})
|
|
||||||
for result in semgrep.get("results", []):
|
|
||||||
extra = result.get("extra", {})
|
|
||||||
start = result.get("start", {})
|
|
||||||
check_id = result.get("check_id", "semgrep finding")
|
|
||||||
file_path = result.get("path", "unknown")
|
|
||||||
line = start.get("line", "unknown")
|
|
||||||
title = trim(f"[security][semgrep] {check_id} in {file_path}:{line}", 255)
|
|
||||||
findings.append((
|
|
||||||
title,
|
|
||||||
body("semgrep", extra.get("message", check_id), [
|
|
||||||
("Check", check_id),
|
|
||||||
("Severity", extra.get("severity")),
|
|
||||||
("File", file_path),
|
|
||||||
("Line", line),
|
|
||||||
("Message", extra.get("message")),
|
|
||||||
]),
|
|
||||||
))
|
|
||||||
|
|
||||||
def add_trivy_findings(path, scanner):
|
|
||||||
report = load_json(path, {})
|
|
||||||
for result in report.get("Results", []):
|
|
||||||
target = result.get("Target", "unknown")
|
|
||||||
for vuln in result.get("Vulnerabilities", []) or []:
|
|
||||||
vuln_id = vuln.get("VulnerabilityID", "vulnerability")
|
|
||||||
package = vuln.get("PkgName", "unknown package")
|
|
||||||
severity = vuln.get("Severity", "UNKNOWN")
|
|
||||||
title = trim(f"[security][{scanner}] {severity} {vuln_id} in {package} on {target}", 255)
|
|
||||||
findings.append((
|
|
||||||
title,
|
|
||||||
body(scanner, vuln.get("Title") or vuln_id, [
|
|
||||||
("Type", "Vulnerability"),
|
|
||||||
("Severity", severity),
|
|
||||||
("Target", target),
|
|
||||||
("Package", package),
|
|
||||||
("Installed version", vuln.get("InstalledVersion")),
|
|
||||||
("Fixed version", vuln.get("FixedVersion")),
|
|
||||||
("Vulnerability ID", vuln_id),
|
|
||||||
("Primary URL", vuln.get("PrimaryURL")),
|
|
||||||
]),
|
|
||||||
))
|
|
||||||
for misconfig in result.get("Misconfigurations", []) or []:
|
|
||||||
misconfig_id = misconfig.get("ID", "misconfiguration")
|
|
||||||
severity = misconfig.get("Severity", "UNKNOWN")
|
|
||||||
title = trim(f"[security][{scanner}] {severity} {misconfig_id} in {target}", 255)
|
|
||||||
findings.append((
|
|
||||||
title,
|
|
||||||
body(scanner, misconfig.get("Title") or misconfig_id, [
|
|
||||||
("Type", "Misconfiguration"),
|
|
||||||
("Severity", severity),
|
|
||||||
("Target", target),
|
|
||||||
("ID", misconfig_id),
|
|
||||||
("Message", misconfig.get("Message")),
|
|
||||||
("Resolution", misconfig.get("Resolution")),
|
|
||||||
]),
|
|
||||||
))
|
|
||||||
for secret in result.get("Secrets", []) or []:
|
|
||||||
rule = secret.get("RuleID", "secret")
|
|
||||||
line = secret.get("StartLine", "unknown")
|
|
||||||
title = trim(f"[security][{scanner}] {rule} in {target}:{line}", 255)
|
|
||||||
findings.append((
|
|
||||||
title,
|
|
||||||
body(scanner, f"{rule} in `{target}:{line}`", [
|
|
||||||
("Type", "Secret"),
|
|
||||||
("Rule", rule),
|
|
||||||
("Category", secret.get("Category")),
|
|
||||||
("Severity", secret.get("Severity")),
|
|
||||||
("Target", target),
|
|
||||||
("Line", line),
|
|
||||||
]),
|
|
||||||
))
|
|
||||||
|
|
||||||
add_trivy_findings("security-results/trivy-fs.json", "trivy-fs")
|
|
||||||
add_trivy_findings("security-results/trivy-image.json", "trivy-image")
|
|
||||||
|
|
||||||
if not findings:
|
|
||||||
print("No security findings found in scanner reports.")
|
|
||||||
raise SystemExit(0)
|
|
||||||
|
|
||||||
for title, issue_body in findings:
|
|
||||||
try:
|
|
||||||
create_issue(title, issue_body)
|
|
||||||
except Exception as exc:
|
|
||||||
print(f"Failed to create issue for {title}: {exc}")
|
|
||||||
PY
|
|
||||||
|
|
||||||
- name: Stop on failed security scans
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
failed=0
|
|
||||||
for result in security-results/*.exit; do
|
|
||||||
name="$(basename "$result" .exit)"
|
|
||||||
code="$(cat "$result")"
|
|
||||||
if [ "$code" != "0" ]; then
|
|
||||||
echo "$name failed with exit code $code"
|
|
||||||
failed=1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$failed" != "0" ]; then
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Notify Apprise (failure)
|
|
||||||
if: failure()
|
|
||||||
run: |
|
|
||||||
curl -X POST \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{
|
|
||||||
\"tags\": \"all\",
|
|
||||||
\"title\": \"Gitea Security Scan Failed\",
|
|
||||||
\"body\": \"Repo: ${{ gitea.repository }}\\nBranch: ${{ gitea.ref_name }}\\nSecurity scan failed; check logs and generated issues in Gitea\"
|
|
||||||
}" \
|
|
||||||
${{ secrets.APPRISE_URL }}
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
id: docker-login
|
|
||||||
if: ${{ gitea.event_name == 'push' }}
|
|
||||||
run: |
|
|
||||||
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login \
|
|
||||||
--username "${{ secrets.DOCKER_USERNAME }}" \
|
|
||||||
--password-stdin
|
|
||||||
|
|
||||||
- name: Push scanned image
|
|
||||||
id: push
|
|
||||||
if: ${{ gitea.event_name == 'push' }}
|
|
||||||
run: |
|
|
||||||
docker push blinkfink182/donetick-notifier:${{ steps.tag.outputs.tag }}
|
|
||||||
|
|
||||||
- name: Notify Apprise (success)
|
|
||||||
if: ${{ success() && gitea.event_name == 'push' }}
|
|
||||||
run: |
|
|
||||||
curl -X POST \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{
|
|
||||||
\"tags\": \"all\",
|
|
||||||
\"title\": \"Gitea Build Succeeded\",
|
|
||||||
\"body\": \"Repo: ${{ gitea.repository }}\\nBranch: ${{ gitea.ref_name }}\\nImage tag ${{ steps.tag.outputs.tag }} pushed successfully\"
|
|
||||||
}" \
|
|
||||||
${{ secrets.APPRISE_URL }}
|
|
||||||
|
|
||||||
- name: Notify Apprise (failure)
|
|
||||||
if: ${{ failure() && (steps.docker-login.outcome == 'failure' || steps.push.outcome == 'failure') }}
|
|
||||||
run: |
|
|
||||||
curl -X POST \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{
|
|
||||||
\"tags\": \"all\",
|
|
||||||
\"title\": \"Gitea Build Failed\",
|
|
||||||
\"body\": \"Repo: ${{ gitea.repository }}\\nBranch: ${{ gitea.ref_name }}\\nCheck logs in Gitea\"
|
|
||||||
}" \
|
|
||||||
${{ secrets.APPRISE_URL }}
|
|
||||||
-16
@@ -4,19 +4,3 @@
|
|||||||
debug.ps1
|
debug.ps1
|
||||||
.postmate/postmate-history.json
|
.postmate/postmate-history.json
|
||||||
.postmate/postmate-envs.json
|
.postmate/postmate-envs.json
|
||||||
# Secrets
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
*.pem
|
|
||||||
*.key
|
|
||||||
*.pfx
|
|
||||||
*.p12
|
|
||||||
secrets/
|
|
||||||
credentials/
|
|
||||||
config.local.*
|
|
||||||
|
|
||||||
# VS Code local settings
|
|
||||||
.vscode/settings.json
|
|
||||||
|
|
||||||
# PowerShell
|
|
||||||
*.psd1.local
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
repos:
|
|
||||||
- repo: https://github.com/gitleaks/gitleaks
|
|
||||||
rev: v8.24.2
|
|
||||||
hooks:
|
|
||||||
- id: gitleaks
|
|
||||||
|
|
||||||
- repo: local
|
|
||||||
hooks:
|
|
||||||
- id: semgrep
|
|
||||||
name: semgrep
|
|
||||||
entry: bash -c 'docker run --rm -v "$(realpath .)":/src:Z docker.io/semgrep/semgrep semgrep scan --config auto /src'
|
|
||||||
language: system
|
|
||||||
pass_filenames: false
|
|
||||||
|
|
||||||
- repo: local
|
|
||||||
hooks:
|
|
||||||
- id: trivy
|
|
||||||
name: trivy filesystem scan
|
|
||||||
entry: bash -c 'docker run --rm -v "$(pwd)":/workspace docker.io/aquasec/trivy fs --scanners vuln,secret,misconfig --severity HIGH,CRITICAL /workspace'
|
|
||||||
language: system
|
|
||||||
pass_filenames: false
|
|
||||||
|
|
||||||
- repo: local
|
|
||||||
hooks:
|
|
||||||
- id: psscriptanalyzer
|
|
||||||
name: powershell static analysis
|
|
||||||
entry: pwsh -NoProfile -Command "Import-Module PSScriptAnalyzer; Invoke-ScriptAnalyzer -Path . -Recurse -Severity Error"
|
|
||||||
language: system
|
|
||||||
pass_filenames: false
|
|
||||||
Vendored
+1
-2
@@ -1,4 +1,3 @@
|
|||||||
{
|
{
|
||||||
"trivy.secretScanning": true,
|
"trivy.secretScanning": true
|
||||||
"semgrep.scan.onlyGitDirty": false
|
|
||||||
}
|
}
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
[string] $appriseWebhookURL = $ENV:APPRISEWEBHOOKURL
|
|
||||||
[string] $appriseWebhookTag = $ENV:APPRISEWEBHOOKTAG
|
|
||||||
|
|
||||||
function Send-AppriseNotification
|
|
||||||
{
|
|
||||||
param(
|
|
||||||
# Title of the notification
|
|
||||||
[Parameter(Mandatory=$true)]
|
|
||||||
[string]
|
|
||||||
$title,
|
|
||||||
|
|
||||||
# Content of the notification
|
|
||||||
[Parameter(Mandatory=$true)]
|
|
||||||
[string]
|
|
||||||
$content
|
|
||||||
)
|
|
||||||
|
|
||||||
$headers = @{
|
|
||||||
"Content-Type"="application/json"
|
|
||||||
}
|
|
||||||
$body = @{
|
|
||||||
title=$title
|
|
||||||
body=$content
|
|
||||||
tags="$appriseWebhookTag"
|
|
||||||
}
|
|
||||||
$json = $body | ConvertTo-Json
|
|
||||||
try {
|
|
||||||
Invoke-WebRequest -URI $appriseWebhookURL -Method post -Body $json -Headers $headers
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Host "Notification Error Encountered: $($global:Error[0])"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+3
-18
@@ -1,23 +1,8 @@
|
|||||||
FROM mcr.microsoft.com/powershell:7.5-ubuntu-24.04
|
FROM mcr.microsoft.com/powershell
|
||||||
|
|
||||||
USER root
|
|
||||||
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends tzdata wget \
|
|
||||||
&& wget -q https://github.com/aptible/supercronic/releases/latest/download/supercronic-linux-amd64 -O /usr/local/bin/supercronic \
|
|
||||||
&& chmod +x /usr/local/bin/supercronic \
|
|
||||||
&& apt-get dist-upgrade -y \
|
|
||||||
&& apt-get clean \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
|
|
||||||
USER 1000:1000
|
USER 1000:1000
|
||||||
|
|
||||||
WORKDIR /data
|
WORKDIR /data
|
||||||
COPY ["crontab", "/data/"]
|
COPY ["Start-DoneTickNotifier.ps1", "/data/"]
|
||||||
COPY --chmod=755 ["main.sh", "/data/"]
|
|
||||||
COPY ["Start-DoneTickNotifier.ps1", "Start-DoneTickConsumer.ps1", "AppriseNotification.psm1", "/data/"]
|
|
||||||
|
|
||||||
ENTRYPOINT ["/data/main.sh"]
|
ENTRYPOINT ["pwsh", "-Command", "/data/Start-DoneTickNotifier.ps1"]
|
||||||
|
|
||||||
EXPOSE 8080
|
|
||||||
@@ -1,170 +1,2 @@
|
|||||||
# donetick-notifier
|
# donetick-notifier
|
||||||
|
|
||||||
> AI disclaimer: AI was used solely for creating this README, the Gitea pipelines, and the base of the webhook consumer.
|
|
||||||
|
|
||||||
A PowerShell-based notifier and webhook consumer for [Donetick](https://github.com/donetick/donetick) chores.
|
|
||||||
|
|
||||||
The project now has two runtime paths:
|
|
||||||
|
|
||||||
- A scheduled notifier that checks the Donetick external API for chores and sends Apprise-compatible summary notifications for overdue and due-today tasks.
|
|
||||||
- A webhook consumer that listens on port `8080`, accepts Donetick webhook payloads, and turns them into Apprise notifications.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Fetches chores from `https://<DONETICKHOST>:<DONETICKPORT>/eapi/v1/chore`.
|
|
||||||
- Authenticates to Donetick with the `secretkey` header.
|
|
||||||
- Sends one notification for overdue chores when any exist.
|
|
||||||
- Sends one notification for chores due today when any exist.
|
|
||||||
- Accepts webhook events for `task.completed`, `task.reminder`, and `task.created`.
|
|
||||||
- Falls back to a generic notification for unknown webhook event types.
|
|
||||||
- Runs the scheduler and webhook listener together in the Docker container.
|
|
||||||
- Includes Gitea pipelines for Docker image publishing and security scanning.
|
|
||||||
|
|
||||||
## How It Runs
|
|
||||||
|
|
||||||
The container entrypoint is `main.sh`.
|
|
||||||
|
|
||||||
- `supercronic` runs `Start-DoneTickNotifier.ps1` on the schedule defined by `JOB_SCHEDULE`.
|
|
||||||
- `Start-DoneTickConsumer.ps1` runs in the foreground and listens on `http://+:8080/`.
|
|
||||||
|
|
||||||
The notifier and consumer both use `AppriseNotification.psm1` to send messages to the configured Apprise webhook.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
All configuration is provided through environment variables.
|
|
||||||
|
|
||||||
| Variable | Required | Description |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `DONETICKHOST` | Yes | Donetick host name or IP address. Do not include `https://`. |
|
|
||||||
| `DONETICKPORT` | Yes | Donetick HTTPS port. |
|
|
||||||
| `DONETICKAPIKEY` | Yes | Donetick external API key. Sent as the `secretkey` header. |
|
|
||||||
| `APPRISEWEBHOOKURL` | Yes | Apprise webhook URL that accepts notification posts. |
|
|
||||||
| `APPRISEWEBHOOKTAG` | Yes | Apprise tag value to include with each notification. |
|
|
||||||
| `JOB_SCHEDULE` | Yes for Docker | Cron expression used by the container scheduler, such as `0 8,17 * * *`. |
|
|
||||||
| `TZ` | No | Container timezone, such as `America/Chicago`. Recommended so notification hours match your local time. |
|
|
||||||
|
|
||||||
## Docker
|
|
||||||
|
|
||||||
The published image is `docker.io/blinkfink182/donetick-notifier`.
|
|
||||||
|
|
||||||
### Run
|
|
||||||
|
|
||||||
```sh
|
|
||||||
docker run -d \
|
|
||||||
--name donetick-notifier \
|
|
||||||
-p 8080:8080 \
|
|
||||||
-e DONETICKHOST=host.docker.internal \
|
|
||||||
-e DONETICKPORT=8787 \
|
|
||||||
-e DONETICKAPIKEY=your-donetick-api-key \
|
|
||||||
-e APPRISEWEBHOOKURL=https://apprise.example.com/notify/config \
|
|
||||||
-e APPRISEWEBHOOKTAG=all \
|
|
||||||
-e JOB_SCHEDULE="0 8,17 * * *" \
|
|
||||||
-e TZ=America/Chicago \
|
|
||||||
docker.io/blinkfink182/donetick-notifier
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker Compose
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
donetick-notifier:
|
|
||||||
container_name: donetick-notifier
|
|
||||||
image: docker.io/blinkfink182/donetick-notifier
|
|
||||||
ports:
|
|
||||||
- 8080:8080
|
|
||||||
environment:
|
|
||||||
- DONETICKHOST=host.docker.internal
|
|
||||||
- DONETICKPORT=8787
|
|
||||||
- DONETICKAPIKEY=your-donetick-api-key
|
|
||||||
- APPRISEWEBHOOKURL=https://apprise.example.com/notify/config
|
|
||||||
- APPRISEWEBHOOKTAG=all
|
|
||||||
- JOB_SCHEDULE=0 8,17 * * *
|
|
||||||
- TZ=America/Chicago
|
|
||||||
```
|
|
||||||
|
|
||||||
Run it with:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
If Donetick and Apprise are on the same Docker network, use service names instead of `host.docker.internal`.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
donetick-notifier:
|
|
||||||
image: docker.io/blinkfink182/donetick-notifier
|
|
||||||
ports:
|
|
||||||
- 8080:8080
|
|
||||||
environment:
|
|
||||||
- DONETICKHOST=donetick
|
|
||||||
- DONETICKPORT=8787
|
|
||||||
- DONETICKAPIKEY=your-donetick-api-key
|
|
||||||
- APPRISEWEBHOOKURL=http://apprise:8000/notify/config
|
|
||||||
- APPRISEWEBHOOKTAG=all
|
|
||||||
- JOB_SCHEDULE=0 8,17 * * *
|
|
||||||
- TZ=America/Chicago
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build Locally
|
|
||||||
|
|
||||||
```sh
|
|
||||||
docker build -t donetick-notifier .
|
|
||||||
```
|
|
||||||
|
|
||||||
```sh
|
|
||||||
docker run -d \
|
|
||||||
--name donetick-notifier \
|
|
||||||
-p 8080:8080 \
|
|
||||||
-e DONETICKHOST=host.docker.internal \
|
|
||||||
-e DONETICKPORT=8787 \
|
|
||||||
-e DONETICKAPIKEY=your-donetick-api-key \
|
|
||||||
-e APPRISEWEBHOOKURL=https://apprise.example.com/notify/config \
|
|
||||||
-e APPRISEWEBHOOKTAG=all \
|
|
||||||
-e JOB_SCHEDULE="0 8,17 * * *" \
|
|
||||||
-e TZ=America/Chicago \
|
|
||||||
donetick-notifier
|
|
||||||
```
|
|
||||||
|
|
||||||
## Run Without Docker
|
|
||||||
|
|
||||||
PowerShell 7 or newer is recommended.
|
|
||||||
|
|
||||||
Scheduled notifier:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$env:DONETICKHOST = "donetick.example.com"
|
|
||||||
$env:DONETICKPORT = "8787"
|
|
||||||
$env:DONETICKAPIKEY = "your-donetick-api-key"
|
|
||||||
$env:APPRISEWEBHOOKURL = "https://apprise.example.com/notify/config"
|
|
||||||
$env:APPRISEWEBHOOKTAG = "all"
|
|
||||||
|
|
||||||
pwsh ./Start-DoneTickNotifier.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
Webhook consumer:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$env:APPRISEWEBHOOKURL = "https://apprise.example.com/notify/config"
|
|
||||||
$env:APPRISEWEBHOOKTAG = "all"
|
|
||||||
|
|
||||||
pwsh ./Start-DoneTickConsumer.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
The consumer listens on port `8080` by default. Stop either script with `Ctrl+C` when running interactively.
|
|
||||||
|
|
||||||
## CI/CD
|
|
||||||
|
|
||||||
This repository includes Gitea workflows for:
|
|
||||||
|
|
||||||
- Building and pushing the Docker image on manual dispatch.
|
|
||||||
- Tagging images as `latest` for `main`, as the ref name for `v*` tags, and as `test` for everything else.
|
|
||||||
- Running security checks with Gitleaks, Semgrep, and Trivy.
|
|
||||||
- Creating Gitea issues for security findings when `GITEA_TOKEN` is configured.
|
|
||||||
- Sending Apprise notifications for Docker build success or failure.
|
|
||||||
|
|
||||||
The security workflow runs on pushes, pull requests, and manual dispatch. On push, it also builds the image so Trivy can scan the resulting container.
|
|
||||||
|
|
||||||
## API References
|
|
||||||
|
|
||||||
The `Donetick/` directory contains request examples and collection metadata used to document and exercise the Donetick API while developing the notifier.
|
|
||||||
|
|||||||
@@ -1,152 +0,0 @@
|
|||||||
[string] $ListenUrl = "http://+:8080/"
|
|
||||||
|
|
||||||
Import-Module ./AppriseNotification.psm1
|
|
||||||
|
|
||||||
function Write-JsonResponse
|
|
||||||
{
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory=$true)]
|
|
||||||
[System.Net.HttpListenerResponse]
|
|
||||||
$Response,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$true)]
|
|
||||||
[int]
|
|
||||||
$StatusCode,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$true)]
|
|
||||||
[hashtable]
|
|
||||||
$Body
|
|
||||||
)
|
|
||||||
|
|
||||||
$json = $Body | ConvertTo-Json -Depth 10
|
|
||||||
$bytes = [System.Text.Encoding]::UTF8.GetBytes($json)
|
|
||||||
|
|
||||||
$Response.StatusCode = $StatusCode
|
|
||||||
$Response.ContentType = "application/json"
|
|
||||||
$Response.ContentEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
$Response.ContentLength64 = $bytes.Length
|
|
||||||
$Response.OutputStream.Write($bytes, 0, $bytes.Length)
|
|
||||||
$Response.OutputStream.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
function Read-RequestBody
|
|
||||||
{
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory=$true)]
|
|
||||||
[System.Net.HttpListenerRequest]
|
|
||||||
$Request
|
|
||||||
)
|
|
||||||
|
|
||||||
$reader = [System.IO.StreamReader]::new($Request.InputStream, $Request.ContentEncoding)
|
|
||||||
try {
|
|
||||||
return $reader.ReadToEnd()
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
$reader.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Receive-Webhook
|
|
||||||
{
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory=$true)]
|
|
||||||
[System.Net.HttpListenerContext]
|
|
||||||
$Context
|
|
||||||
)
|
|
||||||
|
|
||||||
$request = $Context.Request
|
|
||||||
$response = $Context.Response
|
|
||||||
|
|
||||||
if ($request.HttpMethod -ne "POST") {
|
|
||||||
Write-JsonResponse -Response $response -StatusCode 405 -Body @{
|
|
||||||
ok = $false
|
|
||||||
error = "Only POST requests are supported."
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
$rawBody = Read-RequestBody -Request $request
|
|
||||||
$payload = $null
|
|
||||||
|
|
||||||
try {
|
|
||||||
if ($rawBody) {
|
|
||||||
$payload = $rawBody | ConvertFrom-Json
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-JsonResponse -Response $response -StatusCode 400 -Body @{
|
|
||||||
ok = $false
|
|
||||||
error = "Request body was not valid JSON."
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "Webhook received at $(Get-Date -Format o)"
|
|
||||||
Write-Host "From: $($request.RemoteEndPoint)"
|
|
||||||
Write-Host "Path: $($request.Url.AbsolutePath)"
|
|
||||||
|
|
||||||
if ($payload) {
|
|
||||||
Write-Host "Payload:"
|
|
||||||
Write-Host ($payload | ConvertTo-Json -Depth 20)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-Host "Payload: <empty>"
|
|
||||||
}
|
|
||||||
|
|
||||||
$notificationTitle = ""
|
|
||||||
$notificationContent = ""
|
|
||||||
switch($payload.type)
|
|
||||||
{
|
|
||||||
"task.completed" {
|
|
||||||
$notificationTitle = "Donetick Task Completed"
|
|
||||||
$notificationContent = "$($payload.data.chore.name) marked completed!"
|
|
||||||
}
|
|
||||||
"task.reminder" {
|
|
||||||
$notificationTitle = "Donetick Task Reminder"
|
|
||||||
$notificationContent = "$($payload.data.name) is due at $((Get-Date $payload.data.due_date).ToLocalTime())"
|
|
||||||
}
|
|
||||||
"task.created" {
|
|
||||||
$notificationTitle = "Donetick Task Created"
|
|
||||||
if ($null -eq $payload.data.chore.nextDueDate)
|
|
||||||
{
|
|
||||||
$notificationContent = "$($payload.data.chore.name) created"
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$notificationContent = "$($payload.data.chore.name) created with due date of $((Get-Date $payload.data.chore.nextDueDate).ToLocalTime())"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default {
|
|
||||||
$notificationTitle = "Donetick Notification"
|
|
||||||
$notificationContent = "Donetick event of type $($payload.type) received"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Send-AppriseNotification -title $notificationTitle -content $notificationContent
|
|
||||||
|
|
||||||
Write-JsonResponse -Response $response -StatusCode 200 -Body @{
|
|
||||||
ok = $true
|
|
||||||
message = "Webhook accepted."
|
|
||||||
receivedAt = (Get-Date -Format o)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$listener = [System.Net.HttpListener]::new()
|
|
||||||
$listener.Prefixes.Add($ListenUrl)
|
|
||||||
|
|
||||||
try {
|
|
||||||
$listener.Start()
|
|
||||||
Write-Host "Webhook consumer listening on $ListenUrl"
|
|
||||||
Write-Host "Press Ctrl+C to stop."
|
|
||||||
|
|
||||||
while ($listener.IsListening) {
|
|
||||||
$context = $listener.GetContext()
|
|
||||||
Receive-Webhook -Context $context
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
if ($listener.IsListening) {
|
|
||||||
$listener.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
$listener.Close()
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,40 @@
|
|||||||
[string] $dtHost = $ENV:DONETICKHOST
|
[string] $dtHost = $ENV:DONETICKHOST
|
||||||
[string] $dtPort = $ENV:DONETICKPORT
|
[string] $dtPort = $ENV:DONETICKPORT
|
||||||
[string] $dtAPIKey = $ENV:DONETICKAPIKEY
|
[string] $dtAPIKey = $ENV:DONETICKAPIKEY
|
||||||
|
[string] $appriseWebhookURL = $ENV:APPRISEWEBHOOKURL
|
||||||
|
[string] $appriseWebhookTag = $ENV:APPRISEWEBHOOKTAG
|
||||||
|
|
||||||
Import-Module ./AppriseNotification.psm1
|
|
||||||
|
function Send-Notification
|
||||||
|
{
|
||||||
|
param(
|
||||||
|
# Title of the notification
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[string]
|
||||||
|
$title,
|
||||||
|
|
||||||
|
# Content of the notification
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[string]
|
||||||
|
$content
|
||||||
|
)
|
||||||
|
|
||||||
|
$headers = @{
|
||||||
|
"Content-Type"="application/json"
|
||||||
|
}
|
||||||
|
$body = @{
|
||||||
|
title=$title
|
||||||
|
body=$content
|
||||||
|
tags="$appriseWebhookTag"
|
||||||
|
}
|
||||||
|
$json = $body | ConvertTo-Json
|
||||||
|
try {
|
||||||
|
Invoke-WebRequest -URI $appriseWebhookURL -Method post -Body $json -Headers $headers
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Host "Notification Error Encountered: $($global:Error[0])"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function Get-Chores
|
function Get-Chores
|
||||||
{
|
{
|
||||||
@@ -28,7 +60,7 @@ foreach($chore in $chores)
|
|||||||
{
|
{
|
||||||
if ($chore.nextDueDate)
|
if ($chore.nextDueDate)
|
||||||
{
|
{
|
||||||
$dueDate = (Get-Date $chore.nextDueDate).ToLocalTime()
|
$dueDate = Get-Date $chore.nextDueDate
|
||||||
if (($dueDate - $today).Days -lt 0) #OVERDUE
|
if (($dueDate - $today).Days -lt 0) #OVERDUE
|
||||||
{
|
{
|
||||||
write-host "$($chore.name) $dueDate is overdue!"
|
write-host "$($chore.name) $dueDate is overdue!"
|
||||||
@@ -38,6 +70,7 @@ foreach($chore in $chores)
|
|||||||
{
|
{
|
||||||
write-host "$($chore.name) $dueDate is due today!"
|
write-host "$($chore.name) $dueDate is due today!"
|
||||||
$todaysTasks += $chore
|
$todaysTasks += $chore
|
||||||
|
# Send-Notification -title "TASK DUE TODAY" -content "$($chore.Name) is due today!"
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -53,9 +86,9 @@ if ($overdueTasks.Count -ne 0)
|
|||||||
$content = "The following tasks are overdue!`n"
|
$content = "The following tasks are overdue!`n"
|
||||||
foreach($overdueTask in $overdueTasks)
|
foreach($overdueTask in $overdueTasks)
|
||||||
{
|
{
|
||||||
$content += "$($overdueTask.Name) was due $((Get-Date $overdueTask.nextDueDate).ToLocalTime())`n"
|
$content += "$($overdueTask.Name) was due $(Get-Date $overdueTask.nextDueDate -Format "MM/dd/yyyy HH:mm")`n"
|
||||||
}
|
}
|
||||||
Send-AppriseNotification -title "OVERDUE TASKS" -content $content
|
Send-Notification -title "OVERDUE TASKS" -content $content
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($todaysTasks.Count -ne 0)
|
if ($todaysTasks.Count -ne 0)
|
||||||
@@ -64,7 +97,7 @@ if ($todaysTasks.Count -ne 0)
|
|||||||
$content = "The following tasks are due today!`n"
|
$content = "The following tasks are due today!`n"
|
||||||
foreach($task in $todaysTasks)
|
foreach($task in $todaysTasks)
|
||||||
{
|
{
|
||||||
$content += "$($task.Name) is due $((Get-Date $task.nextDueDate).ToLocalTime())`n"
|
$content += "$($task.Name) is due $(Get-Date $task.nextDueDate -Format "MM/dd/yyyy HH:mm")`n"
|
||||||
}
|
}
|
||||||
Send-AppriseNotification -title "TODAY'S TASKS" -content $content
|
Send-Notification -title "TODAY'S TASKS" -content $content
|
||||||
}
|
}
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
# Default schedule (overridden at runtime by JOB_SCHEDULE env var)
|
|
||||||
0 * * * * pwsh /data/Start-DoneTickNotifier.ps1
|
|
||||||
@@ -3,8 +3,6 @@ services:
|
|||||||
donetick-notifier:
|
donetick-notifier:
|
||||||
container_name: donetick-notifier
|
container_name: donetick-notifier
|
||||||
image: docker.io/blinkfink182/donetick-notifier
|
image: docker.io/blinkfink182/donetick-notifier
|
||||||
ports:
|
|
||||||
- 8080:8080
|
|
||||||
environment:
|
environment:
|
||||||
# BELOW ARE ALL REQUIRED
|
# BELOW ARE ALL REQUIRED
|
||||||
- DONETICKHOST=host.docker.internal # donetick host
|
- DONETICKHOST=host.docker.internal # donetick host
|
||||||
@@ -12,5 +10,3 @@ services:
|
|||||||
- DONETICKAPIKEY=adminpass # donetick API key
|
- DONETICKAPIKEY=adminpass # donetick API key
|
||||||
- APPRISEWEBHOOKURL=https://apprisehost/notify/config # apprise notification url
|
- APPRISEWEBHOOKURL=https://apprisehost/notify/config # apprise notification url
|
||||||
- APPRISEWEBHOOKTAG=all # apprise notification tag
|
- APPRISEWEBHOOKTAG=all # apprise notification tag
|
||||||
- TZ=America/Chicago # set timezone from this list: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List
|
|
||||||
- JOB_SCHEDULE=0 8,17 * * * # when to notify about overdue tasks or tasks due today
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Build the crontab dynamically from the env variable
|
|
||||||
echo "${JOB_SCHEDULE} pwsh /data/Start-DoneTickNotifier.ps1" > /tmp/crontab-runtime
|
|
||||||
|
|
||||||
echo "Cron schedule set to: ${JOB_SCHEDULE}"
|
|
||||||
|
|
||||||
# Start supercronic in background using the generated crontab
|
|
||||||
supercronic /tmp/crontab-runtime &
|
|
||||||
|
|
||||||
# Start the HTTP listener in foreground
|
|
||||||
exec pwsh /data/Start-DoneTickConsumer.ps1
|
|
||||||
Reference in New Issue
Block a user