Skip to content

Commit f359db8

Browse files
ahmdmhdjdacoello
authored andcommitted
feat(s2dm-compose): implement a compose action
Signed-off-by: Ahmed Mohamed <ahmed.mohamed@motius.de>
1 parent 53d5172 commit f359db8

File tree

3 files changed

+331
-10
lines changed

3 files changed

+331
-10
lines changed

actions/s2dm-compose/README.md

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# S2DM Compose Action
2+
3+
GitHub Action for composing GraphQL schema files and optionally creating releases.
4+
5+
## Features
6+
7+
- Compose multiple GraphQL schema files into a single schema
8+
- Optional automated release creation with version bumping
9+
- Automatic change detection to skip unnecessary releases
10+
- Integration with `.bumpversion.toml` for version management
11+
12+
## Usage
13+
14+
### Basic Example (Compose Only)
15+
16+
Compose GraphQL schemas without creating a release:
17+
18+
```yaml
19+
name: Compose Schema
20+
on:
21+
push:
22+
branches:
23+
- main
24+
paths:
25+
- 'spec/**'
26+
27+
jobs:
28+
compose:
29+
runs-on: ubuntu-latest
30+
steps:
31+
- name: Checkout repository
32+
uses: actions/checkout@v4
33+
34+
- name: Compose GraphQL schema
35+
uses: COVESA/s2dm/actions/s2dm-compose@main
36+
with:
37+
repository-path: .
38+
spec-path: spec
39+
```
40+
41+
### With Release Creation
42+
43+
Compose schemas and create a GitHub release when changes are detected:
44+
45+
```yaml
46+
name: Compose and Release
47+
on:
48+
push:
49+
branches:
50+
- main
51+
paths:
52+
- 'spec/**'
53+
54+
jobs:
55+
compose-and-release:
56+
runs-on: ubuntu-latest
57+
permissions:
58+
contents: write
59+
steps:
60+
- name: Checkout repository
61+
uses: actions/checkout@v4
62+
with:
63+
fetch-depth: 0 # Required for version bumping
64+
65+
- name: Compose and release
66+
uses: COVESA/s2dm/actions/s2dm-compose@main
67+
with:
68+
repository-path: .
69+
spec-path: spec
70+
create-release: 'true'
71+
github-token: ${{ secrets.GITHUB_TOKEN }}
72+
```
73+
74+
### Using Outputs
75+
76+
```yaml
77+
- name: Compose GraphQL schema
78+
id: compose
79+
uses: COVESA/s2dm/actions/s2dm-compose@main
80+
with:
81+
repository-path: .
82+
spec-path: spec
83+
create-release: 'true'
84+
github-token: ${{ secrets.GITHUB_TOKEN }}
85+
86+
- name: Print outputs
87+
run: |
88+
echo "Schema path: ${{ steps.compose.outputs.composed-schema-path }}"
89+
echo "Version bump: ${{ steps.compose.outputs.version-bump }}"
90+
echo "Latest tag: ${{ steps.compose.outputs.latest-tag }}"
91+
92+
- name: Use the composed schema
93+
run: |
94+
# The schema is available at the path from the output
95+
cat "${{ steps.compose.outputs.composed-schema-path }}"
96+
```
97+
98+
## Inputs
99+
100+
| Input | Description | Required | Default |
101+
|-------|-------------|----------|---------|
102+
| `repository-path` | Path to the git repository root | Yes | - |
103+
| `spec-path` | Path to the spec directory relative to repository root | No | `spec` |
104+
| `create-release` | Whether to create a GitHub release | No | `false` |
105+
| `github-token` | GitHub token for creating releases | No* | `''` |
106+
| `s2dm-path` | Path where S2DM repository will be checked out | No | `s2dm` |
107+
108+
\* Required when `create-release` is `true`
109+
110+
## Outputs
111+
112+
| Output | Description |
113+
|--------|-------------|
114+
| `composed-schema-path` | Path to the composed schema file |
115+
| `version-bump` | Type of version bump (major, minor, patch, none) |
116+
| `latest-tag` | The latest git tag after release |
117+
118+
## Requirements
119+
120+
### For Basic Usage
121+
122+
- GraphQL schema files in the specified directory
123+
124+
### For Release Creation
125+
126+
- `.bumpversion.toml` file in the repository root
127+
- `contents: write` permission for the workflow
128+
- `fetch-depth: 0` when checking out the repository (for version bumping)
129+
130+
## How It Works
131+
132+
### Compose Mode (create-release: false)
133+
134+
1. Checks out the S2DM repository
135+
2. Installs dependencies
136+
3. Composes GraphQL schemas into a single file at a temporary location
137+
4. Outputs the absolute path to the composed schema
138+
139+
### Compose + Release Mode (create-release: true)
140+
141+
1. Performs all compose mode steps
142+
2. Validates prerequisites (`.bumpversion.toml`, `github-token`)
143+
3. Downloads the latest release (if it exists)
144+
4. Compares the new schema with the previous release
145+
5. Determines version bump type (major, minor, patch, none)
146+
6. If changes detected:
147+
- Bumps version using `.bumpversion.toml`
148+
- Pushes new tag
149+
- Creates GitHub release with the composed schema
150+
7. If no changes detected:
151+
- Skips release creation

