Ver a proveniência

Add a Molecule scenario for matrix-alertmanager-receiver

Not for merging as-is. One role's scenario plus the workflow that would
make per-role testing affordable here, so we can see how it behaves before
deciding whether to do the other 69.

Unlike the MASH repositories, one repository holds every role, so running
everything on every push is not an option. The workflow's first job works
out which roles a push touched and builds the matrix from that; a change to
docs, or to a role with no scenario yet, runs nothing.

Three things this role needed that a MASH role does not:

- The variables matrix-base would supply (matrix_base_data_path,
  matrix_domain, matrix_user_name, matrix_group_name and the uid/gid) have
  to be provided by the scenario, and the user and group have to exist
  before the role's file tasks run.
- The service contacts the homeserver while starting up - it fetches
  /_matrix/client/v3/joined_rooms to resolve its room mapping and exits 1
  if that fails - so prepare.yml stands up a stub homeserver. Most roles
  here are bridges and bots, so this is likely the rule rather than the
  exception.
- verify.yml runs as its own play, where role defaults are out of scope, so
  the paths it reads are pinned in molecule.yml. The version is deliberately
  NOT pinned: it is read from defaults/main.yml so the assertion compares
  the running image against what the role ships.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pull/5575/head
Slavi Pantaleev há 18 horas
ascendente
cometimento
ebe13bc360
9 ficheiros alterados com 610 adições e 0 eliminações
  1. +150
    -0
      .github/workflows/molecule.yml
  2. +41
    -0
      roles/custom/matrix-alertmanager-receiver/molecule/default/converge.yml
  3. +89
    -0
      roles/custom/matrix-alertmanager-receiver/molecule/default/molecule.yml
  4. +126
    -0
      roles/custom/matrix-alertmanager-receiver/molecule/default/prepare.yml
  5. +20
    -0
      roles/custom/matrix-alertmanager-receiver/molecule/default/requirements.yml
  6. +3
    -0
      roles/custom/matrix-alertmanager-receiver/molecule/default/requirements.yml.license
  7. +174
    -0
      roles/custom/matrix-alertmanager-receiver/molecule/default/verify.yml
  8. +4
    -0
      roles/custom/matrix-alertmanager-receiver/molecule/requirements.txt
  9. +3
    -0
      roles/custom/matrix-alertmanager-receiver/molecule/requirements.txt.license

+ 150
- 0
.github/workflows/molecule.yml Ver ficheiro

@@ -0,0 +1,150 @@
# SPDX-FileCopyrightText: 2026 Slavi Pantaleev
#
# SPDX-License-Identifier: AGPL-3.0-or-later

---
name: Molecule

# Unlike the MASH role repositories, where one repository holds one role, every
# role here lives in the same repository. Running every scenario on every push
# would be unaffordable, so a first job works out which roles the push actually
# touched and the matrix is built from that. A push that changes documentation,
# or a role with no scenario yet, runs nothing at all.
on: # yamllint disable-line rule:truthy
push:
paths:
- "roles/custom/**"
- ".github/workflows/molecule.yml"
pull_request:
paths:
- "roles/custom/**"
- ".github/workflows/molecule.yml"
workflow_dispatch:
inputs:
role:
description: "Single role to test (directory name under roles/custom), or empty for all roles that have a scenario"
required: false
type: string

permissions:
contents: read

jobs:
detect:
name: Work out which roles to test
runs-on: ubuntu-latest

# Same rule as the MASH repositories: a pull request from a branch of this
# repository would otherwise run everything twice, once for the push and
# once for the pull request.
if: >-
github.event_name != 'pull_request'
|| github.event.pull_request.head.repo.full_name != github.repository

outputs:
roles: ${{ steps.detect.outputs.roles }}

steps:
- name: Check out
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0

- name: Detect roles with a Molecule scenario that this change touches
id: detect
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
BEFORE_SHA: ${{ github.event.before }}
INPUT_ROLE: ${{ inputs.role }}
run: |
set -euo pipefail

have_scenario() {
[ -f "roles/custom/$1/molecule/default/molecule.yml" ]
}

