Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,32 @@ jobs:
run: cargo fmt --check

- name: Clippy (WASM target)
run: cargo clippy --target wasm32-wasip2 -- -D warnings
run: cargo clippy --target wasm32-wasip2 --workspace -- -D warnings

- name: Unit tests (host target)
run: cargo test --target x86_64-unknown-linux-gnu
run: cargo test --target x86_64-unknown-linux-gnu --workspace

- name: Build WASM component
run: cargo build --release --target wasm32-wasip2
- name: Build WASM plugins
run: cargo build --release --target wasm32-wasip2 --workspace

- name: Upload plugin artifact
- name: Upload Unity plugin
uses: actions/upload-artifact@v4
with:
name: plugin-wasm
name: unity-format-plugin
path: target/wasm32-wasip2/release/unity_format_plugin.wasm

- name: Upload RPM plugin
uses: actions/upload-artifact@v4
with:
name: rpm-format-plugin
path: target/wasm32-wasip2/release/rpm_format_plugin.wasm

- name: Upload PyPI plugin
uses: actions/upload-artifact@v4
with:
name: pypi-format-plugin
path: target/wasm32-wasip2/release/pypi_format_plugin.wasm

- name: SonarCloud Scan
uses: SonarSource/sonarqube-scan-action@v5
env:
Expand Down
57 changes: 42 additions & 15 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,39 +19,66 @@ jobs:
with:
targets: wasm32-wasip2

- name: Build WASM component
run: cargo build --release --target wasm32-wasip2
- name: Build all WASM plugins
run: cargo build --release --target wasm32-wasip2 --workspace

- name: Prepare release archive
- name: Package Unity plugin
run: |
mkdir -p release-staging
cp target/wasm32-wasip2/release/unity_format_plugin.wasm release-staging/plugin.wasm
cp plugin.toml release-staging/
cp README.md release-staging/
cp LICENSE release-staging/
cp -r wit release-staging/
cd release-staging && zip -r ../unity-format-plugin-${{ github.ref_name }}.zip .
mkdir -p staging/unity-format
cp target/wasm32-wasip2/release/unity_format_plugin.wasm staging/unity-format/plugin.wasm
cp plugins/unity-format/plugin.toml staging/unity-format/
cp README.md LICENSE staging/unity-format/
cp -r wit staging/unity-format/
cd staging/unity-format && zip -r ../../unity-format-plugin-${{ github.ref_name }}.zip .

- name: Package RPM plugin
run: |
mkdir -p staging/rpm-format
cp target/wasm32-wasip2/release/rpm_format_plugin.wasm staging/rpm-format/plugin.wasm
cp plugins/rpm-format/plugin.toml staging/rpm-format/
cp README.md LICENSE staging/rpm-format/
cp -r wit staging/rpm-format/
cd staging/rpm-format && zip -r ../../rpm-format-plugin-${{ github.ref_name }}.zip .

- name: Package PyPI plugin
run: |
mkdir -p staging/pypi-format
cp target/wasm32-wasip2/release/pypi_format_plugin.wasm staging/pypi-format/plugin.wasm
cp plugins/pypi-format/plugin.toml staging/pypi-format/
cp README.md LICENSE staging/pypi-format/
cp -r wit staging/pypi-format/
cd staging/pypi-format && zip -r ../../pypi-format-plugin-${{ github.ref_name }}.zip .

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: unity-format-plugin-${{ github.ref_name }}.zip
files: |
unity-format-plugin-${{ github.ref_name }}.zip
rpm-format-plugin-${{ github.ref_name }}.zip
pypi-format-plugin-${{ github.ref_name }}.zip
generate_release_notes: true
body: |
## Install into Artifact Keeper

**Option A - ZIP upload (easiest):**
Download the `.zip` below and upload it:
Download a plugin ZIP below and upload it:
```bash
curl -X POST https://your-registry/api/v1/plugins/install/zip \
-H "Authorization: Bearer $TOKEN" \
-F "file=@unity-format-plugin-${{ github.ref_name }}.zip"
-F "file=@<plugin-name>-${{ github.ref_name }}.zip"
```

**Option B - Git install:**
Or install directly from this Git repo:
```bash
curl -X POST https://your-registry/api/v1/plugins/install/git \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"url": "https://github.com/artifact-keeper/artifact-keeper-example-plugin.git", "ref": "${{ github.ref_name }}"}'
```

## Included Plugins

| Plugin | Format Key | Description |
|--------|-----------|-------------|
| unity-format | `unity` | Unity .unitypackage handler |
| rpm-format | `rpm` | RPM package handler |
| pypi-format | `pypi` | Python wheel/sdist handler |
27 changes: 3 additions & 24 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,24 +1,3 @@
[package]
name = "unity-format-plugin"
version = "0.1.0"
edition = "2021"
description = "Example Artifact Keeper plugin - Unity .unitypackage format handler"
license = "MIT"
authors = ["Artifact Keeper Team"]
repository = "https://github.com/artifact-keeper/artifact-keeper-example-plugin"

