Skip to content

Commit bc7493e

Browse files
committed
feat: testing framework
1 parent c110f27 commit bc7493e

File tree

28 files changed

+345
-59
lines changed

28 files changed

+345
-59
lines changed

.github/workflows/lint.yml

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ permissions:
1414
contents: read
1515

1616
env:
17-
PYTHON_VERSION: "3.14"
18-
CSPELL_VERSION: "8.0.0"
19-
BLACK_VERSION: "24.10.0"
20-
PRETTIER_VERSION: "3.3.3"
21-
COMMITLINT_CLI_VERSION: "20.2.0"
17+
PYTHON_VERSION: '3.14'
18+
CSPELL_VERSION: '8.0.0'
19+
BLACK_VERSION: '24.10.0'
20+
PRETTIER_VERSION: '3.3.3'
21+
COMMITLINT_CLI_VERSION: '20.2.0'
2222

2323
jobs:
2424
install-commitlint:
@@ -84,33 +84,25 @@ jobs:
8484
- name: Run Prettier check
8585
run: ./node_modules/.bin/prettier --check .
8686

87-
install-black:
87+
run-black:
8888
runs-on: ubuntu-latest
8989
steps:
9090
- *checkout-code
9191

92-
- &create-black-lockfile
93-
name: Create Black lockfile
94-
run: echo "black==${{ env.BLACK_VERSION }}" > black-requirements.txt
95-
96-
- &setup-black
97-
name: Setup Python
92+
- name: Setup Python
9893
uses: actions/setup-python@v6
9994
with:
100-
cache: "pip"
101-
python-version: "${{ env.PYTHON_VERSION }}"
102-
cache-dependency-path: black-requirements.txt
95+
python-version: '${{ env.PYTHON_VERSION }}'
10396

104-
- name: Install Black
105-
run: pip install -r black-requirements.txt
97+
- name: Cache Black
98+
uses: actions/cache@v4
99+
id: cache-black
100+
with:
101+
path: ~/.cache/pip
102+
key: v1-black-${{ env.BLACK_VERSION }}
106103

107-
run-black:
108-
runs-on: ubuntu-latest
109-
needs: install-black
110-
steps:
111-
- *checkout-code
112-
- *create-black-lockfile
113-
- *setup-black
104+
- name: Install Black
105+
run: pip install black==${{ env.BLACK_VERSION }}
114106

115107
- name: Run Black check
116108
run: black --check .

.github/workflows/test.yml

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
name: Test Actions
2+
3+
on:
4+
push:
5+
branches-ignore:
6+
- main
7+
workflow_call:
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: true
12+
13+
env:
14+
PYTHON_VERSION: '3.14'
15+
16+
jobs:
17+
python-unit-tests-actions:
18+
name: Python Unit Tests (GitHub Actions)
19+
runs-on: ubuntu-latest
20+
steps:
21+
- &checkout-code
22+
name: Checkout code
23+
uses: actions/checkout@v5
24+
25+
- &setup-python
26+
name: Setup Python
27+
uses: actions/setup-python@v5
28+
with:
29+
python-version: ${{ env.PYTHON_VERSION }}
30+
31+
- name: Install dependencies
32+
working-directory: actions
33+
run: |
34+
python -m pip install --upgrade pip
35+
pip install -r requirements.txt
36+
37+
- name: Run unit tests
38+
working-directory: actions
39+
run: python -m unittest discover -s . -p "test_*.py" -v
40+
41+
python-unit-tests-terraform:
42+
name: Python Unit Tests (Terraform)
43+
runs-on: ubuntu-latest
44+
steps:
45+
- *checkout-code
46+
- *setup-python
47+
48+
- name: Run unit tests
49+
working-directory: terraform
50+
run: python -m unittest discover -s . -p "test_*.py" -v
51+
52+
test-actions:
53+
name: Test GitHub Actions Updater
54+
runs-on: ubuntu-latest
55+
steps:
56+
- *checkout-code
57+
58+
- name: Test actions updater
59+
uses: ./actions
60+
with:
61+
dry-run: 'true'
62+
file-glob: 'actions/test/.github/**/*.yml'
63+
64+
test-circleci-orbs:
65+
name: Test CircleCI Orbs Updater
66+
runs-on: ubuntu-latest
67+
steps:
68+
- *checkout-code
69+
70+
- name: Test CircleCI orbs updater
71+
uses: ./circleci-orbs
72+
with:
73+
dry-run: 'true'
74+
circleci-config-file: 'circleci-orbs/test/.circleci/config.yml'
75+
76+
test-golang:
77+
name: Test Go Dependencies Updater
78+
runs-on: ubuntu-latest
79+
steps:
80+
- *checkout-code
81+
82+
- name: Setup test fixtures
83+
run: |
84+
cp golang/test/go.mod .
85+
cp golang/test/go.sum .
86+
87+
- name: Test Go dependencies updater
88+
uses: ./golang
89+
with:
90+
dry-run: 'true'
91+
92+
test-npm:
93+
name: Test NPM Dependencies Updater
94+
runs-on: ubuntu-latest
95+
steps:
96+
- *checkout-code
97+
98+
- name: Setup test fixtures
99+
run: |
100+
cp npm/test/package.json .
101+
cp npm/test/package-lock.json .
102+
cp npm/test/.nvmrc .
103+
104+
- name: Test NPM dependencies updater
105+
uses: ./npm
106+
with:
107+
dry-run: 'true'
108+
109+
test-terraform:
110+
name: Test Terraform Dependencies Updater
111+
runs-on: ubuntu-latest
112+
steps:
113+
- *checkout-code
114+
115+
- name: Test Terraform dependencies updater
116+
uses: ./terraform
117+
with:
118+
dry-run: 'true'
119+
working-dir: 'terraform/test'
120+
var-file-path: 'test.tfvars'

