Documentation
¶
Overview ¶
Package bundles installs bundles: packages that group an application, its workflows, and its plugin dependencies into one .patchcord-bundle archive (vision document, section 9.3). A bundle install delegates to internal/apps and internal/runs rather than duplicating their logic — this package only orchestrates, in order: checking declared plugin dependencies are present, installing the embedded app (if any), and installing the embedded workflows.
Embedding connectors ("configuration" in the vision document's wording) is not modeled yet: internal/connectors has no file-based export/template mechanism today (ADR-0020), so there is nothing a bundle could portably carry beyond a non-secret connector's id and type — deferred to a later pass, not forgotten.
Index ¶
- Constants
- Variables
- func Pack(sourceDir string, key ed25519.PrivateKey, w io.Writer) error
- func Scaffold(dir, id, version string) error
- func ScaffoldVite(dir, id, version string) error
- type Bundle
- func Get(ctx context.Context, db *sql.DB, id string) (*Bundle, error)
- func InstallDir(ctx context.Context, db *sql.DB, dir string, knownActions map[string]struct{}) (*Bundle, error)
- func InstallPackage(ctx context.Context, db *sql.DB, dataDir, packagePath string, ...) (*Bundle, trust.PolicyResult, error)
- func List(ctx context.Context, db *sql.DB) ([]Bundle, error)
- type Manifest
Constants ¶
const ManifestFileName = "bundle.yaml"
ManifestFileName is the file a bundle's source/staging directory must contain at its root.
const PackageExtension = ".patchcord-bundle"
PackageExtension is the conventional file extension for a bundle package produced by Pack (vision document, section 9.3).
Variables ¶
var ErrInvalidManifest = errors.New("invalid bundle manifest")
ErrInvalidManifest is returned by ParseManifest and LoadManifest when the manifest is malformed or missing a required field.
var ErrNotFound = errors.New("bundle not found")
ErrNotFound is returned by Get when no bundle with the given id has been recorded.
Functions ¶
func Pack ¶
Pack archives sourceDir's bundle.yaml plus exactly the app/workflow files it references into w as a gzip-compressed tar stream, plus a checksums.json covering it (see internal/packaging.SignedArchive). If key is non-nil, the package is also signed — signing a bundle covers its embedded app and workflows too, so they are never separately verified again on install (see installEmbeddedApp). The result is what InstallPackage (and therefore `patchcord bundle install`) expects.
Pack never walks sourceDir itself: only the manifest's declared App subtree and Workflows files are staged and archived. sourceDir routinely holds things bundle.yaml does not declare — a Vite app's node_modules (which packaging.Archive would otherwise reject outright the moment it hit a symlink such as node_modules/.bin/esbuild), a .git directory, editor state — none of which belong in the package.
func Scaffold ¶
Scaffold writes a minimal bundle.yaml to dir, plus an embedded app (delegating to apps.Scaffold) and workflow (delegating to workflow.Scaffold) so the result is `bundle pack`-able as-is — requires_plugins starts empty since Scaffold has no way to know what plugin the bundle should depend on. It returns an error if dir already exists and is not empty.
func ScaffoldVite ¶
ScaffoldVite writes a bundle.yaml to dir, plus an embedded Vite + TypeScript project (delegating to apps.ScaffoldVite) and workflow (delegating to workflow.Scaffold). Unlike Scaffold, the result is not `bundle pack`-able as-is: the embedded app's patchcord-app.yaml only exists at app/dist after building it —
cd dir/app && npm install && npm run build
bundle.yaml's app field already points at app/dist, so `bundle pack`/`bundle dev [--watch]` need nothing further once that build has run at least once. It returns an error if dir already exists and is not empty.
Types ¶
type Bundle ¶
type Bundle struct {
ID string
Version string
Manifest string // raw bundle.yaml source
InstalledAt time.Time
}
Bundle is one installed bundle's provenance record: what was declared, not a second source of truth for the app/workflows it installed (see the package doc comment).
func Get ¶
Get returns one installed bundle's provenance record by id. It returns ErrNotFound if no bundle with that id has been recorded.
func InstallDir ¶
func InstallDir(ctx context.Context, db *sql.DB, dir string, knownActions map[string]struct{}) (*Bundle, error)
InstallDir installs a bundle straight from a source directory instead of a packaged .patchcord-bundle archive — no Pack/Extract round trip, no checksum, no signature: dir is local, unsigned-by-design source under active development. It backs `patchcord bundle dev` the same way apps.InstallOrUpdate backs `patchcord app dev`.
Unlike InstallPackage's installEmbeddedApp, the embedded app (if any) is installed in place via apps.InstallOrUpdate pointed straight at dir/manifest.App — never moved under dataDir/apps — so it ends up served live off dir exactly as `app dev` serves an app: rebuilding it (e.g. `vite build --watch`) needs no further agent involvement, and no dataDir argument is needed here at all.
Each embedded workflow goes through installWorkflowForDev (ADR-0055), not the strict installWorkflowIfChanged InstallPackage uses: redeclaring an already-installed version with byte-identical content is a silent no-op (as before, and for the same reason — `bundle dev --watch` reinstalls every embedded workflow on every change under dir, including ones a given save never touched), but redeclaring it with genuinely different content — edited the workflow's body, forgot to bump `version:` — is installed under the next unused version instead of rejected, so a save is never a hard failure. The source file is never rewritten; ADR-0008 immutability stays intact for InstallPackage (`bundle install`/`update`) and `workflow install`, which are unaffected by this.
requires_plugins is enforced exactly as InstallPackage enforces it: a missing dependency is not installed automatically.
func InstallPackage ¶
func InstallPackage(ctx context.Context, db *sql.DB, dataDir, packagePath string, knownActions map[string]struct{}, requireSignature bool) (*Bundle, trust.PolicyResult, error)
InstallPackage installs a bundle from a .patchcord-bundle archive (Pack's output). It orchestrates, in order:
- every "id@version" entry in requires_plugins must already be present in the plugin catalog — a bundle never auto-installs its plugin dependencies (see ADR-0044: this stays deferred even once a registry exists);
- the embedded app (if any) is moved to its permanent location under dataDir/apps/<app-id>/<app-version> and installed via apps.InstallOrUpdate — unlike apps.InstallPackage's own standalone .patchcord-app flow (which stays strict, see apps.Install), a bundle install has always been upsert-by-design at the top level (record(), below): re-running `bundle install`/`bundle update` on an already-installed bundle id must succeed and replace its embedded app in place, not fail with apps.ErrAlreadyExists (ADR-0044);
- each embedded workflow file is installed via installWorkflowIfChanged — workflow definitions need no on-disk home of their own, they live in the workflow_versions table (ADR-0008), and re-running install/ update with an unchanged workflow is a no-op rather than a rejection (only redeclaring a version with genuinely different content still hits ADR-0008's immutability rule).
The package is verified (internal/packaging.Verify) before anything is installed: a checksum mismatch or an invalid signature aborts unconditionally. requireSignature additionally rejects a package that is unsigned, or signed by a key not trusted for its id (internal/trust) — when false, InstallPackage still returns the verification outcome so the caller can warn about either case instead of failing outright. A bundle's signature covers its embedded app and workflows too — they are not separately re-verified (see installEmbeddedApp).
A failure partway through (e.g. the app installs but a workflow fails validation) is not rolled back: this first pass does not implement multi-resource transactions across three independently-catalogued resource kinds. The returned error names which step failed.
type Manifest ¶
type Manifest struct {
ID string
Version string
App string // relative path to the embedded app's source directory; empty if the bundle has no app.
Workflows []string // relative paths to embedded workflow YAML files.
// RequiresPlugins lists "id@version" plugin dependencies InstallPackage
// checks are already installed before proceeding — see the package doc
// comment: v1 validates, it does not auto-install.
RequiresPlugins []string
}
Manifest is the parsed content of a bundle's bundle.yaml.
func LoadManifest ¶
LoadManifest reads and parses dir's bundle.yaml.
func ParseManifest ¶
ParseManifest parses and validates a bundle manifest from its YAML source, returning ErrInvalidManifest if a required field is missing, empty, or malformed.