actions/s2dm-compose/action.yml

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
name: 'S2DM Compose'
2+
description: 'Compose GraphQL schema files and optionally create a release'
3+
author: 'COVESA'
4+
5+
inputs:
6+
repository-path:
7+
description: 'Path to the git repository root'
8+
required: true
9+
10+
spec-path:
11+
description: 'Path to the spec directory relative to the root of the repository'
12+
required: false
13+
default: 'spec'
14+
15+
create-release:
16+
description: 'Whether to create a GitHub release with the composed schema'
17+
required: false
18+
default: 'false'
19+
20+
github-token:
21+
description: 'GitHub token for creating releases (required if create-release is true)'
22+
required: false
23+
default: ''
24+
25+
s2dm-path:
26+
description: 'Path where S2DM repository will be checked out'
27+
required: false
28+
default: 's2dm'
29+
30+
outputs:
31+
composed-schema-path:
32+
description: 'Path to the composed schema file'
33+
value: ${{ steps.compose.outputs.SCHEMA_PATH }}
34+
35+
version-bump:
36+
description: 'Type of version bump (major, minor, patch, none)'
37+
value: ${{ steps.version-check.outputs.VERSION_BUMP }}
38+
39+
latest-tag:
40+
description: 'The latest git tag after release'
41+
value: ${{ steps.get-latest-tag.outputs.LATEST_TAG }}
42+
43+
runs:
44+
using: "composite"
45+
steps:
46+
- name: Checkout S2DM repository
47+
uses: actions/checkout@v4
48+
with:
49+
repository: COVESA/s2dm
50+
path: ${{ inputs.s2dm-path }}
51+
52+
- name: Install Python
53+
uses: actions/setup-python@v5
54+
with:
55+
python-version: '3.13'
56+
57+
- name: Install uv
58+
uses: astral-sh/setup-uv@v4
59+
with:
60+
version: "latest"
61+
62+
- name: Install S2DM dependencies
63+
working-directory: ${{ inputs.s2dm-path }}
64+
shell: bash
65+
run: |
66+
uv sync --frozen
67+
68+
- name: Activate venv
69+
shell: bash
70+
run: |
71+
echo "${{ github.workspace }}/${{ inputs.s2dm-path }}/.venv/bin" >> $GITHUB_PATH
72+
73+
- name: Verify S2DM installation
74+
shell: bash
75+
run: |
76+
s2dm --help
77+
78+
- name: Prepare temp directory
79+
shell: bash
80+
run: |
81+
mkdir -p ${{ github.workspace }}/.s2dm-compose
82+
83+
- name: Compose GraphQL schema
84+
id: compose
85+
working-directory: ${{ inputs.repository-path }}
86+
shell: bash
87+
run: |
88+
s2dm compose -s ${{ inputs.spec-path }} -o ${{ github.workspace }}/.s2dm-compose/schema.graphql
89+
echo "SCHEMA_PATH=${{ github.workspace }}/.s2dm-compose/schema.graphql" >> "$GITHUB_OUTPUT"
90+
91+
- name: Validate release prerequisites
92+
if: inputs.create-release == 'true'
93+
working-directory: ${{ inputs.repository-path }}
94+
shell: bash
95+
run: |
96+
if [ ! -f ".bumpversion.toml" ]; then
97+
echo "ERROR: .bumpversion.toml not found but create-release is true"
98+
exit 1
99+
fi
100+
if [ -z "${{ inputs.github-token }}" ]; then
101+
echo "ERROR: github-token is required when create-release is true"
102+
exit 1
103+
fi
104+
105+
- name: Download latest release
106+
if: inputs.create-release == 'true'
107+
working-directory: ${{ inputs.repository-path }}
108+
env:
109+
GITHUB_TOKEN: ${{ inputs.github-token }}
110+
shell: bash
111+
run: |
112+
mkdir -p ${{ github.workspace }}/.previous-release
113+
gh release download --pattern "${{ inputs.output-path }}" --dir ${{ github.workspace }}/.previous-release || echo "No previous release found"
114+
115+
- name: Check version bump
116+
if: inputs.create-release == 'true'
117+
id: version-check
118+
shell: bash
119+
run: |
120+
VERSION_BUMP=major
121+
CONTINUE=false
122+
if [ -f "${{ github.workspace }}/.previous-release/${{ inputs.output-path }}" ]; then
123+
echo "Previous release found, checking for changes"
124+
VERSION_BUMP=$(s2dm check version-bump -s ${{ inputs.spec-path }} -p ${{ github.workspace }}/.previous-release/${{ inputs.output-path }} --output-type | tail -n 1)
125+
if [[ "$VERSION_BUMP" != "none" ]]; then
126+
echo "Changes detected: $VERSION_BUMP"
127+
CONTINUE=true
128+
else
129+
echo "No changes detected, skipping release"
130+
fi
131+
else
132+
echo "No previous release found, creating initial release"
133+
CONTINUE=true
134+
fi
135+
echo "VERSION_BUMP=$VERSION_BUMP" >> "$GITHUB_OUTPUT"
136+
echo "CONTINUE=$CONTINUE" >> "$GITHUB_OUTPUT"
137+
138+
- name: Bump version and push tags
139+
if: inputs.create-release == 'true' && steps.version-check.outputs.CONTINUE == 'true'
140+
working-directory: ${{ inputs.repository-path }}
141+
shell: bash
142+
run: |
143+
git config --local user.email "s2dm-automation-action@covesa.global"
144+
git config --local user.name "S2DM Automation Action"
145+
bump-my-version bump ${{ steps.version-check.outputs.VERSION_BUMP }} --config-file ./.bumpversion.toml
146+
git push origin ${{ github.ref_name }}
147+
git push origin --tags
148+
149+
- name: Get latest tag
150+
if: inputs.create-release == 'true' && steps.version-check.outputs.CONTINUE == 'true'
151+
id: get-latest-tag
152+
working-directory: ${{ inputs.repository-path }}
153+
shell: bash
154+
run: |
155+
LATEST_TAG=$(git describe --tags --abbrev=0)
156+
echo "LATEST_TAG=$LATEST_TAG" >> "$GITHUB_OUTPUT"
157+
158+
- name: Create release
159+
if: inputs.create-release == 'true' && steps.version-check.outputs.CONTINUE == 'true'
160+
working-directory: ${{ inputs.repository-path }}
161+
env:
162+
GITHUB_TOKEN: ${{ inputs.github-token }}
163+
shell: bash
164+
run: |
165+
LATEST_TAG="${{ steps.get-latest-tag.outputs.LATEST_TAG }}"
166+
gh release create "${LATEST_TAG}" \
167+
--title "Release ${LATEST_TAG}" \
168+
--notes "This release contains the composed GraphQL schema." \
169+
${{ github.workspace }}/.s2dm-compose/schema.graphql

