Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions .github/workflows/cmsend-notify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
name: cmsend-notify
# Reusable workflow to notify a chatmail group about workflow result state changes
# via the [`cmsend`](https://github.com/chatmail/cmsend) CLI, needs to be paired with `cmsend-sync.yml`, see doc/cmsend-notify/README.md.

on:
workflow_call:
inputs:
result: { type: string, required: true } # needs.<job>.result
alert-tag: { type: string, default: "LOG" }
secrets:
cmsend-account: { required: true } # base64 seed, see README.md
env:
# TEMPORARY: --sync is unreleased, swap back to "cmsend@<version>" pin after merge
CMSEND_SPEC: "git+https://github.com/chatmail/cmsend@ci-sync-and-name"
permissions:
actions: read # read the previous run's conclusion (for change-only alerts)
jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: Check notification necessity
id: gate
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
RUN_ID: ${{ github.run_id }}
BRANCH: ${{ github.ref_name }}
RESULT: ${{ inputs.result }}
run: |
set -euo pipefail
WF_ID=$(gh api "repos/$REPO/actions/runs/$RUN_ID" --jq .workflow_id)
PREV=$(gh api \
"repos/$REPO/actions/workflows/$WF_ID/runs?branch=$BRANCH&status=completed&per_page=1" \
--jq '.workflow_runs[0].conclusion // "none"')
case "$RESULT:$PREV" in
success:failure) echo "verb=RECOVERED" >> "$GITHUB_OUTPUT" ;;
failure:failure) echo "no change (still failing), staying quiet" ;;
failure:*) echo "verb=FAILED" >> "$GITHUB_OUTPUT" ;;
success:*) echo "no change (still passing), staying quiet" ;;
*) echo "result=$RESULT: no notification" ;;
esac

- if: steps.gate.outputs.verb
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0

- name: Restore cmsend profile
if: steps.gate.outputs.verb
# Prefer the nightly-refreshed cache, seed is fallback
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
id: cache
with:
path: ~/cmsend-profile.enc
key: cmsend-profile-${{ github.run_id }}
restore-keys: cmsend-profile-

- name: Unpack cmsend profile
if: steps.gate.outputs.verb
# Keep this in sync with account.sh unpack and cmsend-sync.yml.
env:
B64: ${{ secrets.cmsend-account }}
HIT: ${{ steps.cache.outputs.cache-matched-key }}
run: |
set -euo pipefail
mkdir -p ~/.config/cmsend
if [ -n "$HIT" ]; then
printf '%s' "$B64" | sha256sum | cut -d' ' -f1 > ~/.cmsend-key
openssl enc -d -aes-256-cbc -pbkdf2 -pass file:$HOME/.cmsend-key \
-in ~/cmsend-profile.enc | tar -C ~/.config/cmsend -xzf -
else
echo "::warning::cmsend cache retrieve failed, falling back to the seed secret"
printf '%s' "$B64" | base64 -d | tar -C ~/.config/cmsend -xzf -
for s in ~/.config/cmsend/*/dc.sql; do
d=$(dirname "$s"); sqlite3 "$d/dc.db" < "$s"
mkdir -p "$d/dc.db-blobs"; rm -f "$s"
done
fi

- name: Notify
if: steps.gate.outputs.verb
env:
VERB: ${{ steps.gate.outputs.verb }}
ALERT_TAG: ${{ inputs.alert-tag }}
REPO: ${{ github.repository }}
WF: ${{ github.workflow }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
printf '%s %s %s\n%s' "$REPO" "$WF" "$VERB" "$RUN_URL" |
uvx --from "$CMSEND_SPEC" cmsend -t "$ALERT_TAG"

- name: Scrub the profile
if: always()
run: rm -rf ~/.config/cmsend ~/.cmsend-key ~/cmsend-profile.enc
88 changes: 88 additions & 0 deletions .github/workflows/cmsend-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: cmsend-sync
# Reusable nightly workflow going along with cmsend-notify.yml:
# 1. Logs in to keep account alive
# 2. Fetches group updates (memberships and keys)
# 3. Stores the new profile data in an encrypted cache file

on:
workflow_call:
inputs:
alert-tag: { type: string, default: "LOG" }
secrets:
cmsend-account: { required: true }
permissions: {}
env:
# TEMPORARY: --sync is unreleased, so install from the branch that has it.
# Swap back to a plain "cmsend@<version>" pin once cmsend is tagged.
CMSEND_SPEC: "git+https://github.com/chatmail/cmsend@ci-sync-and-name"
concurrency:
group: cmsend-sync-${{ github.repository }}
jobs:
sync:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0

- uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
id: cache
with:
path: ~/cmsend-profile.enc
key: cmsend-profile-${{ github.run_id }}
restore-keys: cmsend-profile-

- name: Unpack cmsend profile
# Note: Keep the following in sync with cmsend-notify.yml and scripts/pack-cmsend-profile.sh!
env:
B64: ${{ secrets.cmsend-account }}
HIT: ${{ steps.cache.outputs.cache-matched-key }}
run: |
set -euo pipefail
mkdir -p ~/.config/cmsend
printf '%s' "$B64" | sha256sum | cut -d' ' -f1 > ~/.cmsend-key
if [ -n "$HIT" ]; then
openssl enc -d -aes-256-cbc -pbkdf2 -pass file:$HOME/.cmsend-key \
-in ~/cmsend-profile.enc | tar -C ~/.config/cmsend -xzf -
else
echo "::warning::cmsend cache cold, seeding from the secret"
printf '%s' "$B64" | base64 -d | tar -C ~/.config/cmsend -xzf -
for s in ~/.config/cmsend/*/dc.sql; do
d=$(dirname "$s"); sqlite3 "$d/dc.db" < "$s"
mkdir -p "$d/dc.db-blobs"; rm -f "$s"
done
fi

