From a5b3f06cdf74af0c4e379dbfaed90020d7b4b9ca Mon Sep 17 00:00:00 2001 From: Federico Tedin Date: Mon, 23 Dec 2024 17:06:35 +0100 Subject: [PATCH 1/6] Add md-k6 script Better options Add workflow Tweak Fix param Test MD creation, modification, rename and deletion More tweaks Revert "Test MD creation, modification, rename and deletion" This reverts commit 393424d4f073f3dfa32d949f27eae060ef1240bd. Tweak script Test file modifications Compare changes on PR commits only Revert "Test file modifications" This reverts commit cab6dae9bd1f3c777160af521d68fa19f5fe6cbd. More tweaks --- .github/workflows/run-code-blocks.yml | 32 +++++++ docs/sources/k6/next/using-k6/checks.md | 2 +- scripts/md-k6.py | 120 ++++++++++++++++++++++++ 3 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/run-code-blocks.yml create mode 100644 scripts/md-k6.py diff --git a/.github/workflows/run-code-blocks.yml b/.github/workflows/run-code-blocks.yml new file mode 100644 index 0000000000..16cc260968 --- /dev/null +++ b/.github/workflows/run-code-blocks.yml @@ -0,0 +1,32 @@ +name: Run Updated Code Blocks (Scripts) + +on: + pull_request: + branches: + - main + paths: + - 'docs/sources/k6/next/**' + +jobs: + run-code-blocks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Get Changed files + id: changed-files + uses: tj-actions/changed-files@v45 + with: + files: | + **.md + - uses: grafana/setup-k6-action@v1 + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + - name: Run Updated Code Blocks + env: + ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} + run: | + for file in ${ALL_CHANGED_FILES}; do + python -u scripts/md-k6.py "$file" + echo + done diff --git a/docs/sources/k6/next/using-k6/checks.md b/docs/sources/k6/next/using-k6/checks.md index 1cfbbc0384..12bda2fcbf 100644 --- a/docs/sources/k6/next/using-k6/checks.md +++ b/docs/sources/k6/next/using-k6/checks.md @@ -100,7 +100,7 @@ $ k6 run script.js In this example, note that the check "is status 200" succeeded 100% of the times it was called. -## Add multiple checks +## Add multiple checks. You can also add multiple checks within a single [check()](https://grafana.com/docs/k6//javascript-api/k6/check) statement: diff --git a/scripts/md-k6.py b/scripts/md-k6.py new file mode 100644 index 0000000000..ca49fcd9f9 --- /dev/null +++ b/scripts/md-k6.py @@ -0,0 +1,120 @@ +# md-k6.py +# Description: A script for running k6 scripts within Markdown files. +# Requires: Python 3.11+ (no external dependencies). +# Usage: +# python3 md-k6.py + +import os +import re +import json +import hashlib +import argparse +import subprocess +import textwrap +import tempfile +from collections import namedtuple + +Script = namedtuple("Script", ["text", "options"]) + + +def run_k6(script: Script) -> None: + script_file = tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".js") + script_file.write(script.text) + script_file.close() + + logs_file = tempfile.NamedTemporaryFile(delete=False, suffix=".json") + logs_file.close() + + k6 = os.getenv("K6_PATH", "k6") + + result = subprocess.run( + [ + k6, + "run", + script_file.name, + "--log-format=json", + f"--log-output=file={logs_file.name}", + "-w", + ], + ) + + if result.returncode: + print("k6 returned non-zero status:", result.returncode) + exit(1) + + with open(logs_file.name) as f: + lines = f.readlines() + + for line in lines: + line = line.strip() + parsed = json.loads(line) + if parsed["level"] == "error": + print("error in k6 script execution:", line) + exit(1) + + +def main() -> None: + print("Starting md-k6 script.") + + parser = argparse.ArgumentParser( + description="Run k6 scripts within Markdown files." + ) + parser.add_argument("file", help="Path to Markdown file.", type=argparse.FileType()) + parser.add_argument( + "--blocks", + default=":", + help="Python-like range of code blocks to run (0, 1, 2, 0:2, 3:, etc.).", + ) + parser.add_argument("--lang", default="javascript", help="Code block language.") + args = parser.parse_args() + + print("Reading from file:", args.file.name) + + lang = args.lang + text = args.file.read() + # Replace magic comment + "```{lang}" with "```{lang}${options}" + text = re.sub("\n```" + lang, "```" + lang + "$" + r"\1", text) + + scripts = [] + blocks = [block.strip() for block in text.split("```")[1::2]] + for b in blocks: + lines = b.splitlines() + if not lines[0].startswith(lang): + continue + + if "$" in lines[0]: + options = lines[0].split("$")[-1].split(",") + else: + options = [] + + if "skip" in options: + continue + + scripts.append(Script(text="\n".join(lines[1:]), options=options)) + + range_parts = args.blocks.split(":") + try: + start = int(range_parts[0]) if range_parts[0] else 0 + end = ( + int(range_parts[1]) + if len(range_parts) > 1 and range_parts[1] + else len(scripts) + ) + except ValueError: + print("Invalid range.") + exit(1) + + print("Number of code blocks (scripts) to run:", len(scripts)) + + for i, script in enumerate(scripts[start:end]): + script_hash = hashlib.sha256(script.text.encode("utf-8")).hexdigest()[:16] + print( + f"Running script #{i} (hash: {script_hash}, options: {script.options}):\n" + ) + print(textwrap.indent(script.text, " ")) + run_k6(script) + print() + + +if __name__ == "__main__": + main() From 7a7649996d956dddd09a151f917a13e35c1839bc Mon Sep 17 00:00:00 2001 From: Federico Tedin Date: Tue, 24 Dec 2024 11:16:01 +0100 Subject: [PATCH 2/6] Undo change --- docs/sources/k6/next/using-k6/checks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/k6/next/using-k6/checks.md b/docs/sources/k6/next/using-k6/checks.md index 12bda2fcbf..1cfbbc0384 100644 --- a/docs/sources/k6/next/using-k6/checks.md +++ b/docs/sources/k6/next/using-k6/checks.md @@ -100,7 +100,7 @@ $ k6 run script.js In this example, note that the check "is status 200" succeeded 100% of the times it was called. -## Add multiple checks. +## Add multiple checks You can also add multiple checks within a single [check()](https://grafana.com/docs/k6//javascript-api/k6/check) statement: From af1404106f1714ed119b89e31024e2d112d254b8 Mon Sep 17 00:00:00 2001 From: Federico Tedin Date: Tue, 31 Dec 2024 12:29:07 +0100 Subject: [PATCH 3/6] Combine with ESLint skip --- CONTRIBUTING/README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ scripts/md-k6.py | 36 ++++++++++++++++++++++++++++++++---- 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING/README.md b/CONTRIBUTING/README.md index f0303c0001..f7e6548f96 100644 --- a/CONTRIBUTING/README.md +++ b/CONTRIBUTING/README.md @@ -164,6 +164,48 @@ export default async function () { ``` ```` +### Code Snippets Evaluation + +In addition to linting code snippets, we also actually run the snippets using k6 OSS. This is done automatically in all open PRs, only for Markdown files that have been changed (when compared to `main`) in the `docs/sources/next` directory. See the `scripts/md-k6.py` script for details on how this works internally. + +Code snippets are run using the `-w` k6 OSS flag. If the code snippet causes k6 to exit with a nonzero status, then the script (and therefore the workflow) will fail. If any error is logged by k6 (for example, because an exception was raised), this will also fail the execution. + +You can control the behaviour of `md-k6.py` via magic `md-k6` HTML comments placed above the code snippets. The format is the following: + +```text + +``` + +That is, `md-k6:` followed by a comma-separated list of options. +Currently, the only option that exists is `skip`, which will cause `md-k6.py` to ignore the code snippet completely (i.e. ``). + +> [!TIP] +> You can combine both `md-k6.py` and ESLint skip directives by placing the `md-k6.py` directive first: +> +> ````Markdown +> +> +> +> ```javascript +> export default async function () { +> const browser = chromium.launch({ headless: false }); +> const page = browser.newPage(); +> } +> ``` +> ```` + +To run the `md-k6.py` script locally, invoke it using Python. For example: + +```bash +python3 scripts/md-k6.py docs/sources/k6/next/examples/functional-testing.md +``` + +You can also read the usage information: + +```bash +python3 scripts/md-k6.py --help +``` + ## Deploy Once a PR is merged to the main branch, if there are any changes made to the `docs/sources` folder, the GitHub Action [`publish-technical-documentation.yml`](https://github.com/grafana/k6-docs/blob/main/.github/workflows/publish-technical-documentation.yml) will sync the changes with the grafana/website repository, and the changes will be deployed to production soon after. diff --git a/scripts/md-k6.py b/scripts/md-k6.py index ca49fcd9f9..b0f7929020 100644 --- a/scripts/md-k6.py +++ b/scripts/md-k6.py @@ -72,8 +72,35 @@ def main() -> None: lang = args.lang text = args.file.read() - # Replace magic comment + "```{lang}" with "```{lang}${options}" - text = re.sub("\n```" + lang, "```" + lang + "$" + r"\1", text) + + # A somewhat complicated regex in order to make parsing of the code block + # easier. Essentially, takes this: + # + # + # ```javascript + # (JS code) + # ``` + # + # And converts it into: + # + # ```javascript$opt1,opt2 + # (JS code) + # ``` + # + # This is done for the entire Markdown file. + # After that's done, we can split the text by "```javascript", and parse + # each part separately. If a part's first line starts with "$", then we + # know one or more options were specified by the user (such as "skip"). + # + # Additionally, we also skip over any "" comments, to + # allow developers to use both md-k6 *and* ESLint skip directives in code + # blocks. + + text = re.sub( + "\n+(\n+)?```" + lang, + "```" + lang + "$" + r"\1", + text, + ) scripts = [] blocks = [block.strip() for block in text.split("```")[1::2]] @@ -83,7 +110,7 @@ def main() -> None: continue if "$" in lines[0]: - options = lines[0].split("$")[-1].split(",") + options = [opt.strip() for opt in lines[0].split("$")[-1].split(",")] else: options = [] @@ -104,7 +131,8 @@ def main() -> None: print("Invalid range.") exit(1) - print("Number of code blocks (scripts) to run:", len(scripts)) + print("Number of code blocks (scripts) read:", len(scripts)) + print("Number of code blocks (scripts) to run:", len(scripts[start:end])) for i, script in enumerate(scripts[start:end]): script_hash = hashlib.sha256(script.text.encode("utf-8")).hexdigest()[:16] From dcdd39b89f9406afbdaad1af876b5e08808adff8 Mon Sep 17 00:00:00 2001 From: Federico Tedin Date: Tue, 31 Dec 2024 12:34:21 +0100 Subject: [PATCH 4/6] Formatting --- CONTRIBUTING/README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING/README.md b/CONTRIBUTING/README.md index f7e6548f96..24fe001da9 100644 --- a/CONTRIBUTING/README.md +++ b/CONTRIBUTING/README.md @@ -16,6 +16,9 @@ When you contribute to the docs, it helps to know how things work. - [Use the `apply-patch` script](#use-the-apply-patch-script) - [Style Guides](#style-guides) - [Shortcodes](#shortcodes) + - [Shortcodes](#shortcodes) + - [Code snippets and ESLint](#code-snippets-and-eslint) + - [Code snippets evaluation](#code-snippets-evaluation) - [Deploy](#deploy) - [Create a new release](#create-a-new-release) @@ -164,9 +167,9 @@ export default async function () { ``` ```` -### Code Snippets Evaluation +### Code snippets evaluation -In addition to linting code snippets, we also actually run the snippets using k6 OSS. This is done automatically in all open PRs, only for Markdown files that have been changed (when compared to `main`) in the `docs/sources/next` directory. See the `scripts/md-k6.py` script for details on how this works internally. +In addition to linting code snippets, we also actually run the snippets using k6 OSS. This is done automatically for all newly opened PRs, only for Markdown files that have been changed (when compared to `main`) in the `docs/sources/next` directory. See the `scripts/md-k6.py` script for details on how this works internally. Code snippets are run using the `-w` k6 OSS flag. If the code snippet causes k6 to exit with a nonzero status, then the script (and therefore the workflow) will fail. If any error is logged by k6 (for example, because an exception was raised), this will also fail the execution. @@ -177,7 +180,8 @@ You can control the behaviour of `md-k6.py` via magic `md-k6` HTML comments plac ``` That is, `md-k6:` followed by a comma-separated list of options. -Currently, the only option that exists is `skip`, which will cause `md-k6.py` to ignore the code snippet completely (i.e. ``). + +Currently, the only option that exists is `skip`, which will cause `md-k6.py` to ignore the code snippet completely (i.e. ``). This is useful for code snippets that only showcase a very specific aspect of k6 scripting and do not contain an actually fully working script. > [!TIP] > You can combine both `md-k6.py` and ESLint skip directives by placing the `md-k6.py` directive first: From 3e07d0318d43c4ac9985296d13902e73af5d1ad5 Mon Sep 17 00:00:00 2001 From: Federico Tedin Date: Mon, 6 Jan 2025 11:22:40 +0100 Subject: [PATCH 5/6] Update CONTRIBUTING/README.md Co-authored-by: Heitor Tashiro Sergent --- CONTRIBUTING/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING/README.md b/CONTRIBUTING/README.md index 24fe001da9..3b26c46647 100644 --- a/CONTRIBUTING/README.md +++ b/CONTRIBUTING/README.md @@ -169,7 +169,7 @@ export default async function () { ### Code snippets evaluation -In addition to linting code snippets, we also actually run the snippets using k6 OSS. This is done automatically for all newly opened PRs, only for Markdown files that have been changed (when compared to `main`) in the `docs/sources/next` directory. See the `scripts/md-k6.py` script for details on how this works internally. +In addition to linting code snippets, we also run the snippets using k6 OSS. This is done automatically on PRs, only for Markdown files that have been changed in the `docs/sources/next` directory when compared to `main`. See the `scripts/md-k6.py` script for details on how this works internally. Code snippets are run using the `-w` k6 OSS flag. If the code snippet causes k6 to exit with a nonzero status, then the script (and therefore the workflow) will fail. If any error is logged by k6 (for example, because an exception was raised), this will also fail the execution. From e03015c5091c340ea62d26db2326cf9c3bf3061f Mon Sep 17 00:00:00 2001 From: Federico Tedin Date: Mon, 6 Jan 2025 11:22:58 +0100 Subject: [PATCH 6/6] Update CONTRIBUTING/README.md Co-authored-by: Heitor Tashiro Sergent --- CONTRIBUTING/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING/README.md b/CONTRIBUTING/README.md index 3b26c46647..3dbb81eaba 100644 --- a/CONTRIBUTING/README.md +++ b/CONTRIBUTING/README.md @@ -171,7 +171,7 @@ export default async function () { In addition to linting code snippets, we also run the snippets using k6 OSS. This is done automatically on PRs, only for Markdown files that have been changed in the `docs/sources/next` directory when compared to `main`. See the `scripts/md-k6.py` script for details on how this works internally. -Code snippets are run using the `-w` k6 OSS flag. If the code snippet causes k6 to exit with a nonzero status, then the script (and therefore the workflow) will fail. If any error is logged by k6 (for example, because an exception was raised), this will also fail the execution. +Code snippets are run using the `-w` k6 OSS flag. If the code snippet causes k6 to exit with a nonzero status, then the script (and, therefore, the workflow) will fail. If any error is logged by k6, for example, because an exception was raised, this will also fail the execution. You can control the behaviour of `md-k6.py` via magic `md-k6` HTML comments placed above the code snippets. The format is the following: