implemented gitops controller + drift detection

This commit is contained in:
2026-05-12 19:51:59 +02:00
parent 35afa8d8f1
commit c7df53708c
17 changed files with 1064 additions and 261 deletions
+207 -143
View File
@@ -9,63 +9,154 @@ Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### In Progress — Phase 3C (Workspaces + Secret management hierarchy)
- `Workspace` model — named collaborative namespace (handle, displayName, description, avatarUrl)
- `WorkspaceMember` model — user membership with owner/admin/member roles
- Repos can be owned by a workspace; URL format stays `/{owner}/{repo}` where owner is a workspace handle or username
- `Secret` model — AES-256-GCM encrypted, scoped to global / workspace / repo / env
- Secret hierarchy resolution in CI executor: Env → Repo → Workspace → Global
- Full CRUD APIs for workspaces, workspace members, secrets at all scope levels
- WorkspacesPage, WorkspacePage, WorkspaceSettingsPage (settings + members)
- Workspace switcher in sidebar header
- Create repo: workspace owner selector
- RepoSecretsPage — write-only secret management per repo and per environment
### Planned — Phase 3E (Observability)
- Prometheus metrics endpoint `GET /metrics`
- Structured internal metrics: pipeline duration, queue depth, deployment frequency, error rates
- Health check endpoint `GET /health` returning DB + NATS status
- Environment cards: live health status via HTTP health check polling
- Repo page: error rate and deployment frequency sparklines
### Planned — Phase 3F (Federation)
- ActivityPub inbox/outbox HTTP handlers
- HTTP signature verification middleware
- WebFinger `/.well-known/webfinger` endpoint
- Cross-instance pull requests via ActivityPub activities
### Planned — Phase 4 (Intelligence + Artifacts)
- AI failure diagnosis (pipeline failure root-cause analysis via Claude API)
- AI deployment risk scoring
- Signed artifacts (Sigstore/Cosign)
- SBOM generation (CycloneDX/SPDX)
- OCI container registry
- Secret scanning (commit-level pattern detection)
- Dependency vulnerability scanning
---
## [0.7.0] — 2026-05-12
Phase 3D complete. Git is now the source of truth for environment deployment state.
### Added — GitOps Controller (`internal/domain/gitops/`)
- `controller.go` — starts as a background goroutine; subscribes to `push.received`,
`deployment.succeeded`, `deployment.failed`; runs a periodic reconciliation ticker
(interval configurable via `GITOPS_RECONCILE_INTERVAL`); recovers stale `syncing`
configs to `drifted` on startup
- `drift.go``CheckDrift` calls `git rev-parse` via the existing git domain wrapper;
`handlePush` queries all GitOpsConfigs matching the pushed branch and evaluates drift;
`periodicCheck` iterates configs whose `SyncInterval` has elapsed; publishes
`environment.drift_detected` when drift is found
- `reconciler.go``TriggerSync` creates a `Deployment` record and publishes
`deployment.started` (same lifecycle path as manual deployments, `TriggeredBy="gitops"`);
`handleDeploymentSucceeded` resolves open drift events and marks config `synced` for
both GitOps and manual deployments; `handleDeploymentFailed` reverts to `drifted`
### Added — GitOps HTTP API (`internal/api/handlers/gitops.go`)
All routes live under `/api/v1/repos/{owner}/{repo}/environments/{envName}/gitops/`:
- `GET /gitops` — current GitOpsConfig or 404 if not configured
- `PUT /gitops` — idempotent upsert (branch, autoSync, syncInterval)
- `DELETE /gitops` — remove config without deleting deployments
- `POST /gitops/sync` — manual reconciliation trigger; creates deployment record
- `GET /gitops/drift` — current sync status: syncStatus, desiredSha, actualSha, isDrifted
- `GET /gitops/drift/history` — paginated drift event log (newest first)
- `POST /gitops/drift/{driftID}/acknowledge` — acknowledge without syncing
### Added — Database Models (migration `013_gitops`)
- `GitOpsConfig` — links environment to a branch; tracks `DesiredSHA`, `ActualSHA`,
`SyncStatus` (`unknown/synced/drifted/syncing`), `AutoSync`, `SyncInterval`,
`LastCheckedAt`
- `GitOpsDriftEvent` — append-only drift record: `DesiredSHA`, `ActualSHA`,
`SyncStatus` (`drifted/synced/acknowledged`), `DetectedAt`, `ResolvedAt`
### Added — Supporting Changes
- `git.RevParse(repoPath, ref)` — new function in `internal/domain/git/binary.go`
used by `CheckDrift` to resolve branch HEAD SHA
- `events.DeploymentEvent` + `events.DriftEvent` types added to `internal/events/types.go`
- `EnvironmentHandler.publishDeployEvent` updated to use shared `events.DeploymentEvent`
so the GitOps controller can unmarshal deployment lifecycle events correctly
- `GITOPS_RECONCILE_INTERVAL` env var (default `300`s); `0` disables the periodic ticker
- `ArtifactRoot` config field + `ARTIFACT_ROOT` env var
---
## [0.6.0] — 2026-05-12
Phase 3C complete. Multi-tenant workspaces and a full secret management hierarchy operational.
### Added — Workspaces
- `Workspace` model (migration `011`): globally unique handle, display name, description, avatarUrl
- `WorkspaceMember` model: owner/admin/member roles per workspace
- Repository `workspace_id` column (optional; null = personal repo)
- Full workspace CRUD API: `GET/POST /api/v1/workspaces`, `GET/PATCH/DELETE /api/v1/workspaces/{handle}`
- Workspace member management: list, add, update role, remove
- `GET /api/v1/workspaces/{handle}/repos` — repos in workspace
- Workspace frontend: WorkspacesPage, WorkspacePage, workspace switcher in sidebar header
- Workspace owner selector in repo create flow
### Added — Secret Management (`internal/api/handlers/secret.go`)
- `Secret` model (migration `012`): `Scope` (global/workspace/repo/env), `ScopeID`, `Name`,
`EncryptedValue` (AES-256-GCM, never returned by API)
- Unique constraint on (scope, scope_id, name)
- CRUD at all scope levels:
- `GET/POST/DELETE /api/v1/admin/secrets` (global, admin-only)
- `GET/POST/DELETE /api/v1/workspaces/{handle}/secrets` (workspace-scoped)
- `GET/POST/DELETE /api/v1/repos/{owner}/{repo}/secrets` (repo-scoped)
- `GET/POST/DELETE /api/v1/repos/{owner}/{repo}/environments/{envName}/secrets` (env-scoped)
- `ResolveSecretsForRun(db, repoID, workspaceID, envID, sessionSecret)` — hierarchy
resolution for CI executor: Env > Repo > Workspace > Global
- CI executor updated to inject resolved secrets as Docker `--env` flags
- RepoSecretsPage — write-only UI, values never displayed after creation
- Sidebar "Secrets" nav item in repo context
### Completed — Phase 3B (Unified Operational Timeline)
- `GET /api/v1/repos/:owner/:repo/timeline` — merges commits, pipeline runs, and deployments into a single chronological feed
- `RepoTimelinePage` at `/repos/:owner/:repo/timeline` — vertical event feed with type filter tabs
---
## [0.5.0] — 2026-05-11
Phases 3A and 3B complete. Environments, deployments, and the operational timeline are operational.
### Added — Environments + Deployments (Phase 3A)
- `Environment` model (migration `010`): repoId, name, URL, protectionRules (JSON)
- `Deployment` model: envId, repoId, sha, ref, status lifecycle
(`pending → in_progress → success/failure/cancelled`), triggeredBy, description, runId link
- CRUD API for environments: `GET/POST /environments`, `GET/PATCH/DELETE /environments/{envName}`
- Deployment API: `GET/POST /environments/{envName}/deployments`,
`PATCH /environments/{envName}/deployments/{id}/status`
- NATS events published on status transitions: `deployment.started`, `deployment.succeeded`,
`deployment.failed`
- `EnvironmentsPage` — environment cards each showing latest deployment status, SHA, actor,
and time since deploy; deployment history per env
- Sidebar "Environments" nav item in repo context
- Repo page deployment status badges (latest deploy per env at a glance)
### Added — Unified Operational Timeline (Phase 3B)
- `GET /api/v1/repos/{owner}/{repo}/timeline` — merged chronological feed of commits,
pipeline runs, and deployments; default 60 events, max 200
- `RepoTimelinePage` at `/repos/:owner/:repo/timeline` — vertical event feed with type
filter tabs (all / commits / runs / deployments)
- Sidebar "Timeline" nav item between Environments and Settings
- Event types: commit (SHA, message, author), run (status, ref, duration), deployment (env, status, SHA)
- Answers "what changed before things broke?" without navigating between separate pages
### Completed — Phase 3A (Environment model + deployment tracking)
- `Environment` model per repo (name, URL, protection rules)
- `Deployment` model (sha, ref, status, triggered_by, run_id link)
- Full CRUD API for environments
- Deployment trigger + status update API
- NATS event publishing for `deployment.*` subjects
- `EnvironmentsPage` per repo — environment cards with live deployment status
- Deployment history per environment
- Sidebar "Environments" nav item
- Repo page deployment status badges
---
### Completed — Phase 2C (CI Legibility)
- `PipelinesPage` — real cross-repo runs feed with status filter tabs
## [0.4.0] — 2026-05-11
Phase 2C complete. CI results are legible in the UI; the dashboard is an operational command center.
### Added — Pipeline Visualization
- `PipelinesPage` — cross-repo pipeline runs feed with status filter tabs (all / running / failed / succeeded)
- `RepoPipelinesPage` — repo-scoped runs list at `/repos/:owner/:repo/pipelines`
- `PipelineRunPage` — run detail with topological DAG visualization + step log viewer
- `PipelineWaterfall` — rewritten to accept real `PipelineJob[]` data with `needs` graph
- Dashboard CI widget — live recent runs replacing "coming soon" placeholder
- Command palette — pipeline run results + Pipelines quick-nav
- `GET /api/v1/pipelines/runs` — cross-repo recent runs endpoint
- Dashboard `recentRuns[]` field added
- `PipelineRunPage` — run detail with topological DAG visualization using real `PipelineJob[]` +
`needs` graph; step log viewer (collapsible per step, ANSI color, auto-scroll with lock toggle)
- `PipelineWaterfall` — rewritten to accept live job data instead of static mock stages
- `GET /api/v1/pipelines/runs` — cross-repo recent runs for the dashboard
### Planned — Phase 3 (GitOps + Observability + Federation)
- GitOps controller with reconciliation loops
- Environment model + deployment tracking
- Unified operational timeline (commits + deployments + CI failures merged)
- Drift detection and sync status
- Deployment promotion workflows (dev → staging → production)
- Rollback visualization and one-click rollbacks
- Canary and blue/green deployment support
- ActivityPub / ForgeFed federation handlers (inbox, outbox, cross-instance PRs)
- Secret management hierarchy (Global → Org → Repo → Env)
- Observability (Prometheus endpoint, health sparklines)
### Added — Dashboard CI Command Center
- Dashboard CI widget replaced "coming soon" with live recent pipeline runs
- Dashboard `recentRuns[]` field added to the `/api/v1/dashboard` response
### Planned — Phase 4
- AI diagnostics (pipeline failure root-cause analysis)
- Signed artifacts (Sigstore/Cosign)
- OCI package registry
- Secret and dependency vulnerability scanning
### Added — Command Palette Wiring
- Pipeline run results surfaced in command palette results
- "Pipelines" quick-nav action
---
@@ -75,38 +166,35 @@ Phase 2B complete. Full CI/CD execution backend operational.
### Added — CI Orchestrator (`internal/domain/ci/`)
- DAG-based pipeline orchestrator (`orchestrator.go`): subscribes to NATS `push.received`,
parses `.forgebucket/workflows/*.yml`, creates `PipelineRun`/`PipelineJob`/`PipelineStep`
records, advances DAG on `job.completed`/`job.failed`, recovers stale runs on startup
- Docker executor (`executor.go`): runs steps in isolated containers (`docker run --rm`),
streams logs to DB and NATS via `pipeline.log` subject, handles `git archive` workspace extraction
- Runner manager (`runner_manager.go`): semaphore-limited concurrent job dispatch (default 4),
subscribes to `job.queued`, calls executor when Docker is available
- DAG engine (`dag.go`): full topological sort (`TopoSort`) and `ReadyJobs` for dependency resolution
- Workflow parser (`parser.go`): reads `.forgebucket/workflows/*.yml` from git ref,
`MatchesPushTrigger` with glob pattern support
- CI types (`types.go`): `WorkflowFile`, `WorkflowJob`, `WorkflowStep`, YAML `StringOrSlice` unmarshaler
parses `.forgebucket/workflows/*.yml`, creates `PipelineRun/Job/Step` records, advances
DAG on `job.completed/failed`, recovers stale runs on startup
- Docker executor (`executor.go`): steps run in isolated containers (`docker run --rm`),
logs stream to DB and NATS via `pipeline.log`, workspace extracted via `git archive`
- Runner manager (`runner_manager.go`): semaphore-limited (default 4 concurrent),
subscribes to `job.queued`, skips gracefully if Docker is unavailable
- DAG engine (`dag.go`): `TopoSort`, `ReadyJobs`
- Workflow parser (`parser.go`): `.forgebucket/workflows/*.yml` from git ref,
`MatchesPushTrigger` with glob branch patterns; `StringOrSlice` YAML unmarshaler
### Added — CI API Handlers
- `GET /api/v1/repos/:owner/:repo/pipelines` list pipeline definitions
- `GET /api/v1/repos/:owner/:repo/runs` list pipeline runs (most recent first, limit 30)
- `GET /api/v1/repos/:owner/:repo/runs/:runID` — run detail with full job + step tree
- `POST /api/v1/repos/:owner/:repo/runs/:runID/cancel` — cancel queued or running run
- `POST /api/v1/repos/:owner/:repo/runs/:runID/jobs/:jobID/retry` — re-queue failed/cancelled job
- `GET /api/v1/repos/:owner/:repo/runs/:runID/jobs/:jobID/logs` — step-level log chunks
- `GET /api/v1/repos/:owner/:repo/runs/:runID/artifacts` — list artifacts for a run
- `POST /api/v1/repos/:owner/:repo/runs/:runID/artifacts` — upload artifact (multipart, 512 MB max)
- `GET /api/v1/repos/:owner/:repo/artifacts/:artifactID/download` — artifact download with path traversal guard
- `GET /api/v1/admin/runners` — list registered runners (admin-only)
- `POST /api/v1/admin/runners/register` — register a new runner with bcrypt token hashing (admin-only)
- `GET /api/v1/repos/:owner/:repo/pipelines` — pipeline definitions
- `GET /api/v1/repos/:owner/:repo/runs` — pipeline runs (newest first)
- `GET /api/v1/repos/:owner/:repo/runs/:runID` — run detail with job + step tree
- `POST /api/v1/repos/:owner/:repo/runs/:runID/cancel`
- `POST /api/v1/repos/:owner/:repo/runs/:runID/jobs/:jobID/retry`
- `GET /api/v1/repos/:owner/:repo/runs/:runID/jobs/:jobID/logs` — step log chunks
- `GET/POST /api/v1/repos/:owner/:repo/runs/:runID/artifacts`
- `GET /api/v1/repos/:owner/:repo/artifacts/:artifactID/download` — path-traversal guarded
- `GET/POST /api/v1/admin/runners` — runner list + registration (admin-only, bcrypt token)
### Added — Database Models (migration `009_ci`)
- `Pipeline` — workflow definition record (name, filePath, repoId)
- `PipelineRun` — execution record (triggerRef, triggerSha, triggeredBy, status, startedAt, finishedAt)
- `PipelineJob` — single DAG node (name, image, needs JSON, status, timing)
- `PipelineStep` — single command within a job (seq, runCmd, usesAction, exitCode, timing)
- `PipelineStepLog` — append-only log chunk storage (stepId, chunkIndex, content)
- `Runner` — registered execution backend (name, labels, status, tokenHash, lastSeenAt)
- `Artifact` — build artifact (runId, repoId, name, storagePath, size, contentType)
- `Pipeline`, `PipelineRun`, `PipelineJob`, `PipelineStep`, `PipelineStepLog`
- `Runner` (name, labels, status, tokenHash, lastSeenAt)
- `Artifact` (runId, repoId, name, storagePath, size, contentType)
### Changed — Git HTTP handler
- `parseAndCheckBody` replaces `checkProtectionsFromBody` — now also returns parsed
`refUpdate` structs for publishing `push.received` after each successful receive-pack
---
@@ -116,105 +204,81 @@ Phase 2A complete. Real-time event infrastructure and audit log operational.
### Added — NATS Event Bus (`internal/events/`)
- `EventBus` interface: `Publish`, `Subscribe`, `Close`
- `NATSBus`: NATS-backed implementation with auto-reconnect, max-reconnect disabled
- `NoOpBus`: silent fallback when `NATS_URL` is not configured (app fully functional without NATS)
- `New(url)` factory: returns `NATSBus` if URL is set, `NoOpBus` otherwise
- Event subjects defined in `subjects.go`:
- `repo.*` (created, deleted, pushed)
- `push.received`
- `pr.*` (opened, merged, closed)
- `issue.*` (opened, closed)
- `pipeline.*` (queued, started, succeeded, failed, cancelled)
- `job.*` (queued, started, completed, failed), `pipeline.log`
- `deployment.*`, `environment.*` (Phase 3 stubs)
- `audit.event`
- `NATSBus`: NATS-backed with auto-reconnect; `NoOpBus` fallback when `NATS_URL` unset
- `New(url)` factory: returns `NATSBus` or `NoOpBus`
- 40+ event subjects in `subjects.go` covering repo, push, PR, issue, pipeline, job,
deployment, environment, and audit namespaces
### Added — WebSocket Hub (`internal/api/handlers/ws.go`)
- `GET /ws`upgrades HTTP to WebSocket (nhooyr.io/websocket)
- Subscribes to all NATS subjects on connect, fans events to the client as JSON
- Optional session auth (`auth.Optional` middleware) — works for guests too
- Phase 2B note: per-user event filtering is a planned upgrade
### Added — WebSocket Hub
- `GET /ws`NATS wildcard subscription (`>`) fans all events to connected clients as JSON
- `{ subject, payload }` envelope format
- Goroutine per client with buffered send channel (64 events); slow clients drop events
### Added — Audit Log
- `AuditLog` model (migration `008_audit_log`): actor, method, path, statusCode, requestBody, ipAddr, timestamp
- `AuditLog` middleware: records every authenticated request to the DB and publishes `audit.event`
- `GET /api/v1/audit` — paginated audit log query (admin-only, filterable by actor/method/time range)
### Fixed — Local development environment
- `DATABASE_URL` was using Docker-internal hostname `postgres`; corrected to `localhost` for `make dev`
- Added `NATS_URL=nats://localhost:4222` to `.env` (was missing; CI orchestrator requires it)
- `REPO_ROOT` corrected to `/tmp/forgebucket/repos` (Docker path `/var/lib/forgebucket/repos` requires sudo on macOS)
### Added — Audit Log (migration `008_audit_log`)
- `AuditLog` model: actorId, actorName, method, path, statusCode, ipAddress, userAgent
- Middleware records every POST/PUT/PATCH/DELETE in the protected route group
- Writes DB row + publishes `audit.event` asynchronously (never blocks the response)
- `GET /api/v1/audit` — paginated, filterable by actor/method/since (admin-only)
---
## [0.1.0] — 2026-05-11
Initial development milestone. Core Git hosting, collaboration, and frontend SPA are functional.
Initial development milestone. Core Git hosting, collaboration, and frontend SPA functional.
### Added — Authentication & Security
- User registration and login with secure session cookies
- CSRF protection on all mutating routes via `X-CSRF-Token` header
- Middleware chain: Logger → RealIP → Recoverer → CORS → CSRF → SessionAuth → RBAC → Handler
- CSRF protection via double-submit cookie pattern (`X-CSRF-Token`)
- SSH key management per user
- OIDC / OAuth2 optional integration (configurable via env)
- Scoped access tokens with optional expiration dates
- Repository deploy keys (read-only or read-write HTTP tokens)
- ENV-driven config with fail-fast validation on missing secrets
- OIDC / OAuth2 optional integration
- Scoped access tokens with optional expiration
- Repository deploy keys (read-only or read-write)
- ENV-driven config with fail-fast on missing secrets
### Added — Git Hosting
- Smart HTTP transport (git clone, push, pull over HTTP)
- AGit protocol support (`refs/for/` push for instant PR creation without branch switching)
- Branch management (list, create, delete, default branch configuration)
- Commit log and diff viewing
- Git LFS per-repository (configurable file size limits, locking)
- Branch protection rules (force-push blocking, required reviews)
- Smart HTTP transport (clone, push, pull over HTTP)
- AGit protocol (`refs/for/` push for instant PR creation)
- Branch management, commit log, diff viewing
- Git LFS per-repository (configurable file size limits)
- Branch protection rules (force-push blocking)
- Repository visibility (public / private)
### Added — Collaboration
- Pull requests (open / merged / closed states) with author tracking
- Pull requests (open / merged / closed) with author tracking
- Issues (open / closed)
- Reviewer assignment (default reviewer per repo, per-PR reviewer assignment)
- Merge strategy selection per repository (merge commit / squash / rebase)
- Reviewer assignment (default reviewer per repo, per-PR overrides)
- Merge strategy selection per repository (merge / squash / rebase)
- Branching model configuration (feature / bugfix / release / hotfix prefixes)
- PR default description templates (per-repo)
- Excluded files from diffs (glob pattern configuration)
- PR default description templates + excluded-files configuration
- Webhook system with event filtering (push, pull_request, issue)
- Repository member RBAC (read / write / admin roles)
- Repository member RBAC (read / write / admin)
### Added — Frontend SPA
- React 18 + TypeScript + Vite, embedded into Go binary via `//go:embed`
- 20 route-level pages: Login, Register, Dashboard, Repos, CreateRepo, ImportRepo, Repo,
RepoSettings, Blob, Commits, Branches, RepoIssues, RepoPRs, CreatePR, PRDetail, Starred,
PRs (cross-repo), Pipelines (placeholder), Explore, Profile, Settings
- AppShell layout wrapper for all authenticated pages
- 20 route-level pages covering auth, dashboard, repos, code, PRs, issues, and settings
- Triple-state sidebar: expanded (320px) / collapsed (56px) / mobile bottom bar
- Mobile-first responsive design (375px → 1440px)
- DiffViewer: side-by-side and unified views with syntax highlighting
- MobileComment: bottom-sheet overlay for inline code review on mobile
- TreeBrowser: repository file tree navigation
- PipelineWaterfall: placeholder pipeline visualization component
- Skeleton loading states for perceived performance
- DiffViewer (side-by-side + unified), MobileComment (bottom-sheet), TreeBrowser
### Added — Design System
- Custom semantic token palette in `frontend/src/ui/tokens.ts`
- Full dark/light mode support via Tailwind CSS v4 `@variant dark`
- Brand colors: `#0052CC` (light) / `#3B82F6` (dark)
- 8px grid system (xs: 4px, sm: 8px, md: 16px, lg: 24px, xl: 32px, xxl: 48px)
- 44px minimum touch targets on all interactive elements (WCAG 2.5.5)
- Consistent border radius scale (subtle 38px, full 9999px)
- Full dark/light mode via Tailwind CSS v4 `@variant dark`
- 8px grid system; 44px minimum touch targets (WCAG 2.5.5)
- System font stack (Segoe UI, Roboto, sans-serif)
### Added — Infrastructure
- PostgreSQL + XORM with 7 migration files covering: users, repositories, issues, SSH keys,
access tokens, deploy keys, workflows, and LFS settings
- ActivityPub actor data model (FederationActor with inbox/outbox URLs and RSA key pairs) — data layer only
- Docker Compose setup for local PostgreSQL + NATS
- Makefile targets: dev, build, migrate, test, lint, docker-up
- WebSockets foundation for live logs and notifications
- PostgreSQL + XORM with migrations 001007
- ActivityPub actor data model (FederationActor) — data layer only
- Docker Compose for local PostgreSQL + NATS
- Makefile: dev, build, migrate, test, lint, docker-up
---
[Unreleased]: https://github.com/forgeo/forgebucket/compare/v0.3.0...HEAD
[Unreleased]: https://github.com/forgeo/forgebucket/compare/v0.7.0...HEAD
[0.7.0]: https://github.com/forgeo/forgebucket/compare/v0.6.0...v0.7.0
[0.6.0]: https://github.com/forgeo/forgebucket/compare/v0.5.0...v0.6.0
[0.5.0]: https://github.com/forgeo/forgebucket/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/forgeo/forgebucket/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/forgeo/forgebucket/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/forgeo/forgebucket/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/forgeo/forgebucket/releases/tag/v0.1.0