- name: Fetch
run: uvx --from "$CMSEND_SPEC" cmsend --fetch-only

- name: Report membership changes
env:
ALERT_TAG: ${{ inputs.alert-tag }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
new=~/.config/cmsend/roster.new
uvx --from "$CMSEND_SPEC" cmsend -l | sort > $new
old=~/.config/cmsend/roster.txt
if [ -f "$old" ] && ! diff -q "$old" "$new" > /dev/null; then
{ printf '%s cmsend member list changed:\n' "$REPO"
diff "$old" "$new" || true
} | uvx --from "$CMSEND_SPEC" cmsend -t "$ALERT_TAG"
fi
cp "$new" "$old"

- name: Seal profile
run: |
set -euo pipefail
tar -C ~/.config/cmsend -czf - . |
openssl enc -aes-256-cbc -pbkdf2 -salt -pass file:$HOME/.cmsend-key \
-out ~/cmsend-profile.enc

- uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: ~/cmsend-profile.enc
key: cmsend-profile-${{ github.run_id }}

- name: Scrub the profile
if: always()
run: rm -rf ~/.config/cmsend ~/.cmsend-key ~/cmsend-profile.enc
79 changes: 79 additions & 0 deletions doc/cmsend-notify/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# cmsend-notify

Reusable Actions workflows that report run results into a Delta Chat
group via the [`cmsend`](https://github.com/chatmail/cmsend) CLI:

- `cmsend-notify.yml` messages the alert group on state **change**, needs to be paired with
- `cmsend-sync.yml`, which runs on an interval to keep account alive and synced
- `cmsend-test` allows to test the workflow

## Setup

Two steps:
- Setup the cmsend profile & save it as action secret
- Add Workflows

### One-time account setup

1. In a Delta Chat client of your choice, pick or create (and send an initial
message) the group to alert, and copy the invite link.
2. Create the bot account, name it, and join the group (feature branch right now):
```
export CMSEND_SPEC="git+https://github.com/chatmail/cmsend@ci-sync-and-name"
uvx --from "$CMSEND_SPEC" --init <relay-domain> --shared --name "CI Bot"
uvx --from "$CMSEND_SPEC" -t LOG --join "<invite link>"
```
3. Test delivery:
```
echo "alert test" | uv run cmsend -t LOG
```
4. Pack a dump of the cmsend account with `scripts/pack-cmsend-profile.sh`
```
scripts/pack-cmsend-profile.sh pack ~/.config/cmsend | tee /tmp/secret.b64 | wc -c # must be < 65536
```
To verify the export works:
```
scripts/pack-cmsend-profile.sh unpack /tmp/rt/cmsend < /tmp/secret.b64
XDG_CONFIG_HOME=/tmp/rt uvx --from \
"git+https://github.com/chatmail/cmsend@ci-sync-and-name" cmsend -l # must list LOG
```
5. Finally, add the output as a repository secret named `CMSEND_ACCOUNT_TGZ_B64`

On initial run, this will be unpacked to seed a cmsend profile, which will carry across action runs in an cache file encrypted with `CMSEND_ACCOUNT_TGZ_B64` as a secret.

### Add the workflows:

### In the workflow you want to be notified about

add after the job to watch:
```yaml
notify:
needs: <watched-job>
if: always()
permissions:
actions: read # required to read the previous run's conclusion
uses: chatmail/workflows/.github/workflows/cmsend-notify.yml@cmsend-notify
with:
result: ${{ needs.<watched-job>.result }}
secrets:
cmsend-account: ${{ secrets.CMSEND_ACCOUNT_TGZ_B64 }}
```

Be sure to replace both instances of <watched-job> with your job name.

### Add the sync workflow, default is nightly

`cp doc/cmsend-notify/cmsend-nightly.yml <project>/.github/workflows/`

The nightly job updates and saves an encrypted copy of the profile.

### Test action

You can also copy `notify-test.yml` which allows to run PASS/FAIL test notifies
against this repo's notify setup from the actions Web UI. If you dispatch it
twice with `force_fail=true`, then `false` should give you FAILED, then
RECOVERED. Dispatching the same result twice should not alert.

## Notes

To point at a different group, `--join` it locally again and redo step 4 + 5.
12 changes: 12 additions & 0 deletions doc/cmsend-notify/cmsend-nightly.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: cmsend-nightly
# cmsend-notify: run the nightly account sync
on:
schedule:
- cron: "43 4 * * *"
workflow_dispatch:
permissions: {}
jobs:
sync:
uses: chatmail/workflows/.github/workflows/cmsend-sync.yml@cmsend-notify
secrets:
cmsend-account: ${{ secrets.CMSEND_ACCOUNT_TGZ_B64 }}
25 changes: 25 additions & 0 deletions doc/cmsend-notify/cmsend-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: notify-test
on:
workflow_dispatch:
inputs:
force_fail: { type: boolean, default: false }
permissions: {}
jobs:
work:
runs-on: ubuntu-latest
permissions: {}
steps:
- if: ${{ inputs.force_fail }}
run: exit 1
- if: ${{ !inputs.force_fail }}
run: exit 0
notify:
needs: work
if: always()
permissions:
actions: read # let the reusable workflow read the previous run's result
uses: chatmail/workflows/.github/workflows/cmsend-notify.yml@cmsend-notify
with:
result: ${{ needs.work.result }}
secrets:
cmsend-account: ${{ secrets.CMSEND_ACCOUNT_TGZ_B64 }}
24 changes: 24 additions & 0 deletions scripts/pack-cmsend-profile.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# Pack/unpack a cmsend account as a small sqlite-dump + accounts.toml so it fits a 64KiB
# GitHub secret, which the naive compressed .config/cmsend directory does not.

set -euo pipefail
case "${1:-}" in
pack) # pack <cmsend-dir> -> base64 seed on stdout
cfg=${2:?cmsend dir}; w=$(mktemp -d)
cp "$cfg/accounts.toml" "$w/"
for db in "$cfg"/*/dc.db; do
a=$(basename "$(dirname "$db")"); mkdir -p "$w/$a"
sqlite3 "$db" ".dump" > "$w/$a/dc.sql"
done
tar -C "$w" -czf - . | base64 -w0; rm -rf "$w" ;;

unpack) # unpack <cmsend-dir> (base64 seed on stdin)
cfg=${2:?cmsend dir}; mkdir -p "$cfg"
base64 -d | tar -C "$cfg" -xzf -
for s in "$cfg"/*/dc.sql; do
d=$(dirname "$s"); sqlite3 "$d/dc.db" < "$s"; mkdir -p "$d/dc.db-blobs"; rm -f "$s"
done ;;

*) echo "usage: $0 pack|unpack <cmsend-dir>" >&2; exit 2 ;;
esac