From 1ce809d6cdf82a6823eb9ed093692355ff721a13 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Sun, 29 Jun 2025 18:39:19 -0400 Subject: [PATCH 01/52] update py webhook action to use a channel-specific url; add debug logging to self-tests --- .github/workflows/zhook.yml | 28 ++++++++++++++--------- Dockerfile | 2 +- action.yml | 30 ++++++++++++------------- zhook.py | 45 +++++++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 26 deletions(-) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index 95a9c38..ef007da 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -20,32 +20,40 @@ jobs: runs-on: ubuntu-latest name: POST Webhook steps: - - uses: actions/checkout@v4 - - name: run hook directly + - name: Checkout + uses: actions/checkout@v4 + + - name: Run Python Script Directly if: | github.repository_owner == 'openziti' && ((github.event_name != 'pull_request_review') || (github.event_name == 'pull_request_review' && github.event.review.state == 'approved')) env: INPUT_ZITIID: ${{ secrets.ZITI_MATTERMOST_IDENTITY }} - INPUT_WEBHOOKURL: ${{ secrets.ZHOOK_URL }} + INPUT_WEBHOOKURL: ${{ secrets.ZHOOK_URL_DEV_NOTIFICATIONS }} INPUT_EVENTJSON: ${{ toJson(github.event) }} INPUT_SENDERUSERNAME: GitHubZ INPUT_DESTCHANNEL: dev-notifications INPUT_SENDERICONURL: https://github.com/fluidicon.png + ZITI_LOG: 6 + TLSUV_DEBUG: 6 + shell: bash run: | - pip install --upgrade requests openziti + set -o pipefail + set -o xtrace + pip install --user --upgrade requests openziti==1.2.0 + echo "DEBUG: PYTHONPATH=${PYTHONPATH:-}" python ./zhook.py - - - uses: ./ # use self to bring the pain forward - name: run action + + - uses: ./ + name: Run as a GH Action from the Local Checkout if: | github.repository_owner == 'openziti' && ((github.event_name != 'pull_request_review') || (github.event_name == 'pull_request_review' && github.event.review.state == 'approved')) with: zitiId: ${{ secrets.ZITI_MATTERMOST_IDENTITY }} - webhookUrl: ${{ secrets.ZHOOK_URL }} + webhookUrl: ${{ secrets.ZHOOK_URL_DEV_NOTIFICATIONS }} eventJson: ${{ toJson(github.event) }} - senderUsername: "GitHubZ" - destChannel: "dev-notifications" + senderUsername: GitHubZ + destChannel: dev-notifications diff --git a/Dockerfile b/Dockerfile index 64f6e94..65527c6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM python:3-slim AS builder -RUN pip install --target=/app requests openziti +RUN pip install --target=/app requests openziti==1.2.0 # https://github.com/GoogleContainerTools/distroless FROM gcr.io/distroless/python3-debian12 diff --git a/action.yml b/action.yml index 4930fd8..9a17a47 100644 --- a/action.yml +++ b/action.yml @@ -1,30 +1,30 @@ -name: 'Ziti Mattermost Action - Python' -description: 'POST to Mattermost Webhook endpoint over a Ziti network' +name: Ziti Mattermost Action - Python +description: POST to Mattermost Webhook endpoint over a Ziti network branding: - icon: 'zap' - color: 'red' + icon: zap + color: red inputs: zitiId: - description: 'Identity JSON for an enrolled Ziti endpoint' + description: Identity JSON for an enrolled Ziti endpoint required: true webhookUrl: - description: 'URL for posting the payload' + description: URL for posting the payload required: true eventJson: - description: 'GitHub event JSON (github.event)' + description: GitHub event JSON (github.event) required: true senderUsername: - description: 'Mattermost username' + description: Mattermost username required: false - default: "GithubZ" + default: GithubZ senderIconUrl: - description: 'Mattermost user icon URL' + description: Mattermost user icon URL required: false - default: "https://github.com/fluidicon.png" + default: https://github.com/fluidicon.png destChannel: - description: 'Mattermost channel' + description: Mattermost channel required: false - default: "dev-notifications" + default: dev-notifications runs: - using: "docker" - image: "Dockerfile" + using: docker + image: Dockerfile diff --git a/zhook.py b/zhook.py index 44bc9bd..cf138d4 100644 --- a/zhook.py +++ b/zhook.py @@ -369,10 +369,55 @@ def dumpJson(self): print("ERROR: no Ziti identity provided, set INPUT_ZITIID or INPUT_ZITIJWT") exit(1) + def generate_json_schema(obj, max_depth=10, current_depth=0): + """Generate a schema representation of a JSON object by inferring types from values.""" + if current_depth >= max_depth: + return "" + + if obj is None: + return "null" + elif isinstance(obj, bool): + return "boolean" + elif isinstance(obj, int): + return "integer" + elif isinstance(obj, float): + return "number" + elif isinstance(obj, str): + return "string" + elif isinstance(obj, list): + if len(obj) == 0: + return "array[]" + # Get schema of first element as representative + element_schema = generate_json_schema(obj[0], max_depth, current_depth + 1) + return f"array[{element_schema}]" + elif isinstance(obj, dict): + schema = {} + for key, value in obj.items(): + schema[key] = generate_json_schema(value, max_depth, current_depth + 1) + return schema + else: + return f"unknown_type({type(obj).__name__})" + + # Validate zitiId as JSON + try: + zitiIdJson = json.loads(zitiId) + except Exception as e: + print(f"ERROR: zitiId is not valid JSON: {e}") + print(f"zitiId content: {zitiId}") + exit(1) + idFilename = "id.json" with open(idFilename, 'w') as f: f.write(zitiId) + + # Load the identity file after it's been written and closed + try: openziti.load(idFilename) + except Exception as e: + print(f"ERROR: Failed to load Ziti identity: {e}") + schema = generate_json_schema(zitiIdJson) + print(f"DEBUG: zitiId schema for troubleshooting: {json.dumps(schema, indent=2)}") + raise e # Create webhook body try: From 9023ee82aa87ac90dc354bfc8e8af012787b7e4d Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Wed, 23 Jul 2025 19:50:45 -0400 Subject: [PATCH 02/52] stop pinning py sdk version so we can always test latest --- .github/workflows/zhook.yml | 2 +- Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index ef007da..81c61ba 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -41,7 +41,7 @@ jobs: run: | set -o pipefail set -o xtrace - pip install --user --upgrade requests openziti==1.2.0 + pip install --user --upgrade requests openziti echo "DEBUG: PYTHONPATH=${PYTHONPATH:-}" python ./zhook.py diff --git a/Dockerfile b/Dockerfile index 65527c6..64f6e94 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM python:3-slim AS builder -RUN pip install --target=/app requests openziti==1.2.0 +RUN pip install --target=/app requests openziti # https://github.com/GoogleContainerTools/distroless FROM gcr.io/distroless/python3-debian12 From 562796e7cb2a25d698d6e90a966eb0820ab76ec9 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Wed, 23 Jul 2025 20:21:30 -0400 Subject: [PATCH 03/52] deprecate channel input because incoming webhooks are now locked to a channel by policy --- .github/workflows/zhook.yml | 2 -- README.md | 1 - action.yml | 6 +++--- zhook.py | 7 ++----- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index 81c61ba..81a01e3 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -33,7 +33,6 @@ jobs: INPUT_WEBHOOKURL: ${{ secrets.ZHOOK_URL_DEV_NOTIFICATIONS }} INPUT_EVENTJSON: ${{ toJson(github.event) }} INPUT_SENDERUSERNAME: GitHubZ - INPUT_DESTCHANNEL: dev-notifications INPUT_SENDERICONURL: https://github.com/fluidicon.png ZITI_LOG: 6 TLSUV_DEBUG: 6 @@ -56,4 +55,3 @@ jobs: webhookUrl: ${{ secrets.ZHOOK_URL_DEV_NOTIFICATIONS }} eventJson: ${{ toJson(github.event) }} senderUsername: GitHubZ - destChannel: dev-notifications diff --git a/README.md b/README.md index cf12ece..11305bc 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,6 @@ jobs: eventJson: ${{ toJson(github.event) }} senderUsername: "GitHubZ" - destChannel: "github-notifications" ``` ### Inputs diff --git a/action.yml b/action.yml index 9a17a47..9096fff 100644 --- a/action.yml +++ b/action.yml @@ -8,7 +8,7 @@ inputs: description: Identity JSON for an enrolled Ziti endpoint required: true webhookUrl: - description: URL for posting the payload + description: Mattermost-channel-specific URL for posting the event required: true eventJson: description: GitHub event JSON (github.event) @@ -22,9 +22,9 @@ inputs: required: false default: https://github.com/fluidicon.png destChannel: - description: Mattermost channel + description: Mattermost channel (deprecated because incoming webhooks are now locked to a channel) required: false - default: dev-notifications + default: null runs: using: docker image: Dockerfile diff --git a/zhook.py b/zhook.py index cf138d4..1c5f0b3 100644 --- a/zhook.py +++ b/zhook.py @@ -20,10 +20,9 @@ class MattermostWebhookBody: todoColor = "#FFFFFF" watchColor = "#FFD700" - def __init__(self, username, icon, channel, eventName, eventJsonStr, actionRepo): + def __init__(self, username, icon, eventName, eventJsonStr, actionRepo): self.username = username self.icon = icon - self.channel = channel self.eventName = eventName.lower() self.eventJsonStr = eventJsonStr self.actionRepo = actionRepo @@ -36,7 +35,6 @@ def __init__(self, username, icon, channel, eventName, eventJsonStr, actionRepo) # "icon_url": self.icon, "username": self.senderJson['login'], "icon_url": self.senderJson['avatar_url'], - "channel": self.channel, "props": {"card": f"```json\n{self.eventJsonStr}\n```"}, } @@ -354,7 +352,6 @@ def dumpJson(self): eventJsonStr = os.getenv("INPUT_EVENTJSON") username = os.getenv("INPUT_SENDERUSERNAME") icon = os.getenv("INPUT_SENDERICONURL") - channel = os.getenv("INPUT_DESTCHANNEL") actionRepo = os.getenv("GITHUB_ACTION_REPOSITORY") eventName = os.getenv("GITHUB_EVENT_NAME") @@ -421,7 +418,7 @@ def generate_json_schema(obj, max_depth=10, current_depth=0): # Create webhook body try: - mwb = MattermostWebhookBody(username, icon, channel, eventName, eventJsonStr, actionRepo) + mwb = MattermostWebhookBody(username, icon, eventName, eventJsonStr, actionRepo) except Exception as e: print(f"Exception creating webhook body: {e}") raise e From a6b6c32ef89e4c9c50b4140e8ed67714dcb8073b Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Thu, 24 Jul 2025 15:53:33 -0400 Subject: [PATCH 04/52] print env and event --- .github/workflows/zhook.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index 81a01e3..dd54ec9 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -20,6 +20,9 @@ jobs: runs-on: ubuntu-latest name: POST Webhook steps: + - name: Debug + uses: hmarr/debug-action@v3 + - name: Checkout uses: actions/checkout@v4 From f31c36b227399a965b9b7ce35eab21ca35e5d431 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Thu, 24 Jul 2025 16:35:49 -0400 Subject: [PATCH 05/52] aggressively debug inconsistent segfault --- .github/workflows/zhook.yml | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index dd54ec9..2044afc 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -17,12 +17,16 @@ on: jobs: mattermost-ziti-webhook: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 name: POST Webhook steps: - name: Debug uses: hmarr/debug-action@v3 + - name: Install Valgrind + shell: bash + run: sudo apt-get install --yes valgrind gdb + - name: Checkout uses: actions/checkout@v4 @@ -43,9 +47,10 @@ jobs: run: | set -o pipefail set -o xtrace - pip install --user --upgrade requests openziti - echo "DEBUG: PYTHONPATH=${PYTHONPATH:-}" - python ./zhook.py + pip install --user --upgrade --requirement ./requirements.txt + sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p.%t' + ulimit -c unlimited + valgrind --verbose --log-file=/tmp/zook.py-valgrind-%p-%n.log --leak-check=full python ./zhook.py - uses: ./ name: Run as a GH Action from the Local Checkout @@ -58,3 +63,19 @@ jobs: webhookUrl: ${{ secrets.ZHOOK_URL_DEV_NOTIFICATIONS }} eventJson: ${{ toJson(github.event) }} senderUsername: GitHubZ + + - name: Print Debug Info + if: always() + shell: bash + run: | + set -o pipefail + set -o xtrace + echo "DEBUG: PYTHONPATH=${PYTHONPATH:-}" + echo "DEBUG: PATH=${PATH:-}" + echo "DEBUG: LD_LIBRARY_PATH=${LD_LIBRARY_PATH:-}" + find $(python -c "import site; print(site.USER_SITE)") -path "*/openziti*" -name "*.so*" -type f -print0 | xargs -0r ldd + cat /tmp/zook.py-valgrind-*.log + if [ -s /tmp/core.python.* ]; then + echo "DEBUG: Core dump found" + gdb -q $(realpath $(which python)) -c /tmp/core.python.* --ex bt --ex exit + fi \ No newline at end of file From 21f81b2db8ceedd47360d2f3e2465f8ccc5df29c Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Thu, 24 Jul 2025 16:37:41 -0400 Subject: [PATCH 06/52] add py requirements file --- .github/workflows/zhook.yml | 4 ++-- requirements.txt | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 requirements.txt diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index 2044afc..b4e78b2 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -20,10 +20,10 @@ jobs: runs-on: ubuntu-24.04 name: POST Webhook steps: - - name: Debug + - name: Debug Environment uses: hmarr/debug-action@v3 - - name: Install Valgrind + - name: Install Debug Tools shell: bash run: sudo apt-get install --yes valgrind gdb diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4f42d9f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests +openziti \ No newline at end of file From 1bad8a94ac0b7d58d2d64c545b755ac4f94c4626 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Thu, 24 Jul 2025 16:38:41 -0400 Subject: [PATCH 07/52] don't raise bash exceptions while printing debug info --- .github/workflows/zhook.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index b4e78b2..695cede 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -70,6 +70,7 @@ jobs: run: | set -o pipefail set -o xtrace + set +o errexit echo "DEBUG: PYTHONPATH=${PYTHONPATH:-}" echo "DEBUG: PATH=${PATH:-}" echo "DEBUG: LD_LIBRARY_PATH=${LD_LIBRARY_PATH:-}" From 831b9f8ed8f92426682b3e2bfc17cddff5e505e0 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Thu, 24 Jul 2025 16:51:49 -0400 Subject: [PATCH 08/52] stop setting pipefail --- .github/workflows/zhook.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index 695cede..76a53d6 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -68,7 +68,6 @@ jobs: if: always() shell: bash run: | - set -o pipefail set -o xtrace set +o errexit echo "DEBUG: PYTHONPATH=${PYTHONPATH:-}" From e7e96937a33983ea02ca3ad6cdc4a504874d6882 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Thu, 24 Jul 2025 17:29:37 -0400 Subject: [PATCH 09/52] also debug docker action --- .github/workflows/zhook.yml | 42 ++++++++++++++++++++++++++++++------- Dockerfile | 8 +++---- action.yml | 6 +++++- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index 76a53d6..33f5c52 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -41,17 +41,41 @@ jobs: INPUT_EVENTJSON: ${{ toJson(github.event) }} INPUT_SENDERUSERNAME: GitHubZ INPUT_SENDERICONURL: https://github.com/fluidicon.png - ZITI_LOG: 6 - TLSUV_DEBUG: 6 + INPUT_ZITILOGLEVEL: 6 shell: bash run: | set -o pipefail set -o xtrace pip install --user --upgrade --requirement ./requirements.txt - sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p.%t' + sudo mkdir -m0777 ${{ github.workspace }}/cores + sudo sysctl -w kernel.core_pattern='${{ github.workspace }}/cores/core.%e.%p.%t' ulimit -c unlimited valgrind --verbose --log-file=/tmp/zook.py-valgrind-%p-%n.log --leak-check=full python ./zhook.py + - name: Run as Docker Action with Core Dumps + if: | + github.repository_owner == 'openziti' + && ((github.event_name != 'pull_request_review') + || (github.event_name == 'pull_request_review' && github.event.review.state == 'approved')) + shell: bash + run: | + set -o pipefail + set -o xtrace + docker build -t zhook-action . + docker run --rm \ + -v "${{ github.workspace }}:${{ github.workspace }}" \ + -w "${{ github.workspace }}" \ + -e INPUT_ZITIID="${{ secrets.ZITI_MATTERMOST_IDENTITY }}" \ + -e INPUT_WEBHOOKURL="${{ secrets.ZHOOK_URL_DEV_NOTIFICATIONS }}" \ + -e INPUT_EVENTJSON='${{ toJson(github.event) }}' \ + -e INPUT_SENDERUSERNAME="GitHubZ" \ + -e INPUT_ZITILOGLEVEL="6" \ + --entrypoint /bin/sh \ + zhook-action -xc " + ulimit -c unlimited + exec /app/zhook.py + " + - uses: ./ name: Run as a GH Action from the Local Checkout if: | @@ -63,6 +87,7 @@ jobs: webhookUrl: ${{ secrets.ZHOOK_URL_DEV_NOTIFICATIONS }} eventJson: ${{ toJson(github.event) }} senderUsername: GitHubZ + zitiLogLevel: 6 - name: Print Debug Info if: always() @@ -75,7 +100,10 @@ jobs: echo "DEBUG: LD_LIBRARY_PATH=${LD_LIBRARY_PATH:-}" find $(python -c "import site; print(site.USER_SITE)") -path "*/openziti*" -name "*.so*" -type f -print0 | xargs -0r ldd cat /tmp/zook.py-valgrind-*.log - if [ -s /tmp/core.python.* ]; then - echo "DEBUG: Core dump found" - gdb -q $(realpath $(which python)) -c /tmp/core.python.* --ex bt --ex exit - fi \ No newline at end of file + for CORE in ${{ github.workspace }}/cores/core.*; do + if [ -s "$CORE" ]; then + echo "DEBUG: Core dump found: $CORE" + EXECUTABLE=$(basename "$CORE" | cut -d. -f2) + gdb -q $(realpath $(which "$EXECUTABLE")) -c "$CORE" --ex bt --ex exit + fi + done \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 64f6e94..21ff2b6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,13 @@ FROM python:3-slim AS builder -RUN pip install --target=/app requests openziti +COPY requirements.txt /tmp/requirements.txt +RUN pip install --target=/app --requirement /tmp/requirements.txt # https://github.com/GoogleContainerTools/distroless FROM gcr.io/distroless/python3-debian12 COPY --from=builder /app /app -COPY ./zhook.py /app/zhook.py +COPY --chmod=0755 ./zhook.py /app/zhook.py WORKDIR /app ENV PYTHONPATH=/app -ENV ZITI_LOG=6 -ENV TLSUV_DEBUG=6 + CMD ["/app/zhook.py"] diff --git a/action.yml b/action.yml index 9096fff..536ab34 100644 --- a/action.yml +++ b/action.yml @@ -22,9 +22,13 @@ inputs: required: false default: https://github.com/fluidicon.png destChannel: - description: Mattermost channel (deprecated because incoming webhooks are now locked to a channel) + description: Mattermost channel (ignored because incoming webhooks are locked to a channel) required: false default: null + zitiLogLevel: + description: Ziti log level + required: false + default: 3 runs: using: docker image: Dockerfile From 7258c2f2e9d0a524030c937e353501b4e8a992bd Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Thu, 24 Jul 2025 17:33:55 -0400 Subject: [PATCH 10/52] fiddle escapes --- .github/workflows/zhook.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index 33f5c52..55289fe 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -52,7 +52,7 @@ jobs: ulimit -c unlimited valgrind --verbose --log-file=/tmp/zook.py-valgrind-%p-%n.log --leak-check=full python ./zhook.py - - name: Run as Docker Action with Core Dumps + - name: Run in Docker with Core Dumps if: | github.repository_owner == 'openziti' && ((github.event_name != 'pull_request_review') @@ -65,11 +65,11 @@ jobs: docker run --rm \ -v "${{ github.workspace }}:${{ github.workspace }}" \ -w "${{ github.workspace }}" \ - -e INPUT_ZITIID="${{ secrets.ZITI_MATTERMOST_IDENTITY }}" \ - -e INPUT_WEBHOOKURL="${{ secrets.ZHOOK_URL_DEV_NOTIFICATIONS }}" \ - -e INPUT_EVENTJSON='${{ toJson(github.event) }}' \ - -e INPUT_SENDERUSERNAME="GitHubZ" \ - -e INPUT_ZITILOGLEVEL="6" \ + -e "INPUT_ZITIID=${{ secrets.ZITI_MATTERMOST_IDENTITY }}" \ + -e "INPUT_WEBHOOKURL=${{ secrets.ZHOOK_URL_DEV_NOTIFICATIONS }}" \ + -e "INPUT_EVENTJSON=${{ toJson(github.event) }}" \ + -e "INPUT_SENDERUSERNAME=GitHubZ" \ + -e "INPUT_ZITILOGLEVEL=6" \ --entrypoint /bin/sh \ zhook-action -xc " ulimit -c unlimited From 7a7b0874cc0ee258caaa08c36ea1ea60841dc1c2 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Thu, 24 Jul 2025 17:38:07 -0400 Subject: [PATCH 11/52] use env file --- .github/workflows/zhook.yml | 24 +++++++++++++++++++----- zhook.py | 4 ++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index 55289fe..377abb4 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -58,18 +58,32 @@ jobs: && ((github.event_name != 'pull_request_review') || (github.event_name == 'pull_request_review' && github.event.review.state == 'approved')) shell: bash + env: + INPUT_ZITIID: ${{ secrets.ZITI_MATTERMOST_IDENTITY }} + INPUT_WEBHOOKURL: ${{ secrets.ZHOOK_URL_DEV_NOTIFICATIONS }} + INPUT_EVENTJSON: ${{ toJson(github.event) }} + INPUT_SENDERUSERNAME: GitHubZ + INPUT_SENDERICONURL: https://github.com/fluidicon.png + INPUT_ZITILOGLEVEL: 6 run: | set -o pipefail set -o xtrace + + # Create environment file for Docker + cat > /tmp/docker.env << 'EOF' + INPUT_ZITIID=${INPUT_ZITIID} + INPUT_WEBHOOKURL=${INPUT_WEBHOOKURL} + INPUT_EVENTJSON=${INPUT_EVENTJSON} + INPUT_SENDERUSERNAME=${INPUT_SENDERUSERNAME} + INPUT_SENDERICONURL=${INPUT_SENDERICONURL} + INPUT_ZITILOGLEVEL=${INPUT_ZITILOGLEVEL} + EOF + docker build -t zhook-action . docker run --rm \ -v "${{ github.workspace }}:${{ github.workspace }}" \ -w "${{ github.workspace }}" \ - -e "INPUT_ZITIID=${{ secrets.ZITI_MATTERMOST_IDENTITY }}" \ - -e "INPUT_WEBHOOKURL=${{ secrets.ZHOOK_URL_DEV_NOTIFICATIONS }}" \ - -e "INPUT_EVENTJSON=${{ toJson(github.event) }}" \ - -e "INPUT_SENDERUSERNAME=GitHubZ" \ - -e "INPUT_ZITILOGLEVEL=6" \ + --env-file /tmp/docker.env \ --entrypoint /bin/sh \ zhook-action -xc " ulimit -c unlimited diff --git a/zhook.py b/zhook.py index 1c5f0b3..3d22928 100644 --- a/zhook.py +++ b/zhook.py @@ -354,6 +354,10 @@ def dumpJson(self): icon = os.getenv("INPUT_SENDERICONURL") actionRepo = os.getenv("GITHUB_ACTION_REPOSITORY") eventName = os.getenv("GITHUB_EVENT_NAME") + zitiLogLevel = os.getenv("INPUT_ZITILOGLEVEL") + if zitiLogLevel is not None: + os.environ["ZITI_LOG"] = zitiLogLevel + os.environ["TLSUV_DEBUG"] = zitiLogLevel # Setup Ziti identity zitiJwt = os.getenv("INPUT_ZITIJWT") From 91faae05b2a30b9aadb32f2bb234ceeb28081976 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Thu, 24 Jul 2025 17:45:27 -0400 Subject: [PATCH 12/52] debug in bash container --- .github/workflows/zhook.yml | 6 ++++-- debug.Dockerfile | 7 +++++++ 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 debug.Dockerfile diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index 377abb4..b91cd93 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -80,12 +80,13 @@ jobs: EOF docker build -t zhook-action . + docker build -t zhook-action-dbg -f debug.Dockerfile . docker run --rm \ -v "${{ github.workspace }}:${{ github.workspace }}" \ -w "${{ github.workspace }}" \ --env-file /tmp/docker.env \ - --entrypoint /bin/sh \ - zhook-action -xc " + --entrypoint /bin/bash \ + zhook-action-dbg -xc " ulimit -c unlimited exec /app/zhook.py " @@ -101,6 +102,7 @@ jobs: webhookUrl: ${{ secrets.ZHOOK_URL_DEV_NOTIFICATIONS }} eventJson: ${{ toJson(github.event) }} senderUsername: GitHubZ + senderIconUrl: https://github.com/fluidicon.png zitiLogLevel: 6 - name: Print Debug Info diff --git a/debug.Dockerfile b/debug.Dockerfile new file mode 100644 index 0000000..2c9d992 --- /dev/null +++ b/debug.Dockerfile @@ -0,0 +1,7 @@ +FROM zhook-action AS distroless + +FROM python:3-slim AS debug + +COPY --from=distroless /app /app + +RUN apt-get update && apt-get install --yes bash From b3311caf55cdf43693732399e89ed22adf6b4824 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Thu, 24 Jul 2025 17:56:48 -0400 Subject: [PATCH 13/52] beef up backtrace script --- .github/workflows/zhook.yml | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index b91cd93..ea119fa 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -69,7 +69,6 @@ jobs: set -o pipefail set -o xtrace - # Create environment file for Docker cat > /tmp/docker.env << 'EOF' INPUT_ZITIID=${INPUT_ZITIID} INPUT_WEBHOOKURL=${INPUT_WEBHOOKURL} @@ -116,10 +115,17 @@ jobs: echo "DEBUG: LD_LIBRARY_PATH=${LD_LIBRARY_PATH:-}" find $(python -c "import site; print(site.USER_SITE)") -path "*/openziti*" -name "*.so*" -type f -print0 | xargs -0r ldd cat /tmp/zook.py-valgrind-*.log - for CORE in ${{ github.workspace }}/cores/core.*; do - if [ -s "$CORE" ]; then - echo "DEBUG: Core dump found: $CORE" - EXECUTABLE=$(basename "$CORE" | cut -d. -f2) - gdb -q $(realpath $(which "$EXECUTABLE")) -c "$CORE" --ex bt --ex exit - fi - done \ No newline at end of file + shopt -s nullglob + typeset -a CORES=(${{ github.workspace }}/cores/core.*) + shopt -u nullglob + if [ ${#CORES[@]} -eq 0 ]; then + echo "DEBUG: No core dumps found" + else + for CORE in "${CORES[@]}"; do + if [ -s "$CORE" ]; then + echo "DEBUG: Core dump found: $CORE" + EXECUTABLE=$(basename "$CORE" | cut -d. -f2) + gdb -q $(realpath $(which "$EXECUTABLE")) -c "$CORE" --ex bt --ex exit + fi + done + fi From ee9e398c0c66f56a6ed17a6aa2cb1ad019ffaa2d Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Thu, 24 Jul 2025 17:59:21 -0400 Subject: [PATCH 14/52] add interpreter to zhook.py --- zhook.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zhook.py b/zhook.py index 3d22928..7e0180f 100644 --- a/zhook.py +++ b/zhook.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 + import requests import openziti import json From bc7503fae065472d4998719baf488a784d9e41d8 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Thu, 24 Jul 2025 18:02:31 -0400 Subject: [PATCH 15/52] interpolate vars in heredoc --- .github/workflows/zhook.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index ea119fa..417ae00 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -69,13 +69,13 @@ jobs: set -o pipefail set -o xtrace - cat > /tmp/docker.env << 'EOF' - INPUT_ZITIID=${INPUT_ZITIID} - INPUT_WEBHOOKURL=${INPUT_WEBHOOKURL} - INPUT_EVENTJSON=${INPUT_EVENTJSON} - INPUT_SENDERUSERNAME=${INPUT_SENDERUSERNAME} - INPUT_SENDERICONURL=${INPUT_SENDERICONURL} - INPUT_ZITILOGLEVEL=${INPUT_ZITILOGLEVEL} + cat > /tmp/docker.env << EOF + INPUT_ZITIID="${INPUT_ZITIID}" + INPUT_WEBHOOKURL="${INPUT_WEBHOOKURL}" + INPUT_EVENTJSON="${INPUT_EVENTJSON}" + INPUT_SENDERUSERNAME="${INPUT_SENDERUSERNAME}" + INPUT_SENDERICONURL="${INPUT_SENDERICONURL}" + INPUT_ZITILOGLEVEL="${INPUT_ZITILOGLEVEL}" EOF docker build -t zhook-action . From 5449a48d641900e0dcdf2c6d797f39f4a8b8b705 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Fri, 25 Jul 2025 12:33:19 -0400 Subject: [PATCH 16/52] test the script with valgrind in docker too --- .github/workflows/zhook.yml | 62 +++++++++++++++++++++++++++---------- debug.Dockerfile | 2 +- 2 files changed, 46 insertions(+), 18 deletions(-) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index 417ae00..5fa65bc 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -47,10 +47,12 @@ jobs: set -o pipefail set -o xtrace pip install --user --upgrade --requirement ./requirements.txt - sudo mkdir -m0777 ${{ github.workspace }}/cores - sudo sysctl -w kernel.core_pattern='${{ github.workspace }}/cores/core.%e.%p.%t' - ulimit -c unlimited - valgrind --verbose --log-file=/tmp/zook.py-valgrind-%p-%n.log --leak-check=full python ./zhook.py + # in case valgrind catches a segfault, it will write a core file in ./vgcore.%p + valgrind \ + --verbose \ + --log-file=${GITHUB_WORKSPACE}/direct-valgrind-%p-%n.log \ + --leak-check=yes \ + python ./zhook.py - name: Run in Docker with Core Dumps if: | @@ -76,19 +78,28 @@ jobs: INPUT_SENDERUSERNAME="${INPUT_SENDERUSERNAME}" INPUT_SENDERICONURL="${INPUT_SENDERICONURL}" INPUT_ZITILOGLEVEL="${INPUT_ZITILOGLEVEL}" + GITHUB_WORKSPACE="${GITHUB_WORKSPACE}" EOF + # configure the kernel to write core dumps to the workspace directory that is writable by the container + sudo sysctl -w kernel.core_pattern="${GITHUB_WORKSPACE}/core.%e.%p.%t" + + # build the action's container image so we can source it for the debug image docker build -t zhook-action . docker build -t zhook-action-dbg -f debug.Dockerfile . docker run --rm \ - -v "${{ github.workspace }}:${{ github.workspace }}" \ - -w "${{ github.workspace }}" \ + --volume "${GITHUB_WORKSPACE}:${GITHUB_WORKSPACE}" \ + --workdir "${GITHUB_WORKSPACE}" \ --env-file /tmp/docker.env \ - --entrypoint /bin/bash \ - zhook-action-dbg -xc " - ulimit -c unlimited - exec /app/zhook.py - " + --entrypoint=/bin/bash \ + zhook-action-dbg -euxo pipefail -c ' + ulimit -c unlimited; + exec valgrind \ + --verbose \ + --log-file=${GITHUB_WORKSPACE}/docker-valgrind-%p-%n.log \ + --leak-check=yes \ + python /app/zhook.py; + ' - uses: ./ name: Run as a GH Action from the Local Checkout @@ -113,19 +124,36 @@ jobs: echo "DEBUG: PYTHONPATH=${PYTHONPATH:-}" echo "DEBUG: PATH=${PATH:-}" echo "DEBUG: LD_LIBRARY_PATH=${LD_LIBRARY_PATH:-}" + # list non-git files in the two uppermost levels of the workspace directory hierarchy + find . -maxdepth 2 -path './.git' -prune -o -print find $(python -c "import site; print(site.USER_SITE)") -path "*/openziti*" -name "*.so*" -type f -print0 | xargs -0r ldd - cat /tmp/zook.py-valgrind-*.log + shopt -s nullglob - typeset -a CORES=(${{ github.workspace }}/cores/core.*) - shopt -u nullglob - if [ ${#CORES[@]} -eq 0 ]; then - echo "DEBUG: No core dumps found" + # find valgrind logs from both execution steps + typeset -a VALGRIND_LOGS=(${GITHUB_WORKSPACE}/*-valgrind-*.log) + if (( ${#VALGRIND_LOGS[@]} )); then + for LOG in "${VALGRIND_LOGS[@]}"; do + if [ -s "$LOG" ]; then + echo "DEBUG: Valgrind log: $LOG" + cat "$LOG" + echo "--- End of $(basename "$LOG") ---" + fi + done else + echo "DEBUG: No Valgrind logs found" + fi + + # find core dumps produced by the kernel and valgrind + typeset -a CORES=(${GITHUB_WORKSPACE}/core.* ${GITHUB_WORKSPACE}/vgcore.*) + shopt -u nullglob + if (( ${#CORES[@]} )); then for CORE in "${CORES[@]}"; do if [ -s "$CORE" ]; then - echo "DEBUG: Core dump found: $CORE" + echo "DEBUG: Core dump: $CORE" EXECUTABLE=$(basename "$CORE" | cut -d. -f2) gdb -q $(realpath $(which "$EXECUTABLE")) -c "$CORE" --ex bt --ex exit fi done + else + echo "DEBUG: No core dumps found" fi diff --git a/debug.Dockerfile b/debug.Dockerfile index 2c9d992..3e7bdf2 100644 --- a/debug.Dockerfile +++ b/debug.Dockerfile @@ -4,4 +4,4 @@ FROM python:3-slim AS debug COPY --from=distroless /app /app -RUN apt-get update && apt-get install --yes bash +RUN apt-get update && apt-get install -y valgrind From d4815756d9253772861e8ed79f34cebba16a5ce3 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Fri, 25 Jul 2025 13:19:18 -0400 Subject: [PATCH 17/52] pass json as an encoding to avoid word-splitting errors --- .github/workflows/zhook.yml | 19 +++++++++++-------- zhook.py | 13 ++++++++++++- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index 5fa65bc..de0b9d0 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -71,17 +71,20 @@ jobs: set -o pipefail set -o xtrace + # Base64 encode JSON to avoid whitespace issues in env file + ENCODED_JSON=$(echo "${INPUT_EVENTJSON}" | base64 -w 0) + cat > /tmp/docker.env << EOF - INPUT_ZITIID="${INPUT_ZITIID}" - INPUT_WEBHOOKURL="${INPUT_WEBHOOKURL}" - INPUT_EVENTJSON="${INPUT_EVENTJSON}" - INPUT_SENDERUSERNAME="${INPUT_SENDERUSERNAME}" - INPUT_SENDERICONURL="${INPUT_SENDERICONURL}" - INPUT_ZITILOGLEVEL="${INPUT_ZITILOGLEVEL}" - GITHUB_WORKSPACE="${GITHUB_WORKSPACE}" + INPUT_ZITIID=${INPUT_ZITIID} + INPUT_WEBHOOKURL=${INPUT_WEBHOOKURL} + INPUT_EVENTJSON_B64=${ENCODED_JSON} + INPUT_SENDERUSERNAME=${INPUT_SENDERUSERNAME} + INPUT_SENDERICONURL=${INPUT_SENDERICONURL} + INPUT_ZITILOGLEVEL=${INPUT_ZITILOGLEVEL} + GITHUB_WORKSPACE=${GITHUB_WORKSPACE} EOF - # configure the kernel to write core dumps to the workspace directory that is writable by the container + # configure the kernel to write core dumps to the workspace directory that is writable by the container in case there is a segfault valgrind cannot catch in a vgcore.%p sudo sysctl -w kernel.core_pattern="${GITHUB_WORKSPACE}/core.%e.%p.%t" # build the action's container image so we can source it for the debug image diff --git a/zhook.py b/zhook.py index 7e0180f..d9d54ed 100644 --- a/zhook.py +++ b/zhook.py @@ -4,6 +4,7 @@ import openziti import json import os +import base64 class MattermostWebhookBody: @@ -351,7 +352,17 @@ def dumpJson(self): if __name__ == '__main__': url = os.getenv("INPUT_WEBHOOKURL") - eventJsonStr = os.getenv("INPUT_EVENTJSON") + + # Handle both base64-encoded and direct JSON input + eventJsonB64 = os.getenv("INPUT_EVENTJSON_B64") + if eventJsonB64: + try: + eventJsonStr = base64.b64decode(eventJsonB64).decode('utf-8') + except Exception as e: + print(f"Failed to decode base64 event JSON: {e}") + eventJsonStr = os.getenv("INPUT_EVENTJSON") + else: + eventJsonStr = os.getenv("INPUT_EVENTJSON") username = os.getenv("INPUT_SENDERUSERNAME") icon = os.getenv("INPUT_SENDERICONURL") actionRepo = os.getenv("GITHUB_ACTION_REPOSITORY") From d726aa19603278df39b4180a124ab797dfa17371 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Fri, 25 Jul 2025 13:33:46 -0400 Subject: [PATCH 18/52] add required vars to the docker env --- .github/workflows/zhook.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index de0b9d0..b9f4b8b 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -82,6 +82,8 @@ jobs: INPUT_SENDERICONURL=${INPUT_SENDERICONURL} INPUT_ZITILOGLEVEL=${INPUT_ZITILOGLEVEL} GITHUB_WORKSPACE=${GITHUB_WORKSPACE} + GITHUB_EVENT_NAME=${GITHUB_EVENT_NAME} + GITHUB_ACTION_REPOSITORY=${GITHUB_ACTION_REPOSITORY} EOF # configure the kernel to write core dumps to the workspace directory that is writable by the container in case there is a segfault valgrind cannot catch in a vgcore.%p From 791a3ddf25ffde638fe1f6b5955a8c0089bfbc93 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Tue, 29 Jul 2025 14:36:03 -0400 Subject: [PATCH 19/52] tidy whitespace --- zhook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zhook.py b/zhook.py index d9d54ed..e4c3734 100644 --- a/zhook.py +++ b/zhook.py @@ -352,7 +352,7 @@ def dumpJson(self): if __name__ == '__main__': url = os.getenv("INPUT_WEBHOOKURL") - + # Handle both base64-encoded and direct JSON input eventJsonB64 = os.getenv("INPUT_EVENTJSON_B64") if eventJsonB64: From 1c778799fe4d8a358230983504b56ada1b479fca Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Thu, 21 Aug 2025 16:51:55 -0400 Subject: [PATCH 20/52] validate json more robustly and hint at contents if invalid --- zhook.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/zhook.py b/zhook.py index e4c3734..1aa7953 100644 --- a/zhook.py +++ b/zhook.py @@ -412,17 +412,26 @@ def generate_json_schema(obj, max_depth=10, current_depth=0): else: return f"unknown_type({type(obj).__name__})" - # Validate zitiId as JSON + # Accept only inline JSON for zitiId; do not interpret as file path or base64 + def _safe_hint(s): + if s is None: + return "" + l = len(s) + head = s[:8].replace('\n', ' ') + return f"len={l}, startswith='{head}...'" + try: zitiIdJson = json.loads(zitiId) + zitiIdContent = zitiId except Exception as e: - print(f"ERROR: zitiId is not valid JSON: {e}") - print(f"zitiId content: {zitiId}") + print("ERROR: zitiId must be inline JSON (not a file path or base64).") + print(f"DEBUG: INPUT_ZITIID hint: {_safe_hint(zitiId)}") + print(f"DEBUG: json error: {e}") exit(1) idFilename = "id.json" with open(idFilename, 'w') as f: - f.write(zitiId) + f.write(zitiIdContent) # Load the identity file after it's been written and closed try: From 5ea63f20e50bfed2441dba94b500fef74cdd309b Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Thu, 21 Aug 2025 17:53:32 -0400 Subject: [PATCH 21/52] trims redundant quotes --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 11305bc..b012267 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,10 @@ jobs: # URL to post the payload. Note that the `zitiId` must provide access to a service # intercepting `my-mattermost-ziti-server` - webhookUrl: 'https://{my-mattermost-ziti-server}/hook/{my-mattermost-webhook-id}}' + webhookUrl: http://{my-mattermost-ziti-server}/hook/{my-mattermost-webhook-id}} eventJson: ${{ toJson(github.event) }} - senderUsername: "GitHubZ" + senderUsername: GitHubZ ``` ### Inputs From f97e3a2e7bd58ce2a06740beab7397d71699266d Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Thu, 21 Aug 2025 17:54:17 -0400 Subject: [PATCH 22/52] load identity in context manager to shut down cleanly --- zhook.py | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/zhook.py b/zhook.py index 1aa7953..441d8d1 100644 --- a/zhook.py +++ b/zhook.py @@ -433,14 +433,8 @@ def _safe_hint(s): with open(idFilename, 'w') as f: f.write(zitiIdContent) - # Load the identity file after it's been written and closed - try: - openziti.load(idFilename) - except Exception as e: - print(f"ERROR: Failed to load Ziti identity: {e}") - schema = generate_json_schema(zitiIdJson) - print(f"DEBUG: zitiId schema for troubleshooting: {json.dumps(schema, indent=2)}") - raise e + # Defer openziti.load() until inside the monkeypatch context to keep + # initialization/teardown paired and avoid double-free on shutdown. # Create webhook body try: @@ -454,13 +448,36 @@ def _safe_hint(s): data = mwb.dumpJson() with openziti.monkeypatch(): + # Load the identity inside the context so that the same owner tears down + # resources, reducing the chance of double shutdown/free. + try: + openziti.load(idFilename) + except Exception as e: + print(f"ERROR: Failed to load Ziti identity: {e}") + schema = generate_json_schema(zitiIdJson) + print(f"DEBUG: zitiId schema for troubleshooting: {json.dumps(schema, indent=2)}") + raise e + + session = None + r = None try: + session = requests.Session() print(f"Posting webhook to {url} with headers {headers} and data {data}") - # breakpoint() - r = requests.post(url, headers=headers, data=data) + r = session.post(url, headers=headers, data=data) print(f"Response Status: {r.status_code}") print(r.headers) print(r.content) except Exception as e: print(f"Exception posting webhook: {e}") raise e + finally: + try: + if r is not None: + r.close() + except Exception: + pass + try: + if session is not None: + session.close() + except Exception: + pass From dc0e801dbb2a9f0c003b59aed316bedcd4e1b33a Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Thu, 21 Aug 2025 18:02:27 -0400 Subject: [PATCH 23/52] move lint exceptions to project file so contributors can follow same conventions --- .flake8 | 10 ++++++++++ .github/workflows/python.yml | 2 +- zhook.py | 4 ++-- 3 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..6fcae90 --- /dev/null +++ b/.flake8 @@ -0,0 +1,10 @@ +[flake8] +ignore = + E111, + E114, + E121, + E501 + +# You can add project-wide settings here later, e.g.: +# max-line-length = 120 +# exclude = .git,__pycache__,.venv diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index f8090cb..5fe1bb4 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -15,4 +15,4 @@ jobs: - name: Lint run: | pip install flake8 - flake8 --ignore E111,E114,E121,E501 zhook.py + flake8 --config ./.flake8 ./zhook.py diff --git a/zhook.py b/zhook.py index 441d8d1..4fab753 100644 --- a/zhook.py +++ b/zhook.py @@ -416,9 +416,9 @@ def generate_json_schema(obj, max_depth=10, current_depth=0): def _safe_hint(s): if s is None: return "" - l = len(s) + hint_len = len(s) head = s[:8].replace('\n', ' ') - return f"len={l}, startswith='{head}...'" + return f"len={hint_len}, startswith='{head}...'" try: zitiIdJson = json.loads(zitiId) From bad94e7cacf253aa1c56d4e46e404696b92623de Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Thu, 21 Aug 2025 18:04:37 -0400 Subject: [PATCH 24/52] run zhook self-checks when owned by netfoundry, too --- .github/workflows/zhook.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index b9f4b8b..129da45 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -32,7 +32,7 @@ jobs: - name: Run Python Script Directly if: | - github.repository_owner == 'openziti' + (github.repository_owner == 'openziti' || github.repository_owner == 'netfoundry') && ((github.event_name != 'pull_request_review') || (github.event_name == 'pull_request_review' && github.event.review.state == 'approved')) env: @@ -56,7 +56,7 @@ jobs: - name: Run in Docker with Core Dumps if: | - github.repository_owner == 'openziti' + (github.repository_owner == 'openziti' || github.repository_owner == 'netfoundry') && ((github.event_name != 'pull_request_review') || (github.event_name == 'pull_request_review' && github.event.review.state == 'approved')) shell: bash @@ -109,7 +109,7 @@ jobs: - uses: ./ name: Run as a GH Action from the Local Checkout if: | - github.repository_owner == 'openziti' + (github.repository_owner == 'openziti' || github.repository_owner == 'netfoundry') && ((github.event_name != 'pull_request_review') || (github.event_name == 'pull_request_review' && github.event.review.state == 'approved')) with: From ffc9fca55949f2288e99ac1f26f3b51e9a436362 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Fri, 22 Aug 2025 10:21:24 -0400 Subject: [PATCH 25/52] upload debug artifacts --- .github/workflows/zhook.yml | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index 129da45..45c9f76 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -56,7 +56,8 @@ jobs: - name: Run in Docker with Core Dumps if: | - (github.repository_owner == 'openziti' || github.repository_owner == 'netfoundry') + always() + && (github.repository_owner == 'openziti' || github.repository_owner == 'netfoundry') && ((github.event_name != 'pull_request_review') || (github.event_name == 'pull_request_review' && github.event.review.state == 'approved')) shell: bash @@ -109,7 +110,8 @@ jobs: - uses: ./ name: Run as a GH Action from the Local Checkout if: | - (github.repository_owner == 'openziti' || github.repository_owner == 'netfoundry') + always() + && (github.repository_owner == 'openziti' || github.repository_owner == 'netfoundry') && ((github.event_name != 'pull_request_review') || (github.event_name == 'pull_request_review' && github.event.review.state == 'approved')) with: @@ -133,22 +135,8 @@ jobs: find . -maxdepth 2 -path './.git' -prune -o -print find $(python -c "import site; print(site.USER_SITE)") -path "*/openziti*" -name "*.so*" -type f -print0 | xargs -0r ldd + # find core dumps produced by the kernel or valgrind shopt -s nullglob - # find valgrind logs from both execution steps - typeset -a VALGRIND_LOGS=(${GITHUB_WORKSPACE}/*-valgrind-*.log) - if (( ${#VALGRIND_LOGS[@]} )); then - for LOG in "${VALGRIND_LOGS[@]}"; do - if [ -s "$LOG" ]; then - echo "DEBUG: Valgrind log: $LOG" - cat "$LOG" - echo "--- End of $(basename "$LOG") ---" - fi - done - else - echo "DEBUG: No Valgrind logs found" - fi - - # find core dumps produced by the kernel and valgrind typeset -a CORES=(${GITHUB_WORKSPACE}/core.* ${GITHUB_WORKSPACE}/vgcore.*) shopt -u nullglob if (( ${#CORES[@]} )); then @@ -162,3 +150,14 @@ jobs: else echo "DEBUG: No core dumps found" fi + + - name: Upload Valgrind Logs and Core Dumps + if: always() + uses: actions/upload-artifact@v4 + with: + name: valgrind-logs-and-core-dumps-${{ github.run_id }} + path: | + ${{ github.workspace }}/*-valgrind-*.log + ${{ github.workspace }}/core.* + ${{ github.workspace }}/vgcore.* + if-no-files-found: ignore \ No newline at end of file From b5a1a2e7cd7fd61dada9b7100bca37b7ec26ce97 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Fri, 22 Aug 2025 12:14:33 -0400 Subject: [PATCH 26/52] fix request body format --- zhook.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/zhook.py b/zhook.py index 4fab753..246f1ec 100644 --- a/zhook.py +++ b/zhook.py @@ -444,8 +444,8 @@ def _safe_hint(s): raise e # Post the webhook over Ziti - headers = {'Content-Type': 'application/json'} - data = mwb.dumpJson() + # Build dict payload; requests will set Content-Type when using json= + payload = mwb.body with openziti.monkeypatch(): # Load the identity inside the context so that the same owner tears down @@ -462,8 +462,8 @@ def _safe_hint(s): r = None try: session = requests.Session() - print(f"Posting webhook to {url} with headers {headers} and data {data}") - r = session.post(url, headers=headers, data=data) + print(f"Posting webhook to {url} with JSON payload keys {list(payload.keys())}") + r = session.post(url, json=payload) print(f"Response Status: {r.status_code}") print(r.headers) print(r.content) From 58c7d9b58bb9012151cd5747d74f62df250443ef Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Fri, 22 Aug 2025 18:39:31 -0400 Subject: [PATCH 27/52] checkpoint --- .github/workflows/zhook.yml | 5 +- zhook.py | 224 ++++++++++++++++++++---------------- 2 files changed, 128 insertions(+), 101 deletions(-) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index 45c9f76..3b7c3b9 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -72,13 +72,10 @@ jobs: set -o pipefail set -o xtrace - # Base64 encode JSON to avoid whitespace issues in env file - ENCODED_JSON=$(echo "${INPUT_EVENTJSON}" | base64 -w 0) - cat > /tmp/docker.env << EOF INPUT_ZITIID=${INPUT_ZITIID} INPUT_WEBHOOKURL=${INPUT_WEBHOOKURL} - INPUT_EVENTJSON_B64=${ENCODED_JSON} + INPUT_EVENTJSON=${INPUT_EVENTJSON} INPUT_SENDERUSERNAME=${INPUT_SENDERUSERNAME} INPUT_SENDERICONURL=${INPUT_SENDERICONURL} INPUT_ZITILOGLEVEL=${INPUT_ZITILOGLEVEL} diff --git a/zhook.py b/zhook.py index 246f1ec..3293ed0 100644 --- a/zhook.py +++ b/zhook.py @@ -23,22 +23,20 @@ class MattermostWebhookBody: todoColor = "#FFFFFF" watchColor = "#FFD700" - def __init__(self, username, icon, eventName, eventJsonStr, actionRepo): + def __init__(self, username, icon, eventName, eventJson, actionRepo): self.username = username self.icon = icon self.eventName = eventName.lower() - self.eventJsonStr = eventJsonStr + self.eventJson = eventJson self.actionRepo = actionRepo - self.eventJson = json.loads(eventJsonStr) - self.repoJson = self.eventJson["repository"] - self.senderJson = self.eventJson["sender"] + self.event = json.loads(eventJson) + self.repo = self.event["repository"] + self.sender = self.event["sender"] self.body = { - # "username": self.username, - # "icon_url": self.icon, - "username": self.senderJson['login'], - "icon_url": self.senderJson['avatar_url'], - "props": {"card": f"```json\n{self.eventJsonStr}\n```"}, + "username": self.sender['login'], + "icon_url": self.sender['avatar_url'], + "props": {"card": f"```json\n{self.eventJson}\n```"}, } # self.attachment = { @@ -79,17 +77,17 @@ def __init__(self, username, icon, eventName, eventJsonStr, actionRepo): self.body["attachments"] = [self.attachment] def createTitle(self): - login = self.senderJson["login"] - loginUrl = self.senderJson["html_url"] - repoName = self.repoJson["full_name"] - repoUrl = self.repoJson["html_url"] + login = self.sender["login"] + loginUrl = self.sender["html_url"] + repoName = self.repo["full_name"] + repoUrl = self.repo["html_url"] # starCount = self.repoJson["stargazers_count"] # starUrl = f"{repoUrl}/stargazers" title = f"{self.eventName.capitalize().replace('_', ' ')}" try: - action = self.eventJson["action"] + action = self.event["action"] title += f" {action}" except Exception: pass @@ -99,15 +97,15 @@ def createTitle(self): def addPushDetails(self): self.body["text"] = self.createTitle() - forced = self.eventJson["forced"] - commits = self.eventJson["commits"] + forced = self.event["forced"] + commits = self.event["commits"] if forced: pushBody = "Force-pushed " else: pushBody = "Pushed " - pushBody += f"[{len(commits)} commit(s)]({self.eventJson['compare']}) to {self.eventJson['ref']}" + pushBody += f"[{len(commits)} commit(s)]({self.event['compare']}) to {self.event['ref']}" for c in commits: pushBody += f"\n[`{c['id'][:6]}`]({c['url']}) {c['message']}" self.attachment["color"] = self.pushColor @@ -115,7 +113,7 @@ def addPushDetails(self): def addPullRequestDetails(self): self.body["text"] = self.createTitle() - prJson = self.eventJson["pull_request"] + prJson = self.event["pull_request"] headJson = prJson["head"] baseJson = prJson["base"] self.attachment["color"] = self.prColor @@ -155,8 +153,8 @@ def addPullRequestDetails(self): def addPullRequestReviewCommentDetails(self): self.body["text"] = self.createTitle() - commentJson = self.eventJson["comment"] - prJson = self.eventJson['pull_request'] + commentJson = self.event["comment"] + prJson = self.event['pull_request'] bodyTxt = f"[Comment]({commentJson['html_url']}) in [PR#{prJson['number']}: {prJson['title']}]({prJson['html_url']}):\n" try: @@ -169,9 +167,9 @@ def addPullRequestReviewCommentDetails(self): def addPullRequestReviewDetails(self): self.body["text"] = self.createTitle() - reviewJson = self.eventJson["review"] + reviewJson = self.event["review"] reviewState = reviewJson['state'] - prJson = self.eventJson['pull_request'] + prJson = self.event['pull_request'] bodyTxt = f"[Review]({reviewJson['html_url']}) of [PR#{prJson['number']}: {prJson['title']}]({prJson['html_url']})\n" bodyTxt += f"Review State: {reviewState.capitalize()}\n" bodyTxt += f"{reviewJson['body']}" @@ -183,16 +181,16 @@ def addPullRequestReviewDetails(self): def addDeleteDetails(self): self.body["text"] = self.createTitle() - self.attachment["text"] = f"Deleted {self.eventJson['ref_type']} \"{self.eventJson['ref']}\"" + self.attachment["text"] = f"Deleted {self.event['ref_type']} \"{self.event['ref']}\"" def addCreateDetails(self): self.body["text"] = self.createTitle() - self.attachment["text"] = f"Created {self.eventJson['ref_type']} \"{self.eventJson['ref']}\"" + self.attachment["text"] = f"Created {self.event['ref_type']} \"{self.event['ref']}\"" def addIssuesDetails(self): self.body["text"] = self.createTitle() - action = self.eventJson["action"] - issueJson = self.eventJson["issue"] + action = self.event["action"] + issueJson = self.event["issue"] issueTitle = issueJson["title"] issueUrl = issueJson["html_url"] issueBody = issueJson["body"] @@ -217,10 +215,10 @@ def addIssuesDetails(self): def addIssueCommentDetails(self): self.body["text"] = self.createTitle() - commentJson = self.eventJson["comment"] + commentJson = self.event["comment"] commentBody = commentJson["body"] commentUrl = commentJson["html_url"] - issueJson = self.eventJson["issue"] + issueJson = self.event["issue"] issueTitle = issueJson["title"] issueNumber = issueJson["number"] @@ -237,14 +235,14 @@ def addIssueCommentDetails(self): def addForkDetails(self): self.body["text"] = self.createTitle() - forkeeJson = self.eventJson["forkee"] + forkeeJson = self.event["forkee"] bodyText = f"Forkee [{forkeeJson['full_name']}]({forkeeJson['html_url']})" self.attachment["text"] = bodyText def addReleaseDetails(self): self.body["text"] = self.createTitle() - action = self.eventJson["action"] - releaseJson = self.eventJson["release"] + action = self.event["action"] + releaseJson = self.event["release"] isDraft = releaseJson["draft"] isPrerelease = releaseJson["prerelease"] @@ -277,10 +275,10 @@ def addReleaseDetails(self): def addWatchDetails(self): self.body["text"] = f"{self.createTitle()} #stargazer" - login = self.senderJson["login"] - loginUrl = self.senderJson["html_url"] - userUrl = self.senderJson["url"] - starCount = self.repoJson["stargazers_count"] + login = self.sender["login"] + loginUrl = self.sender["html_url"] + userUrl = self.sender["url"] + starCount = self.repo["stargazers_count"] bodyText = f"[{login}]({loginUrl}) is stargazer number {starCount}\n\n" @@ -344,7 +342,7 @@ def addWatchDetails(self): def addDefaultDetails(self): self.attachment["color"] = self.todoColor self.attachment["text"] = self.createTitle() - self.attachment["fallback"] = f"{eventName.capitalize().replace('_', ' ')} by {self.senderJson['login']} in {self.repoJson['full_name']}" + self.attachment["fallback"] = f"{self.eventName.capitalize().replace('_', ' ')} by {self.sender['login']} in {self.repo['full_name']}" def dumpJson(self): return json.dumps(self.body) @@ -353,16 +351,59 @@ def dumpJson(self): if __name__ == '__main__': url = os.getenv("INPUT_WEBHOOKURL") - # Handle both base64-encoded and direct JSON input - eventJsonB64 = os.getenv("INPUT_EVENTJSON_B64") - if eventJsonB64: + # Handle event JSON provided inline; auto-detect if it's JSON or base64-encoded JSON + def _try_parse_json(s: str): try: - eventJsonStr = base64.b64decode(eventJsonB64).decode('utf-8') - except Exception as e: - print(f"Failed to decode base64 event JSON: {e}") - eventJsonStr = os.getenv("INPUT_EVENTJSON") + json.loads(s) + return True + except Exception: + return False + + def _try_decode_b64_to_json_str(s: str): + if s is None: + return None + try: + # strict validation first + decoded = base64.b64decode(s, validate=True) + decoded_str = decoded.decode('utf-8') + if _try_parse_json(decoded_str): + return decoded_str + except Exception: + # Try non-strict decode + try: + decoded = base64.b64decode(s) + decoded_str = decoded.decode('utf-8') + if _try_parse_json(decoded_str): + return decoded_str + except Exception: + pass + # As a last resort, try appending one to four '=' padding chars + for i in range(1, 5): + try: + s_padded = s + ("=" * i) + decoded = base64.b64decode(s_padded) + decoded_str = decoded.decode('utf-8') + if _try_parse_json(decoded_str): + return decoded_str + except Exception: + continue + return None + + eventInput = os.getenv("INPUT_EVENTJSON") + eventJson = "" + + if eventInput and _try_parse_json(eventInput): + eventJson = eventInput + print("Detected valid JSON in INPUT_EVENTJSON") else: - eventJsonStr = os.getenv("INPUT_EVENTJSON") + decoded = _try_decode_b64_to_json_str(eventInput) + if decoded is not None: + eventJson = decoded + print("Detected base64-encoded JSON in INPUT_EVENTJSON and decoded it") + + if not eventJson: + print("ERROR: No valid event JSON provided in INPUT_EVENTJSON") + exit(1) username = os.getenv("INPUT_SENDERUSERNAME") icon = os.getenv("INPUT_SENDERICONURL") actionRepo = os.getenv("GITHUB_ACTION_REPOSITORY") @@ -372,47 +413,45 @@ def dumpJson(self): os.environ["ZITI_LOG"] = zitiLogLevel os.environ["TLSUV_DEBUG"] = zitiLogLevel - # Setup Ziti identity - zitiJwt = os.getenv("INPUT_ZITIJWT") - if zitiJwt is not None: - zitiId = openziti.enroll(zitiJwt) + # Set up Ziti identity + zitiJwtInput = os.getenv("INPUT_ZITIJWT") + zitiIdJson = None # validated JSON string form + zitiIdEncoding = None # validated base64 string form + zitiIdContext = None # deserialized dict + if zitiJwtInput is not None: + # Expect enroll to return the identity JSON content + try: + enrolled = openziti.enroll(zitiJwtInput) + # Validate that the returned content is JSON + zitiIdContext = json.loads(enrolled) + zitiIdJson = enrolled + print("Obtained valid identity JSON from INPUT_ZITIJWT enrollment") + except Exception as e: + print(f"ERROR: Failed to enroll or parse identity from INPUT_ZITIJWT: {e}") + exit(1) else: - zitiId = os.getenv("INPUT_ZITIID") - - if zitiId is None: - print("ERROR: no Ziti identity provided, set INPUT_ZITIID or INPUT_ZITIJWT") - exit(1) - - def generate_json_schema(obj, max_depth=10, current_depth=0): - """Generate a schema representation of a JSON object by inferring types from values.""" - if current_depth >= max_depth: - return "" - - if obj is None: - return "null" - elif isinstance(obj, bool): - return "boolean" - elif isinstance(obj, int): - return "integer" - elif isinstance(obj, float): - return "number" - elif isinstance(obj, str): - return "string" - elif isinstance(obj, list): - if len(obj) == 0: - return "array[]" - # Get schema of first element as representative - element_schema = generate_json_schema(obj[0], max_depth, current_depth + 1) - return f"array[{element_schema}]" - elif isinstance(obj, dict): - schema = {} - for key, value in obj.items(): - schema[key] = generate_json_schema(value, max_depth, current_depth + 1) - return schema + # Support inline JSON or base64-encoded identity JSON from a single variable + zitiIdInput = os.getenv("INPUT_ZITIID") + + # Prefer valid inline JSON if present + if zitiIdInput and _try_parse_json(zitiIdInput): + zitiIdJson = zitiIdInput + zitiIdContext = json.loads(zitiIdInput) + print("Detected valid inline JSON in INPUT_ZITIID") else: - return f"unknown_type({type(obj).__name__})" - - # Accept only inline JSON for zitiId; do not interpret as file path or base64 + # Try decoding inline as base64 if provided and not valid JSON + decodedInline = _try_decode_b64_to_json_str(zitiIdInput) if zitiIdInput else None + if decodedInline is not None: + zitiIdEncoding = zitiIdInput + zitiIdJson = decodedInline + zitiIdContext = json.loads(decodedInline) + print("Detected base64-encoded identity in INPUT_ZITIID and decoded it") + + if zitiIdJson is None: + print("ERROR: no Ziti identity provided, set INPUT_ZITIID (inline JSON or base64-encoded), or INPUT_ZITIJWT") + exit(1) + + # Keep a small helper for safe string hints (used only in error/debug prints if needed) def _safe_hint(s): if s is None: return "" @@ -420,25 +459,16 @@ def _safe_hint(s): head = s[:8].replace('\n', ' ') return f"len={hint_len}, startswith='{head}...'" - try: - zitiIdJson = json.loads(zitiId) - zitiIdContent = zitiId - except Exception as e: - print("ERROR: zitiId must be inline JSON (not a file path or base64).") - print(f"DEBUG: INPUT_ZITIID hint: {_safe_hint(zitiId)}") - print(f"DEBUG: json error: {e}") - exit(1) - idFilename = "id.json" with open(idFilename, 'w') as f: - f.write(zitiIdContent) + f.write(zitiIdJson) # Defer openziti.load() until inside the monkeypatch context to keep # initialization/teardown paired and avoid double-free on shutdown. # Create webhook body try: - mwb = MattermostWebhookBody(username, icon, eventName, eventJsonStr, actionRepo) + mwb = MattermostWebhookBody(username, icon, eventName, eventJson, actionRepo) except Exception as e: print(f"Exception creating webhook body: {e}") raise e @@ -454,8 +484,8 @@ def _safe_hint(s): openziti.load(idFilename) except Exception as e: print(f"ERROR: Failed to load Ziti identity: {e}") - schema = generate_json_schema(zitiIdJson) - print(f"DEBUG: zitiId schema for troubleshooting: {json.dumps(schema, indent=2)}") + print(f"DEBUG: INPUT_ZITIID hint: {_safe_hint(os.getenv('INPUT_ZITIID'))}") + print(f"DEBUG: zitiIdJson len={len(zitiIdJson) if zitiIdJson else 0}") raise e session = None From 80db146ed15b8ccecefc92abb6e0557802c372e5 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Mon, 29 Sep 2025 10:57:51 -0400 Subject: [PATCH 28/52] test a version from test pypi --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4f42d9f..efad6d9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ requests -openziti \ No newline at end of file +--extra-index-url https://test.pypi.org/simple/ +openziti==1.3.1.post0.dev11 \ No newline at end of file From 1890cf6068553a0d1b887b1da00f8784a6d494c0 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Mon, 29 Sep 2025 11:15:42 -0400 Subject: [PATCH 29/52] encode event json to avoid whitespace errors --- .github/workflows/zhook.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index 3b7c3b9..35e2e3b 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -64,7 +64,6 @@ jobs: env: INPUT_ZITIID: ${{ secrets.ZITI_MATTERMOST_IDENTITY }} INPUT_WEBHOOKURL: ${{ secrets.ZHOOK_URL_DEV_NOTIFICATIONS }} - INPUT_EVENTJSON: ${{ toJson(github.event) }} INPUT_SENDERUSERNAME: GitHubZ INPUT_SENDERICONURL: https://github.com/fluidicon.png INPUT_ZITILOGLEVEL: 6 @@ -75,7 +74,7 @@ jobs: cat > /tmp/docker.env << EOF INPUT_ZITIID=${INPUT_ZITIID} INPUT_WEBHOOKURL=${INPUT_WEBHOOKURL} - INPUT_EVENTJSON=${INPUT_EVENTJSON} + INPUT_EVENTJSON=$(base64 -w 0 <<< '${{ toJson(github.event) }}') INPUT_SENDERUSERNAME=${INPUT_SENDERUSERNAME} INPUT_SENDERICONURL=${INPUT_SENDERICONURL} INPUT_ZITILOGLEVEL=${INPUT_ZITILOGLEVEL} From 0c15cb9d204e579af07ba0cbe202e61fb9d77f6b Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Mon, 29 Sep 2025 12:53:45 -0400 Subject: [PATCH 30/52] use ziti py v1.4.0 --- requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index efad6d9..f804b98 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ requests ---extra-index-url https://test.pypi.org/simple/ -openziti==1.3.1.post0.dev11 \ No newline at end of file +openziti==1.4.0 From a8ac1ea9c8df5435d4eda10fc9a9e093afdcbf43 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Mon, 29 Sep 2025 14:54:44 -0400 Subject: [PATCH 31/52] Revert "use ziti py v1.4.0" This reverts commit 0c15cb9d204e579af07ba0cbe202e61fb9d77f6b. --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f804b98..efad6d9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ requests -openziti==1.4.0 +--extra-index-url https://test.pypi.org/simple/ +openziti==1.3.1.post0.dev11 \ No newline at end of file From b2b0c49b3eb36e0d7f94ceb49c338cbd2b51ebb1 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Mon, 29 Sep 2025 17:08:40 -0400 Subject: [PATCH 32/52] use ziti py 1.4.1 --- requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index efad6d9..bea9fed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ requests ---extra-index-url https://test.pypi.org/simple/ -openziti==1.3.1.post0.dev11 \ No newline at end of file +openziti==1.4.1 From 713f67bed692b2d22c45ef02465c79b392fdbfbd Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Mon, 29 Sep 2025 23:30:49 -0400 Subject: [PATCH 33/52] refine zhook.py --- zhook.py | 137 +++++++++++++++++++++++++++---------------------------- 1 file changed, 68 insertions(+), 69 deletions(-) diff --git a/zhook.py b/zhook.py index 3293ed0..abf344d 100644 --- a/zhook.py +++ b/zhook.py @@ -348,46 +348,71 @@ def dumpJson(self): return json.dumps(self.body) -if __name__ == '__main__': - url = os.getenv("INPUT_WEBHOOKURL") +def _try_parse_json(s: str): + """Try to parse a string as JSON and return True if successful.""" + try: + json.loads(s) + return True + except Exception: + return False - # Handle event JSON provided inline; auto-detect if it's JSON or base64-encoded JSON - def _try_parse_json(s: str): - try: - json.loads(s) - return True - except Exception: - return False - def _try_decode_b64_to_json_str(s: str): - if s is None: - return None +def _try_decode_b64_to_json_str(s: str): + """Try to decode a base64 string to a JSON string with various fallback strategies.""" + if s is None: + return None + try: + # strict validation first + decoded = base64.b64decode(s, validate=True) + decoded_str = decoded.decode('utf-8') + if _try_parse_json(decoded_str): + return decoded_str + except Exception: + # Try non-strict decode try: - # strict validation first - decoded = base64.b64decode(s, validate=True) + decoded = base64.b64decode(s) decoded_str = decoded.decode('utf-8') if _try_parse_json(decoded_str): return decoded_str except Exception: - # Try non-strict decode + pass + # As a last resort, try appending one to four '=' padding chars + for i in range(1, 5): try: - decoded = base64.b64decode(s) + s_padded = s + ("=" * i) + decoded = base64.b64decode(s_padded) decoded_str = decoded.decode('utf-8') if _try_parse_json(decoded_str): return decoded_str except Exception: - pass - # As a last resort, try appending one to four '=' padding chars - for i in range(1, 5): - try: - s_padded = s + ("=" * i) - decoded = base64.b64decode(s_padded) - decoded_str = decoded.decode('utf-8') - if _try_parse_json(decoded_str): - return decoded_str - except Exception: - continue - return None + continue + return None + + +def _safe_hint(s): + """Create a safe string hint for debugging purposes.""" + if s is None: + return "" + hint_len = len(s) + head = s[:8].replace('\n', ' ') + return f"len={hint_len}, startswith='{head}...'" + + +@openziti.zitify() +def doPost(url, payload): + """Post webhook payload to the specified URL over Ziti.""" + # Single request doesn't need session management + response = requests.post(url, json=payload) + print(f"Response Status: {response.status_code}") + print(response.headers) + print(response.content) + return response + + +if __name__ == '__main__': + url = os.getenv("INPUT_WEBHOOKURL") + + # Handle event JSON provided inline; auto-detect if it's JSON or base64-encoded JSON eventInput = os.getenv("INPUT_EVENTJSON") eventJson = "" @@ -451,14 +476,6 @@ def _try_decode_b64_to_json_str(s: str): print("ERROR: no Ziti identity provided, set INPUT_ZITIID (inline JSON or base64-encoded), or INPUT_ZITIJWT") exit(1) - # Keep a small helper for safe string hints (used only in error/debug prints if needed) - def _safe_hint(s): - if s is None: - return "" - hint_len = len(s) - head = s[:8].replace('\n', ' ') - return f"len={hint_len}, startswith='{head}...'" - idFilename = "id.json" with open(idFilename, 'w') as f: f.write(zitiIdJson) @@ -477,37 +494,19 @@ def _safe_hint(s): # Build dict payload; requests will set Content-Type when using json= payload = mwb.body - with openziti.monkeypatch(): - # Load the identity inside the context so that the same owner tears down - # resources, reducing the chance of double shutdown/free. - try: - openziti.load(idFilename) - except Exception as e: - print(f"ERROR: Failed to load Ziti identity: {e}") - print(f"DEBUG: INPUT_ZITIID hint: {_safe_hint(os.getenv('INPUT_ZITIID'))}") - print(f"DEBUG: zitiIdJson len={len(zitiIdJson) if zitiIdJson else 0}") - raise e + # Load the identity for Ziti operations + try: + openziti.load(idFilename) + except Exception as e: + print(f"ERROR: Failed to load Ziti identity: {e}") + print(f"DEBUG: INPUT_ZITIID hint: {_safe_hint(os.getenv('INPUT_ZITIID'))}") + print(f"DEBUG: zitiIdJson len={len(zitiIdJson) if zitiIdJson else 0}") + raise e - session = None - r = None - try: - session = requests.Session() - print(f"Posting webhook to {url} with JSON payload keys {list(payload.keys())}") - r = session.post(url, json=payload) - print(f"Response Status: {r.status_code}") - print(r.headers) - print(r.content) - except Exception as e: - print(f"Exception posting webhook: {e}") - raise e - finally: - try: - if r is not None: - r.close() - except Exception: - pass - try: - if session is not None: - session.close() - except Exception: - pass + # Post the webhook over Ziti + try: + print(f"Posting webhook to {url} with JSON payload keys {list(payload.keys())}") + response = doPost(url, payload) + except Exception as e: + print(f"Exception posting webhook: {e}") + raise e From b3d7fffe59d7b2a799edecdb8f47af98413ac1b8 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Sun, 29 Jun 2025 18:39:19 -0400 Subject: [PATCH 34/52] update py webhook action to use a channel-specific url; add debug --- .github/workflows/zhook.yml | 42 ++++++++++++------------------ Dockerfile | 6 +++++ README.md | 1 - action.yml | 32 +++++++++++------------ zhook.py | 52 +++++++++++++++++++++++++++++++++---- 5 files changed, 85 insertions(+), 48 deletions(-) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index 4bf20d2..2daca7d 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -21,46 +21,38 @@ jobs: runs-on: ubuntu-latest name: POST Webhook steps: - - uses: actions/checkout@v4 - - name: run hook directly + - name: Checkout + uses: actions/checkout@v4 + + - name: Run Python Script Directly if: | github.repository_owner == 'openziti' && ((github.event_name != 'pull_request_review') || (github.event_name == 'pull_request_review' && github.event.review.state == 'approved')) env: INPUT_ZITIID: ${{ secrets.ZITI_MATTERMOST_IDENTITY }} - INPUT_WEBHOOKURL: ${{ secrets.ZHOOK_URL }} + INPUT_WEBHOOKURL: ${{ secrets.ZHOOK_URL_DEV_NOTIFICATIONS }} INPUT_EVENTJSON: ${{ toJson(github.event) }} INPUT_SENDERUSERNAME: GitHubZ - INPUT_DESTCHANNEL: dev-notifications INPUT_SENDERICONURL: https://github.com/fluidicon.png + ZITI_LOG: 6 + TLSUV_DEBUG: 6 + shell: bash run: | - pip install --upgrade requests openziti - set +e - if [ "${ZHOOK_VALGRIND}" = "true" ]; then - echo "⚠️ running with valgrind..." - sudo apt-get update - sudo apt-get install -y valgrind - echo "⚙️ Running under valgrind..." - valgrind --tool=memcheck --leak-check=full \ - --show-leak-kinds=all --track-origins=yes \ - python ./zhook.py - else - python ./zhook.py - fi - echo "⚠️ zhook.py exited with code $?, continuing..." - exit 0 + set -o pipefail + set -o xtrace + pip install --user --upgrade requests openziti + echo "DEBUG: PYTHONPATH=${PYTHONPATH:-}" + python ./zhook.py - - uses: ./ # use self to bring the pain forward - name: run action + - uses: ./ + name: Run as a GH Action from the Local Checkout if: | github.repository_owner == 'openziti' && ((github.event_name != 'pull_request_review') || (github.event_name == 'pull_request_review' && github.event.review.state == 'approved')) with: zitiId: ${{ secrets.ZITI_MATTERMOST_IDENTITY }} - webhookUrl: ${{ secrets.ZHOOK_URL }} + webhookUrl: ${{ secrets.ZHOOK_URL_DEV_NOTIFICATIONS }} eventJson: ${{ toJson(github.event) }} - senderUsername: "GitHubZ" - destChannel: "dev-notifications" - + senderUsername: GitHubZ diff --git a/Dockerfile b/Dockerfile index e550485..8d8e221 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,11 @@ FROM python:3.11-bullseye +RUN pip install --target=/app requests openziti + +# https://github.com/GoogleContainerTools/distroless +FROM gcr.io/distroless/python3-debian12 +COPY --from=builder /app /app +COPY ./zhook.py /app/zhook.py WORKDIR /app COPY ./zhook.py /app/zhook.py diff --git a/README.md b/README.md index 9762a5e..3215132 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,6 @@ jobs: eventJson: ${{ toJson(github.event) }} senderUsername: "GitHubZ" - destChannel: "github-notifications" ``` ### Inputs diff --git a/action.yml b/action.yml index 6082313..9096fff 100644 --- a/action.yml +++ b/action.yml @@ -1,32 +1,30 @@ -name: 'Ziti Mattermost Action - Python' -description: 'POST to Mattermost Webhook endpoint over a Ziti network' +name: Ziti Mattermost Action - Python +description: POST to Mattermost Webhook endpoint over a Ziti network branding: - icon: 'zap' - color: 'red' + icon: zap + color: red inputs: zitiId: - description: 'Identity JSON for an enrolled Ziti endpoint' + description: Identity JSON for an enrolled Ziti endpoint required: true webhookUrl: - description: 'URL for posting the payload' + description: Mattermost-channel-specific URL for posting the event required: true eventJson: - description: 'GitHub event JSON (github.event)' + description: GitHub event JSON (github.event) required: true senderUsername: - description: 'Mattermost username' + description: Mattermost username required: false - default: "GithubZ" + default: GithubZ senderIconUrl: - description: 'Mattermost user icon URL' + description: Mattermost user icon URL required: false - default: "https://github.com/fluidicon.png" + default: https://github.com/fluidicon.png destChannel: - description: 'Mattermost channel' + description: Mattermost channel (deprecated because incoming webhooks are now locked to a channel) required: false - default: "dev-notifications" + default: null runs: - using: "docker" -# image: "Dockerfile" - # see README.md on how to update the container or if needed, use "Dockerfile" (the line above) - image: docker://ghcr.io/openziti/ziti-mattermost-action-py:latest + using: docker + image: Dockerfile diff --git a/zhook.py b/zhook.py index 0bc80e0..f5b69d3 100644 --- a/zhook.py +++ b/zhook.py @@ -21,10 +21,9 @@ class MattermostWebhookBody: todoColor = "#FFFFFF" watchColor = "#FFD700" - def __init__(self, username, icon, channel, eventName, eventJsonStr, actionRepo): + def __init__(self, username, icon, eventName, eventJsonStr, actionRepo): self.username = username self.icon = icon - self.channel = channel self.eventName = eventName.lower() self.eventJsonStr = eventJsonStr self.actionRepo = actionRepo @@ -37,7 +36,6 @@ def __init__(self, username, icon, channel, eventName, eventJsonStr, actionRepo) # "icon_url": self.icon, "username": self.senderJson['login'], "icon_url": self.senderJson['avatar_url'], - "channel": self.channel, "props": {"card": f"```json\n{self.eventJsonStr}\n```"}, } @@ -355,7 +353,6 @@ def dumpJson(self): eventJsonStr = os.getenv("INPUT_EVENTJSON") username = os.getenv("INPUT_SENDERUSERNAME") icon = os.getenv("INPUT_SENDERICONURL") - channel = os.getenv("INPUT_DESTCHANNEL") actionRepo = os.getenv("GITHUB_ACTION_REPOSITORY") eventName = os.getenv("GITHUB_EVENT_NAME") @@ -370,14 +367,59 @@ def dumpJson(self): print("ERROR: no Ziti identity provided, set INPUT_ZITIID or INPUT_ZITIJWT") exit(1) + def generate_json_schema(obj, max_depth=10, current_depth=0): + """Generate a schema representation of a JSON object by inferring types from values.""" + if current_depth >= max_depth: + return "" + + if obj is None: + return "null" + elif isinstance(obj, bool): + return "boolean" + elif isinstance(obj, int): + return "integer" + elif isinstance(obj, float): + return "number" + elif isinstance(obj, str): + return "string" + elif isinstance(obj, list): + if len(obj) == 0: + return "array[]" + # Get schema of first element as representative + element_schema = generate_json_schema(obj[0], max_depth, current_depth + 1) + return f"array[{element_schema}]" + elif isinstance(obj, dict): + schema = {} + for key, value in obj.items(): + schema[key] = generate_json_schema(value, max_depth, current_depth + 1) + return schema + else: + return f"unknown_type({type(obj).__name__})" + + # Validate zitiId as JSON + try: + zitiIdJson = json.loads(zitiId) + except Exception as e: + print(f"ERROR: zitiId is not valid JSON: {e}") + print(f"zitiId content: {zitiId}") + exit(1) + idFilename = "id.json" with open(idFilename, 'w') as f: f.write(zitiId) + + # Load the identity file after it's been written and closed + try: openziti.load(idFilename) + except Exception as e: + print(f"ERROR: Failed to load Ziti identity: {e}") + schema = generate_json_schema(zitiIdJson) + print(f"DEBUG: zitiId schema for troubleshooting: {json.dumps(schema, indent=2)}") + raise e # Create webhook body try: - mwb = MattermostWebhookBody(username, icon, channel, eventName, eventJsonStr, actionRepo) + mwb = MattermostWebhookBody(username, icon, eventName, eventJsonStr, actionRepo) except Exception as e: print(f"Exception creating webhook body: {e}") raise e From 04d43f0720e59d7ca8c21fb49af62c22354c038f Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Thu, 24 Jul 2025 15:53:33 -0400 Subject: [PATCH 35/52] add input validation and further debugging --- .flake8 | 10 ++ .github/workflows/python.yml | 2 +- .github/workflows/zhook.yml | 118 +++++++++++++- Dockerfile | 9 +- README.md | 4 +- action.yml | 6 +- debug.Dockerfile | 7 + requirements.txt | 2 + zhook.py | 299 +++++++++++++++++++++-------------- 9 files changed, 320 insertions(+), 137 deletions(-) create mode 100644 .flake8 create mode 100644 debug.Dockerfile create mode 100644 requirements.txt diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..6fcae90 --- /dev/null +++ b/.flake8 @@ -0,0 +1,10 @@ +[flake8] +ignore = + E111, + E114, + E121, + E501 + +# You can add project-wide settings here later, e.g.: +# max-line-length = 120 +# exclude = .git,__pycache__,.venv diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index f8090cb..5fe1bb4 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -15,4 +15,4 @@ jobs: - name: Lint run: | pip install flake8 - flake8 --ignore E111,E114,E121,E501 zhook.py + flake8 --config ./.flake8 ./zhook.py diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index 2daca7d..711cba4 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -18,15 +18,22 @@ on: jobs: mattermost-ziti-webhook: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 name: POST Webhook steps: + - name: Debug Environment + uses: hmarr/debug-action@v3 + + - name: Install Debug Tools + shell: bash + run: sudo apt-get install --yes valgrind gdb + - name: Checkout uses: actions/checkout@v4 - name: Run Python Script Directly if: | - github.repository_owner == 'openziti' + (github.repository_owner == 'openziti' || github.repository_owner == 'netfoundry') && ((github.event_name != 'pull_request_review') || (github.event_name == 'pull_request_review' && github.event.review.state == 'approved')) env: @@ -35,20 +42,73 @@ jobs: INPUT_EVENTJSON: ${{ toJson(github.event) }} INPUT_SENDERUSERNAME: GitHubZ INPUT_SENDERICONURL: https://github.com/fluidicon.png - ZITI_LOG: 6 - TLSUV_DEBUG: 6 + INPUT_ZITILOGLEVEL: 6 shell: bash run: | set -o pipefail set -o xtrace - pip install --user --upgrade requests openziti - echo "DEBUG: PYTHONPATH=${PYTHONPATH:-}" - python ./zhook.py + pip install --user --upgrade --requirement ./requirements.txt + # in case valgrind catches a segfault, it will write a core file in ./vgcore.%p + valgrind \ + --verbose \ + --log-file=${GITHUB_WORKSPACE}/direct-valgrind-%p-%n.log \ + --leak-check=yes \ + python ./zhook.py + + - name: Run in Docker with Core Dumps + if: | + always() + && (github.repository_owner == 'openziti' || github.repository_owner == 'netfoundry') + && ((github.event_name != 'pull_request_review') + || (github.event_name == 'pull_request_review' && github.event.review.state == 'approved')) + shell: bash + env: + INPUT_ZITIID: ${{ secrets.ZITI_MATTERMOST_IDENTITY }} + INPUT_WEBHOOKURL: ${{ secrets.ZHOOK_URL_DEV_NOTIFICATIONS }} + INPUT_SENDERUSERNAME: GitHubZ + INPUT_SENDERICONURL: https://github.com/fluidicon.png + INPUT_ZITILOGLEVEL: 6 + run: | + set -o pipefail + set -o xtrace + + cat > /tmp/docker.env << EOF + INPUT_ZITIID=${INPUT_ZITIID} + INPUT_WEBHOOKURL=${INPUT_WEBHOOKURL} + INPUT_EVENTJSON=$(base64 -w 0 <<< '${{ toJson(github.event) }}') + INPUT_SENDERUSERNAME=${INPUT_SENDERUSERNAME} + INPUT_SENDERICONURL=${INPUT_SENDERICONURL} + INPUT_ZITILOGLEVEL=${INPUT_ZITILOGLEVEL} + GITHUB_WORKSPACE=${GITHUB_WORKSPACE} + GITHUB_EVENT_NAME=${GITHUB_EVENT_NAME} + GITHUB_ACTION_REPOSITORY=${GITHUB_ACTION_REPOSITORY} + EOF + + # configure the kernel to write core dumps to the workspace directory that is writable by the container in case there is a segfault valgrind cannot catch in a vgcore.%p + sudo sysctl -w kernel.core_pattern="${GITHUB_WORKSPACE}/core.%e.%p.%t" + + # build the action's container image so we can source it for the debug image + docker build -t zhook-action . + docker build -t zhook-action-dbg -f debug.Dockerfile . + docker run --rm \ + --volume "${GITHUB_WORKSPACE}:${GITHUB_WORKSPACE}" \ + --workdir "${GITHUB_WORKSPACE}" \ + --env-file /tmp/docker.env \ + --entrypoint=/bin/bash \ + zhook-action-dbg -euxo pipefail -c ' + ulimit -c unlimited; + exec valgrind \ + --verbose \ + --log-file=${GITHUB_WORKSPACE}/docker-valgrind-%p-%n.log \ + --leak-check=yes \ + python /app/zhook.py; + ' - uses: ./ name: Run as a GH Action from the Local Checkout if: | - github.repository_owner == 'openziti' + always() + && (github.repository_owner == 'openziti' || github.repository_owner == 'netfoundry') && ((github.event_name != 'pull_request_review') || (github.event_name == 'pull_request_review' && github.event.review.state == 'approved')) with: @@ -56,3 +116,45 @@ jobs: webhookUrl: ${{ secrets.ZHOOK_URL_DEV_NOTIFICATIONS }} eventJson: ${{ toJson(github.event) }} senderUsername: GitHubZ + senderIconUrl: https://github.com/fluidicon.png + zitiLogLevel: 6 + + - name: Print Debug Info + if: always() + shell: bash + run: | + set -o xtrace + set +o errexit + echo "DEBUG: PYTHONPATH=${PYTHONPATH:-}" + echo "DEBUG: PATH=${PATH:-}" + echo "DEBUG: LD_LIBRARY_PATH=${LD_LIBRARY_PATH:-}" + # list non-git files in the two uppermost levels of the workspace directory hierarchy + find . -maxdepth 2 -path './.git' -prune -o -print + find $(python -c "import site; print(site.USER_SITE)") -path "*/openziti*" -name "*.so*" -type f -print0 | xargs -0r ldd + + # find core dumps produced by the kernel or valgrind + shopt -s nullglob + typeset -a CORES=(${GITHUB_WORKSPACE}/core.* ${GITHUB_WORKSPACE}/vgcore.*) + shopt -u nullglob + if (( ${#CORES[@]} )); then + for CORE in "${CORES[@]}"; do + if [ -s "$CORE" ]; then + echo "DEBUG: Core dump: $CORE" + EXECUTABLE=$(basename "$CORE" | cut -d. -f2) + gdb -q $(realpath $(which "$EXECUTABLE")) -c "$CORE" --ex bt --ex exit + fi + done + else + echo "DEBUG: No core dumps found" + fi + + - name: Upload Valgrind Logs and Core Dumps + if: always() + uses: actions/upload-artifact@v4 + with: + name: valgrind-logs-and-core-dumps-${{ github.run_id }} + path: | + ${{ github.workspace }}/*-valgrind-*.log + ${{ github.workspace }}/core.* + ${{ github.workspace }}/vgcore.* + if-no-files-found: ignore \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 8d8e221..8e4dbc8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,17 @@ FROM python:3.11-bullseye -RUN pip install --target=/app requests openziti +COPY requirements.txt /tmp/requirements.txt +RUN pip install --target=/app --requirement /tmp/requirements.txt # https://github.com/GoogleContainerTools/distroless FROM gcr.io/distroless/python3-debian12 COPY --from=builder /app /app -COPY ./zhook.py /app/zhook.py +COPY --chmod=0755 ./zhook.py /app/zhook.py WORKDIR /app COPY ./zhook.py /app/zhook.py RUN pip install --no-cache-dir requests openziti ENV PYTHONPATH=/app -#ENV ZITI_LOG=6 -#ENV TLSUV_DEBUG=6 -CMD ["python", "/app/zhook.py"] +CMD ["/app/zhook.py"] diff --git a/README.md b/README.md index 3215132..a384b63 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,10 @@ jobs: # URL to post the payload. Note that the `zitiId` must provide access to a service # intercepting `my-mattermost-ziti-server` - webhookUrl: 'https://{my-mattermost-ziti-server}/hook/{my-mattermost-webhook-id}}' + webhookUrl: http://{my-mattermost-ziti-server}/hook/{my-mattermost-webhook-id}} eventJson: ${{ toJson(github.event) }} - senderUsername: "GitHubZ" + senderUsername: GitHubZ ``` ### Inputs diff --git a/action.yml b/action.yml index 9096fff..536ab34 100644 --- a/action.yml +++ b/action.yml @@ -22,9 +22,13 @@ inputs: required: false default: https://github.com/fluidicon.png destChannel: - description: Mattermost channel (deprecated because incoming webhooks are now locked to a channel) + description: Mattermost channel (ignored because incoming webhooks are locked to a channel) required: false default: null + zitiLogLevel: + description: Ziti log level + required: false + default: 3 runs: using: docker image: Dockerfile diff --git a/debug.Dockerfile b/debug.Dockerfile new file mode 100644 index 0000000..3e7bdf2 --- /dev/null +++ b/debug.Dockerfile @@ -0,0 +1,7 @@ +FROM zhook-action AS distroless + +FROM python:3-slim AS debug + +COPY --from=distroless /app /app + +RUN apt-get update && apt-get install -y valgrind diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..bea9fed --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests +openziti==1.4.1 diff --git a/zhook.py b/zhook.py index f5b69d3..a01c384 100644 --- a/zhook.py +++ b/zhook.py @@ -1,9 +1,13 @@ -import requests -import openziti +#!/usr/bin/env python3 + +import base64 import json import os import sys +import openziti +import requests + class MattermostWebhookBody: actionRepoIcon = "https://github.com/openziti/branding/blob/main/images/ziggy/png/Ziggy-Gits-It.png?raw=true" @@ -21,22 +25,20 @@ class MattermostWebhookBody: todoColor = "#FFFFFF" watchColor = "#FFD700" - def __init__(self, username, icon, eventName, eventJsonStr, actionRepo): + def __init__(self, username, icon, eventName, eventJson, actionRepo): self.username = username self.icon = icon self.eventName = eventName.lower() - self.eventJsonStr = eventJsonStr + self.eventJson = eventJson self.actionRepo = actionRepo - self.eventJson = json.loads(eventJsonStr) - self.repoJson = self.eventJson["repository"] - self.senderJson = self.eventJson["sender"] + self.event = json.loads(eventJson) + self.repo = self.event["repository"] + self.sender = self.event["sender"] self.body = { - # "username": self.username, - # "icon_url": self.icon, - "username": self.senderJson['login'], - "icon_url": self.senderJson['avatar_url'], - "props": {"card": f"```json\n{self.eventJsonStr}\n```"}, + "username": self.sender['login'], + "icon_url": self.sender['avatar_url'], + "props": {"card": f"```json\n{self.eventJson}\n```"}, } # self.attachment = { @@ -77,17 +79,17 @@ def __init__(self, username, icon, eventName, eventJsonStr, actionRepo): self.body["attachments"] = [self.attachment] def createTitle(self): - login = self.senderJson["login"] - loginUrl = self.senderJson["html_url"] - repoName = self.repoJson["full_name"] - repoUrl = self.repoJson["html_url"] + login = self.sender["login"] + loginUrl = self.sender["html_url"] + repoName = self.repo["full_name"] + repoUrl = self.repo["html_url"] # starCount = self.repoJson["stargazers_count"] # starUrl = f"{repoUrl}/stargazers" title = f"{self.eventName.capitalize().replace('_', ' ')}" try: - action = self.eventJson["action"] + action = self.event["action"] title += f" {action}" except Exception: pass @@ -97,15 +99,15 @@ def createTitle(self): def addPushDetails(self): self.body["text"] = self.createTitle() - forced = self.eventJson["forced"] - commits = self.eventJson["commits"] + forced = self.event["forced"] + commits = self.event["commits"] if forced: pushBody = "Force-pushed " else: pushBody = "Pushed " - pushBody += f"[{len(commits)} commit(s)]({self.eventJson['compare']}) to {self.eventJson['ref']}" + pushBody += f"[{len(commits)} commit(s)]({self.event['compare']}) to {self.event['ref']}" for c in commits: pushBody += f"\n[`{c['id'][:6]}`]({c['url']}) {c['message']}" self.attachment["color"] = self.pushColor @@ -113,7 +115,7 @@ def addPushDetails(self): def addPullRequestDetails(self): self.body["text"] = self.createTitle() - prJson = self.eventJson["pull_request"] + prJson = self.event["pull_request"] headJson = prJson["head"] baseJson = prJson["base"] self.attachment["color"] = self.prColor @@ -153,8 +155,8 @@ def addPullRequestDetails(self): def addPullRequestReviewCommentDetails(self): self.body["text"] = self.createTitle() - commentJson = self.eventJson["comment"] - prJson = self.eventJson['pull_request'] + commentJson = self.event["comment"] + prJson = self.event['pull_request'] bodyTxt = f"[Comment]({commentJson['html_url']}) in [PR#{prJson['number']}: {prJson['title']}]({prJson['html_url']}):\n" try: @@ -167,9 +169,9 @@ def addPullRequestReviewCommentDetails(self): def addPullRequestReviewDetails(self): self.body["text"] = self.createTitle() - reviewJson = self.eventJson["review"] + reviewJson = self.event["review"] reviewState = reviewJson['state'] - prJson = self.eventJson['pull_request'] + prJson = self.event['pull_request'] bodyTxt = f"[Review]({reviewJson['html_url']}) of [PR#{prJson['number']}: {prJson['title']}]({prJson['html_url']})\n" bodyTxt += f"Review State: {reviewState.capitalize()}\n" bodyTxt += f"{reviewJson['body']}" @@ -181,16 +183,16 @@ def addPullRequestReviewDetails(self): def addDeleteDetails(self): self.body["text"] = self.createTitle() - self.attachment["text"] = f"Deleted {self.eventJson['ref_type']} \"{self.eventJson['ref']}\"" + self.attachment["text"] = f"Deleted {self.event['ref_type']} \"{self.event['ref']}\"" def addCreateDetails(self): self.body["text"] = self.createTitle() - self.attachment["text"] = f"Created {self.eventJson['ref_type']} \"{self.eventJson['ref']}\"" + self.attachment["text"] = f"Created {self.event['ref_type']} \"{self.event['ref']}\"" def addIssuesDetails(self): self.body["text"] = self.createTitle() - action = self.eventJson["action"] - issueJson = self.eventJson["issue"] + action = self.event["action"] + issueJson = self.event["issue"] issueTitle = issueJson["title"] issueUrl = issueJson["html_url"] issueBody = issueJson["body"] @@ -215,10 +217,10 @@ def addIssuesDetails(self): def addIssueCommentDetails(self): self.body["text"] = self.createTitle() - commentJson = self.eventJson["comment"] + commentJson = self.event["comment"] commentBody = commentJson["body"] commentUrl = commentJson["html_url"] - issueJson = self.eventJson["issue"] + issueJson = self.event["issue"] issueTitle = issueJson["title"] issueNumber = issueJson["number"] @@ -235,14 +237,14 @@ def addIssueCommentDetails(self): def addForkDetails(self): self.body["text"] = self.createTitle() - forkeeJson = self.eventJson["forkee"] + forkeeJson = self.event["forkee"] bodyText = f"Forkee [{forkeeJson['full_name']}]({forkeeJson['html_url']})" self.attachment["text"] = bodyText def addReleaseDetails(self): self.body["text"] = self.createTitle() - action = self.eventJson["action"] - releaseJson = self.eventJson["release"] + action = self.event["action"] + releaseJson = self.event["release"] isDraft = releaseJson["draft"] isPrerelease = releaseJson["prerelease"] @@ -275,10 +277,10 @@ def addReleaseDetails(self): def addWatchDetails(self): self.body["text"] = f"{self.createTitle()} #stargazer" - login = self.senderJson["login"] - loginUrl = self.senderJson["html_url"] - userUrl = self.senderJson["url"] - starCount = self.repoJson["stargazers_count"] + login = self.sender["login"] + loginUrl = self.sender["html_url"] + userUrl = self.sender["url"] + starCount = self.repo["stargazers_count"] bodyText = f"[{login}]({loginUrl}) is stargazer number {starCount}\n\n" @@ -342,114 +344,171 @@ def addWatchDetails(self): def addDefaultDetails(self): self.attachment["color"] = self.todoColor self.attachment["text"] = self.createTitle() - self.attachment["fallback"] = f"{eventName.capitalize().replace('_', ' ')} by {self.senderJson['login']} in {self.repoJson['full_name']}" + self.attachment["fallback"] = f"{self.eventName.capitalize().replace('_', ' ')} by {self.sender['login']} in {self.repo['full_name']}" def dumpJson(self): return json.dumps(self.body) +def _try_parse_json(s: str): + """Try to parse a string as JSON and return True if successful.""" + try: + json.loads(s) + return True + except Exception: + return False + + +def _try_decode_b64_to_json_str(s: str): + """Try to decode a base64 string to a JSON string with various fallback strategies.""" + if s is None: + return None + try: + # strict validation first + decoded = base64.b64decode(s, validate=True) + decoded_str = decoded.decode('utf-8') + if _try_parse_json(decoded_str): + return decoded_str + except Exception: + # Try non-strict decode + try: + decoded = base64.b64decode(s) + decoded_str = decoded.decode('utf-8') + if _try_parse_json(decoded_str): + return decoded_str + except Exception: + pass + # As a last resort, try appending one to four '=' padding chars + for i in range(1, 5): + try: + s_padded = s + ("=" * i) + decoded = base64.b64decode(s_padded) + decoded_str = decoded.decode('utf-8') + if _try_parse_json(decoded_str): + return decoded_str + except Exception: + continue + return None + + +def _safe_hint(s): + """Create a safe string hint for debugging purposes.""" + if s is None: + return "" + hint_len = len(s) + head = s[:8].replace('\n', ' ') + return f"len={hint_len}, startswith='{head}...'" + + +@openziti.zitify() +def doPost(url, payload): + """Post webhook payload to the specified URL over Ziti.""" + # Single request doesn't need session management + response = requests.post(url, json=payload) + print(f"Response Status: {response.status_code}") + print(response.headers) + print(response.content) + return response + + if __name__ == '__main__': url = os.getenv("INPUT_WEBHOOKURL") - eventJsonStr = os.getenv("INPUT_EVENTJSON") + + # Handle event JSON provided inline; auto-detect if it's JSON or base64-encoded JSON + + eventInput = os.getenv("INPUT_EVENTJSON") + eventJson = "" + + if eventInput and _try_parse_json(eventInput): + eventJson = eventInput + print("Detected valid JSON in INPUT_EVENTJSON") + else: + decoded = _try_decode_b64_to_json_str(eventInput) + if decoded is not None: + eventJson = decoded + print("Detected base64-encoded JSON in INPUT_EVENTJSON and decoded it") + + if not eventJson: + print("ERROR: No valid event JSON provided in INPUT_EVENTJSON") + exit(1) username = os.getenv("INPUT_SENDERUSERNAME") icon = os.getenv("INPUT_SENDERICONURL") actionRepo = os.getenv("GITHUB_ACTION_REPOSITORY") eventName = os.getenv("GITHUB_EVENT_NAME") - - # Setup Ziti identity - zitiJwt = os.getenv("INPUT_ZITIJWT") - if zitiJwt is not None: - zitiId = openziti.enroll(zitiJwt) + zitiLogLevel = os.getenv("INPUT_ZITILOGLEVEL") + if zitiLogLevel is not None: + os.environ["ZITI_LOG"] = zitiLogLevel + os.environ["TLSUV_DEBUG"] = zitiLogLevel + + # Set up Ziti identity + zitiJwtInput = os.getenv("INPUT_ZITIJWT") + zitiIdJson = None # validated JSON string form + zitiIdEncoding = None # validated base64 string form + zitiIdContext = None # deserialized dict + if zitiJwtInput is not None: + # Expect enroll to return the identity JSON content + try: + enrolled = openziti.enroll(zitiJwtInput) + # Validate that the returned content is JSON + zitiIdContext = json.loads(enrolled) + zitiIdJson = enrolled + print("Obtained valid identity JSON from INPUT_ZITIJWT enrollment") + except Exception as e: + print(f"ERROR: Failed to enroll or parse identity from INPUT_ZITIJWT: {e}") + exit(1) else: - zitiId = os.getenv("INPUT_ZITIID") - - if zitiId is None: - print("ERROR: no Ziti identity provided, set INPUT_ZITIID or INPUT_ZITIJWT") - exit(1) - - def generate_json_schema(obj, max_depth=10, current_depth=0): - """Generate a schema representation of a JSON object by inferring types from values.""" - if current_depth >= max_depth: - return "" - - if obj is None: - return "null" - elif isinstance(obj, bool): - return "boolean" - elif isinstance(obj, int): - return "integer" - elif isinstance(obj, float): - return "number" - elif isinstance(obj, str): - return "string" - elif isinstance(obj, list): - if len(obj) == 0: - return "array[]" - # Get schema of first element as representative - element_schema = generate_json_schema(obj[0], max_depth, current_depth + 1) - return f"array[{element_schema}]" - elif isinstance(obj, dict): - schema = {} - for key, value in obj.items(): - schema[key] = generate_json_schema(value, max_depth, current_depth + 1) - return schema + # Support inline JSON or base64-encoded identity JSON from a single variable + zitiIdInput = os.getenv("INPUT_ZITIID") + + # Prefer valid inline JSON if present + if zitiIdInput and _try_parse_json(zitiIdInput): + zitiIdJson = zitiIdInput + zitiIdContext = json.loads(zitiIdInput) + print("Detected valid inline JSON in INPUT_ZITIID") else: - return f"unknown_type({type(obj).__name__})" - - # Validate zitiId as JSON - try: - zitiIdJson = json.loads(zitiId) - except Exception as e: - print(f"ERROR: zitiId is not valid JSON: {e}") - print(f"zitiId content: {zitiId}") - exit(1) + # Try decoding inline as base64 if provided and not valid JSON + decodedInline = _try_decode_b64_to_json_str(zitiIdInput) if zitiIdInput else None + if decodedInline is not None: + zitiIdEncoding = zitiIdInput + zitiIdJson = decodedInline + zitiIdContext = json.loads(decodedInline) + print("Detected base64-encoded identity in INPUT_ZITIID and decoded it") + + if zitiIdJson is None: + print("ERROR: no Ziti identity provided, set INPUT_ZITIID (inline JSON or base64-encoded), or INPUT_ZITIJWT") + exit(1) idFilename = "id.json" with open(idFilename, 'w') as f: - f.write(zitiId) + f.write(zitiIdJson) - # Load the identity file after it's been written and closed - try: - openziti.load(idFilename) - except Exception as e: - print(f"ERROR: Failed to load Ziti identity: {e}") - schema = generate_json_schema(zitiIdJson) - print(f"DEBUG: zitiId schema for troubleshooting: {json.dumps(schema, indent=2)}") - raise e + # Defer openziti.load() until inside the monkeypatch context to keep + # initialization/teardown paired and avoid double-free on shutdown. # Create webhook body try: - mwb = MattermostWebhookBody(username, icon, eventName, eventJsonStr, actionRepo) + mwb = MattermostWebhookBody(username, icon, eventName, eventJson, actionRepo) except Exception as e: print(f"Exception creating webhook body: {e}") raise e # Post the webhook over Ziti - headers = {'Content-Type': 'application/json'} - data = mwb.dumpJson() - debug = os.getenv("ZHOOK_DEBUG", "").casefold() == "true" + # Build dict payload; requests will set Content-Type when using json= + payload = mwb.body + # Load the identity for Ziti operations try: - with openziti.monkeypatch(): - if debug: - print(f"Posting webhook to {url} with headers {headers} and data {data}") - else: - print(f"Posting webhook to {url} with headers {headers}") - - r = requests.post(url, headers=headers, data=data) - print(f"Response Status: {r.status_code}") - - if debug: - print(f"Response HEADERS: {r.headers}") - print(f"Response CONTENT: {r.content}") - - if 200 <= r.status_code < 300: - print(f"INFO: successfully posted. status code {r.status_code}") - sys.exit(0) - else: - print(f"ERROR: unexpected status code {r.status_code}") - sys.exit(1) + openziti.load(idFilename) + except Exception as e: + print(f"ERROR: Failed to load Ziti identity: {e}") + print(f"DEBUG: INPUT_ZITIID hint: {_safe_hint(os.getenv('INPUT_ZITIID'))}") + print(f"DEBUG: zitiIdJson len={len(zitiIdJson) if zitiIdJson else 0}") + raise e + # Post the webhook over Ziti + try: + print(f"Posting webhook to {url} with JSON payload keys {list(payload.keys())}") + response = doPost(url, payload) except Exception as e: - print(f"Exception in webhook or ziti context: {e}") - sys.exit(0) + print(f"Exception posting webhook: {e}") + raise e From 1e39dc450e30d7d214e6547a16aaaf067448e5c3 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Sun, 29 Jun 2025 18:39:19 -0400 Subject: [PATCH 36/52] update py webhook action to use a channel-specific url; add debug logging to self-tests --- .github/workflows/zhook.yml | 2 +- action.yml | 4 ++-- zhook.py | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index 711cba4..c5f2323 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -157,4 +157,4 @@ jobs: ${{ github.workspace }}/*-valgrind-*.log ${{ github.workspace }}/core.* ${{ github.workspace }}/vgcore.* - if-no-files-found: ignore \ No newline at end of file + if-no-files-found: ignore diff --git a/action.yml b/action.yml index 536ab34..90f2a05 100644 --- a/action.yml +++ b/action.yml @@ -24,11 +24,11 @@ inputs: destChannel: description: Mattermost channel (ignored because incoming webhooks are locked to a channel) required: false - default: null + default: "" zitiLogLevel: description: Ziti log level required: false - default: 3 + default: "3" runs: using: docker image: Dockerfile diff --git a/zhook.py b/zhook.py index a01c384..69bf263 100644 --- a/zhook.py +++ b/zhook.py @@ -478,6 +478,43 @@ def doPost(url, payload): print("ERROR: no Ziti identity provided, set INPUT_ZITIID (inline JSON or base64-encoded), or INPUT_ZITIJWT") exit(1) + def generate_json_schema(obj, max_depth=10, current_depth=0): + """Generate a schema representation of a JSON object by inferring types from values.""" + if current_depth >= max_depth: + return "" + + if obj is None: + return "null" + elif isinstance(obj, bool): + return "boolean" + elif isinstance(obj, int): + return "integer" + elif isinstance(obj, float): + return "number" + elif isinstance(obj, str): + return "string" + elif isinstance(obj, list): + if len(obj) == 0: + return "array[]" + # Get schema of first element as representative + element_schema = generate_json_schema(obj[0], max_depth, current_depth + 1) + return f"array[{element_schema}]" + elif isinstance(obj, dict): + schema = {} + for key, value in obj.items(): + schema[key] = generate_json_schema(value, max_depth, current_depth + 1) + return schema + else: + return f"unknown_type({type(obj).__name__})" + + # Validate zitiId as JSON + try: + zitiIdJson = json.loads(zitiId) + except Exception as e: + print(f"ERROR: zitiId is not valid JSON: {e}") + print(f"zitiId content: {zitiId}") + exit(1) + idFilename = "id.json" with open(idFilename, 'w') as f: f.write(zitiIdJson) From 433a185545de1480f719eb1840bf4138d46a6178 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Thu, 24 Jul 2025 17:29:37 -0400 Subject: [PATCH 37/52] also debug docker action --- .github/workflows/zhook.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index c5f2323..9787524 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -104,6 +104,30 @@ jobs: python /app/zhook.py; ' + - name: Run as Docker Action with Core Dumps + if: | + github.repository_owner == 'openziti' + && ((github.event_name != 'pull_request_review') + || (github.event_name == 'pull_request_review' && github.event.review.state == 'approved')) + shell: bash + run: | + set -o pipefail + set -o xtrace + docker build -t zhook-action . + docker run --rm \ + -v "${{ github.workspace }}:${{ github.workspace }}" \ + -w "${{ github.workspace }}" \ + -e INPUT_ZITIID="${{ secrets.ZITI_MATTERMOST_IDENTITY }}" \ + -e INPUT_WEBHOOKURL="${{ secrets.ZHOOK_URL_DEV_NOTIFICATIONS }}" \ + -e INPUT_EVENTJSON='${{ toJson(github.event) }}' \ + -e INPUT_SENDERUSERNAME="GitHubZ" \ + -e INPUT_ZITILOGLEVEL="6" \ + --entrypoint /bin/sh \ + zhook-action -xc " + ulimit -c unlimited + exec /app/zhook.py + " + - uses: ./ name: Run as a GH Action from the Local Checkout if: | From f529ce71c09aa8460543df95e2d7340d2a64193e Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Thu, 24 Jul 2025 17:33:55 -0400 Subject: [PATCH 38/52] fiddle escapes --- .github/workflows/zhook.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index 9787524..06f4a91 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -104,7 +104,7 @@ jobs: python /app/zhook.py; ' - - name: Run as Docker Action with Core Dumps + - name: Run in Docker with Core Dumps if: | github.repository_owner == 'openziti' && ((github.event_name != 'pull_request_review') @@ -117,11 +117,11 @@ jobs: docker run --rm \ -v "${{ github.workspace }}:${{ github.workspace }}" \ -w "${{ github.workspace }}" \ - -e INPUT_ZITIID="${{ secrets.ZITI_MATTERMOST_IDENTITY }}" \ - -e INPUT_WEBHOOKURL="${{ secrets.ZHOOK_URL_DEV_NOTIFICATIONS }}" \ - -e INPUT_EVENTJSON='${{ toJson(github.event) }}' \ - -e INPUT_SENDERUSERNAME="GitHubZ" \ - -e INPUT_ZITILOGLEVEL="6" \ + -e "INPUT_ZITIID=${{ secrets.ZITI_MATTERMOST_IDENTITY }}" \ + -e "INPUT_WEBHOOKURL=${{ secrets.ZHOOK_URL_DEV_NOTIFICATIONS }}" \ + -e "INPUT_EVENTJSON=${{ toJson(github.event) }}" \ + -e "INPUT_SENDERUSERNAME=GitHubZ" \ + -e "INPUT_ZITILOGLEVEL=6" \ --entrypoint /bin/sh \ zhook-action -xc " ulimit -c unlimited From eac677f50e843283750b656e71f7d6df1a427202 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Thu, 24 Jul 2025 17:38:07 -0400 Subject: [PATCH 39/52] use env file --- .github/workflows/zhook.yml | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index 06f4a91..8b65b33 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -110,18 +110,32 @@ jobs: && ((github.event_name != 'pull_request_review') || (github.event_name == 'pull_request_review' && github.event.review.state == 'approved')) shell: bash + env: + INPUT_ZITIID: ${{ secrets.ZITI_MATTERMOST_IDENTITY }} + INPUT_WEBHOOKURL: ${{ secrets.ZHOOK_URL_DEV_NOTIFICATIONS }} + INPUT_EVENTJSON: ${{ toJson(github.event) }} + INPUT_SENDERUSERNAME: GitHubZ + INPUT_SENDERICONURL: https://github.com/fluidicon.png + INPUT_ZITILOGLEVEL: 6 run: | set -o pipefail set -o xtrace + + # Create environment file for Docker + cat > /tmp/docker.env << 'EOF' + INPUT_ZITIID=${INPUT_ZITIID} + INPUT_WEBHOOKURL=${INPUT_WEBHOOKURL} + INPUT_EVENTJSON=${INPUT_EVENTJSON} + INPUT_SENDERUSERNAME=${INPUT_SENDERUSERNAME} + INPUT_SENDERICONURL=${INPUT_SENDERICONURL} + INPUT_ZITILOGLEVEL=${INPUT_ZITILOGLEVEL} + EOF + docker build -t zhook-action . docker run --rm \ -v "${{ github.workspace }}:${{ github.workspace }}" \ -w "${{ github.workspace }}" \ - -e "INPUT_ZITIID=${{ secrets.ZITI_MATTERMOST_IDENTITY }}" \ - -e "INPUT_WEBHOOKURL=${{ secrets.ZHOOK_URL_DEV_NOTIFICATIONS }}" \ - -e "INPUT_EVENTJSON=${{ toJson(github.event) }}" \ - -e "INPUT_SENDERUSERNAME=GitHubZ" \ - -e "INPUT_ZITILOGLEVEL=6" \ + --env-file /tmp/docker.env \ --entrypoint /bin/sh \ zhook-action -xc " ulimit -c unlimited From 376fb77cdebed466905604a272274bfa87631442 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Thu, 24 Jul 2025 17:45:27 -0400 Subject: [PATCH 40/52] debug in bash container --- .github/workflows/zhook.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index 8b65b33..f74d061 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -132,12 +132,13 @@ jobs: EOF docker build -t zhook-action . + docker build -t zhook-action-dbg -f debug.Dockerfile . docker run --rm \ -v "${{ github.workspace }}:${{ github.workspace }}" \ -w "${{ github.workspace }}" \ --env-file /tmp/docker.env \ - --entrypoint /bin/sh \ - zhook-action -xc " + --entrypoint /bin/bash \ + zhook-action-dbg -xc " ulimit -c unlimited exec /app/zhook.py " From f93288aa2c9e031bffcb1515a6ee6dc295691340 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Thu, 24 Jul 2025 17:56:48 -0400 Subject: [PATCH 41/52] beef up backtrace script --- .github/workflows/zhook.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index f74d061..384ace3 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -121,7 +121,6 @@ jobs: set -o pipefail set -o xtrace - # Create environment file for Docker cat > /tmp/docker.env << 'EOF' INPUT_ZITIID=${INPUT_ZITIID} INPUT_WEBHOOKURL=${INPUT_WEBHOOKURL} From 6b763cafaca4b939ef2a15fa0a2b3cd182548029 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Thu, 24 Jul 2025 17:59:21 -0400 Subject: [PATCH 42/52] add interpreter to zhook.py --- zhook.py | 1 - 1 file changed, 1 deletion(-) diff --git a/zhook.py b/zhook.py index 69bf263..607f7b3 100644 --- a/zhook.py +++ b/zhook.py @@ -3,7 +3,6 @@ import base64 import json import os -import sys import openziti import requests From 39f5070b9975b1745a6ea04987c844924496b7e6 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Thu, 24 Jul 2025 18:02:31 -0400 Subject: [PATCH 43/52] interpolate vars in heredoc --- .github/workflows/zhook.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index 384ace3..caccb9e 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -121,13 +121,13 @@ jobs: set -o pipefail set -o xtrace - cat > /tmp/docker.env << 'EOF' - INPUT_ZITIID=${INPUT_ZITIID} - INPUT_WEBHOOKURL=${INPUT_WEBHOOKURL} - INPUT_EVENTJSON=${INPUT_EVENTJSON} - INPUT_SENDERUSERNAME=${INPUT_SENDERUSERNAME} - INPUT_SENDERICONURL=${INPUT_SENDERICONURL} - INPUT_ZITILOGLEVEL=${INPUT_ZITILOGLEVEL} + cat > /tmp/docker.env << EOF + INPUT_ZITIID="${INPUT_ZITIID}" + INPUT_WEBHOOKURL="${INPUT_WEBHOOKURL}" + INPUT_EVENTJSON="${INPUT_EVENTJSON}" + INPUT_SENDERUSERNAME="${INPUT_SENDERUSERNAME}" + INPUT_SENDERICONURL="${INPUT_SENDERICONURL}" + INPUT_ZITILOGLEVEL="${INPUT_ZITILOGLEVEL}" EOF docker build -t zhook-action . From 30f15f35a3f343499773c56e9f5eeb76deea63ee Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Fri, 25 Jul 2025 12:33:19 -0400 Subject: [PATCH 44/52] test the script with valgrind in docker too --- .github/workflows/zhook.yml | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index caccb9e..dfbdfc1 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -128,19 +128,28 @@ jobs: INPUT_SENDERUSERNAME="${INPUT_SENDERUSERNAME}" INPUT_SENDERICONURL="${INPUT_SENDERICONURL}" INPUT_ZITILOGLEVEL="${INPUT_ZITILOGLEVEL}" + GITHUB_WORKSPACE="${GITHUB_WORKSPACE}" EOF + # configure the kernel to write core dumps to the workspace directory that is writable by the container + sudo sysctl -w kernel.core_pattern="${GITHUB_WORKSPACE}/core.%e.%p.%t" + + # build the action's container image so we can source it for the debug image docker build -t zhook-action . docker build -t zhook-action-dbg -f debug.Dockerfile . docker run --rm \ - -v "${{ github.workspace }}:${{ github.workspace }}" \ - -w "${{ github.workspace }}" \ + --volume "${GITHUB_WORKSPACE}:${GITHUB_WORKSPACE}" \ + --workdir "${GITHUB_WORKSPACE}" \ --env-file /tmp/docker.env \ - --entrypoint /bin/bash \ - zhook-action-dbg -xc " - ulimit -c unlimited - exec /app/zhook.py - " + --entrypoint=/bin/bash \ + zhook-action-dbg -euxo pipefail -c ' + ulimit -c unlimited; + exec valgrind \ + --verbose \ + --log-file=${GITHUB_WORKSPACE}/docker-valgrind-%p-%n.log \ + --leak-check=yes \ + python /app/zhook.py; + ' - uses: ./ name: Run as a GH Action from the Local Checkout From baedce7498d8ef56fbf5989013e3e88dd143b5c2 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Fri, 25 Jul 2025 13:19:18 -0400 Subject: [PATCH 45/52] pass json as an encoding to avoid word-splitting errors --- .github/workflows/zhook.yml | 19 +++++++++++-------- zhook.py | 1 + 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index dfbdfc1..cf54d7b 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -121,17 +121,20 @@ jobs: set -o pipefail set -o xtrace + # Base64 encode JSON to avoid whitespace issues in env file + ENCODED_JSON=$(echo "${INPUT_EVENTJSON}" | base64 -w 0) + cat > /tmp/docker.env << EOF - INPUT_ZITIID="${INPUT_ZITIID}" - INPUT_WEBHOOKURL="${INPUT_WEBHOOKURL}" - INPUT_EVENTJSON="${INPUT_EVENTJSON}" - INPUT_SENDERUSERNAME="${INPUT_SENDERUSERNAME}" - INPUT_SENDERICONURL="${INPUT_SENDERICONURL}" - INPUT_ZITILOGLEVEL="${INPUT_ZITILOGLEVEL}" - GITHUB_WORKSPACE="${GITHUB_WORKSPACE}" + INPUT_ZITIID=${INPUT_ZITIID} + INPUT_WEBHOOKURL=${INPUT_WEBHOOKURL} + INPUT_EVENTJSON_B64=${ENCODED_JSON} + INPUT_SENDERUSERNAME=${INPUT_SENDERUSERNAME} + INPUT_SENDERICONURL=${INPUT_SENDERICONURL} + INPUT_ZITILOGLEVEL=${INPUT_ZITILOGLEVEL} + GITHUB_WORKSPACE=${GITHUB_WORKSPACE} EOF - # configure the kernel to write core dumps to the workspace directory that is writable by the container + # configure the kernel to write core dumps to the workspace directory that is writable by the container in case there is a segfault valgrind cannot catch in a vgcore.%p sudo sysctl -w kernel.core_pattern="${GITHUB_WORKSPACE}/core.%e.%p.%t" # build the action's container image so we can source it for the debug image diff --git a/zhook.py b/zhook.py index 607f7b3..583cc9b 100644 --- a/zhook.py +++ b/zhook.py @@ -3,6 +3,7 @@ import base64 import json import os +import base64 import openziti import requests From be695b61c974ee0e5d02b6f8664438e088c7a77b Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Fri, 25 Jul 2025 13:33:46 -0400 Subject: [PATCH 46/52] add required vars to the docker env --- .github/workflows/zhook.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index cf54d7b..1453482 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -132,6 +132,8 @@ jobs: INPUT_SENDERICONURL=${INPUT_SENDERICONURL} INPUT_ZITILOGLEVEL=${INPUT_ZITILOGLEVEL} GITHUB_WORKSPACE=${GITHUB_WORKSPACE} + GITHUB_EVENT_NAME=${GITHUB_EVENT_NAME} + GITHUB_ACTION_REPOSITORY=${GITHUB_ACTION_REPOSITORY} EOF # configure the kernel to write core dumps to the workspace directory that is writable by the container in case there is a segfault valgrind cannot catch in a vgcore.%p From 6c6c4331d2e2b32bab01e3982495a1d6cb022105 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Thu, 21 Aug 2025 16:51:55 -0400 Subject: [PATCH 47/52] validate json more robustly and hint at contents if invalid --- zhook.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/zhook.py b/zhook.py index 583cc9b..595cf98 100644 --- a/zhook.py +++ b/zhook.py @@ -507,12 +507,21 @@ def generate_json_schema(obj, max_depth=10, current_depth=0): else: return f"unknown_type({type(obj).__name__})" - # Validate zitiId as JSON + # Accept only inline JSON for zitiId; do not interpret as file path or base64 + def _safe_hint(s): + if s is None: + return "" + l = len(s) + head = s[:8].replace('\n', ' ') + return f"len={l}, startswith='{head}...'" + try: zitiIdJson = json.loads(zitiId) + zitiIdContent = zitiId except Exception as e: - print(f"ERROR: zitiId is not valid JSON: {e}") - print(f"zitiId content: {zitiId}") + print("ERROR: zitiId must be inline JSON (not a file path or base64).") + print(f"DEBUG: INPUT_ZITIID hint: {_safe_hint(zitiId)}") + print(f"DEBUG: json error: {e}") exit(1) idFilename = "id.json" From 2b961396e24a2d982e07b2b7e72ec11d7812a386 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Thu, 21 Aug 2025 18:02:27 -0400 Subject: [PATCH 48/52] move lint exceptions to project file so contributors can follow same conventions --- zhook.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zhook.py b/zhook.py index 595cf98..643016d 100644 --- a/zhook.py +++ b/zhook.py @@ -511,9 +511,9 @@ def generate_json_schema(obj, max_depth=10, current_depth=0): def _safe_hint(s): if s is None: return "" - l = len(s) + hint_len = len(s) head = s[:8].replace('\n', ' ') - return f"len={l}, startswith='{head}...'" + return f"len={hint_len}, startswith='{head}...'" try: zitiIdJson = json.loads(zitiId) From 3826bbeb98c7f6b2867f27fe8efe1aedf4711bea Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Fri, 22 Aug 2025 18:39:31 -0400 Subject: [PATCH 49/52] checkpoint --- .github/workflows/zhook.yml | 5 +---- zhook.py | 23 ++++++++++++----------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index 1453482..ff7174b 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -121,13 +121,10 @@ jobs: set -o pipefail set -o xtrace - # Base64 encode JSON to avoid whitespace issues in env file - ENCODED_JSON=$(echo "${INPUT_EVENTJSON}" | base64 -w 0) - cat > /tmp/docker.env << EOF INPUT_ZITIID=${INPUT_ZITIID} INPUT_WEBHOOKURL=${INPUT_WEBHOOKURL} - INPUT_EVENTJSON_B64=${ENCODED_JSON} + INPUT_EVENTJSON=${INPUT_EVENTJSON} INPUT_SENDERUSERNAME=${INPUT_SENDERUSERNAME} INPUT_SENDERICONURL=${INPUT_SENDERICONURL} INPUT_ZITILOGLEVEL=${INPUT_ZITILOGLEVEL} diff --git a/zhook.py b/zhook.py index 643016d..433261e 100644 --- a/zhook.py +++ b/zhook.py @@ -505,9 +505,19 @@ def generate_json_schema(obj, max_depth=10, current_depth=0): schema[key] = generate_json_schema(value, max_depth, current_depth + 1) return schema else: - return f"unknown_type({type(obj).__name__})" + # Try decoding inline as base64 if provided and not valid JSON + decodedInline = _try_decode_b64_to_json_str(zitiIdInput) if zitiIdInput else None + if decodedInline is not None: + zitiIdEncoding = zitiIdInput + zitiIdJson = decodedInline + zitiIdContext = json.loads(decodedInline) + print("Detected base64-encoded identity in INPUT_ZITIID and decoded it") - # Accept only inline JSON for zitiId; do not interpret as file path or base64 + if zitiIdJson is None: + print("ERROR: no Ziti identity provided, set INPUT_ZITIID (inline JSON or base64-encoded), or INPUT_ZITIJWT") + exit(1) + + # Keep a small helper for safe string hints (used only in error/debug prints if needed) def _safe_hint(s): if s is None: return "" @@ -515,15 +525,6 @@ def _safe_hint(s): head = s[:8].replace('\n', ' ') return f"len={hint_len}, startswith='{head}...'" - try: - zitiIdJson = json.loads(zitiId) - zitiIdContent = zitiId - except Exception as e: - print("ERROR: zitiId must be inline JSON (not a file path or base64).") - print(f"DEBUG: INPUT_ZITIID hint: {_safe_hint(zitiId)}") - print(f"DEBUG: json error: {e}") - exit(1) - idFilename = "id.json" with open(idFilename, 'w') as f: f.write(zitiIdJson) From 30a284eac55d39b2baf88f16af08275cf1f24abe Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Mon, 29 Sep 2025 11:15:42 -0400 Subject: [PATCH 50/52] encode event json to avoid whitespace errors --- .github/workflows/zhook.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/zhook.yml b/.github/workflows/zhook.yml index ff7174b..ede75f9 100644 --- a/.github/workflows/zhook.yml +++ b/.github/workflows/zhook.yml @@ -124,7 +124,7 @@ jobs: cat > /tmp/docker.env << EOF INPUT_ZITIID=${INPUT_ZITIID} INPUT_WEBHOOKURL=${INPUT_WEBHOOKURL} - INPUT_EVENTJSON=${INPUT_EVENTJSON} + INPUT_EVENTJSON=$(base64 -w 0 <<< '${{ toJson(github.event) }}') INPUT_SENDERUSERNAME=${INPUT_SENDERUSERNAME} INPUT_SENDERICONURL=${INPUT_SENDERICONURL} INPUT_ZITILOGLEVEL=${INPUT_ZITILOGLEVEL} From eaddcf0dffa30bb66a39be87e3f7ede4bc7c96cc Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Mon, 20 Oct 2025 14:48:30 -0400 Subject: [PATCH 51/52] add test mode --- README.md | 52 +++++++++++ TESTING.md | 42 +++++++++ zhook.py | 257 ++++++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 297 insertions(+), 54 deletions(-) create mode 100644 TESTING.md diff --git a/README.md b/README.md index a384b63..7395e21 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,58 @@ The identity can be created by enrolling via the `ziti edge enroll path/to/jwt [ This input value is a Mattermost "Incoming Webhook" URL available over an OpenZiti Network to the identity specified by `zitiId`. This URL should be configured in Mattermost to allow posting to any valid channel with any sender username. The default username will be the `sender.login` from the GitHub Action event. +## Testing + +Test `zhook.py` locally before deploying it as a GitHub Action using the built-in test mode: + +### Basic Usage + +```bash +# Quick test with default push event +INPUT_ZITIID="$(< /path/to/ziti-identity.json)" \ +INPUT_WEBHOOKURL="http://webhook.mattermost.ziti/hooks/YOUR_ID" \ +python3 zhook.py --test + +# Test different event types +python3 zhook.py --test --event-type pull_request +python3 zhook.py --test --event-type issues +python3 zhook.py --test --event-type release + +# Preview payload without sending (dry-run) +python3 zhook.py --test --event-type push --dry-run +``` + +### Available Options + +**Event types:** `push`, `pull_request`, `issues`, `release`, `watch`, `fork` + +**Flags:** + +- `--test`: Enable test mode with generated event data +- `--event-type TYPE`: Specify which GitHub event to simulate (default: push) +- `--dry-run`: Preview the webhook payload without sending it + +**Environment variables:** + +- `INPUT_ZITIID`: Ziti identity JSON (required, or use `INPUT_ZITIJWT`) +- `INPUT_ZITIJWT`: Ziti enrollment JWT (alternative to `INPUT_ZITIID`) +- `INPUT_WEBHOOKURL`: Mattermost webhook URL (uses default if not set in test mode) +- `INPUT_SENDERUSERNAME`: Override sender username (optional) +- `INPUT_SENDERICONURL`: Override sender icon URL (optional) +- `GITHUB_ACTION_REPOSITORY`: Override repository name (optional) +- `ZITI_LOG`: Ziti log level 0-6 (default: 3) + +### Advanced: Manual Event JSON + +For testing with custom event data, provide your own `INPUT_EVENTJSON`: + +```bash +INPUT_ZITIID="$(< /path/to/ziti-identity.json)" \ +INPUT_WEBHOOKURL="http://webhook.mattermost.ziti/hooks/YOUR_ID" \ +INPUT_EVENTJSON='{"repository": {...}, "sender": {...}}' \ +GITHUB_EVENT_NAME="push" \ +python3 zhook.py +``` ## Updating the Container diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..86b7b0a --- /dev/null +++ b/TESTING.md @@ -0,0 +1,42 @@ +# Testing zhook.py + +## Quick Start + +Run tests with dummy event data using the `--test` flag: + +```bash +# Basic test with push event +INPUT_ZITIID="$(< ziti-id.json)" \ +INPUT_WEBHOOKURL="http://webhook.mattermost.ziti.internal/hooks/" \ +python3 zhook.py --test + +# Test different event types +python3 zhook.py --test --event-type pull_request +python3 zhook.py --test --event-type issues + +# Preview payload without sending +python3 zhook.py --test --dry-run +``` + +## Required Inputs + +**Ziti Identity** (one of): +- `INPUT_ZITIID` - Identity JSON, e.g., `"$(< file.json)"` or unescaped string +- `INPUT_ZITIID` - Base64-encoded identity JSON +- `INPUT_ZITIJWT` - Enrollment JWT token + +**Webhook URL**: +- `INPUT_WEBHOOKURL` - Mattermost webhook URL accessible via Ziti +- Optional in test mode (defaults to `http://127.0.0.1:2171/post` for httpbin testing) + +## Test Modes + +**`--test`** - Generate dummy GitHub event data automatically + +**`--event-type TYPE`** - Choose event: `push`, `pull_request`, `issues`, `release`, `watch`, `fork` (default: push) + +**`--dry-run`** - Print payload without sending (no Ziti connection needed) + +## Help + +Run `python3 zhook.py --help` to see all environment variables and their GitHub Actions sources. diff --git a/zhook.py b/zhook.py index 433261e..171dfd8 100644 --- a/zhook.py +++ b/zhook.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 +import argparse import base64 import json import os -import base64 +import sys import openziti import requests @@ -400,6 +401,127 @@ def _safe_hint(s): return f"len={hint_len}, startswith='{head}...'" +def generate_json_schema(obj, max_depth=10, current_depth=0): + """Generate a schema representation of a JSON object by inferring types from values.""" + if current_depth >= max_depth: + return "" + + if obj is None: + return "null" + elif isinstance(obj, bool): + return "boolean" + elif isinstance(obj, int): + return "integer" + elif isinstance(obj, float): + return "number" + elif isinstance(obj, str): + return "string" + elif isinstance(obj, list): + if len(obj) == 0: + return "array[]" + # Get schema of first element as representative + element_schema = generate_json_schema(obj[0], max_depth, current_depth + 1) + return f"array[{element_schema}]" + elif isinstance(obj, dict): + schema = {} + for key, value in obj.items(): + schema[key] = generate_json_schema(value, max_depth, current_depth + 1) + return schema + else: + return "unknown" + + +def generate_test_event(event_type): + """Generate a test GitHub event JSON for the specified event type.""" + base_repo = { + "full_name": "testuser/testrepo", + "html_url": "https://github.com/testuser/testrepo", + "stargazers_count": 42 + } + + base_sender = { + "login": "testuser", + "avatar_url": "https://avatars.githubusercontent.com/u/12345", + "html_url": "https://github.com/testuser", + "url": "https://api.github.com/users/testuser" + } + + events = { + "push": { + "repository": base_repo, + "sender": base_sender, + "forced": False, + "commits": [ + { + "id": "abc123def456", + "url": "https://github.com/testuser/testrepo/commit/abc123", + "message": "Test commit message" + } + ], + "compare": "https://github.com/testuser/testrepo/compare/abc123..def456", + "ref": "refs/heads/main" + }, + "pull_request": { + "action": "opened", + "repository": base_repo, + "sender": base_sender, + "pull_request": { + "number": 123, + "title": "Test Pull Request", + "html_url": "https://github.com/testuser/testrepo/pull/123", + "body": "This is a test PR description", + "head": {"label": "testuser:feature-branch"}, + "base": {"label": "testuser:main"}, + "requested_reviewers": [] + } + }, + "issues": { + "action": "opened", + "repository": base_repo, + "sender": base_sender, + "issue": { + "number": 456, + "title": "Test Issue", + "html_url": "https://github.com/testuser/testrepo/issues/456", + "body": "This is a test issue description", + "assignees": [] + } + }, + "release": { + "action": "released", + "repository": base_repo, + "sender": base_sender, + "release": { + "name": "v1.0.0", + "tag_name": "v1.0.0", + "html_url": "https://github.com/testuser/testrepo/releases/tag/v1.0.0", + "body": "## What's Changed\n- Feature A\n- Bug fix B", + "draft": False, + "prerelease": False + } + }, + "watch": { + "action": "started", + "repository": base_repo, + "sender": base_sender + }, + "fork": { + "repository": base_repo, + "sender": base_sender, + "forkee": { + "full_name": "anotheruser/testrepo", + "html_url": "https://github.com/anotheruser/testrepo" + } + } + } + + if event_type not in events: + available = ", ".join(sorted(events.keys())) + raise ValueError(f"Unknown event type: {event_type}. Available: {available}") + + return json.dumps(events[event_type]) + + @openziti.zitify() def doPost(url, payload): """Post webhook payload to the specified URL over Ziti.""" @@ -412,6 +534,76 @@ def doPost(url, payload): if __name__ == '__main__': + # Parse command-line arguments + parser = argparse.ArgumentParser( + description='Post GitHub events to Mattermost over Ziti', + epilog=''' +Environment Variables (set by GitHub Actions or manually for testing): + +Required: + INPUT_ZITIID Ziti identity JSON (from: secrets.ZITI_IDENTITY) + Alternative: INPUT_ZITIJWT for enrollment JWT + INPUT_WEBHOOKURL Mattermost webhook URL (from: secrets.WEBHOOK_URL) + INPUT_EVENTJSON GitHub event JSON (from: toJson(github.event)) + GITHUB_EVENT_NAME Event type (from: github.event_name) + Examples: push, pull_request, issues, release + +Optional: + INPUT_SENDERUSERNAME Mattermost username (default: sender.login from event) + INPUT_SENDERICONURL Icon URL (default: sender.avatar_url from event) + GITHUB_ACTION_REPOSITORY Action repo (from: github.action_repository) + INPUT_ZITILOGLEVEL Ziti log level 0-6 (default: 3) + +Test Mode Examples: + # Quick test with push event + INPUT_ZITIID="$(< ziti-id.json)" INPUT_WEBHOOKURL="http://webhook.ziti/hooks/ID" \\ + python3 zhook.py --test + + # Test pull request with dry-run + INPUT_ZITIID="$(< ziti-id.json)" python3 zhook.py --test --event-type pull_request --dry-run + + # List available event types + python3 zhook.py --help +''', + formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument( + '--test', + action='store_true', + help='Run in test mode with generated event data' + ) + parser.add_argument( + '--event-type', + default='push', + choices=['push', 'pull_request', 'issues', 'release', 'watch', 'fork'], + help='Event type for test mode (default: push)' + ) + parser.add_argument( + '--dry-run', + action='store_true', + help='Print the webhook payload without sending it' + ) + + args = parser.parse_args() + + # Test mode: generate dummy data + if args.test: + print(f"=== TEST MODE: Generating {args.event_type} event ===") + if not os.getenv("INPUT_WEBHOOKURL"): + os.environ["INPUT_WEBHOOKURL"] = "http://127.0.0.1:2171/post" + print(f"Using default webhook URL: {os.environ['INPUT_WEBHOOKURL']}") + if not os.getenv("INPUT_ZITIID") and not os.getenv("INPUT_ZITIJWT"): + print("ERROR: Test mode requires INPUT_ZITIID or INPUT_ZITIJWT environment variable") + print("Set one of these to your Ziti identity JSON or enrollment JWT") + sys.exit(1) + + os.environ["INPUT_EVENTJSON"] = generate_test_event(args.event_type) + os.environ["GITHUB_EVENT_NAME"] = args.event_type + os.environ["INPUT_SENDERUSERNAME"] = os.getenv("INPUT_SENDERUSERNAME", "TestUser") + os.environ["INPUT_SENDERICONURL"] = os.getenv("INPUT_SENDERICONURL", "https://github.com/fluidicon.png") + os.environ["GITHUB_ACTION_REPOSITORY"] = os.getenv("GITHUB_ACTION_REPOSITORY", "testuser/testrepo") + print("") + url = os.getenv("INPUT_WEBHOOKURL") # Handle event JSON provided inline; auto-detect if it's JSON or base64-encoded JSON @@ -443,14 +635,12 @@ def doPost(url, payload): # Set up Ziti identity zitiJwtInput = os.getenv("INPUT_ZITIJWT") zitiIdJson = None # validated JSON string form - zitiIdEncoding = None # validated base64 string form - zitiIdContext = None # deserialized dict if zitiJwtInput is not None: # Expect enroll to return the identity JSON content try: enrolled = openziti.enroll(zitiJwtInput) # Validate that the returned content is JSON - zitiIdContext = json.loads(enrolled) + json.loads(enrolled) zitiIdJson = enrolled print("Obtained valid identity JSON from INPUT_ZITIJWT enrollment") except Exception as e: @@ -463,68 +653,18 @@ def doPost(url, payload): # Prefer valid inline JSON if present if zitiIdInput and _try_parse_json(zitiIdInput): zitiIdJson = zitiIdInput - zitiIdContext = json.loads(zitiIdInput) print("Detected valid inline JSON in INPUT_ZITIID") else: # Try decoding inline as base64 if provided and not valid JSON decodedInline = _try_decode_b64_to_json_str(zitiIdInput) if zitiIdInput else None if decodedInline is not None: - zitiIdEncoding = zitiIdInput - zitiIdJson = decodedInline - zitiIdContext = json.loads(decodedInline) - print("Detected base64-encoded identity in INPUT_ZITIID and decoded it") - - if zitiIdJson is None: - print("ERROR: no Ziti identity provided, set INPUT_ZITIID (inline JSON or base64-encoded), or INPUT_ZITIJWT") - exit(1) - - def generate_json_schema(obj, max_depth=10, current_depth=0): - """Generate a schema representation of a JSON object by inferring types from values.""" - if current_depth >= max_depth: - return "" - - if obj is None: - return "null" - elif isinstance(obj, bool): - return "boolean" - elif isinstance(obj, int): - return "integer" - elif isinstance(obj, float): - return "number" - elif isinstance(obj, str): - return "string" - elif isinstance(obj, list): - if len(obj) == 0: - return "array[]" - # Get schema of first element as representative - element_schema = generate_json_schema(obj[0], max_depth, current_depth + 1) - return f"array[{element_schema}]" - elif isinstance(obj, dict): - schema = {} - for key, value in obj.items(): - schema[key] = generate_json_schema(value, max_depth, current_depth + 1) - return schema - else: - # Try decoding inline as base64 if provided and not valid JSON - decodedInline = _try_decode_b64_to_json_str(zitiIdInput) if zitiIdInput else None - if decodedInline is not None: - zitiIdEncoding = zitiIdInput zitiIdJson = decodedInline - zitiIdContext = json.loads(decodedInline) print("Detected base64-encoded identity in INPUT_ZITIID and decoded it") if zitiIdJson is None: print("ERROR: no Ziti identity provided, set INPUT_ZITIID (inline JSON or base64-encoded), or INPUT_ZITIJWT") exit(1) - # Keep a small helper for safe string hints (used only in error/debug prints if needed) - def _safe_hint(s): - if s is None: - return "" - hint_len = len(s) - head = s[:8].replace('\n', ' ') - return f"len={hint_len}, startswith='{head}...'" - idFilename = "id.json" with open(idFilename, 'w') as f: f.write(zitiIdJson) @@ -543,6 +683,15 @@ def _safe_hint(s): # Build dict payload; requests will set Content-Type when using json= payload = mwb.body + # Dry-run mode: print payload and exit + if args.dry_run: + print("=== DRY RUN MODE: Webhook payload ===") + print(f"URL: {url}") + print(f"Payload:") + print(json.dumps(payload, indent=2)) + print("=== Dry run complete (not sent) ===") + sys.exit(0) + # Load the identity for Ziti operations try: openziti.load(idFilename) From b5cb599b17b2428018926e57147d1c9c46fb31ee Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Mon, 10 Nov 2025 09:49:55 -0500 Subject: [PATCH 52/52] finish Dockerfile --- Dockerfile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8e4dbc8..205dbc3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11-bullseye +FROM python:3.11-bullseye AS builder COPY requirements.txt /tmp/requirements.txt RUN pip install --target=/app --requirement /tmp/requirements.txt @@ -10,8 +10,6 @@ COPY --chmod=0755 ./zhook.py /app/zhook.py WORKDIR /app COPY ./zhook.py /app/zhook.py -RUN pip install --no-cache-dir requests openziti - ENV PYTHONPATH=/app CMD ["/app/zhook.py"]