# An explicit request through workflow_dispatch wins over detection.
if [ -n "${INPUT_ROLE}" ]; then
if have_scenario "${INPUT_ROLE}"; then
printf 'roles=["%s"]\n' "${INPUT_ROLE}" >> "$GITHUB_OUTPUT"
else
echo "No scenario at roles/custom/${INPUT_ROLE}/molecule/default" >&2
exit 1
fi
exit 0
fi

# A hand-triggered run with no role named, and any run where the diff
# base is unusable (a new branch, a force push, the very first commit),
# falls back to every role that has a scenario. That is the safe
# direction to fail in: too much testing rather than too little.
base=""
case "${EVENT_NAME}" in
pull_request) base="${BASE_SHA}" ;;
push)
if [ -n "${BEFORE_SHA}" ] && [ "${BEFORE_SHA}" != "0000000000000000000000000000000000000000" ] \
&& git cat-file -e "${BEFORE_SHA}^{commit}" 2>/dev/null; then
base="${BEFORE_SHA}"
fi
;;
esac

if [ -n "${base}" ]; then
changed="$(git diff --name-only "${base}" HEAD -- 'roles/custom/*' || true)"
candidates="$(printf '%s\n' "${changed}" | awk -F/ 'NF>2 {print $3}' | sort -u)"
echo "Changed roles: ${candidates:-none}"
else
candidates="$(find roles/custom -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort)"
echo "No usable diff base; considering every role"
fi

selected=""
for role in ${candidates}; do
if have_scenario "${role}"; then
selected="${selected} ${role}"
fi
done

if [ -z "${selected}" ]; then
echo "Nothing to test"
echo 'roles=[]' >> "$GITHUB_OUTPUT"
exit 0
fi

echo "Testing:${selected}"
json="$(printf '%s\n' ${selected} | jq -R . | jq -c -s .)"
echo "roles=${json}" >> "$GITHUB_OUTPUT"

molecule:
name: "Molecule: ${{ matrix.role }}"
runs-on: ubuntu-latest

needs: detect
if: needs.detect.outputs.roles != '[]'

strategy:
matrix:
role: ${{ fromJson(needs.detect.outputs.roles) }}
fail-fast: false

env:
MOLECULE_DISTRO: ubuntu2604
PY_COLORS: "1"
ANSIBLE_FORCE_COLOR: "1"

steps:
- name: Check out
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.x"

- name: Install test dependencies
run: python3 -m pip install -r roles/custom/${{ matrix.role }}/molecule/requirements.txt

- name: Run Molecule
working-directory: roles/custom/${{ matrix.role }}
run: molecule test --scenario-name default

+ 41
- 0
roles/custom/matrix-alertmanager-receiver/molecule/default/converge.yml Ver ficheiro

@@ -0,0 +1,41 @@
# SPDX-FileCopyrightText: 2026 Slavi Pantaleev
#
# SPDX-License-Identifier: AGPL-3.0-or-later

---
# The devture base roles carry the variables this role reads
# (`devture_systemd_docker_base_*`, `devture_playbook_help_*`), the same way
# they do when the playbook runs. `matrix-base` is deliberately NOT included:
# it does far more than this role needs, and the two variables it would supply
# are set directly in molecule.yml instead.
- name: Include roles for matrix-alertmanager-receiver Molecule tests
hosts: all
become: true
gather_facts: true
tasks:
- name: Include roles for matrix-alertmanager-receiver Molecule tests
ansible.builtin.include_role:
name: "{{ role_name }}"
public: true
loop:
- com.devture.ansible.role.playbook_help
- com.devture.ansible.role.systemd_docker_base
- "custom/{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}"
loop_control:
loop_var: role_name

# The role installs the unit but does not start it - in the playbook that is
# `systemd_service_manager`'s job - so the scenario starts it here.
- name: Ensure matrix-alertmanager-receiver is started
hosts: all
become: true
gather_facts: false
tasks:
- name: Ensure systemd daemon is reloaded
ansible.builtin.systemd_service:
daemon_reload: true

- name: Ensure matrix-alertmanager-receiver systemd service is started
ansible.builtin.systemd_service:
name: matrix-alertmanager-receiver.service
state: started

