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
274 changes: 274 additions & 0 deletions .github/workflows/release-wheel.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# SPDX-License-Identifier: MIT

name: Release Wheel (Reusable)

# Generic reusable workflow: finds the latest successful run of a given workflow,
# downloads matching wheel artifacts from it, extracts the package version, and
# publishes a GitHub release.
#
# Inputs:
# artifact-workflow – Workflow filename whose runs contain the artifacts (e.g. tilegym-ci.yml) (required)
# artifact-pattern – Extended regex (grep -E) to filter artifact names (required)
# commit-sha – If provided, find the run at this commit and tag it here;
# otherwise use the latest successful run on `branch`
# branch – Branch to search when commit-sha is empty (default: main)
# tag-prefix – Prepended to the version string, e.g. "v" → "v1.0.0"
# prerelease – Whether to mark the GitHub release as a pre-release
#
# Behaviour when a release/tag already exists:
# The existing GitHub release and its git tag are deleted, then recreated
# pointing to the resolved commit. Release notes are auto-generated by GitHub.

on:
workflow_call:
inputs:
artifact-workflow:
description: 'Workflow filename whose runs contain the artifacts to release (e.g. tilegym-ci.yml)'
required: true
type: string
artifact-pattern:
description: 'Extended regex (grep -E) to filter artifact names from the run'
required: true
type: string
commit-sha:
description: 'Find the run at this commit and tag it here (uses latest run on branch when empty)'
required: false
type: string
default: ''
branch:
description: 'Branch to search when commit-sha is empty'
required: false
type: string
default: 'main'
tag-prefix:
description: 'Prefix prepended to the version string (e.g. "v" → "v1.0.0")'
required: false
type: string
default: 'v'
prerelease:
description: 'Mark the GitHub release as a pre-release'
required: false
type: boolean
default: true

permissions:
contents: write

jobs:
release:
name: Create GitHub Release
runs-on: ubuntu-latest
steps:
# -----------------------------------------------------------------------
# 1. Resolve the run ID and commit SHA
# - If commit-sha is provided: find the latest successful run of
# `workflow` at that exact commit.
# - Otherwise: find the latest successful run of `workflow` on `branch`
# and derive the commit from it.
# -----------------------------------------------------------------------
- name: Resolve run ID and commit SHA
id: resolve
env:
GH_TOKEN: ${{ github.token }}
run: |
ARTIFACT_WORKFLOW="${{ inputs.artifact-workflow }}"
COMMIT_SHA="${{ inputs.commit-sha }}"
BRANCH="${{ inputs.branch }}"
REPO="${{ github.repository }}"

if [ -n "${COMMIT_SHA}" ]; then
echo "Searching for latest successful ${ARTIFACT_WORKFLOW} run at commit ${COMMIT_SHA}..."
RUN_ID=$(gh api \
"repos/${REPO}/actions/workflows/${ARTIFACT_WORKFLOW}/runs?head_sha=${COMMIT_SHA}&status=success&per_page=1" \
--jq '.workflow_runs[0].id // empty')
if [ -z "${RUN_ID}" ]; then
echo "ERROR: no successful ${ARTIFACT_WORKFLOW} run found for commit ${COMMIT_SHA}" >&2
exit 1
fi
else
echo "Searching for latest successful ${ARTIFACT_WORKFLOW} run on branch ${BRANCH}..."
RUN_ID=$(gh api \
"repos/${REPO}/actions/workflows/${ARTIFACT_WORKFLOW}/runs?branch=${BRANCH}&status=success&per_page=1" \
--jq '.workflow_runs[0].id // empty')
if [ -z "${RUN_ID}" ]; then
echo "ERROR: no successful ${ARTIFACT_WORKFLOW} run found on branch ${BRANCH}" >&2
exit 1
fi
COMMIT_SHA=$(gh api "repos/${REPO}/actions/runs/${RUN_ID}" --jq '.head_sha')
fi

