- Go 98.3%
- Nix 1.7%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .forgejo | ||
| .github/workflows | ||
| .vscode | ||
| .zed | ||
| internal | ||
| .envrc.project | ||
| .gitignore | ||
| action.yaml | ||
| AGENTS.md | ||
| flake.lock | ||
| flake.nix | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| main.go | ||
| README.md | ||
| treefmt.toml | ||
flake release
Generates release artifacts for packages in a nix flake:
dockerTools.buildLayeredImage&dockerTools.streamLayeredImagecan be uploaded to a container registry- packages have every non-empty output bundled into a
.tar.xz, or a.zipon Windows - archives contain a single top-level folder named after the asset, such as
app_1.2.3_linux_amd64/ outcontents are placed in that folder, while other split outputs keep their names, such asbin/,dev/, anddoc/- dynamic ELF executables in the
outandbinoutputs are patched with their non-glibc dependencies - shell scripts in the
outandbinoutputs are patched to find bundled files relative to themselves and other nix store programs throughPATH - Linux packages whose
meta.mainProgramis a script rather than a native binary can be bundled into an AppImage when explicitly enabled - Go, Cargo, npm, PyPI, Maven, and Gradle packages can be published from package source manifests
Packages are built and published in order of their platform's distance from the runner, so a slow cross-compiled build doesn't hold up the rest of the release: native packages and container images first, then package registries, then packages for another architecture on the same OS, another OS, and finally another OS and architecture. Container image destinations are validated before anything is built, and the combined image manifest is updated once every image is uploaded.
Runs that produce no releasable outputs fail without creating a release, unless no packages were selected and the flake provides no packages for the current system; then a release is created with only the changelog. Works with GitHub, Gitea & Forgejo
Usage
flake-release [packages...] [--dry-run] [--bundle-appimage]
Packages are flake attribute paths such as packages.x86_64-linux.server. If a package isn't found, the current system is inserted after its first attribute, so packages.server.x86_64-unknown-linux-musl resolves to packages.x86_64-linux.server.x86_64-unknown-linux-musl on x86_64-linux.
Environment
| Variable | Description | Default | Example |
|---|---|---|---|
| PACKAGES | Packages to release, separated by spaces or newlines; added to any arguments | all packages for the current system | default api |
| GIT_TYPE | Host type for release | detected from the CI environment or origin URL | github / gitea / forgejo |
| GIT_REPOSITORY | Repository to push releases | GITHUB_REPOSITORY in Actions, or from remote.origin.url |
spotdemo4/flake-release |
| GIT_SERVER_URL | Server to push releases | GITHUB_SERVER_URL in Actions, or from remote.origin.url |
https://github.com |
| GIT_ACTOR | User for Gitea & Forgejo | GITHUB_ACTOR in Actions, or Git user name |
github-actions[bot] |
| GIT_TOKEN | Token used to push releases | GITHUB_TOKEN |
|
| GITHUB_TOKEN | github.com access token for Nix, and release token when GIT_TOKEN is unset |
||
| TAG | Exact short release tag | CI tag event, or latest Git tag | packages/api/v1.2.3 |
| CONTAINER_REGISTRY | Container registry | ghcr.io on GitHub, GIT_SERVER_URL host |
ghcr.io |
| CONTAINER_REGISTRY_USERNAME | Username for container registry | GIT_ACTOR |
github-actions[bot] |
| CONTAINER_REGISTRY_PASSWORD | Password for container registry | GIT_TOKEN |
|
| PACKAGE_REGISTRY_OWNER | Package owner or namespace | owner from GIT_REPOSITORY |
spotdemo4 |
| PACKAGE_REGISTRY_URL | Registry URL override | host-specific | https://npm.pkg.github.com |
| PACKAGE_REGISTRY_USERNAME | Registry username | GIT_ACTOR |
github-actions[bot] |
| PACKAGE_REGISTRY_TOKEN | Package registry write token; enables package publishing | ||
| DRY_RUN | Validate and prepare releases without publishing or cleanup | false |
true |
| DELETE_OLD_RELEASE_ARTIFACTS | Cleanup release assets and image tags: false, true, or a release retention count |
false |
2 |
| BUNDLE_APPIMAGE | Bundle eligible Linux script packages as AppImages | false |
true |
GITHUB_REPOSITORY, GITHUB_SERVER_URL, and GITHUB_ACTOR are read only as fallbacks, since GitHub, Gitea, and Forgejo Actions runners set them; setting them by hand outside Actions is deprecated. GITHUB_TOKEN is always passed to Nix as the github.com access token, so on GitHub it can serve as both; on Gitea and Forgejo set the release token as GIT_TOKEN and use GITHUB_TOKEN only for a real GitHub token.
By default, packages are released as normal output archives. Enable automatic AppImage conversion with --bundle-appimage, BUNDLE_APPIMAGE=true, or the Action input below. Explicitly selected package outputs that already contain an .AppImage are uploaded as AppImages regardless of this setting.
Artifact retention
DELETE_OLD_RELEASE_ARTIFACTS (or the Action input delete_old_release_artifacts) accepts:
false,0,no,off, or unset: disable cleanup (the default).true,1,yes, oron: keep the current release's artifacts and clean up previous artifacts.- A positive integer
N: keep the current release plus the newestN-1older releases in the same tag namespace, ordered by version.
Values are case-insensitive. Invalid values, negative counts, and fractional counts fail before publication.
For example, DELETE_OLD_RELEASE_ARTIFACTS=2 when publishing v1.3.0 keeps the artifacts of v1.3.0 and v1.2.0 and deletes those of v1.1.0 and older. The count is per release, not per file, and includes releases without attachments since they may still have container images. Retained releases keep all attachments and container tags, the latest image tag is never deleted, and legacy tags with an empty version (v) don't count toward retention.
Cleanup runs only after the current release is published successfully, and never during dry runs. It deletes release attachments and old container tags, but not release records or package registry versions. Releases in other namespaces, or with versions equal to or newer than the current release, are left untouched.
Scoped release tags
Tags may include a namespace before the version, such as packages/api/v1.2.3. The full tag identifies the release, and changelog history and old-artifact cleanup are limited to that namespace.
The namespace is treated as a case-sensitive repository path: a scoped changelog only includes commits that touch that path (compared to their first parent). Unscoped tags include changes from the entire repository. For Go modules, the namespace must match the module path within the repository.
A namespace does not select Nix packages or filter source manifests, so for a scoped release pass only the packages that belong to that namespace.
Container images append the namespace to the repository path and use the version as the image tag. For example, packages/api/v1.2.3 publishes registry/owner/repo/packages/api:1.2.3-amd64 (per architecture), registry/owner/repo/packages/api:1.2.3 (combined manifest), and registry/owner/repo/packages/api:latest. Unscoped tags publish directly to registry/owner/repo. Container paths are lowercased, so namespaces that differ only by case share a container repository.
Package publishing
Package publishing is enabled by setting PACKAGE_REGISTRY_TOKEN. Every package manifest discovered in the selected sources is then published, limited to the kinds the registry host supports:
| Registry host | Go | Cargo | Gradle | Maven | npm | PyPI |
|---|---|---|---|---|---|---|
| Forgejo | yes | yes | yes | yes | yes | yes |
| Gitea | yes | yes | yes | yes | yes | yes |
| GitHub | no | no | yes | yes | yes | no |
Forgejo and Gitea use GIT_SERVER_URL by default. GitHub npm uses https://npm.pkg.github.com and GitHub Maven/Gradle use https://maven.pkg.github.com/{owner}/{repo} by default. PACKAGE_REGISTRY_URL overrides these defaults. GitHub npm packages must be published under the PACKAGE_REGISTRY_OWNER scope, so the staged package.json name is rewritten to @owner/name, adding the scope to an unscoped name or replacing a different scope; the name itself must be lowercase.
PACKAGE_REGISTRY_TOKEN never falls back to GIT_TOKEN, so publishing stays opt-in. On GitHub, the workflow's GITHUB_TOKEN can be passed as PACKAGE_REGISTRY_TOKEN when the job has packages: write and publishes to packages owned by or linked to the workflow's repository; publishing to another owner or repository requires a personal access token (classic) with write:packages. Forgejo and Gitea Actions tokens can't write packages, so they need a dedicated token with package write access. PACKAGE_REGISTRY_USERNAME defaults to GIT_ACTOR; Forgejo and Gitea require it for PyPI Basic authentication. Container registry credentials remain separate under CONTAINER_REGISTRY_USERNAME and CONTAINER_REGISTRY_PASSWORD.
Source discovery
For each requested Nix package, flake-release evaluates .#<package>.src and looks only at that source root for the requested manifests:
| Kind | Root manifest |
|---|---|
| Go | go.mod |
| Cargo | Cargo.toml |
| Gradle | build.gradle or build.gradle.kts |
| Maven | pom.xml |
| npm | package.json |
| PyPI | pyproject.toml |
Manifest discovery is not recursive. Sources shared by multiple Nix package attributes are published only once per package kind. Evaluated sources are copied to writable temporary staging directories before package tools run.
Manifests that opt out of publishing are skipped: an npm package.json with "private": true, and a Cargo.toml that sets publish = false or only defines a workspace. Any other discovered manifest must be publishable and match the release version, so opt out of publishing packages that are not meant for a registry. A Gradle build and a Maven pom.xml with the same coordinates in one source are rejected as duplicates.
Tools and versions
Package ecosystem tools are used from PATH when available:
- Go requires
go - Cargo requires
cargo - Gradle requires
gradleorgradlewwrapper - Maven requires
mvnormvnwwrapper - npm requires
npm; whenpackage-lock.jsonexists,npm ciinstalls dependencies before publishing - PyPI requires
uv, orpython3with thebuildandtwinemodules;uvis preferred when it is onPATH
A tool missing from PATH is built from the flake's nixpkgs input, or from the nixpkgs flake registry entry when the flake has no such input, along with the toolchain it needs (for example, rustc and a C compiler for cargo, or python3 for uv), so the stock Docker action can publish packages too. Tools that can't be found or built are fatal instead of silently skipping publication.
Package versions are strict: Go publishes the exact release tag, including a leading v; Cargo, Gradle, Maven, npm, and every built PyPI artifact must match the release tag after removing one leading v. Existing immutable or duplicate package versions are fatal conflicts, not idempotent success; this includes an HTTP 409 response from a Go registry. DELETE_OLD_RELEASE_ARTIFACTS does not delete package registry versions.
With --dry-run or DRY_RUN=true, nothing is written to a registry and old artifacts aren't cleaned up. When PACKAGE_REGISTRY_TOKEN is set, a dry run still does the following, without using the token:
- discovers sources and checks for required tools
- validates package metadata and versions
- prepares Go archives
- runs
cargo publish --dry-runandnpm publish --dry-run - builds and checks PyPI distributions with
twine checkoruv publish --dry-run - generates Maven and Gradle registry authentication
Install
Action
Check out the full Git history and tags with fetch-depth: 0. flake-release uses them to find the previous release tag for the changelog, and to find the latest tag when the run is not triggered by a tag. A shallow checkout produces an incomplete changelog.
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Release
uses: spotdemo4/flake-release@v0.38.0
with:
packages: # default: all
git_type: # default: detected
git_repository: # default: ${{ github.repository }}
git_server_url: # default: ${{ github.server_url }}
git_actor: # default: ${{ github.actor }}
git_token: # default: ${{ github.token }}
github_token: # github.com token for Nix; default: git_token when releasing to github.com
container_registry: # default: ghcr.io on GitHub, server host otherwise
container_registry_username: # default: ${{ github.actor }}
container_registry_password: # default: ${{ github.token }}
package_registry_owner: # default: repository owner
package_registry_url: # default: host-specific registry
package_registry_username: # default: ${{ github.actor }}
package_registry_token: # dedicated package write token; enables package publishing
delete_old_release_artifacts: # false (default), true, or a count including current (e.g. "2")
bundle_appimage: # default: false
Nix
nix run github:spotdemo4/flake-release
Flake
inputs = {
flake-release = {
url = "github:spotdemo4/flake-release";
};
};
outputs = { nixpkgs, flake-release, ... }: {
devShells.x86_64-linux.default = nixpkgs.legacyPackages.x86_64-linux.mkShell {
packages = [ flake-release.packages.x86_64-linux.default ];
};
}
also available from the nix user repository as nur.repos.trev.flake-release
Docker
docker run -it \
-v "$(pwd):/app" \
-w /app \
-v "$HOME/.ssh:/root/.ssh" \
-e GIT_TOKEN=... \
-e GIT_REPOSITORY=... \
-e CONTAINER_REGISTRY=... \
-e CONTAINER_REGISTRY_USERNAME=... \
-e CONTAINER_REGISTRY_PASSWORD=... \
-e PACKAGE_REGISTRY_TOKEN=... \
-e BUNDLE_APPIMAGE=true \
ghcr.io/spotdemo4/flake-release:0.38.0