Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ outputs:
description: |
The commit SHA of the release if a release was made, otherwise an empty string

id:
description: |
The release ID from the remote VCS, if a release was made. If no release was made,
this will be an empty string.

is_prerelease:
description: |
"true" if the version is a prerelease, "false" otherwise
Expand Down Expand Up @@ -153,6 +158,25 @@ outputs:
description: |
The Git tag corresponding to the version output

upload_url:
description: |
The URL for uploading additional assets to the release, if a release was made.
If no release was made, this will be an empty string. This can be used with
actions like actions/upload-release-asset to add more files to the release.

assets:
description: |
A JSON array containing information about each uploaded asset. Each asset object
includes metadata such as name, size, content_type, browser_download_url, etc.
If no release was made, this will be an empty JSON array ([]).

assets_dist:
description: |
A JSON object containing information about Python distribution assets (wheel and sdist),
organized by type. Keys are 'wheel' for .whl files and 'sdist' for .tar.gz files.
Each value is an asset object with metadata. If no release was made or no dist assets
were uploaded, this will be an empty JSON object ({}).

version:
description: |
The newly released version if one was made, otherwise the current version
Expand Down
155 changes: 155 additions & 0 deletions docs/configuration/automatic-releases/github-actions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,107 @@ Example: ``v1.2.3``

----

.. _gh_actions-psr-outputs-id:

``id``
""""""

**Type:** ``string``

The release ID from the GitHub API if a release was made, otherwise an empty string.
This can be used in subsequent workflow steps to interact with the release via the
GitHub API.

Example upon release: ``123456789``
Example when no release was made: ``""``

----

.. _gh_actions-psr-outputs-upload_url:

``upload_url``
""""""""""""""

**Type:** ``string``

The upload URL for the release from the GitHub API if a release was made, otherwise
an empty string. This URL can be used to upload additional assets to the release.

Example upon release: ``https://uploads.github.com/repos/user/repo/releases/123456789/assets{?name,label}``
Example when no release was made: ``""``

----

.. _gh_actions-psr-outputs-assets:

``assets``
""""""""""

**Type:** ``string`` (JSON array)

A JSON array of asset metadata objects that were uploaded to the release. Each object
contains information about an uploaded asset including its name, browser_download_url,
and other GitHub API metadata. If no release was made or no assets were uploaded, this
will be an empty JSON array ``[]``.

Example upon release with assets:

.. code-block:: json

[
{
"name": "package-1.2.3-py3-none-any.whl",
"browser_download_url": "https://github.com/user/repo/releases/download/v1.2.3/package-1.2.3-py3-none-any.whl",
"size": 123456,
...
},
{
"name": "package-1.2.3.tar.gz",
"browser_download_url": "https://github.com/user/repo/releases/download/v1.2.3/package-1.2.3.tar.gz",
"size": 234567,
...
}
]

Example when no release was made: ``[]``

----

.. _gh_actions-psr-outputs-assets_dist:

``assets_dist``
"""""""""""""""

**Type:** ``string`` (JSON object)

A JSON object containing Python distribution assets organized by type (``wheel`` and ``sdist``).
This is a convenience output that categorizes the assets from the ``assets`` output into
their distribution types. If no distribution assets were uploaded, this will be an empty
JSON object ``{}``.

Example upon release with distribution assets:

.. code-block:: json

{
"wheel": {
"name": "package-1.2.3-py3-none-any.whl",
"browser_download_url": "https://github.com/user/repo/releases/download/v1.2.3/package-1.2.3-py3-none-any.whl",
"size": 123456,
...
},
"sdist": {
"name": "package-1.2.3.tar.gz",
"browser_download_url": "https://github.com/user/repo/releases/download/v1.2.3/package-1.2.3.tar.gz",
"size": 234567,
...
}
}

Example when no release was made: ``{}``

----

.. _gh_actions-publish:

Python Semantic Release Publish Action
Expand Down Expand Up @@ -1021,6 +1122,60 @@ The equivalent GitHub Action configuration would be:

