Skip to content
Open
99 changes: 99 additions & 0 deletions .github/workflows/_conda-forge-package-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
name: _Conda Forge Package Release

on:
workflow_call:
inputs:
package:
description: 'Package being released (e.g. sagemaker-train)'
required: true
type: string
feedstock:
description: 'Feedstock repo (e.g. conda-forge/sagemaker-train-feedstock)'
required: true
type: string
pr_search:
description: 'PR title search string (e.g. sagemaker-train v1.6.0)'
required: true
type: string
version:
description: 'Version of this package being released (e.g. 1.6.0)'
required: true
type: string
dep_package:
description: 'Conda dependency to wait for before retrying CI (e.g. sagemaker-core)'
required: true
type: string
dep_version:
description: 'Version of the dependency to wait for (e.g. 2.6.0)'
required: true
type: string
poll_interval:
required: true
type: string
max_attempts:
required: true
type: string
secrets:
token:
required: true

jobs:
release:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.token }}
steps:
- name: Wait for dependency (${{ inputs.dep_package }}==${{ inputs.dep_version }}) on conda-forge
run: |
PACKAGE="${{ inputs.dep_package }}"
VERSION="${{ inputs.dep_version }}"
echo "Waiting for ${PACKAGE}==${VERSION} on conda-forge..."
for i in $(seq 1 ${{ inputs.max_attempts }}); do
RESULT=$(conda search -c conda-forge --override-channels \
"${PACKAGE}==${VERSION}" --json 2>/dev/null \
| python3 -c "import sys,json; d=json.load(sys.stdin); print('found' if d.get('$PACKAGE') else 'not_found')" \
2>/dev/null || echo "not_found")
echo "Attempt $i: ${RESULT}"
[ "$RESULT" = "found" ] && echo "${PACKAGE}==${VERSION} is available." && exit 0
sleep ${{ inputs.poll_interval }}
done
echo "Timed out waiting for ${PACKAGE}==${VERSION}." && exit 1

