GitHub
Concept

Releases & Tags

Versioned releases anchored on git tags, file assets stored on the local filesystem, and an integrated OCI container registry mounted at /v2.

A release in ProxifAI is a row in the Release table tied to a git tag, optionally with binary assets and a release-notes body. The full surface — list/get/create/update/delete plus per-asset upload/download — is available at /api/v1/forge/{owner}/{repo}/releases/.... The integrated OCI registry is a separate but co-located concern, mounted at /v2/ on the same host.

Creating a release

Tag the commit

Either push a new tag from your local clone (git tag v1.2.3 && git push origin v1.2.3), or let the release endpoint create the tag for you by passing targetCommit (a branch name or SHA).

Create the release

From the UI: Releases tab → Draft a new release. Pick the tag, write the title and notes, attach assets, optionally mark as draft or pre-release, then publish. From the CLI:

pfai release create v1.2.3 \
  --target main \
  --title "v1.2.3 — Auth & analytics improvements" \
  --body "$(cat CHANGELOG-v1.2.3.md)" \
  --prerelease

From the API:

curl -X POST http://localhost:3000/api/v1/forge/{owner}/{repo}/releases \
  -H "Authorization: Bearer $PFAI_TOKEN" \
  -d '{
    "tagName":"v1.2.3",
    "targetCommit":"main",
    "name":"v1.2.3",
    "body":"…",
    "draft":false,
    "prerelease":true
  }'

Attach assets

curl -X POST http://localhost:3000/api/v1/forge/{owner}/{repo}/releases/v1.2.3/assets \
  -H "Authorization: Bearer $PFAI_TOKEN" \
  -F "file=@dist/proxifai-linux-amd64.tar.gz"

The upload endpoint accepts a multipart form. Server-side, asset bytes go to the configured storage backend (internal/storage/), and a release_asset row records name, contentType, size, and downloadCount.

Release schema

Release
├── id            int64
├── tagName       string         (the git tag this release points at)
├── name          string         (display title)
├── body          string         (Markdown release notes)
├── draft         bool           (not visible to non-maintainers)
├── prerelease    bool           (won't be marked "Latest")
├── author        ForgeUser
├── targetCommit  string         (resolved when tagName didn't pre-exist)
├── publishedAt   timestamp?
└── assets        []ReleaseAsset
                    ├── id, name, label, contentType, size
                    ├── downloadUrl
                    └── downloadCount

PATCH /releases/{tag} updates name, body, draft, prerelease (each is nullable in the input — only fields you pass change). Deleting the release does not delete the underlying git tag — tag deletion is a separate operation and is gated by tag protection.

Auto-generating release notes from merged PRs since the last tag isn’t implemented in OSS — write the body yourself or pipe a generator like git-cliff into pfai release create --body-file -.

Asset storage

Assets are written to the storage backend defined by internal/storage/:

BackendSelected whenPath layout
Local filesystem (default)ASSET_STORAGE_PATH is set, or fallback to $DATA_DIR/assets<base>/<owner>/<repo>/<release-id>/<asset-id>-<filename>

Asset rows reference a stable internal id (text like ast_xxx); the API serializes IDs as int64 for backward compatibility, so the canonical handle is the downloadUrl field. To delete:

pfai release delete-asset v1.2.3 dist-linux-amd64.tar.gz
# or
curl -X DELETE http://localhost:3000/api/v1/forge/{owner}/{repo}/releases/v1.2.3/assets/{assetId} \
  -H "Authorization: Bearer $PFAI_TOKEN"

Tags

Lightweight tagAnnotated tag
Stored asA refs/tags/<name> ref pointing at a commit SHAA separate object with message, tagger, date
Created bygit tag <name>git tag -a <name> -m "..."
Surfaced in APITag with empty message/taggerTag with full metadata

Both work as anchors for releases. Use annotated tags when the tag itself should carry release notes independent of a Release row.

Protected tags

Configure protection patterns at Repository Settings → Tag Protection or via pfai protected-tag. Each rule is a glob (v*, release-*, …); matching tags can only be deleted by users with admin access on the repo. This guards against accidental deletes that would orphan releases.

Deleting a git tag (via git push --delete origin <tag> or the UI’s Tags tab) does not delete the corresponding Release row, but the release becomes unanchored — its commit reference may become invalid if the commit isn’t reachable from any branch. Protected tags prevent this.

OCI container registry

A full OCI Distribution Spec implementation is mounted at /v2/* on the same host as the rest of the platform (internal/registry). There is no separate registry hostname.

# Auth uses your pfai token as both username and password
echo $PFAI_TOKEN | docker login http://localhost:3000 -u <user> --password-stdin

# Push
docker tag my-image:v1 localhost:3000/my-org/my-repo/my-image:v1.0.0
docker push localhost:3000/my-org/my-repo/my-image:v1.0.0

# Pull
docker pull localhost:3000/my-org/my-repo/my-image:v1.0.0

Behind the scenes, every blob upload (POST /v2/<repo>/blobs/uploads/), manifest push (PUT /v2/<repo>/manifests/<tag>), and pull goes through the in-process handler. Tags and manifests are queryable via OCIQuerier, which also powers the Packages tab in the repo UI.

FormatSupported
OCI image manifest v2
Docker manifest v2
Multi-arch manifest list / OCI index

For production, front the registry with TLS via your reverse proxy (Cloudflare tunnel, nginx, Caddy) and tell Docker to trust the host. For development, --insecure-registry flags work against localhost.

Versioning

Semantic versioning (MAJOR.MINOR.PATCH) is convention, not enforcement. The releases list sorts by publishedAt descending; the topmost non-draft non-prerelease release is highlighted as Latest in the UI.

BumpWhen
MAJORBreaking changes
MINORBackward-compatible features
PATCHBug fixes

Pre-releases (prerelease=true) participate in the list but never become the Latest badge holder, so you can ship v1.2.3-rc.1 alongside v1.2.2 without confusing consumers.

REST endpoint reference

Method · PathPurpose
GET /api/v1/forge/{owner}/{repo}/releasesList
POST /api/v1/forge/{owner}/{repo}/releasesCreate
GET /api/v1/forge/{owner}/{repo}/releases/{tag}Read
PATCH /api/v1/forge/{owner}/{repo}/releases/{tag}Update
DELETE /api/v1/forge/{owner}/{repo}/releases/{tag}Delete release (not the tag)
POST /api/v1/forge/{owner}/{repo}/releases/{tag}/assetsUpload asset (multipart)
GET /api/v1/forge/{owner}/{repo}/releases/{tag}/assets/{assetId}Download asset
DELETE /api/v1/forge/{owner}/{repo}/releases/{tag}/assets/{assetId}Delete asset
GET /api/v1/forge/{owner}/{repo}/tagsList tags
/v2/...OCI Distribution API

See also