[lib]
crate-type = ["cdylib"]

[dependencies]
wit-bindgen = "0.36"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

[package.metadata.component]
package = "artifact-keeper:format"

[package.metadata.component.target]
path = "wit"

[package.metadata.component.dependencies]
[workspace]
members = ["plugins/*"]
resolver = "2"
69 changes: 43 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# Artifact Keeper Example Plugin
# Artifact Keeper Example Plugins

A fully working example of a custom format handler plugin for [Artifact Keeper](https://github.com/artifact-keeper/artifact-keeper). This plugin handles **Unity `.unitypackage`** files (gzipped tarballs), demonstrating real-world format validation, metadata extraction, and index generation.
A collection of working example plugins for [Artifact Keeper](https://github.com/artifact-keeper/artifact-keeper). Each plugin implements a custom format handler using the WASM Component Model and the `artifact-keeper:format@1.0.0` WIT contract.

Use this repo as a starting point for building your own plugins. Fork it, change the format key, and implement your logic.
Use these as starting points for building your own plugins. Fork, change the format key, and implement your logic.

## What this plugin does
## Included plugins

| Capability | Description |
|------------|-------------|
| **Validate** | Checks gzip magic bytes and correct file extension |
| **Parse metadata** | Extracts version from path/filename, detects content type |
| **Generate index** | Creates a `unity-index.json` listing all packages in a repository |
| Plugin | Format Key | What it demonstrates |
|--------|-----------|---------------------|
| [Unity](plugins/unity-format/) | `unity` | Gzip magic byte validation, path-based version extraction, JSON index |
| [RPM](plugins/rpm-format/) | `rpm` | Binary format validation (RPM lead magic), right-to-left filename parsing, structured metadata |
| [PyPI](plugins/pypi-format/) | `pypi` | PEP 427 wheel parsing, PEP 503 name normalization, HTML + JSON index generation |

## Prerequisites

Expand All @@ -20,22 +20,28 @@ Use this repo as a starting point for building your own plugins. Fork it, change
## Build

```bash
# Clone this repo
git clone https://github.com/artifact-keeper/artifact-keeper-example-plugin.git
cd artifact-keeper-example-plugin

# Build the WASM component
# Build all plugins
cargo build --release

# Output: target/wasm32-wasip2/release/unity_format_plugin.wasm
# Build a specific plugin
cargo build --release -p rpm-format-plugin

# Output: target/wasm32-wasip2/release/<plugin_name>.wasm
```

## Test

Unit tests run on the host target (not WASM):

```bash
cargo test --target $(rustc -vV | grep host | awk '{print $2}')
# All plugins
cargo test --target $(rustc -vV | grep host | awk '{print $2}') --workspace

# Single plugin
cargo test --target $(rustc -vV | grep host | awk '{print $2}') -p pypi-format-plugin
```

## Install into Artifact Keeper
Expand All @@ -54,12 +60,12 @@ curl -X POST https://your-registry/api/v1/plugins/install/git \

### From ZIP (release artifact)

Download the ZIP from the [Releases](https://github.com/artifact-keeper/artifact-keeper-example-plugin/releases) page, then:
Download a plugin ZIP from the [Releases](https://github.com/artifact-keeper/artifact-keeper-example-plugin/releases) page, then:

```bash
curl -X POST https://your-registry/api/v1/plugins/install/zip \
-H "Authorization: Bearer $TOKEN" \
-F "file=@unity-format-plugin-v0.1.0.zip"
-F "file=@rpm-format-plugin-v0.1.0.zip"
```

### From local path
Expand All @@ -73,7 +79,7 @@ curl -X POST https://your-registry/api/v1/plugins/install/local \

## Create your own plugin

1. **Fork this repo** or use it as a template
1. **Copy one of the example plugins** as a starting point (the Unity plugin is the simplest)
2. Update `plugin.toml` with your format key, extensions, and description
3. Implement the four functions in `src/lib.rs`:
- `format_key()` -- return your unique format identifier
Expand All @@ -87,18 +93,29 @@ curl -X POST https://your-registry/api/v1/plugins/install/local \

```
.
├── .cargo/config.toml # Default WASM target
├── .github/workflows/
│ ├── ci.yml # Lint + test + build on push/PR
│ └── release.yml # Build + package + GitHub Release on tag
├── src/lib.rs # Plugin implementation
├── wit/format-plugin.wit # WIT contract (from Artifact Keeper)
├── plugin.toml # Plugin manifest
├── Cargo.toml # Rust project config
└── rust-toolchain.toml # Rust toolchain + WASM target
├── Cargo.toml # Workspace root
├── .cargo/config.toml # Default WASM target (wasm32-wasip2)
├── rust-toolchain.toml # Rust stable + WASM target
├── wit/format-plugin.wit # Shared WIT contract
├── plugins/
│ ├── unity-format/ # Unity .unitypackage handler
│ │ ├── Cargo.toml
│ │ ├── plugin.toml
│ │ └── src/lib.rs
│ ├── rpm-format/ # RPM package handler
│ │ ├── Cargo.toml
│ │ ├── plugin.toml
│ │ └── src/lib.rs
│ └── pypi-format/ # Python wheel/sdist handler
│ ├── Cargo.toml
│ ├── plugin.toml
│ └── src/lib.rs
└── .github/workflows/
├── ci.yml # Lint + test + build on push/PR
└── release.yml # Build + package + GitHub Release on tag
```

## WIT Interface
## WIT interface

Plugins implement the `artifact-keeper:format@1.0.0` interface:

Expand Down
48 changes: 38 additions & 10 deletions docs/TEST_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,70 @@

## Overview

The artifact-keeper example plugin is a Rust WASM plugin template using wit-bindgen. It compiles to wasm32-wasip2 and implements the FormatHandler WIT contract.
The artifact-keeper example plugins are Rust WASM plugin templates using wit-bindgen. They compile to wasm32-wasip2 and implement the FormatHandler WIT contract. The workspace contains three plugins: Unity, RPM, and PyPI.

## Test Inventory

| Test Type | Framework | Count | CI Job | Status |
|-----------|-----------|-------|--------|--------|
| Check | cargo check | Full | `check` | Active |
| Format | cargo fmt | Full | CI | Active |
| Lint | cargo clippy | Full | CI | Active |
| Unit | cargo test | Minimal | `test` | Active |
| WASM build | cargo build --release | Full | `build` | Active |
| Unit - Unity | cargo test | 12 | CI | Active |
| Unit - RPM | cargo test | 18 | CI | Active |
| Unit - PyPI | cargo test | 27 | CI | Active |
| WASM build | cargo build --release | 3 plugins | CI | Active |
| Integration | (none) | 0 | - | Missing |

## How to Run

### Check and Lint
### Lint
```bash
cargo check --workspace
cargo fmt --check
cargo clippy --workspace -- -D warnings
cargo clippy --target wasm32-wasip2 --workspace -- -D warnings
```

### Unit Tests (must run on host, not WASM target)
```bash
cargo test --target $(rustc -vV | grep host | awk '{print $2}')
cargo test --target $(rustc -vV | grep host | awk '{print $2}') --workspace
```

### Build WASM
```bash
cargo build --release
# Output: target/wasm32-wasip2/release/unity_format_plugin.wasm
cargo build --release --workspace
# Output: target/wasm32-wasip2/release/{unity,rpm,pypi}_format_plugin.wasm
```

## Plugin Test Coverage

### Unity Format (12 tests)
- Format key identity
- Gzip magic byte detection
- Non-gzip content type fallback
- Validation: accepts valid gzip, rejects empty, rejects wrong extension, rejects bad magic
- Version extraction from path component and filename
- Index generation: empty returns None, produces valid JSON

### RPM Format (18 tests)
- Format key identity
- Filename parsing: simple, hyphens in name, noarch, no extension
- Version extraction from paths
- Metadata: RPM magic detection, non-RPM fallback, empty error
- Validation: accepts valid RPM, rejects empty, wrong extension, too small, bad magic, empty path
- Index generation: empty returns None, produces JSON with name/arch/release fields

### PyPI Format (27 tests)
- Format key identity
- PEP 503 name normalization: simple, underscores, dots, consecutive separators, mixed, leading/trailing
- Wheel filename parsing: name extraction, version extraction, build tag handling
- Source distribution parsing: name from sdist, name with hyphens, version from tar.gz/zip
- Metadata: wheel content type, sdist content type, empty error
- Validation: accepts wheel, accepts sdist, rejects empty, wrong extension, bad wheel filename, sdist without version, empty path
- Index generation: empty returns None, produces HTML + JSON, normalizes package names

## Gaps and Roadmap

| Gap | Recommendation | Priority |
|-----|---------------|----------|
| No integration test | Add test that loads WASM in wasmtime and calls FormatHandler methods | P2 |
| No plugin lifecycle test | Test register, upload, download, list cycle | P3 |
| No cross-plugin test | Verify all three plugins can coexist in the same Artifact Keeper instance | P3 |
24 changes: 24 additions & 0 deletions plugins/pypi-format/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "pypi-format-plugin"
version = "0.1.0"
edition = "2021"
description = "Example Artifact Keeper plugin - Python package (PyPI) format handler"
license = "MIT"
authors = ["Artifact Keeper Team"]
repository = "https://github.com/artifact-keeper/artifact-keeper-example-plugin"

[lib]
crate-type = ["cdylib"]

[dependencies]
wit-bindgen = "0.36"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

[package.metadata.component]
package = "artifact-keeper:format"

[package.metadata.component.target]
path = "../../wit"

[package.metadata.component.dependencies]
Loading
Loading