- name: Merge and wait for ${{ inputs.package }} feedstock PR
run: |
REPO="${{ inputs.feedstock }}"
SEARCH="${{ inputs.pr_search }}"
for i in $(seq 1 ${{ inputs.max_attempts }}); do
STATE=$(gh pr list --repo "$REPO" --state all \
--search "$SEARCH" --json state -q '.[0].state // "NOT_FOUND"')
echo "Attempt $i: ${STATE}"
[ "$STATE" = "MERGED" ] && exit 0
PR=$(gh pr list --repo "$REPO" --state open \
--search "$SEARCH" --json number -q '.[0].number')
if [ -n "$PR" ]; then
CI_STATUS=$(gh pr view "$PR" --repo "$REPO" \
--json statusCheckRollup -q '
.statusCheckRollup | map(
if .__typename == "CheckRun" then .conclusion
elif .__typename == "StatusContext" then .state
else null end
) | if length == 0 then "pending"
elif any(. == null or . == "" or . == "IN_PROGRESS" or . == "QUEUED" or . == "WAITING" or . == "PENDING") then "pending"
elif all(. == "SUCCESS") then "success"
else "failure" end')
echo "CI status: ${CI_STATUS}"
if [ "$CI_STATUS" = "success" ]; then
echo "CI passed, merging PR #${PR}..."
gh pr merge "$PR" --repo "$REPO" --merge 2>/dev/null || true
elif [ "$CI_STATUS" = "failure" ]; then
echo "CI failed, retriggering..."
BRANCH=$(gh pr view "$PR" --repo "$REPO" --json headRefName -q .headRefName)
RUN_ID=$(gh run list --repo "$REPO" --branch "$BRANCH" \
--json databaseId,conclusion -q \
'[.[] | select(.conclusion=="failure")][0].databaseId')
[ -n "$RUN_ID" ] && gh run rerun "$RUN_ID" --repo "$REPO" --failed || true
fi
fi
sleep ${{ inputs.poll_interval }}
done
echo "Timed out waiting for ${{ inputs.package }} PR to merge." && exit 1
122 changes: 122 additions & 0 deletions .github/workflows/conda-forge-release-chain.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
name: Conda Forge Release Chain

on:
workflow_dispatch:
inputs:
poll_interval_seconds:
description: 'Seconds between polls (default: 300)'
required: false
default: '300'
timeout_attempts:
description: 'Max poll attempts per package before failing (default: 20 = 100min at 300s)'
required: false
default: '20'

jobs:
read-versions:
runs-on: ubuntu-latest
outputs:
core: ${{ steps.v.outputs.core }}
train: ${{ steps.v.outputs.train }}
serve: ${{ steps.v.outputs.serve }}
mlops: ${{ steps.v.outputs.mlops }}
pysdk: ${{ steps.v.outputs.pysdk }}
meta: ${{ steps.v.outputs.meta }}
steps:
- uses: actions/checkout@v4
- name: Read versions
id: v
run: |
echo "core=$(cat sagemaker-core/VERSION)" >> $GITHUB_OUTPUT
echo "train=$(cat sagemaker-train/VERSION)" >> $GITHUB_OUTPUT
echo "serve=$(cat sagemaker-serve/VERSION)" >> $GITHUB_OUTPUT
echo "mlops=$(cat sagemaker-mlops/VERSION)" >> $GITHUB_OUTPUT
echo "pysdk=$(cat VERSION)" >> $GITHUB_OUTPUT
echo "meta=$(cat VERSION)" >> $GITHUB_OUTPUT
echo "Versions:"
echo " sagemaker-core: $(cat sagemaker-core/VERSION)"
echo " sagemaker-train: $(cat sagemaker-train/VERSION)"
echo " sagemaker-serve: $(cat sagemaker-serve/VERSION)"
echo " sagemaker-mlops: $(cat sagemaker-mlops/VERSION)"
echo " sagemaker-python-sdk: $(cat VERSION)"
echo " sagemaker (meta): $(cat VERSION)"

# sagemaker-train waits for sagemaker-core
release-sagemaker-train:
needs: read-versions
uses: ./.github/workflows/_conda-forge-package-release.yml
with:
package: sagemaker-train
feedstock: conda-forge/sagemaker-train-feedstock
pr_search: "sagemaker-train v${{ needs.read-versions.outputs.train }}"
version: ${{ needs.read-versions.outputs.train }}
dep_package: sagemaker-core
dep_version: ${{ needs.read-versions.outputs.core }}
poll_interval: ${{ github.event.inputs.poll_interval_seconds }}
max_attempts: ${{ github.event.inputs.timeout_attempts }}
secrets:
token: ${{ secrets.CONDA_FORGE_RELEASE }}

# sagemaker-serve waits for sagemaker-train
release-sagemaker-serve:
needs: [read-versions, release-sagemaker-train]
uses: ./.github/workflows/_conda-forge-package-release.yml
with:
package: sagemaker-serve
feedstock: conda-forge/sagemaker-serve-feedstock
pr_search: "sagemaker-serve v${{ needs.read-versions.outputs.serve }}"
version: ${{ needs.read-versions.outputs.serve }}
dep_package: sagemaker-train
dep_version: ${{ needs.read-versions.outputs.train }}
poll_interval: ${{ github.event.inputs.poll_interval_seconds }}
max_attempts: ${{ github.event.inputs.timeout_attempts }}
secrets:
token: ${{ secrets.CONDA_FORGE_RELEASE }}

# sagemaker-mlops waits for sagemaker-serve
release-sagemaker-mlops:
needs: [read-versions, release-sagemaker-serve]
uses: ./.github/workflows/_conda-forge-package-release.yml
with:
package: sagemaker-mlops
feedstock: conda-forge/sagemaker-mlops-feedstock
pr_search: "sagemaker-mlops v${{ needs.read-versions.outputs.mlops }}"
version: ${{ needs.read-versions.outputs.mlops }}
dep_package: sagemaker-serve
dep_version: ${{ needs.read-versions.outputs.serve }}
poll_interval: ${{ github.event.inputs.poll_interval_seconds }}
max_attempts: ${{ github.event.inputs.timeout_attempts }}
secrets:
token: ${{ secrets.CONDA_FORGE_RELEASE }}

# sagemaker-python-sdk waits for sagemaker-mlops
release-sagemaker-python-sdk:
needs: [read-versions, release-sagemaker-mlops]
uses: ./.github/workflows/_conda-forge-package-release.yml
with:
package: sagemaker-python-sdk
feedstock: conda-forge/sagemaker-python-sdk-feedstock
pr_search: "[bot-automerge] sagemaker-python-sdk v${{ needs.read-versions.outputs.pysdk }}"
version: ${{ needs.read-versions.outputs.pysdk }}
dep_package: sagemaker-mlops
dep_version: ${{ needs.read-versions.outputs.mlops }}
poll_interval: ${{ github.event.inputs.poll_interval_seconds }}
max_attempts: ${{ github.event.inputs.timeout_attempts }}
secrets:
token: ${{ secrets.CONDA_FORGE_RELEASE }}

# sagemaker (meta) waits for sagemaker-python-sdk
release-sagemaker:
needs: [read-versions, release-sagemaker-python-sdk]
uses: ./.github/workflows/_conda-forge-package-release.yml
with:
package: sagemaker
feedstock: conda-forge/sagemaker-feedstock
pr_search: "[bot-automerge] sagemaker v${{ needs.read-versions.outputs.meta }}"
version: ${{ needs.read-versions.outputs.meta }}
dep_package: sagemaker-python-sdk
dep_version: ${{ needs.read-versions.outputs.pysdk }}
poll_interval: ${{ github.event.inputs.poll_interval_seconds }}
max_attempts: ${{ github.event.inputs.timeout_attempts }}
secrets:
token: ${{ secrets.CONDA_FORGE_RELEASE }}
Loading