Skip to content

Commit 9a6a8f7

Browse files
authored
Add deployment pipelines (#2)
1 parent e0f6318 commit 9a6a8f7

File tree

8 files changed

+722
-4
lines changed

8 files changed

+722
-4
lines changed

.github/workflows/ci.yml

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: true
12+
13+
jobs:
14+
detect-changes:
15+
runs-on: ubuntu-latest
16+
outputs:
17+
ts-packages: ${{ steps.changes.outputs.ts-packages }}
18+
rust-packages: ${{ steps.changes.outputs.rust-packages }}
19+
python-packages: ${{ steps.changes.outputs.python-packages }}
20+
has-ts: ${{ steps.changes.outputs.has-ts }}
21+
has-rust: ${{ steps.changes.outputs.has-rust }}
22+
has-python: ${{ steps.changes.outputs.has-python }}
23+
steps:
24+
- uses: actions/checkout@v4
25+
with:
26+
fetch-depth: 0
27+
28+
- name: Detect changed packages
29+
id: changes
30+
run: |
31+
if [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == "refs/heads/main" ]; then
32+
# On push to main, compare with previous commit
33+
BASE_SHA="${{ github.event.before }}"
34+
else
35+
# On PR, compare with base branch
36+
BASE_SHA="${{ github.event.pull_request.base.sha }}"
37+
fi
38+
39+
# Get changed files
40+
CHANGED_FILES=$(git diff --name-only "$BASE_SHA" HEAD 2>/dev/null || echo "")
41+
42+
# If we can't get diff (e.g., initial commit), run all
43+
if [ -z "$CHANGED_FILES" ]; then
44+
echo "Could not determine changed files, will run all tests"
45+
TS_PACKAGES=$(ls -d packages/*/ts 2>/dev/null | xargs -I{} dirname {} | xargs -I{} basename {} | jq -R -s -c 'split("\n") | map(select(length > 0))')
46+
RUST_PACKAGES=$(ls -d packages/*/rust 2>/dev/null | xargs -I{} dirname {} | xargs -I{} basename {} | jq -R -s -c 'split("\n") | map(select(length > 0))')
47+
PYTHON_PACKAGES=$(ls -d packages/*/python 2>/dev/null | xargs -I{} dirname {} | xargs -I{} basename {} | jq -R -s -c 'split("\n") | map(select(length > 0))')
48+
else
49+
# Extract unique package names for each language
50+
TS_PACKAGES=$(echo "$CHANGED_FILES" | grep -E '^packages/[^/]+/ts/' | sed 's|packages/\([^/]*\)/ts/.*|\1|' | sort -u | jq -R -s -c 'split("\n") | map(select(length > 0))')
51+
RUST_PACKAGES=$(echo "$CHANGED_FILES" | grep -E '^packages/[^/]+/rust/' | sed 's|packages/\([^/]*\)/rust/.*|\1|' | sort -u | jq -R -s -c 'split("\n") | map(select(length > 0))')
52+
PYTHON_PACKAGES=$(echo "$CHANGED_FILES" | grep -E '^packages/[^/]+/python/' | sed 's|packages/\([^/]*\)/python/.*|\1|' | sort -u | jq -R -s -c 'split("\n") | map(select(length > 0))')
53+
fi
54+
55+
echo "ts-packages=$TS_PACKAGES" >> $GITHUB_OUTPUT
56+
echo "rust-packages=$RUST_PACKAGES" >> $GITHUB_OUTPUT
57+
echo "python-packages=$PYTHON_PACKAGES" >> $GITHUB_OUTPUT
58+
59+
# Set boolean flags for whether each language has changes
60+
echo "has-ts=$( [ "$TS_PACKAGES" != "[]" ] && echo true || echo false )" >> $GITHUB_OUTPUT
61+
echo "has-rust=$( [ "$RUST_PACKAGES" != "[]" ] && echo true || echo false )" >> $GITHUB_OUTPUT
62+
echo "has-python=$( [ "$PYTHON_PACKAGES" != "[]" ] && echo true || echo false )" >> $GITHUB_OUTPUT
63+
64+
echo "TypeScript packages: $TS_PACKAGES"
65+
echo "Rust packages: $RUST_PACKAGES"
66+
echo "Python packages: $PYTHON_PACKAGES"
67+
68+
test-typescript:
69+
needs: detect-changes
70+
if: needs.detect-changes.outputs.has-ts == 'true'
71+
runs-on: ubuntu-latest
72+
steps:
73+
- uses: actions/checkout@v4
74+
75+
- name: Setup Bun
76+
uses: oven-sh/setup-bun@v2
77+
78+
- name: Install dependencies
79+
run: bun ci
80+
81+
- name: Build and test changed packages
82+
run: |
83+
TS_PACKAGES='${{ needs.detect-changes.outputs.ts-packages }}'
84+
echo "$TS_PACKAGES" | jq -r '.[]' | while read -r pkg; do
85+
echo "==> $pkg"
86+
if [ -d "packages/$pkg/ts" ]; then
87+
(
88+
cd "packages/$pkg/ts"
89+
if jq -e '.scripts.build' package.json >/dev/null; then
90+
bun run build
91+
else
92+
echo "No build script found, skipping"
93+
fi
94+
if jq -e '.scripts.test' package.json >/dev/null; then
95+
bun run test
96+
else
97+
echo "No test script found, skipping"
98+
fi
99+
)
100+
else
101+
echo "Package path packages/$pkg/ts not found"
102+
exit 1
103+
fi
104+
done
105+
106+
test-rust:
107+
needs: detect-changes
108+
if: needs.detect-changes.outputs.has-rust == 'true'
109+
runs-on: ubuntu-latest
110+
strategy:
111+
fail-fast: true
112+
matrix:
113+
toolchain: [stable, beta, '1.75']
114+
package: ${{ fromJson(needs.detect-changes.outputs.rust-packages) }}
115+
steps:
116+
- uses: actions/checkout@v4
117+
118+
- name: Setup Rust ${{ matrix.toolchain }}
119+
uses: dtolnay/rust-toolchain@master
120+
with:
121+
toolchain: ${{ matrix.toolchain }}
122+
components: rustfmt, clippy
123+
124+
- name: Cache cargo
125+
uses: actions/cache@v4
126+
with:
127+
path: |
128+
~/.cargo/registry/
129+
~/.cargo/git/
130+
target/
131+
key: cargo-${{ runner.os }}-${{ matrix.toolchain }}-${{ matrix.package }}-${{ hashFiles(format('packages/{0}/rust/Cargo.toml', matrix.package)) }}
132+
restore-keys: |
133+
cargo-${{ runner.os }}-${{ matrix.toolchain }}-${{ matrix.package }}-
134+
cargo-${{ runner.os }}-${{ matrix.toolchain }}-
135+
136+
- name: Check formatting
137+
working-directory: packages/${{ matrix.package }}/rust
138+
run: cargo fmt -- --check
139+
140+
- name: Clippy
141+
working-directory: packages/${{ matrix.package }}/rust
142+
run: cargo clippy --all-targets --all-features -- -D warnings
143+
144+
- name: Build
145+
working-directory: packages/${{ matrix.package }}/rust
146+
run: cargo build --all-targets
147+
148+
- name: Test
149+
working-directory: packages/${{ matrix.package }}/rust
150+
run: cargo test
151+
152+
test-python:
153+
needs: detect-changes
154+
if: needs.detect-changes.outputs.has-python == 'true'
155+
runs-on: ubuntu-latest
156+
strategy:
157+
fail-fast: true
158+
matrix:
159+
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
160+
package: ${{ fromJson(needs.detect-changes.outputs.python-packages) }}
161+
steps:
162+
- uses: actions/checkout@v4
163+
164+
- name: Setup Python ${{ matrix.python-version }}
165+
uses: actions/setup-python@v5
166+
with:
167+
python-version: ${{ matrix.python-version }}
168+
cache: 'pip'
169+
170+
- name: Install dependencies
171+
run: |
172+
python -m pip install --upgrade pip
173+
pip install ruff pytest pytest-cov build
174+
175+
- name: Install package
176+
working-directory: packages/${{ matrix.package }}/python
177+
run: pip install -e .
178+
179+
- name: Lint with ruff
180+
working-directory: packages/${{ matrix.package }}/python
181+
run: ruff check .
182+
183+
- name: Test with pytest
184+
working-directory: packages/${{ matrix.package }}/python
185+
run: |
186+
if [ -d "tests" ]; then
187+
pytest tests -v
188+
else
189+
echo "No tests directory found, skipping"
190+
fi
191+
192+
ci-success:
193+
runs-on: ubuntu-latest
194+
needs: [detect-changes, test-typescript, test-rust, test-python]
195+
if: |
196+
needs.detect-changes.result == 'success' && (
197+
needs.detect-changes.outputs.has-ts == 'true' ||
198+
needs.detect-changes.outputs.has-rust == 'true' ||
199+
needs.detect-changes.outputs.has-python == 'true'
200+
)
201+
steps:
202+
- name: Check CI status
203+
run: |
204+
if [[ "${{ needs.detect-changes.result }}" != "success" ]]; then
205+
echo "detect-changes failed"
206+
exit 1
207+
fi
208+
209+
if [[ "${{ needs.detect-changes.outputs.has-ts }}" == "true" ]] && \
210+
[[ "${{ needs.test-typescript.result }}" != "success" ]]; then
211+
echo "TypeScript tests did not succeed"
212+
exit 1
213+
fi
214+
215+
if [[ "${{ needs.detect-changes.outputs.has-rust }}" == "true" ]] && \
216+
[[ "${{ needs.test-rust.result }}" != "success" ]]; then
217+
echo "Rust tests did not succeed"
218+
exit 1
219+
fi
220+
221+
if [[ "${{ needs.detect-changes.outputs.has-python }}" == "true" ]] && \
222+
[[ "${{ needs.test-python.result }}" != "success" ]]; then
223+
echo "Python tests did not succeed"
224+
exit 1
225+
fi
226+
227+
echo "All required checks passed"
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: Publish to crates.io
2+
3+
on:
4+
push:
5+
tags:
6+
- '*/rust@*'
7+
8+
jobs:
9+
publish:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
14+
- name: Parse tag
15+
id: tag
16+
run: |
17+
# Tag format: pq-oid/rust@1.0.0
18+
TAG="${GITHUB_REF_NAME}"
19+
PACKAGE="${TAG%%/rust@*}" # pq-oid
20+
VERSION="${TAG##*/rust@}" # 1.0.0
21+
echo "package=$PACKAGE" >> $GITHUB_OUTPUT
22+
echo "version=$VERSION" >> $GITHUB_OUTPUT
23+
echo "Publishing $PACKAGE version $VERSION to crates.io"
24+
25+
- name: Setup Rust
26+
uses: dtolnay/rust-toolchain@stable
27+
28+
- name: Cache cargo
29+
uses: actions/cache@v4
30+
with:
31+
path: |
32+
~/.cargo/registry/
33+
~/.cargo/git/
34+
target/
35+
key: cargo-publish-${{ hashFiles('**/Cargo.lock') }}
36+
restore-keys: |
37+
cargo-publish-
38+
39+
- name: Verify version matches
40+
run: |
41+
CARGO_VERSION=$(grep '^version' packages/${{ steps.tag.outputs.package }}/rust/Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
42+
if [ "$CARGO_VERSION" != "${{ steps.tag.outputs.version }}" ]; then
43+
echo "::error::Tag version (${{ steps.tag.outputs.version }}) doesn't match Cargo.toml ($CARGO_VERSION)"
44+
exit 1
45+
fi
46+
echo "Version verified: $CARGO_VERSION"
47+
48+
- name: Publish
49+
working-directory: packages/${{ steps.tag.outputs.package }}/rust
50+
run: cargo publish
51+
env:
52+
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_TOKEN }}

.github/workflows/publish-npm.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: Publish to npm
2+
3+
on:
4+
push:
5+
tags:
6+
- '*/ts@*'
7+
8+
jobs:
9+
publish:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: read
13+
id-token: write
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- name: Parse tag
18+
id: tag
19+
run: |
20+
# Tag format: pq-oid/ts@1.0.0
21+
TAG="${GITHUB_REF_NAME}"
22+
PACKAGE="${TAG%%/ts@*}" # pq-oid
23+
VERSION="${TAG##*/ts@}" # 1.0.0
24+
echo "package=$PACKAGE" >> $GITHUB_OUTPUT
25+
echo "version=$VERSION" >> $GITHUB_OUTPUT
26+
echo "Publishing $PACKAGE version $VERSION to npm"
27+
28+
- name: Setup Bun
29+
uses: oven-sh/setup-bun@v2
30+
31+
- name: Install dependencies
32+
run: bun ci
33+
34+
- name: Build
35+
working-directory: packages/${{ steps.tag.outputs.package }}/ts
36+
run: bun run build
37+
38+
- name: Verify version matches
39+
working-directory: packages/${{ steps.tag.outputs.package }}/ts
40+
run: |
41+
PKG_VERSION=$(jq -r .version package.json)
42+
if [ "$PKG_VERSION" != "${{ steps.tag.outputs.version }}" ]; then
43+
echo "::error::Tag version (${{ steps.tag.outputs.version }}) doesn't match package.json ($PKG_VERSION)"
44+
exit 1
45+
fi
46+
echo "Version verified: $PKG_VERSION"
47+
48+
- name: Publish
49+
working-directory: packages/${{ steps.tag.outputs.package }}/ts
50+
run: bun publish --access public
51+
env:
52+
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}

