message: modify the sop for the project operation check

diff:
@@ -931,12 +931,12 @@ g_template_openshift_master:
- name: "openshift project creation failed on {HOST.NAME}"
expression: "{Template Openshift Master:openshift.master.project.create.last(#1)}=0"
- url: "https://github.com/openshift/ops-sop/blob/master/v3/alerts/check_create_app.asciidoc"
+ url: "https://github.com/openshift/ops-sop/blob/master/v3/alerts/check_project_operation.asciidoc"
priority: high
- name: "openshift project deletion failed on {HOST.NAME}"
expression: "{Template Openshift Master:openshift.master.project.delete.last(#1)}=1"
- url: "https://github.com/openshift/ops-sop/blob/master/v3/alerts/check_create_app.asciidoc"
+ url: "https://github.com/openshift/ops-sop/blob/master/v3/alerts/check_project_operation.asciidoc"
priority: high
- name: "Openshift Master API health check is failing on {HOST.NAME}"


message:
Automatic commit of package [openshift-tools-scripts] release [0.1.64-1].
Created by command:
/usr/bin/tito tag

change_type: MODIFY

diff:
Summary: OpenShift Tools Scripts
Name: openshift-tools-scripts
-Version: 0.1.63
+Version: 0.1.64
Release: 1%{?dist}
License: ASL 2.0
URL: https://github.com/openshift/openshift-tools
@@ -412,6 +412,13 @@ OpenShift Tools cicd scripts
/usr/bin/verify-cicd-operation.py
%changelog
+* Wed Jun 07 2017 zhiwliu <zhiwliu@redhat.com> 0.1.64-1
+- fix the pylint complain (zhiwliu@redhat.com)
+- give a second chance for the deletion of the project operation check
+ (zhiwliu@redhat.com)
+- Fix faux-HTTP client of SSO SSH endpoint to send \r\n as per spec
+ (joesmith@redhat.com)
+
* Fri Jun 02 2017 Justin Pierce <jupierce@redhat.com> 0.1.63-1
- Enabling ci message generation (jupierce@redhat.com)

license: Apache License 2.0

repo: openshift/openshift-tools


message: added router2 key

diff:
@@ -909,12 +909,17 @@ g_template_openshift_master:
description: The application create loop has failed 4 or more times in the last hour
priority: high
- - name: "openshift router pods are in the same infra node on {HOST.NAME}"
+ - name: "openshift router pods are on the same infra node on {HOST.NAME}"
expression: "{Template Openshift Master:openshift.router.pod.location.last(#1)}=0"
url: "https://github.com/openshift/ops-sop/blob/master/v3/alerts/check_internal_pod.asciidoc"
priority: avg
- - name: "openshift registry pods are in the same infra node on {HOST.NAME}"
+ - name: "openshift router2 pods are on the same infra node on {HOST.NAME}"
+ expression: "{Template Openshift Master:openshift.router2.pod.location.last(#1)}=0"
+ url: "https://github.com/openshift/ops-sop/blob/master/v3/alerts/check_internal_pod.asciidoc"
+ priority: avg
+
+ - name: "openshift registry pods are on the same infra node on {HOST.NAME}"
expression: "{Template Openshift Master:openshift.registry.pod.location.last(#1)}=0"
url: "https://github.com/openshift/ops-sop/blob/master/v3/alerts/check_internal_pod.asciidoc"
priority: avg
@@ -924,6 +929,11 @@ g_template_openshift_master:
url: "https://github.com/openshift/ops-sop/blob/master/v3/alerts/check_internal_pod.asciidoc"
priority: avg
+ - name: "openshift router2 pods are in bad status on {HOST.NAME}"
+ expression: "{Template Openshift Master:openshift.router2.pod.status.last(#1)}<2"
+ url: "https://github.com/openshift/ops-sop/blob/master/v3/alerts/check_internal_pod.asciidoc"
+ priority: avg
+
- name: "openshift registry pods are in bad status on {HOST.NAME}"
expression: "{Template Openshift Master:openshift.registry.pod.status.last(#1)}<2"
url: "https://github.com/openshift/ops-sop/blob/master/v3/alerts/check_internal_pod.asciidoc"

original_message: added router2 key


message:
Automatic commit of package [openshift-tools-scripts] release [0.1.65-1].
Created by command:
/usr/bin/tito tag

diff:
Summary: OpenShift Tools Scripts
Name: openshift-tools-scripts
-Version: 0.1.64
+Version: 0.1.65
Release: 1%{?dist}
License: ASL 2.0
URL: https://github.com/openshift/openshift-tools
@@ -412,6 +412,9 @@ OpenShift Tools cicd scripts
/usr/bin/verify-cicd-operation.py
%changelog
+* Mon Jun 12 2017 zhiwliu <zhiwliu@redhat.com> 0.1.65-1
+- upgrade cron-send-internal-pods-check (dranders@redhat.com)
+
* Wed Jun 07 2017 zhiwliu <zhiwliu@redhat.com> 0.1.64-1
- fix the pylint complain (zhiwliu@redhat.com)
- give a second chance for the deletion of the project operation check


message: changed project operation check severity from high to avg

diff:
@@ -954,12 +954,12 @@ g_template_openshift_master:
- name: "openshift project creation failed on {HOST.NAME}"
expression: "{Template Openshift Master:openshift.master.project.create.last(#1)}=0"
url: "https://github.com/openshift/ops-sop/blob/master/v3/alerts/check_project_operation.asciidoc"
- priority: high
+ priority: avg
- name: "openshift project deletion failed on {HOST.NAME}"
expression: "{Template Openshift Master:openshift.master.project.delete.last(#1)}=1"
url: "https://github.com/openshift/ops-sop/blob/master/v3/alerts/check_project_operation.asciidoc"
- priority: high
+ priority: avg
- name: "Openshift Master API health check is failing on {HOST.NAME}"
expression: "{Template Openshift Master:openshift.master.api.healthz.max(#3)}<1"


message: changed failed pv alerts only page on primary master

