actionspack is a lockfile-first GitHub Actions workflow packer. It lets you
author workflows in .github/workflows/src/, lock every remote workflow/action
dependency in .github/workflow.lock.yml, and generate pinned workflows in
.github/workflows/.
It currently supports inlining composite actions and safely transformable reusable workflows. JavaScript and Docker actions are pinned as external dependencies instead of being bundled.
GitHub Actions workflows often depend on reusable workflows and actions from
other repositories. You may want to author those dependencies with convenient
floating refs like @main in .github/workflows/src/, but generated workflows
should be reproducible and reviewable.
actionspack gives workflows a lockfile mechanism similar to pnpm. It locks
remote workflows and actions in .github/workflow.lock.yml, inlines everything
that can be transformed safely into the local repository, and pins anything that
cannot be inlined to a fixed SHA.
To update workflow and action dependencies, run actionspack update
periodically. The updated lockfile and generated workflows are normal repository
files, so git diff shows exactly which dependencies changed and what generated
workflow output changed.
npm i actionspackPut authored workflows under .github/workflows/src/:
# .github/workflows/src/ci.yml
name: CI
on:
push:
jobs:
test:
uses: owner/repo/.github/workflows/test.yml@mainThen run:
npx actionspackactionspack defaults to actionspack pack. It writes:
.github/workflow.lock.yml.github/workflows/ci.yml
Generated workflows are safe to commit. Existing lockfile SHAs are reused until
you explicitly run actionspack update.
When you want to refresh workflow/action dependencies:
npx actionspack update
git diffReview the dependency SHA changes in .github/workflow.lock.yml and the
resulting generated workflow changes before committing.
Generated workflows should not be edited by hand. Consider marking them as read-only in your workspace settings:
{
"files.readonlyInclude": {
".github/workflows/*.yml": true
}
}actionspack packScan source workflows, resolve missing dependencies, update the lockfile, and write generated workflows.
actionspack scanUpdate the lockfile graph shape only. This adds newly discovered dependencies and removes unreachable ones without refreshing existing SHAs.
actionspack update [package]Refresh all locked dependencies, or only the selected package. By default this
also packs workflows. Use --lockfile-only to update only
.github/workflow.lock.yml.
actionspack verifyCheck that generated workflows are current and contain no unsupported unpinned remote references.
actionspack tree
actionspack why <package>
actionspack diff
actionspack diff --jsonInspect the lockfile dependency tree, explain why a package is present, or
compare the current lockfile with HEAD.
actionspack.yml is optional. Without it, actionspack discovers
.github/workflows/src/*.yml and .github/workflows/src/*.yaml, then writes
matching generated workflows to .github/workflows/*.yml.
Use explicit entries when you need custom paths:
$schema: ./actionspack.schema.json
entries:
- source: .github/workflows/src/ci.yml
output: .github/workflows/ci.ymlUse external to pin a workflow or action without bundling it:
external:
- actions/checkout
- owner/repo/pathThe same configuration can be supplied through CLI flags:
actionspack pack \
--entry .github/workflows/src/ci.yml:.github/workflows/ci.yml \
--external actions/checkoutComposite actions are recursively inlined when runs.using is composite.
Inputs are substituted from caller with values or action defaults. Missing
required inputs fail closed.
Reusable workflows are inlined only when they use workflow_call and can be
transformed into local jobs deterministically. Unsupported cases, unresolved
refs, unsafe reusable workflows, and leftover remote uses fail closed.
JavaScript actions, Docker actions, and docker:// references are not bundled
yet. They are pinned to locked SHAs as external dependencies.
import { diff, pack, scan, tree, update, verify, why } from 'actionspack'
await pack()
await update({ packageName: 'owner/repo', lockfileOnly: true })
await verify()MIT License © 2026-PRESENT Kevin Deng