.. _Publish Action Manual Release Workflow: https://github.com/python-semantic-release/publish-action/blob/main/.github/workflows/release.yml

Using Release Assets Outputs
-----------------------------

The Python Semantic Release Action provides outputs for release assets that can be used
in subsequent workflow steps. This example demonstrates how to access asset information
and download specific distribution files for further processing.

.. code:: yaml

# snippet

- name: Action | Semantic Version Release
id: release
uses: python-semantic-release/python-semantic-release@v10.5.3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}

- name: Publish | Upload to GitHub Release Assets
uses: python-semantic-release/publish-action@v10.5.3
if: steps.release.outputs.released == 'true'
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ steps.release.outputs.tag }}

# Example: Download wheel from the release assets
- name: Process | Download wheel asset
if: steps.release.outputs.released == 'true'
run: |
# Parse assets_dist JSON output to get wheel download URL
WHEEL_URL=$(echo '${{ steps.release.outputs.assets_dist }}' | jq -r '.wheel.browser_download_url')
if [ -n "$WHEEL_URL" ]; then
echo "Downloading wheel from: $WHEEL_URL"
curl -L -o wheel.whl "$WHEEL_URL"
fi

# Example: List all uploaded assets
- name: Display | Show all release assets
if: steps.release.outputs.released == 'true'
run: |
echo "Release ID: ${{ steps.release.outputs.id }}"
echo "Upload URL: ${{ steps.release.outputs.upload_url }}"
echo "All assets:"
echo '${{ steps.release.outputs.assets }}' | jq -r '.[] | " - \(.name) (\(.size) bytes)"'

# Example: Use release ID to add a comment via GitHub API
- name: Comment | Add release comment
if: steps.release.outputs.released == 'true'
run: |
curl -X POST \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/${{ github.repository }}/releases/${{ steps.release.outputs.id }}/assets \
-d '{"body": "Release artifacts are now available!"}'

.. _gh_actions-monorepo:

