diff --git a/.github/dependabot.yml b/.allspice/dependabot.yml similarity index 100% rename from .github/dependabot.yml rename to .allspice/dependabot.yml diff --git a/.allspice/examples/config.yml b/.allspice/examples/config.yml new file mode 100644 index 0000000..ebd1eee --- /dev/null +++ b/.allspice/examples/config.yml @@ -0,0 +1 @@ +# Mock config yaml file \ No newline at end of file diff --git a/.allspice/examples/input.txt b/.allspice/examples/input.txt new file mode 100644 index 0000000..c619fbf --- /dev/null +++ b/.allspice/examples/input.txt @@ -0,0 +1 @@ +Mock input file \ No newline at end of file diff --git a/.allspice/workflows/add-on-workflow-example.yml b/.allspice/workflows/add-on-workflow-example.yml new file mode 100644 index 0000000..a0006cd --- /dev/null +++ b/.allspice/workflows/add-on-workflow-example.yml @@ -0,0 +1,22 @@ +name: Example AllSpice Add-on Template + +on: [push, pull_request] + +jobs: + hardware-devops: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Hardware DevOps Action + uses: https://hub.allspice.io/AllSpice-Demos/Add-on-template@v0.1 + with: + source_path: ".allspice/examples/input.txt" + output_file_name: "output.txt" + config_file: ".allspice/examples/config.yml" + task_type: "Schematic-Review" + additional_params: '{"SCH_VER":"3"}' + env: + ALLSPICE_TOKEN: ${{ allspice.token }} \ No newline at end of file diff --git a/.github/workflows/test.yml b/.allspice/workflows/test.yml similarity index 100% rename from .github/workflows/test.yml rename to .allspice/workflows/test.yml diff --git a/Add-on-script.py b/Add-on-script.py new file mode 100755 index 0000000..7be0011 --- /dev/null +++ b/Add-on-script.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 + +import argparse +import os +import sys +import json +from allspice import AllSpice + +def hello_world(task_type, source_file, output_file, additional_params): + print(f"Hello, World! Performing task: {task_type}") + print(f"Source file: {source_file}") + print(f"Output file: {output_file}") + print(f"Additional parameters: {additional_params}") + + # Parse additional_params + params = json.loads(additional_params) + sch_ver = params.get("SCH_VER", "3") + print(f"Schematic version: {sch_ver}") + +def test_allspice_connection(allspice_hub_url, auth_token): + try: + allspice = AllSpice(token_text=auth_token, allspice_hub_url=allspice_hub_url) + print("AllSpice Version: " + allspice.get_version()) + print("API-Token belongs to user: " + allspice.get_user().username) + except Exception as e: + print(f"Error connecting to AllSpice API: {e}") + sys.exit(1) + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + prog="hardware_devops_action", + description="Perform hardware development tasks such as Schematic-Review, PCB-Review, ECO-Review, and Release." + ) + parser.add_argument( + "--source_file", + help="The path to the source file used for the task. Example: 'Archimajor.PrjPcb', 'Schematics/Beagleplay.dsn'." + ) + parser.add_argument( + "--task_type", + help="The type of hardware task to perform. Options include 'Schematic-Review', 'PCB-Review', 'ECO-Review', 'Release'.", + default="Schematic-Review", + ) + parser.add_argument( + "--source_ref", + help="The git reference the task should be performed for (eg. branch name, tag name, commit SHA).", + default="main", + ) + parser.add_argument( + "--server_url", + help="The URL of your GitHub server instance.", + ) + parser.add_argument( + "--output_file", + help="The path to the output file. If absent, the output will be printed to the command line.", + ) + parser.add_argument( + "--additional_params", + help="Any additional parameters required for the task, provided as a JSON string.", + default="{}", + ) + parser.add_argument( + "--allspice_token", + help="Your AllSpice application token. Generate a token: https://hub.allspice.io/user/settings/applications", + ) + parser.add_argument( + "--config_file", + help="Path to config file" + ) + parser.add_argument( + "--input_file", + help="path/to/input_file" + ) + + args = parser.parse_args() + + auth_token = os.environ.get("ALLSPICE_TOKEN") or args.allspice_token + if auth_token is None: + print("Please set the environment variable ALLSPICE_TOKEN or supply a token with --allspice_token . Generate a token: https://hub.allspice.io/user/settings/applications") + exit(1) + + # Test connection to AllSpice API + test_allspice_connection(args.server_url, auth_token) + + # Perform the Hello World task + hello_world( + task_type=args.task_type, + source_file=args.source_file, + output_file=args.output_file, + additional_params=args.additional_params, + ) \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 2155fcd..fe09250 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ FROM python:3.12-bookworm COPY requirements.txt /requirements.txt -COPY entrypoint.py /entrypoint.py +COPY Add-on-script.py /Add-on-script.py RUN pip install -r /requirements.txt -ENTRYPOINT [ "/entrypoint.py" ] +ENTRYPOINT [ "/Add-on-script.py" ] diff --git a/README.md b/README.md index efc8be9..984351f 100644 --- a/README.md +++ b/README.md @@ -1,141 +1,188 @@ -# Generate BOM for E-CAD Projects +# AllSpice Actions Add-on template -Generate a BOM output file for an Altium project on AllSpice Hub using [AllSpice Actions](https://learn.allspice.io/docs/actions-cicd). +This Add-on shows you how to create an Add-on, and how to set up the files to make an API to pass inputs to the Add-on. + +## Table of Contents + + - [Usage](#usage) + - [Inputs](#inputs) + - [Outputs](#outputs) + - [Example Workflow](#example-workflow) + - [License](#license) ## Usage -Add the following steps to your actions: +### Calling this add-on +This Add-on can be called from an external repository workflow file. +There is an example workflow file in this repository: +[.allspice/workflows/add-on-workflow-example.yml](.allspice/workflows/add-on-workflow-example.yml) + +You can copy this workflow file to another repository and use it to call this Add-on template. + +### Define API in action.yml +The file [action.yml](action.yml) describes how to connect your workflow call of the add-on to the actual script and specifies how parameters are used. + +This is considered an API contract. + +Below is the `action.yml` file for this repo. You can see that the `inputs` section maps inputs to input variables. + +The second section composes the args from inputs and other values and passes it to the Dockerfile. +```yaml +name: "Hardware DevOps Action" +description: > + A generic AllSpice Action Add-on for hardware development tasks such as schematic review, + PCB review, ECO review, and release. This action demonstrates defining parameters + for these tasks and utilizing GitHub context information. + +inputs: + source_file: + description: > + Path to the source file or directory from the root of the repo. For example, + the path to a schematic or PCB file. + required: true + output_file_name: + description: "Name of the output file" + required: true + default: "output.txt" + config_file: + description: > + Path to a configuration file for the task. + required: true + task_type: + description: > + The type of hardware task to perform. Options include 'Schematic-Review', + 'PCB-Review', 'ECO-Review', 'Release'. + default: "Schematic-Review" + additional_params: + description: > + Any additional parameters required for the task, provided as a JSON string. + default: "{}" + +runs: + using: "docker" + image: "Dockerfile" + args: + - "--source_file" + - "${{ inputs.source_file}}" + - "--output_file" + - "${{ github.workspace}}/${{ inputs.output_file_name }}" + - "--config_file" + - ${{ inputs.config_file }} + - "--task_type" + - ${{ inputs.task_type }} + - "--additional_params" + - ${{ inputs.additional_params }} + - "--source_ref" + - ${{ allspice.sha }} + - "--server_url" + - ${{ allspice.server_url }} + - "--allspice_token" + - ${{ secrets.ALLSPICE_TOKEN }} +env: + GITHUB_TOKEN: ${{ github.token }} +``` + +### Define Add-on script + +[`Add-on-script.py`](Add-on-script.py) +This is the program that you will use to perform your Add-on. In this case, we use Python and the py-allspice API wrapper. + +The first part of the program is parsing the arguments from the API contract, and the second part runs your actual Add-on. In this template example the Python script performs a connection test to AllSpice, and displays the parameters passed from the calling Workflow file. + +### Define Dockerfile + +`Dockerfile` +The Dockerfile specifies how to set up the environment and what file to run as the Add-on script. + +In this repo template, we load Python 3.12, install modules from requirements.py, and thn run Add-on-script.py. + +```Dockerfile +FROM python:3.12-bookworm + +COPY requirements.txt /requirements.txt +COPY Add-on-script.py /Add-on-script.py + +RUN pip install -r /requirements.txt + +ENTRYPOINT [ "/Add-on-script.py" ] +``` + +### requirements.txt + +The requirements.txt file specifies which Python modules to load and which version to load. + +module-name=version# + +`py-allspice` AllSpice’s native Python wrapper to the AllSpice API. + +`pyyaml` A YAML markdown language processor. Helps parse workflow.yml files. + +``` +py-allspice==3.3.0 +pyyaml~=6.0 +``` + +### Testing files + +This repo has an optional Action workflow that checks the syntax of Add-on-script.py. This is helpful because Python is an interpreted language. + +You do not need these files to run your Add-on, however using tests will help you spot errors. + +- `.allspice/dependabot.yml` - Instructions for repository workflow tests. +- `.allspice/workflows/test.yml` - Workflow to test this repo on design review. + - Lints 3 different versions of python (Checks syntax) +- `pyproject.toml` - Linter setup. Specifies how the repository workflow tests will check the syntax of this repository +- `requirements-test.txt` - the requirements for the test workflow. + + +## Add-on input API +You can customize your Add-on inputs to match your own workflow. These are some example inputs that are helpful for running Add-ons. + +You can have as many or as few inputs as you need for your workflow. + +- `source_file`: The path to the source file used for the task. Example: `Archimajor.PrjPcb`, `Schematics/Beagleplay.dsn`. +- `task_type`: The type of hardware task to perform. Options include `Schematic-Review`, `PCB-Review`, `ECO-Review`, `Release`. (default: `Schematic-Review`) +- `source_ref`: The git reference the task should be performed for (e.g., branch name, tag name, commit SHA). (default: `main`) +- `server_url`: The URL of your AllSpice server instance. +- `output_file`: The path to the output file. If absent, the output will be printed to the command line. +- `additional_params`: Any additional parameters required for the task, provided as a JSON string. (default: `{}`) +- `allspice_token`: Your AllSpice application token. Generate a token at: https://hub.allspice.io/user/settings/applications +- `config_file`: Path to the config file. +- `input_file`: Path to the input file. + +## Outputs + +The outputs are dependent on the task performed and will be printed to the command line or saved to the specified `output_file`. + +In this template repo, there are no actual outputs, however the name of the output is displayed to demnostrate the correct passing of inputs to the script. + +## Example Workflow + +Here is the file [./allspice/workflows/add-on-workflow-example.yml](./allspice/workflows/add-on-workflow-example.yml) + +This shows how to call the Add-on in this repo. ```yaml -# Checkout is only needed if columns.yml is committed in your Altium project repo. -- name: Checkout - uses: actions/checkout@v3 +name: Example AllSpice Add-on Template -- name: Generate BOM - uses: https://hub.allspice.io/Actions/generate-bom@v0.4 - with: - # The path to the project file in your repo (.PrjPcb for Altium, .DSN for OrCad). - source_path: Archimajor.PrjPcb - # [optional] A path to a YAML file mapping columns to the component - # attributes they are from. - # Default: 'columns.yml' - columns: .allspice/columns.yml - # [optional] The path to the output file that will be generated. - # Default: 'bom.csv' - output_file_name: bom.csv - # [optional] A comma-separated list of columns to group the BOM by. If empty - # or not present, the BOM will be flat. - # Default: '' - group_by: "Part ID" - # [optional] The variant of the project to generate the BOM for. If empty - # or not present, the BOM will be generated for the default variant. - # Default: '' - variant: "" -``` +on: [push, pull_request] -### Customizing the Attributes Extracted by the BOM Script +jobs: + hardware-devops: + runs-on: ubuntu-latest -This script relies on a YAML file to specify the columns in the BOM and which -attributes or properties of the components they are populated from. This file is -typically called `columns.yml` and can be checked into your repo. To learn more -about YAML, [check out the AllSpice Knowledge Base.](https://learn.allspice.io/docs/yaml) + steps: + - name: Checkout code + uses: actions/checkout@v4 -The format of this YAML file is as follows: - -```yml -columns: - - name: "Manufacturer" - part_attributes: - - "Manufacturer" - - "MANUFACTURER" - - name: "Part Number" - part_attributes: - - "PART" - - "MANUFACTURER #" - - "_part_id" - - name: "Designator" - part_attributes: "Designator" - - name: "Description" - part_attributes: - - "PART DESCRIPTION" - - "_description" -``` - -First, you have the key `columns:` which is mapped to a list. Each element of -the list has two key/value pairs. The first is `name`, which will be the column -name in the output file. Next, you have `part_attributes`. This can either be -just a string (like in the case of the `Designator` column) or a list of strings -(like in the other cases). - -If `part_attributes` is a string, that property or attribute of the component -is used as the value for that column. If that property is not present -in a particular part, that column will be blank for that part. If -`part_attributes` is a list, those properties will be checked in the order they -are defined for each part. The _first_ property found is used as the value for -that column in the row for that part. So if both `PART` and `MANUFACTURER #` are -defined, it will use `PART`. - -An example for OrCad `columns.yml` file content is: - -```yml -columns: - - name: "Part Number" - part_attributes: - - "Part Number" - - "_name" - - name: "Designator" - part_attributes: "Part Reference" - - name: "Type" - part_attributes: "Part Type" - - name: "Value" - part_attributes: "Value" -``` - -By default, the action will pick up a `columns.yml` file from the working -directory. If you want to keep it in a different place or rename it, you can -pass the `--columns` argument to the step in the workflow to specify where it -is. - -### Py-allspice injected attributes - -Note that py-allspice also adds a few static attributes, which are taken from -the part itself, and not from the properties or attributes. For Altium projects, -`_part_id` and `_description` are available, which correspond to the Library -Reference and Description fields of the component. For OrCAD projects, `_name` -is available, which corresponds to the name of the component. - -The underscore is added ahead of the name to prevent these additional attributes -from overriding any of your own. - -## Group By - -You can also group lines by a column value. The most common is `_part_id`. You -can combine this with the columns YAML example above, like so: - -```yaml -- name: Generate BOM - uses: https://hub.allspice.io/Actions/generate-bom@v0.4 - with: - project_path: Archimajor.PrjPcb - columns: .allspice/columns.yml - group_by: "Part ID" -``` - -Which will generate a BOM squashed by components with matchin Part IDs. - -## Variants - -To generate the BOM for a variant of the project, pass the `--variant` argument -to the script. For example: - -```yaml -- name: Generate BOM - uses: https://hub.allspice.io/Actions/generate-bom@v0.4 - with: - project_path: Archimajor.PrjPcb - columns: .allspice/columns.yml - output_file_name: bom-lite.csv - variant: "LITE" -``` - -When no variant is given, the BOM is generated without considering any variants. + - name: Run Hardware DevOps Action + uses: https://hub.allspice.io/AllSpice-Demos/Add-on-template@v0.1 + with: + source_path: ".allspice/examples/input.txt" + output_file_name: "output.txt" + config_file: ".allspice/examples/config.yml" + task_type: "Schematic-Review" + additional_params: '{"SCH_VER":"3"}' + env: + ALLSPICE_TOKEN: ${{ allspice.token }} +``` \ No newline at end of file diff --git a/action.yml b/action.yml index eeea51d..2188337 100644 --- a/action.yml +++ b/action.yml @@ -1,53 +1,52 @@ -name: "Generate BOM" +name: "Hardware DevOps Action" description: > - Generate a BOM for the project using py-allspice and attach it as an artifact - to the run. - - Works for Altium and OrCAD projects. + A generic AllSpice Action Add-on for hardware development tasks such as schematic review, + PCB review, ECO review, and release. This action demonstrates defining parameters + for these tasks and utilizing GitHub context information. inputs: - source_path: + source_file: description: > - Path to the source file from the root of the repo. For Altium projects, - this should be the path to the .PrjPcb file. For OrCAD projects, this - should be the path to the .dsn file. + Path to the source file or directory from the root of the repo. For example, + the path to a schematic or PCB file. required: true output_file_name: description: "Name of the output file" required: true - default: "bom.csv" - columns: + default: "output.txt" + config_file: description: > - A path to a JSON file mapping columns to the attributes they are from. + Path to a configuration file for the task. required: true - group_by: + task_type: description: > - A comma-separated list of columns to group the BOM by. If not present, the - BOM will be flat. - default: "" - variant: + The type of hardware task to perform. Options include 'Schematic-Review', + 'PCB-Review', 'ECO-Review', 'Release'. + default: "Schematic-Review" + additional_params: description: > - The variant of the project to generate the BOM for. If not present, the - BOM will be generated for the default variant. Not supported for OrCAD - projects. - default: "" + Any additional parameters required for the task, provided as a JSON string. + default: "{}" + runs: using: "docker" image: "Dockerfile" args: - - "--source_ref" - - ${{ github.sha }} - - "--allspice_hub_url" - - ${{ github.server_url }} - - "--columns" - - ${{ inputs.columns }} - - "--group_by" - - ${{ inputs.group_by }} - - "--variant" - - ${{ inputs.variant }} + - "--source_file" + - "${{ inputs.source_file}}" - "--output_file" - "${{ github.workspace}}/${{ inputs.output_file_name }}" - - ${{ github.repository }} - - ${{ inputs.source_path }} - env: - ALLSPICE_AUTH_TOKEN: ${{ github.token }} + - "--config_file" + - ${{ inputs.config_file }} + - "--task_type" + - ${{ inputs.task_type }} + - "--additional_params" + - ${{ inputs.additional_params }} + - "--source_ref" + - ${{ allspice.sha }} + - "--server_url" + - ${{ allspice.server_url }} + - "--allspice_token" + - ${{ secrets.ALLSPICE_TOKEN }} +env: + GITHUB_TOKEN: ${{ github.token }} \ No newline at end of file diff --git a/entrypoint.py b/entrypoint.py deleted file mode 100755 index 8b3ed25..0000000 --- a/entrypoint.py +++ /dev/null @@ -1,123 +0,0 @@ -#! /usr/bin/env python3 - -# Generate a BOM from a PrjPcb file. -# For more information, read the README file in this directory. - -import argparse -import csv -import os -import yaml -import sys -from contextlib import ExitStack - -from allspice import AllSpice -from allspice.utils.bom_generation import generate_bom - - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - prog="generate_bom", description="Generate a BOM from a project repository." - ) - parser.add_argument( - "repository", help="The repo containing the project in the form 'owner/repo'" - ) - parser.add_argument( - "source_file", - help=( - "The path to the source file used to generate the BOM. If this is an Altium project, " - "this should be the .PrjPcb file. For an OrCAD project, this should be the .dsn file. " - "Example: 'Archimajor.PrjPcb', 'Schematics/Beagleplay.dsn'." - ), - ) - parser.add_argument( - "--columns", - help=( - "A path to a YAML file mapping columns to the attributes they are from. See the README " - "for more details. Defaults to 'columns.yml'." - ), - default="columns.yml", - ) - parser.add_argument( - "--source_ref", - help=( - "The git reference the BOM should be generated for (eg. branch name, tag name, commit " - "SHA). Defaults to the main branch." - ), - default="main", - ) - parser.add_argument( - "--allspice_hub_url", - help="The URL of your AllSpice Hub instance. Defaults to https://hub.allspice.io.", - ) - parser.add_argument( - "--output_file", - help="The path to the output file. If absent, the CSV will be output to the command line.", - ) - parser.add_argument( - "--group_by", - help=( - "A comma-separated list of columns to group the BOM by. If not present, the BOM will " - "be flat." - ), - ) - parser.add_argument( - "--variant", - help=( - "The variant of the project to generate the BOM for. If not present, the BOM will be " - "generated for the default variant. This is not used for OrCAD projects." - ), - ) - - args = parser.parse_args() - - columns_file = args.columns - columns = {} - try: - with open(columns_file, "r") as f: - columns_data = yaml.safe_load(f.read()) - for column_value in columns_data["columns"]: - columns[column_value["name"]] = column_value["part_attributes"] - except KeyError as e: - print(f"Error: columns file {columns_file} does not seem to be in the right format.") - print("Please refer to the README for more information.") - print(f"Caused by: {e}") - sys.exit(1) - - auth_token = os.environ.get("ALLSPICE_AUTH_TOKEN") - if auth_token is None: - print("Please set the environment variable ALLSPICE_AUTH_TOKEN") - exit(1) - - if args.allspice_hub_url is None: - allspice = AllSpice(token_text=auth_token) - else: - allspice = AllSpice(token_text=auth_token, allspice_hub_url=args.allspice_hub_url) - - repo_owner, repo_name = args.repository.split("/") - repository = allspice.get_repository(repo_owner, repo_name) - group_by = args.group_by.split(",") if args.group_by else None - - print("Generating BOM...", file=sys.stderr) - - bom_rows = generate_bom( - allspice, - repository, - args.source_file, - columns, - group_by=group_by, - ref=args.source_ref if args.source_ref else "main", - variant=args.variant if args.variant else None, - ) - - with ExitStack() as stack: - keys = bom_rows[0].keys() - if args.output_file is not None: - f = stack.enter_context(open(args.output_file, "w")) - writer = csv.DictWriter(f, fieldnames=keys) - else: - writer = csv.DictWriter(sys.stdout, fieldnames=keys) - - writer.writeheader() - writer.writerows(bom_rows) - - print("Generated bom.", file=sys.stderr)