+ 89
- 0
roles/custom/matrix-alertmanager-receiver/molecule/default/molecule.yml Ver ficheiro

@@ -0,0 +1,89 @@
# SPDX-FileCopyrightText: 2026 Slavi Pantaleev
#
# SPDX-License-Identifier: AGPL-3.0-or-later

---
dependency:
name: galaxy
options:
requirements-file: requirements.yml
force: true
driver:
name: docker
platforms:
- name: matrix-alertmanager-receiver-${MOLECULE_DISTRO:-ubuntu2604}-default
image: "geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2604}-ansible:latest"
command: ${MOLECULE_DOCKER_COMMAND:-""}
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:rw
cgroupns_mode: host
privileged: true
pre_build_image: true
provisioner:
name: ansible
config_options:
defaults:
callback_result_format: yaml
inventory:
group_vars:
all:
# This role lives inside the playbook, so the variables the playbook
# would normally supply from `matrix-base` and from group_vars have to
# be provided here instead. These are the ones the role actually reads.
matrix_base_data_path: /matrix
matrix_domain: molecule.local
matrix_user_name: matrix
matrix_group_name: matrix
# Deliberately not 1000: the ubuntu2604 image already has a user there,
# so a distinct id proves the role used the one it was given.
matrix_user_uid: 1234
matrix_user_gid: 1234

matrix_alertmanager_receiver_hostname: alertmanager-receiver.molecule.local
matrix_alertmanager_receiver_path_prefix: /
matrix_alertmanager_receiver_container_network: matrix-alertmanager-receiver-molecule

# verify.yml runs as its own play, where the role's defaults are out
# of scope, so the paths it reads are pinned here as literals. They
# match what the role derives from matrix_base_data_path above.
matrix_alertmanager_receiver_base_path: /matrix/alertmanager-receiver
matrix_alertmanager_receiver_config_path: /matrix/alertmanager-receiver/config

# Traefik is not deployed in this scenario, so the labels the role would
# render for it are switched off and their absence is asserted instead.
matrix_alertmanager_receiver_container_labels_traefik_enabled: false

# Deliberately different from the role's own defaults (port 12345,
# metrics disabled, alerts under /alerts), so that `verify.yml` can tell
# what the role rendered apart from what the application would have done
# on its own.
matrix_alertmanager_receiver_config_http_port: 12399
matrix_alertmanager_receiver_config_http_metrics_enabled: true
matrix_alertmanager_receiver_config_http_metrics_path: /molecule-metrics
matrix_alertmanager_receiver_config_http_alerts_path_prefix: /molecule-alerts

# The homeserver IS reached at startup - the service fetches its joined
# rooms and exits 1 if that fails - so prepare.yml stands up a stub for it
matrix_alertmanager_receiver_config_matrix_homeserver_url: http://matrix.molecule.local:8008
matrix_alertmanager_receiver_config_matrix_user_id: "@alertmanager:molecule.local"
matrix_alertmanager_receiver_config_matrix_access_token: molecule_access_token_4f2a91
matrix_alertmanager_receiver_config_matrix_room_mapping:
molecule-room: "!molecule-room-id:molecule.local"
env:
# Workaround for https://github.com/ansible/molecule/issues/4391
ANSIBLE_ROLES_PATH: ${MOLECULE_PROJECT_DIRECTORY}/../..:/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles:~/.ansible/roles
scenario:
test_sequence:
- dependency
- cleanup
- destroy
- syntax
- create
- prepare
- converge
- idempotence
- verify
- cleanup
- destroy
verifier:
name: ansible

+ 126
- 0
roles/custom/matrix-alertmanager-receiver/molecule/default/prepare.yml Ver ficheiro

@@ -0,0 +1,126 @@
# SPDX-FileCopyrightText: 2026 Slavi Pantaleev
#
# SPDX-License-Identifier: AGPL-3.0-or-later

---
- name: Prepare matrix-alertmanager-receiver Molecule tests
hosts: all
become: true
gather_facts: true
tasks:
- name: Ensure apt cache is updated
ansible.builtin.apt:
update_cache: true
cache_valid_time: 600
when: ansible_os_family == 'Debian'