echo "Resolved run ID: ${RUN_ID}"
echo "Target commit SHA: ${COMMIT_SHA}"
echo "run-id=${RUN_ID}" >> $GITHUB_OUTPUT
echo "sha=${COMMIT_SHA}" >> $GITHUB_OUTPUT

# -----------------------------------------------------------------------
# 2. Download all artifacts whose names match the caller-supplied regex
# -----------------------------------------------------------------------
- name: Download matching artifacts
env:
GH_TOKEN: ${{ github.token }}
run: |
RUN_ID="${{ steps.resolve.outputs.run-id }}"
REPO="${{ github.repository }}"
mkdir -p wheels

# Collect artifact names from the run (all pages) and filter by regex.
# grep -E returns exit 1 when there are no matches, so || true prevents
# an early failure before the explicit emptiness check below.
ARTIFACT_NAMES=$(gh api \
"repos/${REPO}/actions/runs/${RUN_ID}/artifacts" \
--paginate \
--jq '.artifacts[].name' \
| grep -E '${{ inputs.artifact-pattern }}' || true)

if [ -z "${ARTIFACT_NAMES}" ]; then
echo "ERROR: no artifacts matched pattern '${{ inputs.artifact-pattern }}'" >&2
echo "Available artifacts in run ${RUN_ID}:" >&2
gh api "repos/${REPO}/actions/runs/${RUN_ID}/artifacts" \
--paginate --jq '.artifacts[].name' >&2
exit 1
fi

echo "Matched artifacts:"
echo "${ARTIFACT_NAMES}"
echo ""

# <<< here-string feeds the variable to the loop without a subshell.
while IFS= read -r ARTIFACT_NAME; do
[ -z "${ARTIFACT_NAME}" ] && continue
echo "Downloading: ${ARTIFACT_NAME}"
gh run download "${RUN_ID}" \
--name "${ARTIFACT_NAME}" \
--dir "wheels/${ARTIFACT_NAME}"
done <<< "${ARTIFACT_NAMES}"

echo ""
echo "Downloaded wheel tree:"
find wheels -type f -name "*.whl" | sort

# -----------------------------------------------------------------------
# 3. Read the package version out of the first wheel's METADATA file
# -----------------------------------------------------------------------
- name: Extract package version from wheel METADATA
id: version
run: |
python3 - <<'PYEOF'
import glob, os, sys, zipfile

whl_files = glob.glob('wheels/**/*.whl', recursive=True)
if not whl_files:
print('ERROR: no .whl files found under wheels/', file=sys.stderr)
sys.exit(1)

whl = sorted(whl_files)[0]
print(f'Reading METADATA from: {whl}')

with zipfile.ZipFile(whl) as z:
meta_path = next(
(n for n in z.namelist() if n.endswith('/METADATA')),
None,
)
if meta_path is None:
print('ERROR: no METADATA entry found in wheel', file=sys.stderr)
sys.exit(1)
lines = z.read(meta_path).decode().splitlines()

version = None
for line in lines:
if line.startswith('Version:'):
version = line.split(':', 1)[1].strip()
break

if not version:
print('ERROR: Version field not found in METADATA', file=sys.stderr)
sys.exit(1)

print(f'Package version: {version}')

github_output = os.environ.get('GITHUB_OUTPUT')
if github_output:
with open(github_output, 'a') as f:
f.write(f'version={version}\n')
PYEOF

- name: Compute release tag
id: tag
run: |
TAG="${{ inputs.tag-prefix }}${{ steps.version.outputs.version }}"
echo "Release tag: ${TAG}"
echo "tag=${TAG}" >> $GITHUB_OUTPUT

# -----------------------------------------------------------------------
# 4. Remove any pre-existing release and git tag for this version so that
# the re-created release always points to the correct commit.
# -----------------------------------------------------------------------
- name: Delete existing release and tag (if any)
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="${{ steps.tag.outputs.tag }}"
REPO="${{ github.repository }}"