Actions with Monorepos
Expand Down
17 changes: 15 additions & 2 deletions src/semantic_release/cli/commands/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,9 @@ def build_distributions(
rprint("[bold green]Build completed successfully!")
except subprocess.CalledProcessError as exc:
logger.exception(exc)
logger.error("Build command failed with exit code %s", exc.returncode) # noqa: TRY400
logger.error(
"Build command failed with exit code %s", exc.returncode
) # noqa: TRY400
raise BuildDistributionsError from exc


Expand Down Expand Up @@ -826,13 +828,24 @@ def version( # noqa: C901
exception: Exception | None = None
help_message = ""
try:
hvcs_client.create_release(
release_result = hvcs_client.create_release(
tag=new_version.as_tag(),
release_notes=release_notes,
prerelease=new_version.is_prerelease,
assets=assets,
noop=opts.noop,
)
# Update GitHub Actions output with release information
# Only Github returns ReleaseInfo with asset details
if isinstance(hvcs_client, Github):
from semantic_release.hvcs.github import ReleaseInfo

if isinstance(release_result, ReleaseInfo):
gha_output.release_id = release_result.id
gha_output.upload_url = release_result.upload_url
gha_output.assets = release_result.assets
elif isinstance(release_result, int):
gha_output.release_id = release_result
except HTTPError as err:
exception = err
except UnexpectedResponse as err:
Expand Down
72 changes: 69 additions & 3 deletions src/semantic_release/cli/github_actions_output.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import json
import os
from enum import Enum
from re import compile as regexp
Expand Down Expand Up @@ -31,6 +32,9 @@ def __init__(
commit_sha: str | None = None,
release_notes: str | None = None,
prev_version: Version | None = None,
release_id: int | None = None,
upload_url: str | None = None,
assets: list[dict[str, Any]] | None = None,
) -> None:
self._gh_client = gh_client
self._mode = mode
Expand All @@ -39,6 +43,9 @@ def __init__(
self._commit_sha = commit_sha
self._release_notes = release_notes
self._prev_version = prev_version
self._release_id = release_id
self._upload_url = upload_url
self._assets = assets or []

@property
def released(self) -> bool | None:
Expand Down Expand Up @@ -112,6 +119,59 @@ def gh_client(self) -> Github:
raise ValueError("GitHub client not set, cannot create links")
return self._gh_client

@property
def release_id(self) -> int | None:
return self._release_id if self._release_id else None

@release_id.setter
def release_id(self, value: int) -> None:
if not isinstance(value, int):
raise TypeError("output 'release_id' should be an integer")
self._release_id = value

@property
def upload_url(self) -> str | None:
return self._upload_url if self._upload_url else None

@upload_url.setter
def upload_url(self, value: str) -> None:
if not isinstance(value, str):
raise TypeError("output 'upload_url' should be a string")
self._upload_url = value

@property
def assets(self) -> list[dict[str, Any]]:
return self._assets

@assets.setter
def assets(self, value: list[dict[str, Any]]) -> None:
if not isinstance(value, list):
raise TypeError("output 'assets' should be a list")
self._assets = value

@property
def assets_dist(self) -> dict[str, dict[str, Any]]:
"""
Returns a dictionary of dist assets organized by type (wheel, sdist, etc.)

Identifies Python distribution files by extension:
- .whl files are categorized as 'wheel'
- .tar.gz files are categorized as 'sdist'
- Other files retain their extension as the key
"""
dist_assets: dict[str, dict[str, Any]] = {}
for asset in self._assets:
name = asset.get("name", "")
if name.endswith(".whl"):
dist_assets["wheel"] = asset
elif name.endswith(".tar.gz"):
dist_assets["sdist"] = asset
else:
# Use file extension as key for other files
ext = name.split(".")[-1] if "." in name else "unknown"
dist_assets[ext] = asset
return dist_assets

def to_output_text(self) -> str:
missing: set[str] = set()
if self.version is None:
Expand All @@ -137,6 +197,10 @@ def to_output_text(self) -> str:
"link": self.gh_client.create_release_url(self.tag) if self.tag else "",
"previous_version": str(self.prev_version) if self.prev_version else "",
"commit_sha": self.commit_sha if self.commit_sha else "",
"id": str(self.release_id) if self.release_id else "",
"upload_url": self.upload_url if self.upload_url else "",
"assets": json.dumps(self.assets) if self.assets else "[]",
"assets_dist": json.dumps(self.assets_dist) if self.assets_dist else "{}",
}

multiline_output_values: dict[str, str] = {
Expand All @@ -146,9 +210,11 @@ def to_output_text(self) -> str:
output_lines = [
*[f"{key}={value!s}{os.linesep}" for key, value in output_values.items()],
*[
f"{key}<<EOF{os.linesep}{value}EOF{os.linesep}"
if value
else f"{key}={os.linesep}"
(
f"{key}<<EOF{os.linesep}{value}EOF{os.linesep}"
if value
else f"{key}={os.linesep}"
)
for key, value in multiline_output_values.items()
],
]
Expand Down
6 changes: 4 additions & 2 deletions src/semantic_release/hvcs/bitbucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
if TYPE_CHECKING: # pragma: no cover
from typing import Any, Callable

from semantic_release.hvcs.github import ReleaseInfo


class Bitbucket(RemoteHvcsBase):
"""
Expand Down Expand Up @@ -241,7 +243,7 @@ def upload_dists(self, tag: str, dist_glob: str) -> int:

def create_or_update_release(
self, tag: str, release_notes: str, prerelease: bool = False
) -> int | str:
) -> int | str | ReleaseInfo:
return super().create_or_update_release(tag, release_notes, prerelease)

def create_release(
Expand All @@ -251,7 +253,7 @@ def create_release(
prerelease: bool = False,
assets: list[str] | None = None,
noop: bool = False,
) -> int | str:
) -> int | str | ReleaseInfo:
return super().create_release(tag, release_notes, prerelease, assets, noop)


Expand Down
Loading
Loading