.github/workflows/publish-pypi.yml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: Publish to PyPI
2+
3+
on:
4+
push:
5+
tags:
6+
- '*/python@*'
7+
8+
jobs:
9+
publish:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
id-token: write
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Parse tag
17+
id: tag
18+
run: |
19+
# Tag format: pq-oid/python@1.0.0
20+
TAG="${GITHUB_REF_NAME}"
21+
PACKAGE="${TAG%%/python@*}" # pq-oid
22+
VERSION="${TAG##*/python@}" # 1.0.0
23+
echo "package=$PACKAGE" >> $GITHUB_OUTPUT
24+
echo "version=$VERSION" >> $GITHUB_OUTPUT
25+
echo "Publishing $PACKAGE version $VERSION to PyPI"
26+
27+
- name: Setup Python
28+
uses: actions/setup-python@v5
29+
with:
30+
python-version: '3.11'
31+
cache: 'pip'
32+
33+
- name: Install build tools
34+
run: pip install build twine
35+
36+
- name: Verify version matches
37+
working-directory: packages/${{ steps.tag.outputs.package }}/python
38+
run: |
39+
PYPROJECT_VERSION=$(grep '^version' pyproject.toml | sed 's/.*"\(.*\)"/\1/')
40+
if [ "$PYPROJECT_VERSION" != "${{ steps.tag.outputs.version }}" ]; then
41+
echo "::error::Tag version (${{ steps.tag.outputs.version }}) doesn't match pyproject.toml ($PYPROJECT_VERSION)"
42+
exit 1
43+
fi
44+
echo "Version verified: $PYPROJECT_VERSION"
45+
46+
- name: Build
47+
working-directory: packages/${{ steps.tag.outputs.package }}/python
48+
run: python -m build
49+
50+
- name: Publish
51+
working-directory: packages/${{ steps.tag.outputs.package }}/python
52+
run: twine upload dist/*
53+
env:
54+
TWINE_USERNAME: __token__
55+
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}

0 commit comments

Comments
 (0)