콘텐츠로 이동

자산 파이프라인 훅

자산 파이프라인은 build 단계 이후, GitHub Release 업로드 이전에 실행됩니다. 플러그인은 내부 구조를 그대로 반영한 여섯 개의 훅을 통해 각 단계를 가로챌 수 있습니다.

releaseAssets config
→ glob 매칭 → ResolvedAsset[]
→ [resolveAssets]: asset 목록을 필터링, 추가, 또는 교체
→ [transformAsset]: 각 asset을 서명, strip, 또는 분할 (1 → N)
→ [compressAsset]: 기본 tar.gz / zip 압축을 교체
→ [nameAsset]: 동적 로직으로 이름 템플릿을 덮어씀
→ sha256 해시
→ [generateChecksums]: checksum 파일을 asset 목록에 추가
→ GitHub Release 업로드 (기본 내장)
→ [uploadAssets]: 추가 대상(S3, R2, CDN 등)에 업로드
→ ReleaseContext 조립
→ [afterRelease]: 최종 asset 목록을 소비(brew, notify 등)
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[];
}

모든 훅은 PluginHooks에 추가되며, 표준 pubm.config.tsplugins 배열을 통해 등록합니다.

glob으로 매칭된 전체 asset 목록과 함께 한 번 호출됩니다. 이 훅을 사용해 asset을 필터링하거나, 프로그램적으로 찾은 파일을 추가하거나, 메타데이터를 붙입니다.

입력: ResolvedAsset[] - glob 매칭 결과와 파싱된 플랫폼 정보를 가진 asset 출력: ResolvedAsset[] - 새 asset 목록(이전 목록을 대체)

const filterPlugin: PubmPlugin = {
name: "filter-assets",
hooks: {
resolveAssets(resolved, ctx) {
// 지원되는 플랫폼의 asset만 포함
return resolved.filter(
(a) =>
(a.platform.os === "darwin" || a.platform.os === "linux") &&
a.platform.arch === "arm64",
);
},
},
};

압축 전에 각 asset마다 한 번 호출됩니다. 이 훅은 바이너리 서명, strip 실행, archive에 sidecar 파일 추가, 또는 하나의 입력을 여러 출력으로 분할할 때 사용합니다.

입력: ResolvedAsset - asset 하나 출력: TransformedAsset | TransformedAsset[] - 하나 이상의 변환된 asset

TransformedAsset는 선택적 extraFiles?: string[] 필드를 가진 ResolvedAsset 확장입니다. extraFiles에 나열된 파일은 압축 시 primary 파일과 함께 archive에 포함됩니다.

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;
},
},
};

배열을 반환하면 하나의 입력에서 여러 asset을 만들 수 있습니다. 각 요소는 파이프라인의 나머지 단계를 독립적으로 진행합니다.

hooks: {
async transformAsset(asset, ctx) {
if (asset.platform.os === "windows") {
// 일반 바이너리와 디버그 빌드 둘 다 생성
return [
asset,
{ ...asset, filePath: asset.filePath.replace(".exe", "-debug.exe") },
];
}
return asset;
},
},

기본 압축 로직을 완전히 대체합니다. 이 훅을 설정하면, 올바른 filePathcompressFormat을 가진 CompressedAsset을 만들어야 합니다.

입력: TransformedAsset 출력: CompressedAsset

pubm이 네이티브로 지원하지 않는 형식이 필요하거나, 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,
};
}
// 다른 플랫폼은 기본 동작으로 되돌림
return defaultCompress(asset);
},
},
};

최종 업로드 파일명을 반환합니다(compressFormat에서 확장자가 붙으므로, 여기서는 확장자를 포함하지 않습니다). 이름 템플릿 시스템이 충분히 표현력이 없을 때 사용합니다.

입력: CompressedAsset 출력: string - 확장자가 없는 파일명

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}`;
},
},
};

준비된 전체 asset 목록과 함께 호출됩니다. 이 훅을 사용해 checksum manifest 파일을 추가합니다.

입력: PreparedAsset[] - sha256 값을 포함한 모든 asset 출력: PreparedAsset[] - 원본 asset과 새 checksum 파일들

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" },
},
];
},
},
};

GitHub Release 업로드 이후, 같은 PreparedAsset[]와 함께 호출됩니다. 이 훅은 artifact를 추가 대상에 미러링할 때 사용합니다. 각 plugin의 결과는 모아서 이어 붙이며, 모든 plugin은 같은 입력 목록을 받습니다.

입력: PreparedAsset[] 출력: UploadedAsset[] - urltarget 필드를 가진 업로드된 asset

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",
};
}),
);
},
},
};

여러 플러그인이 같은 훅을 등록하면, 등록 순서대로 조합됩니다.

Hook조합 규칙
resolveAssets체인 방식 - plugin N의 출력이 plugin N+1의 입력
transformAsset체인 방식 - plugin N의 출력이 plugin N+1의 입력
compressAsset체인 방식 - plugin N의 출력이 plugin N+1의 입력
nameAsset마지막 값 우선 - 각 plugin은 같은 (asset, ctx)를 받으며 마지막 등록 plugin의 반환값이 사용됨
generateChecksums체인 방식 - plugin N의 출력이 plugin N+1의 입력
uploadAssets추가 방식 - 각 plugin은 같은 PreparedAsset[]를 독립적으로 받으며, 결과는 모두 이어 붙임

훅 체인에서 처리되지 않은 에러가 발생하면 파이프라인은 중단되고 onError 훅으로 전달됩니다.

전체 예시: 코드 서명 + checksum + S3

섹션 제목: “전체 예시: 코드 서명 + checksum + S3”
import { defineConfig } from "@pubm/core";
export default defineConfig({
releaseAssets: ["platforms/*/bin/mytool"],
plugins: [
// macOS 바이너리 서명
{
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;
},
},
},
// SHA256SUMS.txt 추가
checksumsPlugin,
// S3로 미러링
s3Plugin,
],
});