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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ repos:
rev: v1.18.2
hooks:
- id: mypy
exclude: ^(tests/)
exclude: ^(tests/|scripts/)
additional_dependencies: [types-requests]
- repo: https://github.com/PyCQA/flake8
rev: "7.3.0"
hooks:
Expand Down
7 changes: 7 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why we need this file

"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@ container-run:
container-build-run: container-build container-run

container-build-test: container-build container-test

smoke-openshift-ci:
./scripts/smoke-openshift-ci-replay.sh
15 changes: 15 additions & 0 deletions development/env.list.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copy to development/env.list (gitignored) and fill secrets.
# See docs/contribution_guide.md and scripts/smoke-openshift-ci-replay.sh

# Prow run under test (example from contribution guide; pick any completed run)
BUILD_ID=1696039978221441024
JOB_NAME=periodic-ci-openshift-pipelines-release-tests-release-v1.11-openshift-pipelines-ocp4.14-lp-interop-openshift-pipelines-interop-aws
JOB_NAME_SAFE=openshift-pipelines-interop-aws

# Staging Jira (recommended for experiments)
JIRA_SERVER_URL=https://redhat.stage.atlassian.net
JIRA_TOKEN=

# firewatch config: include failure_rules; example exercises slack + watcher fields (merge with your rules)
FIREWATCH_DEFAULT_JIRA_PROJECT=LPINTEROP
FIREWATCH_CONFIG='{"failure_rules":[{"step":"*","failure_type":"all","classification":"Smoke","jira_project":"LPINTEROP","slack_channel":"#firewatch-dev","slack_user":"firewatch-tool@redhat.com"}]}'
107 changes: 106 additions & 1 deletion docs/configuration_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
- [`jira_assignee`](#jira_assignee)
- [`jira_priority`](#jira_priority)
- [`jira_security_level`](#jira_security_level)
- [`jira_watchers`](#jira_watchers)
- [`jira_additional_assignees`](#jira_additional_assignees)
- [`slack_channel`](#slack_channel)
- [`slack_user`](#slack_user)
- [Slack Workflow Builder](#slack-workflow-builder)
- [`ignore`](#ignore)
- [`group`](#group)
- [Using a base config file](#using-a-base-config-file)
Expand All @@ -37,7 +42,9 @@ Firewatch was designed to allow for users to define which Jira issues get create
{"step": "*partial-name*", "failure_type": "all", "classification": "Misc.", "jira_project": "OTHER", "jira_component": ["component-1", "component-2", "!default"], "jira_priority": "major", "group": {"name": "some-group", "priority": 1}},
{"step": "*ends-with-this", "failure_type": "test_failure", "classification": "Test failures", "jira_epic": "!default", "jira_additional_labels": ["test-label-1", "test-label-2", "!default"], "group": {"name": "some-group", "priority": 2}},
{"step": "*ignore*", "failure_type": "test_failure", "classification": "NONE", "jira_project": "NONE", "ignore": "true"},
{"step": "affects-version", "failure_type": "all", "classification": "Affects Version", "jira_project": "TEST", "jira_epic": "!default", "jira_affects_version": "4.14", "jira_assignee": "!default"}
{"step": "affects-version", "failure_type": "all", "classification": "Affects Version", "jira_project": "TEST", "jira_epic": "!default", "jira_affects_version": "4.14", "jira_assignee": "!default"},
{"step": "watched-step", "failure_type": "all", "classification": "Watched", "jira_project": "TEST", "jira_watchers": ["user1@redhat.com"], "jira_additional_assignees": ["user2@redhat.com", "!default"]},
{"step": "slack-notify-step", "failure_type": "all", "classification": "Notified", "jira_project": "TEST", "slack_channel": "#my-channel", "slack_user": "some-user@redhat.com"}
],

#OPTIONAL
Expand Down Expand Up @@ -75,6 +82,11 @@ The firewatch configuration is a list of rules, each rule is defined using the f
- [`jira_assignee`](#jiraassignee)
- [`jira_priority`](#jirapriority)
- [`jira_security_level`](#jirasecuritylevel)
- [`jira_watchers`](#jirawatchers)
- [`jira_additional_assignees`](#jiraadditionalassignees)
- [`slack_channel`](#slackchannel)
- [`slack_user`](#slackuser)
- [Slack Workflow Builder](#slack-workflow-builder)
- [`ignore`](#ignore)
- [`group`](#group)

Expand All @@ -93,6 +105,11 @@ The firewatch configuration is a list of rules, each rule is defined using the f
- [`jira_assignee`](#jiraassignee)
- [`jira_priority`](#jirapriority)
- [`jira_security_level`](#jirasecuritylevel)
- [`jira_watchers`](#jirawatchers)
- [`jira_additional_assignees`](#jiraadditionalassignees)
- [`slack_channel`](#slackchannel)
- [`slack_user`](#slackuser)
- [Slack Workflow Builder](#slack-workflow-builder)

## Rule Configuration Value Definitions

Expand Down Expand Up @@ -303,6 +320,94 @@ The security level desired for a bug created using this rule.

---

### `jira_watchers`

A list of email addresses of users who should be added as watchers on the created issue.

**Example:**

- `"jira_watchers": ["user1@redhat.com"]`
- `"jira_watchers": ["user1@redhat.com", "user2@redhat.com"]`
- `"jira_watchers": ["user1@redhat.com", "!default"]` or `"jira_watchers": ["!default"]`
- `$FIREWATCH_DEFAULT_JIRA_WATCHERS` environment variable must be defined.
- Example: `export FIREWATCH_DEFAULT_JIRA_WATCHERS='["default-watcher@redhat.com"]'`

**Notes:**

- Each value must be a valid email address of a user in your Jira instance.
- Users who cannot be found in Jira will be skipped with a warning (this will not block issue creation).

---

### `jira_additional_assignees`

A list of email addresses of users who should be set as additional assignees on the created issue. This populates the "Additional Assignees" custom field in Jira.

**Example:**

- `"jira_additional_assignees": ["user1@redhat.com"]`
- `"jira_additional_assignees": ["user1@redhat.com", "user2@redhat.com"]`
- `"jira_additional_assignees": ["user1@redhat.com", "!default"]` or `"jira_additional_assignees": ["!default"]`
- `$FIREWATCH_DEFAULT_JIRA_ADDITIONAL_ASSIGNEES` environment variable must be defined.
- Example: `export FIREWATCH_DEFAULT_JIRA_ADDITIONAL_ASSIGNEES='["default-assignee@redhat.com"]'`

**Notes:**

- Each value must be a valid email address of a user in your Jira instance.
- Users who cannot be found in Jira will be skipped with a warning (this will not block issue creation).

---

### `slack_channel`

The Slack channel to associate with a Jira issue created by this rule. When set, firewatch adds a `slack-channel:<channel>` label to the issue (with the `#` prefix stripped). Jira Automation rules can then match on this label to route notifications.

**Example:**

- `"slack_channel": "#my-channel"`
- `"slack_channel": "!default"`
- `$FIREWATCH_DEFAULT_SLACK_CHANNEL` environment variable must be defined.
- Example: `export FIREWATCH_DEFAULT_SLACK_CHANNEL="#my-channel"`

**Notes:**

- The `#` prefix is stripped from the label value (e.g., `#my-channel` becomes `slack-channel:my-channel`).
- If the field is absent or empty, no slack-channel label is added.

---

### `slack_user`

The Slack user to associate with a Jira issue created by this rule. You may set an email address (only the part before `@` is used) or a bare username. When set, firewatch adds a `slack-user:<username>` label to the issue. Jira Automation rules can then match on this label to notify the user.

**Example:**

- `"slack_user": "some-user@redhat.com"` (label suffix: `some-user`)
- `"slack_user": "some-user"` (label suffix: `some-user`)

**Notes:**

- If the value contains `@`, everything after `@` is discarded for the label (domain is not stored on the issue).
- If the field is absent or empty, no slack-user label is added.

---

### Slack Workflow Builder

The **`report`** command only turns `slack_channel` / `slack_user` into Jira labels; it does not call Slack or send workflow webhooks. Elsewhere in this repo, the **`jira-escalation`** command posts to Slack via the Slack Web API for a separate escalation workflow; that path does not use these rule fields.

To drive Slack from the labels above, use **Jira Automation** (or another integration) to send an HTTP request to a **Slack workflow** webhook. Have Automation include **flat JSON keys** that match what the workflow expects, usually `slack_channel` and `slack_user`, populated from the issue labels (same values as the label suffixes: channel name without `#`, Slack username only, no email domain).

In **Slack Workflow Builder** for that webhook trigger:

1. Add optional **variables** whose names match the JSON keys exactly (`slack_channel`, `slack_user`).
2. Use those variables in **Send a message** text (for example dedicated lines like “Route channel: …” / “Notify user: …”) or in **branch** conditions (for example run an extra step when `slack_channel` is not empty).
3. Keep any URL-shaped fields your message template uses as **links** non-empty in the Automation payload; empty strings can break workflow steps that wrap values in Slack link syntax.

If variable names or payload keys do not match, Slack will not bind the routing fields even though the issue still has `slack-channel:` / `slack-user:` labels.

---

### `ignore`

A value that be set to "true" or "false" and allows the user to define `step`/`failure_type` combinations that should be ignored when creating tickets.
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ dependencies = [
"google-cloud-storage>=3.1.0",
"hatchling>=1.27.0",
"jinja2>=3.1.6",
"jira>=3.8.0",
"jira>=3.10",
"junitparser>=3.2.0",
"pyhelper-utils>=1.0.13",
"python-simple-logger>=2.0.9",
"slack-sdk>=3.35.0"
"slack-sdk>=3.35.0",
]

[project.urls]
Expand All @@ -38,6 +38,7 @@ commit = [
tests = [
"pytest>=8.3.5",
"pytest-cov>=6.1.1",
"python-dotenv>=1.2.2",
"requests>=2.32.3",
]

Expand Down
78 changes: 78 additions & 0 deletions scripts/jira_issue_endpoint_probe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import json
import os
import subprocess
import sys

import requests

JIRA_CLOUD = "https://redhat.atlassian.net"
ISSUE_URL = f"{JIRA_CLOUD}/rest/api/3/issue"


def _unwrap_kv_payload(data: dict) -> dict:
inner = data.get("data") or {}
nested = inner.get("data")
if isinstance(nested, dict) and ("email" in nested or "token" in nested or "username" in nested):
return nested
return inner if isinstance(inner, dict) else {}


def load_credentials_from_vault() -> tuple[str | None, str | None]:
path = os.environ.get("FIREWATCH_JIRA_VAULT_PATH")
if not path:
return None, None
out = subprocess.check_output(
["vault", "kv", "get", "-format=json", path],
text=True,
)
payload = _unwrap_kv_payload(json.loads(out))
email_key = os.environ.get("FIREWATCH_JIRA_VAULT_EMAIL_FIELD", "email")
token_key = os.environ.get("FIREWATCH_JIRA_VAULT_TOKEN_FIELD", "token")
return payload.get(email_key), payload.get(token_key)


def main() -> None:
print("GET /rest/api/3/issue (no auth)")
r = requests.get(ISSUE_URL, timeout=30)
print(f" status={r.status_code}")
print(f" body snippet: {r.text[:200]!r}")
print()
print("POST /rest/api/3/issue with empty fields (no auth)")
r2 = requests.post(
ISSUE_URL,
json={"fields": {}},
headers={"Content-Type": "application/json"},
timeout=30,
)
print(f" status={r2.status_code}")
print(f" body snippet: {r2.text[:200]!r}")
email = os.getenv("JIRA_EMAIL")
token = os.getenv("JIRA_TOKEN")
if (not email or not token) and os.getenv("FIREWATCH_JIRA_VAULT_PATH"):
ve, vt = load_credentials_from_vault()
if ve and vt:
email, token = ve, vt
print()
print("Loaded JIRA_EMAIL / JIRA_TOKEN from Vault (FIREWATCH_JIRA_VAULT_PATH).")
if not email or not token:
print()
print(
"Set JIRA_EMAIL and JIRA_TOKEN, or source scripts/jira_vault_env.sh "
"(FIREWATCH_JIRA_VAULT_PATH) before running, for an authenticated POST.",
)
sys.exit(0)
print()
print("POST /rest/api/3/issue with invalid fields (auth)")
r3 = requests.post(
ISSUE_URL,
json={"fields": {}},
headers={"Content-Type": "application/json"},
auth=(email, token),
timeout=30,
)
print(f" status={r3.status_code}")
print(f" body snippet: {r3.text[:300]!r}")


if __name__ == "__main__":
main()
23 changes: 23 additions & 0 deletions scripts/jira_vault_env.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail

if [[ -z "${FIREWATCH_JIRA_VAULT_PATH:-}" ]]; then
echo "FIREWATCH_JIRA_VAULT_PATH is not set (KV path to the firewatch-tool Jira secret)." >&2
exit 1
fi

EMAIL_FIELD="${FIREWATCH_JIRA_VAULT_EMAIL_FIELD:-email}"
TOKEN_FIELD="${FIREWATCH_JIRA_VAULT_TOKEN_FIELD:-token}"

JSON=$(vault kv get -format=json "${FIREWATCH_JIRA_VAULT_PATH}")

export JIRA_EMAIL
export JIRA_TOKEN
JIRA_EMAIL=$(echo "${JSON}" | jq -r --arg k "${EMAIL_FIELD}" '.data.data[$k] // .data[$k] // empty')
JIRA_TOKEN=$(echo "${JSON}" | jq -r --arg k "${TOKEN_FIELD}" '.data.data[$k] // .data[$k] // empty')

if [[ -z "${JIRA_EMAIL}" || -z "${JIRA_TOKEN}" ]]; then
echo "Could not read email/token from Vault at ${FIREWATCH_JIRA_VAULT_PATH}." >&2
echo "Set FIREWATCH_JIRA_VAULT_EMAIL_FIELD / FIREWATCH_JIRA_VAULT_TOKEN_FIELD if your KV keys differ." >&2
exit 1
fi
90 changes: 90 additions & 0 deletions scripts/smoke-openshift-ci-replay.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# Replay firewatch against one completed Prow job (Phase 1 of openshift-ci validation).
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this a test? shouldnt this go under test dir

# Prerequisites: development/env.list (see env.list.example). JIRA_TOKEN may be set there or in repo-root .env.
#
# Phase 2 (rehearsal): use fork https://github.com/amp-rh/openshift-release -- push a branch, open a PR to
# openshift/release, then comment on the PR:
# /pj-rehearse periodic-ci-RedHatQE-interop-testing-master-ibm-fusion-access-operator-ocp4.21-lp-interop-ibm-fusion-access-operator-ipi-ocp421
# That periodic uses workflow firewatch-ipi-aws (post: firewatch-report-issues). Rehearsal uses the
# firewatch image pinned in openshift/release, not your local firewatch branch, unless you bump the image there.
set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"

ENV_FILE="${ROOT}/development/env.list"
if [[ ! -f "$ENV_FILE" ]]; then
echo "Missing ${ENV_FILE}. Copy development/env.list.example and set JIRA_TOKEN." >&2
exit 1
fi

set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a

if [[ -f "${ROOT}/.env" ]]; then
while IFS= read -r line; do
[[ "$line" =~ ^[[:space:]]*# ]] && continue
case "$line" in
JIRA_TOKEN=*)
if [[ -z "${JIRA_TOKEN:-}" ]]; then
v="${line#JIRA_TOKEN=}"
v="${v#\"}"
v="${v%\"}"
v="${v#\'}"
v="${v%\'}"
export JIRA_TOKEN="$v"
fi
;;
JIRA_EMAIL=*)
if [[ -z "${JIRA_EMAIL:-}" ]]; then
v="${line#JIRA_EMAIL=}"
v="${v#\"}"
v="${v%\"}"
export JIRA_EMAIL="$v"
fi
;;
esac
done < "${ROOT}/.env"
fi

if [[ -z "${JIRA_TOKEN:-}" ]]; then
echo "JIRA_TOKEN is empty. Set it in development/env.list or as JIRA_TOKEN= in repo-root .env." >&2
exit 1
fi

if [[ -z "${JIRA_SERVER_URL:-}" ]]; then
echo "JIRA_SERVER_URL is required (see env.list.example)." >&2
exit 1
fi

echo "${JIRA_TOKEN}" > /tmp/firewatch-smoke-token
uv sync -q
GEN_ARGS=(
firewatch jira-config-gen
--token-path /tmp/firewatch-smoke-token
--server-url "${JIRA_SERVER_URL}"
--template-path "${ROOT}/src/templates/jira.config.j2"
)
if [[ -n "${JIRA_EMAIL:-}" ]]; then
GEN_ARGS+=(--email "${JIRA_EMAIL}")
fi
uv run "${GEN_ARGS[@]}"
rm -f /tmp/firewatch-smoke-token

uv run python -c "
import json
with open('/tmp/jira.config') as f:
cfg = json.load(f)
cfg.pop('proxies', None)
with open('/tmp/jira.config', 'w') as f:
json.dump(cfg, f, indent=2)
"

export FIREWATCH_DEFAULT_JIRA_PROJECT="${FIREWATCH_DEFAULT_JIRA_PROJECT:?}"
export FIREWATCH_CONFIG="${FIREWATCH_CONFIG:?}"

exec uv run firewatch report \
--keep-job-dir \
--jira-config-path /tmp/jira.config
Loading