actions/README.md

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ python cli.py --root /path/to/repo --file-glob '.github/**/*.yml' --prefixes 'ac
3737
## :gear: Inputs
3838

3939
| Input | Description | Required | Default |
40-
|------------------|-----------------------------------------------------|--------------------|-------------------------|
40+
| ---------------- | --------------------------------------------------- | ------------------ | ----------------------- |
4141
| `base-branch` | Base branch for the pull request | :white_check_mark: | `main` |
4242
| `token` | GitHub token for authentication | :x: | `${{ github.token }}` |
4343
| `branch-prefix` | Prefix for the update branch | :x: | `update-actions` |
@@ -52,9 +52,3 @@ python cli.py --root /path/to/repo --file-glob '.github/**/*.yml' --prefixes 'ac
5252
- Workflow files must be under `.github` and match the configured `file-glob`
5353
- The action requires write permissions to create branches and pull requests
5454
- GitHub CLI must be available in the runner environment
55-
56-
## :test_tube: Tests
57-
58-
```bash
59-
python -m unittest discover -s tests
60-
```

actions/action.yml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,16 @@ inputs:
4343
description: 'Whether a successful PR should be automatically merged'
4444
default: 'false'
4545

46+
dry-run:
47+
required: false
48+
description: 'Run without creating a PR (for testing purposes)'
49+
default: 'false'
50+
4651
runs:
4752
using: 'composite'
4853
steps:
4954
- name: Checkout and setup
55+
if: inputs.dry-run != 'true'
5056
uses: alchemaxinc/composite-toolbox/checkout-and-setup@v1
5157
with:
5258
token: ${{ inputs.token }}
@@ -75,13 +81,14 @@ runs:
7581
7682
- name: Check for changes
7783
id: check_changes
84+
if: inputs.dry-run != 'true'
7885
uses: alchemaxinc/composite-toolbox/check-changes@v1
7986
with:
8087
files: '.github'
8188

8289
- name: Create Pull Request
8390
id: open-pr
84-
if: steps.check_changes.outputs.has_changes == 'true'
91+
if: inputs.dry-run != 'true' && steps.check_changes.outputs.has_changes == 'true'
8592
uses: alchemaxinc/composite-toolbox/create-pr@v1
8693
with:
8794
token: ${{ inputs.token }}
@@ -113,7 +120,7 @@ runs:
113120
*Generated by the GitHub Actions Update action* :sparkles:
114121
115122
- name: Auto-merge Pull Request
116-
if: inputs.auto-merge == 'true' && steps.open-pr.outcome == 'success'
123+
if: inputs.dry-run != 'true' && inputs.auto-merge == 'true' && steps.open-pr.outcome == 'success'
117124
uses: alchemaxinc/composite-toolbox/merge-pr@v1
118125
with:
119126
token: ${{ inputs.token }}

actions/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
PyYAML==6.0.1
1+
ruamel.yaml==0.18.5
22
semver==3.0.2
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
name: Test Workflow
2+
3+
on: [push]
4+
5+
jobs:
6+
test:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: actions/checkout@v3

