Skip to content

Commit 984574d

Browse files
authored
refactor: separate workflows (#39)
1 parent cda204d commit 984574d

8 files changed

Lines changed: 342 additions & 96 deletions

File tree

.github/workflows/README.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# GitHub Actions Workflows
2+
3+
This directory contains the CI/CD workflows for the SAP Cloud SDK for Python.
4+
5+
## Workflow Structure
6+
7+
The CI pipeline is split into three independent workflows for optimal performance and security:
8+
9+
### 1. Code Quality Checks (`checks.yaml`)
10+
11+
**Trigger**: `pull_request` (runs on all PRs)
12+
**Purpose**: Fast feedback on code style and type safety
13+
**Jobs**:
14+
- `lint` - Ruff linter checks
15+
- `format` - Ruff formatter checks
16+
- `typecheck` - ty type checker
17+
18+
**Parallelization**: All jobs run in parallel (~1-2 min total)
19+
20+
**Security**:
21+
- Uses `pull_request` trigger (read-only by default)
22+
- ✅ SAFE: Only runs static analysis tools (no code execution)
23+
- ❌ NO secrets or write permissions
24+
25+
**Fork-friendly**: Contributors get immediate feedback on code quality checks.
26+
27+
### 2. Tests & Coverage (`test.yaml`)
28+
29+
**Trigger**: `pull_request` (runs on all PRs)
30+
**Purpose**: Run tests and report coverage
31+
**Jobs**:
32+
- `test` - Unit tests with coverage reporting
33+
34+
**Permissions**:
35+
- `contents: read` - Read-only access
36+
37+
**Security**:
38+
- Uses `pull_request` trigger (read-only)
39+
- Coverage visible in workflow summary
40+
41+
### 3. Build & Package (`build.yaml`)
42+
43+
**Trigger**: `pull_request` (requires approval for fork PRs)
44+
**Purpose**: Build and verify distribution packages
45+
**Jobs**:
46+
- `build` - Creates wheel and source distributions
47+
48+
**Artifacts**:
49+
- Uploads built packages for 7 days
50+
51+
## Workflow Execution Flow
52+
53+
```
54+
Fork PR opened
55+
56+
┌─────────────────────────────────────────┐
57+
│ checks.yaml (auto-runs) │
58+
│ ├─ lint (parallel) │
59+
│ ├─ format (parallel) │
60+
│ └─ typecheck (parallel) │
61+
└─────────────────────────────────────────┘
62+
↓ (1-2 min)
63+
✓ Quick feedback to contributor
64+
65+
↓ (Runs in parallel)
66+
67+
┌─────────────────────────────────────────┐
68+
│ test.yaml (auto-runs) │
69+
│ └─ test (with coverage) │
70+
└─────────────────────────────────────────┘
71+
↓ (3-5 min)
72+
73+
↓ (Maintainer clicks "Approve and run")
74+
75+
┌─────────────────────────────────────────┐
76+
│ build.yaml (requires approval) │
77+
│ └─ build (package creation) │
78+
└─────────────────────────────────────────┘
79+
↓ (1-2 min)
80+
81+
✓ All checks passed, ready to merge
82+
```
83+
84+
## Benefits
85+
86+
### For Contributors
87+
- **Immediate feedback** on code style and tests
88+
- **Faster iteration** - fix issues quickly
89+
- **Clear separation** of concerns in CI status
90+
91+
### For Maintainers
92+
- **Parallel execution** - faster CI overall (~3-5 min vs ~8-10 min)
93+
- **Security** - limited permissions for all workflows
94+
- **Selective approval** - only approve build jobs
95+
96+
## Security Considerations
97+
98+
### Why Build Requires Approval
99+
100+
The `build.yaml` workflow requires approval because it:
101+
- Creates distribution packages
102+
- May access secrets in the future (e.g., PyPI tokens)
103+
104+
## Local Development
105+
106+
Run checks locally before pushing:
107+
108+
```bash
109+
# Lint
110+
uv run ruff check .
111+
112+
# Format
113+
uv run ruff format .
114+
115+
# Type check
116+
uv run ty check .
117+
118+
# Tests
119+
uv run pytest -m "not integration"
120+
121+
# Build
122+
uv build
123+
```
124+
125+
## Troubleshooting
126+
127+
### Fork PR workflows not running?
128+
129+
1. **checks.yaml/test.yaml not running**: Check if Actions are enabled in repository settings
130+
2. **build.yaml not running**: Requires manual approval for fork PRs - look for "Approve and run" button
131+
132+
### All workflows require approval?
133+
134+
This means fork PR workflows are disabled in repository settings:
135+
1. Go to Settings → Actions → General
136+
2. Enable "Fork pull request workflows from outside collaborators"
137+
3. Select "Require approval for first-time contributors"
138+
139+
## References
140+
141+
- [GitHub Actions: Events that trigger workflows](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows)
142+
- [Keeping your GitHub Actions secure](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions)
143+
- [Using pull_request_target safely](https://securitylab.github.com/research/github-actions-preventing-pwn-requests/)

.github/workflows/build.yaml

Lines changed: 5 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
name: Build
1+
name: Build & Package
22

3+
# This workflow builds and packages the SDK
4+
# Requires approval for fork PRs (uses same approval as tests)
35
on:
46
pull_request:
57
branches:
@@ -11,15 +13,13 @@ on:
1113
env:
1214
PYTHON_VERSION: "3.11"
1315
SDK_DIST_NAME: "sap_cloud_sdk"
14-
COVERAGE_THRESHOLD: "80"
1516

1617
jobs:
1718
build:
1819
name: Build SDK
1920
runs-on: ${{ vars.RUNNER && fromJSON(vars.RUNNER) || 'ubuntu-latest' }}
2021
permissions:
2122
contents: read
22-
pull-requests: write
2323
steps:
2424
- name: "Checkout Repository"
2525
uses: actions/checkout@v4
@@ -34,98 +34,10 @@ jobs:
3434
enable-cache: true
3535

3636
- name: "Set up Python"
37-
uses: actions/setup-python@v4
37+
uses: actions/setup-python@v6
3838
with:
3939
python-version: ${{ env.PYTHON_VERSION }}
4040

41-
- name: "Run Ruff Linter"
42-
run: uv run ruff check --output-format=github .
43-
44-
- name: "Run Ruff Formatter Check"
45-
run: uv run ruff format --check --diff .
46-
47-
- name: "Run ty Type Check"
48-
run: uv run ty check --output-format github --python-version ${{ env.PYTHON_VERSION }} .
49-
50-
- name: "Run Unit Tests with Coverage"
51-
run: uv run pytest -m "not integration" --cov=src/sap_cloud_sdk --cov-report=xml --cov-report=html --cov-report=term-missing
52-
53-
- name: "Upload Coverage Report"
54-
uses: actions/upload-artifact@v4
55-
if: contains(github.server_url, 'github.com')
56-
with:
57-
name: coverage-report
58-
path: |
59-
coverage.xml
60-
htmlcov/
61-
retention-days: 30
62-
63-
- name: "Coverage Report Summary"
64-
run: |
65-
echo "## 📊 Coverage Report" >> $GITHUB_STEP_SUMMARY
66-
echo "" >> $GITHUB_STEP_SUMMARY
67-
uv run coverage report --format=markdown --skip-empty >> $GITHUB_STEP_SUMMARY
68-
69-
- name: "Post Coverage Comment on PR"
70-
if: github.event_name == 'pull_request'
71-
env:
72-
GH_TOKEN: ${{ github.token }}
73-
run: |
74-
# Extract unique module paths from changed files
75-
MODULE_PATHS=$(git diff --name-only "${{ github.event.pull_request.base.sha }}" "${{ github.event.pull_request.head.sha }}" \
76-
| grep -E '^src/sap_cloud_sdk/[^/]+/.*\.py$' \
77-
| grep -vE '__pycache__' \
78-
| sed -E -e 's|^(src/sap_cloud_sdk/core)/.*|\1|' -e 't' -e 's|^(src/sap_cloud_sdk/[^/]+)/.*|\1|' \
79-
| sort -u)
80-
81-
if [ -z "$MODULE_PATHS" ]; then
82-
echo "No module-level Python files changed, skipping coverage report"
83-
exit 0
84-
fi
85-
86-
# Initialize tracking
87-
FAILED_MODULES=()
88-
COMMENT_BODY="## 📊 Coverage Report by Module"$'\n\n'
89-
90-
# Process each module
91-
while IFS= read -r module_path; do
92-
echo "Checking coverage for $module_path..."
93-
94-
# Generate coverage report
95-
MODULE_COVERAGE=$(uv run coverage report \
96-
--include="$module_path/*.py,$module_path/**/*.py" \
97-
--format=markdown \
98-
--skip-empty \
99-
--skip-covered 2>/dev/null || echo "No coverage data for $module_path")
100-
101-
# Check if coverage meets threshold
102-
if uv run coverage report \
103-
--include="$module_path/*.py,$module_path/**/*.py" \
104-
--skip-empty \
105-
--fail-under=${{ env.COVERAGE_THRESHOLD }} >/dev/null 2>&1; then
106-
COMMENT_BODY+="### \`$module_path\` ✅"$'\n\n'"$MODULE_COVERAGE"$'\n\n'
107-
else
108-
COMMENT_BODY+="### \`$module_path\` ❌"$'\n\n'"$MODULE_COVERAGE"$'\n\n'
109-
FAILED_MODULES+=("$module_path")
110-
fi
111-
done <<< "$MODULE_PATHS"
112-
113-
# Add overall status
114-
COMMENT_BODY+="---"$'\n\n'
115-
if [ ${#FAILED_MODULES[@]} -eq 0 ]; then
116-
COMMENT_BODY+="✅ **All changed modules meet the ${{ env.COVERAGE_THRESHOLD }}% coverage threshold!**"
117-
else
118-
COMMENT_BODY+="❌ **Coverage check failed!** The following modules are below ${{ env.COVERAGE_THRESHOLD }}% coverage: \`$(IFS=', '; echo "${FAILED_MODULES[*]}")\`"
119-
fi
120-
121-
COMMENT_BODY+=$'\n\n'"---"$'\n'"*Coverage report from commit [\`${GITHUB_SHA:0:7}\`](${{ github.event.pull_request.head.repo.html_url }}/commit/${{ github.event.pull_request.head.sha }})*"
122-
123-
# Post comment on PR
124-
echo "$COMMENT_BODY" | gh pr comment ${{ github.event.pull_request.number }} --body-file -
125-
126-
# Exit with failure if any module failed
127-
[ ${#FAILED_MODULES[@]} -eq 0 ] || exit 1
128-
12941
- name: "Build Packages"
13042
run: uv build
13143

@@ -158,4 +70,4 @@ jobs:
15870
with:
15971
name: ${{ env.SDK_DIST_NAME }}
16072
path: dist/
161-
retention-days: 7
73+
retention-days: 7

.github/workflows/checks.yaml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Code Quality Checks
2+
3+
# This workflow runs static analysis that doesn't require secrets
4+
on:
5+
pull_request:
6+
types: [opened, synchronize, reopened]
7+
push:
8+
branches:
9+
- main
10+
11+
env:
12+
PYTHON_VERSION: "3.11"
13+
14+
jobs:
15+
checks:
16+
name: Code Quality Checks
17+
runs-on: ubuntu-latest
18+
permissions:
19+
contents: read
20+
steps:
21+
- name: "Checkout PR Code"
22+
uses: actions/checkout@v4
23+
24+
- name: "Install uv"
25+
uses: astral-sh/setup-uv@v4
26+
with:
27+
version: "latest"
28+
enable-cache: true
29+
30+
- name: "Set up Python"
31+
uses: actions/setup-python@v6
32+
with:
33+
python-version: ${{ env.PYTHON_VERSION }}
34+
35+
- name: "Run Ruff Linter"
36+
run: uv run ruff check --output-format=github .
37+
38+
- name: "Run Ruff Formatter Check"
39+
run: uv run ruff format --check --diff .
40+
41+
- name: "Run ty Type Check"
42+
run: uv run ty check --output-format github --python-version ${{ env.PYTHON_VERSION }} .

.github/workflows/integration-tests.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@ on:
1919
jobs:
2020
integration-tests:
2121
runs-on: ${{ vars.RUNNER && fromJSON(vars.RUNNER) || 'ubuntu-latest' }}
22-
22+
permissions:
23+
contents: read
2324
steps:
2425
- name: Checkout code
2526
uses: actions/checkout@v4
2627

2728
- name: Set up Python
28-
uses: actions/setup-python@v5
29+
uses: actions/setup-python@v6
2930
with:
3031
python-version: '3.11'
3132

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
uses: actions/checkout@v4
1818

1919
- name: Set up Python
20-
uses: actions/setup-python@v4
20+
uses: actions/setup-python@v6
2121
with:
2222
python-version: "3.11"
2323

0 commit comments

Comments
 (0)