diff --git a/.coderabbit.yaml b/.coderabbit.yaml index ae50ad9..551742e 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -64,6 +64,17 @@ reviews: - Use `pathlib` instead of `os.path` for platform portability in Python - If the project contains Python files, ensure there's a GitHub Actions workflow called `project--python-quality.yml` + - path: "project-*/**/*.sh" + instructions: >- + Shell scripts should be written in portable shell (/bin/sh) syntax since bash is not + installed on the Axis devices. Since `local` is not supported in all cameras (like + AXIS OS 9.80), it should be avoided unless the AXIS OS limitations are explicitly + mentioned. If not using `local`, it can be good to use a function specific prefix + for the variable names to avoid conflicts with other variables. + Clearly document in the head of the file which environment variables they expect + and if they are optional or required. + It's normally good to use `set -eu` for stricter error handling, but `-o pipefail` + is not supported in the Axis devices' shell. - path: "**/*.md" instructions: >- Documentation files should clearly communicate the dual audience: (1) server-side diff --git a/README.md b/README.md index 6f62e87..05ace0a 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ This repository provides resources for the [FixedIT Data Agent ACAP](https://fix - [Edge Device Customization Examples](#edge-device-customization-examples) - [Older Examples](#older-examples) - [Hello, World!](#hello-world) + - [Visualizing a GitHub Workflow Status with an Axis Strobe](#visualizing-a-github-workflow-status-with-an-axis-strobe) - [Creating a Timelapse with AWS S3 Upload](#creating-a-timelapse-with-aws-s3-upload) @@ -58,6 +59,61 @@ flowchart TD style C fill:#ffebee,stroke:#e53935 ``` +### Visualizing a GitHub Workflow Status with an Axis Strobe + +The [Strobe Color From GitHub Workflow](./project-strobe-color-from-github-workflow) project demonstrates real-time CI/CD status visualization by automatically controlling an Axis strobe light based on GitHub Actions workflow results. When your workflow succeeds, the strobe glows green; when it fails, it turns red; and yellow indicates tests are running. The FixedIT Data Agent should be running on the Axis strobe device, since this will poll the GitHub API, no other infrastructure is required. + +![Axis strobe with green color](./project-strobe-color-from-github-workflow/.images/strobe.jpg) + +The following diagram shows the data flow of the "Strobe Color From GitHub Workflow" project. For more details see the [README](./project-strobe-color-from-github-workflow/README.md) in the `project-strobe-color-from-github-workflow` directory. + +```mermaid +flowchart TD + XAgent["config_agent.conf:
Agent Configuration
interval=5s, collection_jitter=1s"] --> A + XDebug["TELEGRAF_DEBUG"] --> XAgent + + A["📡 config_input_github.conf:
Fetch GitHub Actions API
Get recent workflow runs"] -->|github_workflow| B["🔍 config_process_filter_by_name.conf:
Filter by workflow name
Keep only target workflow"] + + XGithubCreds["GITHUB_TOKEN
GITHUB_USER
GITHUB_REPO
GITHUB_BRANCH"] --> A + XWorkflowName["GITHUB_WORKFLOW"] --> B + + B -->|github_workflow_filtered| C1 + + subgraph SelectLatest ["config_process_select_latest.conf:
Select Latest Workflow Run"] + C1{"Compare run_number
with state"} + C1 -->|"run_number ≥ latest"| C2["Update state
latest_run_number = run_number"] + C1 -->|"run_number < latest"| C3["Drop older run"] + C2 --> C4["Pass through metric"] + + C2 -.->|"💾 Persistent state"| CX["state.latest_run_number"] + CX -.-> C1 + end + + C4 -->|github_workflow_latest| D["🎨 config_process_status_to_color.conf:
Map workflow conclusion to color
success → green
failure → red
running → yellow"] + + D -->|workflow_color| E["🚨 config_output_strobe.conf:
Execute trigger_strobe.sh script
Enable target color profile
Disable other profiles"] + + XVapix["HELPER_FILES_DIR
VAPIX_USERNAME
VAPIX_PASSWORD
VAPIX_IP"] --> E + + style XAgent fill:#f5f5f5,stroke:#9e9e9e + style XDebug fill:#f5f5f5,stroke:#9e9e9e + style XGithubCreds fill:#f5f5f5,stroke:#9e9e9e + style XWorkflowName fill:#f5f5f5,stroke:#9e9e9e + style XVapix fill:#f5f5f5,stroke:#9e9e9e + style A fill:#e8f5e9,stroke:#43a047 + style B fill:#f3e5f5,stroke:#8e24aa + style SelectLatest fill:#f3e5f5,stroke:#8e24aa + style C1 fill:#ffffff,stroke:#673ab7 + style C2 fill:#ffffff,stroke:#673ab7 + style C3 fill:#ffffff,stroke:#673ab7 + style C4 fill:#ffffff,stroke:#673ab7 + style CX fill:#fff3e0,stroke:#fb8c00 + style D fill:#f3e5f5,stroke:#8e24aa + style E fill:#ffebee,stroke:#e53935 +``` + +This example showcases how simple configuration files and shell scripts can create powerful edge intelligence in your Axis strobes without traditional embedded development complexity. The project could easily be adapted to work together with other APIs to visualize statuses such as server health monitoring, weather warnings (like high wind alerts), IoT sensor data (temperature, moisture, etc.), security system states, or any REST API (or most other APIs) that provides status information. + ### Creating a Timelapse with AWS S3 Upload The [Timelapse with AWS S3 Upload](./project-timelapse-s3) project demonstrates automated timelapse video creation using the FixedIT Data Agent. This solution captures images at regular intervals from an AXIS device and uploads them to AWS S3 with timestamped filenames, creating a chronological sequence perfect for timelapse generation. Perfect for construction sites, environmental monitoring, safety applications, or any scenario requiring periodic visual documentation. diff --git a/project-strobe-color-from-github-workflow/.gitignore b/project-strobe-color-from-github-workflow/.gitignore new file mode 100644 index 0000000..2dc6f14 --- /dev/null +++ b/project-strobe-color-from-github-workflow/.gitignore @@ -0,0 +1,2 @@ +vars.* +token.txt \ No newline at end of file diff --git a/project-strobe-color-from-github-workflow/.images/axis-strobe-all-profiles.png b/project-strobe-color-from-github-workflow/.images/axis-strobe-all-profiles.png new file mode 100644 index 0000000..2a5eb87 Binary files /dev/null and b/project-strobe-color-from-github-workflow/.images/axis-strobe-all-profiles.png differ diff --git a/project-strobe-color-from-github-workflow/.images/axis-strobe-profile-configuration.png b/project-strobe-color-from-github-workflow/.images/axis-strobe-profile-configuration.png new file mode 100644 index 0000000..923ec96 Binary files /dev/null and b/project-strobe-color-from-github-workflow/.images/axis-strobe-profile-configuration.png differ diff --git a/project-strobe-color-from-github-workflow/.images/debug-mode-script-logs.png b/project-strobe-color-from-github-workflow/.images/debug-mode-script-logs.png new file mode 100644 index 0000000..a449032 Binary files /dev/null and b/project-strobe-color-from-github-workflow/.images/debug-mode-script-logs.png differ diff --git a/project-strobe-color-from-github-workflow/.images/debug_github_api_data.png b/project-strobe-color-from-github-workflow/.images/debug_github_api_data.png new file mode 100644 index 0000000..31d07ef Binary files /dev/null and b/project-strobe-color-from-github-workflow/.images/debug_github_api_data.png differ diff --git a/project-strobe-color-from-github-workflow/.images/debug_logs_from_github_api.png b/project-strobe-color-from-github-workflow/.images/debug_logs_from_github_api.png new file mode 100644 index 0000000..c19fbce Binary files /dev/null and b/project-strobe-color-from-github-workflow/.images/debug_logs_from_github_api.png differ diff --git a/project-strobe-color-from-github-workflow/.images/github-no-workflow-runs.png b/project-strobe-color-from-github-workflow/.images/github-no-workflow-runs.png new file mode 100644 index 0000000..f441258 Binary files /dev/null and b/project-strobe-color-from-github-workflow/.images/github-no-workflow-runs.png differ diff --git a/project-strobe-color-from-github-workflow/.images/github-token.png b/project-strobe-color-from-github-workflow/.images/github-token.png new file mode 100644 index 0000000..aace771 Binary files /dev/null and b/project-strobe-color-from-github-workflow/.images/github-token.png differ diff --git a/project-strobe-color-from-github-workflow/.images/github-workflow-runs.png b/project-strobe-color-from-github-workflow/.images/github-workflow-runs.png new file mode 100644 index 0000000..c144982 Binary files /dev/null and b/project-strobe-color-from-github-workflow/.images/github-workflow-runs.png differ diff --git a/project-strobe-color-from-github-workflow/.images/logs-skipped-workflow.png b/project-strobe-color-from-github-workflow/.images/logs-skipped-workflow.png new file mode 100644 index 0000000..d868256 Binary files /dev/null and b/project-strobe-color-from-github-workflow/.images/logs-skipped-workflow.png differ diff --git a/project-strobe-color-from-github-workflow/.images/logs-with-errors.png b/project-strobe-color-from-github-workflow/.images/logs-with-errors.png new file mode 100644 index 0000000..cab2a6a Binary files /dev/null and b/project-strobe-color-from-github-workflow/.images/logs-with-errors.png differ diff --git a/project-strobe-color-from-github-workflow/.images/output-messages-in-the-log.png b/project-strobe-color-from-github-workflow/.images/output-messages-in-the-log.png new file mode 100644 index 0000000..3bbd1f4 Binary files /dev/null and b/project-strobe-color-from-github-workflow/.images/output-messages-in-the-log.png differ diff --git a/project-strobe-color-from-github-workflow/.images/strobe.jpg b/project-strobe-color-from-github-workflow/.images/strobe.jpg new file mode 100644 index 0000000..e657308 Binary files /dev/null and b/project-strobe-color-from-github-workflow/.images/strobe.jpg differ diff --git a/project-strobe-color-from-github-workflow/.images/uploaded-files-with-order.png b/project-strobe-color-from-github-workflow/.images/uploaded-files-with-order.png new file mode 100644 index 0000000..e2d29b2 Binary files /dev/null and b/project-strobe-color-from-github-workflow/.images/uploaded-files-with-order.png differ diff --git a/project-strobe-color-from-github-workflow/.images/webinar-on-youtube.png b/project-strobe-color-from-github-workflow/.images/webinar-on-youtube.png new file mode 100644 index 0000000..7ecb579 Binary files /dev/null and b/project-strobe-color-from-github-workflow/.images/webinar-on-youtube.png differ diff --git a/project-strobe-color-from-github-workflow/LICENSE b/project-strobe-color-from-github-workflow/LICENSE new file mode 100644 index 0000000..6f9a12c --- /dev/null +++ b/project-strobe-color-from-github-workflow/LICENSE @@ -0,0 +1,7 @@ +Copyright 2025 FixedIT Consulting AB + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/project-strobe-color-from-github-workflow/README.md b/project-strobe-color-from-github-workflow/README.md new file mode 100644 index 0000000..b08b785 --- /dev/null +++ b/project-strobe-color-from-github-workflow/README.md @@ -0,0 +1,619 @@ +# Strobe Color From GitHub API + +This project demonstrates how the [FixedIT Data Agent](https://fixedit.ai/products-data-agent/) can be deployed on an [Axis strobe light](https://www.axis.com/products/axis-d4100-e-network-strobe-siren/support) to fetch GitHub API data and dynamically control the device's color based on workflow execution status. The target GitHub repository should have a configured workflow, and this project will monitor the execution status of the latest workflow run on the main branch. + +## How It Works + +The system operates in a continuous monitoring loop, automatically fetching GitHub workflow status and updating the strobe light color accordingly: + +```mermaid +flowchart TD + XAgent["config_agent.conf:
Agent Configuration
interval=5s, collection_jitter=1s"] --> A + XDebug["TELEGRAF_DEBUG"] --> XAgent + + A["📡 config_input_github.conf:
Fetch GitHub Actions API
Get recent workflow runs"] -->|github_workflow| B["🔍 config_process_filter_by_name.conf:
Filter by workflow name
Keep only target workflow"] + + XGithubCreds["GITHUB_TOKEN
GITHUB_USER
GITHUB_REPO
GITHUB_BRANCH"] --> A + XWorkflowName["GITHUB_WORKFLOW"] --> B + + B -->|github_workflow_filtered| C1 + + subgraph SelectLatest ["config_process_select_latest.conf:
Select Latest Workflow Run"] + C1{"Compare run_number
with state"} + C1 -->|"run_number ≥ latest"| C2["Update state
latest_run_number = run_number"] + C1 -->|"run_number < latest"| C3["Drop older run"] + C2 --> C4["Pass through metric"] + + C2 -.->|"💾 Persistent state"| CX["state.latest_run_number"] + CX -.-> C1 + end + + C4 -->|github_workflow_latest| D["🎨 config_process_status_to_color.conf:
Map workflow conclusion to color
success → green
failure → red
running → yellow"] + + D -->|workflow_color| E["🚨 config_output_strobe.conf:
Execute trigger_strobe.sh script
Enable target color profile
Disable other profiles"] + + XVapix["HELPER_FILES_DIR
VAPIX_USERNAME
VAPIX_PASSWORD
VAPIX_IP"] --> E + + style XAgent fill:#f5f5f5,stroke:#9e9e9e + style XDebug fill:#f5f5f5,stroke:#9e9e9e + style XGithubCreds fill:#f5f5f5,stroke:#9e9e9e + style XWorkflowName fill:#f5f5f5,stroke:#9e9e9e + style XVapix fill:#f5f5f5,stroke:#9e9e9e + style A fill:#e8f5e9,stroke:#43a047 + style B fill:#f3e5f5,stroke:#8e24aa + style SelectLatest fill:#f3e5f5,stroke:#8e24aa + style C1 fill:#ffffff,stroke:#673ab7 + style C2 fill:#ffffff,stroke:#673ab7 + style C3 fill:#ffffff,stroke:#673ab7 + style C4 fill:#ffffff,stroke:#673ab7 + style CX fill:#fff3e0,stroke:#fb8c00 + style D fill:#f3e5f5,stroke:#8e24aa + style E fill:#ffebee,stroke:#e53935 +``` + +Color scheme: + +- Light green: Input nodes / data ingestion +- Purple: Processing nodes / data processing and logic +- Orange: Storage nodes / persistent data +- Red: Output nodes / notifications +- Light gray: Configuration data +- White: Logical operations + +This project uses **Telegraf's Starlark processor** for flexible data transformation and filtering. Starlark is a Python-like scripting language that provides more powerful processing capabilities than Telegraf's built-in processors, especially for complex logic and environment variable support. + +**Learn more about Starlark in Telegraf:** + +- [How to Use Starlark with Telegraf](https://www.influxdata.com/blog/how-use-starlark-telegraf) - Overview and basic concepts +- [Quick Start Guide: Telegraf Starlark Processor](https://www.influxdata.com/blog/quick-start-telegraf-starlark-processor-plugin/) - Practical examples and state management + +## Why Choose This Approach? + +**No C/C++ development required!** Unlike traditional Axis ACAP applications that require complex C/C++ programming, this solution uses simple configuration files and basic shell scripting. + +This example is perfect for **system integrators and IT professionals** who want to create custom device automation without the complexity of traditional embedded development. All you need is: + +- Experience configuring IT services (similar to setting up monitoring tools) +- Basic shell scripting knowledge (can be learned quickly) +- Familiarity with REST APIs and JSON (common in modern IT environments) +- Access to an Axis device with strobe capability (AXIS D4100-E Network Strobe Siren, AXIS D4100-VE Mk II Network Strobe Siren, AXIS D4200-VE Network Strobe Speaker, or similar) + +**The result:** Custom edge intelligence that would typically require months of ACAP development can now be implemented in hours using familiar IT tools and practices. + +## Table of Contents + + + +- [Demo Video](#demo-video) +- [Compatibility](#compatibility) + - [AXIS OS Compatibility](#axis-os-compatibility) + - [FixedIT Data Agent Compatibility](#fixedit-data-agent-compatibility) +- [Quick Setup](#quick-setup) + - [High-Level Steps](#high-level-steps) + - [Creating the GitHub workflow](#creating-the-github-workflow) + - [Creating a GitHub access token](#creating-a-github-access-token) + - [Creating the color profiles in the Axis strobe](#creating-the-color-profiles-in-the-axis-strobe) +- [Troubleshooting](#troubleshooting) + - [Strobe doesn't change color](#strobe-doesnt-change-color) + - [No workflow data appears](#no-workflow-data-appears) + - [Workflow not found among recent runs](#workflow-not-found-among-recent-runs) + - [GitHub API issues](#github-api-issues) +- [Configuration Files](#configuration-files) + - [config_agent.conf](#config_agentconf) + - [config_input_github.conf](#config_input_githubconf) + - [config_process_filter_by_name.conf](#config_process_filter_by_nameconf) + - [config_process_select_latest.conf](#config_process_select_latestconf) + - [config_process_status_to_color.conf](#config_process_status_to_colorconf) + - [config_output_strobe.conf](#config_output_strobeconf) + - [test_files/config_output_stdout.conf](#test_filesconfig_output_stdoutconf) + - [test_files/config_input_file.conf](#test_filesconfig_input_fileconf) +- [Local Testing on Host](#local-testing-on-host) + - [Prerequisites](#prerequisites) + - [Host Testing Limitations](#host-testing-limitations) + - [Test GitHub API data parsing using mock data](#test-github-api-data-parsing-using-mock-data) + - [Test latest workflow selection logic](#test-latest-workflow-selection-logic) + - [Test GitHub API integration](#test-github-api-integration) + - [Test strobe control using mock data](#test-strobe-control-using-mock-data) + - [Test complete workflow integration](#test-complete-workflow-integration) +- [License](#license) + + + +## Demo Video + +[![Watch the demo](./.images/webinar-on-youtube.png)](https://www.youtube.com/watch?v=nLwVUYieFLE) + +In this demo, we show how **anyone with basic IT skills can create intelligent edge devices** using the FixedIT Data Agent—no cloud dependency, no C/C++ programming, no complex development environment setup required. + +Using a GitHub Actions job as an example input, we demonstrate how to: + +- Make the Axis strobe fetch external API data from the GitHub Actions CI status +- Transform data using simple Starlark scripts to decide the color of the strobe light +- Trigger a change of the strobe light color via standard HTTP API calls (VAPIX) + +This effectively shows how to transform an Axis strobe to an intelligent device that can poll third-party APIs and set its color based on the API return status. This can easily be adapted to use any cloud-based or locally hosted API as an input. Whether you're building smart alerts, visual indicators, or edge-based automation pipelines—this is a glimpse of what FixedIT Data Agent makes possible. + +## Compatibility + +### AXIS OS Compatibility + +- **Minimum AXIS OS version**: Should be compatible with AXIS OS 11 and 12+. +- **Required tools**: Uses `jq` which was not available in older AXIS OS versions. Uses `curl` which is installed by default, and standard Unix utilities (`cat`, `echo`, `printf`, `tr`). +- **Other notes**: Uses HTTP Digest authentication for VAPIX API calls which is supported in all AXIS OS versions. + +### FixedIT Data Agent Compatibility + +- **Minimum Data Agent version**: 1.1 +- **Required features**: Uses the `VAPIX_USERNAME` and `VAPIX_PASSWORD` environment variables which was added in FixedIT Data Agent v1.1. Depends on the load order of config files which was not visible in the web user interface in versions prior to 1.1. + +## Quick Setup + +### High-Level Steps + +1. **Create a GitHub repository with a workflow** (see instructions below at [Creating the GitHub workflow](#creating-the-github-workflow)) + +2. **Create a GitHub access token** with `workflow` scope (see instructions below at [Creating a GitHub access token](#creating-a-github-access-token)) + +3. **Create color profiles in your Axis strobe** named `green`, `yellow`, and `red` (see instructions below at [Creating the color profiles in the Axis strobe](#creating-the-color-profiles-in-the-axis-strobe)) + +4. **Configure FixedIT Data Agent variables:** + + Set the custom environment variables in the `Extra env` parameter as a semicolon-separated list: + + ```txt + GITHUB_TOKEN=your_github_token;GITHUB_USER=your_github_username;GITHUB_REPO=your_repo_name;GITHUB_BRANCH=main;GITHUB_WORKFLOW=Your Workflow Name + ``` + + Replace `your_github_token` with the token you created in step 2 (as explained in [Creating a GitHub access token](#creating-a-github-access-token)), `your_github_username` with your GitHub username, `your_repo_name` with the name of the repository (you can see this from the repository URL, e.g. `https://github.com/your_github_username/your_repo_name`), `main` with the branch you want to monitor (keep `main` if that’s the branch you want), and `Your Workflow Name` with the precise name of the workflow (as explained in step 1, [Creating the GitHub workflow](#creating-the-github-workflow)) + + Also set the `Vapix username` and `Vapix password` parameters. For increased security, it is recommended to create a new user with `operator` privileges (which is the lowest privilege level that allows you to control the strobe light). This can be done by going to the `System` tab and click on the `Accounts` sub-tab. Then click on `Add account`. You can however use the default `root` user with the same password as you used to login to the device's web interface. + +5. **Upload the configuration files to the FixedIT Data Agent** + + Upload all the `*.conf` files from this directory to the FixedIT Data Agent by pressing the `Upload Config` button in the top right corner of the UI. You should also upload the `trigger_strobe.sh` file as a helper file by pressing "Upload Helper File" and select the "Make executable" checkbox. + +6. **Disable the bundled configuration files and enable the configuration files you uploaded** + + **In this project, order of the files matters!** + + This project is making use of multiple processors stacked after each other, in Telegraf, this means that the first processor must already be defined when the second processor depending on the first processor is defined. This is handled by the load order which is visible in the configuration UI. Files enabled later will have a later load order. In this case it is important that you enable the `config_process_filter_by_name.conf` file before the `config_process_select_latest.conf` file, and that one before the `config_process_status_to_color.conf` file. + + This can be seen in the configuration UI: + ![Configuration UI](./.images/uploaded-files-with-order.png) + +> [!IMPORTANT] +> Note that the load order of the files is visible in the configuration UI. Make sure that you enable the files in the correct order and verify that the load order is the same as in the screenshot above. +> Also make sure that the uploaded helper file is marked as executable which is seen by the green color and the terminal icon. + +The strobe light should now change color based on the status of the latest workflow run on your specified branch. If it does not, see the [Troubleshooting](#troubleshooting) section below. Try editing the file `data.json` file in your repository and you should see the strobe first change to yellow, then to green or red depending on the result of the workflow. + +### Creating the GitHub workflow + +How to create a GitHub workflow is outside the scope of this project. However, here is one example. + +Adding this example file to the `/.github/workflows` folder will create a workflow for the repository. The name of the file does not matter, but the suffix `.yml` is required. You can name it e.g. `my-workflow.yml`. For more information about creating GitHub workflows, see the [GitHub Actions documentation](https://docs.github.com/en/actions). + +```yml +# This job will trigger when the data.json file is changed. It will then +# validate the JSON format and fail the job if the file is not valid. +name: Validate JSON + +on: + # Run on push and pull requests that modify data.json + push: + paths: + - "data.json" + pull_request: + paths: + - "data.json" + +jobs: + validate-json: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Validate JSON + run: | + # Check if data.json exists + if [ ! -f data.json ]; then + echo "Error: data.json file not found" + exit 1 + fi + + # Validate JSON using jq + if ! jq '.' data.json > /dev/null 2>&1; then + echo "Error: data.json contains invalid JSON" + exit 1 + fi + + echo "Success: data.json is valid JSON" +``` + +> [!IMPORTANT] +> **Important notes about the `GITHUB_WORKFLOW` variable:** +> +> - Must be set to exactly the name of the workflow you want to monitor (the `name` field in the workflow YAML file). +> - **Do NOT use quotes** around workflow names with spaces in the `Extra Env` parameter due to how the FixedIT Data Agent parses the variables. The FixedIT Data Agent preserves quotes as literal characters, so `GITHUB_WORKFLOW="Validate JSON"` would become `"Validate JSON"` (with quotes) instead of `Validate JSON`. + +Now you have created a GitHub workflow that will run whenever the `data.json` file is changed, but you still don't have any runs of the workflow. This can be seen by going to the "Actions" tab in the repository. + +![GitHub Actions tab with no workflow runs](.images/github-no-workflow-runs.png) + +Add the file `data.json` to the repository and you should see that there are historical runs with a successful or failed result. + +![GitHub Actions tab with workflow runs](.images/github-workflow-runs.png) + +### Creating a GitHub access token + +1. Go to GitHub Settings by pressing your profile picture in the top right corner and select "Settings" +1. Click on "Developer settings" +1. Click on "Personal access tokens" +1. Click on "Tokens (classic)" +1. Select "workflow" under "Select scope" + +Note that you can create a fine-grained token instead if you want to be more specific about what the token gives access to. + +![GitHub token setup](./.images/github-token.png) + +Copy this token and use it in the `GITHUB_TOKEN` environment variable in the FixedIT Data Agent configuration. + +### Creating the color profiles in the Axis strobe + +The application workflow will set the strobe light based on the name of the color profile. The script ensures exclusive operation by automatically deactivating all other color profiles when activating a new one, which means you don't need to worry about profile priorities or overlapping durations. Before this works, you need to login to the Axis strobe and create three color profiles named `green`, `yellow`, and `red`. + +1. Go to the Axis device web interface. +1. Click on "Profiles". +1. Click on "Create". +1. Enter a name for the profile (`green`, `yellow`, or `red`). Note that these must have exactly these names for the script to work. +1. Choose the "Pattern" and "Intensity" based on your preference. +1. Set the "Color" (e.g. "Green"). +1. Set "Duration" to "Continuous" or "Time" and select a duration that is at least as long as the sync interval specified in the `config_agent.conf` file. Since the script deactivates all other color profiles when activating a new one, the duration can be set generously (e.g., 60 seconds or more) without worrying about overlapping profiles. You might set it to "Continuous", but then it will not be visible if the application would stop running since the light will continue in the same color without a time limitation. +1. Leave "Priority" as is. Since only one profile is active at a time, priority settings don't matter. +1. Click on "Save". +1. Repeat for the other two profiles. + +![Axis strobe profile configuration](./.images/axis-strobe-profile-configuration.png) + +It should now look like this: + +![All profiles](./.images/axis-strobe-all-profiles.png) + +## Troubleshooting + +The first step in troubleshooting is to go to the "Logs" page in the FixedIT Data Agent and check the logs for any errors. + +![Logs page with error messages](.images/logs-with-errors.png) + +In the image above, we can see that there are error messages being repeatedly logged. More specifically, we can see that there is an error on line 41 in the `trigger_strobe.sh` script and that the problem is that the `VAPIX_IP` variable is not set. If this is the case, take a look again at the [Quick Setup](#quick-setup) section above and make sure you specified the variables correctly in the `Extra env` parameter. + +If the problem is still not visible, enable the `Debug mode` option in the FixedIT Data Agent for detailed logs. This will also make the `trigger_strobe.sh` script output debug logs to the `trigger_strobe.debug` file in the helper files directory. You need to refresh the web UI to see the new file. + +![Debug logs created by the script](./.images/debug-mode-script-logs.png) + +In the example above, the debug log for the `trigger_strobe.sh` script shows that the API command fails due to a space incorrectly specified in the URL of the strobe. + +### Strobe doesn't change color + +**First, check if workflow data is being processed:** + +Upload and enable `test_files/config_output_stdout.conf`. This will cause the Telegraf pipeline to show the internal messages in the Logs page of the FixedIT Data Agent. There you can look to see if `workflow_color` metrics are being produced. + +![Logs page with workflow color metrics](.images/output-messages-in-the-log.png) + +If no data is being produced, see [No workflow data appears](#no-workflow-data-appears) section below. + +**If workflow data is being processed, check strobe configuration:** + +- Verify color profiles (`green`, `yellow`, `red`) are created on the device +- Check that `VAPIX_USERNAME` and `VAPIX_PASSWORD` are correct +- The strobe control API requires at least operator privileges +- You'll see errors like `curl: (22) The requested URL returned error: 401` and `Failed to start profile 'green'` if the VAPIX user is not valid +- Check the FixedIT Data Agent logs page for any VAPIX-related errors + +### No workflow data appears + +This typically occurs when the system can't find your target workflow among the recent runs, or when there's a configuration issue. This would be indicated by FixedIT Data Agent showing messages like "Buffer fullness: 0 / 2 metrics" in debug logs (when `Debug mode` is enabled) which means that it is polling the GitHub API but does not find any jobs. If you see a line like `[processors.starlark] Workflow name matches target 'Validate JSON', passing through`, then it means that a job for the specified workflow was found, while if you see `[processors.starlark] Skipping workflow 'Validate JSON' (target is 'Validate JSONN')` then it means that if found a job but it did not match the specified workflow name and was thus dropped. In this example, the reason is an extra `N` at the end of the specified `GITHUB_WORKFLOW` environment variable (`Validate JSONN` instead of `Validate JSON`). Note that these messages are only visible when `Debug mode` is enabled. + +![Workflow name mismatch](.images/logs-skipped-workflow.png) + +- **No jobs in the repository**: If the file `data.json` does not exist or your workflow configuration is not set up correctly, there will be no jobs in the repository. Check the [Creating the GitHub workflow](#creating-the-github-workflow) section above again. +- **Workflow name mismatch**: `GITHUB_WORKFLOW` doesn't match exactly with the workflow name in your GitHub Actions YAML file. Check the [Creating the GitHub workflow](#creating-the-github-workflow) section above again. +- **Quote parsing issue**: The FixedIT Data Agent's `Extra env` parameter handling (see below) +- **Target workflow not in results**: Your target workflow is not among the fetched runs due to `per_page` being too small, see [Workflow not found among recent runs](#workflow-not-found-among-recent-runs) section below. +- **Invalid credentials**: GitHub token issues (you should see 401 errors in logs). + +**How the FixedIT Data Agent parses the `Extra env` parameter:** +The FixedIT Data Agent preserves quotes as literal characters instead of treating them as string delimiters. When you set `GITHUB_WORKFLOW="Validate JSON"` in the `Extra env` parameter, the actual environment variable value becomes `"Validate JSON"` (with literal quotes), not `Validate JSON`. + +### Workflow not found among recent runs + +If your target workflow isn't among the recent runs fetched by the API, no metrics are produced and the strobe appears "silent". + +**The Problem:** The `per_page` parameter in [config_input_github.conf](./config_input_github.conf) might be too small. If you have 6 workflows but `per_page=3`, your target workflow might not be in the returned results. + +**The Solution:** Increase `per_page` in [config_input_github.conf](./config_input_github.conf) to at least the number of workflows in the repository. + +**Diagnose this issue:** + +Disable all config files except `config_agent.conf`, `config_input_github.conf`, and `config_output_stdout.conf`. + +![Debugging the GitHub API data](.images/debug_github_api_data.png) + +This should cause the raw metric parsed from the GitHub API input to be logged to the Logs page of the FixedIT Data Agent. The reason they will not be logged if other config files are enabled is that a metric consumed by another processor will not propagate to the output plugins unless explicitly specified. + +![Debug logs from GitHub API](.images/debug_logs_from_github_api.png) + +You can also try to run the API client command manually from your developer machine to see if you get the same result: + +```bash +curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$GITHUB_USER/$GITHUB_REPO/actions/runs?branch=$GITHUB_BRANCH&per_page=10" | jq '.workflow_runs[] | .name' +``` + +Check if your `GITHUB_WORKFLOW` value appears in the output. If it does, it should be found by the starlark scripts, if it does not, then you need to debug your GitHub workflows and possibly increase the `per_page` limit. + +### GitHub API issues + +**Common API errors:** + +- `"Bad credentials"` (401): GitHub token is invalid or expired +- `received status code 401 (Unauthorized)`: Token lacks `workflow` scope or repository access +- Verify repository/branch/workflow names are correct + +**Test the GitHub API:** + +```bash +# Test fetching recent workflow runs (same as the system does) +curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$GITHUB_USER/$GITHUB_REPO/actions/runs?branch=$GITHUB_BRANCH&per_page=10" | jq '.workflow_runs[] | select(.name == env.GITHUB_WORKFLOW) | .conclusion' | head -1 +``` + +The conclusion can be `success`, `failure` or null (when it is running). + +An example successful response can be seen in the [test_files/sample.json](./test_files/sample.json) file. + +## Configuration Files + +This project uses several configuration files that work together to create a data pipeline. Each file handles a specific part of the workflow: + +### config_agent.conf + +Controls how often the system checks GitHub for updates (every 5 seconds by default). Also includes timing randomization to prevent multiple devices from overwhelming GitHub's servers. + +### config_input_github.conf + +Defines how to fetch workflow status from GitHub's REST API. Uses your GitHub token for authentication and retrieves information about the most recent workflow run on your specified branch. + +### config_process_filter_by_name.conf + +Uses a Starlark script to filter GitHub workflow runs by name, keeping only workflows that match the `GITHUB_WORKFLOW` environment variable. This early-stage filtering ensures only relevant workflows are processed. Uses Starlark instead of Telegraf's built-in `processors.filter` because the built-in filter doesn't support environment variable substitution. + +### config_process_select_latest.conf + +Uses a Starlark script to select only the most recent workflow run when multiple workflow runs are returned by the GitHub API. The processor tracks the highest `run_number` seen and drops older workflow runs, ensuring the strobe always reflects the current workflow status. This needs to be loaded (enabled) after `config_process_filter_by_name.conf`. + +### config_process_status_to_color.conf + +Contains a Starlark script that converts GitHub's workflow status (`success`, `failure`, or `null` for running) into simple color names (`green`, `red`, or `yellow`) that the strobe can understand. This needs to be loaded (enabled) after `config_process_select_latest.conf`. + +### config_output_strobe.conf + +Executes the `trigger_strobe.sh` script whenever the workflow status changes. This script uses VAPIX commands to actually change the strobe light color on your Axis device. + +### test_files/config_output_stdout.conf + +When enabled, this outputs all pipeline data to the FixedIT Data Agent logs. Useful for troubleshooting if the strobe isn't responding as expected. This is also very useful for on-host testing since it makes it easy to validate the data at each stage of the pipeline. + +### test_files/config_input_file.conf + +This file can be used together with the `sample.json` file to test the pipeline without having to wait for a GitHub Actions job to complete. This is intended primarily for on-host testing, but works well for testing in the FixedIT Data Agent too. Upload this file instead of the `config_input_github.conf` file, then upload the `sample.json` file as a helper file. You also need to set the `SAMPLE_FILE` environment variable to `sample.json` in the FixedIT Data Agent's `Extra env` parameter. + +## Local Testing on Host + +If using Linux (or Windows Subsystem for Linux), you can test the workflow on your developer machine before deploying to your Axis device. This requires [Telegraf](https://www.influxdata.com/time-series-platform/telegraf/) to be installed locally. + +### Prerequisites + +- Install Telegraf on your developer machine +- Have `jq` installed for JSON processing (used by `trigger_strobe.sh`) +- Clone this repository and navigate to the project directory + +### Host Testing Limitations + +**What works on host:** + +- GitHub API data fetching and parsing +- Data transformation logic +- Configuration validation + +**What requires an actual Axis device:** + +- Strobe light control (VAPIX API calls) + +The strobe control functionality requires actual Axis device hardware and VAPIX API access, which cannot be simulated on a host machine. + +### Test GitHub API data parsing using mock data + +Test the API parsing pipeline using sample GitHub API data without making actual API calls. + +First, set up the environment variables: + +```bash +# Set up environment +export HELPER_FILES_DIR=$(pwd) +export SAMPLE_FILE=test_files/sample.json +export GITHUB_WORKFLOW="Validate JSON" +export TELEGRAF_DEBUG=true +``` + +Then run the following command: + +```bash +# Test with mock data (no GitHub API calls needed) +telegraf --config config_agent.conf \ + --config test_files/config_input_file.conf \ + --config config_process_filter_by_name.conf \ + --config config_process_select_latest.conf \ + --config config_process_status_to_color.conf \ + --config test_files/config_output_stdout.conf \ + --once +``` + +**Expected output:** You'll see Telegraf load the configs and then output a single JSON line like: + +```json +{ + "fields": { "color": "green", "run_number": 20 }, + "name": "workflow_color", + "tags": {}, + "timestamp": 1754301969 +} +``` + +This shows the pipeline successfully identified the latest workflow run and converted the sample GitHub "success" status into "green" color output. + +### Test latest workflow selection logic + +Test only the latest workflow selection processor with intentionally out-of-order workflow data to verify it correctly selects the most recent run at each time: + +```bash +# Set up environment for latest selection test +export HELPER_FILES_DIR=$(pwd) +export SAMPLE_FILE=test_files/sample_select_latest.json +export GITHUB_WORKFLOW="Validate JSON" +export TELEGRAF_DEBUG=true + +# Test only the latest selection logic (no color transformation) +telegraf --config config_agent.conf \ + --config test_files/config_input_file.conf \ + --config config_process_filter_by_name.conf \ + --config config_process_select_latest.conf \ + --config test_files/config_output_stdout.conf \ + --once +``` + +**Expected behavior:** The test data contains workflow runs with `run_number` values 16, 20, 20, 19 (intentionally out of order). The processor should: + +- Let through run 16 (first metric) +- Let through run 20 (higher number) +- Let through run 20 again (same, still the highest number) +- Drop run 19 (lower than current state) + +**Expected output:** You'll see 3 JSON metrics (runs 16, 20, 20) with the original GitHub workflow data structure, demonstrating the latest selection logic works correctly. + +### Test GitHub API integration + +Test with live GitHub API data (requires valid credentials): + +```bash +# Test with real GitHub API (requires valid credentials) +export GITHUB_TOKEN=your_github_token +export GITHUB_USER=your_github_username +export GITHUB_REPO=your_repo_name +export GITHUB_BRANCH=main +export GITHUB_WORKFLOW="Your Workflow Name" + +export TELEGRAF_DEBUG=true +export HELPER_FILES_DIR=$(pwd) + +telegraf --config config_agent.conf \ + --config config_input_github.conf \ + --config config_process_filter_by_name.conf \ + --config config_process_select_latest.conf \ + --config config_process_status_to_color.conf \ + --config test_files/config_output_stdout.conf \ + --once +``` + +**Expected output:** + +- With valid credentials, you'll see the JSON result like the mock test above. +- With invalid/expired credentials, you'll see: + +```text +Error in plugin: received status code 401 (Unauthorized) +``` + +If you get a 401 error, check that your GitHub token is valid and has the required permissions. + +### Test strobe control using mock data + +Test the data transformation and strobe control using sample data (no GitHub API calls needed): + +```bash +# Set up your Axis device credentials +export VAPIX_USERNAME=your_vapix_user +export VAPIX_PASSWORD=your_vapix_password +export VAPIX_IP=your.axis.device.ip + +# Set helper files directory +export HELPER_FILES_DIR=$(pwd) +export TELEGRAF_DEBUG=true +export SAMPLE_FILE=test_files/sample.json + +# Run with mock data but real strobe control +telegraf --config config_agent.conf \ + --config test_files/config_input_file.conf \ + --config config_process_filter_by_name.conf \ + --config config_process_select_latest.conf \ + --config config_process_status_to_color.conf \ + --config config_output_strobe.conf \ + --config test_files/config_output_stdout.conf \ + --once +``` + +This will process the sample GitHub API response and **actually control your strobe light** based on the sample data (which shows a "success" status, so it should turn the strobe green). + +**Expected output:** + +- With valid VAPIX credentials, you'll see the strobe light change to green. +- With invalid credentials, you'll see: + +```text +Error: curl: (22) The requested URL returned error: 401 +Error: Failed to start profile 'green' +``` + +If you get a 401 error, check that your VAPIX username and password are correct and that the user has at least operator privileges. + +### Test complete workflow integration + +Test the whole workflow from getting the job status from the GitHub API to controlling the strobe light. This will test the same functionality that will run in the strobe device. + +```bash +# Set up your GitHub credentials +export GITHUB_TOKEN=your_github_token +export GITHUB_USER=your_github_username +export GITHUB_REPO=your_repo_name +export GITHUB_BRANCH=main +export GITHUB_WORKFLOW="Your Workflow Name" + +# Set up your Axis device credentials +export VAPIX_USERNAME=your_vapix_user +export VAPIX_PASSWORD=your_vapix_password +export VAPIX_IP=your.axis.device.ip + +# Set helper files directory +export HELPER_FILES_DIR=$(pwd) +export TELEGRAF_DEBUG=true + +# Full pipeline including strobe control +telegraf --config config_agent.conf \ + --config config_input_github.conf \ + --config config_process_filter_by_name.conf \ + --config config_process_select_latest.conf \ + --config config_process_status_to_color.conf \ + --config config_output_strobe.conf \ + --config test_files/config_output_stdout.conf \ + --once +``` + +You should now see the strobe change color based on the status of the last GitHub workflow. The option `--once` will make it run once and then exit, you can remove this to run it continuously. + +## License + +This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details. diff --git a/project-strobe-color-from-github-workflow/config_agent.conf b/project-strobe-color-from-github-workflow/config_agent.conf new file mode 100644 index 0000000..9dcc07d --- /dev/null +++ b/project-strobe-color-from-github-workflow/config_agent.conf @@ -0,0 +1,36 @@ +# Telegraf Agent Core Configuration for GitHub Workflow Monitoring +# +# This configuration defines the global settings for the Telegraf agent that +# monitors GitHub workflow status and controls Axis strobe light colors. +# +# Required Environment Variables: +# - TELEGRAF_DEBUG: Set to "true" for verbose logging, "false" for normal operation +# +# Key Features: +# - Configures polling interval for GitHub API checks +# - Implements jitter to prevent API rate limiting across multiple devices +# - Controls debug output for troubleshooting +# - Optimized for real-time workflow status monitoring + +[agent] + +# Enable debug mode for verbose logging and troubleshooting +# Set to true when diagnosing pipeline issues or during initial setup +# Set to false in production to reduce log volume +debug = ${TELEGRAF_DEBUG} + +# Polling interval for GitHub API checks +# This determines how quickly the strobe responds to workflow changes +# The strobe profile duration should be at least as long as this interval +# For production usage, a much longer interval is recommended to avoid +# hitting GitHub's rate limit. +interval = "5s" + +# Random delay added to each collection cycle to prevent API rate limiting +# When multiple devices monitor the same repo, this spreads out API calls +# Prevents synchronized requests that could trigger GitHub's rate limiting +# Range: 0 to collection_jitter seconds added randomly to each interval +collection_jitter = "1s" + +# Set the flush interval to 1s to make sure the data is output immediately. +flush_interval = "1s" \ No newline at end of file diff --git a/project-strobe-color-from-github-workflow/config_input_github.conf b/project-strobe-color-from-github-workflow/config_input_github.conf new file mode 100644 index 0000000..f91f0c5 --- /dev/null +++ b/project-strobe-color-from-github-workflow/config_input_github.conf @@ -0,0 +1,64 @@ +# GitHub API Input Configuration for Workflow Status Monitoring +# +# This configuration uses Telegraf's HTTP input plugin to fetch the latest +# workflow run status from GitHub's REST API for continuous monitoring. +# +# Key Features: +# - Fetches recent workflow runs from ALL workflows in the repository +# - Alternative approach: Could filter by workflow ID in URL, but workflow IDs are harder to find than names +# - Therefore: Get all workflows here, filter by user-friendly workflow name in processor +# - Uses per_page parameter to ensure target workflow is included in results +# - Extracts both status (running state) and conclusion (final result) +# - Uses authenticated requests for access to private repositories +# - Processes JSON response and converts to Telegraf metrics +# +# Environment Variables Required: +# - GITHUB_TOKEN: Personal access token with 'workflow' scope +# - GITHUB_USER: GitHub username or organization +# - GITHUB_REPO: Repository name +# - GITHUB_BRANCH: Branch to monitor (typically 'main' or 'master') + +[[inputs.http]] + # GitHub Actions API endpoint for workflow runs + # Fetches recent workflow runs across ALL workflows for the specified branch + # Note: Could use /workflows/{workflow_id}/runs for direct filtering, but workflow IDs are hard to find + # Instead: Get all recent runs here, filter by user-friendly workflow name in processor + # per_page=10 works for repos with few workflows - increase for busy repos + # IMPORTANT: Must be >= number of workflows to ensure target workflow is found + urls = ["https://api.github.com/repos/${GITHUB_USER}/${GITHUB_REPO}/actions/runs?branch=${GITHUB_BRANCH}&per_page=10"] + + # HTTP method for the API request + method = "GET" + + # Parse response as JSON format + data_format = "json" + + # Override metric name for clarity in downstream processing + # This name is used by the processor to identify GitHub workflow metrics + name_override = "github_workflow" + + # JSON query to extract the workflow_runs array from the API response + # The API returns: {"total_count": N, "workflow_runs": [...]} + # This extracts just the workflow_runs array for processing + json_query = "workflow_runs" + + # Fields to extract as tags (for filtering/grouping) + # Tags are indexed and can be used for filtering in processors + # status: current workflow state (queued, in_progress, completed) + # conclusion: final result (success, failure, null if running) + # name: workflow name + tag_keys = ["status", "conclusion", "name"] + + # Fields to preserve as string values (not just numeric) + # By default, Telegraf only preserves numeric fields from JSON + json_string_fields = ["status", "conclusion", "name", "display_title", "id", "run_number"] + + # HTTP headers for API authentication and content negotiation + [inputs.http.headers] + # GitHub Personal Access Token for authenticated requests + # Token should have 'workflow' scope for reading workflow runs + Authorization = "Bearer ${GITHUB_TOKEN}" + + # Accept header specifying JSON response format + # GitHub API supports multiple formats; JSON is most suitable for parsing + Accept = "application/json" diff --git a/project-strobe-color-from-github-workflow/config_output_strobe.conf b/project-strobe-color-from-github-workflow/config_output_strobe.conf new file mode 100644 index 0000000..2109c42 --- /dev/null +++ b/project-strobe-color-from-github-workflow/config_output_strobe.conf @@ -0,0 +1,61 @@ +# Axis Strobe Light Controller Output Configuration +# +# This configuration uses Telegraf's exec output plugin to trigger the +# trigger_strobe.sh script whenever workflow status changes are detected. +# +# Key Features: +# - Executes shell script for each color change event +# - Immediate execution with no batching for real-time response +# - JSON data format for structured communication with script +# - Timeout protection for API call sequences +# - Exec plugin runs external commands with metric data as stdin +# - Script receives JSON with color information: {"fields":{"color":"green"}} +# +# Environment Variables Required: +# - HELPER_FILES_DIR: Directory containing trigger_strobe.sh script (set automatically by the FixedIt Data Agent) +# - VAPIX_USERNAME: Axis device username +# - VAPIX_PASSWORD: Axis device password +# - VAPIX_IP: IP address of the Axis strobe device, should be 127.0.0.1 when running in the FixedIT Data Agent on an Axis Strobe. + +[[outputs.exec]] + # Filter to process only workflow color metrics + # This ensures the script only runs when color changes occur + # and ignores other metrics that might be in the pipeline + namepass = ["workflow_color"] + + # Shell script command to execute for each color change + # The script receives JSON data via stdin and controls the strobe + # HELPER_FILES_DIR should point to the directory containing the script, + # which is set automatically by the FixedIt Data Agent to the directory where + # helper files are uploaded. + command = ["${HELPER_FILES_DIR}/trigger_strobe.sh"] + + # Data format sent to the script via stdin + # JSON format provides structured data that the script can parse easily + # Format: {"fields":{"color":"green"},"name":"workflow_color",...} + data_format = "json" + + # Disable batch format to send individual metrics to the script. + # When false: sends single metric JSON object per execution. + # When true: would send array of metrics (not needed for this use case). + use_batch_format = false + + # Immediate execution settings for real-time response. + # These settings minimize delay between GitHub status change and strobe activation. + + # Process one metric at a time for immediate response. + # This ensures each color change triggers script execution immediately. + # Higher values would batch multiple changes (undesirable for visual feedback). + metric_batch_size = 1 + + # Minimal buffer size. 2*metric_batch_size is the minimum value allowed in the + # configuration according to the documentation. + metric_buffer_limit = 2 + + # Script execution timeout to prevent hanging. + # The script makes multiple VAPIX API calls (start/stop profiles). + # 10 seconds allows sufficient time for network round trips and error handling. + # Increase if experiencing timeout issues on slow networks. + # Setting this makes the application more robust since it can recover from + # a hanging script. + timeout = "10s" \ No newline at end of file diff --git a/project-strobe-color-from-github-workflow/config_process_filter_by_name.conf b/project-strobe-color-from-github-workflow/config_process_filter_by_name.conf new file mode 100644 index 0000000..e7f0aed --- /dev/null +++ b/project-strobe-color-from-github-workflow/config_process_filter_by_name.conf @@ -0,0 +1,63 @@ +# Workflow Name Filter Processor Configuration +# +# This configuration uses Telegraf's Starlark processor to filter GitHub +# workflow runs by name, keeping only the workflows that match the target +# workflow specified in the GITHUB_WORKFLOW environment variable. +# +# Required Environment Variables: +# - GITHUB_WORKFLOW: Workflow name to filter by (display name from GitHub Actions) +# +# Key Features: +# - Processes only github_workflow metrics from the input stage +# - Filters by workflow name using GITHUB_WORKFLOW environment variable +# - Single responsibility: workflow name filtering only +# - Early stage filtering reduces processing load on downstream processors +# - Uses Starlark instead of processors.filter because processors.filter doesn't support environment variable substitution (${GITHUB_WORKFLOW}) +# +# Example: +# +# Input (multiple workflows): +# {"fields":{"name":"Deploy Check","conclusion":"success","run_number":1},"name":"github_workflow","tags":{},"timestamp":1754667012} +# {"fields":{"name":"Validate JSON","conclusion":"failure","run_number":20},"name":"github_workflow","tags":{},"timestamp":1754667012} +# {"fields":{"name":"Security Check","conclusion":"success","run_number":5},"name":"github_workflow","tags":{},"timestamp":1754667012} +# +# With GITHUB_WORKFLOW="Validate JSON": +# Result: Only the "Validate JSON" workflow is passed through to the next processor + +[[processors.starlark]] + # Filter to process only GitHub workflow metrics from input + namepass = ["github_workflow"] + + # Starlark script for workflow name filtering + source = ''' +load("logging.star", "log") + +# Helper function to copy a metric with a new name +def copy_to_new_name(original_metric, new_name): + """Create a new metric with the same data but different name""" + new = Metric(new_name) + # Copy fields one by one (deepcopy doesn't work on Fields objects) + for field_name, field_value in original_metric.fields.items(): + new.fields[field_name] = field_value + # Copy tags one by one (deepcopy doesn't work on Tags objects) + for tag_name, tag_value in original_metric.tags.items(): + new.tags[tag_name] = tag_value + new.time = original_metric.time + return new + +def apply(metric): + # Get the workflow name from the metric tags (name field is now available as a tag) + workflow_name = metric.tags.get("name", "") + + # Get the target workflow from environment variable + target_workflow = "${GITHUB_WORKFLOW}" + + # Only pass through workflows that match the target + if workflow_name == target_workflow: + log.debug("Workflow name matches target '{}', passing through".format(target_workflow)) + return copy_to_new_name(metric, "github_workflow_filtered") + + # Drop non-matching workflows + log.debug("Skipping workflow '{}' (target is '{}')".format(workflow_name, target_workflow)) + return None # Drop the metric (don't pass it to the next stage) +''' \ No newline at end of file diff --git a/project-strobe-color-from-github-workflow/config_process_select_latest.conf b/project-strobe-color-from-github-workflow/config_process_select_latest.conf new file mode 100644 index 0000000..bef7e73 --- /dev/null +++ b/project-strobe-color-from-github-workflow/config_process_select_latest.conf @@ -0,0 +1,75 @@ +# Latest Workflow Selector Processor Configuration +# +# This configuration uses Telegraf's Starlark processor to select only the +# most recent workflow run from multiple github_workflow metrics, ensuring +# the strobe reflects the current workflow status. +# +# Key Features: +# - Processes only github_workflow metrics from the previous processor +# - Uses run_number to identify the most recent workflow run +# - Maintains state to track the highest run_number seen +# - Allows repeated metrics (strobe updates on each sync even if no new runs) +# - Assumes all metrics are for the same workflow (filtering by workflow name +# is handled by config_process_filter_by_name.conf upstream in the pipeline) +# +# Usage: +# This processor should be placed AFTER config_process_filter_by_name.conf +# in the processing pipeline to select the latest github_workflow metric +# from the multiple metrics that the filter processor passes through. +# +# Example processing sequence: +# +# Input 1: {"fields":{"run_number":16},"name":"github_workflow_filtered","tags":{"conclusion":"failure"}} +# Effect: Let through (first metric, state updated to run_number=16) +# +# Input 2: {"fields":{"run_number":20},"name":"github_workflow_filtered","tags":{"conclusion":"success"}} +# Effect: Let through (higher run_number, state updated to run_number=20) +# +# Input 3: {"fields":{"run_number":20},"name":"github_workflow_filtered","tags":{"conclusion":"success"}} +# Effect: Let through (same run_number, i.e. most up-to-date) +# +# Input 4: {"fields":{"run_number":19},"name":"github_workflow_filtered","tags":{"conclusion":"success"}} +# Effect: Drop (lower run_number than current state of 20) +# +# Result: Metrics with run_number=16, 20, and 20 are passed through; run_number=19 is dropped + +[[processors.starlark]] + # Filter to process only github_workflow_filtered metrics + # This ensures we only process the metrics from the filter processor + namepass = ["github_workflow_filtered"] + + # Starlark script for latest workflow selection + # Uses state variable to track the highest run_number seen + source = ''' +# Helper function to copy a metric with a new name +def copy_to_new_name(original_metric, new_name): + """Create a new metric with the same data but different name""" + new = Metric(new_name) + # Copy fields one by one (deepcopy doesn't work on Fields objects) + for field_name, field_value in original_metric.fields.items(): + new.fields[field_name] = field_value + # Copy tags one by one (deepcopy doesn't work on Tags objects) + for tag_name, tag_value in original_metric.tags.items(): + new.tags[tag_name] = tag_value + new.time = original_metric.time + return new + +# State to track the latest run_number we've seen +state = { + "latest_run_number": 0 +} + +def apply(metric): + # Get the run_number from the metric fields + # This comes from the original GitHub API data filtered by the name filter processor + run_number = metric.fields.get("run_number", 0) + + # Drop older workflow runs + if run_number < state["latest_run_number"]: + return None # Drop the metric (don't pass it to the next stage) + + # This is the newest run we've seen - update state and let it through + state["latest_run_number"] = run_number + + return copy_to_new_name(metric, "github_workflow_latest") +''' \ No newline at end of file diff --git a/project-strobe-color-from-github-workflow/config_process_status_to_color.conf b/project-strobe-color-from-github-workflow/config_process_status_to_color.conf new file mode 100644 index 0000000..a2a18d0 --- /dev/null +++ b/project-strobe-color-from-github-workflow/config_process_status_to_color.conf @@ -0,0 +1,63 @@ +# GitHub Workflow Status to Strobe Color Processor Configuration +# +# This configuration uses Telegraf's Starlark processor to transform GitHub +# workflow status data into simple profile names for the Axis strobe light. +# +# Key Features: +# - Processes github_workflow metrics (already filtered by workflow name) +# - Converts GitHub workflow conclusions to strobe-compatible profile names +# - Creates simplified metrics containing only profile name information +# - Uses Starlark scripting language for flexible data transformation +# +# Color Mapping Logic: +# - success → green (workflow passed successfully) +# - failure → red (workflow failed with errors) +# - null/undefined → yellow (workflow is still running or queued) + +[[processors.starlark]] + # Filter to process only GitHub workflow latest metrics + # This ensures the processor only runs on metrics from the select latest processor + # and ignores other metrics that might be in the pipeline + namepass = ["github_workflow_latest"] + + # Starlark script for data transformation + # Starlark is a Python-like language optimized for configuration + # The apply() function is called for each matching metric + source = ''' +def apply(metric): + # Extract the workflow conclusion from metric tags + # The conclusion field indicates the final workflow result + # It will be null/undefined for workflows that are still running + conclusion = metric.tags.get("conclusion") + + # Map GitHub workflow conclusions to strobe light colors + # Default to yellow for running/unknown states (safe fallback) + color = "yellow" # Default: workflow is running, queued, or unknown state + + # Green: workflow completed successfully + # This indicates all tests passed, build succeeded, deployment worked + if conclusion == "success": + color = "green" + + # Red: workflow failed with errors + # This indicates test failures, build errors, or deployment issues + elif conclusion == "failure": + color = "red" + + # Note: Other conclusion values (neutral, cancelled, skipped, timed_out, + # action_required) default to yellow for visual indication + + # Create a new simplified metric containing only the color + # This reduces data size and focuses on actionable information + # The output metric name 'workflow_color' is used by the strobe controller + new = Metric("workflow_color") + new.fields["color"] = color + + # Add the original run_number as a field so we can use it for deduplication + # GitHub API returns run_number which increases for each workflow run + run_number = metric.fields.get("run_number", 0) + new.fields["run_number"] = run_number + + # Return the new metric to pass to the next stage in the pipeline + return new +''' diff --git a/project-strobe-color-from-github-workflow/test_files/config_input_file.conf b/project-strobe-color-from-github-workflow/test_files/config_input_file.conf new file mode 100644 index 0000000..636a7e4 --- /dev/null +++ b/project-strobe-color-from-github-workflow/test_files/config_input_file.conf @@ -0,0 +1,47 @@ +# Mock GitHub API Input Configuration for Testing +# +# This configuration uses Telegraf's file input plugin to simulate GitHub API +# responses for testing the complete pipeline without making actual API calls. +# +# Required Environment Variables: +# - HELPER_FILES_DIR: Directory containing sample JSON files +# - SAMPLE_FILE: JSON filename to use for testing (e.g., "sample.json", +# "test_files/sample_select_latest.json"), relative to the HELPER_FILES_DIR +# +# Key Features: +# - Reads sample GitHub API response from local JSON file +# - Provides identical data structure to real GitHub API +# - Enables testing of processor and output logic independently +# - Allows development and testing without GitHub token or network access +# - Perfect for validating strobe control logic with known data + +[[inputs.file]] + # Path to sample GitHub API response file + # SAMPLE_FILE should specify the JSON filename (relative to HELPER_FILES_DIR) + # Examples: sample.json, sample_select_latest.json + files = ["${HELPER_FILES_DIR}/${SAMPLE_FILE}"] + + # Parse file content as JSON format + # Same parsing logic as used for real GitHub API responses + data_format = "json" + + # Override metric name to match real GitHub input + # This ensures processor configuration works identically with test data + # Must match namepass filter in processor configuration + name_override = "github_workflow" + + # Extract workflow_runs array from JSON response + # GitHub API returns: {"total_count": N, "workflow_runs": [...]} + # This query extracts just the workflow_runs array for processing + json_query = "workflow_runs" + + # Fields to extract as tags for filtering and processing + # Same tag extraction as real API to ensure consistent behavior + # status: workflow execution state, conclusion: final result + # name: workflow name (for filtering by GITHUB_WORKFLOW) + tag_keys = ["status", "conclusion", "name"] + + # String fields to preserve (not just numeric values) + # Maintains same field structure as real API input configuration + # Useful for debugging and logging during testing + json_string_fields = ["status", "conclusion", "name", "display_title", "id", "run_number"] \ No newline at end of file diff --git a/project-strobe-color-from-github-workflow/test_files/config_output_stdout.conf b/project-strobe-color-from-github-workflow/test_files/config_output_stdout.conf new file mode 100644 index 0000000..fbfb3af --- /dev/null +++ b/project-strobe-color-from-github-workflow/test_files/config_output_stdout.conf @@ -0,0 +1,15 @@ +# Debug Output Configuration for Console Logging +# +# This configuration uses Telegraf's file output plugin to send all pipeline +# data to stdout for debugging and troubleshooting purposes. + +[[outputs.file]] + # Output target: stdout for console display. + # 'stdout' is a special file target that writes to standard output. + # This makes the data visible in Telegraf logs and terminal output. + files = ["stdout"] + + # Data serialization format for human-readable output. + # JSON format preserves all metric information in structured form. + # Each metric becomes one line of JSON output for easy parsing/reading. + data_format = "json" \ No newline at end of file diff --git a/project-strobe-color-from-github-workflow/test_files/sample.json b/project-strobe-color-from-github-workflow/test_files/sample.json new file mode 100644 index 0000000..39fc4a9 --- /dev/null +++ b/project-strobe-color-from-github-workflow/test_files/sample.json @@ -0,0 +1,2245 @@ +{ + "total_count": 25, + "workflow_runs": [ + { + "id": 16833205167, + "name": "Deploy Check", + "node_id": "WFR_kwLOOvI4888AAAAD61ZTrw", + "head_branch": "main", + "head_sha": "8cbc137fb9c702e58061cd1d49e5c985fdc6a8b6", + "path": ".github/workflows/deploy-check.yml", + "display_title": "test", + "run_number": 1, + "event": "push", + "status": "completed", + "conclusion": "success", + "workflow_id": 180043263, + "check_suite_id": 43172930755, + "check_suite_node_id": "CS_kwDOOvI4888AAAAKDU6kww", + "url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205167", + "html_url": "https://github.com/testuser/my-test-repo/actions/runs/16833205167", + "pull_requests": [], + "created_at": "2025-08-08T14:40:42Z", + "updated_at": "2025-08-08T14:40:49Z", + "actor": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "run_attempt": 1, + "referenced_workflows": [], + "run_started_at": "2025-08-08T14:40:42Z", + "triggering_actor": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "jobs_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205167/jobs", + "logs_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205167/logs", + "check_suite_url": "https://api.github.com/repos/testuser/my-test-repo/check-suites/43172930755", + "artifacts_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205167/artifacts", + "cancel_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205167/cancel", + "rerun_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205167/rerun", + "previous_attempt_url": null, + "workflow_url": "https://api.github.com/repos/testuser/my-test-repo/actions/workflows/180043263", + "head_commit": { + "id": "8cbc137fb9c702e58061cd1d49e5c985fdc6a8b6", + "tree_id": "b2c403b629cf0661ee2221cc2e3975cd5cdbd345", + "message": "test", + "timestamp": "2025-08-08T14:40:36Z", + "author": { + "name": "Test User", + "email": "user@example.com" + }, + "committer": { + "name": "Test User", + "email": "user@example.com" + } + }, + "repository": { + "id": 988952819, + "node_id": "R_kgDOOvI48w", + "name": "my-test-repo", + "full_name": "testuser/my-test-repo", + "private": true, + "owner": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/testuser/my-test-repo", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/testuser/my-test-repo", + "forks_url": "https://api.github.com/repos/testuser/my-test-repo/forks", + "keys_url": "https://api.github.com/repos/testuser/my-test-repo/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/testuser/my-test-repo/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/testuser/my-test-repo/teams", + "hooks_url": "https://api.github.com/repos/testuser/my-test-repo/hooks", + "issue_events_url": "https://api.github.com/repos/testuser/my-test-repo/issues/events{/number}", + "events_url": "https://api.github.com/repos/testuser/my-test-repo/events", + "assignees_url": "https://api.github.com/repos/testuser/my-test-repo/assignees{/user}", + "branches_url": "https://api.github.com/repos/testuser/my-test-repo/branches{/branch}", + "tags_url": "https://api.github.com/repos/testuser/my-test-repo/tags", + "blobs_url": "https://api.github.com/repos/testuser/my-test-repo/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/testuser/my-test-repo/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/testuser/my-test-repo/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/testuser/my-test-repo/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/testuser/my-test-repo/statuses/{sha}", + "languages_url": "https://api.github.com/repos/testuser/my-test-repo/languages", + "stargazers_url": "https://api.github.com/repos/testuser/my-test-repo/stargazers", + "contributors_url": "https://api.github.com/repos/testuser/my-test-repo/contributors", + "subscribers_url": "https://api.github.com/repos/testuser/my-test-repo/subscribers", + "subscription_url": "https://api.github.com/repos/testuser/my-test-repo/subscription", + "commits_url": "https://api.github.com/repos/testuser/my-test-repo/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/testuser/my-test-repo/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/testuser/my-test-repo/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/testuser/my-test-repo/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/testuser/my-test-repo/contents/{+path}", + "compare_url": "https://api.github.com/repos/testuser/my-test-repo/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/testuser/my-test-repo/merges", + "archive_url": "https://api.github.com/repos/testuser/my-test-repo/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/testuser/my-test-repo/downloads", + "issues_url": "https://api.github.com/repos/testuser/my-test-repo/issues{/number}", + "pulls_url": "https://api.github.com/repos/testuser/my-test-repo/pulls{/number}", + "milestones_url": "https://api.github.com/repos/testuser/my-test-repo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/testuser/my-test-repo/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/testuser/my-test-repo/labels{/name}", + "releases_url": "https://api.github.com/repos/testuser/my-test-repo/releases{/id}", + "deployments_url": "https://api.github.com/repos/testuser/my-test-repo/deployments" + }, + "head_repository": { + "id": 988952819, + "node_id": "R_kgDOOvI48w", + "name": "my-test-repo", + "full_name": "testuser/my-test-repo", + "private": true, + "owner": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/testuser/my-test-repo", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/testuser/my-test-repo", + "forks_url": "https://api.github.com/repos/testuser/my-test-repo/forks", + "keys_url": "https://api.github.com/repos/testuser/my-test-repo/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/testuser/my-test-repo/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/testuser/my-test-repo/teams", + "hooks_url": "https://api.github.com/repos/testuser/my-test-repo/hooks", + "issue_events_url": "https://api.github.com/repos/testuser/my-test-repo/issues/events{/number}", + "events_url": "https://api.github.com/repos/testuser/my-test-repo/events", + "assignees_url": "https://api.github.com/repos/testuser/my-test-repo/assignees{/user}", + "branches_url": "https://api.github.com/repos/testuser/my-test-repo/branches{/branch}", + "tags_url": "https://api.github.com/repos/testuser/my-test-repo/tags", + "blobs_url": "https://api.github.com/repos/testuser/my-test-repo/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/testuser/my-test-repo/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/testuser/my-test-repo/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/testuser/my-test-repo/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/testuser/my-test-repo/statuses/{sha}", + "languages_url": "https://api.github.com/repos/testuser/my-test-repo/languages", + "stargazers_url": "https://api.github.com/repos/testuser/my-test-repo/stargazers", + "contributors_url": "https://api.github.com/repos/testuser/my-test-repo/contributors", + "subscribers_url": "https://api.github.com/repos/testuser/my-test-repo/subscribers", + "subscription_url": "https://api.github.com/repos/testuser/my-test-repo/subscription", + "commits_url": "https://api.github.com/repos/testuser/my-test-repo/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/testuser/my-test-repo/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/testuser/my-test-repo/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/testuser/my-test-repo/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/testuser/my-test-repo/contents/{+path}", + "compare_url": "https://api.github.com/repos/testuser/my-test-repo/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/testuser/my-test-repo/merges", + "archive_url": "https://api.github.com/repos/testuser/my-test-repo/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/testuser/my-test-repo/downloads", + "issues_url": "https://api.github.com/repos/testuser/my-test-repo/issues{/number}", + "pulls_url": "https://api.github.com/repos/testuser/my-test-repo/pulls{/number}", + "milestones_url": "https://api.github.com/repos/testuser/my-test-repo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/testuser/my-test-repo/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/testuser/my-test-repo/labels{/name}", + "releases_url": "https://api.github.com/repos/testuser/my-test-repo/releases{/id}", + "deployments_url": "https://api.github.com/repos/testuser/my-test-repo/deployments" + } + }, + { + "id": 16833205133, + "name": "Security Check", + "node_id": "WFR_kwLOOvI4888AAAAD61ZTjQ", + "head_branch": "main", + "head_sha": "8cbc137fb9c702e58061cd1d49e5c985fdc6a8b6", + "path": ".github/workflows/security-check.yml", + "display_title": "test", + "run_number": 1, + "event": "push", + "status": "completed", + "conclusion": "success", + "workflow_id": 180043266, + "check_suite_id": 43172930659, + "check_suite_node_id": "CS_kwDOOvI4888AAAAKDU6kYw", + "url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205133", + "html_url": "https://github.com/testuser/my-test-repo/actions/runs/16833205133", + "pull_requests": [], + "created_at": "2025-08-08T14:40:42Z", + "updated_at": "2025-08-08T14:40:51Z", + "actor": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "run_attempt": 1, + "referenced_workflows": [], + "run_started_at": "2025-08-08T14:40:42Z", + "triggering_actor": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "jobs_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205133/jobs", + "logs_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205133/logs", + "check_suite_url": "https://api.github.com/repos/testuser/my-test-repo/check-suites/43172930659", + "artifacts_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205133/artifacts", + "cancel_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205133/cancel", + "rerun_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205133/rerun", + "previous_attempt_url": null, + "workflow_url": "https://api.github.com/repos/testuser/my-test-repo/actions/workflows/180043266", + "head_commit": { + "id": "8cbc137fb9c702e58061cd1d49e5c985fdc6a8b6", + "tree_id": "b2c403b629cf0661ee2221cc2e3975cd5cdbd345", + "message": "test", + "timestamp": "2025-08-08T14:40:36Z", + "author": { + "name": "Test User", + "email": "user@example.com" + }, + "committer": { + "name": "Test User", + "email": "user@example.com" + } + }, + "repository": { + "id": 988952819, + "node_id": "R_kgDOOvI48w", + "name": "my-test-repo", + "full_name": "testuser/my-test-repo", + "private": true, + "owner": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/testuser/my-test-repo", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/testuser/my-test-repo", + "forks_url": "https://api.github.com/repos/testuser/my-test-repo/forks", + "keys_url": "https://api.github.com/repos/testuser/my-test-repo/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/testuser/my-test-repo/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/testuser/my-test-repo/teams", + "hooks_url": "https://api.github.com/repos/testuser/my-test-repo/hooks", + "issue_events_url": "https://api.github.com/repos/testuser/my-test-repo/issues/events{/number}", + "events_url": "https://api.github.com/repos/testuser/my-test-repo/events", + "assignees_url": "https://api.github.com/repos/testuser/my-test-repo/assignees{/user}", + "branches_url": "https://api.github.com/repos/testuser/my-test-repo/branches{/branch}", + "tags_url": "https://api.github.com/repos/testuser/my-test-repo/tags", + "blobs_url": "https://api.github.com/repos/testuser/my-test-repo/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/testuser/my-test-repo/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/testuser/my-test-repo/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/testuser/my-test-repo/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/testuser/my-test-repo/statuses/{sha}", + "languages_url": "https://api.github.com/repos/testuser/my-test-repo/languages", + "stargazers_url": "https://api.github.com/repos/testuser/my-test-repo/stargazers", + "contributors_url": "https://api.github.com/repos/testuser/my-test-repo/contributors", + "subscribers_url": "https://api.github.com/repos/testuser/my-test-repo/subscribers", + "subscription_url": "https://api.github.com/repos/testuser/my-test-repo/subscription", + "commits_url": "https://api.github.com/repos/testuser/my-test-repo/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/testuser/my-test-repo/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/testuser/my-test-repo/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/testuser/my-test-repo/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/testuser/my-test-repo/contents/{+path}", + "compare_url": "https://api.github.com/repos/testuser/my-test-repo/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/testuser/my-test-repo/merges", + "archive_url": "https://api.github.com/repos/testuser/my-test-repo/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/testuser/my-test-repo/downloads", + "issues_url": "https://api.github.com/repos/testuser/my-test-repo/issues{/number}", + "pulls_url": "https://api.github.com/repos/testuser/my-test-repo/pulls{/number}", + "milestones_url": "https://api.github.com/repos/testuser/my-test-repo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/testuser/my-test-repo/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/testuser/my-test-repo/labels{/name}", + "releases_url": "https://api.github.com/repos/testuser/my-test-repo/releases{/id}", + "deployments_url": "https://api.github.com/repos/testuser/my-test-repo/deployments" + }, + "head_repository": { + "id": 988952819, + "node_id": "R_kgDOOvI48w", + "name": "my-test-repo", + "full_name": "testuser/my-test-repo", + "private": true, + "owner": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/testuser/my-test-repo", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/testuser/my-test-repo", + "forks_url": "https://api.github.com/repos/testuser/my-test-repo/forks", + "keys_url": "https://api.github.com/repos/testuser/my-test-repo/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/testuser/my-test-repo/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/testuser/my-test-repo/teams", + "hooks_url": "https://api.github.com/repos/testuser/my-test-repo/hooks", + "issue_events_url": "https://api.github.com/repos/testuser/my-test-repo/issues/events{/number}", + "events_url": "https://api.github.com/repos/testuser/my-test-repo/events", + "assignees_url": "https://api.github.com/repos/testuser/my-test-repo/assignees{/user}", + "branches_url": "https://api.github.com/repos/testuser/my-test-repo/branches{/branch}", + "tags_url": "https://api.github.com/repos/testuser/my-test-repo/tags", + "blobs_url": "https://api.github.com/repos/testuser/my-test-repo/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/testuser/my-test-repo/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/testuser/my-test-repo/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/testuser/my-test-repo/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/testuser/my-test-repo/statuses/{sha}", + "languages_url": "https://api.github.com/repos/testuser/my-test-repo/languages", + "stargazers_url": "https://api.github.com/repos/testuser/my-test-repo/stargazers", + "contributors_url": "https://api.github.com/repos/testuser/my-test-repo/contributors", + "subscribers_url": "https://api.github.com/repos/testuser/my-test-repo/subscribers", + "subscription_url": "https://api.github.com/repos/testuser/my-test-repo/subscription", + "commits_url": "https://api.github.com/repos/testuser/my-test-repo/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/testuser/my-test-repo/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/testuser/my-test-repo/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/testuser/my-test-repo/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/testuser/my-test-repo/contents/{+path}", + "compare_url": "https://api.github.com/repos/testuser/my-test-repo/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/testuser/my-test-repo/merges", + "archive_url": "https://api.github.com/repos/testuser/my-test-repo/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/testuser/my-test-repo/downloads", + "issues_url": "https://api.github.com/repos/testuser/my-test-repo/issues{/number}", + "pulls_url": "https://api.github.com/repos/testuser/my-test-repo/pulls{/number}", + "milestones_url": "https://api.github.com/repos/testuser/my-test-repo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/testuser/my-test-repo/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/testuser/my-test-repo/labels{/name}", + "releases_url": "https://api.github.com/repos/testuser/my-test-repo/releases{/id}", + "deployments_url": "https://api.github.com/repos/testuser/my-test-repo/deployments" + } + }, + { + "id": 16833205130, + "name": "Build Test", + "node_id": "WFR_kwLOOvI4888AAAAD61ZTig", + "head_branch": "main", + "head_sha": "8cbc137fb9c702e58061cd1d49e5c985fdc6a8b6", + "path": ".github/workflows/build-test.yml", + "display_title": "test", + "run_number": 1, + "event": "push", + "status": "completed", + "conclusion": "success", + "workflow_id": 180043262, + "check_suite_id": 43172930653, + "check_suite_node_id": "CS_kwDOOvI4888AAAAKDU6kXQ", + "url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205130", + "html_url": "https://github.com/testuser/my-test-repo/actions/runs/16833205130", + "pull_requests": [], + "created_at": "2025-08-08T14:40:42Z", + "updated_at": "2025-08-08T14:40:51Z", + "actor": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "run_attempt": 1, + "referenced_workflows": [], + "run_started_at": "2025-08-08T14:40:42Z", + "triggering_actor": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "jobs_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205130/jobs", + "logs_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205130/logs", + "check_suite_url": "https://api.github.com/repos/testuser/my-test-repo/check-suites/43172930653", + "artifacts_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205130/artifacts", + "cancel_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205130/cancel", + "rerun_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205130/rerun", + "previous_attempt_url": null, + "workflow_url": "https://api.github.com/repos/testuser/my-test-repo/actions/workflows/180043262", + "head_commit": { + "id": "8cbc137fb9c702e58061cd1d49e5c985fdc6a8b6", + "tree_id": "b2c403b629cf0661ee2221cc2e3975cd5cdbd345", + "message": "test", + "timestamp": "2025-08-08T14:40:36Z", + "author": { + "name": "Test User", + "email": "user@example.com" + }, + "committer": { + "name": "Test User", + "email": "user@example.com" + } + }, + "repository": { + "id": 988952819, + "node_id": "R_kgDOOvI48w", + "name": "my-test-repo", + "full_name": "testuser/my-test-repo", + "private": true, + "owner": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/testuser/my-test-repo", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/testuser/my-test-repo", + "forks_url": "https://api.github.com/repos/testuser/my-test-repo/forks", + "keys_url": "https://api.github.com/repos/testuser/my-test-repo/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/testuser/my-test-repo/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/testuser/my-test-repo/teams", + "hooks_url": "https://api.github.com/repos/testuser/my-test-repo/hooks", + "issue_events_url": "https://api.github.com/repos/testuser/my-test-repo/issues/events{/number}", + "events_url": "https://api.github.com/repos/testuser/my-test-repo/events", + "assignees_url": "https://api.github.com/repos/testuser/my-test-repo/assignees{/user}", + "branches_url": "https://api.github.com/repos/testuser/my-test-repo/branches{/branch}", + "tags_url": "https://api.github.com/repos/testuser/my-test-repo/tags", + "blobs_url": "https://api.github.com/repos/testuser/my-test-repo/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/testuser/my-test-repo/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/testuser/my-test-repo/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/testuser/my-test-repo/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/testuser/my-test-repo/statuses/{sha}", + "languages_url": "https://api.github.com/repos/testuser/my-test-repo/languages", + "stargazers_url": "https://api.github.com/repos/testuser/my-test-repo/stargazers", + "contributors_url": "https://api.github.com/repos/testuser/my-test-repo/contributors", + "subscribers_url": "https://api.github.com/repos/testuser/my-test-repo/subscribers", + "subscription_url": "https://api.github.com/repos/testuser/my-test-repo/subscription", + "commits_url": "https://api.github.com/repos/testuser/my-test-repo/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/testuser/my-test-repo/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/testuser/my-test-repo/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/testuser/my-test-repo/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/testuser/my-test-repo/contents/{+path}", + "compare_url": "https://api.github.com/repos/testuser/my-test-repo/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/testuser/my-test-repo/merges", + "archive_url": "https://api.github.com/repos/testuser/my-test-repo/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/testuser/my-test-repo/downloads", + "issues_url": "https://api.github.com/repos/testuser/my-test-repo/issues{/number}", + "pulls_url": "https://api.github.com/repos/testuser/my-test-repo/pulls{/number}", + "milestones_url": "https://api.github.com/repos/testuser/my-test-repo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/testuser/my-test-repo/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/testuser/my-test-repo/labels{/name}", + "releases_url": "https://api.github.com/repos/testuser/my-test-repo/releases{/id}", + "deployments_url": "https://api.github.com/repos/testuser/my-test-repo/deployments" + }, + "head_repository": { + "id": 988952819, + "node_id": "R_kgDOOvI48w", + "name": "my-test-repo", + "full_name": "testuser/my-test-repo", + "private": true, + "owner": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/testuser/my-test-repo", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/testuser/my-test-repo", + "forks_url": "https://api.github.com/repos/testuser/my-test-repo/forks", + "keys_url": "https://api.github.com/repos/testuser/my-test-repo/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/testuser/my-test-repo/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/testuser/my-test-repo/teams", + "hooks_url": "https://api.github.com/repos/testuser/my-test-repo/hooks", + "issue_events_url": "https://api.github.com/repos/testuser/my-test-repo/issues/events{/number}", + "events_url": "https://api.github.com/repos/testuser/my-test-repo/events", + "assignees_url": "https://api.github.com/repos/testuser/my-test-repo/assignees{/user}", + "branches_url": "https://api.github.com/repos/testuser/my-test-repo/branches{/branch}", + "tags_url": "https://api.github.com/repos/testuser/my-test-repo/tags", + "blobs_url": "https://api.github.com/repos/testuser/my-test-repo/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/testuser/my-test-repo/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/testuser/my-test-repo/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/testuser/my-test-repo/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/testuser/my-test-repo/statuses/{sha}", + "languages_url": "https://api.github.com/repos/testuser/my-test-repo/languages", + "stargazers_url": "https://api.github.com/repos/testuser/my-test-repo/stargazers", + "contributors_url": "https://api.github.com/repos/testuser/my-test-repo/contributors", + "subscribers_url": "https://api.github.com/repos/testuser/my-test-repo/subscribers", + "subscription_url": "https://api.github.com/repos/testuser/my-test-repo/subscription", + "commits_url": "https://api.github.com/repos/testuser/my-test-repo/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/testuser/my-test-repo/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/testuser/my-test-repo/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/testuser/my-test-repo/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/testuser/my-test-repo/contents/{+path}", + "compare_url": "https://api.github.com/repos/testuser/my-test-repo/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/testuser/my-test-repo/merges", + "archive_url": "https://api.github.com/repos/testuser/my-test-repo/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/testuser/my-test-repo/downloads", + "issues_url": "https://api.github.com/repos/testuser/my-test-repo/issues{/number}", + "pulls_url": "https://api.github.com/repos/testuser/my-test-repo/pulls{/number}", + "milestones_url": "https://api.github.com/repos/testuser/my-test-repo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/testuser/my-test-repo/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/testuser/my-test-repo/labels{/name}", + "releases_url": "https://api.github.com/repos/testuser/my-test-repo/releases{/id}", + "deployments_url": "https://api.github.com/repos/testuser/my-test-repo/deployments" + } + }, + { + "id": 16833205128, + "name": "Integration Test", + "node_id": "WFR_kwLOOvI4888AAAAD61ZTiA", + "head_branch": "main", + "head_sha": "8cbc137fb9c702e58061cd1d49e5c985fdc6a8b6", + "path": ".github/workflows/integration-test.yml", + "display_title": "test", + "run_number": 1, + "event": "push", + "status": "completed", + "conclusion": "success", + "workflow_id": 180043264, + "check_suite_id": 43172930645, + "check_suite_node_id": "CS_kwDOOvI4888AAAAKDU6kVQ", + "url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205128", + "html_url": "https://github.com/testuser/my-test-repo/actions/runs/16833205128", + "pull_requests": [], + "created_at": "2025-08-08T14:40:42Z", + "updated_at": "2025-08-08T14:40:59Z", + "actor": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "run_attempt": 1, + "referenced_workflows": [], + "run_started_at": "2025-08-08T14:40:42Z", + "triggering_actor": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "jobs_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205128/jobs", + "logs_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205128/logs", + "check_suite_url": "https://api.github.com/repos/testuser/my-test-repo/check-suites/43172930645", + "artifacts_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205128/artifacts", + "cancel_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205128/cancel", + "rerun_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205128/rerun", + "previous_attempt_url": null, + "workflow_url": "https://api.github.com/repos/testuser/my-test-repo/actions/workflows/180043264", + "head_commit": { + "id": "8cbc137fb9c702e58061cd1d49e5c985fdc6a8b6", + "tree_id": "b2c403b629cf0661ee2221cc2e3975cd5cdbd345", + "message": "test", + "timestamp": "2025-08-08T14:40:36Z", + "author": { + "name": "Test User", + "email": "user@example.com" + }, + "committer": { + "name": "Test User", + "email": "user@example.com" + } + }, + "repository": { + "id": 988952819, + "node_id": "R_kgDOOvI48w", + "name": "my-test-repo", + "full_name": "testuser/my-test-repo", + "private": true, + "owner": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/testuser/my-test-repo", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/testuser/my-test-repo", + "forks_url": "https://api.github.com/repos/testuser/my-test-repo/forks", + "keys_url": "https://api.github.com/repos/testuser/my-test-repo/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/testuser/my-test-repo/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/testuser/my-test-repo/teams", + "hooks_url": "https://api.github.com/repos/testuser/my-test-repo/hooks", + "issue_events_url": "https://api.github.com/repos/testuser/my-test-repo/issues/events{/number}", + "events_url": "https://api.github.com/repos/testuser/my-test-repo/events", + "assignees_url": "https://api.github.com/repos/testuser/my-test-repo/assignees{/user}", + "branches_url": "https://api.github.com/repos/testuser/my-test-repo/branches{/branch}", + "tags_url": "https://api.github.com/repos/testuser/my-test-repo/tags", + "blobs_url": "https://api.github.com/repos/testuser/my-test-repo/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/testuser/my-test-repo/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/testuser/my-test-repo/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/testuser/my-test-repo/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/testuser/my-test-repo/statuses/{sha}", + "languages_url": "https://api.github.com/repos/testuser/my-test-repo/languages", + "stargazers_url": "https://api.github.com/repos/testuser/my-test-repo/stargazers", + "contributors_url": "https://api.github.com/repos/testuser/my-test-repo/contributors", + "subscribers_url": "https://api.github.com/repos/testuser/my-test-repo/subscribers", + "subscription_url": "https://api.github.com/repos/testuser/my-test-repo/subscription", + "commits_url": "https://api.github.com/repos/testuser/my-test-repo/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/testuser/my-test-repo/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/testuser/my-test-repo/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/testuser/my-test-repo/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/testuser/my-test-repo/contents/{+path}", + "compare_url": "https://api.github.com/repos/testuser/my-test-repo/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/testuser/my-test-repo/merges", + "archive_url": "https://api.github.com/repos/testuser/my-test-repo/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/testuser/my-test-repo/downloads", + "issues_url": "https://api.github.com/repos/testuser/my-test-repo/issues{/number}", + "pulls_url": "https://api.github.com/repos/testuser/my-test-repo/pulls{/number}", + "milestones_url": "https://api.github.com/repos/testuser/my-test-repo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/testuser/my-test-repo/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/testuser/my-test-repo/labels{/name}", + "releases_url": "https://api.github.com/repos/testuser/my-test-repo/releases{/id}", + "deployments_url": "https://api.github.com/repos/testuser/my-test-repo/deployments" + }, + "head_repository": { + "id": 988952819, + "node_id": "R_kgDOOvI48w", + "name": "my-test-repo", + "full_name": "testuser/my-test-repo", + "private": true, + "owner": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/testuser/my-test-repo", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/testuser/my-test-repo", + "forks_url": "https://api.github.com/repos/testuser/my-test-repo/forks", + "keys_url": "https://api.github.com/repos/testuser/my-test-repo/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/testuser/my-test-repo/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/testuser/my-test-repo/teams", + "hooks_url": "https://api.github.com/repos/testuser/my-test-repo/hooks", + "issue_events_url": "https://api.github.com/repos/testuser/my-test-repo/issues/events{/number}", + "events_url": "https://api.github.com/repos/testuser/my-test-repo/events", + "assignees_url": "https://api.github.com/repos/testuser/my-test-repo/assignees{/user}", + "branches_url": "https://api.github.com/repos/testuser/my-test-repo/branches{/branch}", + "tags_url": "https://api.github.com/repos/testuser/my-test-repo/tags", + "blobs_url": "https://api.github.com/repos/testuser/my-test-repo/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/testuser/my-test-repo/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/testuser/my-test-repo/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/testuser/my-test-repo/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/testuser/my-test-repo/statuses/{sha}", + "languages_url": "https://api.github.com/repos/testuser/my-test-repo/languages", + "stargazers_url": "https://api.github.com/repos/testuser/my-test-repo/stargazers", + "contributors_url": "https://api.github.com/repos/testuser/my-test-repo/contributors", + "subscribers_url": "https://api.github.com/repos/testuser/my-test-repo/subscribers", + "subscription_url": "https://api.github.com/repos/testuser/my-test-repo/subscription", + "commits_url": "https://api.github.com/repos/testuser/my-test-repo/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/testuser/my-test-repo/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/testuser/my-test-repo/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/testuser/my-test-repo/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/testuser/my-test-repo/contents/{+path}", + "compare_url": "https://api.github.com/repos/testuser/my-test-repo/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/testuser/my-test-repo/merges", + "archive_url": "https://api.github.com/repos/testuser/my-test-repo/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/testuser/my-test-repo/downloads", + "issues_url": "https://api.github.com/repos/testuser/my-test-repo/issues{/number}", + "pulls_url": "https://api.github.com/repos/testuser/my-test-repo/pulls{/number}", + "milestones_url": "https://api.github.com/repos/testuser/my-test-repo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/testuser/my-test-repo/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/testuser/my-test-repo/labels{/name}", + "releases_url": "https://api.github.com/repos/testuser/my-test-repo/releases{/id}", + "deployments_url": "https://api.github.com/repos/testuser/my-test-repo/deployments" + } + }, + { + "id": 16833205169, + "name": "Validate JSON", + "node_id": "WFR_kwLOOvI4888AAAAD61ZTsQ", + "head_branch": "main", + "head_sha": "8cbc137fb9c702e58061cd1d49e5c985fdc6a8b6", + "path": ".github/workflows/validate-json.yml", + "display_title": "test", + "run_number": 20, + "event": "push", + "status": "completed", + "conclusion": "success", + "workflow_id": 163811337, + "check_suite_id": 43172930753, + "check_suite_node_id": "CS_kwDOOvI4888AAAAKDU6kwQ", + "url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205169", + "html_url": "https://github.com/testuser/my-test-repo/actions/runs/16833205169", + "pull_requests": [], + "created_at": "2025-08-08T14:40:42Z", + "updated_at": "2025-08-08T14:41:03Z", + "actor": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "run_attempt": 1, + "referenced_workflows": [], + "run_started_at": "2025-08-08T14:40:42Z", + "triggering_actor": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "jobs_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205169/jobs", + "logs_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205169/logs", + "check_suite_url": "https://api.github.com/repos/testuser/my-test-repo/check-suites/43172930753", + "artifacts_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205169/artifacts", + "cancel_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205169/cancel", + "rerun_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205169/rerun", + "previous_attempt_url": null, + "workflow_url": "https://api.github.com/repos/testuser/my-test-repo/actions/workflows/163811337", + "head_commit": { + "id": "8cbc137fb9c702e58061cd1d49e5c985fdc6a8b6", + "tree_id": "b2c403b629cf0661ee2221cc2e3975cd5cdbd345", + "message": "test", + "timestamp": "2025-08-08T14:40:36Z", + "author": { + "name": "Test User", + "email": "user@example.com" + }, + "committer": { + "name": "Test User", + "email": "user@example.com" + } + }, + "repository": { + "id": 988952819, + "node_id": "R_kgDOOvI48w", + "name": "my-test-repo", + "full_name": "testuser/my-test-repo", + "private": true, + "owner": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/testuser/my-test-repo", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/testuser/my-test-repo", + "forks_url": "https://api.github.com/repos/testuser/my-test-repo/forks", + "keys_url": "https://api.github.com/repos/testuser/my-test-repo/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/testuser/my-test-repo/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/testuser/my-test-repo/teams", + "hooks_url": "https://api.github.com/repos/testuser/my-test-repo/hooks", + "issue_events_url": "https://api.github.com/repos/testuser/my-test-repo/issues/events{/number}", + "events_url": "https://api.github.com/repos/testuser/my-test-repo/events", + "assignees_url": "https://api.github.com/repos/testuser/my-test-repo/assignees{/user}", + "branches_url": "https://api.github.com/repos/testuser/my-test-repo/branches{/branch}", + "tags_url": "https://api.github.com/repos/testuser/my-test-repo/tags", + "blobs_url": "https://api.github.com/repos/testuser/my-test-repo/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/testuser/my-test-repo/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/testuser/my-test-repo/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/testuser/my-test-repo/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/testuser/my-test-repo/statuses/{sha}", + "languages_url": "https://api.github.com/repos/testuser/my-test-repo/languages", + "stargazers_url": "https://api.github.com/repos/testuser/my-test-repo/stargazers", + "contributors_url": "https://api.github.com/repos/testuser/my-test-repo/contributors", + "subscribers_url": "https://api.github.com/repos/testuser/my-test-repo/subscribers", + "subscription_url": "https://api.github.com/repos/testuser/my-test-repo/subscription", + "commits_url": "https://api.github.com/repos/testuser/my-test-repo/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/testuser/my-test-repo/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/testuser/my-test-repo/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/testuser/my-test-repo/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/testuser/my-test-repo/contents/{+path}", + "compare_url": "https://api.github.com/repos/testuser/my-test-repo/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/testuser/my-test-repo/merges", + "archive_url": "https://api.github.com/repos/testuser/my-test-repo/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/testuser/my-test-repo/downloads", + "issues_url": "https://api.github.com/repos/testuser/my-test-repo/issues{/number}", + "pulls_url": "https://api.github.com/repos/testuser/my-test-repo/pulls{/number}", + "milestones_url": "https://api.github.com/repos/testuser/my-test-repo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/testuser/my-test-repo/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/testuser/my-test-repo/labels{/name}", + "releases_url": "https://api.github.com/repos/testuser/my-test-repo/releases{/id}", + "deployments_url": "https://api.github.com/repos/testuser/my-test-repo/deployments" + }, + "head_repository": { + "id": 988952819, + "node_id": "R_kgDOOvI48w", + "name": "my-test-repo", + "full_name": "testuser/my-test-repo", + "private": true, + "owner": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/testuser/my-test-repo", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/testuser/my-test-repo", + "forks_url": "https://api.github.com/repos/testuser/my-test-repo/forks", + "keys_url": "https://api.github.com/repos/testuser/my-test-repo/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/testuser/my-test-repo/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/testuser/my-test-repo/teams", + "hooks_url": "https://api.github.com/repos/testuser/my-test-repo/hooks", + "issue_events_url": "https://api.github.com/repos/testuser/my-test-repo/issues/events{/number}", + "events_url": "https://api.github.com/repos/testuser/my-test-repo/events", + "assignees_url": "https://api.github.com/repos/testuser/my-test-repo/assignees{/user}", + "branches_url": "https://api.github.com/repos/testuser/my-test-repo/branches{/branch}", + "tags_url": "https://api.github.com/repos/testuser/my-test-repo/tags", + "blobs_url": "https://api.github.com/repos/testuser/my-test-repo/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/testuser/my-test-repo/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/testuser/my-test-repo/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/testuser/my-test-repo/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/testuser/my-test-repo/statuses/{sha}", + "languages_url": "https://api.github.com/repos/testuser/my-test-repo/languages", + "stargazers_url": "https://api.github.com/repos/testuser/my-test-repo/stargazers", + "contributors_url": "https://api.github.com/repos/testuser/my-test-repo/contributors", + "subscribers_url": "https://api.github.com/repos/testuser/my-test-repo/subscribers", + "subscription_url": "https://api.github.com/repos/testuser/my-test-repo/subscription", + "commits_url": "https://api.github.com/repos/testuser/my-test-repo/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/testuser/my-test-repo/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/testuser/my-test-repo/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/testuser/my-test-repo/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/testuser/my-test-repo/contents/{+path}", + "compare_url": "https://api.github.com/repos/testuser/my-test-repo/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/testuser/my-test-repo/merges", + "archive_url": "https://api.github.com/repos/testuser/my-test-repo/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/testuser/my-test-repo/downloads", + "issues_url": "https://api.github.com/repos/testuser/my-test-repo/issues{/number}", + "pulls_url": "https://api.github.com/repos/testuser/my-test-repo/pulls{/number}", + "milestones_url": "https://api.github.com/repos/testuser/my-test-repo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/testuser/my-test-repo/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/testuser/my-test-repo/labels{/name}", + "releases_url": "https://api.github.com/repos/testuser/my-test-repo/releases{/id}", + "deployments_url": "https://api.github.com/repos/testuser/my-test-repo/deployments" + } + }, + { + "id": 16833205143, + "name": "Quality Gate", + "node_id": "WFR_kwLOOvI4888AAAAD61ZTlw", + "head_branch": "main", + "head_sha": "8cbc137fb9c702e58061cd1d49e5c985fdc6a8b6", + "path": ".github/workflows/quality-gate.yml", + "display_title": "test", + "run_number": 1, + "event": "push", + "status": "completed", + "conclusion": "success", + "workflow_id": 180043265, + "check_suite_id": 43172930681, + "check_suite_node_id": "CS_kwDOOvI4888AAAAKDU6keQ", + "url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205143", + "html_url": "https://github.com/testuser/my-test-repo/actions/runs/16833205143", + "pull_requests": [], + "created_at": "2025-08-08T14:40:42Z", + "updated_at": "2025-08-08T14:40:53Z", + "actor": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "run_attempt": 1, + "referenced_workflows": [], + "run_started_at": "2025-08-08T14:40:42Z", + "triggering_actor": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "jobs_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205143/jobs", + "logs_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205143/logs", + "check_suite_url": "https://api.github.com/repos/testuser/my-test-repo/check-suites/43172930681", + "artifacts_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205143/artifacts", + "cancel_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205143/cancel", + "rerun_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/16833205143/rerun", + "previous_attempt_url": null, + "workflow_url": "https://api.github.com/repos/testuser/my-test-repo/actions/workflows/180043265", + "head_commit": { + "id": "8cbc137fb9c702e58061cd1d49e5c985fdc6a8b6", + "tree_id": "b2c403b629cf0661ee2221cc2e3975cd5cdbd345", + "message": "test", + "timestamp": "2025-08-08T14:40:36Z", + "author": { + "name": "Test User", + "email": "user@example.com" + }, + "committer": { + "name": "Test User", + "email": "user@example.com" + } + }, + "repository": { + "id": 988952819, + "node_id": "R_kgDOOvI48w", + "name": "my-test-repo", + "full_name": "testuser/my-test-repo", + "private": true, + "owner": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/testuser/my-test-repo", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/testuser/my-test-repo", + "forks_url": "https://api.github.com/repos/testuser/my-test-repo/forks", + "keys_url": "https://api.github.com/repos/testuser/my-test-repo/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/testuser/my-test-repo/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/testuser/my-test-repo/teams", + "hooks_url": "https://api.github.com/repos/testuser/my-test-repo/hooks", + "issue_events_url": "https://api.github.com/repos/testuser/my-test-repo/issues/events{/number}", + "events_url": "https://api.github.com/repos/testuser/my-test-repo/events", + "assignees_url": "https://api.github.com/repos/testuser/my-test-repo/assignees{/user}", + "branches_url": "https://api.github.com/repos/testuser/my-test-repo/branches{/branch}", + "tags_url": "https://api.github.com/repos/testuser/my-test-repo/tags", + "blobs_url": "https://api.github.com/repos/testuser/my-test-repo/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/testuser/my-test-repo/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/testuser/my-test-repo/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/testuser/my-test-repo/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/testuser/my-test-repo/statuses/{sha}", + "languages_url": "https://api.github.com/repos/testuser/my-test-repo/languages", + "stargazers_url": "https://api.github.com/repos/testuser/my-test-repo/stargazers", + "contributors_url": "https://api.github.com/repos/testuser/my-test-repo/contributors", + "subscribers_url": "https://api.github.com/repos/testuser/my-test-repo/subscribers", + "subscription_url": "https://api.github.com/repos/testuser/my-test-repo/subscription", + "commits_url": "https://api.github.com/repos/testuser/my-test-repo/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/testuser/my-test-repo/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/testuser/my-test-repo/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/testuser/my-test-repo/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/testuser/my-test-repo/contents/{+path}", + "compare_url": "https://api.github.com/repos/testuser/my-test-repo/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/testuser/my-test-repo/merges", + "archive_url": "https://api.github.com/repos/testuser/my-test-repo/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/testuser/my-test-repo/downloads", + "issues_url": "https://api.github.com/repos/testuser/my-test-repo/issues{/number}", + "pulls_url": "https://api.github.com/repos/testuser/my-test-repo/pulls{/number}", + "milestones_url": "https://api.github.com/repos/testuser/my-test-repo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/testuser/my-test-repo/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/testuser/my-test-repo/labels{/name}", + "releases_url": "https://api.github.com/repos/testuser/my-test-repo/releases{/id}", + "deployments_url": "https://api.github.com/repos/testuser/my-test-repo/deployments" + }, + "head_repository": { + "id": 988952819, + "node_id": "R_kgDOOvI48w", + "name": "my-test-repo", + "full_name": "testuser/my-test-repo", + "private": true, + "owner": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/testuser/my-test-repo", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/testuser/my-test-repo", + "forks_url": "https://api.github.com/repos/testuser/my-test-repo/forks", + "keys_url": "https://api.github.com/repos/testuser/my-test-repo/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/testuser/my-test-repo/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/testuser/my-test-repo/teams", + "hooks_url": "https://api.github.com/repos/testuser/my-test-repo/hooks", + "issue_events_url": "https://api.github.com/repos/testuser/my-test-repo/issues/events{/number}", + "events_url": "https://api.github.com/repos/testuser/my-test-repo/events", + "assignees_url": "https://api.github.com/repos/testuser/my-test-repo/assignees{/user}", + "branches_url": "https://api.github.com/repos/testuser/my-test-repo/branches{/branch}", + "tags_url": "https://api.github.com/repos/testuser/my-test-repo/tags", + "blobs_url": "https://api.github.com/repos/testuser/my-test-repo/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/testuser/my-test-repo/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/testuser/my-test-repo/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/testuser/my-test-repo/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/testuser/my-test-repo/statuses/{sha}", + "languages_url": "https://api.github.com/repos/testuser/my-test-repo/languages", + "stargazers_url": "https://api.github.com/repos/testuser/my-test-repo/stargazers", + "contributors_url": "https://api.github.com/repos/testuser/my-test-repo/contributors", + "subscribers_url": "https://api.github.com/repos/testuser/my-test-repo/subscribers", + "subscription_url": "https://api.github.com/repos/testuser/my-test-repo/subscription", + "commits_url": "https://api.github.com/repos/testuser/my-test-repo/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/testuser/my-test-repo/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/testuser/my-test-repo/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/testuser/my-test-repo/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/testuser/my-test-repo/contents/{+path}", + "compare_url": "https://api.github.com/repos/testuser/my-test-repo/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/testuser/my-test-repo/merges", + "archive_url": "https://api.github.com/repos/testuser/my-test-repo/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/testuser/my-test-repo/downloads", + "issues_url": "https://api.github.com/repos/testuser/my-test-repo/issues{/number}", + "pulls_url": "https://api.github.com/repos/testuser/my-test-repo/pulls{/number}", + "milestones_url": "https://api.github.com/repos/testuser/my-test-repo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/testuser/my-test-repo/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/testuser/my-test-repo/labels{/name}", + "releases_url": "https://api.github.com/repos/testuser/my-test-repo/releases{/id}", + "deployments_url": "https://api.github.com/repos/testuser/my-test-repo/deployments" + } + }, + { + "id": 15269818206, + "name": "Validate JSON", + "node_id": "WFR_kwLOOvI4888AAAADjibvXg", + "head_branch": "main", + "head_sha": "5b79ac2382a1200d3f393e96ff578d9ce5f8a209", + "path": ".github/workflows/validate-json.yml", + "display_title": "Update data.json", + "run_number": 19, + "event": "push", + "status": "completed", + "conclusion": "success", + "workflow_id": 163811337, + "check_suite_id": 39170373595, + "check_suite_node_id": "CS_kwDOOvI4888AAAAJHrx32w", + "url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/15269818206", + "html_url": "https://github.com/testuser/my-test-repo/actions/runs/15269818206", + "pull_requests": [], + "created_at": "2025-05-27T08:00:51Z", + "updated_at": "2025-05-27T08:01:02Z", + "actor": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "run_attempt": 1, + "referenced_workflows": [], + "run_started_at": "2025-05-27T08:00:51Z", + "triggering_actor": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "jobs_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/15269818206/jobs", + "logs_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/15269818206/logs", + "check_suite_url": "https://api.github.com/repos/testuser/my-test-repo/check-suites/39170373595", + "artifacts_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/15269818206/artifacts", + "cancel_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/15269818206/cancel", + "rerun_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/15269818206/rerun", + "previous_attempt_url": null, + "workflow_url": "https://api.github.com/repos/testuser/my-test-repo/actions/workflows/163811337", + "head_commit": { + "id": "5b79ac2382a1200d3f393e96ff578d9ce5f8a209", + "tree_id": "e98a02d39de1fd8ab9c7fed3b0fa213fae6dd950", + "message": "Update data.json", + "timestamp": "2025-05-27T08:00:49Z", + "author": { + "name": "testuser", + "email": "123725069+testuser@users.noreply.github.com" + }, + "committer": { + "name": "GitHub", + "email": "noreply@github.com" + } + }, + "repository": { + "id": 988952819, + "node_id": "R_kgDOOvI48w", + "name": "my-test-repo", + "full_name": "testuser/my-test-repo", + "private": true, + "owner": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/testuser/my-test-repo", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/testuser/my-test-repo", + "forks_url": "https://api.github.com/repos/testuser/my-test-repo/forks", + "keys_url": "https://api.github.com/repos/testuser/my-test-repo/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/testuser/my-test-repo/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/testuser/my-test-repo/teams", + "hooks_url": "https://api.github.com/repos/testuser/my-test-repo/hooks", + "issue_events_url": "https://api.github.com/repos/testuser/my-test-repo/issues/events{/number}", + "events_url": "https://api.github.com/repos/testuser/my-test-repo/events", + "assignees_url": "https://api.github.com/repos/testuser/my-test-repo/assignees{/user}", + "branches_url": "https://api.github.com/repos/testuser/my-test-repo/branches{/branch}", + "tags_url": "https://api.github.com/repos/testuser/my-test-repo/tags", + "blobs_url": "https://api.github.com/repos/testuser/my-test-repo/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/testuser/my-test-repo/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/testuser/my-test-repo/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/testuser/my-test-repo/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/testuser/my-test-repo/statuses/{sha}", + "languages_url": "https://api.github.com/repos/testuser/my-test-repo/languages", + "stargazers_url": "https://api.github.com/repos/testuser/my-test-repo/stargazers", + "contributors_url": "https://api.github.com/repos/testuser/my-test-repo/contributors", + "subscribers_url": "https://api.github.com/repos/testuser/my-test-repo/subscribers", + "subscription_url": "https://api.github.com/repos/testuser/my-test-repo/subscription", + "commits_url": "https://api.github.com/repos/testuser/my-test-repo/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/testuser/my-test-repo/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/testuser/my-test-repo/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/testuser/my-test-repo/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/testuser/my-test-repo/contents/{+path}", + "compare_url": "https://api.github.com/repos/testuser/my-test-repo/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/testuser/my-test-repo/merges", + "archive_url": "https://api.github.com/repos/testuser/my-test-repo/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/testuser/my-test-repo/downloads", + "issues_url": "https://api.github.com/repos/testuser/my-test-repo/issues{/number}", + "pulls_url": "https://api.github.com/repos/testuser/my-test-repo/pulls{/number}", + "milestones_url": "https://api.github.com/repos/testuser/my-test-repo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/testuser/my-test-repo/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/testuser/my-test-repo/labels{/name}", + "releases_url": "https://api.github.com/repos/testuser/my-test-repo/releases{/id}", + "deployments_url": "https://api.github.com/repos/testuser/my-test-repo/deployments" + }, + "head_repository": { + "id": 988952819, + "node_id": "R_kgDOOvI48w", + "name": "my-test-repo", + "full_name": "testuser/my-test-repo", + "private": true, + "owner": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/testuser/my-test-repo", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/testuser/my-test-repo", + "forks_url": "https://api.github.com/repos/testuser/my-test-repo/forks", + "keys_url": "https://api.github.com/repos/testuser/my-test-repo/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/testuser/my-test-repo/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/testuser/my-test-repo/teams", + "hooks_url": "https://api.github.com/repos/testuser/my-test-repo/hooks", + "issue_events_url": "https://api.github.com/repos/testuser/my-test-repo/issues/events{/number}", + "events_url": "https://api.github.com/repos/testuser/my-test-repo/events", + "assignees_url": "https://api.github.com/repos/testuser/my-test-repo/assignees{/user}", + "branches_url": "https://api.github.com/repos/testuser/my-test-repo/branches{/branch}", + "tags_url": "https://api.github.com/repos/testuser/my-test-repo/tags", + "blobs_url": "https://api.github.com/repos/testuser/my-test-repo/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/testuser/my-test-repo/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/testuser/my-test-repo/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/testuser/my-test-repo/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/testuser/my-test-repo/statuses/{sha}", + "languages_url": "https://api.github.com/repos/testuser/my-test-repo/languages", + "stargazers_url": "https://api.github.com/repos/testuser/my-test-repo/stargazers", + "contributors_url": "https://api.github.com/repos/testuser/my-test-repo/contributors", + "subscribers_url": "https://api.github.com/repos/testuser/my-test-repo/subscribers", + "subscription_url": "https://api.github.com/repos/testuser/my-test-repo/subscription", + "commits_url": "https://api.github.com/repos/testuser/my-test-repo/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/testuser/my-test-repo/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/testuser/my-test-repo/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/testuser/my-test-repo/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/testuser/my-test-repo/contents/{+path}", + "compare_url": "https://api.github.com/repos/testuser/my-test-repo/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/testuser/my-test-repo/merges", + "archive_url": "https://api.github.com/repos/testuser/my-test-repo/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/testuser/my-test-repo/downloads", + "issues_url": "https://api.github.com/repos/testuser/my-test-repo/issues{/number}", + "pulls_url": "https://api.github.com/repos/testuser/my-test-repo/pulls{/number}", + "milestones_url": "https://api.github.com/repos/testuser/my-test-repo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/testuser/my-test-repo/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/testuser/my-test-repo/labels{/name}", + "releases_url": "https://api.github.com/repos/testuser/my-test-repo/releases{/id}", + "deployments_url": "https://api.github.com/repos/testuser/my-test-repo/deployments" + } + }, + { + "id": 15269799447, + "name": "Validate JSON", + "node_id": "WFR_kwLOOvI4888AAAADjiamFw", + "head_branch": "main", + "head_sha": "8a4085ad687028fe166fee05cb75c104478ff4c4", + "path": ".github/workflows/validate-json.yml", + "display_title": "Update data.json", + "run_number": 18, + "event": "push", + "status": "completed", + "conclusion": "failure", + "workflow_id": 163811337, + "check_suite_id": 39170324977, + "check_suite_node_id": "CS_kwDOOvI4888AAAAJHru58Q", + "url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/15269799447", + "html_url": "https://github.com/testuser/my-test-repo/actions/runs/15269799447", + "pull_requests": [], + "created_at": "2025-05-27T07:59:58Z", + "updated_at": "2025-05-27T08:00:08Z", + "actor": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "run_attempt": 1, + "referenced_workflows": [], + "run_started_at": "2025-05-27T07:59:58Z", + "triggering_actor": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "jobs_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/15269799447/jobs", + "logs_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/15269799447/logs", + "check_suite_url": "https://api.github.com/repos/testuser/my-test-repo/check-suites/39170324977", + "artifacts_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/15269799447/artifacts", + "cancel_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/15269799447/cancel", + "rerun_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/15269799447/rerun", + "previous_attempt_url": null, + "workflow_url": "https://api.github.com/repos/testuser/my-test-repo/actions/workflows/163811337", + "head_commit": { + "id": "8a4085ad687028fe166fee05cb75c104478ff4c4", + "tree_id": "29f761a25abc872befa9d86f6addb2e4e022931c", + "message": "Update data.json", + "timestamp": "2025-05-27T07:59:56Z", + "author": { + "name": "testuser", + "email": "123725069+testuser@users.noreply.github.com" + }, + "committer": { + "name": "GitHub", + "email": "noreply@github.com" + } + }, + "repository": { + "id": 988952819, + "node_id": "R_kgDOOvI48w", + "name": "my-test-repo", + "full_name": "testuser/my-test-repo", + "private": true, + "owner": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/testuser/my-test-repo", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/testuser/my-test-repo", + "forks_url": "https://api.github.com/repos/testuser/my-test-repo/forks", + "keys_url": "https://api.github.com/repos/testuser/my-test-repo/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/testuser/my-test-repo/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/testuser/my-test-repo/teams", + "hooks_url": "https://api.github.com/repos/testuser/my-test-repo/hooks", + "issue_events_url": "https://api.github.com/repos/testuser/my-test-repo/issues/events{/number}", + "events_url": "https://api.github.com/repos/testuser/my-test-repo/events", + "assignees_url": "https://api.github.com/repos/testuser/my-test-repo/assignees{/user}", + "branches_url": "https://api.github.com/repos/testuser/my-test-repo/branches{/branch}", + "tags_url": "https://api.github.com/repos/testuser/my-test-repo/tags", + "blobs_url": "https://api.github.com/repos/testuser/my-test-repo/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/testuser/my-test-repo/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/testuser/my-test-repo/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/testuser/my-test-repo/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/testuser/my-test-repo/statuses/{sha}", + "languages_url": "https://api.github.com/repos/testuser/my-test-repo/languages", + "stargazers_url": "https://api.github.com/repos/testuser/my-test-repo/stargazers", + "contributors_url": "https://api.github.com/repos/testuser/my-test-repo/contributors", + "subscribers_url": "https://api.github.com/repos/testuser/my-test-repo/subscribers", + "subscription_url": "https://api.github.com/repos/testuser/my-test-repo/subscription", + "commits_url": "https://api.github.com/repos/testuser/my-test-repo/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/testuser/my-test-repo/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/testuser/my-test-repo/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/testuser/my-test-repo/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/testuser/my-test-repo/contents/{+path}", + "compare_url": "https://api.github.com/repos/testuser/my-test-repo/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/testuser/my-test-repo/merges", + "archive_url": "https://api.github.com/repos/testuser/my-test-repo/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/testuser/my-test-repo/downloads", + "issues_url": "https://api.github.com/repos/testuser/my-test-repo/issues{/number}", + "pulls_url": "https://api.github.com/repos/testuser/my-test-repo/pulls{/number}", + "milestones_url": "https://api.github.com/repos/testuser/my-test-repo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/testuser/my-test-repo/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/testuser/my-test-repo/labels{/name}", + "releases_url": "https://api.github.com/repos/testuser/my-test-repo/releases{/id}", + "deployments_url": "https://api.github.com/repos/testuser/my-test-repo/deployments" + }, + "head_repository": { + "id": 988952819, + "node_id": "R_kgDOOvI48w", + "name": "my-test-repo", + "full_name": "testuser/my-test-repo", + "private": true, + "owner": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/testuser/my-test-repo", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/testuser/my-test-repo", + "forks_url": "https://api.github.com/repos/testuser/my-test-repo/forks", + "keys_url": "https://api.github.com/repos/testuser/my-test-repo/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/testuser/my-test-repo/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/testuser/my-test-repo/teams", + "hooks_url": "https://api.github.com/repos/testuser/my-test-repo/hooks", + "issue_events_url": "https://api.github.com/repos/testuser/my-test-repo/issues/events{/number}", + "events_url": "https://api.github.com/repos/testuser/my-test-repo/events", + "assignees_url": "https://api.github.com/repos/testuser/my-test-repo/assignees{/user}", + "branches_url": "https://api.github.com/repos/testuser/my-test-repo/branches{/branch}", + "tags_url": "https://api.github.com/repos/testuser/my-test-repo/tags", + "blobs_url": "https://api.github.com/repos/testuser/my-test-repo/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/testuser/my-test-repo/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/testuser/my-test-repo/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/testuser/my-test-repo/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/testuser/my-test-repo/statuses/{sha}", + "languages_url": "https://api.github.com/repos/testuser/my-test-repo/languages", + "stargazers_url": "https://api.github.com/repos/testuser/my-test-repo/stargazers", + "contributors_url": "https://api.github.com/repos/testuser/my-test-repo/contributors", + "subscribers_url": "https://api.github.com/repos/testuser/my-test-repo/subscribers", + "subscription_url": "https://api.github.com/repos/testuser/my-test-repo/subscription", + "commits_url": "https://api.github.com/repos/testuser/my-test-repo/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/testuser/my-test-repo/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/testuser/my-test-repo/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/testuser/my-test-repo/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/testuser/my-test-repo/contents/{+path}", + "compare_url": "https://api.github.com/repos/testuser/my-test-repo/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/testuser/my-test-repo/merges", + "archive_url": "https://api.github.com/repos/testuser/my-test-repo/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/testuser/my-test-repo/downloads", + "issues_url": "https://api.github.com/repos/testuser/my-test-repo/issues{/number}", + "pulls_url": "https://api.github.com/repos/testuser/my-test-repo/pulls{/number}", + "milestones_url": "https://api.github.com/repos/testuser/my-test-repo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/testuser/my-test-repo/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/testuser/my-test-repo/labels{/name}", + "releases_url": "https://api.github.com/repos/testuser/my-test-repo/releases{/id}", + "deployments_url": "https://api.github.com/repos/testuser/my-test-repo/deployments" + } + }, + { + "id": 15259682734, + "name": "Validate JSON", + "node_id": "WFR_kwLOOvI4888AAAADjYxHrg", + "head_branch": "main", + "head_sha": "0f4bd65845c8a40a7114534864f967d5c862dc4a", + "path": ".github/workflows/validate-json.yml", + "display_title": "Update data.json", + "run_number": 17, + "event": "push", + "status": "completed", + "conclusion": "success", + "workflow_id": 163811337, + "check_suite_id": 39146589062, + "check_suite_node_id": "CS_kwDOOvI4888AAAAJHVGLhg", + "url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/15259682734", + "html_url": "https://github.com/testuser/my-test-repo/actions/runs/15259682734", + "pull_requests": [], + "created_at": "2025-05-26T18:00:59Z", + "updated_at": "2025-05-26T18:01:09Z", + "actor": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "run_attempt": 1, + "referenced_workflows": [], + "run_started_at": "2025-05-26T18:00:59Z", + "triggering_actor": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "jobs_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/15259682734/jobs", + "logs_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/15259682734/logs", + "check_suite_url": "https://api.github.com/repos/testuser/my-test-repo/check-suites/39146589062", + "artifacts_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/15259682734/artifacts", + "cancel_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/15259682734/cancel", + "rerun_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/15259682734/rerun", + "previous_attempt_url": null, + "workflow_url": "https://api.github.com/repos/testuser/my-test-repo/actions/workflows/163811337", + "head_commit": { + "id": "0f4bd65845c8a40a7114534864f967d5c862dc4a", + "tree_id": "e98a02d39de1fd8ab9c7fed3b0fa213fae6dd950", + "message": "Update data.json", + "timestamp": "2025-05-26T18:00:57Z", + "author": { + "name": "testuser", + "email": "123725069+testuser@users.noreply.github.com" + }, + "committer": { + "name": "GitHub", + "email": "noreply@github.com" + } + }, + "repository": { + "id": 988952819, + "node_id": "R_kgDOOvI48w", + "name": "my-test-repo", + "full_name": "testuser/my-test-repo", + "private": true, + "owner": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/testuser/my-test-repo", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/testuser/my-test-repo", + "forks_url": "https://api.github.com/repos/testuser/my-test-repo/forks", + "keys_url": "https://api.github.com/repos/testuser/my-test-repo/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/testuser/my-test-repo/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/testuser/my-test-repo/teams", + "hooks_url": "https://api.github.com/repos/testuser/my-test-repo/hooks", + "issue_events_url": "https://api.github.com/repos/testuser/my-test-repo/issues/events{/number}", + "events_url": "https://api.github.com/repos/testuser/my-test-repo/events", + "assignees_url": "https://api.github.com/repos/testuser/my-test-repo/assignees{/user}", + "branches_url": "https://api.github.com/repos/testuser/my-test-repo/branches{/branch}", + "tags_url": "https://api.github.com/repos/testuser/my-test-repo/tags", + "blobs_url": "https://api.github.com/repos/testuser/my-test-repo/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/testuser/my-test-repo/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/testuser/my-test-repo/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/testuser/my-test-repo/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/testuser/my-test-repo/statuses/{sha}", + "languages_url": "https://api.github.com/repos/testuser/my-test-repo/languages", + "stargazers_url": "https://api.github.com/repos/testuser/my-test-repo/stargazers", + "contributors_url": "https://api.github.com/repos/testuser/my-test-repo/contributors", + "subscribers_url": "https://api.github.com/repos/testuser/my-test-repo/subscribers", + "subscription_url": "https://api.github.com/repos/testuser/my-test-repo/subscription", + "commits_url": "https://api.github.com/repos/testuser/my-test-repo/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/testuser/my-test-repo/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/testuser/my-test-repo/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/testuser/my-test-repo/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/testuser/my-test-repo/contents/{+path}", + "compare_url": "https://api.github.com/repos/testuser/my-test-repo/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/testuser/my-test-repo/merges", + "archive_url": "https://api.github.com/repos/testuser/my-test-repo/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/testuser/my-test-repo/downloads", + "issues_url": "https://api.github.com/repos/testuser/my-test-repo/issues{/number}", + "pulls_url": "https://api.github.com/repos/testuser/my-test-repo/pulls{/number}", + "milestones_url": "https://api.github.com/repos/testuser/my-test-repo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/testuser/my-test-repo/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/testuser/my-test-repo/labels{/name}", + "releases_url": "https://api.github.com/repos/testuser/my-test-repo/releases{/id}", + "deployments_url": "https://api.github.com/repos/testuser/my-test-repo/deployments" + }, + "head_repository": { + "id": 988952819, + "node_id": "R_kgDOOvI48w", + "name": "my-test-repo", + "full_name": "testuser/my-test-repo", + "private": true, + "owner": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/testuser/my-test-repo", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/testuser/my-test-repo", + "forks_url": "https://api.github.com/repos/testuser/my-test-repo/forks", + "keys_url": "https://api.github.com/repos/testuser/my-test-repo/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/testuser/my-test-repo/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/testuser/my-test-repo/teams", + "hooks_url": "https://api.github.com/repos/testuser/my-test-repo/hooks", + "issue_events_url": "https://api.github.com/repos/testuser/my-test-repo/issues/events{/number}", + "events_url": "https://api.github.com/repos/testuser/my-test-repo/events", + "assignees_url": "https://api.github.com/repos/testuser/my-test-repo/assignees{/user}", + "branches_url": "https://api.github.com/repos/testuser/my-test-repo/branches{/branch}", + "tags_url": "https://api.github.com/repos/testuser/my-test-repo/tags", + "blobs_url": "https://api.github.com/repos/testuser/my-test-repo/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/testuser/my-test-repo/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/testuser/my-test-repo/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/testuser/my-test-repo/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/testuser/my-test-repo/statuses/{sha}", + "languages_url": "https://api.github.com/repos/testuser/my-test-repo/languages", + "stargazers_url": "https://api.github.com/repos/testuser/my-test-repo/stargazers", + "contributors_url": "https://api.github.com/repos/testuser/my-test-repo/contributors", + "subscribers_url": "https://api.github.com/repos/testuser/my-test-repo/subscribers", + "subscription_url": "https://api.github.com/repos/testuser/my-test-repo/subscription", + "commits_url": "https://api.github.com/repos/testuser/my-test-repo/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/testuser/my-test-repo/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/testuser/my-test-repo/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/testuser/my-test-repo/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/testuser/my-test-repo/contents/{+path}", + "compare_url": "https://api.github.com/repos/testuser/my-test-repo/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/testuser/my-test-repo/merges", + "archive_url": "https://api.github.com/repos/testuser/my-test-repo/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/testuser/my-test-repo/downloads", + "issues_url": "https://api.github.com/repos/testuser/my-test-repo/issues{/number}", + "pulls_url": "https://api.github.com/repos/testuser/my-test-repo/pulls{/number}", + "milestones_url": "https://api.github.com/repos/testuser/my-test-repo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/testuser/my-test-repo/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/testuser/my-test-repo/labels{/name}", + "releases_url": "https://api.github.com/repos/testuser/my-test-repo/releases{/id}", + "deployments_url": "https://api.github.com/repos/testuser/my-test-repo/deployments" + } + }, + { + "id": 15259667432, + "name": "Validate JSON", + "node_id": "WFR_kwLOOvI4888AAAADjYwL6A", + "head_branch": "main", + "head_sha": "d6466538c9490f92921071900ca5cbea893a323d", + "path": ".github/workflows/validate-json.yml", + "display_title": "Update data.json", + "run_number": 16, + "event": "push", + "status": "completed", + "conclusion": "failure", + "workflow_id": 163811337, + "check_suite_id": 39146547394, + "check_suite_node_id": "CS_kwDOOvI4888AAAAJHVDowg", + "url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/15259667432", + "html_url": "https://github.com/testuser/my-test-repo/actions/runs/15259667432", + "pull_requests": [], + "created_at": "2025-05-26T17:59:55Z", + "updated_at": "2025-05-26T18:00:05Z", + "actor": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "run_attempt": 1, + "referenced_workflows": [], + "run_started_at": "2025-05-26T17:59:55Z", + "triggering_actor": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "jobs_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/15259667432/jobs", + "logs_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/15259667432/logs", + "check_suite_url": "https://api.github.com/repos/testuser/my-test-repo/check-suites/39146547394", + "artifacts_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/15259667432/artifacts", + "cancel_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/15259667432/cancel", + "rerun_url": "https://api.github.com/repos/testuser/my-test-repo/actions/runs/15259667432/rerun", + "previous_attempt_url": null, + "workflow_url": "https://api.github.com/repos/testuser/my-test-repo/actions/workflows/163811337", + "head_commit": { + "id": "d6466538c9490f92921071900ca5cbea893a323d", + "tree_id": "29f761a25abc872befa9d86f6addb2e4e022931c", + "message": "Update data.json", + "timestamp": "2025-05-26T17:59:52Z", + "author": { + "name": "testuser", + "email": "123725069+testuser@users.noreply.github.com" + }, + "committer": { + "name": "GitHub", + "email": "noreply@github.com" + } + }, + "repository": { + "id": 988952819, + "node_id": "R_kgDOOvI48w", + "name": "my-test-repo", + "full_name": "testuser/my-test-repo", + "private": true, + "owner": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/testuser/my-test-repo", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/testuser/my-test-repo", + "forks_url": "https://api.github.com/repos/testuser/my-test-repo/forks", + "keys_url": "https://api.github.com/repos/testuser/my-test-repo/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/testuser/my-test-repo/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/testuser/my-test-repo/teams", + "hooks_url": "https://api.github.com/repos/testuser/my-test-repo/hooks", + "issue_events_url": "https://api.github.com/repos/testuser/my-test-repo/issues/events{/number}", + "events_url": "https://api.github.com/repos/testuser/my-test-repo/events", + "assignees_url": "https://api.github.com/repos/testuser/my-test-repo/assignees{/user}", + "branches_url": "https://api.github.com/repos/testuser/my-test-repo/branches{/branch}", + "tags_url": "https://api.github.com/repos/testuser/my-test-repo/tags", + "blobs_url": "https://api.github.com/repos/testuser/my-test-repo/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/testuser/my-test-repo/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/testuser/my-test-repo/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/testuser/my-test-repo/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/testuser/my-test-repo/statuses/{sha}", + "languages_url": "https://api.github.com/repos/testuser/my-test-repo/languages", + "stargazers_url": "https://api.github.com/repos/testuser/my-test-repo/stargazers", + "contributors_url": "https://api.github.com/repos/testuser/my-test-repo/contributors", + "subscribers_url": "https://api.github.com/repos/testuser/my-test-repo/subscribers", + "subscription_url": "https://api.github.com/repos/testuser/my-test-repo/subscription", + "commits_url": "https://api.github.com/repos/testuser/my-test-repo/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/testuser/my-test-repo/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/testuser/my-test-repo/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/testuser/my-test-repo/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/testuser/my-test-repo/contents/{+path}", + "compare_url": "https://api.github.com/repos/testuser/my-test-repo/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/testuser/my-test-repo/merges", + "archive_url": "https://api.github.com/repos/testuser/my-test-repo/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/testuser/my-test-repo/downloads", + "issues_url": "https://api.github.com/repos/testuser/my-test-repo/issues{/number}", + "pulls_url": "https://api.github.com/repos/testuser/my-test-repo/pulls{/number}", + "milestones_url": "https://api.github.com/repos/testuser/my-test-repo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/testuser/my-test-repo/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/testuser/my-test-repo/labels{/name}", + "releases_url": "https://api.github.com/repos/testuser/my-test-repo/releases{/id}", + "deployments_url": "https://api.github.com/repos/testuser/my-test-repo/deployments" + }, + "head_repository": { + "id": 988952819, + "node_id": "R_kgDOOvI48w", + "name": "my-test-repo", + "full_name": "testuser/my-test-repo", + "private": true, + "owner": { + "login": "testuser", + "id": 123725069, + "node_id": "U_kgDOB1_lDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/123725069?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/testuser", + "html_url": "https://github.com/testuser", + "followers_url": "https://api.github.com/users/testuser/followers", + "following_url": "https://api.github.com/users/testuser/following{/other_user}", + "gists_url": "https://api.github.com/users/testuser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/testuser/subscriptions", + "organizations_url": "https://api.github.com/users/testuser/orgs", + "repos_url": "https://api.github.com/users/testuser/repos", + "events_url": "https://api.github.com/users/testuser/events{/privacy}", + "received_events_url": "https://api.github.com/users/testuser/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/testuser/my-test-repo", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/testuser/my-test-repo", + "forks_url": "https://api.github.com/repos/testuser/my-test-repo/forks", + "keys_url": "https://api.github.com/repos/testuser/my-test-repo/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/testuser/my-test-repo/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/testuser/my-test-repo/teams", + "hooks_url": "https://api.github.com/repos/testuser/my-test-repo/hooks", + "issue_events_url": "https://api.github.com/repos/testuser/my-test-repo/issues/events{/number}", + "events_url": "https://api.github.com/repos/testuser/my-test-repo/events", + "assignees_url": "https://api.github.com/repos/testuser/my-test-repo/assignees{/user}", + "branches_url": "https://api.github.com/repos/testuser/my-test-repo/branches{/branch}", + "tags_url": "https://api.github.com/repos/testuser/my-test-repo/tags", + "blobs_url": "https://api.github.com/repos/testuser/my-test-repo/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/testuser/my-test-repo/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/testuser/my-test-repo/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/testuser/my-test-repo/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/testuser/my-test-repo/statuses/{sha}", + "languages_url": "https://api.github.com/repos/testuser/my-test-repo/languages", + "stargazers_url": "https://api.github.com/repos/testuser/my-test-repo/stargazers", + "contributors_url": "https://api.github.com/repos/testuser/my-test-repo/contributors", + "subscribers_url": "https://api.github.com/repos/testuser/my-test-repo/subscribers", + "subscription_url": "https://api.github.com/repos/testuser/my-test-repo/subscription", + "commits_url": "https://api.github.com/repos/testuser/my-test-repo/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/testuser/my-test-repo/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/testuser/my-test-repo/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/testuser/my-test-repo/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/testuser/my-test-repo/contents/{+path}", + "compare_url": "https://api.github.com/repos/testuser/my-test-repo/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/testuser/my-test-repo/merges", + "archive_url": "https://api.github.com/repos/testuser/my-test-repo/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/testuser/my-test-repo/downloads", + "issues_url": "https://api.github.com/repos/testuser/my-test-repo/issues{/number}", + "pulls_url": "https://api.github.com/repos/testuser/my-test-repo/pulls{/number}", + "milestones_url": "https://api.github.com/repos/testuser/my-test-repo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/testuser/my-test-repo/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/testuser/my-test-repo/labels{/name}", + "releases_url": "https://api.github.com/repos/testuser/my-test-repo/releases{/id}", + "deployments_url": "https://api.github.com/repos/testuser/my-test-repo/deployments" + } + } + ] +} diff --git a/project-strobe-color-from-github-workflow/test_files/sample_select_latest.json b/project-strobe-color-from-github-workflow/test_files/sample_select_latest.json new file mode 100644 index 0000000..2df504b --- /dev/null +++ b/project-strobe-color-from-github-workflow/test_files/sample_select_latest.json @@ -0,0 +1,37 @@ +{ + "total_count": 4, + "workflow_runs": [ + { + "id": 16833205169, + "name": "Validate JSON", + "display_title": "test failure", + "status": "completed", + "conclusion": "failure", + "run_number": 16 + }, + { + "id": 16833205170, + "name": "Validate JSON", + "display_title": "test success", + "status": "completed", + "conclusion": "success", + "run_number": 20 + }, + { + "id": 16833205171, + "name": "Validate JSON", + "display_title": "test success repeat", + "status": "completed", + "conclusion": "success", + "run_number": 20 + }, + { + "id": 16833205172, + "name": "Validate JSON", + "display_title": "test older success", + "status": "completed", + "conclusion": "success", + "run_number": 19 + } + ] +} diff --git a/project-strobe-color-from-github-workflow/trigger_strobe.sh b/project-strobe-color-from-github-workflow/trigger_strobe.sh new file mode 100755 index 0000000..87bd021 --- /dev/null +++ b/project-strobe-color-from-github-workflow/trigger_strobe.sh @@ -0,0 +1,185 @@ +#!/bin/sh + +# Axis Strobe Light Controller Script +# +# This script controls an Axis strobe light device by activating specific +# color profiles based on GitHub workflow status received via Telegraf. +# +# Key Features: +# - Receives color commands via JSON from Telegraf exec output plugin +# - Controls Axis strobe device using VAPIX HTTP API +# - Manages exclusive color profiles (only one active at a time) +# - Provides comprehensive error handling and validation +# - Supports green (success), yellow (running), and red (failure) colors +# +# Environment Variables Required: +# - VAPIX_USERNAME: Device username +# - VAPIX_PASSWORD: Device password +# - TELEGRAF_DEBUG: Enable debug logging when set to "true" +# - HELPER_FILES_DIR: Directory for debug log files +# +# Optional Environment Variables: +# - VAPIX_IP: IP address of the Axis strobe device (defaults to 127.0.0.1) +# +# Error Codes: +# - 10: Missing required environment variables +# - 11: Empty input received from stdin +# - 12: No color field found in JSON input +# - 13: Invalid color value (not green/yellow/red) +# - 14: VAPIX API call failed + +# Set stricter error handling +set -eu + +# Color profile constants +readonly COLOR_GREEN="green" +readonly COLOR_YELLOW="yellow" +readonly COLOR_RED="red" + +# Validate required environment variables for VAPIX API access +# These credentials are needed to authenticate with the Axis device +# and control the strobe light profiles via HTTP API calls +if [ -z "$VAPIX_USERNAME" ] || [ -z "$VAPIX_PASSWORD" ]; then + printf "Error: VAPIX_USERNAME and VAPIX_PASSWORD must be set" >&2 + exit 10 +fi + +# Set default IP to localhost if not provided. This is what is needed when +# running directly on the Axis strobe. +VAPIX_IP="${VAPIX_IP:-127.0.0.1}" + +# Debug mode - use TELEGRAF_DEBUG environment variable +DEBUG="${TELEGRAF_DEBUG:-false}" + +# Function to log debug messages to a file +debug_log_file() { + _dbg_log_message="$1" + if [ "$DEBUG" = "true" ]; then + echo "DEBUG: $_dbg_log_message" >> "${HELPER_FILES_DIR}/trigger_strobe.debug" 2>/dev/null || true + fi + return 0 +} + +debug_log_file "Starting trigger_strobe.sh script" +debug_log_file "Environment variables - VAPIX_USERNAME: $VAPIX_USERNAME, VAPIX_IP: $VAPIX_IP, DEBUG: $DEBUG" + +# Read JSON input from Telegraf via stdin +# Expected format: {"fields":{"color":"green"},"name":"workflow_color",...} +# This is the metric data sent by the Telegraf exec output plugin +json_input=$(cat) + +debug_log_file "Received JSON input: $json_input" + +# Validate that we received input data +# Empty input indicates a problem with the Telegraf pipeline +if [ -z "$json_input" ]; then + debug_log_file "ERROR: Empty input received from Telegraf" + printf "Error: Empty input received from Telegraf. Expected JSON format: {\"fields\":{\"color\":\"value\"}}" >&2 + exit 11 +fi + +# Extract color value from JSON using jq +# Telegraf sends individual metric objects when use_batch_format=false +# JSON structure: {"fields":{"color":"value"},"name":"workflow_color",...} +# The .fields.color path extracts the color command from the metric +# Note: If use_batch_format=true was used, we'd need '.metrics[0].fields.color' +if ! color="$(printf '%s\n' "$json_input" | jq -re '.fields.color? // empty')"; then + debug_log_file "ERROR: jq failed to extract .fields.color from input:$json_input" + printf "Error: No color field found in JSON input:$json_input" >&2 + exit 12 +fi + +debug_log_file "Extracted color value: $color" + +# Validate color value against supported strobe profiles +# Only these three colors have corresponding profiles configured on the device +# Each color represents a different workflow state for visual monitoring +debug_log_file "Validating color value: $color" +case $color in + "$COLOR_GREEN") # Workflow success - all tests passed, deployment successful + debug_log_file "Color validation successful: green (success)" + ;; + "$COLOR_YELLOW") # Workflow running - in progress, queued, or unknown state + debug_log_file "Color validation successful: yellow (running)" + ;; + "$COLOR_RED") # Workflow failure - tests failed, build errors, deployment issues + debug_log_file "Color validation successful: red (failure)" + ;; + *) + debug_log_file "ERROR: Invalid color value: $color" + printf "Error: Invalid color '%s'. Supported colors: green ($COLOR_GREEN), yellow ($COLOR_YELLOW), red ($COLOR_RED)." "$color" >&2 + exit 13 + ;; +esac + +# Function to control strobe light profiles via VAPIX API +# This function handles both starting and stopping of color profiles +# Uses HTTP Digest authentication since the Axis devices requires that for HTTP +control_profile() { + _ctrl_profile="$1" # Profile name (green, yellow, red) + _ctrl_action="$2" # Action to perform (start, stop) + + debug_log_file "Making VAPIX API call - Profile: $_ctrl_profile, Action: $_ctrl_action" + debug_log_file "API endpoint: http://${VAPIX_IP}/axis-cgi/siren_and_light.cgi" + debug_log_file "Using credentials: $VAPIX_USERNAME:$(printf '%*s' ${#VAPIX_PASSWORD} '' | tr ' ' '*')" + + # Make VAPIX API call to control the strobe profile + # Endpoint: siren_and_light.cgi for controlling light patterns + # Method: POST with JSON payload specifying action and profile + # Auth: HTTP Digest authentication + _ctrl_api_response=$(curl --fail --digest --user "${VAPIX_USERNAME}:${VAPIX_PASSWORD}" "http://${VAPIX_IP}/axis-cgi/siren_and_light.cgi" \ + -X POST \ + -H "Content-Type: application/json" \ + -d "{\"apiVersion\":\"1.0\",\"method\":\"$_ctrl_action\",\"params\":{\"profile\":\"$_ctrl_profile\"}}" 2>&1) + _ctrl_api_exit=$? + + debug_log_file "API call exit code: $_ctrl_api_exit" + debug_log_file "API response: $_ctrl_api_response" + + # Check if the API call was successful + # Non-zero exit code indicates network error, authentication failure, + # or invalid profile/action parameters + if [ $_ctrl_api_exit -ne 0 ]; then + debug_log_file "ERROR: VAPIX API call failed - Profile: $_ctrl_profile, Action: $_ctrl_action" + debug_log_file "ERROR: Response: $_ctrl_api_response" + printf "Failed to %s profile '%s'" "$_ctrl_action" "$_ctrl_profile" >&2 + exit 14 + else + debug_log_file "VAPIX API call successful - Profile: $_ctrl_profile, Action: $_ctrl_action" + fi + return 0 +} + +# Activate the requested color profile +# This starts the strobe pattern corresponding to the workflow status +debug_log_file "Starting color profile: $color" +control_profile "$color" "start" + +# Deactivate all other color profiles to ensure exclusive operation, +# this way we do not need to care about the priorities of the profiles +# when creating them. +debug_log_file "Deactivating other color profiles to ensure exclusive operation" +case $color in + "$COLOR_GREEN") # Success state - stop running and failure indicators + debug_log_file "Stopping yellow and red profiles" + control_profile "$COLOR_YELLOW" "stop" + control_profile "$COLOR_RED" "stop" + ;; + "$COLOR_YELLOW") # Running state - stop success and failure indicators + debug_log_file "Stopping green and red profiles" + control_profile "$COLOR_GREEN" "stop" + control_profile "$COLOR_RED" "stop" + ;; + "$COLOR_RED") # Failure state - stop success and running indicators + debug_log_file "Stopping green and yellow profiles" + control_profile "$COLOR_GREEN" "stop" + control_profile "$COLOR_YELLOW" "stop" + ;; + *) + debug_log_file "ERROR: Invalid color value: $color" + printf "Error: Invalid color '%s'. Supported colors: green ($COLOR_GREEN), yellow ($COLOR_YELLOW), red ($COLOR_RED)." "$color" >&2 + exit 13 + ;; +esac + +debug_log_file "Script completed successfully - Color: $color" \ No newline at end of file