diff:
@@ -283,11 +283,6 @@ host_monitoring_cron:
job: ops-runner -f -s 15 -n cspc.openshift.master.process.count /usr/bin/cron-send-process-count '^/usr/bin/openshift start master' openshift.master.process.count
{% endif %}
-- name: send openshift-master counts (user, pod, project)
- hour: "*/2"
- minute: "0"
- job: ops-runner -f -s 15 -n csosmm.openshift.master.counts /usr/bin/cron-send-os-master-metrics --project-count --pod-count --user-count --pv-info
-
- name: send openshift-master local (test https://127.0.0.1) status
minute: "*/10"
job: ops-runner -f -s 60 -n csosmm.openshift.master.api.local /usr/bin/cron-send-os-master-metrics --local
@@ -371,6 +366,12 @@ host_monitoring_cron:
minute: "*/15"
job: ops-runner -f -s 15 -n cspo.openshift.project.status /usr/bin/cron-send-project-operation
+- name: send openshift-master counts (user, pod, project)
+ hour: "*/2"
+ minute: "0"
+ job: ops-runner -f -s 15 -n csosmm.openshift.master.counts /usr/bin/cron-send-os-master-metrics --project-count --pod-count --user-count --pv-info
+
+
{% if osohm_pruning.pruning_enabled | default(True) | bool %}
- name: "Prune builds/deployments/images"
hour: "{{ osohm_pruning.cron.hour }}"


message:
Automatic commit of package [openshift-tools-scripts] release [0.1.66-1].
Created by command:
/usr/bin/tito tag

diff:
Summary: OpenShift Tools Scripts
Name: openshift-tools-scripts
-Version: 0.1.65
+Version: 0.1.66
Release: 1%{?dist}
License: ASL 2.0
URL: https://github.com/openshift/openshift-tools
@@ -412,6 +412,13 @@ OpenShift Tools cicd scripts
/usr/bin/verify-cicd-operation.py
%changelog
+* Wed Jun 21 2017 zhiwliu <zhiwliu@redhat.com> 0.1.66-1
+- pylint has an odd idea of before (sten@redhat.com)
+- moar pylint (sten@redhat.com)
+- fixed pylint errors (sten@redhat.com)
+- require an INC field which is stored as a comment on digicert's side
+ (sten@redhat.com)
+
* Mon Jun 12 2017 zhiwliu <zhiwliu@redhat.com> 0.1.65-1
- upgrade cron-send-internal-pods-check (dranders@redhat.com)


message: added HEAL flag for low docker storage and high docker memory usage

diff:
@@ -109,12 +109,12 @@ g_template_docker:
# The storage checks run every 10 minutes. Trigering on two item values
# means pages will begin after 30 minutes same as the previous interval
# of 10 minutes.
- - name: "Critically low docker storage data space on {HOST.NAME}"
+ - name: "[HEAL] Critically low docker storage data space on {HOST.NAME}"
expression: "{Template Docker:docker.storage.data.space.percent_available.max(#2)}<5 or {Template Docker:docker.storage.data.space.available.max(#2)}<5" # < 5% or < 5GB
url: "https://github.com/openshift/ops-sop/blob/master/v3/alerts/check_docker_storage.asciidoc"
priority: high
- - name: "Critically High Memory usage of docker on {HOST.NAME}"
+ - name: "[HEAL] Critically High Memory usage of docker on {HOST.NAME}"
expression: "{Template Docker:docker.memoryusage.rss.last()}>10737418240"
url: "https://github.com/openshift/ops-sop/blob/master/v3/alerts/check_docker_memoryusage.asciidoc"
priority: high
@@ -126,24 +126,24 @@ g_template_docker:
- "Critically High Memory usage of docker on {HOST.NAME}"
priority: average
- - name: "Critically low docker storage metadata space on {HOST.NAME}"
+ - name: "[HEAL] Critically low docker storage metadata space on {HOST.NAME}"
expression: "{Template Docker:docker.storage.metadata.space.percent_available.max(#2)}<5 or {Template Docker:docker.storage.metadata.space.available.max(#2)}<0.005" # < 5% or < 5MB
url: "https://github.com/openshift/ops-sop/blob/master/v3/alerts/check_docker_storage.asciidoc"
priority: high
# Put triggers that depend on other triggers here (deps must be created first)
- - name: "Low docker storage data space on {HOST.NAME}"
+ - name: "[HEAL] Low docker storage data space on {HOST.NAME}"
expression: "{Template Docker:docker.storage.data.space.percent_available.max(#2)}<10 or {Template Docker:docker.storage.data.space.available.max(#2)}<10" # < 10% or < 10GB
url: "https://github.com/openshift/ops-sop/blob/master/v3/alerts/check_docker_storage.asciidoc"
dependencies:
- - "Critically low docker storage data space on {HOST.NAME}"
+ - "[HEAL] Critically low docker storage data space on {HOST.NAME}"
priority: average
- - name: "Low docker storage metadata space on {HOST.NAME}"
+ - name: "[HEAL] Low docker storage metadata space on {HOST.NAME}"
expression: "{Template Docker:docker.storage.metadata.space.percent_available.max(#2)}<10 or {Template Docker:docker.storage.metadata.space.available.max(#2)}<0.01" # < 10% or < 10MB
url: "https://github.com/openshift/ops-sop/blob/master/v3/alerts/check_docker_storage.asciidoc"
dependencies:
- - "Critically low docker storage metadata space on {HOST.NAME}"
+ - "[HEAL] Critically low docker storage metadata space on {HOST.NAME}"
priority: average
zdiscoveryrules:


message: remove average heal for low docker storage

diff:
@@ -123,7 +123,7 @@ g_template_docker:
expression: "{Template Docker:docker.memoryusage.rss.last()}>6442450944"
url: "https://github.com/openshift/ops-sop/blob/master/v3/alerts/check_docker_memoryusage.asciidoc"
dependencies:
- - "Critically High Memory usage of docker on {HOST.NAME}"
+ - "[HEAL] Critically High Memory usage of docker on {HOST.NAME}"
priority: average
- name: "[HEAL] Critically low docker storage metadata space on {HOST.NAME}"
@@ -132,14 +132,14 @@ g_template_docker:
priority: high
# Put triggers that depend on other triggers here (deps must be created first)
- - name: "[HEAL] Low docker storage data space on {HOST.NAME}"
+ - name: "Low docker storage data space on {HOST.NAME}"
expression: "{Template Docker:docker.storage.data.space.percent_available.max(#2)}<10 or {Template Docker:docker.storage.data.space.available.max(#2)}<10" # < 10% or < 10GB
url: "https://github.com/openshift/ops-sop/blob/master/v3/alerts/check_docker_storage.asciidoc"
dependencies:
- "[HEAL] Critically low docker storage data space on {HOST.NAME}"
priority: average
- - name: "[HEAL] Low docker storage metadata space on {HOST.NAME}"
+ - name: "Low docker storage metadata space on {HOST.NAME}"
expression: "{Template Docker:docker.storage.metadata.space.percent_available.max(#2)}<10 or {Template Docker:docker.storage.metadata.space.available.max(#2)}<0.01" # < 10% or < 10MB
url: "https://github.com/openshift/ops-sop/blob/master/v3/alerts/check_docker_storage.asciidoc"
dependencies:


message: changed tag_Name to oo_name so that this playbook can also run on gcp hosts

diff:
- name: Clean up Docker Storage
gather_facts: no
- hosts: "tag_Name_{{ cli_tag_name }}"
+ hosts: "oo_name_{{ cli_tag_name }}"
user: root
connection: ssh


message: remove the heal for docker_storage_low

diff:
@@ -109,7 +109,7 @@ g_template_docker:
# The storage checks run every 10 minutes. Trigering on two item values
# means pages will begin after 30 minutes same as the previous interval
# of 10 minutes.
- - name: "[HEAL] Critically low docker storage data space on {HOST.NAME}"
+ - name: "Critically low docker storage data space on {HOST.NAME}"
expression: "{Template Docker:docker.storage.data.space.percent_available.max(#2)}<5 or {Template Docker:docker.storage.data.space.available.max(#2)}<5" # < 5% or < 5GB
url: "https://github.com/openshift/ops-sop/blob/master/v3/alerts/check_docker_storage.asciidoc"
priority: high
@@ -126,7 +126,7 @@ g_template_docker:
- "[HEAL] Critically High Memory usage of docker on {HOST.NAME}"
priority: average
- - name: "[HEAL] Critically low docker storage metadata space on {HOST.NAME}"
+ - name: "Critically low docker storage metadata space on {HOST.NAME}"
expression: "{Template Docker:docker.storage.metadata.space.percent_available.max(#2)}<5 or {Template Docker:docker.storage.metadata.space.available.max(#2)}<0.005" # < 5% or < 5MB
url: "https://github.com/openshift/ops-sop/blob/master/v3/alerts/check_docker_storage.asciidoc"
priority: high
@@ -136,14 +136,14 @@ g_template_docker:
expression: "{Template Docker:docker.storage.data.space.percent_available.max(#2)}<10 or {Template Docker:docker.storage.data.space.available.max(#2)}<10" # < 10% or < 10GB
url: "https://github.com/openshift/ops-sop/blob/master/v3/alerts/check_docker_storage.asciidoc"
dependencies:
- - "[HEAL] Critically low docker storage data space on {HOST.NAME}"
+ - "Critically low docker storage data space on {HOST.NAME}"
priority: average
- name: "Low docker storage metadata space on {HOST.NAME}"
expression: "{Template Docker:docker.storage.metadata.space.percent_available.max(#2)}<10 or {Template Docker:docker.storage.metadata.space.available.max(#2)}<0.01" # < 10% or < 10MB
url: "https://github.com/openshift/ops-sop/blob/master/v3/alerts/check_docker_storage.asciidoc"
dependencies:
- - "[HEAL] Critically low docker storage metadata space on {HOST.NAME}"
+ - "Critically low docker storage metadata space on {HOST.NAME}"
priority: average
zdiscoveryrules:

original_message: remove the heal for docker_storage_low


message: added the second time metrics check if the first time check failed

diff:
#pylint: disable=invalid-name
#If a check throws an exception it is failed and should alert
#pylint: disable=bare-except
-
+#pylint: disable=wrong-import-position
+#pylint: disable=line-too-long
import ssl
import urllib2
import argparse
+import time
# These are here for a metrics pre 3.4 workaround
import sys
@@ -33,6 +35,15 @@ import tempfile
import shutil
import base64
+
+import logging
+logging.basicConfig(
+ format='%(asctime)s - %(relativeCreated)6d - %(levelname)-8s - %(message)s',
+)
+logger = logging.getLogger()
+logger.setLevel(logging.INFO)
+commandDelay = 5
+
import yaml
# pylint: disable=import-error
@@ -227,6 +238,13 @@ class OpenshiftMetricsStatus(object):
pod_report = self.check_pods()
self.get_hawkular_creds()
metrics_report = self.check_node_metrics()
+ # if metrics_report = 0, we need this check run again
+ if metrics_report == 0:
+ # sleep for 5 seconds, then run the second time node check
+ logger.info("The first time metrics check failed, 5 seconds later will start a second time check")
+ time.sleep(commandDelay)
+ logger.info("starting the second time metrics check")
+ metrics_report = self.check_node_metrics()
self.report_to_zabbix(pod_report, metrics_report)


message:
Automatic commit of package [openshift-tools-scripts] release [0.1.75-1].
Created by command:
/usr/bin/tito tag


message: added a second time check for dnsmasq to aviod some false alerts

diff:
# to be different than the class name
# pylint: disable=invalid-name
+import logging
+import time
import argparse
import urllib2
import psutil
@@ -27,7 +29,7 @@ import psutil
from openshift_tools.monitoring.ocutil import OCUtil
from openshift_tools.monitoring.metric_sender import MetricSender
-import logging
+
logging.basicConfig(
format='%(asctime)s - %(relativeCreated)6d - %(levelname)-8s - %(message)s',
)
@@ -49,6 +51,7 @@ def parse_args():
parser = argparse.ArgumentParser(description='Check the status of dnsmasq')
parser.add_argument('-v', '--verbose', action='store_true', default=None, help='Verbose?')
+ parser.add_argument('--url', default='www.redhat.com', help='site to be checked')
return parser.parse_args()
def send_metrics(curlresult, service_status):
@@ -92,18 +95,29 @@ def get_status_of_service():
print 'error'
return status_code
+def get_curl_service_result(url_tobe_check):
+ "get curl_result and service_status"
+ curl_result = curl(url_tobe_check, 80)
+ service_status = get_status_of_service()
+ return curl_result, service_status
+
def main():
"""check status of dnsmq and report"""
args = parse_args()
if args.verbose:
logger.setLevel(logging.DEBUG)
- curlresult = curl("www.google.com", 80)
- service_status = get_status_of_service()
- send_metrics(curlresult, service_status)
- logger.debug("curlresult:%s", curlresult)
+ curl_service_result = get_curl_service_result(args.url)
+ print curl_service_result
+ if curl_service_result[0] + curl_service_result[1] < 2:
+ time.sleep(5)
+ curl_service_result = get_curl_service_result(args.url)
+
+ curl_result = curl_service_result[0]
+ service_status = curl_service_result[1]
+ send_metrics(curl_result, service_status)
+ logger.debug("curlresult:%s", curl_result)
logger.debug("service_status:%s", service_status)
if __name__ == "__main__":
main()
-


message: added the remote heal for the disk space /var low

change_type: ADD

diff:
+#!/usr/bin/ansible-playbook
+# this playblook is used to cleanup /var disk space low
+# it will gzip openshift-master-audit-* log files and
+# remove openshift-master-audit-*.log.gz older than 14 days
+# this playbook only run on master
+#
+# To run
+# 1. run the playbook
+# ansible-playbook cleanup_var.yaml -e cli_tag_name=opstest1234
+---
+- name: check the log file and remove it if necessary.
+ hosts: "oo_name_{{ cli_tag_name }}"
+ gather_facts: no
+ remote_user: root
+
+ tasks:
+ - name: find the openshift-master-audit-* files greater than 99M
+ shell: find /var/log -size +99M -name "openshift-master-audit-*"
+ register: audit_file
+
+ - name: output the audit_files
+ debug:
+ var: audit_file.stdout_lines
+
+ - name: find the openshift-master-audit-* files greater than 99M and gzip it
+ shell: find /var/log -size +99M -name "openshift-master-audit-*" -exec gzip {} \;
+ when: "audit_file.stdout_lines | length != 0"
+
+ - name: find the openshift-master-audit-*.log.gz files older than 14 days
+ shell: find /var/log -name "openshift-master-audit-*.log.gz" -mtime +13
+ register: audit_file_old
+
+ - name: output the audit_files
+ debug:
+ msg: "we have {{ audit_file_old.stdout_lines | length }} old files over 14 days"
+ when: "audit_file_old.stdout_lines | length != 0"
+
+ - name: find the openshift-master-audit-*.log.gz files older than 14 days and remove them
+ shell: find /var/log -name "openshift-master-audit-*.log.gz" -mtime +13 -exec rm -f {} \;
+ when: "audit_file_old.stdout_lines | length != 0"
+
+ - name: update zabbix filesystem items
+ command: docker exec -i oso-rhel7-host-monitoring /usr/bin/cron-send-filesystem-metrics

diff:
@@ -174,6 +174,12 @@ class RemoteHealer(object):
#run
self.run_cmd(cmd.split())
+ elif re.search(r'^Filesystem: /dev/mapper/rootvg-var has less than 1[05]% free disk space on', self._args.trigger):
+ logging.info("Cleaningup /var on " + self._args.host)
+ # run the playbook to cleanup the log files
+ cmd = 'ansible-playbook /usr/bin/heal_cleanup_rootvg-var.yml -e "cli_tag_name=' + self._args.host + '"'
+ self.run_cmd(cmd.split())
+
else:
logging.info("No healing action defined for trigger: " + self._args.trigger)


message: added heal_cleanup_rootvg-var.yml to scripts/openshift-tools-scripts.spec

diff:
@@ -62,6 +62,7 @@ cp -p monitoring/cron-send-project-stats.py %{buildroot}/usr/bin/cron-send-proje
cp -p monitoring/cron-openshift-pruner.py %{buildroot}/usr/bin/cron-openshift-pruner
cp -p remote-heal/remote-healer.py %{buildroot}/usr/bin/remote-healer
cp -p remote-heal/heal_for_docker_use_too_much_memory.yml %{buildroot}/usr/bin/heal_for_docker_use_too_much_memory.yml
+cp -p remote-heal/heal_cleanup_rootvg-var.yml %{buildroot}/usr/bin/heal_cleanup_rootvg-var.yml
cp -p cloud/aws/ops-ec2-copy-ami-to-all-regions.py %{buildroot}/usr/bin/ops-ec2-copy-ami-to-all-regions
cp -p cloud/aws/ops-ec2-snapshot-ebs-volumes.py %{buildroot}/usr/bin/ops-ec2-snapshot-ebs-volumes
cp -p cloud/aws/ops-ec2-trim-ebs-snapshots.py %{buildroot}/usr/bin/ops-ec2-trim-ebs-snapshots
@@ -155,6 +156,7 @@ OpenShift Tools Monitoring Remoteheal Scripts
%files monitoring-remoteheal
/usr/bin/remote-healer
/usr/bin/heal_for_docker_use_too_much_memory.yml
+/usr/bin/heal_cleanup_rootvg-var.yml
%config(noreplace)/etc/openshift_tools/remote_healer.conf
# ----------------------------------------------------------------------------------


message:
Automatic commit of package [openshift-tools-scripts] release [0.1.92-1].
Created by command:
/usr/bin/tito tag

diff:
Summary: OpenShift Tools Scripts
Name: openshift-tools-scripts
-Version: 0.1.91
+Version: 0.1.92
Release: 1%{?dist}
License: ASL 2.0
URL: https://github.com/openshift/openshift-tools
@@ -456,6 +456,11 @@ OpenShift Tools cicd scripts
/usr/bin/verify-gather-logs-operations.py
%changelog
+* Wed Sep 13 2017 zhiwliu <zhiwliu@redhat.com> 0.1.92-1
+- added heal_cleanup_rootvg-var.yml to scripts/openshift-tools-scripts.spec
+ (zhiwliu@redhat.com)
+- added the remote heal for the disk space /var low (zhiwliu@redhat.com)
+
* Wed Sep 13 2017 Drew Anderson <dranders@redhat.com> 0.1.91-1
- Add filter to hide customer PVs add --filter-pod-pv and --force-send-zeros
(dranders@redhat.com)


message: added HEAL for the trigger

diff:
@@ -289,19 +289,19 @@ g_template_os_linux:
priority: average
# This has a dependency on the previous trigger
- - name: "{% raw %}Filesystem: {{ '{#' }}OSO_FILESYS} has less than 10% free disk space on {HOST.NAME}{% endraw %}"
+ - name: "{% raw %}[HEAL] Filesystem: {{ '{#' }}OSO_FILESYS} has less than 10% free disk space on {HOST.NAME}{% endraw %}"
expression: "{% raw %}{Template OS Linux:disc.filesys.full[{{ '{#' }}OSO_FILESYS}].last()}>90{% endraw %}"
url: "https://github.com/openshift/ops-sop/blob/master/v3/alerts/check_filesys_full.asciidoc"
priority: high
# This has a dependency on the previous trigger
# Trigger Prototypes do not work in 2.4. They will work in Zabbix 3.0
- - name: "{% raw %}Filesystem: {{ '{#' }}OSO_FILESYS} has less than 15% free disk space on {HOST.NAME}{% endraw %}"
+ - name: "{% raw %}[HEAL] Filesystem: {{ '{#' }}OSO_FILESYS} has less than 15% free disk space on {HOST.NAME}{% endraw %}"
expression: "{% raw %}{Template OS Linux:disc.filesys.full[{{ '{#' }}OSO_FILESYS}].last()}>85{% endraw %}"
url: "https://github.com/openshift/ops-sop/blob/master/v3/alerts/check_filesys_full.asciidoc"
priority: warn
dependencies:
- - "{% raw %}Filesystem: {{ '{#' }}OSO_FILESYS} has less than 10% free disk space on {HOST.NAME}{% endraw %}"
+ - "{% raw %}[HEAL] Filesystem: {{ '{#' }}OSO_FILESYS} has less than 10% free disk space on {HOST.NAME}{% endraw %}"
- name: "{% raw %}Filesystem: {{ '{#' }}OSO_FILESYS} has less than 5% free inodes on {HOST.NAME}{% endraw %}"
expression: "{% raw %}{Template OS Linux:disc.filesys.inodes.pused[{{ '{#' }}OSO_FILESYS}].last()}>95{% endraw %}"

diff:
@@ -174,7 +174,7 @@ class RemoteHealer(object):
#run
self.run_cmd(cmd.split())
- elif re.search(r'^Filesystem: /dev/mapper/rootvg-var has less than 1[05]% free disk space on', self._args.trigger):
+ elif re.search(r'^\[HEAL\] Filesystem: /dev/mapper/rootvg-var has less than 1[05]% free disk space on', self._args.trigger):
logging.info("Cleaningup /var on " + self._args.host)
# run the playbook to cleanup the log files
cmd = 'ansible-playbook /usr/bin/heal_cleanup_rootvg-var.yml -e "cli_tag_name=' + self._args.host + '"'

original_message: added HEAL for the trigger


message:
Automatic commit of package [openshift-tools-scripts] release [0.1.94-1].
Created by command:
/usr/bin/tito tag

diff:
Summary: OpenShift Tools Scripts
Name: openshift-tools-scripts
-Version: 0.1.93
+Version: 0.1.94
Release: 1%{?dist}
License: ASL 2.0
URL: https://github.com/openshift/openshift-tools
@@ -456,6 +456,9 @@ OpenShift Tools cicd scripts
/usr/bin/verify-gather-logs-operations.py
%changelog
+* Thu Sep 14 2017 zhiwliu <zhiwliu@redhat.com> 0.1.94-1
+- added HEAL for the trigger (zhiwliu@redhat.com)
+
* Wed Sep 13 2017 Matt Woodson <mwoodson@redhat.com> 0.1.93-1
- added a fix to get clusters in verify-cicd (mwoodson@redhat.com)


message: changed HEAL to Heal

diff:
@@ -289,19 +289,19 @@ g_template_os_linux:
priority: average
# This has a dependency on the previous trigger
- - name: "{% raw %}[HEAL] Filesystem: {{ '{#' }}OSO_FILESYS} has less than 10% free disk space on {HOST.NAME}{% endraw %}"
+ - name: "{% raw %}[Heal] Filesystem: {{ '{#' }}OSO_FILESYS} has less than 10% free disk space on {HOST.NAME}{% endraw %}"
expression: "{% raw %}{Template OS Linux:disc.filesys.full[{{ '{#' }}OSO_FILESYS}].last()}>90{% endraw %}"
url: "https://github.com/openshift/ops-sop/blob/master/v3/alerts/check_filesys_full.asciidoc"
priority: high
# This has a dependency on the previous trigger
# Trigger Prototypes do not work in 2.4. They will work in Zabbix 3.0
- - name: "{% raw %}[HEAL] Filesystem: {{ '{#' }}OSO_FILESYS} has less than 15% free disk space on {HOST.NAME}{% endraw %}"
+ - name: "{% raw %}[Heal] Filesystem: {{ '{#' }}OSO_FILESYS} has less than 15% free disk space on {HOST.NAME}{% endraw %}"
expression: "{% raw %}{Template OS Linux:disc.filesys.full[{{ '{#' }}OSO_FILESYS}].last()}>85{% endraw %}"
url: "https://github.com/openshift/ops-sop/blob/master/v3/alerts/check_filesys_full.asciidoc"
priority: warn
dependencies:
- - "{% raw %}[HEAL] Filesystem: {{ '{#' }}OSO_FILESYS} has less than 10% free disk space on {HOST.NAME}{% endraw %}"
+ - "{% raw %}[Heal] Filesystem: {{ '{#' }}OSO_FILESYS} has less than 10% free disk space on {HOST.NAME}{% endraw %}"
- name: "{% raw %}Filesystem: {{ '{#' }}OSO_FILESYS} has less than 5% free inodes on {HOST.NAME}{% endraw %}"
expression: "{% raw %}{Template OS Linux:disc.filesys.inodes.pused[{{ '{#' }}OSO_FILESYS}].last()}>95{% endraw %}"

diff:
@@ -174,7 +174,7 @@ class RemoteHealer(object):
#run
self.run_cmd(cmd.split())
- elif re.search(r'^\[HEAL\] Filesystem: /dev/mapper/rootvg-var has less than 1[05]% free disk space on', self._args.trigger):
+ elif re.search(r'^\[Heal\] Filesystem: /dev/mapper/rootvg-var has less than 1[05]% free disk space on', self._args.trigger):
logging.info("Cleaningup /var on " + self._args.host)
# run the playbook to cleanup the log files
cmd = 'ansible-playbook /usr/bin/heal_cleanup_rootvg-var.yml -e "cli_tag_name=' + self._args.host + '"'

original_message: changed HEAL to Heal


message:
Automatic commit of package [openshift-tools-scripts] release [0.1.95-1].
Created by command:
/usr/bin/tito tag

diff:
Summary: OpenShift Tools Scripts
Name: openshift-tools-scripts
-Version: 0.1.94
+Version: 0.1.95
Release: 1%{?dist}
License: ASL 2.0
URL: https://github.com/openshift/openshift-tools
@@ -456,6 +456,9 @@ OpenShift Tools cicd scripts
/usr/bin/verify-gather-logs-operations.py
%changelog
+* Thu Sep 14 2017 zhiwliu <zhiwliu@redhat.com> 0.1.95-1
+- changed HEAL to Heal (zhiwliu@redhat.com)
+
* Thu Sep 14 2017 zhiwliu <zhiwliu@redhat.com> 0.1.94-1
- added HEAL for the trigger (zhiwliu@redhat.com)


message: added the exception for the pods without status

diff:
@@ -31,7 +31,8 @@ import json
from openshift_tools.monitoring.ocutil import OCUtil
from openshift_tools.monitoring.metric_sender import MetricSender
# pylint: enable=import-error
-
+# pylint think check_fluentd is too complex
+#pylint: disable=too-many-branches
class OpenshiftLoggingStatus(object):
'''
This is a check for the entire EFK stack shipped with OCP
@@ -86,7 +87,7 @@ class OpenshiftLoggingStatus(object):
es_master = "exec -ti {} -- {}".format(pod_name, curl_cmd)
master_name = self.oc.run_user_cmd(es_master).split(' ')[1]
- if es_status['single_master'] == None:
+ if es_status['single_master'] is None:
es_status['single_master'] = 1
es_master_name = master_name
elif es_master_name != master_name:
@@ -116,7 +117,7 @@ class OpenshiftLoggingStatus(object):
has_matched = True
break
- if has_matched == False:
+ if has_matched is False:
es_status['all_nodes_registered'] = 0
return es_status
@@ -185,7 +186,11 @@ class OpenshiftLoggingStatus(object):
for pod in self.fluentd_pods:
node_matched = False
- if pod['status']['containerStatuses'][0]['ready'] == False:
+ try:
+ if pod['status']['containerStatuses'][0]['ready'] is False:
+ fluentd_status['running'] = 0
+ # do not want to see too much info if pod outofcpu
+ except KeyError:
fluentd_status['running'] = 0
# If there is already a problem don't worry about looping over the remaining pods/nodes
@@ -206,7 +211,7 @@ class OpenshiftLoggingStatus(object):
node_matched = True
break
- if node_matched == False:
+ if node_matched is False:
fluentd_status['node_mismatch'] = 1
break


message: Fix Add "from" field to chat events

change_type: RENAME

diff:
package com.github.insanusmokrassar.TelegramBotAPI.types.message
import com.github.insanusmokrassar.TelegramBotAPI.types.MessageIdentifier
+import com.github.insanusmokrassar.TelegramBotAPI.types.User
import com.github.insanusmokrassar.TelegramBotAPI.types.chat.abstracts.GroupChat
import com.github.insanusmokrassar.TelegramBotAPI.types.message.ChatEvents.abstracts.GroupEvent
-import com.github.insanusmokrassar.TelegramBotAPI.types.message.abstracts.ChatEventMessage
+import com.github.insanusmokrassar.TelegramBotAPI.types.message.abstracts.GroupEventMessage
import com.soywiz.klock.DateTime
-data class GroupEventMessage(
+data class CommonGroupEventMessage(
override val messageId: MessageIdentifier,
+ override val user: User,
override val chat: GroupChat,
override val chatEvent: GroupEvent,
override val date: DateTime
-) : ChatEventMessage
+) : GroupEventMessage

diff:
package com.github.insanusmokrassar.TelegramBotAPI.types.message
import com.github.insanusmokrassar.TelegramBotAPI.types.MessageIdentifier
+import com.github.insanusmokrassar.TelegramBotAPI.types.User
import com.github.insanusmokrassar.TelegramBotAPI.types.chat.abstracts.SupergroupChat
import com.github.insanusmokrassar.TelegramBotAPI.types.message.ChatEvents.abstracts.SupergroupEvent
-import com.github.insanusmokrassar.TelegramBotAPI.types.message.abstracts.ChatEventMessage
+import com.github.insanusmokrassar.TelegramBotAPI.types.message.abstracts.SupergroupEventMessage
import com.soywiz.klock.DateTime
-data class SupergroupEventMessage(
+data class CommonSupergroupEventMessage(
override val messageId: MessageIdentifier,
+ override val user: User,
override val chat: SupergroupChat,
override val chatEvent: SupergroupEvent,
override val date: DateTime
-) : ChatEventMessage
+) : SupergroupEventMessage

diff:
@@ -192,14 +192,16 @@ internal data class RawMessage(
try {
chatEvent?.let { chatEvent ->
when (chat) {
- is SupergroupChat -> SupergroupEventMessage(
+ is SupergroupChat -> CommonSupergroupEventMessage(
messageId,
+ from ?: error("Supergroup events are expected to contain 'from' field"),
chat,
chatEvent as? SupergroupEvent ?: throwWrongChatEvent(SupergroupEvent::class, chatEvent),
date.asDate
)
- is GroupChat -> GroupEventMessage(
+ is GroupChat -> CommonGroupEventMessage(
messageId,
+ from ?: error("Supergroup events are expected to contain 'from' field"),
chat,
chatEvent as? GroupEvent ?: throwWrongChatEvent(GroupChat::class, chatEvent),
date.asDate

diff:
+package com.github.insanusmokrassar.TelegramBotAPI.types.message.abstracts
+
+interface GroupEventMessage : ChatEventMessage, FromUserMessage

diff:
+package com.github.insanusmokrassar.TelegramBotAPI.types.message.abstracts
+
+interface SupergroupEventMessage : GroupEventMessage

diff:
@@ -17,5 +17,5 @@ fun <T : ChatEventMessage> Flow<ChatEventMessage>.divideBySource(contentType: KC
}
fun Flow<ChatEventMessage>.onlyChannelEvents() = divideBySource(ChannelEventMessage::class)
-fun Flow<ChatEventMessage>.onlyGroupEvents() = divideBySource(GroupEventMessage::class)
-fun Flow<ChatEventMessage>.onlySupergroupEvents() = divideBySource(SupergroupEventMessage::class)
+fun Flow<ChatEventMessage>.onlyGroupEvents() = divideBySource(CommonGroupEventMessage::class)
+fun Flow<ChatEventMessage>.onlySupergroupEvents() = divideBySource(CommonSupergroupEventMessage::class)

repo: insanusmokrassar/telegrambotapi

original_message: Fix #139: Add "from" field to chat events


message: Fix Anonymous Admins: support for is_anonymous flag

diff:
* `LocationContent#createResend` now can create `LiveLocation`
* Support of `ProximityAlertTriggered`. It is `CommonEvent`
* Property `pollQuestionTextLength` now have maximum up to `300`
+ * Anonymous Admins:
+ * New field `AdministratorChatMember#isAnonymous`
* `API`:
* Extensions `TelegramBot#pinChatMessage` now support any `Chat` and `Message`s from any `Chat`
* New extensions `TelegramBot#unpinAllChatMessages`

diff:
@@ -14,5 +14,6 @@ data class AdministratorChatMemberImpl(
override val canRestrictMembers: Boolean,
override val canPinMessages: Boolean,
override val canPromoteMembers: Boolean,
+ override val isAnonymous: Boolean,
override val customTitle: String?
) : AdministratorChatMember

diff:
@@ -5,6 +5,7 @@ import dev.inmo.tgbotapi.types.User
data class CreatorChatMember(
override val user: User,
+ override val isAnonymous: Boolean,
override val customTitle: String?
) : AdministratorChatMember {
override val canBeEdited: Boolean = true

diff:
@@ -40,12 +40,14 @@ internal data class RawChatMember(
private val canSendOtherMessages: Boolean = false,
@SerialName(canAddWebPagePreviewsField)
private val canAddWebPagePreviews: Boolean = false,
+ @SerialName(isAnonymousField)
+ private val isAnonymous: Boolean = false,
@SerialName(customTitleField)
private val customTitle: String? = null
) {
val asChatMember: ChatMember by lazy {
when (status) {
- "creator" -> CreatorChatMember(user, customTitle)
+ "creator" -> CreatorChatMember(user, isAnonymous, customTitle)
"administrator" -> AdministratorChatMemberImpl(
user,
canBeEdited,
@@ -57,6 +59,7 @@ internal data class RawChatMember(
canRestrictMembers,
canPinMessages,
canPromoteMembers,
+ isAnonymous,
customTitle
)
"member" -> MemberChatMember(user)

diff:
@@ -7,5 +7,6 @@ interface AdministratorChatMember : SpecialRightsChatMember {
val canRemoveMessages: Boolean
val canRestrictMembers: Boolean
val canPromoteMembers: Boolean
+ val isAnonymous: Boolean
val customTitle: String?
}
\ No newline at end of file

original_message: Fix #184: Anonymous Admins: support for is_anonymous flag


message: Fix Support for message_id field in unpinChatMessage method

diff:
* New fields `AudioFile#fileName` and `VideoFile#fileName`
* New fields `SendDocument#disableContentTypeDetection` and `InputMediaDocument#disableContentTypeDetection`
* New request `UnpinAllChatMessages`
+ * New parameter for `unpinChatMessage` method: `messageId`
* New dice type `FootballDiceAnimationType`
* Limits for dices has been changed
* `commonDiceResultLimit` has been deprecated

diff:
@@ -3,14 +3,18 @@ package dev.inmo.tgbotapi.requests.chat.modify
import dev.inmo.tgbotapi.CommonAbstracts.types.ChatRequest
import dev.inmo.tgbotapi.requests.abstracts.SimpleRequest
import dev.inmo.tgbotapi.types.ChatIdentifier
+import dev.inmo.tgbotapi.types.MessageIdentifier
import dev.inmo.tgbotapi.types.chatIdField
+import dev.inmo.tgbotapi.types.messageIdField
import kotlinx.serialization.*
import kotlinx.serialization.builtins.serializer
@Serializable
data class UnpinChatMessage(
@SerialName(chatIdField)
- override val chatId: ChatIdentifier
+ override val chatId: ChatIdentifier,
+ @SerialName(messageIdField)
+ val messageId: MessageIdentifier? = null
): ChatRequest, SimpleRequest<Boolean> {
override fun method(): String = "unpinChatMessage"
override val resultDeserializer: DeserializationStrategy<Boolean>

diff:
@@ -3,12 +3,21 @@ package dev.inmo.tgbotapi.extensions.api.chat.modify
import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.requests.chat.modify.UnpinChatMessage
import dev.inmo.tgbotapi.types.ChatIdentifier
+import dev.inmo.tgbotapi.types.MessageIdentifier
+import dev.inmo.tgbotapi.types.chat.abstracts.Chat
import dev.inmo.tgbotapi.types.chat.abstracts.PublicChat
+import dev.inmo.tgbotapi.types.message.abstracts.Message
suspend fun TelegramBot.unpinChatMessage(
- chatId: ChatIdentifier
-) = execute(UnpinChatMessage(chatId))
+ chatId: ChatIdentifier,
+ messageId: MessageIdentifier? = null
+) = execute(UnpinChatMessage(chatId, messageId))
suspend fun TelegramBot.unpinChatMessage(
- chat: PublicChat
-) = unpinChatMessage(chat.id)
+ chat: Chat,
+ messageId: MessageIdentifier? = null
+) = unpinChatMessage(chat.id, messageId)
+
+suspend fun TelegramBot.unpinChatMessage(
+ message: Message
+) = unpinChatMessage(message.chat.id, message.messageId)

original_message: Fix #175: Support for message_id field in unpinChatMessage method


message: Fix Support for Google Places values in venue-related methods and types

diff:
* Update all classes which must have `entities`/`caption_entities` fields
* New request `CopyMessage`
* New extension `List<TextSource>#makeString` for more comfortable work with new api with entities
+ * Support for Google Places identifiers for venues
* `API`:
* Extensions `TelegramBot#pinChatMessage` now support any `Chat` and `Message`s from any `Chat`
* New extensions `TelegramBot#unpinAllChatMessages`

diff:
package dev.inmo.tgbotapi.CommonAbstracts
+import dev.inmo.tgbotapi.types.FoursquareId
+import dev.inmo.tgbotapi.types.FoursquareType
+import dev.inmo.tgbotapi.types.GooglePlaceId
+import dev.inmo.tgbotapi.types.GooglePlaceType
+
interface CommonVenueData : Titled {
override val title: String
val address: String
- val foursquareId: String?
- val foursquareType: String? // TODO:: Rewrite with enum or interface
+ val foursquareId: FoursquareId?
+ val foursquareType: FoursquareType? // TODO:: Rewrite with enum or interface
+ val googlePlaceId: GooglePlaceId?
+ val googlePlaceType: GooglePlaceType?
}

diff:
@@ -25,7 +25,13 @@ data class SendVenue(
@SerialName(addressField)
val address: String,
@SerialName(foursquareIdField)
- val foursquareId: String? = null,
+ val foursquareId: FoursquareId? = null,
+ @SerialName(foursquareTypeField)
+ val foursquareType: FoursquareType? = null,
+ @SerialName(googlePlaceIdField)
+ val googlePlaceId: GooglePlaceId? = null,
+ @SerialName(googlePlaceTypeField)
+ val googlePlaceType: GooglePlaceType? = null,
@SerialName(disableNotificationField)
override val disableNotification: Boolean = false,
@SerialName(replyToMessageIdField)
@@ -47,16 +53,19 @@ data class SendVenue(
allowSendingWithoutReply: Boolean? = null,
replyMarkup: KeyboardMarkup? = null
): this(
- chatId,
- venue.location.latitude,
- venue.location.longitude,
- venue.title,
- venue.address,
- venue.foursquareId,
- disableNotification,
- replyToMessageId,
- allowSendingWithoutReply,
- replyMarkup
+ chatId = chatId,
+ latitude = venue.location.latitude,
+ longitude = venue.location.longitude,
+ title = venue.title,
+ address = venue.address,
+ foursquareId = venue.foursquareId,
+ foursquareType = venue.foursquareType,
+ googlePlaceId = venue.googlePlaceId,
+ googlePlaceType = venue.googlePlaceType,
+ disableNotification = disableNotification,
+ replyToMessageId = replyToMessageId,
+ allowSendingWithoutReply = allowSendingWithoutReply,
+ replyMarkup = replyMarkup
)
override fun method(): String = "sendVenue"

diff:
@@ -24,6 +24,8 @@ typealias FileUniqueId = String
typealias DiceResult = Int
typealias FoursquareId = String
typealias FoursquareType = String
+typealias GooglePlaceId = String
+typealias GooglePlaceType = String
typealias Seconds = Int
typealias LongSeconds = Long
@@ -126,6 +128,8 @@ const val showAlertField = "show_alert"
const val cachedTimeField = "cached_time"
const val foursquareIdField = "foursquare_id"
const val foursquareTypeField = "foursquare_type"
+const val googlePlaceIdField = "google_place_id"
+const val googlePlaceTypeField = "google_place_type"
const val untilDateField = "until_date"
const val errorMessageField = "error_message"
const val messageTextField = "message_text"

diff:
@@ -22,9 +22,13 @@ data class InlineQueryResultVenue(
@SerialName(addressField)
override val address: String,
@SerialName(foursquareIdField)
- override val foursquareId: String? = null,
+ override val foursquareId: FoursquareId? = null,
@SerialName(foursquareTypeField)
- override val foursquareType: String? = null,
+ override val foursquareType: FoursquareType? = null,
+ @SerialName(googlePlaceIdField)
+ override val googlePlaceId: GooglePlaceId? = null,
+ @SerialName(googlePlaceTypeField)
+ override val googlePlaceType: GooglePlaceType? = null,
@SerialName(thumbUrlField)
override val thumbUrl: String? = null,
@SerialName(thumbWidthField)

diff:
@@ -18,7 +18,11 @@ data class InputVenueMessageContent(
@SerialName(addressField)
override val address: String,
@SerialName(foursquareIdField)
- override val foursquareId: String? = null,
+ override val foursquareId: FoursquareId? = null,
@SerialName(foursquareTypeField)
- override val foursquareType: String? = null
+ override val foursquareType: FoursquareType? = null,
+ @SerialName(googlePlaceIdField)
+ override val googlePlaceId: GooglePlaceId? = null,
+ @SerialName(googlePlaceTypeField)
+ override val googlePlaceType: GooglePlaceType? = null
) : Locationed, CommonVenueData, InputMessageContent

diff:
@@ -18,5 +18,9 @@ data class Venue(
@SerialName(foursquareIdField)
override val foursquareId: FoursquareId? = null,
@SerialName(foursquareTypeField)
- override val foursquareType: FoursquareType? = null
+ override val foursquareType: FoursquareType? = null,
+ @SerialName(googlePlaceIdField)
+ override val googlePlaceId: GooglePlaceId? = null,
+ @SerialName(googlePlaceTypeField)
+ override val googlePlaceType: GooglePlaceType? = null
) : CommonVenueData, Locationed by location

diff:
@@ -3,6 +3,10 @@ package dev.inmo.tgbotapi.extensions.api.send
import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.requests.send.SendVenue
import dev.inmo.tgbotapi.types.ChatIdentifier
+import dev.inmo.tgbotapi.types.FoursquareId
+import dev.inmo.tgbotapi.types.FoursquareType
+import dev.inmo.tgbotapi.types.GooglePlaceId
+import dev.inmo.tgbotapi.types.GooglePlaceType
import dev.inmo.tgbotapi.types.MessageIdentifier
import dev.inmo.tgbotapi.types.buttons.KeyboardMarkup
import dev.inmo.tgbotapi.types.chat.abstracts.Chat
@@ -16,14 +20,29 @@ suspend fun TelegramBot.sendVenue(
longitude: Double,
title: String,
address: String,
- foursquareId: String? = null,
+ foursquareId: FoursquareId? = null,
+ foursquareType: FoursquareType? = null,
+ googlePlaceId: GooglePlaceId? = null,
+ googlePlaceType: GooglePlaceType? = null,
disableNotification: Boolean = false,
replyToMessageId: MessageIdentifier? = null,
allowSendingWithoutReply: Boolean? = null,
replyMarkup: KeyboardMarkup? = null
) = execute(
SendVenue(
- chatId, latitude, longitude, title, address, foursquareId, disableNotification, replyToMessageId, allowSendingWithoutReply, replyMarkup
+ chatId = chatId,
+ latitude = latitude,
+ longitude = longitude,
+ title = title,
+ address = address,
+ foursquareId = foursquareId,
+ foursquareType = foursquareType,
+ googlePlaceId = googlePlaceId,
+ googlePlaceType = googlePlaceType,
+ disableNotification = disableNotification,
+ replyToMessageId = replyToMessageId,
+ allowSendingWithoutReply = allowSendingWithoutReply,
+ replyMarkup = replyMarkup
)
)
@@ -33,13 +52,28 @@ suspend fun TelegramBot.sendVenue(
longitude: Double,
title: String,
address: String,
- foursquareId: String? = null,
+ foursquareId: FoursquareId? = null,
+ foursquareType: FoursquareType? = null,
+ googlePlaceId: GooglePlaceId? = null,
+ googlePlaceType: GooglePlaceType? = null,
disableNotification: Boolean = false,
replyToMessageId: MessageIdentifier? = null,
allowSendingWithoutReply: Boolean? = null,
replyMarkup: KeyboardMarkup? = null
) = sendVenue(
- chat.id, latitude, longitude, title, address, foursquareId, disableNotification, replyToMessageId, allowSendingWithoutReply, replyMarkup
+ chatId = chat.id,
+ latitude = latitude,
+ longitude = longitude,
+ title = title,
+ address = address,
+ foursquareId = foursquareId,
+ foursquareType = foursquareType,
+ googlePlaceId = googlePlaceId,
+ googlePlaceType = googlePlaceType,
+ disableNotification = disableNotification,
+ replyToMessageId = replyToMessageId,
+ allowSendingWithoutReply = allowSendingWithoutReply,
+ replyMarkup = replyMarkup
)
suspend fun TelegramBot.sendVenue(
@@ -47,13 +81,28 @@ suspend fun TelegramBot.sendVenue(
location: StaticLocation,
title: String,
address: String,
- foursquareId: String? = null,
+ foursquareId: FoursquareId? = null,
+ foursquareType: FoursquareType? = null,
+ googlePlaceId: GooglePlaceId? = null,
+ googlePlaceType: GooglePlaceType? = null,
disableNotification: Boolean = false,
replyToMessageId: MessageIdentifier? = null,
allowSendingWithoutReply: Boolean? = null,
replyMarkup: KeyboardMarkup? = null
) = sendVenue(
- chatId, location.latitude, location.longitude, title, address, foursquareId, disableNotification, replyToMessageId, allowSendingWithoutReply, replyMarkup
+ chatId = chatId,
+ latitude = location.latitude,
+ longitude = location.longitude,
+ title = title,
+ address = address,
+ foursquareId = foursquareId,
+ foursquareType = foursquareType,
+ googlePlaceId = googlePlaceId,
+ googlePlaceType = googlePlaceType,
+ disableNotification = disableNotification,
+ replyToMessageId = replyToMessageId,
+ allowSendingWithoutReply = allowSendingWithoutReply,
+ replyMarkup = replyMarkup
)
suspend fun TelegramBot.sendVenue(
@@ -61,13 +110,28 @@ suspend fun TelegramBot.sendVenue(
location: StaticLocation,
title: String,
address: String,
- foursquareId: String? = null,
+ foursquareId: FoursquareId? = null,
+ foursquareType: FoursquareType? = null,
+ googlePlaceId: GooglePlaceId? = null,
+ googlePlaceType: GooglePlaceType? = null,
disableNotification: Boolean = false,
replyToMessageId: MessageIdentifier? = null,
allowSendingWithoutReply: Boolean? = null,
replyMarkup: KeyboardMarkup? = null
) = sendVenue(
- chat.id, location.latitude, location.longitude, title, address, foursquareId, disableNotification, replyToMessageId, allowSendingWithoutReply, replyMarkup
+ chatId = chat.id,
+ latitude = location.latitude,
+ longitude = location.longitude,
+ title = title,
+ address = address,
+ foursquareId = foursquareId,
+ foursquareType = foursquareType,
+ googlePlaceId = googlePlaceId,
+ googlePlaceType = googlePlaceType,
+ disableNotification = disableNotification,
+ replyToMessageId = replyToMessageId,
+ allowSendingWithoutReply = allowSendingWithoutReply,
+ replyMarkup = replyMarkup
)
suspend fun TelegramBot.sendVenue(
@@ -79,7 +143,12 @@ suspend fun TelegramBot.sendVenue(
replyMarkup: KeyboardMarkup? = null
) = execute(
SendVenue(
- chatId, venue, disableNotification, replyToMessageId, allowSendingWithoutReply, replyMarkup
+ chatId = chatId,
+ venue = venue,
+ disableNotification = disableNotification,
+ replyToMessageId = replyToMessageId,
+ allowSendingWithoutReply = allowSendingWithoutReply,
+ replyMarkup = replyMarkup
)
)
@@ -91,7 +160,12 @@ suspend fun TelegramBot.sendVenue(
allowSendingWithoutReply: Boolean? = null,
replyMarkup: KeyboardMarkup? = null
) = sendVenue(
- chat.id, venue, disableNotification, replyToMessageId, allowSendingWithoutReply, replyMarkup
+ chatId = chat.id,
+ venue = venue,
+ disableNotification = disableNotification,
+ replyToMessageId = replyToMessageId,
+ allowSendingWithoutReply = allowSendingWithoutReply,
+ replyMarkup = replyMarkup
)
suspend inline fun TelegramBot.reply(
@@ -100,12 +174,27 @@ suspend inline fun TelegramBot.reply(
longitude: Double,
title: String,
address: String,
- foursquareId: String? = null,
+ foursquareId: FoursquareId? = null,
+ foursquareType: FoursquareType? = null,
+ googlePlaceId: GooglePlaceId? = null,
+ googlePlaceType: GooglePlaceType? = null,
disableNotification: Boolean = false,
allowSendingWithoutReply: Boolean? = null,
replyMarkup: KeyboardMarkup? = null
) = sendVenue(
- to.chat, latitude, longitude, title, address, foursquareId, disableNotification, to.messageId, allowSendingWithoutReply, replyMarkup
+ chat = to.chat,
+ latitude = latitude,
+ longitude = longitude,
+ title = title,
+ address = address,
+ foursquareId = foursquareId,
+ foursquareType = foursquareType,
+ googlePlaceId = googlePlaceId,
+ googlePlaceType = googlePlaceType,
+ disableNotification = disableNotification,
+ replyToMessageId = to.messageId,
+ allowSendingWithoutReply = allowSendingWithoutReply,
+ replyMarkup = replyMarkup
)
suspend inline fun TelegramBot.reply(
@@ -113,12 +202,27 @@ suspend inline fun TelegramBot.reply(
location: StaticLocation,
title: String,
address: String,
- foursquareId: String? = null,
+ foursquareId: FoursquareId? = null,
+ foursquareType: FoursquareType? = null,
+ googlePlaceId: GooglePlaceId? = null,
+ googlePlaceType: GooglePlaceType? = null,
disableNotification: Boolean = false,
allowSendingWithoutReply: Boolean? = null,
replyMarkup: KeyboardMarkup? = null
) = sendVenue(
- to.chat, location, title, address, foursquareId, disableNotification, to.messageId, allowSendingWithoutReply, replyMarkup
+ chat = to.chat,
+ latitude = location.latitude,
+ longitude = location.longitude,
+ title = title,
+ address = address,
+ foursquareId = foursquareId,
+ foursquareType = foursquareType,
+ googlePlaceId = googlePlaceId,
+ googlePlaceType = googlePlaceType,
+ disableNotification = disableNotification,
+ replyToMessageId = to.messageId,
+ allowSendingWithoutReply = allowSendingWithoutReply,
+ replyMarkup = replyMarkup
)
suspend inline fun TelegramBot.reply(
@@ -128,5 +232,10 @@ suspend inline fun TelegramBot.reply(
allowSendingWithoutReply: Boolean? = null,
replyMarkup: KeyboardMarkup? = null
) = sendVenue(
- to.chat, venue, disableNotification, to.messageId, allowSendingWithoutReply, replyMarkup
+ chat = to.chat,
+ venue = venue,
+ disableNotification = disableNotification,
+ replyToMessageId = to.messageId,
+ allowSendingWithoutReply = allowSendingWithoutReply,
+ replyMarkup = replyMarkup
)

diff:
@@ -16,7 +16,7 @@ fun Venue(
title: String,
address: String,
foursquare: Foursquare
-) = Venue(location, title, address, foursquare.id, foursquare.type)
+) = Venue(location, title, address, foursquareId = foursquare.id, foursquareType = foursquare.type)
@Serializable
data class Foursquare(

diff:
+package dev.inmo.tgbotapi.extensions.utils.extensions.venue
+
+import dev.inmo.tgbotapi.types.*
+import dev.inmo.tgbotapi.types.location.StaticLocation
+import dev.inmo.tgbotapi.types.venue.Venue
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+val Venue.googlePlace: GooglePlace?
+ get() = googlePlaceId?.let {
+ GooglePlace(it, googlePlaceType)
+ }
+
+fun Venue(
+ location: StaticLocation,
+ title: String,
+ address: String,
+ googlePlace: GooglePlace
+) = Venue(location, title, address, googlePlaceId = googlePlace.id, googlePlaceType = googlePlace.type)
+
+@Serializable
+data class GooglePlace(
+ @SerialName(googlePlaceIdField)
+ val id: GooglePlaceId,
+ @SerialName(googlePlaceTypeField)
+ val type: GooglePlaceType? = null
+)

original_message: Fix #190: Support for Google Places values in venue-related methods and types


message:
PossiblyReplyMessage casts
Add asPossiblyReplyMessage / requirePossiblyReplyMessage / whenPossiblyReplyMessage

diff:
* Two new dsl:
* `inlineKeyboard` for creating `InlineKeyboardMarkup`
* `replyKeyboard` for creating `ReplyKeyboardMarkup`
+ * Cast helpers for `Message`:
+ * `asPossiblyReplyMessage`: tries to cast a `Message` to `PossiblyReplyMessage`, returns `null` if the message is not of that type
+ * `requirePossiblyReplyMessage`: casts a `Message` to `PossiblyReplyMessage`, fails if the message is not of that type
+ * `whenPossiblyReplyMessage`: tries to cast a `Message` to `PossiblyReplyMessage` and runs the given block of code with it, if the cast is successful
* `Behaviour Builder`:
* New expecters and waiters:
* `waitShippingQueries`/`onShippingQuery`

diff:
@@ -1132,6 +1132,15 @@ inline fun Message.asPossiblyEditedMessage(): PossiblyEditedMessage? = this as?
@PreviewFeature
inline fun Message.requirePossiblyEditedMessage(): PossiblyEditedMessage = this as PossiblyEditedMessage
+@PreviewFeature
+inline fun <T> Message.whenPossiblyReplyMessage(block: (PossiblyReplyMessage) -> T) = asPossiblyReplyMessage() ?.let(block)
+
+@PreviewFeature
+inline fun Message.asPossiblyReplyMessage(): PossiblyReplyMessage? = this as? PossiblyReplyMessage
+
+@PreviewFeature
+inline fun Message.requirePossiblyReplyMessage(): PossiblyReplyMessage = this as PossiblyReplyMessage
+
@PreviewFeature
inline fun <T> Message.whenPossiblyForwardedMessage(block: (PossiblyForwardedMessage) -> T) = asPossiblyForwardedMessage() ?.let(block)


message: Support for "migrated to supergroup" events

diff:
# TelegramBotAPI changelog
## 0.37.4
+* `Core`:
+ * New `SupergroupEvent` subtype: `MigratedToSupergroup`. This event is sent when a group is converted to a supergroup while bot is in the group.
+ * Helper extenstion functions on `ChatEvent` to cast it to `MigratedToSupergroup`.
## 0.37.3 Hotfix of 0.37.3

diff:
package dev.inmo.tgbotapi.types.message.ChatEvents
-import dev.inmo.tgbotapi.types.ChatIdentifier
+import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.message.ChatEvents.abstracts.GroupEvent
class GroupChatCreated(
- val migratedTo: ChatIdentifier?
+ val migratedTo: ChatId?
): GroupEvent

diff:
+package dev.inmo.tgbotapi.types.message.ChatEvents
+
+import dev.inmo.tgbotapi.types.ChatId
+import dev.inmo.tgbotapi.types.message.ChatEvents.abstracts.SupergroupEvent
+
+/**
+ * This event is sent when a group is converted to a supergroup.
+ */
+class MigratedToSupergroup(
+ val migratedFrom: ChatId
+): SupergroupEvent

diff:
package dev.inmo.tgbotapi.types.message.ChatEvents
-import dev.inmo.tgbotapi.types.ChatIdentifier
+import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.message.ChatEvents.abstracts.SupergroupEvent
class SupergroupChatCreated(
- val migratedFrom: ChatIdentifier?
+ val migratedFrom: ChatId?
): SupergroupEvent

diff:
@@ -75,8 +75,8 @@ internal data class RawMessage(
private val group_chat_created: Boolean = false,
private val supergroup_chat_created: Boolean = false,
private val channel_chat_created: Boolean = false,
- private val migrate_to_chat_id: ChatIdentifier? = null,
- private val migrate_from_chat_id: ChatIdentifier? = null,
+ private val migrate_to_chat_id: ChatId? = null,
+ private val migrate_from_chat_id: ChatId? = null,
private val pinned_message: RawMessage? = null,
private val invoice: Invoice? = null,
private val dice: Dice? = null,
@@ -194,6 +194,9 @@ internal data class RawMessage(
supergroup_chat_created -> SupergroupChatCreated(
migrate_from_chat_id
)
+ migrate_from_chat_id != null -> MigratedToSupergroup(
+ migrate_from_chat_id
+ )
channel_chat_created -> ChannelChatCreated()
pinned_message != null -> PinnedMessage(pinned_message.asMessage)
proximity_alert_triggered != null -> proximity_alert_triggered

diff:
+package dev.inmo.tgbotapi.types.message.ChatEvents
+
+import dev.inmo.tgbotapi.TestsJsonFormat
+import dev.inmo.tgbotapi.extensions.utils.asMessageUpdate
+import dev.inmo.tgbotapi.extensions.utils.asMigratedToSupergroup
+import dev.inmo.tgbotapi.extensions.utils.asSupergroupChatCreated
+import dev.inmo.tgbotapi.extensions.utils.asSupergroupEventMessage
+import dev.inmo.tgbotapi.types.ChatId
+import dev.inmo.tgbotapi.types.update.abstracts.UpdateDeserializationStrategy
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertIs
+import kotlin.test.fail
+
+
+class MigratedToSupergroupTest {
+ @Test
+ fun `MigratedToSupergroup event should be parsed`() {
+ val payload = """
+ {
+ "update_id": 42,
+ "message": {
+ "message_id": 1,
+ "from": {
+ "id": 1087968824,
+ "is_bot": true,
+ "first_name": "Group",
+ "username": "GroupAnonymousBot"
+ },
+ "sender_chat": {
+ "id": 42,
+ "title": "MigratedToSupergroupTest",
+ "type": "supergroup"
+ },
+ "chat": {
+ "id": 42,
+ "title": "MigratedToSupergroupTest",
+ "type": "supergroup"
+ },
+ "date": 1639955462,
+ "migrate_from_chat_id": 57005
+ }
+ }
+ """.trimIndent()
+ val update = TestsJsonFormat.decodeFromString(UpdateDeserializationStrategy, payload)
+ val message = update.asMessageUpdate() ?: fail("update should be of MessageUpdate subtype")
+ val data = message.data.asSupergroupEventMessage() ?: fail("message should be of SupergroupEventMessage subtype")
+ val event = data.chatEvent.asMigratedToSupergroup() ?: fail("event should be of SupergroupChatCreated subtype")
+
+ assertEquals(ChatId(57005), event.migratedFrom)
+ }
+}
\ No newline at end of file

diff:
@@ -3081,6 +3081,15 @@ inline fun ChatEvent.asSupergroupChatCreated(): SupergroupChatCreated? = this as
@PreviewFeature
inline fun ChatEvent.requireSupergroupChatCreated(): SupergroupChatCreated = this as SupergroupChatCreated
+@PreviewFeature
+inline fun <T> ChatEvent.whenMigratedToSupergroup(block: (MigratedToSupergroup) -> T) = asMigratedToSupergroup() ?.let(block)
+
+@PreviewFeature
+inline fun ChatEvent.asMigratedToSupergroup(): MigratedToSupergroup? = this as? MigratedToSupergroup
+
+@PreviewFeature
+inline fun ChatEvent.requireMigratedToSupergroup(): MigratedToSupergroup = this as MigratedToSupergroup
+
@PreviewFeature
inline fun <T> ChatEvent.whenChannelEvent(block: (ChannelEvent) -> T) = asChannelEvent() ?.let(block)


message:
Add `hasCommands` / `hasNoCommands` extensions on `CommonMessage`
These extensions are meant to be used as arguments to the `initialFilter` parameter in behaviour builder triggers.

diff:
# TelegramBotAPI changelog
+## 3.4.0
+
+* `Utils`:
+ * New extensions on `CommonMessage`: `hasCommands` and `hasNoCommands`. Useful for the `initialFilter` parameter in behaviour builder triggers.
+
## 3.3.0
**THIS VERSION CONTAINS UPGRADE KOTLIN (AND ALL RELATED LIBRARIES) UP TO 1.7.20**

diff:
+package dev.inmo.tgbotapi.extensions.utils.updates
+
+import dev.inmo.tgbotapi.extensions.utils.botCommandTextSourceOrNull
+import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage
+import dev.inmo.tgbotapi.types.message.content.TextContent
+
+/**
+ * A predicate to test whether a message contains any commands in its body.
+ * Use it as the `initialFilter` parameter in behaviour builder triggers.
+ * E.g.
+ *
+ * ```kotlin
+ * onContentMessage(
+ * initialFilter = CommonMessage<MessageContent>::hasCommands
+ * ) {
+ * // the message contains at least one command here
+ * }
+ * ```
+ *
+ * @return true if this [CommonMessage] contains any commands. False otherwise.
+ * @see hasNoCommands
+ */
+fun CommonMessage<*>.hasCommands(): Boolean =
+ (this.content as? TextContent)?.textSources?.any { it.botCommandTextSourceOrNull() != null } ?: false
+
+/**
+ * A predicate to test whether a message contains any commands in its body.
+ * Use it as the `initialFilter` parameter in behaviour builder triggers.
+ * E.g.
+ *
+ * ```kotlin
+ * onContentMessage(
+ * initialFilter = CommonMessage<MessageContent>::hasNoCommands
+ * ) {
+ * // the message contains no commands here
+ * }
+ * ```
+ *
+ * @return true if this [CommonMessage] does not contain any commands. False otherwise.
+ * @see hasCommands
+ */
+fun CommonMessage<*>.hasNoCommands(): Boolean = !this.hasCommands()


message: Fix the `requestSerializer` for `SetChatAdministratorCustomTitle`

diff:
@@ -18,7 +18,7 @@ data class SetChatAdministratorCustomTitle(
override val resultDeserializer: DeserializationStrategy<Boolean>
get() = Boolean.serializer()
override val requestSerializer: SerializationStrategy<*>
- get() = RestrictChatMember.serializer()
+ get() = serializer()
init {
if (customTitle.length !in customTitleLength) {


diff:
## 5.1.0
+* `Core`:
+ * [Bot API 6.5](https://core.telegram.org/bots/api#february-3-2023) support
+ * Added the field `user_chat_id` to the class `ChatJoinRequest`.
+
## 5.0.1
* `Versions`:

diff:
@@ -375,6 +375,7 @@ const val latitudeField = "latitude"
const val longitudeField = "longitude"
const val headingField = "heading"
const val fromField = "from"
+const val userChatIdField = "user_chat_id"
const val userField = "user"
const val dateField = "date"
const val chatField = "chat"

diff:
@@ -15,6 +15,8 @@ data class ChatJoinRequest(
val chat: PublicChat,
@SerialName(fromField)
override val from: User,
+ @SerialName(userChatIdField)
+ val userChatId: Identifier,
@SerialName(dateField)
val date: TelegramDate,
@SerialName(inviteLinkField)


message: Replaced the fields `can_send_media_messages` in the classes `RestrictedChatMember` and `ChatPermissions` with separate fields `can_send_audios`, `can_send_documents`, `can_send_photos`, `can_send_videos`, `can_send_video_notes`, and `can_send_voice_notes` for different media types.

diff:
## 5.1.0
+* `Core`:
+ * [Bot API 6.5](https://core.telegram.org/bots/api#february-3-2023) support
+ * Replaced the fields `can_send_media_messages` in the classes `ChatMemberRestricted` and `ChatPermissions` with separate fields `can_send_audios`, `can_send_documents`, `can_send_photos`, `can_send_videos`, `can_send_video_notes`, and `can_send_voice_notes` for different media types.
+
## 5.0.1
* `Versions`:

diff:
@@ -327,7 +327,12 @@ const val scopeField = "scope"
const val isMemberField = "is_member"
const val isForumField = "is_forum"
const val canSendMessagesField = "can_send_messages"
-const val canSendMediaMessagesField = "can_send_media_messages"
+const val canSendAudiosField = "can_send_audios"
+const val canSendDocumentsField = "can_send_documents"
+const val canSendPhotosField = "can_send_photos"
+const val canSendVideosField = "can_send_videos"
+const val canSendVideoNotesField = "can_send_video_notes"
+const val canSendVoiceNotesField = "can_send_voice_notes"
const val canSendOtherMessagesField = "can_send_other_messages"
const val canSendPollsField = "can_send_polls"
const val canAddWebPagePreviewsField = "can_add_web_page_previews"

diff:
@@ -8,8 +8,18 @@ import kotlinx.serialization.Serializable
data class ChatPermissions(
@SerialName(canSendMessagesField)
val canSendMessages: Boolean = false,
- @SerialName(canSendMediaMessagesField)
- val canSendMediaMessages: Boolean = false,
+ @SerialName(canSendAudiosField)
+ val canSendAudios: Boolean = false,
+ @SerialName(canSendDocumentsField)
+ val canSendDocuments: Boolean = false,
+ @SerialName(canSendPhotosField)
+ val canSendPhotos: Boolean = false,
+ @SerialName(canSendVideosField)
+ val canSendVideos: Boolean = false,
+ @SerialName(canSendVideoNotesField)
+ val canSendVideoNotes: Boolean = false,
+ @SerialName(canSendVoiceNotesField)
+ val canSendVoiceNotes: Boolean = false,
@SerialName(canSendPollsField)
val canSendPolls: Boolean = false,
@SerialName(canSendOtherMessagesField)
@@ -26,7 +36,12 @@ data class ChatPermissions(
val LeftRestrictionsChatPermissions = ChatPermissions(
canSendMessages = true,
- canSendMediaMessages = true,
+ canSendAudios = true,
+ canSendDocuments = true,
+ canSendPhotos = true,
+ canSendVideos = true,
+ canSendVideoNotes = true,
+ canSendVoiceNotes = true,
canSendPolls = true,
canSendOtherMessages = true,
canAddWebPagePreviews = true,
@@ -37,7 +52,12 @@ val LeftRestrictionsChatPermissions = ChatPermissions(
val RestrictionsChatPermissions = ChatPermissions(
canSendMessages = false,
- canSendMediaMessages = false,
+ canSendAudios = false,
+ canSendDocuments = false,
+ canSendPhotos = false,
+ canSendVideos = false,
+ canSendVideoNotes = false,
+ canSendVoiceNotes = false,
canSendPolls = false,
canSendOtherMessages = false,
canAddWebPagePreviews = false,

diff:
@@ -14,8 +14,18 @@ data class RestrictedChatMember(
val isMember: Boolean = false,
@SerialName(canSendMessagesField)
val canSendMessages: Boolean = false,
- @SerialName(canSendMediaMessagesField)
- val canSendMediaMessages: Boolean = false,
+ @SerialName(canSendAudiosField)
+ val canSendAudios: Boolean = false,
+ @SerialName(canSendDocumentsField)
+ val canSendDocuments: Boolean = false,
+ @SerialName(canSendPhotosField)
+ val canSendPhotos: Boolean = false,
+ @SerialName(canSendVideosField)
+ val canSendVideos: Boolean = false,
+ @SerialName(canSendVideoNotesField)
+ val canSendVideoNotes: Boolean = false,
+ @SerialName(canSendVoiceNotesField)
+ val canSendVoiceNotes: Boolean = false,
@SerialName(canSendPollsField)
val canSendPolls: Boolean = false,
@SerialName(canSendOtherMessagesField)


message: [documentation-updates] - Updated getting started - Mob

diff:
So you're interested in trying this out. Start by cloning the repo and checking out the 'WorkflowExample' scheme. This should give you a decent idea of how the library works conceptually.
-### Installation
-For all installation instructions, see the wiki on [installation](https://github.com/wwt/Workflow/wiki/Installation). For this guide, we will use Cocoapods. Add this to your Podfile:
+## Getting the project started
+For this guide, we will create a new iOS application project in Xcode, using UIKit and Storyboards for the views. We will also use Cocoapods for pulling in Workflow, so be sure to initialize your project with
+```ruby
+pod init
+```
+and add
```ruby
pod 'DynamicWorkflow/UIKit'
```
-And run a `pod install`.
+to your Podfile.
+
+For more installation instructions, see the wiki on [installation](https://github.com/wwt/Workflow/wiki/Installation).
-### How to build an app with Workflow
-Start with your views. For the purposes of this document we'll assume you're using UIKit and not SwiftUI. When you start a new iOS project there's 1 view controller named "ViewController" with no logic in it. Let's keep that there, because we need a starting point.
+## Creating your first screen with Workflow
+Start with your views. With your new iOS project there's 1 view controller named "ViewController" with no logic in it. Let's keep that there, because we need a starting point.
-For views you want to display from now on we're going to create them as FlowRepresentable. So create a new view.
+For views you want to display from now on we're going to create them as [FlowRepresentable](). So create a new view.
```swift
-class FirstViewController: UIWorkflowItem<String> {
- var name:String?
-}
+import Workflow
+
+class FirstViewController: UIWorkflowItem<String, Never>, StoryboardLoadable {
+ static var storyboardId: String { String(describing: Self.self) }
+ static var storyboard: UIStoryboard { UIStoryboard(name: "Main", bundle: Bundle(for: Self.self)) }
-extension FirstViewController: FlowRepresentable {
- func shouldLoad(with name: String) -> Bool {
+ var name: String
+
+ required init?(coder: NSCoder, with name: String) {
self.name = name
- return true
- }
- static func instance() -> AnyFlowRepresentable {
- return UIStoryboard(name: "Main", bundle: Bundle(for: FirstViewController.self)).instantiateViewController(withIdentifier: "FirstViewController") as! FirstViewController
+ super.init(coder: coder)
}
+
+ required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}
```
Okay a couple things to notice.
-- We're using the *optional* convenience class `UIWorkflowItem` to describe this view takes in a `String`, if no `String` is passed to it, the view will not load.
-- We've slightly modified the method signature of `shouldLoad` to use `name` instead of `args`.
+- We're using the *optional* convenience class [UIWorkflowItem]() to describe this view takes in a `String` and outputs `Never`, ~~if no `String` is passed to it, the view will not load.~~
+- We're using the *optional* convenience protocol [StoryboardLoadable]() to more easily integrate our [FlowRepresentable]() with our storyboard based UI.
+- We've slightly modified the method signature of `init` to use `name` instead of `args`.
- We're pointing to a storyboard that has a view controller with an identifier the same as our class name.
-Now from `ViewController` we can create a new action
+Now from `ViewController` we can import Workflow and create a new action
```swift
+import UIKit
+import Workflow
+
class ViewController: UIViewController {
@IBAction func launchWorkflow() {
- launchInto([ FirstViewController.self ], args: "Some Name")
+ launchInto(Workflow(FirstViewController.self), args: "Some Name")
}
}
```
-Congratulations! You've created your first FlowRepresentable, not too hard eh?
+Don't forget to hook everything up in storyboards. And congratulations! You've created your first [FlowRepresentable]() and launched your first [Workflow]()! Pretty simple.
+
+## Enhancing the first screen
On `FirstViewController` let's add a `UITextField` so the user can enter their email address, a `UILabel` to welcome them and a `UIButton` for them to save.
```swift
-class FirstViewController: UIWorkflowItem<String> {
+class FirstViewController: UIWorkflowItem<String, String?>, StoryboardLoadable {
//...
+
+ @IBOutlet weak var emailTextField: UITextField!
@IBOutlet weak var welcomeLabel: UILabel! {
willSet(this) {
this.text = ["Welcome", name].compactMap { $0 }.joined(separator: " ") + "!"
}
}
- @IBOutlet weak var emailTextField:UITextField!
- @IBAction func savePressed() {
+ //...
+
+ @IBAction func savePressed(_ sender: Any) {
proceedInWorkflow(emailTextField.text)
}
}
@@ -64,12 +81,13 @@ class FirstViewController: UIWorkflowItem<String> {
Now when they press the button their email gets passed as data. Let's modify our original workflow launcher to do something with that data:
```swift
-// back in ViewController.swift
+class ViewController: UIViewController {
@IBAction func launchWorkflow() {
- let workflow:Workflow = [ FirstViewController.self ]
- launchInto(workflow, args: "Some Name") { email in
+ let workflow = Workflow(FirstViewController.self)
+ launchInto(workflow, args: "Some Name") { passedArgs in
workflow.abandon()
- print(String(describing: email))
+ print(String(describing: passedArgs.extractArgs(defaultValue: nil)))
+ }
}
}
```

repo: wwt/swiftcurrent


message:
Adds sample test for value passed when proceeding in workflow MOB
Next step:
sample test for shouldLoad on SecondViewController

diff:
@@ -143,3 +143,52 @@ The `onFinish` closure for `launchInto(_:args:onFinish:)` provides the last pass
Calling `abandon()` closes all the views launched as part of the workflow, leaving you back on `ViewController`.
</details>
+
+## Testing
+
+```swift
+import XCTest
+import UIUTest
+import Workflow
+
+@testable import GettingStarted
+
+class FirstViewControllerTests: XCTestCase {
+ func testProceedingInTheWorkflowSendsTheCorrectData() {
+ let ref = AnyFlowRepresentable(FirstViewController.self, args: .args(""))
+ var testViewController = (ref.underlyingInstance as! FirstViewController)
+
+ // Mimicking the lifecycle of a storyboard.
+ _ = testViewController.shouldLoad()
+ testViewController.loadForTesting()
+
+ var proceedInWorkflowCalled = false
+
+ testViewController.emailTextField?.simulateTouch()
+ testViewController.emailTextField?.simulateTyping("foo")
+
+ testViewController.proceedInWorkflowStorage = { passedArgs in
+ proceedInWorkflowCalled = true
+
+ // 1
+ XCTAssertEqual(passedArgs.extractArgs(defaultValue: nil) as? String, "foo")
+
+ // 2
+ XCTAssertEqual(passedArgs.extractArgs(defaultValue: "defaultValue used") as? String, "foo")
+ }
+
+ testViewController.saveButton?.simulateTouch()
+
+ XCTAssertTrue(proceedInWorkflowCalled)
+ }
+}
+
+fileprivate extension UIViewController {
+ var emailTextField: UITextField? {
+ view.viewWithAccessibilityIdentifier("email") as? UITextField
+ }
+ var saveButton: UIButton? {
+ view.viewWithAccessibilityIdentifier("save") as? UIButton
+ }
+}
+```


message: [removedAfterProceeding] Replaces filter and last with a last(where:) - MOB

diff:
@@ -42,6 +42,10 @@ extension AnyWorkflow: Sequence {
public func makeIterator() -> LinkedList<_WorkflowItem>.Iterator {
storageBase.makeIterator()
}
+
+ public func last(where predicate: (LinkedList<_WorkflowItem>.Element) throws -> Bool) rethrows -> LinkedList<_WorkflowItem>.Element? {
+ try storageBase.last(where: predicate)
+ }
}
extension AnyWorkflow {
@@ -69,15 +73,21 @@ extension AnyWorkflow {
fileprivate class AnyWorkflowStorageBase {
var orchestrationResponder: OrchestrationResponder?
- var count: Int { fatalError("Count not overriden by AnyWorkflowStorage") }
+ var count: Int { fatalError("Count not overridden by AnyWorkflowStorage") }
+
+ // https://github.com/Tyler-Keith-Thompson/Workflow/blob/master/STYLEGUIDE.md#type-erasure
+ // swiftlint:disable:next unavailable_function
+ func _abandon() { fatalError("_abandon not overridden by AnyWorkflowStorage") }
// https://github.com/Tyler-Keith-Thompson/Workflow/blob/master/STYLEGUIDE.md#type-erasure
// swiftlint:disable:next unavailable_function
- func _abandon() { fatalError("_abandon not overriden by AnyWorkflowStorage") }
+ func makeIterator() -> LinkedList<_WorkflowItem>.Iterator { fatalError("makeIterator not overridden by AnyWorkflowStorage") }
// https://github.com/Tyler-Keith-Thompson/Workflow/blob/master/STYLEGUIDE.md#type-erasure
// swiftlint:disable:next unavailable_function
- func makeIterator() -> LinkedList<_WorkflowItem>.Iterator { fatalError("makeIterator not overriden by AnyWorkflowStorage") }
+ func last(where predicate: (LinkedList<_WorkflowItem>.Element) throws -> Bool) rethrows -> LinkedList<_WorkflowItem>.Element? {
+ fatalError("last(where:) not overridden by AnyWorkflowStorage")
+ }
}
fileprivate final class AnyWorkflowStorage<F: FlowRepresentable>: AnyWorkflowStorageBase {
@@ -105,4 +115,8 @@ fileprivate final class AnyWorkflowStorage<F: FlowRepresentable>: AnyWorkflowSto
override func makeIterator() -> LinkedList<_WorkflowItem>.Iterator {
workflow.makeIterator()
}
+
+ override func last(where predicate: (LinkedList<_WorkflowItem>.Element) throws -> Bool) rethrows -> LinkedList<_WorkflowItem>.Element? {
+ try workflow.last(where: predicate)
+ }
}

diff:
@@ -62,7 +62,7 @@ open class UIKitPresenter: OrchestrationResponder {
}
public func complete(_ workflow: AnyWorkflow, passedArgs: AnyWorkflow.PassedArgs, onFinish: ((AnyWorkflow.PassedArgs) -> Void)?) {
- let lastInstance = workflow.filter { $0.value.instance != nil }.last
+ let lastInstance = workflow.last { $0.value.instance != nil }
if lastInstance?.value.metadata.persistence == .removedAfterProceeding,
let view = lastInstance?.value.instance?.underlyingInstance as? UIViewController {


message: [removedAfterProceeding] Immediately completes if there's no first loadedInstance - MOB

diff:
@@ -123,8 +123,7 @@ public final class Workflow<F: FlowRepresentable>: LinkedList<_WorkflowItem> {
}
guard let first = firstLoadedInstance else {
- #warning("This should be changed to orchestrationResponder.complete")
- onFinish?(passedArgs)
+ orchestrationResponder.complete(AnyWorkflow(self), passedArgs: passedArgs, onFinish: onFinish)
return nil
}

diff:
@@ -374,6 +374,7 @@ class SkipThroughWorkflowTests: XCTestCase {
.thenProceed(with: FR2.self)
.thenProceed(with: FR3.self)
let responder = MockOrchestrationResponder()
+ responder.complete_EnableDefaultImplementation = true
let launchedRepresentable = wf.launch(withOrchestrationResponder: responder, args: FR1.id) { id in
expectation.fulfill()
@@ -384,6 +385,7 @@ class SkipThroughWorkflowTests: XCTestCase {
XCTAssertNil(launchedRepresentable)
XCTAssertNil(responder.lastTo)
XCTAssertNil(responder.lastFrom)
+ XCTAssertEqual(responder.completeCalled, 1)
wait(for: [FR2.expectation, FR3.expectation, expectation], timeout: 3)
}

diff:
@@ -153,7 +153,9 @@ class WorkflowConsumerTests: XCTestCase {
let wf: Workflow = Workflow(FR1.self)
var callbackCalled = false
- _ = wf.launch(withOrchestrationResponder: MockOrchestrationResponder(), args: 1) { args in
+ let responder = MockOrchestrationResponder()
+ responder.complete_EnableDefaultImplementation = true
+ wf.launch(withOrchestrationResponder: responder, args: 1) { args in
callbackCalled = true
XCTAssertEqual(args.extractArgs(defaultValue: nil) as? String, "args")
}


message: [removedAfterProceeding] - Adds completion call to destroy - MOB

diff:
@@ -66,11 +66,11 @@ open class UIKitPresenter: OrchestrationResponder {
if lastInstance?.value.metadata.persistence == .removedAfterProceeding,
let view = lastInstance?.value.instance?.underlyingInstance as? UIViewController {
- destroy(view)
- }
-
+ destroy(view) { onFinish?(passedArgs) }
+ } else {
onFinish?(passedArgs)
}
+ }
func abandon(_ workflow: AnyWorkflow, animated: Bool, onFinish: (() -> Void)?) {
guard let first = firstLoadedInstance else { return }
@@ -100,12 +100,13 @@ open class UIKitPresenter: OrchestrationResponder {
}
}
- private func destroy(_ view: UIViewController) {
+ private func destroy(_ view: UIViewController, completion: (() -> Void)? = nil) {
if let nav = view.navigationController {
let vcs = nav.viewControllers.filter {
$0 !== view
}
nav.setViewControllers(vcs, animated: false)
+ completion?()
} else {
let parent = view.presentingViewController
let child = view.presentedViewController
@@ -116,6 +117,8 @@ open class UIKitPresenter: OrchestrationResponder {
if let p = parent,
let c = child {
p.present(c, animated: false)
+ } else {
+ completion?()
}
}
}


message: [removedAfterProceeding] Calls completion even when workflow item presented a new ViewController - BL MW

diff:
@@ -116,7 +116,9 @@ open class UIKitPresenter: OrchestrationResponder {
parent?.dismiss(animated: false) {
if let p = parent,
let c = child {
- p.present(c, animated: false)
+ p.present(c, animated: false) {
+ completion?()
+ }
} else {
completion?()
}

diff:
@@ -573,6 +573,48 @@ class UIKitConsumerPersistenceTests: XCTestCase {
wait(for: [expectOnFinish], timeout: 3)
}
+
+ func testModalWorkflow_CanPresentNewViewControllerModally_AndCallOnFinish() {
+ class FR1: TestViewController { }
+ class FR2: TestViewController { }
+ class FR3: TestViewController {
+ let presented = UIViewController()
+ func presentOutsideViewController() {
+ present(presented, animated: false)
+ }
+ }
+ var presented: UIViewController?
+ let root = UIViewController()
+ let nav = UINavigationController(rootViewController: root)
+ nav.loadForTesting()
+
+ let expectOnFinish = self.expectation(description: "onFinish called")
+ root.launchInto(Workflow(FR1.self, flowPersistence: .removedAfterProceeding)
+ .thenProceed(with: FR2.self, flowPersistence: .removedAfterProceeding)
+ .thenProceed(with: FR3.self, flowPersistence: .removedAfterProceeding),
+ withLaunchStyle: .modal) { _ in
+ XCTAssertUIViewControllerDisplayed(isInstance: presented!)
+ XCTAssertEqual(UIApplication.topViewController()?.presentingViewController, nav)
+ expectOnFinish.fulfill()
+ }
+ XCTAssertUIViewControllerDisplayed(ofType: FR1.self)
+ XCTAssert(UIApplication.topViewController()?.presentingViewController === nav,
+ "\(String(describing: UIApplication.topViewController()?.presentingViewController)) was not \(nav)")
+ (UIApplication.topViewController() as? FR1)?.proceedInWorkflow(nil)
+ XCTAssertUIViewControllerDisplayed(ofType: FR2.self)
+ XCTAssert(UIApplication.topViewController()?.presentingViewController === nav,
+ "\(String(describing: UIApplication.topViewController()?.presentingViewController)) was not \(nav)")
+ (UIApplication.topViewController() as? FR2)?.proceedInWorkflow(nil)
+
+ XCTAssertUIViewControllerDisplayed(ofType: FR3.self)
+
+ let fr3 = (UIApplication.topViewController() as? FR3)
+ presented = fr3?.presented
+ fr3?.presentOutsideViewController()
+ fr3?.proceedInWorkflow(nil)
+
+ wait(for: [expectOnFinish], timeout: 3)
+ }
}
extension UIKitConsumerPersistenceTests {


message: [removedAfterProceeding] - Adds _ to last(where:) - MOB

diff:
@@ -85,7 +85,7 @@ fileprivate class AnyWorkflowStorageBase {
// https://github.com/Tyler-Keith-Thompson/Workflow/blob/master/STYLEGUIDE.md#type-erasure
// swiftlint:disable:next unavailable_function
- func last(where: (LinkedList<_WorkflowItem>.Element) throws -> Bool) rethrows -> LinkedList<_WorkflowItem>.Element? {
+ func last(where _: (LinkedList<_WorkflowItem>.Element) throws -> Bool) rethrows -> LinkedList<_WorkflowItem>.Element? {
fatalError("last(where:) not overridden by AnyWorkflowStorage")
}
}


message: [removedAfterProceeding] - Adds implementation for complete in WorkflowExampleTests MockOrchestrationResponder - MW

diff:
@@ -10,8 +10,17 @@ import Foundation
import Workflow
class MockOrchestrationResponder: OrchestrationResponder {
+ var completeCalled = 0
+ var lastPassedArgs: AnyWorkflow.PassedArgs?
+ var lastCompleteOnFinish: ((AnyWorkflow.PassedArgs) -> Void)?
+ var complete_EnableDefaultImplementation = false
func complete(_ workflow: AnyWorkflow, passedArgs: AnyWorkflow.PassedArgs, onFinish: ((AnyWorkflow.PassedArgs) -> Void)?) {
+ lastWorkflow = workflow
+ lastPassedArgs = passedArgs
+ lastCompleteOnFinish = onFinish
+ completeCalled += 1
+ if complete_EnableDefaultImplementation { onFinish?(passedArgs) }
}
var launchCalled = 0


message: [getting-started-updates] - Creates document for Getting Started with Programmatic UIKit - BL MW

diff:
+# Interested in seeing examples of Workflow in action?
+Start by cloning the repo and checking out the 'WorkflowExample' scheme. This should give you a decent idea of how the library works. If you want to create a new project, read on.
+
+# CocoaPods with Programmatic Views
+
+## Adding the dependency
+```ruby
+pod 'DynamicWorkflow/UIKit'
+```
+Add the above line to your Podfile.
+
+For more installation instructions, see the wiki on [installation](https://github.com/wwt/Workflow/wiki/Installation).
+
+## Create your view controllers
+Create two view controllers that inherit from [UIWorkflowItem](https://gitcdn.link/cdn/wwt/Workflow/faf9273f154954848bf6b6d5c592a7f0740ef53a/docs/Classes/UIWorkflowItem.html).
+
+```swift
+import UIKit
+import Workflow
+import WorkflowUIKit
+
+class FirstViewController: UIWorkflowItem<String, String>, FlowRepresentable {
+ private let name: String
+ private let emailTextField = UITextField()
+ private let welcomeLabel = UILabel()
+ private let saveButton = UIButton()
+
+ required init(with name: String) {
+ self.name = name
+ super.init(nibName: nil, bundle: nil)
+
+ view.backgroundColor = .systemGray5
+
+ welcomeLabel.text = "Welcome \(name)!"
+
+ emailTextField.backgroundColor = .systemGray3
+ emailTextField.borderStyle = .roundedRect
+ emailTextField.placeholder = "Enter email..."
+
+ saveButton.setTitle("Save", for: .normal)
+ saveButton.setTitleColor(.systemBlue, for: .normal)
+ saveButton.addTarget(self, action: #selector(savePressed), for: .touchUpInside)
+
+ view.addSubview(welcomeLabel)
+ view.addSubview(emailTextField)
+ view.addSubview(saveButton)
+
+ welcomeLabel.translatesAutoresizingMaskIntoConstraints = false
+ welcomeLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
+ welcomeLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 100).isActive = true
+
+ emailTextField.translatesAutoresizingMaskIntoConstraints = false
+ emailTextField.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
+ emailTextField.topAnchor.constraint(equalTo: welcomeLabel.bottomAnchor, constant: 16).isActive = true
+ emailTextField.widthAnchor.constraint(equalToConstant: 300).isActive = true
+
+ saveButton.translatesAutoresizingMaskIntoConstraints = false
+ saveButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
+ saveButton.topAnchor.constraint(equalTo: emailTextField.bottomAnchor, constant: 24).isActive = true
+ }
+
+ required init?(coder: NSCoder) { nil }
+
+ @objc
+ private func savePressed() {
+ proceedInWorkflow(emailTextField.text ?? "")
+ }
+}
+
+// This screen shows an employee only screen
+class SecondViewController: UIWorkflowItem<String, String>, FlowRepresentable {
+ private let email: String
+ private let emailLabel = UILabel()
+ private let nextButton = UIButton()
+
+ required init(with email: String) {
+ self.email = email
+ super.init(nibName: nil, bundle: nil)
+
+ view.backgroundColor = .systemGray5
+
+ emailLabel.text = "Entered: \(email)"
+
+ nextButton.setTitle("Next", for: .normal)
+ nextButton.setTitleColor(.systemBlue, for: .normal)
+ nextButton.addTarget(self, action: #selector(nextPressed), for: .touchUpInside)
+
+ view.addSubview(emailLabel)
+ view.addSubview(nextButton)
+
+ emailLabel.translatesAutoresizingMaskIntoConstraints = false
+ emailLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
+ emailLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
+
+ nextButton.translatesAutoresizingMaskIntoConstraints = false
+ nextButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
+ nextButton.topAnchor.constraint(equalTo: emailLabel.bottomAnchor, constant: 16).isActive = true
+ }
+
+ required init?(coder: NSCoder) { nil }
+
+ func shouldLoad() -> Bool { email.contains("@wwt.com") }
+
+ @objc
+ private func nextPressed() {
+ proceedInWorkflow(email)
+ }
+}
+```
+### Let's talk about what is going on with these view controllers.
+#### **What's this `shouldLoad()`?**
+
+
+It is part of the [FlowRepresentable](https://gitcdn.link/cdn/wwt/Workflow/faf9273f154954848bf6b6d5c592a7f0740ef53a/docs/Protocols/FlowRepresentable.html) protocol. It has default implementations created for your convenience but is still implementable if you want to control when a [FlowRepresentable](https://gitcdn.link/cdn/wwt/Workflow/faf9273f154954848bf6b6d5c592a7f0740ef53a/docs/Protocols/FlowRepresentable.html) should load in the work flow. It is called after `init` but before `viewDidLoad()`.
+
+
+## Launching the [Workflow](https://gitcdn.link/cdn/wwt/Workflow/faf9273f154954848bf6b6d5c592a7f0740ef53a/docs/Classes/Workflow.html)
+Next, we create a [Workflow](https://gitcdn.link/cdn/wwt/Workflow/faf9273f154954848bf6b6d5c592a7f0740ef53a/docs/Classes/Workflow.html) that is initialized with our [FlowRepresentable](https://gitcdn.link/cdn/wwt/Workflow/faf9273f154954848bf6b6d5c592a7f0740ef53a/docs/Protocols/FlowRepresentable.html)s and launch it from a view controller that is already loaded onto the screen (in our case, the 'ViewController' class provided by Xcode).
+
+```swift
+import UIKit
+import Workflow
+
+class ViewController: UIViewController {
+ private let launchButton = UIButton()
+
+ init() {
+ super.init(nibName: nil, bundle: nil)
+
+ launchButton.setTitle("Launch Workflow", for: .normal)
+ launchButton.addTarget(self, action: #selector(launchWorkflow), for: .touchUpInside)
+
+ view.addSubview(launchButton)
+
+ launchButton.translatesAutoresizingMaskIntoConstraints = false
+ launchButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
+ launchButton.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ @objc
+ private func launchWorkflow() {
+ let workflow = Workflow(FirstViewController.self)
+ .thenPresent(SecondViewController.self)
+
+ launchInto(workflow, args: "Noble Six") { passedArgs in
+ workflow.abandon()
+
+ guard case .args(let emailAddress as String) = passedArgs else {
+ print("No email address supplied")
+ return
+ }
+ print(emailAddress)
+ }
+ }
+}
+```
+### Let's discuss what's going on here.
+#### **Where is the type safety, I heard about?**
+
+
+The [Workflow](https://gitcdn.link/cdn/wwt/Workflow/faf9273f154954848bf6b6d5c592a7f0740ef53a/docs/Classes/Workflow.html) has compile-time type safety on the Input/Output types of the supplied [FlowRepresentable](https://gitcdn.link/cdn/wwt/Workflow/faf9273f154954848bf6b6d5c592a7f0740ef53a/docs/Protocols/FlowRepresentable.html)s. This means that you will get a build error if the output of `FirstViewController` does not match the input type of `SecondViewController`.
+
+
+#### **What's going on with this `passedArgs`?**
+
+
+The `onFinish` closure for `launchInto(_:args:onFinish:)` provides the last passed [AnyWorkflow.PassedArgs](https://gitcdn.link/cdn/wwt/Workflow/faf9273f154954848bf6b6d5c592a7f0740ef53a/docs/Classes/AnyWorkflow/PassedArgs.html) in the work flow. For this Workflow, that could be the output of `FirstViewController` or `SecondViewController` depending on the email signature typed in `FirstViewController`. To extract the value, we unwrap the variable within the case of `.args()` as we expect this workflow to return some argument.
+
+
+#### **Why call `abandon()`?**
+
+
+Calling `abandon()` closes all the views launched as part of the workflow, leaving you back on `ViewController`.
+
+
+## Testing
+
+### Installing test dependencies
+For our test example, we will be using a library called [UIUTest](https://github.com/nallick/UIUTest). It is optional for testing Workflow, but in order for the example to be copyable, you will need to add
+```ruby
+pod 'UIUTest'
+```
+to your test target.
+
+### Creating the tests
+
+```swift
+import XCTest
+import UIUTest
+import Workflow
+
+@testable import GettingStarted
+
+class SecondViewControllerTests: XCTestCase {
+ func testSecondViewControllerDoesNotLoadWhenInputIsEmpty() {
+ let ref = AnyFlowRepresentable(SecondViewController.self, args: .args(""))
+ let testViewController = (ref.underlyingInstance as! SecondViewController)
+
+ XCTAssertFalse(testViewController.shouldLoad(), "SecondViewController should not load")
+ }
+
+ func testSecondViewControllerLoadsWhenInputIsContainsWWTEmail() {
+ let ref = AnyFlowRepresentable(SecondViewController.self, args: .args("Awesome.Possum@wwt.com"))
+ let testViewController = (ref.underlyingInstance as! SecondViewController)
+
+ XCTAssert(testViewController.shouldLoad(), "SecondViewController should load")
+ }
+
+ func testProceedPassesThroughInput() {
+ var proceedInWorkflowCalled = false
+ let expectedString = "Awesome.Possum@wwt.com"
+ let ref = AnyFlowRepresentable(SecondViewController.self, args: .args(expectedString))
+ var testViewController = (ref.underlyingInstance as! SecondViewController)
+ // Mimicking the lifecycle of the view controller
+ _ = testViewController.shouldLoad()
+ testViewController.loadForTesting() // UIUTest helper
+
+ testViewController.proceedInWorkflowStorage = { passedArgs in
+ proceedInWorkflowCalled = true
+ XCTAssertEqual(passedArgs.extractArgs(defaultValue: "defaultValue used") as? String, expectedString)
+ }
+
+ (testViewController.view.viewWithAccessibilityIdentifier("finish") as? UIButton)?.simulateTouch() // UIUTest helper
+
+ XCTAssert(proceedInWorkflowCalled, "proceedInWorkflow should be called")
+ }
+}
+```
+While this team finds that testing our view controllers with [UIUTest](https://github.com/nallick/UIUTest) allows us to decrease the visibility of our properties and provide better coverage, [UIUTest](https://github.com/nallick/UIUTest) is not needed for testing Workflow. If you do not want to take the dependency, you will have to elevate visibility or find a way to invoke the `finishPressed` method.
+
+#### **What is going on with: `testSecondViewControllerDoesNotLoadWhenInputIsEmpty`?**
+
+This test is super simple. We create the view controller in a way that will go through the correct init, with expected arguments. Then we call `shouldLoad` to validate if the provided Input gets us the results we want.
+
+
+#### **What is going on with: `testProceedPassesThroughInput`?**
+
+At a high level we are loading the view controller for testing (similar to before but now with an added step of triggering lifecycle events). We update the `proceedInWorkflow` closure so that we can confirm it was called. Finally we invoke the method that will call proceed. The assert is verifying that the Output is the same as the input, as this view controller is passing it through.
+
+
+#### **I added UIUTest, why isn't it hitting the finish button?**
+
+It's easy to forget to set the accessibility identifier on the button, please check that first. Second, if you don't call `loadForTesting()` your view controller doesn't make it to the window and the hit testing of `simulateTouch()` will also fail. Finally, make sure the button is visible and tappable on the simulator you are using.
+


message: [getting-started-updates] - Updates annotations for annotations to be on the same line - MW

diff:
@@ -61,8 +61,7 @@ class FirstViewController: UIWorkflowItem<String, String>, FlowRepresentable {
required init?(coder: NSCoder) { nil }
- @objc
- private func savePressed() {
+ @objc private func savePressed() {
proceedInWorkflow(emailTextField.text ?? "")
}
}
@@ -101,8 +100,7 @@ class SecondViewController: UIWorkflowItem<String, String>, FlowRepresentable {
func shouldLoad() -> Bool { email.contains("@wwt.com") }
- @objc
- private func nextPressed() {
+ @objc private func nextPressed() {
proceedInWorkflow(email)
}
}
@@ -141,8 +139,7 @@ class ViewController: UIViewController {
fatalError("init(coder:) has not been implemented")
}
- @objc
- private func launchWorkflow() {
+ @objc private func launchWorkflow() {
let workflow = Workflow(FirstViewController.self)
.thenPresent(SecondViewController.self)


message: [getting-started-updates] - Fixes import statements to reflect CocoaPods instead of SPM and removes unnecessary elements - MW

diff:
@@ -17,7 +17,6 @@ Create two view controllers that inherit from [UIWorkflowItem<I, O>](https://git
```swift
import UIKit
import Workflow
-import WorkflowUIKit
class FirstViewController: UIWorkflowItem<String, String>, FlowRepresentable {
private let name: String
@@ -69,8 +68,7 @@ class FirstViewController: UIWorkflowItem<String, String>, FlowRepresentable {
// This screen shows an employee only screen
class SecondViewController: UIWorkflowItem<String, String>, FlowRepresentable {
private let email: String
- private let emailLabel = UILabel()
- private let nextButton = UIButton()
+ private let finishButton = UIButton()
required init(with email: String) {
self.email = email
@@ -78,31 +76,27 @@ class SecondViewController: UIWorkflowItem<String, String>, FlowRepresentable {
view.backgroundColor = .systemGray5
- emailLabel.text = "Entered: \(email)"
+ finishButton.setTitle("Finish", for: .normal)
+ finishButton.setTitleColor(.systemBlue, for: .normal)
+ finishButton.addTarget(self, action: #selector(finishPressed), for: .touchUpInside)
+ finishButton.accessibilityIdentifier = "finish"
- nextButton.setTitle("Next", for: .normal)
- nextButton.setTitleColor(.systemBlue, for: .normal)
- nextButton.addTarget(self, action: #selector(nextPressed), for: .touchUpInside)
+ view.addSubview(finishButton)
- view.addSubview(emailLabel)
- view.addSubview(nextButton)
-
- emailLabel.translatesAutoresizingMaskIntoConstraints = false
- emailLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
- emailLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
-
- nextButton.translatesAutoresizingMaskIntoConstraints = false
- nextButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
- nextButton.topAnchor.constraint(equalTo: emailLabel.bottomAnchor, constant: 16).isActive = true
+ finishButton.translatesAutoresizingMaskIntoConstraints = false
+ finishButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
+ finishButton.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
required init?(coder: NSCoder) { nil }
- func shouldLoad() -> Bool { email.contains("@wwt.com") }
-
- @objc private func nextPressed() {
+ @objc private func finishPressed() {
proceedInWorkflow(email)
}
+
+ func shouldLoad() -> Bool {
+ return email.contains("@wwt.com")
+ }
}
```
### Let's talk about what is going on with these view controllers.


message: [getting-started-updates] - Adds Getting Started with Programmatic UIKit to the wiki Home as an additional option - MW

diff:
- [Why This Library?](https://github.com/wwt/Workflow/wiki/Why-This-Library%3F)
- [Installation](https://github.com/wwt/Workflow/wiki/Installation)
-- [Getting Started](https://github.com/wwt/Workflow/wiki/getting-started)
+- [Getting Started with Storyboards](https://github.com/wwt/Workflow/wiki/getting-started)
+- [Getting Started with Programmatic UIKit](https://github.com/wwt/Workflow/wiki/getting-started-uikit)
- [Developer Documentation](https://gitcdn.link/repo/wwt/Workflow/main/docs/index.html)
\ No newline at end of file


message: [getting-started-updates] - Fixes sample not working if project is not set up by entirely removing storyboards - MW

diff:
@@ -90,13 +90,13 @@ class SecondViewController: UIWorkflowItem<String, String>, FlowRepresentable {
required init?(coder: NSCoder) { nil }
- @objc private func finishPressed() {
- proceedInWorkflow(email)
- }
-
func shouldLoad() -> Bool {
return email.contains("@wwt.com")
}
+
+ @objc private func finishPressed() {
+ proceedInWorkflow(email)
+ }
}
```
### Let's talk about what is going on with these view controllers.
@@ -116,11 +116,10 @@ import Workflow
class ViewController: UIViewController {
private let launchButton = UIButton()
- init() {
- super.init(nibName: nil, bundle: nil)
-
+ override func viewDidLoad() {
launchButton.setTitle("Launch Workflow", for: .normal)
- launchButton.addTarget(self, action: #selector(launchWorkflow), for: .touchUpInside)
+ launchButton.setTitleColor(.systemBlue, for: .normal)
+ launchButton.addTarget(self, action: #selector(didTapLaunchWorkflow), for: .touchUpInside)
view.addSubview(launchButton)
@@ -129,11 +128,7 @@ class ViewController: UIViewController {
launchButton.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- @objc private func launchWorkflow() {
+ @objc private func didTapLaunchWorkflow() {
let workflow = Workflow(FirstViewController.self)
.thenPresent(SecondViewController.self)


message: [getting-started-updates] - Update README to include programmatic UIKit getting started - MW

diff:
@@ -52,6 +52,7 @@ And just like that you're started! To see something more practical and in-depth
# Deep Dive
- [Why Workflow?](https://github.com/wwt/Workflow/wiki/Why-This-Library%3F)
- [Installation](https://github.com/wwt/Workflow/wiki/Installation)
-- [Getting Started](https://github.com/wwt/Workflow/wiki/getting-started)
+- [Getting Started with Storyboards](https://github.com/wwt/Workflow/wiki/getting-started)
+- [Getting Started with Programmatic UIKit](https://github.com/wwt/Workflow/wiki/Getting-Started-with-Programmatic-UIKit)
- [Developer Documentation](https://gitcdn.link/repo/wwt/Workflow/main/docs/index.html)
- [Upgrade Path](https://github.com/wwt/Workflow/blob/main/UPGRADE_PATH.md)


message: [getting-started-updates] - Updates link for Getting Started with Programmatic UIKit - MW

diff:
- [Why This Library?](https://github.com/wwt/Workflow/wiki/Why-This-Library%3F)
- [Installation](https://github.com/wwt/Workflow/wiki/Installation)
- [Getting Started with Storyboards](https://github.com/wwt/Workflow/wiki/getting-started)
-- [Getting Started with Programmatic UIKit](https://github.com/wwt/Workflow/wiki/getting-started-uikit)
+- [Getting Started with Programmatic UIKit](https://github.com/wwt/Workflow/wiki/getting-started-with-programmatic-uikit)
- [Developer Documentation](https://gitcdn.link/repo/wwt/Workflow/main/docs/index.html)
\ No newline at end of file