# --cleanup-tag deletes both the GitHub release object and the git tag.
if gh release delete "${TAG}" --cleanup-tag --yes 2>/dev/null; then
echo "Deleted existing release and tag: ${TAG}"
else
echo "No release found for ${TAG}"
# A bare git tag might still exist from a previously interrupted run.
if gh api --method DELETE \
"repos/${REPO}/git/refs/tags/${TAG}" 2>/dev/null; then
echo "Deleted orphan git tag: ${TAG}"
else
echo "No existing tag to delete for: ${TAG}"
fi
fi

# -----------------------------------------------------------------------
# 5. Publish the release and upload all wheels as assets
# -----------------------------------------------------------------------
- name: Create release and upload wheels
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="${{ steps.tag.outputs.tag }}"
TARGET_SHA="${{ steps.resolve.outputs.sha }}"
REPO="${{ github.repository }}"

WHEEL_FILES=$(find wheels -type f -name "*.whl" | sort | tr '\n' ' ')
if [ -z "${WHEEL_FILES}" ]; then
echo "ERROR: no .whl files found to upload" >&2
exit 1
fi

# PRERELEASE_FLAG must remain UNQUOTED in the gh invocation below.
# An empty string with word-splitting collapses to nothing; if quoted,
# the empty string would be passed as a spurious positional argument.
if [ "${{ inputs.prerelease }}" = "true" ]; then
PRERELEASE_FLAG="--prerelease"
else
PRERELEASE_FLAG=""
fi

echo "Creating release ${TAG} targeting ${TARGET_SHA}"
echo "Pre-release: ${{ inputs.prerelease }}"
echo "Wheels:"
echo "${WHEEL_FILES}" | tr ' ' '\n'
echo ""

# ${PRERELEASE_FLAG} and ${WHEEL_FILES} are intentionally unquoted:
# PRERELEASE_FLAG – empty string must vanish via word-splitting
# WHEEL_FILES – space-separated paths must split into individual args
# shellcheck disable=SC2086
gh release create "${TAG}" \
--repo "${REPO}" \
--target "${TARGET_SHA}" \
--title "${TAG}" \
--generate-notes \
${PRERELEASE_FLAG} \
${WHEEL_FILES}

echo ""
echo "Release published: https://github.com/${REPO}/releases/tag/${TAG}"
52 changes: 52 additions & 0 deletions .github/workflows/tilegym-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# SPDX-License-Identifier: MIT

name: tilegym-release

# Publishes tilegym wheels from the latest successful tilegym-ci run to a
# GitHub pre-release.
#
# Usage:
# Trigger manually via "Run workflow". By default it releases the wheels
# from the latest successful tilegym-ci run on main. To release a specific
# commit, supply its SHA in commit-sha.
#
# The workflow downloads all tilegym-wheel-*-verified artifacts, extracts the
# package version, and creates a tagged GitHub release. If a release for that
# version already exists it is overwritten. The git tag points to the commit
# that produced the artifacts.

on:
workflow_dispatch:
inputs:
commit-sha:
description: 'Release wheels built from this commit (uses latest successful run on main when empty)'
required: false
type: string
default: ''
prerelease:
description: 'Mark the GitHub release as a pre-release'
required: false
type: boolean
default: true

permissions:
contents: write

jobs:
release-tilegym:
name: Release tilegym wheels
uses: ./.github/workflows/release-wheel.yml
with:
artifact-workflow: tilegym-ci.yml
# Match only the verified tilegym wheel artifacts, e.g.:
# tilegym-wheel-<sha>-py310-x86_64-verified
# tilegym-wheel-<sha>-py311-arm64-verified
# Excludes tilegym-pr-wheel-* PR builds (leading tilegym-pr- won't match).
artifact-pattern: '^tilegym-wheel-[0-9a-f]+-py[0-9]+-[a-z0-9_]+-verified$'
commit-sha: ${{ inputs.commit-sha }}
branch: main
tag-prefix: 'v'
prerelease: ${{ inputs.prerelease }}
secrets: inherit