Skip to content

Asset Pipeline Hooks

The asset pipeline runs after the build step and before GitHub Release upload. Plugins can intercept every stage through six hooks that mirror the pipeline structure.

releaseAssets config
→ glob matching → ResolvedAsset[]
→ [resolveAssets] : filter, add, or replace the asset list
→ [transformAsset] : sign, strip, or split each asset (1 → N)
→ [compressAsset] : replace default tar.gz / zip compression
→ [nameAsset] : override the name template with dynamic logic
→ sha256 hash
→ [generateChecksums] : append checksum files to the asset list
→ GitHub Release upload (built-in)
→ [uploadAssets] : upload to additional targets (S3, R2, CDN, etc.)
→ ReleaseContext assembled
→ [afterRelease] : consume final asset list (brew, notify, etc.)
interface AssetPipelineHooks {
resolveAssets?(
resolved: ResolvedAsset[],
ctx: PubmContext,
): Promise<ResolvedAsset[]> | ResolvedAsset[];
transformAsset?(
asset: ResolvedAsset,
ctx: PubmContext,
): Promise<TransformedAsset | TransformedAsset[]>
| TransformedAsset
| TransformedAsset[];
compressAsset?(
asset: TransformedAsset,
ctx: PubmContext,
): Promise<CompressedAsset> | CompressedAsset;
nameAsset?(
asset: CompressedAsset,
ctx: PubmContext,
): string;
generateChecksums?(
assets: PreparedAsset[],
ctx: PubmContext,
): Promise<PreparedAsset[]> | PreparedAsset[];
uploadAssets?(
assets: PreparedAsset[],
ctx: PubmContext,
): Promise<UploadedAsset[]> | UploadedAsset[];
}

All hooks are added to PluginHooks and registered through the standard plugins array in pubm.config.ts.

Called once with the full list of glob-matched assets. Use this hook to filter assets, add programmatically discovered files, or attach metadata.

Input: ResolvedAsset[]: assets from glob matching with parsed platform info Output: ResolvedAsset[]: the new asset list (replaces the previous list)

const filterPlugin: PubmPlugin = {
name: "filter-assets",
hooks: {
resolveAssets(resolved, ctx) {
// Only include assets for supported platforms
return resolved.filter(
(a) =>
(a.platform.os === "darwin" || a.platform.os === "linux") &&
a.platform.arch === "arm64",
);
},
},
};

Called once per asset before compression. Use this hook to sign binaries, run strip, add sidecar files to an archive, or split one input into multiple outputs.

Input: ResolvedAsset: one asset Output: TransformedAsset | TransformedAsset[]: one or more transformed assets

TransformedAsset extends ResolvedAsset with an optional extraFiles?: string[] field. Files listed in extraFiles are bundled into the archive alongside the primary file during compression.

const codeSignPlugin: PubmPlugin = {
name: "code-sign",
hooks: {
async transformAsset(asset, ctx) {
if (asset.platform.os === "darwin") {
await exec("codesign", [
"--sign", process.env.SIGNING_IDENTITY,
"--options", "runtime",
asset.filePath,
]);
}
return asset;
},
},
};

Return an array to produce multiple assets from one input. Each element proceeds through the rest of the pipeline independently.

hooks: {
async transformAsset(asset, ctx) {
if (asset.platform.os === "windows") {
// Produce both a regular binary and a debug build
return [
asset,
{ ...asset, filePath: asset.filePath.replace(".exe", "-debug.exe") },
];
}
return asset;
},
},

Replaces the built-in compression logic entirely. When set, the hook is responsible for producing a CompressedAsset with the correct filePath and compressFormat.

Input: TransformedAsset Output: CompressedAsset

Use this when you need a format that pubm does not support natively, or when you want to bundle additional files into the archive.