actions/update_actions/scanner.py

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
from __future__ import annotations
22

3-
import re
43
import sys
4+
from io import StringIO
55
from pathlib import Path
66

7-
import yaml
8-
9-
USES_PATTERN = re.compile(r"(^\s*-?\s*uses:\s*)([^@\s]+)@([^\s#]+)", re.MULTILINE)
7+
from ruamel.yaml import YAML
108

119

1210
def find_uses(obj) -> list[str]:
11+
"""Recursively find all 'uses' values in a YAML structure."""
1312
found = []
1413
if isinstance(obj, dict):
1514
if isinstance(obj.get("steps"), list):
@@ -24,10 +23,45 @@ def find_uses(obj) -> list[str]:
2423
return found
2524

2625

26+
def update_uses_in_structure(obj, upgrades: dict[tuple[str, str], str]) -> bool:
27+
"""
28+
Recursively update 'uses' values in a YAML structure.
29+
Returns True if any updates were made.
30+
"""
31+
updated = False
32+
if isinstance(obj, dict):
33+
if isinstance(obj.get("steps"), list):
34+
for step in obj["steps"]:
35+
if isinstance(step, dict) and isinstance(step.get("uses"), str):
36+
use = step["uses"]
37+
if "@" in use:
38+
repo, tag = use.split("@", 1)
39+
new_tag = upgrades.get((repo, tag))
40+
if new_tag:
41+
step["uses"] = f"{repo}@{new_tag}"
42+
updated = True
43+
for value in obj.values():
44+
if update_uses_in_structure(value, upgrades):
45+
updated = True
46+
elif isinstance(obj, list):
47+
for item in obj:
48+
if update_uses_in_structure(item, upgrades):
49+
updated = True
50+
return updated
51+
52+
2753
def find_uses_in_file(path: Path) -> tuple[list[str], str]:
54+
"""Parse a YAML file and find all 'uses' entries."""
2855
text = path.read_text(encoding="utf-8")
56+
yaml = YAML()
57+
yaml.preserve_quotes = True
58+
yaml.default_flow_style = False
59+
yaml.map_indent = 2
60+
yaml.sequence_indent = 4
61+
yaml.sequence_dash_offset = 2
62+
2963
try:
30-
docs = list(yaml.safe_load_all(text))
64+
docs = list(yaml.load_all(text))
3165
except Exception as exc:
3266
print(
3367
f"::warning file={path}::Failed to parse YAML: {exc}",
@@ -44,17 +78,42 @@ def find_uses_in_file(path: Path) -> tuple[list[str], str]:
4478

4579

4680
def collect_workflow_files(root: Path, file_glob: str) -> list[Path]:
81+
"""Collect all workflow files matching the glob pattern."""
4782
return sorted(root.glob(file_glob))
4883

4984

5085
def apply_updates(text: str, upgrades: dict[tuple[str, str], str]) -> str:
51-
def replace_match(match):
52-
repo = match.group(2)
53-
tag = match.group(3)
86+
"""
87+
Apply updates to a YAML workflow file using ruamel.yaml.
88+
This preserves formatting and comments.
89+
"""
90+
yaml = YAML()
91+
yaml.preserve_quotes = True
92+
yaml.default_flow_style = False
93+
yaml.map_indent = 2
94+
yaml.sequence_indent = 4
95+
yaml.sequence_dash_offset = 2
96+
97+
try:
98+
docs = list(yaml.load_all(text))
99+
except Exception:
100+
# If parsing fails, return original text unchanged
101+
return text
102+
103+
# Check if any updates are needed
104+
any_updates = False
105+
for doc in docs:
106+
if doc is not None and update_uses_in_structure(doc, upgrades):
107+
any_updates = True
108+
109+
if not any_updates:
110+
return text
54111

55-
new_tag = upgrades.get((repo, tag))
56-
if new_tag:
57-
return f"{match.group(1)}{repo}@{new_tag}"
58-
return match.group(0)
112+
# Write back with preserved formatting
113+
output = StringIO()
114+
if len(docs) == 1:
115+
yaml.dump(docs[0], output)
116+
else:
117+
yaml.dump_all(docs, output)
59118

60-
return USES_PATTERN.sub(replace_match, text)
119+
return output.getvalue()

actions/update_actions/tests/__init__.py

Whitespace-only changes.
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)