diff --git a/.github/workflows/cut-release-candidate.yaml b/.github/workflows/cut-release-candidate.yaml index 7dda671..e8afba9 100644 --- a/.github/workflows/cut-release-candidate.yaml +++ b/.github/workflows/cut-release-candidate.yaml @@ -7,15 +7,10 @@ on: description: 'Version (e.g. 0.1.1rc2)' required: true type: string - commit_id: - description: 'Commit ID for "cut" (default: origin/main)' + commit_hash: + description: 'Optional: specific commit override (will be validated)' required: false type: string - inference_provider: - description: 'Inference provider (fireworks, together)' - required: false - type: string - default: 'fireworks' cut_mode: description: 'Mode to run the script in (test-and-cut, cut-only)' type: string @@ -40,13 +35,9 @@ jobs: - uses: ./actions/test-and-cut with: version: ${{ inputs.version }} - commit_id: ${{ inputs.commit_id }} - inference_provider: ${{ inputs.inference_provider }} + commit_hash: ${{ inputs.commit_hash }} cut_mode: ${{ inputs.cut_mode }} llama_stack_only: ${{ inputs.llama_stack_only }} - fireworks_api_key: ${{ secrets.FIREWORKS_API_KEY }} - together_api_key: ${{ secrets.TOGETHER_API_KEY }} - tavily_search_api_key: ${{ secrets.TAVILY_SEARCH_API_KEY }} # TODO: this will expire in 90 days; we should figure out a # GitHub App setup that can be used instead github_token: ${{ secrets.LLAMA_REPOS_PAT }} @@ -80,7 +71,3 @@ jobs: - uses: ./actions/test-published-package with: version: ${{ inputs.version }} - inference_provider: ${{ inputs.inference_provider }} - together_api_key: ${{ secrets.TOGETHER_API_KEY }} - tavily_search_api_key: ${{ secrets.TAVILY_SEARCH_API_KEY }} - fireworks_api_key: ${{ secrets.FIREWORKS_API_KEY }} diff --git a/.github/workflows/nightly-build-publish.yml b/.github/workflows/nightly-build-publish.yml index b52dbd8..03ea815 100644 --- a/.github/workflows/nightly-build-publish.yml +++ b/.github/workflows/nightly-build-publish.yml @@ -7,15 +7,10 @@ on: description: 'Version (e.g. 0.2.21-dev.20250911)' required: false type: string - commit_id: - description: 'Commit ID for "cut" (default: origin/main)' + commit_hash: + description: 'Optional: specific commit hash to use' required: false type: string - inference_provider: - description: 'Inference provider (fireworks, together)' - required: false - type: string - default: 'fireworks' cut_mode: description: 'Mode to run the script in (test-and-cut, cut-only)' type: string @@ -75,13 +70,9 @@ jobs: - uses: ./actions/test-and-cut with: version: ${{ needs.generate-version.outputs.version }} - commit_id: ${{ inputs.commit_id }} - inference_provider: ${{ inputs.inference_provider }} + commit_hash: ${{ inputs.commit_hash }} cut_mode: ${{ inputs.cut_mode }} llama_stack_only: ${{ inputs.llama_stack_only }} - fireworks_api_key: ${{ secrets.FIREWORKS_API_KEY }} - together_api_key: ${{ secrets.TOGETHER_API_KEY }} - tavily_search_api_key: ${{ secrets.TAVILY_SEARCH_API_KEY }} # TODO: this will expire in 90 days; we should figure out a # GitHub App setup that can be used instead github_token: ${{ secrets.LLAMA_REPOS_PAT }} @@ -117,7 +108,3 @@ jobs: - uses: ./actions/test-published-package with: version: ${{ needs.generate-version.outputs.version }} - inference_provider: ${{ inputs.inference_provider }} - together_api_key: ${{ secrets.TOGETHER_API_KEY }} - tavily_search_api_key: ${{ secrets.TAVILY_SEARCH_API_KEY }} - fireworks_api_key: ${{ secrets.FIREWORKS_API_KEY }} diff --git a/.github/workflows/release-final-package.yaml b/.github/workflows/release-final-package.yaml index aa9b29f..5591216 100644 --- a/.github/workflows/release-final-package.yaml +++ b/.github/workflows/release-final-package.yaml @@ -45,9 +45,6 @@ jobs: - uses: ./actions/test-published-package with: version: ${{ inputs.release_version }} - together_api_key: ${{ secrets.TOGETHER_API_KEY }} - tavily_search_api_key: ${{ secrets.TAVILY_SEARCH_API_KEY }} - fireworks_api_key: ${{ secrets.FIREWORKS_API_KEY }} publish-docker-images: needs: diff --git a/.github/workflows/test-maybe-cut.yaml b/.github/workflows/test-maybe-cut.yaml index 5985cbf..a246909 100644 --- a/.github/workflows/test-maybe-cut.yaml +++ b/.github/workflows/test-maybe-cut.yaml @@ -3,16 +3,12 @@ name: Test and Possibly Cut a Branch on: workflow_dispatch: inputs: - commit_id: - description: 'llama-stack commit ID' - required: true - client_python_commit_id: - description: 'llama-stack-client-python commit ID' + commit_hash: + description: 'Optional: specific commit hash to test' required: false type: string - default: 'origin/main' version: - description: 'Version (e.g. 0.1.1rc2); if optional, will not cut a branch' + description: 'Version (e.g. 0.1.1rc2); if empty, will use dev version' required: false type: string cut_mode: @@ -20,11 +16,6 @@ on: required: false type: string default: 'test-and-cut' - inference_provider: - description: 'Inference provider to use for testing' - required: false - type: string - default: 'fireworks' schedule: - cron: "0 0 * * *" # Run every day at midnight @@ -37,17 +28,15 @@ jobs: contents: read steps: - uses: actions/checkout@v4 - - name: Set commit ID and version for scheduled runs + - name: Set inputs for scheduled runs id: inputs run: | if [[ "${{ github.event_name }}" == "schedule" ]]; then - echo "commit_id=origin/main" >> $GITHUB_OUTPUT + echo "commit_hash=" >> $GITHUB_OUTPUT echo "version=" >> $GITHUB_OUTPUT - echo "client_python_commit_id=origin/main" >> $GITHUB_OUTPUT else - echo "commit_id=${{ inputs.commit_id }}" >> $GITHUB_OUTPUT + echo "commit_hash=${{ inputs.commit_hash }}" >> $GITHUB_OUTPUT echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT - echo "client_python_commit_id=${{ inputs.client_python_commit_id }}" >> $GITHUB_OUTPUT fi shell: bash - name: Set version if not provided @@ -64,13 +53,8 @@ jobs: - uses: ./actions/test-and-cut with: version: ${{ steps.version.outputs.value }} - commit_id: ${{ steps.inputs.outputs.commit_id }} - client_python_commit_id: ${{ steps.inputs.outputs.client_python_commit_id }} - inference_provider: ${{ inputs.inference_provider }} + commit_hash: ${{ steps.inputs.outputs.commit_hash }} cut_mode: ${{ steps.version.outputs.cut_mode }} - fireworks_api_key: ${{ secrets.FIREWORKS_API_KEY }} - together_api_key: ${{ secrets.TOGETHER_API_KEY }} - tavily_search_api_key: ${{ secrets.TAVILY_SEARCH_API_KEY }} # TODO: this will expire in 90 days; we should figure out a # GitHub App setup that can be used instead github_token: ${{ secrets.LLAMA_REPOS_PAT }} diff --git a/.github/workflows/test-published-package.yaml b/.github/workflows/test-published-package.yaml index 38e5dda..6bc9d0e 100644 --- a/.github/workflows/test-published-package.yaml +++ b/.github/workflows/test-published-package.yaml @@ -7,11 +7,6 @@ on: description: 'Version number (e.g. 0.1.1rc2, 0.1.1.dev20250201)' required: true type: string - inference_provider: - description: 'Inference provider to use for the release candidate (fireworks, together) (default: fireworks)' - required: false - type: string - default: 'fireworks' jobs: test-published-package: @@ -21,7 +16,3 @@ jobs: - uses: ./actions/test-published-package with: version: ${{ inputs.version }} - inference_provider: ${{ inputs.inference_provider }} - together_api_key: ${{ secrets.TOGETHER_API_KEY }} - tavily_search_api_key: ${{ secrets.TAVILY_SEARCH_API_KEY }} - fireworks_api_key: ${{ secrets.FIREWORKS_API_KEY }} diff --git a/RELEASE_PROCESS.md b/RELEASE_PROCESS.md new file mode 100644 index 0000000..ac592ef --- /dev/null +++ b/RELEASE_PROCESS.md @@ -0,0 +1,51 @@ +# Release Process + +Releases are managed through long-lived release branches. Each minor version (e.g., `0.1.x`) has one branch containing all patch releases for that series. + +``` +main + ↓ +release-0.1.x ← v0.1.0rc1, v0.1.0rc2, v0.1.0, v0.1.1rc1, v0.1.1, ... + ↓ +release-0.2.x ← v0.2.0rc1, v0.2.0, ... +``` + +## Workflows + +**Cut Release Candidate** (`cut-release-candidate.yaml`) + +Creates an RC for testing. Provide the RC version (e.g., `0.1.0rc1`) and optionally a specific commit hash. The workflow derives the release branch from the version (`0.1.0rc1` → `release-0.1.x`). If the branch exists, it uses HEAD; otherwise it creates the branch from main (or the specified commit). It then bumps the version, builds packages, runs tests, and publishes to test.pypi. + +**Release Final Package** (`release-final-package.yaml`) + +Promotes an RC to final. Provide the RC version to promote (e.g., `0.1.0rc2`) and the final version name (e.g., `0.1.0`). The workflow checks out the RC tag, bumps version strings, updates lockfiles, publishes to PyPI/npm, and creates a PR to bump main to the next dev version (`0.1.1.dev0`). + +**Test Published Package** (`test-published-package.yaml`) + +Manually tests any published package version. + +## Examples + +**New minor version (0.1.0):** + +1. Cut first RC: `version=0.1.0rc1` (creates `release-0.1.x` from main) +2. Test, find bugs, cherry-pick fixes to `release-0.1.x` +3. Cut another RC: `version=0.1.0rc2` (uses HEAD of `release-0.1.x`) +4. Promote to final: `rc_version=0.1.0rc2`, `release_version=0.1.0` + +**Patch release (0.1.1):** + +1. Cherry-pick fixes to `release-0.1.x` +2. Cut RC: `version=0.1.1rc1` +3. Promote to final: `rc_version=0.1.1rc1`, `release_version=0.1.1` + +## Version Naming + +- RC: `X.Y.Zrc1`, `X.Y.Zrc2` (manual, sequential) +- Final: `X.Y.Z` +- Dev: `X.Y.Z.dev0` (auto-generated for main) +- Branches: `release-X.Y.x` + +## Notes + +Release branches are created on the first RC, not on final release. Main is automatically bumped via PR after each final release (patch + 1). Cherry-picking is done manually between RCs. The `commit_hash` parameter is optional and validates against branch ancestry if provided. diff --git a/actions/common.sh b/actions/common.sh index 51b2b1c..00d878f 100644 --- a/actions/common.sh +++ b/actions/common.sh @@ -1,10 +1,5 @@ github_org() { - repo=$1 - if [ "$repo" == "stack" ]; then - echo "meta-llama" - else - echo "llamastack" - fi + echo "llamastack" } run_integration_tests() { diff --git a/actions/lib/release_utils.sh b/actions/lib/release_utils.sh deleted file mode 100644 index 991abb1..0000000 --- a/actions/lib/release_utils.sh +++ /dev/null @@ -1,70 +0,0 @@ -determine_base_branch() { - local parent_commit - if ! parent_commit=$(git rev-parse HEAD^ 2>/dev/null); then - echo "Unable to determine parent commit for release candidate tag" >&2 - return 1 - fi - - if git merge-base --is-ancestor "$parent_commit" origin/main >/dev/null 2>&1; then - echo "main" - return 0 - fi - - # Collect the origin branches that already contain the parent commit; Git stops - # walking each branch's history once it finds the commit, so this stays fast even - # with many remote branches. - mapfile -t candidates < <(git for-each-ref \ - --format='%(refname:strip=3)' \ - --sort=-committerdate \ - --contains "$parent_commit" \ - "refs/remotes/origin") - - local releases=() - local main_candidate="" - - for branch in "${candidates[@]}"; do - if [ "$branch" = "HEAD" ]; then - continue - fi - if [[ "$branch" == rc-* ]]; then - continue - fi - if [[ "$branch" == release-* ]]; then - releases+=("$branch") - continue - fi - if [ "$branch" = "main" ]; then - main_candidate="main" - fi - done - - if [ ${#releases[@]} -gt 0 ]; then - local best_branch="" - local best_distance="" - - for branch in "${releases[@]}"; do - local distance - distance=$(git rev-list --count "$parent_commit".. "origin/$branch") - if [ -z "$best_branch" ]; then - best_branch="$branch" - best_distance="$distance" - elif [ "$distance" -lt "$best_distance" ]; then - best_branch="$branch" - best_distance="$distance" - fi - done - - if [ -n "$best_branch" ]; then - echo "$best_branch" - return 0 - fi - fi - - if [ -n "$main_candidate" ]; then - echo "$main_candidate" - return 0 - fi - - echo "Unable to determine base branch for parent commit $parent_commit" >&2 - return 1 -} diff --git a/actions/release-final-package/main.sh b/actions/release-final-package/main.sh index 263d88c..10238eb 100755 --- a/actions/release-final-package/main.sh +++ b/actions/release-final-package/main.sh @@ -20,7 +20,6 @@ LLAMA_STACK_ONLY=${LLAMA_STACK_ONLY:-false} DRY_RUN=${DRY_RUN:-false} source $(dirname $0)/../common.sh -source $(dirname $0)/../lib/release_utils.sh npm config set '//registry.npmjs.org/:_authToken' "$NPM_TOKEN" @@ -34,6 +33,31 @@ is_truthy() { esac } +# Parse version to derive release branch name +# Examples: 0.1.0 -> release-0.1.x, 1.2.3 -> release-1.2.x, 0.2.10.1 -> release-0.2.x +parse_version_and_branch() { + local version=$1 + + # Validate version format (X.Y.Z or X.Y.Z.W, no rc suffix for final releases) + if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then + echo "ERROR: Invalid version format: $version" >&2 + echo "Expected format: X.Y.Z[.W] (e.g., 0.1.0, 1.2.3, 0.2.10.1)" >&2 + exit 1 + fi + + # Extract major.minor (e.g., 0.1.0 -> 0.1) + local major=$(echo "$version" | cut -d. -f1) + local minor=$(echo "$version" | cut -d. -f2) + + # Derive branch name: release-{major}.{minor}.x + local branch_name="release-${major}.${minor}.x" + + echo "$branch_name" +} + +RELEASE_BRANCH=$(parse_version_and_branch "$RELEASE_VERSION") +echo "Derived release branch: $RELEASE_BRANCH" + # Yell loudly if RELEASE is already on pypi, but keep going anyway version_tag=$(curl -s https://pypi.org/pypi/llama-stack/json | jq -r '.info.version') if [ x"$version_tag" = x"$RELEASE_VERSION" ]; then @@ -125,10 +149,14 @@ add_bump_version_commit() { perl -pi -e "s/^version = .*$/version = \"$version\"/" pyproject.toml if ! is_truthy "$LLAMA_STACK_ONLY"; then - perl -pi -e "s/llama-stack-client>=.*,/llama-stack-client>=$RELEASE_VERSION\",/" pyproject.toml - - if [ "$repo" == "stack" ]; then - perl -pi -e "s/(\"llama-stack-client\": \").+\"/\1^$RELEASE_VERSION\"/" llama_stack/ui/package.json + # Only update client dependency for non-dev versions + # Dev versions (e.g., 0.1.1.dev0) should keep the last stable client dependency + if [[ ! "$version" =~ \.dev ]]; then + perl -pi -e "s/llama-stack-client>=.*,/llama-stack-client>=$version\",/" pyproject.toml + + if [ "$repo" == "stack" ]; then + perl -pi -e "s/(\"llama-stack-client\": \").+\"/\1^$version\"/" llama_stack/ui/package.json + fi fi if [ -f "src/llama_stack_client/_version.py" ]; then @@ -161,10 +189,7 @@ source build-env/bin/activate uv pip install twine npm install -g yarn -BASE_BRANCHES=() - -for idx in "${!REPOS[@]}"; do - repo="${REPOS[$idx]}" +for repo in "${REPOS[@]}"; do org=$(github_org $repo) git clone "https://x-access-token:${GITHUB_TOKEN}@github.com/$org/llama-$repo.git" cd llama-$repo @@ -172,17 +197,6 @@ for idx in "${!REPOS[@]}"; do git checkout -b release-$RELEASE_VERSION refs/tags/v${RC_VERSION} git fetch origin --prune - if ! base_branch=$(determine_base_branch); then - echo "Failed to determine base branch for $repo" >&2 - exit 1 - fi - - if ! git ls-remote --heads origin "$base_branch" >/dev/null 2>&1; then - echo "Base branch $base_branch not found on remote for $repo" >&2 - exit 1 - fi - BASE_BRANCHES[$idx]="$base_branch" - # don't run uv lock here because the dependency isn't pushed upstream so uv will fail add_bump_version_commit $repo $RELEASE_VERSION false @@ -245,35 +259,74 @@ done deactivate rm -rf build-env -for idx in "${!REPOS[@]}"; do - repo="${REPOS[$idx]}" - cd $TMPDIR - if [ "$repo" != "stack-client-typescript" ]; then - uv venv -p python3.12 repo-$repo-env - source repo-$repo-env/bin/activate - fi - - cd llama-$repo +# Push release branch and tags to remote +for repo in "${REPOS[@]}"; do + cd $TMPDIR/llama-$repo - # push the release branch/tag and update the source branch - echo "Pushing branch and tag v$RELEASE_VERSION for $repo" + echo "Pushing release branch and tag v$RELEASE_VERSION for $repo" org=$(github_org $repo) - git push -f "https://x-access-token:${GITHUB_TOKEN}@github.com/$org/llama-$repo.git" "release-$RELEASE_VERSION" - git push -f "https://x-access-token:${GITHUB_TOKEN}@github.com/$org/llama-$repo.git" "v$RELEASE_VERSION" - if ! is_truthy "$LLAMA_STACK_ONLY"; then - base_branch="${BASE_BRANCHES[$idx]}" - git fetch origin "$base_branch" - git checkout -B "$base_branch" "origin/$base_branch" - add_bump_version_commit $repo $RELEASE_VERSION true - git push "https://x-access-token:${GITHUB_TOKEN}@github.com/$org/llama-$repo.git" "$base_branch" - fi + # Push the release branch with the version bump commit + git push -f "https://x-access-token:${GITHUB_TOKEN}@github.com/$org/llama-$repo.git" "release-$RELEASE_VERSION:$RELEASE_BRANCH" - if [ "$repo" != "stack-client-typescript" ]; then - deactivate - fi + # Push the tag + git push -f "https://x-access-token:${GITHUB_TOKEN}@github.com/$org/llama-$repo.git" "v$RELEASE_VERSION" - cd .. + cd $TMPDIR done +echo "Release $RELEASE_VERSION published successfully" + +# Auto-bump main branch version (create PR) +if ! is_truthy "$LLAMA_STACK_ONLY"; then + echo "Creating PR to bump main branch version" + + # Calculate next dev version: 0.1.0 -> 0.1.1.dev0 + MAJOR=$(echo $RELEASE_VERSION | cut -d. -f1) + MINOR=$(echo $RELEASE_VERSION | cut -d. -f2) + PATCH=$(echo $RELEASE_VERSION | cut -d. -f3) + NEXT_PATCH=$((PATCH + 1)) + NEXT_DEV_VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}.dev0" + + echo "Next dev version: $NEXT_DEV_VERSION" + + for repo in "${REPOS[@]}"; do + cd $TMPDIR + + if [ "$repo" != "stack-client-typescript" ]; then + uv venv -p python3.12 bump-main-$repo-env + source bump-main-$repo-env/bin/activate + fi + + cd llama-$repo + + org=$(github_org $repo) + + # Checkout main branch + git fetch origin main + git checkout -B main origin/main + + # Bump version to next dev version + add_bump_version_commit $repo $NEXT_DEV_VERSION true + + # Push to a new branch for PR + BUMP_BRANCH="release-automation/bump-to-${NEXT_DEV_VERSION}" + git push -f "https://x-access-token:${GITHUB_TOKEN}@github.com/$org/llama-$repo.git" "main:${BUMP_BRANCH}" + + # Create PR using gh CLI + GH_TOKEN=$GITHUB_TOKEN gh pr create \ + --repo "$org/llama-$repo" \ + --base main \ + --head "${BUMP_BRANCH}" \ + --title "chore: bump version to ${NEXT_DEV_VERSION}" \ + --body "Automated version bump after releasing ${RELEASE_VERSION}" || echo "PR creation failed or PR already exists" + + if [ "$repo" != "stack-client-typescript" ]; then + deactivate + fi + + cd $TMPDIR + done +fi + echo "Done" diff --git a/actions/test-and-cut/action.yaml b/actions/test-and-cut/action.yaml index 91561f7..3594855 100644 --- a/actions/test-and-cut/action.yaml +++ b/actions/test-and-cut/action.yaml @@ -4,35 +4,18 @@ inputs: version: description: 'Version of the package to publish' required: true - commit_id: - description: 'Commit ID for cut' + commit_hash: + description: 'Optional: specific commit override (will be validated)' required: false - default: 'origin/main' - client_python_commit_id: - description: 'llama-stack-client-python commit ID for "cut"' - required: false - default: 'origin/main' + default: '' cut_mode: description: 'Mode to run the script in (test-and-cut, test-only, cut-only)' required: false default: 'test-and-cut' - inference_provider: - description: "Inference provider to use for testing" - required: false - default: 'fireworks' llama_stack_only: description: 'Only cut a llama-stack release candidate' required: false default: 'false' - fireworks_api_key: - description: 'Fireworks API key' - required: true - together_api_key: - description: 'Together API key' - required: true - tavily_search_api_key: - description: 'Tavily Search API key' - required: true github_token: description: 'Personal Access Token (PAT) with access to all llama repositories' required: true @@ -67,17 +50,10 @@ runs: shell: bash env: VERSION: ${{ inputs.version }} - COMMIT_ID: ${{ inputs.commit_id }} - CLIENT_PYTHON_COMMIT_ID: ${{ inputs.client_python_commit_id }} - INFERENCE_PROVIDER: ${{ inputs.inference_provider }} + COMMIT_HASH: ${{ inputs.commit_hash }} CUT_MODE: ${{ inputs.cut_mode }} LLAMA_STACK_ONLY: ${{ inputs.llama_stack_only }} - TOGETHER_API_KEY: ${{ inputs.together_api_key }} - TAVILY_SEARCH_API_KEY: ${{ inputs.tavily_search_api_key }} - FIREWORKS_API_KEY: ${{ inputs.fireworks_api_key }} GITHUB_TOKEN: ${{ inputs.github_token }} - LLAMA_STACK_LOG_FILE: server.log - LLAMA_STACK_CLIENT_LOG_FILE: client.log run: | chmod +x ${{ github.action_path }}/main.sh ${{ github.action_path }}/main.sh diff --git a/actions/test-and-cut/main.sh b/actions/test-and-cut/main.sh index dedcf2b..845f3db 100755 --- a/actions/test-and-cut/main.sh +++ b/actions/test-and-cut/main.sh @@ -4,19 +4,11 @@ if [ -z "$VERSION" ]; then echo "You must set the VERSION environment variable" >&2 exit 1 fi -if [ -z "${COMMIT_ID+x}" ]; then - echo "You must set the COMMIT_ID environment variable" >&2 - exit 1 -fi - -if [ -z "${CLIENT_PYTHON_COMMIT_ID+x}" ]; then - echo "You must set the CLIENT_PYTHON_COMMIT_ID environment variable" >&2 - exit 1 -fi GITHUB_TOKEN=${GITHUB_TOKEN:-} CUT_MODE=${CUT_MODE:-test-and-cut} LLAMA_STACK_ONLY=${LLAMA_STACK_ONLY:-false} +COMMIT_HASH=${COMMIT_HASH:-} source $(dirname $0)/../common.sh @@ -36,6 +28,38 @@ is_truthy() { esac } +# Parse version to extract base version and derive release branch name +# Examples: +# 0.1.0rc1 -> base=0.1.0, branch=release-0.1.x +# 0.1.1rc2 -> base=0.1.1, branch=release-0.1.x +# 1.2.3 -> base=1.2.3, branch=release-1.2.x +# 0.2.10.1rc1 -> base=0.2.10.1, branch=release-0.2.x +parse_version_and_branch() { + local version=$1 + + # Validate version format (basic check for X.Y.Z or X.Y.Z.W pattern) + if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?(rc[0-9]+)?$ ]]; then + echo "ERROR: Invalid version format: $version" >&2 + echo "Expected format: X.Y.Z[.W][rcN] (e.g., 0.1.0rc1, 1.2.3, 0.2.10.1)" >&2 + exit 1 + fi + + # Remove rc suffix if present (e.g., 0.1.0rc1 -> 0.1.0) + local base_version=$(echo "$version" | sed 's/rc[0-9]*$//') + + # Extract major.minor (e.g., 0.1.0 -> 0.1) + local major=$(echo "$base_version" | cut -d. -f1) + local minor=$(echo "$base_version" | cut -d. -f2) + + # Derive branch name: release-{major}.{minor}.x + local branch_name="release-${major}.${minor}.x" + + echo "$branch_name" +} + +RELEASE_BRANCH=$(parse_version_and_branch "$VERSION") +echo "Derived release branch: $RELEASE_BRANCH" + DISTRO=starter TMPDIR=$(mktemp -d) @@ -44,6 +68,63 @@ cd $TMPDIR uv venv --python 3.12 source .venv/bin/activate +determine_source_commit_for_repo() { + local repo=$1 + local org=$(github_org $repo) + + # Check if release branch exists for this repo + if git ls-remote --heads "https://github.com/$org/llama-$repo.git" "$RELEASE_BRANCH" | grep -q .; then + echo "Release branch $RELEASE_BRANCH exists for $repo" >&2 + + if [ -n "$COMMIT_HASH" ]; then + # COMMIT_HASH override provided - validate it + echo "Validating commit override: $COMMIT_HASH" >&2 + + # Fetch both the commit and the branch + git fetch origin "$COMMIT_HASH" || { + echo "ERROR: Commit $COMMIT_HASH does not exist in $repo" >&2 + exit 1 + } + + git fetch origin "$RELEASE_BRANCH" + + # Check if commit is related to the branch (ancestor or descendant) + if ! git merge-base --is-ancestor "$COMMIT_HASH" "origin/$RELEASE_BRANCH" && \ + ! git merge-base --is-ancestor "origin/$RELEASE_BRANCH" "$COMMIT_HASH"; then + echo "ERROR: Commit $COMMIT_HASH is not related to branch $RELEASE_BRANCH" >&2 + echo "ERROR: The commit must be an ancestor or descendant of the release branch" >&2 + exit 1 + fi + + echo "Using commit override: $COMMIT_HASH" >&2 + echo "$COMMIT_HASH" + else + # Use HEAD of release branch + echo "Using HEAD of existing release branch" >&2 + echo "origin/$RELEASE_BRANCH" + fi + else + echo "Release branch $RELEASE_BRANCH does not exist for $repo - will create it" >&2 + + if [ -n "$COMMIT_HASH" ]; then + # Creating new branch from commit override + echo "Creating new release branch from commit: $COMMIT_HASH" >&2 + + # Validate commit exists + git fetch origin "$COMMIT_HASH" || { + echo "ERROR: Commit $COMMIT_HASH does not exist in $repo" >&2 + exit 1 + } + + echo "$COMMIT_HASH" + else + # Creating new branch from main + echo "Creating new release branch from origin/main" >&2 + echo "origin/main" + fi + fi +} + build_packages() { npm install -g yarn @@ -54,21 +135,28 @@ build_packages() { for repo in "${REPOS[@]}"; do org=$(github_org $repo) - git clone --depth 10 "https://x-access-token:${GITHUB_TOKEN}@github.com/$org/llama-$repo.git" + git clone "https://x-access-token:${GITHUB_TOKEN}@github.com/$org/llama-$repo.git" cd llama-$repo - if [ "$repo" == "stack" ] && [ -n "$COMMIT_ID" ]; then - REF="${COMMIT_ID#origin/}" - git fetch origin "$REF" + # Determine which commit to use as the base + SOURCE_COMMIT=$(determine_source_commit_for_repo "$repo") - # Use FETCH_HEAD which is where the fetched commit is stored - git checkout -b "rc-$VERSION" FETCH_HEAD - elif [ "$repo" == "stack-client-python" ] && [ -n "$CLIENT_PYTHON_COMMIT_ID" ]; then - REF="${CLIENT_PYTHON_COMMIT_ID#origin/}" + # Checkout/create the release branch + if [[ "$SOURCE_COMMIT" == origin/* ]]; then + # It's a remote ref (either origin/release-X.Y.x or origin/main) + REF="${SOURCE_COMMIT#origin/}" git fetch origin "$REF" - git checkout -b "rc-$VERSION" FETCH_HEAD + + if [ "$REF" == "$RELEASE_BRANCH" ]; then + # Branch already exists, check it out + git checkout -b "$RELEASE_BRANCH" FETCH_HEAD + else + # Creating new release branch from main or other ref + git checkout -b "$RELEASE_BRANCH" FETCH_HEAD + fi else - git checkout -b "rc-$VERSION" + # It's a commit hash, create release branch from it + git checkout -b "$RELEASE_BRANCH" "$SOURCE_COMMIT" fi # TODO: this is dangerous use uvx toml-cli toml set project.version $VERSION instead of this @@ -136,9 +224,6 @@ test_docker() { -e SAFETY_MODEL=ollama/llama-guard3:1b \ -e LLAMA_STACK_TEST_INFERENCE_MODE=replay \ -e LLAMA_STACK_TEST_STACK_CONFIG_TYPE=server \ - -e TOGETHER_API_KEY=$TOGETHER_API_KEY \ - -e FIREWORKS_API_KEY=$FIREWORKS_API_KEY \ - -e TAVILY_SEARCH_API_KEY=$TAVILY_SEARCH_API_KEY \ -v $(pwd)/llama-stack:/app/llama-stack-source \ distribution-$DISTRO:dev \ --port $LLAMA_STACK_PORT @@ -179,12 +264,11 @@ if [ "$CUT_MODE" == "test-only" ]; then fi for repo in "${REPOS[@]}"; do - echo "Pushing branch rc-$VERSION for llama-$repo" + echo "Pushing release branch $RELEASE_BRANCH for llama-$repo" cd llama-$repo org=$(github_org $repo) - git push -f "https://x-access-token:${GITHUB_TOKEN}@github.com/$org/llama-$repo.git" "rc-$VERSION" + git push -f "https://x-access-token:${GITHUB_TOKEN}@github.com/$org/llama-$repo.git" "$RELEASE_BRANCH" cd .. - done -echo "Successfully cut a release candidate branch $VERSION" +echo "Successfully cut release candidate $VERSION on branch $RELEASE_BRANCH" diff --git a/actions/test-published-package/action.yaml b/actions/test-published-package/action.yaml index b947749..e15b856 100644 --- a/actions/test-published-package/action.yaml +++ b/actions/test-published-package/action.yaml @@ -4,19 +4,6 @@ inputs: version: description: 'Version of the package to test' required: true - inference_provider: - description: 'Inference provider to use for the release candidate (fireworks, together) (default: fireworks)' - required: false - default: 'fireworks' - fireworks_api_key: - description: 'Fireworks API key' - required: true - together_api_key: - description: 'Together API key' - required: true - tavily_search_api_key: - description: 'Tavily Search API key' - required: true runs: using: 'composite' @@ -33,10 +20,6 @@ runs: shell: bash env: VERSION: ${{ inputs.version }} - INFERENCE_PROVIDER: ${{ inputs.inference_provider }} - TOGETHER_API_KEY: ${{ inputs.together_api_key }} - TAVILY_SEARCH_API_KEY: ${{ inputs.tavily_search_api_key }} - FIREWORKS_API_KEY: ${{ inputs.fireworks_api_key }} run: | chmod +x ${{ github.action_path }}/main.sh ${{ github.action_path }}/main.sh diff --git a/actions/upload-packages-and-tag/main.sh b/actions/upload-packages-and-tag/main.sh index 1c1fcb9..c58d6fe 100755 --- a/actions/upload-packages-and-tag/main.sh +++ b/actions/upload-packages-and-tag/main.sh @@ -28,6 +28,30 @@ is_truthy() { esac } +# Parse version to derive release branch name +parse_version_and_branch() { + local version=$1 + + # Validate version format + if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?(rc[0-9]+)?$ ]]; then + echo "ERROR: Invalid version format: $version" >&2 + exit 1 + fi + + # Remove rc suffix if present + local base_version=$(echo "$version" | sed 's/rc[0-9]*$//') + + # Extract major.minor + local major=$(echo "$base_version" | cut -d. -f1) + local minor=$(echo "$base_version" | cut -d. -f2) + + # Derive branch name + echo "release-${major}.${minor}.x" +} + +RELEASE_BRANCH=$(parse_version_and_branch "$VERSION") +echo "Derived release branch: $RELEASE_BRANCH" + TMPDIR=$(mktemp -d) cd $TMPDIR @@ -45,12 +69,12 @@ fi for repo in "${REPOS[@]}"; do org=$(github_org $repo) - git clone --depth 10 "https://x-access-token:${GITHUB_TOKEN}@github.com/$org/llama-$repo.git" + git clone "https://x-access-token:${GITHUB_TOKEN}@github.com/$org/llama-$repo.git" cd llama-$repo - echo "Building package..." - git fetch origin "rc-$VERSION":"rc-$VERSION" - git checkout "rc-$VERSION" + echo "Fetching release branch $RELEASE_BRANCH..." + git fetch origin "$RELEASE_BRANCH":"$RELEASE_BRANCH" + git checkout "$RELEASE_BRANCH" if [ "$repo" == "stack-client-typescript" ]; then NPM_VERSION=$(cat package.json | jq -r '.version') diff --git a/tests/integration/test-cut-rc.sh b/tests/integration/test-cut-rc.sh new file mode 100755 index 0000000..98a8872 --- /dev/null +++ b/tests/integration/test-cut-rc.sh @@ -0,0 +1,286 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) +WORK_ROOT=$(mktemp -d) +trap 'rm -rf "$WORK_ROOT"' EXIT + +passed=0 +failed=0 + +log_test() { echo "[TEST] $1"; } +log_pass() { echo "[PASS] $1"; passed=$((passed + 1)); } +log_fail() { echo "[FAIL] $1" >&2; failed=$((failed + 1)); } +log_info() { echo "[INFO] $1"; } + +# Create a synthetic repo with proper structure +create_synthetic_repo() { + local repo_name=$1 + local repo_path="$WORK_ROOT/repos/$repo_name.git" + + mkdir -p "$repo_path" + cd "$repo_path" + git init --bare -q + + # Create working copy + local work_dir="$WORK_ROOT/work/$repo_name" + git clone -q "$repo_path" "$work_dir" + cd "$work_dir" + + git config user.email "test@example.com" + git config user.name "Test User" + + # Create initial structure based on repo type + if [ "$repo_name" = "llama-stack" ]; then + cat > pyproject.toml <<'EOF' +[project] +name = "llama-stack" +version = "0.0.1" +dependencies = [ + "llama-stack-client>=0.0.1", +] +EOF + mkdir -p llama_stack/ui + cat > llama_stack/ui/package.json <<'EOF' +{ + "name": "llama-stack-ui", + "version": "0.0.1", + "dependencies": { + "llama-stack-client": "^0.0.1" + } +} +EOF + elif [ "$repo_name" = "llama-stack-client-python" ]; then + cat > pyproject.toml <<'EOF' +[project] +name = "llama-stack-client" +version = "0.0.1" +EOF + mkdir -p src/llama_stack_client + cat > src/llama_stack_client/_version.py <<'EOF' +__version__ = "0.0.1" +EOF + elif [ "$repo_name" = "llama-stack-client-typescript" ]; then + cat > package.json <<'EOF' +{ + "name": "llama-stack-client", + "version": "0.0.1" +} +EOF + fi + + echo "# $repo_name" > README.md + git add . + git commit -q -m "Initial commit" + git branch -M main + git push -q -u origin main +} + +# Mock github_org function +github_org() { + echo "llamastack" +} +export -f github_org + +echo "========================================" +echo "Integration Test: Cut Release Candidate" +echo "========================================" +echo "" + +log_info "Setting up synthetic repos..." + +# Create synthetic repos +create_synthetic_repo "llama-stack" 2>&1 >/dev/null +create_synthetic_repo "llama-stack-client-python" 2>&1 >/dev/null +create_synthetic_repo "llama-stack-client-typescript" 2>&1 >/dev/null + +log_pass "Created synthetic repos" + +# Simulate the workflow logic for cutting an RC +log_test "Cut first RC (0.1.0rc1) - creates release branch" + +export VERSION="0.1.0rc1" +export RELEASE_BRANCH="release-0.1.x" +export GITHUB_TOKEN="dummy" +export LLAMA_STACK_ONLY="false" +export COMMIT_HASH="" + +# For each repo, simulate what test-and-cut/main.sh does +for repo in llama-stack-client-python llama-stack-client-typescript llama-stack; do + log_info "Processing $repo..." + + cd "$WORK_ROOT/work/$repo" + + # Check if release branch exists (it shouldn't for first RC) + if git ls-remote origin "$RELEASE_BRANCH" | grep -q .; then + log_fail "$repo: Release branch shouldn't exist yet" + continue + fi + + # Create release branch from main (simulating first RC) + git checkout -q main + git pull -q origin main + git checkout -q -b "$RELEASE_BRANCH" + + # Bump version to RC version + if [ "$repo" = "llama-stack-client-typescript" ]; then + perl -pi -e "s/\"version\": \".*\"/\"version\": \"$VERSION\"/" package.json + else + perl -pi -e "s/^version = .*$/version = \"$VERSION\"/" pyproject.toml + + if [ "$repo" = "llama-stack-client-python" ]; then + perl -pi -e "s/__version__ = .*$/__version__ = \"$VERSION\"/" src/llama_stack_client/_version.py + fi + + if [ "$repo" = "llama-stack" ]; then + # Update client dependency + perl -pi -e "s/llama-stack-client>=.*/llama-stack-client>=$VERSION\",/" pyproject.toml + perl -pi -e "s/(\"llama-stack-client\": \").+\"/\1^$VERSION\"/" llama_stack/ui/package.json + fi + fi + + # Verify version was updated + if [ "$repo" = "llama-stack-client-typescript" ]; then + if grep -q "\"version\": \"$VERSION\"" package.json; then + log_pass "$repo: Version bumped in package.json" + else + log_fail "$repo: Version not updated in package.json" + cat package.json + continue + fi + else + if grep -q "version = \"$VERSION\"" pyproject.toml; then + log_pass "$repo: Version bumped in pyproject.toml" + else + log_fail "$repo: Version not updated in pyproject.toml" + cat pyproject.toml + continue + fi + fi + + # Verify client dependency updated for stack + if [ "$repo" = "llama-stack" ]; then + if grep -q "llama-stack-client>=$VERSION" pyproject.toml && \ + grep -q "\"llama-stack-client\": \"^$VERSION\"" llama_stack/ui/package.json; then + log_pass "$repo: Client dependencies updated" + else + log_fail "$repo: Client dependencies not updated correctly" + grep "llama-stack-client" pyproject.toml llama_stack/ui/package.json + continue + fi + fi + + # Commit and push + git commit -q -am "Release candidate $VERSION" + git push -q origin "$RELEASE_BRANCH" + + # Verify branch exists remotely + if git ls-remote origin "$RELEASE_BRANCH" | grep -q .; then + log_pass "$repo: Release branch pushed to remote" + else + log_fail "$repo: Release branch not found on remote" + fi +done + +echo "" +log_test "Cut second RC (0.1.0rc2) - uses existing release branch" + +export VERSION="0.1.0rc2" + +for repo in llama-stack-client-python llama-stack-client-typescript llama-stack; do + log_info "Processing $repo for RC2..." + + cd "$WORK_ROOT/work/$repo" + + # Check release branch exists + if ! git ls-remote origin "$RELEASE_BRANCH" | grep -q .; then + log_fail "$repo: Release branch should exist" + continue + fi + + # Fetch and checkout existing release branch + git fetch -q origin "$RELEASE_BRANCH" + git checkout -q "$RELEASE_BRANCH" + git pull -q origin "$RELEASE_BRANCH" + + # Add a "fix" to simulate cherry-picked changes + echo "// Fix for RC2" >> README.md + git commit -q -am "Fix for RC2" + + # Bump version to new RC + if [ "$repo" = "llama-stack-client-typescript" ]; then + perl -pi -e "s/\"version\": \".*\"/\"version\": \"$VERSION\"/" package.json + else + perl -pi -e "s/^version = .*$/version = \"$VERSION\"/" pyproject.toml + + if [ "$repo" = "llama-stack-client-python" ]; then + perl -pi -e "s/__version__ = .*$/__version__ = \"$VERSION\"/" src/llama_stack_client/_version.py + fi + + if [ "$repo" = "llama-stack" ]; then + perl -pi -e "s/llama-stack-client>=.*/llama-stack-client>=$VERSION\",/" pyproject.toml + perl -pi -e "s/(\"llama-stack-client\": \").+\"/\1^$VERSION\"/" llama_stack/ui/package.json + fi + fi + + # Verify version updated + if [ "$repo" = "llama-stack-client-typescript" ]; then + if grep -q "\"version\": \"$VERSION\"" package.json; then + log_pass "$repo: Version bumped to RC2" + else + log_fail "$repo: Version not updated to RC2" + fi + else + if grep -q "version = \"$VERSION\"" pyproject.toml; then + log_pass "$repo: Version bumped to RC2" + else + log_fail "$repo: Version not updated to RC2" + fi + fi + + git commit -q -am "Release candidate $VERSION" + git push -q origin "$RELEASE_BRANCH" +done + +echo "" +log_test "Branch history verification" + +cd "$WORK_ROOT/work/llama-stack" +git fetch -q origin "$RELEASE_BRANCH" +git checkout -q "$RELEASE_BRANCH" + +# Check we have commits for both RCs +commit_count=$(git log --oneline | wc -l | tr -d ' ') +if [ "$commit_count" -ge 3 ]; then + log_pass "Release branch has expected commit history" +else + log_fail "Release branch should have at least 3 commits (initial, RC1, fix, RC2)" +fi + +# Verify both RC commits exist +log_output=$(git log --oneline) +if echo "$log_output" | grep -q "0.1.0rc1" && \ + echo "$log_output" | grep -q "0.1.0rc2" && \ + echo "$log_output" | grep -q "Fix for RC2"; then + log_pass "Both RC commits present in history" +else + log_fail "RC commits not found in history" + echo "$log_output" +fi + +echo "" +echo "========================================" +echo "Summary" +echo "========================================" +echo "Passed: $passed" +echo "Failed: $failed" +echo "" + +if [ $failed -eq 0 ]; then + echo "✓ All integration tests passed!" + exit 0 +else + echo "✗ Some integration tests failed" + exit 1 +fi diff --git a/tests/lib/release-functions.sh b/tests/lib/release-functions.sh new file mode 100644 index 0000000..651a2d3 --- /dev/null +++ b/tests/lib/release-functions.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +# Extracted functions from release workflows for testing + +is_truthy() { + case "$1" in + true | 1) return 0 ;; + false | 0) return 1 ;; + *) return 1 ;; + esac +} + +parse_version_and_branch() { + local version=$1 + + # Validate version format (basic check for X.Y.Z or X.Y.Z.W pattern) + if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?(rc[0-9]+)?$ ]]; then + echo "ERROR: Invalid version format: $version" >&2 + echo "Expected format: X.Y.Z[.W][rcN] (e.g., 0.1.0rc1, 1.2.3, 0.2.10.1)" >&2 + return 1 + fi + + # Remove rc suffix if present (e.g., 0.1.0rc1 -> 0.1.0) + local base_version=$(echo "$version" | sed 's/rc[0-9]*$//') + + # Extract major.minor (e.g., 0.1.0 -> 0.1) + local major=$(echo "$base_version" | cut -d. -f1) + local minor=$(echo "$base_version" | cut -d. -f2) + + # Derive branch name: release-{major}.{minor}.x + local branch_name="release-${major}.${minor}.x" + + echo "$branch_name" +} + +github_org() { + echo "llamastack" +} diff --git a/tests/test-determine-base-branch.sh b/tests/test-determine-base-branch.sh deleted file mode 100644 index ca70542..0000000 --- a/tests/test-determine-base-branch.sh +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) -source "$ROOT_DIR/actions/lib/release_utils.sh" - -REPO_URL=${REPO_URL:-https://github.com/llamastack/llama-stack.git} -WORK_ROOT=$(mktemp -d) -trap 'rm -rf "$WORK_ROOT"' EXIT - -REAL_REPO="$WORK_ROOT/llama-stack" - -echo ">>> Cloning $REPO_URL" -git clone --filter=blob:none "$REPO_URL" "$REAL_REPO" >/dev/null -git -C "$REAL_REPO" fetch origin --tags --prune >/dev/null - -assert_branch() { - local repo=$1 - local tag=$2 - local expected=$3 - - echo "==> Checking $tag expected $expected" - git -C "$repo" checkout --detach "$tag" >/dev/null - git -C "$repo" fetch origin --prune >/dev/null - pushd "$repo" >/dev/null - local branch - branch=$(determine_base_branch) - popd >/dev/null - - if [ "$branch" != "$expected" ]; then - echo "[FAIL] $tag resolved to $branch (expected $expected)" >&2 - return 1 - fi - - echo "[PASS] $tag -> $branch" -} - -failures=0 - -# Real history examples should map back to main because release branches are -# materialised after the RC is cut. -assert_branch "$REAL_REPO" "v0.3.0rc6" "main" || failures=$((failures + 1)) -assert_branch "$REAL_REPO" "v0.2.10rc2" "main" || failures=$((failures + 1)) -assert_branch "$REAL_REPO" "v0.2.10.1rc1" "main" || failures=$((failures + 1)) - -# Synthetic repo to cover patch release flow (release branch diverges from main) -SYNTH_REMOTE="$WORK_ROOT/synth-origin.git" -SYNTH_CLONE="$WORK_ROOT/synth-work" - -git init --bare "$SYNTH_REMOTE" >/dev/null -git clone "$SYNTH_REMOTE" "$SYNTH_CLONE" >/dev/null - -pushd "$SYNTH_CLONE" >/dev/null -git config user.email "ci@example.com" -git config user.name "CI" - -echo "base" >base.txt -git add base.txt -git commit -m "Initial commit" >/dev/null - -git branch -M main -git push -u origin main >/dev/null -git -C "$SYNTH_REMOTE" symbolic-ref HEAD refs/heads/main >/dev/null - -git checkout main -echo "feature" >feature.txt -git add feature.txt -git commit -m "Main feature" >/dev/null -git push origin main >/dev/null - -git checkout -b release-1 -echo "fix1" >fix.txt -git add fix.txt -git commit -m "Release fix prep" >/dev/null -echo "rc content" >rc.txt -git add rc.txt -git commit -m "Release candidate commit" >/dev/null -git tag v1.0.0-rc1 -git push origin release-1 >/dev/null -git push origin v1.0.0-rc1 >/dev/null -popd >/dev/null - -git -C "$SYNTH_CLONE" checkout --detach v1.0.0-rc1 >/dev/null -git -C "$SYNTH_CLONE" fetch origin --prune >/dev/null -pushd "$SYNTH_CLONE" >/dev/null -branch=$(determine_base_branch) -popd >/dev/null - -echo "Synthetic branch resolved: $branch" -if [ "$branch" != "release-1" ]; then - echo "[FAIL] v1.0.0-rc1 resolved to $branch (expected release-1)" >&2 - failures=$((failures + 1)) -else - echo "[PASS] v1.0.0-rc1 -> $branch" -fi - -if [ $failures -ne 0 ]; then - echo "Detected $failures failing cases." >&2 - exit 1 -fi - -echo "All cases passed." diff --git a/tests/test-release-workflows.sh b/tests/test-release-workflows.sh new file mode 100755 index 0000000..2a20ce3 --- /dev/null +++ b/tests/test-release-workflows.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env bash + +ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +WORK_ROOT=$(mktemp -d) +trap 'rm -rf "$WORK_ROOT"' EXIT + +# Source the functions +source "$ROOT_DIR/tests/lib/release-functions.sh" + +passed=0 +failed=0 + +log_test() { echo "[TEST] $1"; } +log_pass() { echo "[PASS] $1"; ((passed++)); } +log_fail() { echo "[FAIL] $1"; ((failed++)); } + +echo "===============================" +echo "Release Workflow Tests" +echo "===============================" +echo "" + +# Test version parsing +log_test "Version parsing" + +result=$(parse_version_and_branch "0.1.0rc1") +if [ "$result" = "release-0.1.x" ]; then + log_pass "0.1.0rc1 → release-0.1.x" +else + log_fail "0.1.0rc1 failed: got $result" +fi + +result=$(parse_version_and_branch "1.2.3") +if [ "$result" = "release-1.2.x" ]; then + log_pass "1.2.3 → release-1.2.x" +else + log_fail "1.2.3 failed: got $result" +fi + +result=$(parse_version_and_branch "0.2.10.1rc5") +if [ "$result" = "release-0.2.x" ]; then + log_pass "0.2.10.1rc5 → release-0.2.x" +else + log_fail "0.2.10.1rc5 failed: got $result" +fi + +# Test invalid versions (expect them to fail) +if parse_version_and_branch "foobar" >/dev/null 2>&1; then + log_fail "Should reject 'foobar'" +else + log_pass "Rejected invalid version 'foobar'" +fi + +if parse_version_and_branch "0.1" >/dev/null 2>&1; then + log_fail "Should reject '0.1'" +else + log_pass "Rejected incomplete version '0.1'" +fi + +echo "" + +# Test git operations with synthetic repos +log_test "Git branch operations" + +test_repo="$WORK_ROOT/test-repo" +git init -q "$test_repo" +cd "$test_repo" +git config user.email "test@example.com" +git config user.name "Test" + +echo "initial" > file.txt +git add file.txt +git commit -q -m "Initial" +git branch -M main + +# Add more commits +echo "feature1" >> file.txt +git commit -q -am "Feature 1" +echo "feature2" >> file.txt +git commit -q -am "Feature 2" + +# Create release branch +git checkout -q -b release-0.1.x +echo "rc prep" >> file.txt +git commit -q -am "RC prep" +rc_commit=$(git rev-parse HEAD) + +log_pass "Created synthetic repo with release branch" + +# Test branch exists +git checkout -q main +if git show-ref --verify --quiet refs/heads/release-0.1.x; then + log_pass "Release branch exists" +else + log_fail "Release branch should exist" +fi + +# Test getting HEAD of branch +branch_head=$(git rev-parse release-0.1.x) +[ "$branch_head" = "$rc_commit" ] && log_pass "Can get HEAD of release branch" || log_fail "Branch HEAD mismatch" + +echo "" + +# Test commit ancestry +log_test "Commit ancestry checking" + +old_commit=$(git rev-parse main~1) +if git merge-base --is-ancestor "$old_commit" "release-0.1.x"; then + log_pass "Old commit is ancestor of release branch" +else + log_fail "Ancestry check failed" +fi + +# Test that main and release have common ancestor +if git merge-base main release-0.1.x >/dev/null 2>&1; then + log_pass "Main and release branch have common ancestor" +else + log_fail "Should have common ancestor" +fi + +echo "" + +# Test version bump operations +log_test "Version bump operations" + +bump_test_dir="$WORK_ROOT/bump-test" +mkdir -p "$bump_test_dir" +cd "$bump_test_dir" + +cat > pyproject.toml <<'EOF' +[project] +name = "test" +version = "0.1.0rc1" +dependencies = [ + "llama-stack-client>=0.1.0rc1", +] +EOF + +perl -pi -e 's/^version = .*$/version = "0.1.0"/' pyproject.toml +perl -pi -e 's/llama-stack-client>=.*/llama-stack-client>=0.1.0",/' pyproject.toml + +if grep -q 'version = "0.1.0"' pyproject.toml && grep -q 'llama-stack-client>=0.1.0"' pyproject.toml; then + log_pass "Version bump RC → final" +else + log_fail "Version bump failed" +fi + +echo "" + +# Test dev version calculation +log_test "Dev version calculation" + +calc_dev_version() { + local release=$1 + local major=$(echo "$release" | cut -d. -f1) + local minor=$(echo "$release" | cut -d. -f2) + local patch=$(echo "$release" | cut -d. -f3) + local next_patch=$((patch + 1)) + echo "${major}.${minor}.${next_patch}.dev0" +} + +result=$(calc_dev_version "0.1.0") +[ "$result" = "0.1.1.dev0" ] && log_pass "0.1.0 → 0.1.1.dev0" || log_fail "Dev version calc failed" + +result=$(calc_dev_version "1.2.5") +[ "$result" = "1.2.6.dev0" ] && log_pass "1.2.5 → 1.2.6.dev0" || log_fail "Dev version calc failed" + +echo "" + +# Run integration tests +echo "===============================" +echo "Running Integration Tests" +echo "===============================" +echo "" + +if bash "$ROOT_DIR/tests/integration/test-cut-rc.sh"; then + log_pass "Integration test suite passed" +else + log_fail "Integration test suite failed" +fi + +echo "" + +# Summary +echo "===============================" +echo "Summary" +echo "===============================" +echo "Passed: $passed" +echo "Failed: $failed" +echo "" + +if [ $failed -eq 0 ]; then + echo "✓ All tests passed!" + exit 0 +else + echo "✗ Some tests failed" + exit 1 +fi