generates release artifacts for packages in a nix flake
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
renovate ddf69263f9
All checks were successful
check / check (push) Successful in 44s
chore(deps): update go toolchain directive to v1.27.2 (#93)
2026-10-09 05:57:31 -04:00
.forgejo chore(deps): update spotdemo4/nix-init action to v1.67.0 (#92) 2026-10-08 05:53:35 -04:00
.github/workflows chore(deps): update spotdemo4/nix-init action to v1.67.0 (#92) 2026-10-08 05:53:35 -04:00
.vscode refactor: migrate flake-release from shell scripts to Go 2026-06-02 05:53:00 -04:00
.zed chore(zed): add workspace settings for nix and formatting 2026-06-02 08:25:25 -04:00
internal feat: release cross-compiled packages last (#90) 2026-10-05 05:50:30 -04:00
.envrc.project style: dotenv 2026-01-21 09:15:35 -05:00
.gitignore refactor: migrate flake-release from shell scripts to Go 2026-06-02 05:53:00 -04:00
action.yaml bump: v0.37.1 -> v0.38.0 2026-10-05 22:01:43 +00:00
AGENTS.md chore: add version control reqs 2026-09-25 02:16:58 -04:00
flake.lock chore(deps): lock file maintenance (#88) 2026-10-04 07:22:00 -04:00
flake.nix bump: v0.37.1 -> v0.38.0 2026-10-05 22:01:43 +00:00
go.mod chore(deps): update go toolchain directive to v1.27.2 (#93) 2026-10-09 05:57:31 -04:00
go.sum fix(deps): update module github.com/go-git/go-git/v6 to v6.0.0-beta.1 (#89) 2026-10-05 05:49:42 -04:00
LICENSE init 2025-12-05 01:21:30 -05:00
main.go Improve release output formatting (#57) 2026-09-15 21:14:24 -04:00
README.md bump: v0.37.1 -> v0.38.0 2026-10-05 22:01:43 +00:00
treefmt.toml refactor: migrate flake-release from shell scripts to Go 2026-06-02 05:53:00 -04:00

NixOS flake release

check vulnerable nixpkgs go

Generates release artifacts for packages in a nix flake:

  • dockerTools.buildLayeredImage & dockerTools.streamLayeredImage can be uploaded to a container registry
  • packages have every non-empty output bundled into a .tar.xz, or a .zip on Windows
  • archives contain a single top-level folder named after the asset, such as app_1.2.3_linux_amd64/
  • out contents are placed in that folder, while other split outputs keep their names, such as bin/, dev/, and doc/
  • dynamic ELF executables in the out and bin outputs are patched with their non-glibc dependencies
  • shell scripts in the out and bin outputs are patched to find bundled files relative to themselves and other nix store programs through PATH
  • Linux packages whose meta.mainProgram is 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, or on: keep the current release's artifacts and clean up previous artifacts.
  • A positive integer N: keep the current release plus the newest N-1 older 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 gradle or gradlew wrapper
  • Maven requires mvn or mvnw wrapper
  • npm requires npm; when package-lock.json exists, npm ci installs dependencies before publishing
  • PyPI requires uv, or python3 with the build and twine modules; uv is preferred when it is on PATH

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-run and npm publish --dry-run
  • builds and checks PyPI distributions with twine check or uv 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

Downloads

https://trev.zip/llc/flake-release/releases