Skip to content

Commit baad088

Browse files
authored
Merge pull request #2 from zcloudpass/feature/ci-cd
added CI/CD and integration tests
2 parents a02f603 + 83e94fd commit baad088

7 files changed

Lines changed: 705 additions & 49 deletions

File tree

.github/workflows/ci.yml

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
env:
10+
CARGO_TERM_COLOR: always
11+
12+
jobs:
13+
# ── Lint & unit tests (no database needed) ──────────────────────────────────
14+
check:
15+
name: Lint & Unit Tests
16+
runs-on: ubuntu-latest
17+
18+
steps:
19+
- name: Checkout repository
20+
uses: actions/checkout@v4
21+
22+
- name: Install Rust stable
23+
uses: dtolnay/rust-toolchain@stable
24+
with:
25+
components: rustfmt, clippy
26+
27+
- name: Cache Cargo registry & build
28+
uses: Swatinem/rust-cache@v2
29+
30+
- name: Check formatting
31+
run: cargo fmt --all -- --check
32+
33+
- name: Run Clippy
34+
run: cargo clippy -- -D warnings
35+
36+
- name: Build
37+
run: cargo build --verbose
38+
39+
- name: Run unit tests
40+
run: cargo test --test unit --verbose
41+
42+
# ── Integration tests (requires Postgres) ───────────────────────────────────
43+
integration:
44+
name: Integration Tests
45+
runs-on: ubuntu-latest
46+
needs: check
47+
48+
services:
49+
postgres:
50+
image: postgres:15
51+
env:
52+
POSTGRES_USER: test
53+
POSTGRES_PASSWORD: test
54+
POSTGRES_DB: zcloudpass_test
55+
ports:
56+
- 5432:5432
57+
options: >-
58+
--health-cmd "pg_isready -U test"
59+
--health-interval 10s
60+
--health-timeout 5s
61+
--health-retries 5
62+
63+
env:
64+
DATABASE_URL: postgres://test:test@localhost:5432/zcloudpass_test
65+
66+
steps:
67+
- name: Checkout repository
68+
uses: actions/checkout@v4
69+
70+
- name: Install Rust stable
71+
uses: dtolnay/rust-toolchain@stable
72+
73+
- name: Cache Cargo registry & build
74+
uses: Swatinem/rust-cache@v2
75+
76+
- name: Run integration tests
77+
run: cargo test --test integration --verbose

.github/workflows/release.yml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
8+
env:
9+
CARGO_TERM_COLOR: always
10+
REGISTRY: ghcr.io
11+
IMAGE_NAME: ${{ github.repository }}
12+
13+
permissions:
14+
contents: write
15+
packages: write
16+
17+
jobs:
18+
release:
19+
name: Build & Release
20+
runs-on: ubuntu-latest
21+
22+
steps:
23+
- name: Checkout repository
24+
uses: actions/checkout@v4
25+
26+
- name: Install Rust stable
27+
uses: dtolnay/rust-toolchain@stable
28+
29+
- name: Cache Cargo registry & build
30+
uses: Swatinem/rust-cache@v2
31+
32+
- name: Build release binary
33+
run: cargo build --release
34+
35+
- name: Run tests
36+
run: cargo test --verbose
37+
38+
- name: Log in to GitHub Container Registry
39+
uses: docker/login-action@v3
40+
with:
41+
registry: ${{ env.REGISTRY }}
42+
username: ${{ github.actor }}
43+
password: ${{ secrets.GITHUB_TOKEN }}
44+
45+
- name: Extract Docker metadata
46+
id: meta
47+
uses: docker/metadata-action@v5
48+
with:
49+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
50+
tags: |
51+
type=semver,pattern={{version}}
52+
type=semver,pattern={{major}}.{{minor}}
53+
type=sha
54+
55+
- name: Build and push Docker image
56+
uses: docker/build-push-action@v6
57+
with:
58+
context: .
59+
push: true
60+
tags: ${{ steps.meta.outputs.tags }}
61+
labels: ${{ steps.meta.outputs.labels }}
62+
63+
- name: Upload release binary
64+
uses: softprops/action-gh-release@v2
65+
with:
66+
files: target/release/zcloudpass-backend
67+
generate_release_notes: true