tests/test_schema_loader.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@ def test_reference_directive_only_applied_to_supported_locations(spec_directory:
4141
assert 'type TestObject @reference(source: "test.graphql")' in result
4242
assert 'enum TestEnum @reference(source: "test.graphql")' in result
4343

44-
assert 'scalar TestScalar @reference' not in result
45-
assert 'interface TestInterface @reference' not in result
46-
assert 'input TestInput @reference' not in result
47-
assert 'union TestUnion @reference' not in result
44+
assert "scalar TestScalar @reference" not in result
45+
assert "interface TestInterface @reference" not in result
46+
assert "input TestInput @reference" not in result
47+
assert "union TestUnion @reference" not in result
4848

4949

5050
def test_reference_directive_not_applied_when_source_argument_missing() -> None:
@@ -66,8 +66,8 @@ def test_reference_directive_not_applied_when_source_argument_missing() -> None:
6666

6767
result = print_schema_with_directives_preserved(schema, source_map)
6868

69-
assert 'type TestObject @reference' not in result
70-
assert 'enum TestEnum @reference' not in result
69+
assert "type TestObject @reference" not in result
70+
assert "enum TestEnum @reference" not in result
7171

7272

7373
def test_reference_directive_not_applied_when_directive_missing() -> None:
@@ -87,9 +87,9 @@ def test_reference_directive_not_applied_when_directive_missing() -> None:
8787

8888
result = print_schema_with_directives_preserved(schema, source_map)
8989

90-
assert '@reference' not in result
91-
assert 'type TestObject' in result
92-
assert 'enum TestEnum' in result
90+
assert "@reference" not in result
91+
assert "type TestObject" in result
92+
assert "enum TestEnum" in result
9393

9494

9595
def test_reference_directive_not_duplicated_when_already_present() -> None:
@@ -120,7 +120,8 @@ def test_reference_directive_not_duplicated_when_already_present() -> None:
120120
def test_reference_directive_with_all_standard_locations(spec_directory: Path) -> None:
121121
"""Test @reference with all standard type locations from spec."""
122122
schema_str = """
123-
directive @reference(uri: String, source: String, versionTag: String) on OBJECT | INTERFACE | UNION | ENUM | ENUM_VALUE | SCALAR | INPUT_OBJECT | FIELD_DEFINITION
123+
directive @reference(uri: String, source: String, versionTag: String)
124+
on OBJECT | INTERFACE | UNION | ENUM | ENUM_VALUE | SCALAR | INPUT_OBJECT | FIELD_DEFINITION
124125
125126
type TestObject { field: String }
126127
interface TestInterface { field: String }

0 commit comments

Comments
 (0)