- name: Ensure required packages are installed
ansible.builtin.package:
name:
- python3-requests
- fuse-overlayfs
state: present

- name: Ensure Docker is installed
ansible.builtin.include_role:
name: ansible-role-docker
vars:
docker_daemon_options:
storage-driver: fuse-overlayfs

# The role's file tasks set owner/group by name, and Ansible resolves those
# through the passwd database - so they have to exist before it runs. In a
# real deployment `matrix-base` creates them.
- name: Ensure the matrix group exists
ansible.builtin.group:
name: "{{ matrix_group_name }}"
gid: "{{ matrix_user_gid }}"
state: present

- name: Ensure the matrix user exists
ansible.builtin.user:
name: "{{ matrix_user_name }}"
uid: "{{ matrix_user_uid }}"
group: "{{ matrix_group_name }}"
create_home: false
system: true
state: present

- name: Ensure the base data path exists
ansible.builtin.file:
path: "{{ matrix_base_data_path }}"
state: directory
owner: "{{ matrix_user_name }}"
group: "{{ matrix_group_name }}"
mode: "0750"

- name: Ensure the container network the role attaches to exists
ansible.builtin.command:
argv:
- docker
- network
- create
- "{{ matrix_alertmanager_receiver_container_network }}"
register: matrix_alertmanager_receiver_molecule_network
changed_when: matrix_alertmanager_receiver_molecule_network.rc == 0
failed_when:
- matrix_alertmanager_receiver_molecule_network.rc != 0
- "'already exists' not in matrix_alertmanager_receiver_molecule_network.stderr"

# matrix-alertmanager-receiver contacts the homeserver while starting up -
# it fetches /_matrix/client/v3/joined_rooms to resolve its room mapping -
# and exits 1 if that fails. So a homeserver has to exist for the service
# to come up at all. A stub is enough: the scenario is testing this role,
# not Synapse, and it keeps the run offline and fast.
- name: Ensure the Matrix homeserver stub script exists
ansible.builtin.copy:
dest: /root/matrix-homeserver-stub.py
mode: "0755"
content: |
import json
from http.server import BaseHTTPRequestHandler, HTTPServer

ROOMS = {"joined_rooms": ["{{ matrix_alertmanager_receiver_config_matrix_room_mapping['molecule-room'] }}"]}

class Handler(BaseHTTPRequestHandler):
def _send(self, payload):
body = json.dumps(payload).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)

def do_GET(self):
if self.path.endswith("/joined_rooms"):
self._send(ROOMS)
else:
self._send({})

def do_POST(self):
self._send({"event_id": "$molecule-event-id"})

def log_message(self, *args):
pass

HTTPServer(("0.0.0.0", 8008), Handler).serve_forever()

- name: Ensure the Matrix homeserver stub is running on the role's network
ansible.builtin.command:
argv:
- docker
- run
- --detach
- --rm
- --name=matrix-homeserver-stub
- --network={{ matrix_alertmanager_receiver_container_network }}
- --network-alias=matrix.molecule.local
- --volume=/root/matrix-homeserver-stub.py:/stub.py:ro
- docker.io/library/python:3.13-alpine
- python3
- /stub.py
register: matrix_alertmanager_receiver_molecule_stub
changed_when: matrix_alertmanager_receiver_molecule_stub.rc == 0
failed_when:
- matrix_alertmanager_receiver_molecule_stub.rc != 0
- "'already in use' not in matrix_alertmanager_receiver_molecule_stub.stderr"

+ 20
- 0
roles/custom/matrix-alertmanager-receiver/molecule/default/requirements.yml Ver ficheiro

@@ -0,0 +1,20 @@
---
roles:
- name: ansible-role-docker
src: https://github.com/geerlingguy/ansible-role-docker
scm: git
version: 8.0.0

- name: com.devture.ansible.role.playbook_help
src: https://github.com/devture/com.devture.ansible.role.playbook_help
scm: git
version: main

- name: com.devture.ansible.role.systemd_docker_base
src: https://github.com/devture/com.devture.ansible.role.systemd_docker_base
scm: git
version: v1.5.0-0