Dockerfile

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# ── Builder stage ─────────────────────────────────────────────────────────────
2+
FROM rust:1.85-slim AS builder
3+
4+
WORKDIR /app
5+
6+
# Install build dependencies for native TLS (required by sqlx native-tls)
7+
RUN apt-get update && \
8+
apt-get install -y --no-install-recommends pkg-config libssl-dev && \
9+
rm -rf /var/lib/apt/lists/*
10+
11+
# Copy manifests first for better layer caching
12+
COPY Cargo.toml Cargo.lock ./
13+
14+
# Create a dummy main to cache dependency builds
15+
RUN mkdir src && \
16+
echo "fn main() {}" > src/main.rs && \
17+
echo "" > src/lib.rs && \
18+
cargo build --release && \
19+
rm -rf src
20+
21+
# Copy actual source and build
22+
COPY src ./src
23+
COPY tests ./tests
24+
RUN touch src/main.rs src/lib.rs && \
25+
cargo build --release
26+
27+
# ── Runtime stage ─────────────────────────────────────────────────────────────
28+
FROM debian:bookworm-slim
29+
30+
RUN apt-get update && \
31+
apt-get install -y --no-install-recommends ca-certificates libssl3 && \
32+
rm -rf /var/lib/apt/lists/*
33+
34+
COPY --from=builder /app/target/release/zcloudpass-backend /usr/local/bin/zcloudpass-backend
35+
36+
ENV BIND_ADDRESS=0.0.0.0:3000
37+
38+
EXPOSE 3000
39+
40+
ENTRYPOINT ["zcloudpass-backend"]

src/lib.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,54 @@ pub mod middleware;
55
pub struct AppState {
66
pub db: sqlx::PgPool,
77
}
8+
9+
/// Creates the `users` and `sessions` tables if they do not already exist.
10+
/// Also attempts to enable the `pgcrypto` extension (best-effort).
11+
pub async fn ensure_tables(pool: &sqlx::PgPool) -> Result<(), sqlx::Error> {
12+
match sqlx::query("CREATE EXTENSION IF NOT EXISTS pgcrypto;")
13+
.execute(pool)
14+
.await
15+
{
16+
Ok(_) => println!("pgcrypto extension ensured"),
17+
Err(e) => eprintln!(
18+
"notice: could not create pgcrypto extension (continuing): {:?}. \
19+
If you need pgcrypto functionality, create the extension as a superuser.",
20+
e
21+
),
22+
}
23+
24+
sqlx::query(
25+
r#"
26+
CREATE TABLE IF NOT EXISTS users (
27+
id SERIAL PRIMARY KEY,
28+
username VARCHAR(255),
29+
email VARCHAR(255) UNIQUE NOT NULL,
30+
srp_salt TEXT,
31+
srp_verifier TEXT,
32+
encrypted_vault TEXT,
33+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
34+
last_login TIMESTAMP,
35+
account_status VARCHAR(20) DEFAULT 'active'
36+
)
37+
"#,
38+
)
39+
.execute(pool)
40+
.await?;
41+
42+
sqlx::query(
43+
r#"
44+
CREATE TABLE IF NOT EXISTS sessions (
45+
id SERIAL PRIMARY KEY,
46+
user_id INT REFERENCES users(id) ON DELETE CASCADE,
47+
session_token TEXT UNIQUE NOT NULL,
48+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
49+
expires_at TIMESTAMP,
50+
last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP
51+
)
52+
"#,
53+
)
54+
.execute(pool)
55+
.await?;
56+
57+
Ok(())
58+
}

src/main.rs

Lines changed: 3 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ async fn main() {
1717
.await
1818
.expect("Failed to connect to database");
1919

20-
ensure_tables(&pool).await.expect("Failed to ensure tables");
20+
zcloudpass_backend::ensure_tables(&pool)
21+
.await
22+
.expect("Failed to ensure tables");
2123

2224
let app_state = AppState { db: pool.clone() };
2325
let shared_state = Arc::new(app_state);
@@ -44,51 +46,3 @@ async fn main() {
4446
axum::serve(listener, app).await.unwrap();
4547
}
4648

47-
async fn ensure_tables(pool: &sqlx::PgPool) -> Result<(), sqlx::Error> {
48-
match sqlx::query("CREATE EXTENSION IF NOT EXISTS pgcrypto;")
49-
.execute(pool)
50-
.await
51-
{
52-
Ok(_) => println!("pgcrypto extension ensured"),
53-
Err(e) => eprintln!(
54-
"notice: could not create pgcrypto extension (continuing): {:?}. \
55-
If you need pgcrypto functionality, create the extension as a superuser.",
56-
e
57-
),
58-
}
59-
60-
sqlx::query(
61-
r#"
62-
CREATE TABLE IF NOT EXISTS users (
63-
id SERIAL PRIMARY KEY,
64-
username VARCHAR(255),
65-
email VARCHAR(255) UNIQUE NOT NULL,
66-
srp_salt TEXT,
67-
srp_verifier TEXT,
68-
encrypted_vault TEXT,
69-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
70-
last_login TIMESTAMP,
71-
account_status VARCHAR(20) DEFAULT 'active'
72-
)
73-
"#,
74-
)
75-
.execute(pool)
76-
.await?;
77-
78-
sqlx::query(
79-
r#"
80-
CREATE TABLE IF NOT EXISTS sessions (
81-
id SERIAL PRIMARY KEY,
82-
user_id INT REFERENCES users(id) ON DELETE CASCADE,
83-
session_token TEXT UNIQUE NOT NULL,
84-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
85-
expires_at TIMESTAMP,
86-
last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP
87-
)
88-
"#,
89-
)
90-
.execute(pool)
91-
.await?;
92-
93-
Ok(())
94-
}

0 commit comments

Comments
 (0)