Skip to content

Commit

Permalink
Automatically check front matter of standards documents via a Zuul job (
Browse files Browse the repository at this point in the history
#435)

* Automatically check front matter of standards documents via a Zuul job
   closes #385
* Fix errors found by the script, had to invent fictions regarding stabilization dates
* Remove unused import to make flake happy
* Rectify task name (copy-paste error)
* Drop unnecessary tasks (copy-paste error)
* minor improvements
* Improve error message for duplicates as well as missing front matter

Signed-off-by: Matthias Büchse <[email protected]>
  • Loading branch information
mbuechse authored Dec 22, 2023
1 parent 9dbec71 commit b97f9d4
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 2 deletions.
7 changes: 7 additions & 0 deletions .zuul.d/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
post:
jobs:
- scs-check-gx-scs
check:
jobs:
- scs-check-adr-syntax
- job:
name: scs-check-adr-syntax
parent: base
run: playbooks/adr_syntax.yaml
- job:
name: scs-check-gx-scs
parent: base
Expand Down
1 change: 1 addition & 0 deletions Standards/scs-0100-v1-flavor-naming.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type: Standard
track: IaaS
status: Deprecated
state: v1.1 (for R3)
stabilized_at: 2022-09-08
obsoleted_at: 2023-10-31
---

Expand Down
1 change: 1 addition & 0 deletions Standards/scs-0100-v2-flavor-naming.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ type: Standard
status: Deprecated
track: IaaS
replaces: scs-0100-v1-flavor-naming.md
stabilized_at: 2023-02-21
obsoleted_at: 2023-11-30
---

Expand Down
4 changes: 2 additions & 2 deletions Standards/scs-XXXX-vN-template.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
---
title: _Descriptive title_
type: _Standard | Decision Record_
type: Standard # | Decision Record | Procedural
status: Draft
track: _Global | IaaS | Ops | KaaS | IAM_
track: Global # | IaaS | Ops | KaaS | IAM
---

<!---
Expand Down
134 changes: 134 additions & 0 deletions Tests/chk_adrs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
#!/usr/bin/env python3
"""Tool that can check the syntax of the ADRs in the standards directory
See scs-0001-v1-sovereign-cloud-standards.md for reference.
"""

import collections
import datetime
import os
import os.path
import sys

import yaml


# quoting scs-0001-v1-sovereign-cloud-standards.md regarding front-matter fields:

# | `type` | REQUIRED | one of `Procedural`, `Standard`, or `Decision Record` |
# | `status` | REQUIRED | one of `Proposal`, `Draft`, `Stable`, `Deprecated`, or `Rejected` |
# | `track` | REQUIRED | one of `Global`, `IaaS`, `KaaS`, `IAM`, `Ops` |
# | `obsoleted_at` | REQUIRED if `status` is `Deprecated` | ISO formatted date indicating the date after which the deprecation is in effect |
# | `stabilized_at` | REQUIRED if `status` was ever `Stable` | ISO formatted date indicating the date after which the document was considered stable |
# | `rejected_at` | REQUIRED if `status` is `Rejected` | ISO formatted date indicating the date on which the document was rejected |
# | `replaced_by` | RECOMMENDED if `status` is `Deprecated` or `Rejected`, FORBIDDEN otherwise | List of documents which replace this document. |

UNDEFINED = object()


def optional(predicate):
def check(value):
return value is UNDEFINED or predicate(value)
return check


def iso_date(value):
try:
return isinstance(value, datetime.date) or datetime.date.fromisoformat(value) and True
except ValueError:
return False


# map key to value-checker predicate
FRONT_MATTER_KEYS = {
"type": ("Procedural", "Standard", "Decision Record").__contains__,
"status": ("Proposal", "Draft", "Stable", "Deprecated", "Rejected").__contains__,
"track": ("Global", "IaaS", "KaaS", "IAM", "Ops").__contains__,
"obsoleted_at": optional(iso_date),
"stabilized_at": optional(iso_date),
"rejected_at": optional(iso_date),
}


def print_usage(file=sys.stderr):
"""Help output"""
print("""Usage: chk_adrs.py PATH
This tool checks the syntax of the ADRs in PATH according to scs-0001-v1-sovereign-cloud-standards.
Arguments:
PATH path to the folder containing the adr documents (md files)
""", end='', file=file)


class Checker:
def __init__(self):
self.errors = 0

def emit(self, s):
print(f"ERROR: {s}", file=sys.stderr)
self.errors += 1

def check_names(self, mds):
"""Check the list `mds` of md file names for name collisions"""
# count the occurrences of the prefixes of length 12, e.g., scs-0001-v1-
# no duplicates allowed
counts = collections.Counter([fn[:12] for fn in mds])
duplicates = sorted([fn for fn in mds if counts[fn[:12]] > 1])
if duplicates:
self.emit(f"duplicates found: {', '.join(duplicates)}")

def check_front_matter(self, fn, front):
"""Check the dict `front` of front matter; `fn` is for context in error messages"""
if front is None:
self.emit(f"in {fn}: is missing front matter altogether")
return
# check each field in isolation
errors = [
key
for key, predicate in FRONT_MATTER_KEYS.items()
if not predicate(front.get(key, UNDEFINED))
]
if errors:
self.emit(f"in {fn}: syntax errors with key(s) {', '.join(errors)}")
# now do cross-field checks
status = front.get("status")
if "replaced_by" in front and status not in ("Deprecated", "Rejected"):
self.emit(f"in {fn}: replaced_by is set, but status does not match")
if status == "Deprecated" and "obsoleted_at" not in front:
self.emit(f"in {fn}: status is Deprecated, but deprecated_at date is missing")
if status in ("Stable", "Deprecated") and "stabilized_at" not in front:
self.emit(f"in {fn}: status is Stable or Deprecated, but stabilized_at date is missing")
if status == "Rejected" and "rejected_at" not in front:
self.emit(f"in {fn}: status is Rejected, but rejected_at date is missing")


def main(argv):
if len(argv) != 2:
raise RuntimeError("must specify exactly one argument, PATH")
path = argv[1]
mds = sorted([
fn
for fn in os.listdir(path)
if fn.startswith("scs-") and fn.endswith(".md")
])
checker = Checker()
checker.check_names(mds)
# now load each file and check front matter
for fn in mds:
with open(os.path.join(path, fn), "rb") as flo:
loader = yaml.SafeLoader(flo)
try:
front = loader.get_data()
finally:
loader.dispose()
checker.check_front_matter(fn, front)
return checker.errors


if __name__ == "__main__":
try:
sys.exit(main(sys.argv))
except Exception as e:
print(f"CRITICAL: {e!s}", file=sys.stderr)
sys.exit(1)
33 changes: 33 additions & 0 deletions playbooks/adr_syntax.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
- name: Run ADR syntax check tool
hosts: all
roles:
- role: ensure-pip # https://zuul-ci.org/docs/zuul-jobs/latest/python-roles.html#role-ensure-pip
tasks:
- name: Copy ADRs on the node
ansible.builtin.copy:
src: "../Standards"
dest: "~/"
mode: 0500
no_log: false

- name: Copy Tests on the node
ansible.builtin.copy:
src: "../Tests"
dest: "~/"
mode: 0500
no_log: false

- name: Install dependencies
ansible.builtin.pip:
requirements: /home/ubuntu/Tests/requirements.txt

- name: Run check script
ansible.builtin.command:
cmd: python3 /home/ubuntu/Tests/chk_adrs.py /home/ubuntu/Standards
register: result
changed_when: true
failed_when: result.rc != 0

- ansible.builtin.debug:
msg: "{{ result.stdout }} {{ result.stderr }}"

0 comments on commit b97f9d4

Please sign in to comment.