const customArchivePlugin: PubmPlugin = {
name: "deb-package",
hooks: {
async compressAsset(asset, ctx) {
if (asset.platform.os === "linux") {
const debPath = await buildDebPackage(asset.filePath, ctx.version);
return {
...asset,
filePath: debPath,
originalPath: asset.filePath,
compressFormat: false,
};
}
// Fall back to default for other platforms
return defaultCompress(asset);
},
},
};

Returns the final upload filename without the extension. The extension is appended from compressFormat. Use this hook when the name template system is not expressive enough.

Input: CompressedAsset Output: string: the filename without extension

const dateStampPlugin: PubmPlugin = {
name: "date-stamp",
hooks: {
nameAsset(asset, ctx) {
const date = new Date().toISOString().slice(0, 10).replace(/-/g, "");
return `${ctx.packageName}-${ctx.version}-${date}-${asset.platform.raw}`;
},
},
};

Called with the complete list of prepared assets. Use this hook to append a checksum manifest file.

Input: PreparedAsset[]: all assets including their sha256 values Output: PreparedAsset[]: original assets plus any new checksum files

import { writeFileSync } from "node:fs";
import { join } from "node:path";
const checksumsPlugin: PubmPlugin = {
name: "checksums",
hooks: {
async generateChecksums(assets, ctx) {
const lines = assets
.map((a) => `${a.sha256} ${a.name}`)
.join("\n");
const checksumPath = join(ctx.runtime.tempDir, "SHA256SUMS.txt");
writeFileSync(checksumPath, lines + "\n");
const sha256 = await computeSha256(checksumPath);
return [
...assets,
{
filePath: checksumPath,
originalPath: checksumPath,
name: "SHA256SUMS.txt",
sha256,
platform: { raw: "" },
compressFormat: false,
config: { path: "", compress: false, name: "SHA256SUMS.txt" },
},
];
},
},
};

Called after GitHub Release upload with the same PreparedAsset[]. Use this hook to mirror artifacts to additional targets. Each plugin’s results are collected and concatenated. All plugins receive the same input list.

Input: PreparedAsset[] Output: UploadedAsset[]: uploaded assets with url and target fields

import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { readFileSync } from "node:fs";
const s3Plugin: PubmPlugin = {
name: "s3-mirror",
hooks: {
async uploadAssets(assets, ctx) {
const s3 = new S3Client({ region: "us-east-1" });
const bucket = process.env.RELEASE_BUCKET;
return Promise.all(
assets.map(async (asset) => {
const key = `releases/${ctx.version}/${asset.name}`;
await s3.send(
new PutObjectCommand({
Bucket: bucket,
Key: key,
Body: readFileSync(asset.filePath),
}),
);
return {
...asset,
url: `https://${bucket}.s3.amazonaws.com/${key}`,
target: "s3",
};
}),
);
},
},
};

When multiple plugins register the same hook, they are composed in registration order:

HookComposition rule
resolveAssetsChained: output of plugin N is input of plugin N+1
transformAssetChained: output of plugin N is input of plugin N+1
compressAssetChained: output of plugin N is input of plugin N+1
nameAssetLast-wins: each plugin receives the same (asset, ctx) arguments; the last registered plugin’s return value is used
generateChecksumsChained: output of plugin N is input of plugin N+1
uploadAssetsAdditive: each plugin receives the original PreparedAsset[] independently; all results are concatenated

Any uncaught error in a hook chain aborts the pipeline and routes to the onError hook.

Complete example: code signing + checksums + S3

Section titled “Complete example: code signing + checksums + S3”
import { defineConfig } from "@pubm/core";
export default defineConfig({
releaseAssets: ["platforms/*/bin/mytool"],
plugins: [
// Sign macOS binaries
{
name: "code-sign",
hooks: {
async transformAsset(asset, ctx) {
if (asset.platform.os === "darwin") {
await exec("codesign", ["--sign", process.env.SIGNING_IDENTITY, asset.filePath]);
}
return asset;
},
},
},
// Append SHA256SUMS.txt
checksumsPlugin,
// Mirror to S3
s3Plugin,
],
});