collections:
- name: community.docker
version: 5.2.2

+ 3
- 0
roles/custom/matrix-alertmanager-receiver/molecule/default/requirements.yml.license Ver ficheiro

@@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2026 Slavi Pantaleev

SPDX-License-Identifier: AGPL-3.0-or-later

+ 174
- 0
roles/custom/matrix-alertmanager-receiver/molecule/default/verify.yml Ver ficheiro

@@ -0,0 +1,174 @@
# SPDX-FileCopyrightText: 2026 Slavi Pantaleev
#
# SPDX-License-Identifier: AGPL-3.0-or-later

---
- name: Verify matrix-alertmanager-receiver
hosts: all
become: true
gather_facts: false

tasks:
# The version is read out of the role's own defaults rather than pinned in
# molecule.yml, so that the assertion further down compares the running
# image against what defaults/main.yml actually ships. Pinning it here
# would make that assertion compare the scenario with itself.
- name: Load the role's defaults under a separate name
ansible.builtin.include_vars:
file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/main.yml"
name: matrix_alertmanager_receiver_role_defaults

- name: Wait for the matrix-alertmanager-receiver service to become active
ansible.builtin.systemd_service:
name: matrix-alertmanager-receiver.service
register: matrix_alertmanager_receiver_service
until: matrix_alertmanager_receiver_service.status.ActiveState == 'active'
retries: 30
delay: 5
failed_when: false

# `Restart=always` means a crash-looping container still reports `active`,
# so the restart counter is checked alongside it. Asserted as `is defined`
# too, because `| int` turns a missing property into 0 and would pass
# vacuously on a systemd that does not expose it.
- name: Assert the service is active and has not been restarting
ansible.builtin.assert:
that:
- matrix_alertmanager_receiver_service.status.ActiveState == 'active'
- matrix_alertmanager_receiver_service.status.NRestarts is defined
- matrix_alertmanager_receiver_service.status.NRestarts | int == 0
fail_msg: >-
matrix-alertmanager-receiver.service is
{{ matrix_alertmanager_receiver_service.status.ActiveState | default('unknown') }}
after {{ matrix_alertmanager_receiver_service.status.NRestarts | default('?') }}
automatic restart(s)
success_msg: "matrix-alertmanager-receiver.service is active and has not restarted"

# Probed from inside the container network rather than from the host: the
# role publishes no host port, exactly as it does in a real deployment,
# where Traefik reaches it over the network instead.
- name: Wait for matrix-alertmanager-receiver to answer on the port the role configured
ansible.builtin.command:
argv:
- docker
- run
- --rm
- --network={{ matrix_alertmanager_receiver_container_network }}
- docker.io/curlimages/curl:8.11.1
- --silent
- --show-error
- --write-out
- "\nHTTP_STATUS=%{http_code}"
- "http://matrix-alertmanager-receiver:{{ matrix_alertmanager_receiver_config_http_port }}{{ matrix_alertmanager_receiver_config_http_metrics_path }}"
register: matrix_alertmanager_receiver_metrics
changed_when: false
until: "'HTTP_STATUS=200' in matrix_alertmanager_receiver_metrics.stdout"
retries: 24
delay: 5
failed_when: false

# The port and the metrics path are both non-default in this scenario, so a
# 200 here is only reachable if the configuration the role rendered is what
# the process is actually running on.
- name: Assert the configured port and metrics path reached the process
ansible.builtin.assert:
that:
- "'HTTP_STATUS=200' in matrix_alertmanager_receiver_metrics.stdout"
fail_msg: >-
matrix-alertmanager-receiver did not serve metrics on port
{{ matrix_alertmanager_receiver_config_http_port }} at
{{ matrix_alertmanager_receiver_config_http_metrics_path }}
({{ matrix_alertmanager_receiver_metrics.stdout | default('no output') }})
success_msg: >-
matrix-alertmanager-receiver serves metrics on the configured port and path

- name: Assert the metrics endpoint is really Prometheus metrics
ansible.builtin.assert:
that:
- "'# HELP' in matrix_alertmanager_receiver_metrics.stdout"
fail_msg: >-
The metrics endpoint answered, but did not return Prometheus metrics
success_msg: "The metrics endpoint returns Prometheus metrics"

