Tinfoil.Publish (tinfoil v0.2.22)

Copy Markdown View Source

Create a GitHub Release and upload archive assets to it.

Tinfoil's own replacement for softprops/action-gh-release in the generated workflow. It uses Req to talk to GitHub's REST API directly, so the release lifecycle (create → upload assets → handle existing releases) happens inside the tool rather than inside CI-specific third-party actions.

The generated workflow calls this module via mix tinfoil.publish once, after the build matrix finishes, on a single ubuntu-latest runner.

Homebrew formula publishing is handled separately by Tinfoil.Homebrew -- the generated workflow runs mix tinfoil.homebrew in a follow-up job once the GitHub Release exists.

Summary

Types

Known error atoms returned by publish/2. Callers can pattern-match on these; the catch-all is {:error, term()} for unexpected failures.

How publish/2 obtains the release it uploads to.

Functions

Publish release archives from input_dir (default "artifacts") to a new GitHub Release on the repo configured in the tinfoil config.

Types

error()

@type error() ::
  :missing_github_token
  | :missing_tag
  | :release_already_exists_no_replace
  | :release_not_found_for_attach
  | :attach_and_replace
  | {:missing_input_dir, Path.t()}
  | {:create_release_failed, non_neg_integer(), term()}
  | {:find_release_failed, non_neg_integer(), term()}
  | {:delete_release_failed, non_neg_integer(), term()}
  | {:upload_failed, String.t(), non_neg_integer(), term()}
  | {:create_release_error, term()}
  | {:find_release_error, term()}
  | {:delete_release_error, term()}
  | {:upload_error, String.t(), term()}
  | String.t()

Known error atoms returned by publish/2. Callers can pattern-match on these; the catch-all is {:error, term()} for unexpected failures.

  • :missing_github_token -- no GITHUB_TOKEN / GH_TOKEN env var
  • :missing_tag -- no tag given and GITHUB_REF_NAME is unset
  • :release_already_exists_no_replace -- release for the tag already exists and neither attach: true nor replace: true was passed
  • :release_not_found_for_attach -- attach: true was passed but no release exists for the tag
  • :attach_and_replace -- both attach: true and replace: true were passed; they are mutually exclusive
  • {:missing_input_dir, dir} -- input directory doesn't exist
  • {:create_release_failed, status, body} -- GitHub API refused the release create (non-201, non-422)
  • {:find_release_failed, status, body} -- lookup for existing release during --replace failed
  • {:delete_release_failed, status, body} -- delete during --replace failed
  • {:upload_failed, name, status, body} -- asset upload returned non-2xx
  • {:create_release_error, reason} -- transport failure (no HTTP response) while creating the release
  • {:find_release_error, reason} -- transport failure while looking up the existing release during --replace
  • {:delete_release_error, reason} -- transport failure while deleting the existing release during --replace
  • {:upload_error, name, reason} -- asset upload transport failure
  • "... :github :repo is unresolved ..." (string) -- the tinfoil github.repo config isn't set and couldn't be inferred from git

mode()

@type mode() :: :create | :attach | :replace

How publish/2 obtains the release it uploads to.

  • :create -- create a new release for the tag (default)
  • :attach -- upload to a release something else already created
  • :replace -- delete the existing release and create a fresh one

opts()

@type opts() :: [
  input_dir: Path.t(),
  tag: String.t() | nil,
  draft: boolean() | nil,
  replace: boolean() | nil,
  attach: boolean() | nil,
  dry_run: boolean() | nil,
  req: Req.Request.t() | nil
]

preview()

@type preview() :: %{
  dry_run: true,
  repo: String.t(),
  tag: String.t(),
  mode: mode(),
  draft: boolean(),
  prerelease: boolean(),
  replace: boolean(),
  attach: boolean(),
  assets: [%{name: String.t(), path: Path.t(), size: non_neg_integer()}]
}

result()

@type result() :: %{
  mode: mode(),
  release_id: integer(),
  html_url: String.t(),
  uploaded: [String.t()]
}

Functions

publish(config, opts \\ [])

@spec publish(Tinfoil.Config.t(), opts()) ::
  {:ok, result()} | {:ok, preview()} | {:error, error() | term()}

Publish release archives from input_dir (default "artifacts") to a new GitHub Release on the repo configured in the tinfoil config.

The set of assets uploaded is every *.tar.gz or *.zip in input_dir, plus the combined checksums-sha256.txt produced from their .sha256 sidecars.

Authentication

Requires a GITHUB_TOKEN environment variable (or a GH_TOKEN fallback) with contents: write permission on the target repo.

Tag

The tag to release against is taken from opts[:tag] if given, otherwise from the GITHUB_REF_NAME environment variable (which CI sets automatically on tag pushes). Versions matching -rc, -beta, or -alpha are auto-marked as prerelease.

Existing releases

By default, if a release for tag already exists, publish/2 returns {:error, :release_already_exists_no_replace} without touching the existing release or its assets — failing fast is safer than silently clobbering something a user already shipped.

Two escape hatches, which are mutually exclusive:

attach: true (or --attach) uploads to the release that already exists and changes nothing about it — body, prerelease flag, and draft state are left exactly as the other tool wrote them. This is the mode for projects where something else owns release creation: release-please tags the version, creates the release, and writes the curated changelog as the body, and tinfoil only adds the binaries. It errors with :release_not_found_for_attach when no release exists, since attaching to nothing is always a mistake.

replace: true (or --replace) deletes and recreates the existing release, which discards whatever body it had. The git tag itself is never touched; only the release object and its attached assets are removed before the new release is created. Use this for development and force-retag iteration loops, not for published versions.

Attach mode only makes sense when the workflow runs after the release exists. Set trigger: :release_published in the tinfoil config so the generated workflow fires on the release event rather than racing it on the tag push.