# A negative control for the assertion above: the role's own default metrics
# path must NOT answer, or a 200 on the configured path would prove nothing
# about the configuration having been applied.
- name: Ask for the role's default metrics path, which this scenario moved away from
ansible.builtin.command:
argv:
- docker
- run
- --rm
- --network={{ matrix_alertmanager_receiver_container_network }}
- docker.io/curlimages/curl:8.11.1
- --silent
- --output
- /dev/null
- --write-out
- "HTTP_STATUS=%{http_code}"
- "http://matrix-alertmanager-receiver:{{ matrix_alertmanager_receiver_config_http_port }}/metrics"
register: matrix_alertmanager_receiver_default_path
changed_when: false
failed_when: false

- name: Assert the default metrics path does not answer
ansible.builtin.assert:
that:
- "'HTTP_STATUS=200' not in matrix_alertmanager_receiver_default_path.stdout"
fail_msg: >-
/metrics answered as well, so serving on
{{ matrix_alertmanager_receiver_config_http_metrics_path }} does not
prove the role's configuration reached the process
({{ matrix_alertmanager_receiver_default_path.stdout | default('no output') }})
success_msg: "Only the configured metrics path answers"

- name: Read the configuration file the role rendered
ansible.builtin.slurp:
src: "{{ matrix_alertmanager_receiver_config_path }}/config.yml"
register: matrix_alertmanager_receiver_config_file

- name: Assert the rendered configuration carries this scenario's values
ansible.builtin.assert:
that:
- matrix_alertmanager_receiver_config_matrix_user_id in matrix_alertmanager_receiver_config_rendered
- matrix_alertmanager_receiver_config_matrix_access_token in matrix_alertmanager_receiver_config_rendered
- "'molecule-room' in matrix_alertmanager_receiver_config_rendered"
fail_msg: "The rendered configuration does not carry the scenario's Matrix settings"
success_msg: "The rendered configuration carries the scenario's Matrix settings"
vars:
matrix_alertmanager_receiver_config_rendered: "{{ matrix_alertmanager_receiver_config_file.content | b64decode }}"

- name: Assert the running container is the image the role pins
ansible.builtin.command:
argv:
- docker
- container
- inspect
- matrix-alertmanager-receiver
- --format
- "{{ '{{' }} .Config.Image {{ '}}' }}"
register: matrix_alertmanager_receiver_image
changed_when: false

- name: Assert the image carries the version defaults/main.yml pins
ansible.builtin.assert:
that:
- matrix_alertmanager_receiver_role_defaults.matrix_alertmanager_receiver_version in matrix_alertmanager_receiver_image.stdout
fail_msg: >-
The running container is {{ matrix_alertmanager_receiver_image.stdout }},
which does not carry the pinned version {{ matrix_alertmanager_receiver_role_defaults.matrix_alertmanager_receiver_version }}
success_msg: "The running container is the version defaults/main.yml pins"

- name: Read the labels the role rendered
ansible.builtin.slurp:
src: "{{ matrix_alertmanager_receiver_base_path }}/labels"
register: matrix_alertmanager_receiver_labels

- name: Assert no Traefik labels are emitted while Traefik support is disabled
ansible.builtin.assert:
that:
- "'traefik.' not in (matrix_alertmanager_receiver_labels.content | b64decode)"
fail_msg: >-
Traefik labels were emitted even though
matrix_alertmanager_receiver_container_labels_traefik_enabled is false
success_msg: "No Traefik labels are emitted while Traefik support is disabled"

+ 4
- 0
roles/custom/matrix-alertmanager-receiver/molecule/requirements.txt Ver ficheiro

@@ -0,0 +1,4 @@
ansible==14.1.0
molecule==26.6.0
molecule-plugins==26.7.8
docker==7.2.0

+ 3
- 0
roles/custom/matrix-alertmanager-receiver/molecule/requirements.txt.license Ver ficheiro

@@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2026 Slavi Pantaleev

SPDX-License-Identifier: AGPL-3.0-or-later

Carregando…
Cancelar
Guardar