Skip to content
Merged
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
8 changes: 8 additions & 0 deletions .agents/skills/release-openshell/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,11 @@ updating a Snap Store support track. Docs retain an
immutable version and advance latest only for a newer version.

Watch the stable release run and report its result.

## PyPI publishing

Stable releases publish the built Python wheel directly to PyPI from the `publish-pypi` job in `release.yml`, after the other release publishers succeed. The job downloads the wheel from the stable GitHub Release, not the run's build artifact, and checks the stable tag's source commit and the wheel's release checksum. It uses Trusted Publishing with the `pypi` GitHub environment. Configure the `openshell` PyPI project's GitHub publisher with owner `NVIDIA`, repository `OpenShell`, workflow `release.yml`, and environment `pypi`. No PyPI API token or GitLab trigger credentials are required.

The environment must allow the branch used to dispatch Release. For a support release, explicitly allow its support branch before dispatching. Required reviewers, if configured, must approve the PyPI job. Stable docs publish after PyPI succeeds. An existing wheel on PyPI is skipped when retrying Release; PyPI does not allow replacing a published wheel. Because the job publishes the release asset, a new Release run for the same tag can finish a failed PyPI upload.

Development and prerelease wheels remain GitHub Release assets and do not publish to PyPI.
46 changes: 2 additions & 44 deletions .github/workflows/release-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,6 @@ jobs:
id-token: write
attestations: write
artifact-metadata: write
outputs:
wheel_filenames: ${{ steps.wheel_filenames.outputs.wheel_filenames }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

Expand Down Expand Up @@ -397,8 +395,7 @@ jobs:

ls -la release/

- name: Capture wheel filenames
id: wheel_filenames
- name: Verify wheel count
run: |
set -euo pipefail
shopt -s nullglob
Expand All @@ -407,8 +404,6 @@ jobs:
echo "expected exactly one Python wheel, found ${#wheels[@]}" >&2
exit 1
fi
wheel_filename=$(basename "${wheels[0]}")
echo "wheel_filenames=${wheel_filename}" >> "$GITHUB_OUTPUT"

- name: Generate checksums
run: |
Expand Down Expand Up @@ -580,7 +575,7 @@ jobs:

publish-fern-docs:
name: Sync and Publish Fern Docs
needs: [compute-versions, release-dev, release-helm, trigger-wheel-publish]
needs: [compute-versions, release-dev, release-helm]
permissions:
contents: write
uses: ./.github/workflows/sync-docs.yml
Expand All @@ -593,40 +588,3 @@ jobs:
publish: true
secrets:
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}

trigger-wheel-publish:
Comment thread
SDAChess marked this conversation as resolved.
Resolved
name: Trigger Wheel Publish
needs: [compute-versions, release-dev]
runs-on: [self-hosted, nv]
timeout-minutes: 10
steps:
- name: Trigger GitLab CI
env:
GITLAB_CI_TRIGGER_TOKEN: ${{ secrets.GITLAB_CI_TRIGGER_TOKEN }}
GITLAB_CI_TRIGGER_URL: ${{ secrets.GITLAB_CI_TRIGGER_URL }}
RELEASE_VERSION: ${{ needs.compute-versions.outputs.python_version }}
WHEEL_FILENAMES: ${{ needs.release-dev.outputs.wheel_filenames }}
run: |
set -euo pipefail
if [ -z "${WHEEL_FILENAMES}" ]; then
echo "No wheel filenames provided by build job" >&2
exit 1
fi

response=$(curl -X POST \
--fail \
--silent \
--show-error \
-F "token=${GITLAB_CI_TRIGGER_TOKEN}" \
-F "ref=main" \
-F "variables[PIPELINE_ACTION]=publish_wheels" \
-F "variables[GITHUB_REPOSITORY]=${GITHUB_REPOSITORY}" \
-F "variables[COMMIT_SHA]=${GITHUB_SHA}" \
-F "variables[RELEASE_TAG]=dev" \
-F "variables[RELEASE_VERSION]=${RELEASE_VERSION}" \
-F "variables[RELEASE_KIND]=dev" \
-F "variables[WHEEL_FILENAMES]=${WHEEL_FILENAMES}" \
"${GITLAB_CI_TRIGGER_URL}")
pipeline_id=$(printf '%s' "$response" | sed -n 's/.*"id":\([0-9][0-9]*\).*/\1/p')
pipeline_status=$(printf '%s' "$response" | sed -n 's/.*"status":"\([^"]*\)".*/\1/p')
echo "Triggered GitLab pipeline ${pipeline_id:-unknown} with status=${pipeline_status:-unknown}"
1 change: 0 additions & 1 deletion .github/workflows/release-prerelease.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
concurrency:
group: release-prerelease-tags
cancel-in-progress: false
queue: max

Check failure on line 30 in .github/workflows/release-prerelease.yml

View workflow job for this annotation

GitHub Actions / Actionlint (informational)

unexpected key "queue" for "concurrency" section. expected one of "cancel-in-progress", "group"
outputs:
tag: ${{ steps.tag.outputs.tag }}
source-sha: ${{ steps.tag.outputs.source_sha }}
Expand Down Expand Up @@ -124,7 +124,6 @@
release-tag: ${{ needs.resolve.outputs.tag }}
source-sha: ${{ needs.build.outputs.source_sha }}
semver: ${{ needs.build.outputs.semver }}
python-version: ${{ needs.build.outputs.python_version }}
npm-version: ${{ needs.build.outputs.npm_version }}
is-prerelease: true
secrets:
Expand Down
55 changes: 2 additions & 53 deletions .github/workflows/release-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,13 @@ on:
description: Release version without the v prefix
required: true
type: string
python-version:
description: Python package version
required: true
type: string
npm-version:
description: TypeScript package version
required: true
type: string
secrets:
SNAPCRAFT_STORE_CREDENTIALS:
required: false
FERN_TOKEN:
required: false
GITLAB_CI_TRIGGER_TOKEN:
required: false
GITLAB_CI_TRIGGER_URL:
required: false

permissions:
contents: read
Expand All @@ -66,8 +56,6 @@ jobs:
id-token: write
attestations: write
artifact-metadata: write
outputs:
wheel_filenames: ${{ steps.wheel_filenames.outputs.wheel_filenames }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand Down Expand Up @@ -149,8 +137,7 @@ jobs:
path: release/
merge-multiple: true

- name: Capture wheel filenames
id: wheel_filenames
- name: Verify wheel count
run: |
set -euo pipefail
shopt -s nullglob
Expand All @@ -159,8 +146,6 @@ jobs:
echo "expected exactly one Python wheel, found ${#wheels[@]}" >&2
exit 1
fi
wheel_filename=$(basename "${wheels[0]}")
echo "wheel_filenames=${wheel_filename}" >> "$GITHUB_OUTPUT"

- name: Generate checksums
run: |
Expand Down Expand Up @@ -352,8 +337,6 @@ jobs:
timeout-minutes: 10
permissions:
contents: write
outputs:
wheel_filenames: ${{ needs.assemble.outputs.wheel_filenames }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
if: ${{ !inputs.is-prerelease && inputs.make-latest }}
Expand Down Expand Up @@ -563,38 +546,6 @@ jobs:
version: ${{ inputs.semver }}
source-sha: ${{ inputs.source-sha }}

trigger-wheel-publish:
name: Trigger Wheel Publish
needs: release
if: ${{ !inputs.is-prerelease }}
uses: ./.github/workflows/wheel-publish.yml
with:
release-tag: ${{ inputs.release-tag }}
source-sha: ${{ inputs.source-sha }}
python-version: ${{ inputs.python-version }}
wheel-filenames: ${{ needs.release.outputs.wheel_filenames }}
secrets:
GITLAB_CI_TRIGGER_TOKEN: ${{ secrets.GITLAB_CI_TRIGGER_TOKEN }}
GITLAB_CI_TRIGGER_URL: ${{ secrets.GITLAB_CI_TRIGGER_URL }}

publish-fern-docs:
name: Sync and Publish Fern Docs
needs: [release, publish-sdk-typescript, trigger-wheel-publish]
if: ${{ !inputs.is-prerelease }}
permissions:
contents: write
uses: ./.github/workflows/sync-docs.yml
with:
operation: sync
channel: stable
source_ref: ${{ inputs.source-sha }}
release_version: ${{ inputs.semver }}
version_slug: v${{ inputs.semver }}
display_name: Latest (v${{ inputs.semver }})
publish: true
secrets:
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}

# GitHub treats skipped jobs as passing, so a publisher that never ran would
# leave the run green. Fail when any publisher expected for this release kind
# did not succeed, so callers' failure notifications fire.
Expand All @@ -609,8 +560,6 @@ jobs:
- publish-prerelease-snap
- publish-sdk-typescript
- tag-sdk-go
- trigger-wheel-publish
- publish-fern-docs
if: always()
runs-on: ubuntu-latest
timeout-minutes: 2
Expand All @@ -626,7 +575,7 @@ jobs:
if [[ "${IS_PRERELEASE}" == "true" ]]; then
expected+=(publish-prerelease-snap)
else
expected+=(publish-sdk-typescript tag-sdk-go trigger-wheel-publish publish-fern-docs)
expected+=(publish-sdk-typescript tag-sdk-go)
if [[ "${MAKE_LATEST}" == "true" ]]; then
expected+=(publish-snap)
fi
Expand Down
92 changes: 88 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
concurrency:
group: ${{ inputs.notes-only && format('release-notes-preview-{0}', github.ref) || 'release-stable' }}
cancel-in-progress: false
queue: max

Check failure on line 25 in .github/workflows/release.yml

View workflow job for this annotation

GitHub Actions / Actionlint (informational)

unexpected key "queue" for "concurrency" section. expected one of "cancel-in-progress", "group"

jobs:
preview-notes:
Expand Down Expand Up @@ -153,19 +153,103 @@
release-tag: ${{ needs.promote.outputs.tag }}
source-sha: ${{ needs.build.outputs.source_sha }}
semver: ${{ needs.build.outputs.semver }}
python-version: ${{ needs.build.outputs.python_version }}
npm-version: ${{ needs.build.outputs.npm_version }}
is-prerelease: false
make-latest: ${{ needs.promote.outputs.make-latest == 'true' }}
secrets:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}

publish-pypi:
name: Publish Python Wheel to PyPI
needs: [promote, build, publish]
runs-on: ubuntu-latest
timeout-minutes: 10
environment:
name: pypi
url: https://pypi.org/project/openshell/
Comment thread
SDAChess marked this conversation as resolved.
Resolved
permissions:
contents: read
id-token: write
steps:
# Publish the wheel attached to the GitHub Release rather than this run's
# build artifact, so a later run can finish publishing the same bytes.
- name: Download and verify the released wheel
shell: bash
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ needs.promote.outputs.tag }}
SOURCE_SHA: ${{ needs.build.outputs.source_sha }}
PYTHON_VERSION: ${{ needs.build.outputs.python_version }}
run: |
set -euo pipefail
tag_sha=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${RELEASE_TAG}" --jq .sha)
if [[ "${tag_sha}" != "${SOURCE_SHA}" ]]; then
echo "${RELEASE_TAG} points to ${tag_sha}, not ${SOURCE_SHA}" >&2
exit 1
fi
gh release download "${RELEASE_TAG}" -R "${GITHUB_REPOSITORY}" \
--pattern '*.whl' --pattern openshell-checksums-sha256.txt --dir release
shopt -s nullglob
wheels=(release/*.whl)
if [[ "${#wheels[@]}" -ne 1 ]]; then
echo "Expected exactly one Python wheel on ${RELEASE_TAG}, found ${#wheels[@]}" >&2
exit 1
fi
wheel=$(basename "${wheels[0]}")
if [[ "${wheel}" != openshell-"${PYTHON_VERSION}"-*.whl ]]; then
echo "${wheel} is not an openshell ${PYTHON_VERSION} wheel" >&2
exit 1
fi
checksum=$(awk -v wheel="${wheel}" '$2 == wheel { print $1 " " $2 }' release/openshell-checksums-sha256.txt)
if [[ -z "${checksum}" ]]; then
echo "${wheel} is missing from the release checksums" >&2
exit 1
fi
(cd release && sha256sum -c - <<<"${checksum}")
mkdir dist
mv "release/${wheel}" dist/

- name: Publish wheel to PyPI
uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
with:
skip-existing: true

publish-fern-docs:
name: Sync and Publish Fern Docs
needs: [build, publish, publish-pypi]
permissions:
contents: write
uses: ./.github/workflows/sync-docs.yml
with:
operation: sync
channel: stable
source_ref: ${{ needs.build.outputs.source_sha }}
release_version: ${{ needs.build.outputs.semver }}
version_slug: v${{ needs.build.outputs.semver }}
display_name: Latest (v${{ needs.build.outputs.semver }})
publish: true
secrets:
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}
GITLAB_CI_TRIGGER_TOKEN: ${{ secrets.GITLAB_CI_TRIGGER_TOKEN }}
GITLAB_CI_TRIGGER_URL: ${{ secrets.GITLAB_CI_TRIGGER_URL }}

check-publication:
name: Check Stable Publication
needs: [promote, build, release-notes, publish, publish-pypi, publish-fern-docs]
if: ${{ always() && !inputs.notes-only }}
runs-on: ubuntu-latest
timeout-minutes: 2
permissions: {}
steps:
- name: Require every stable publisher to succeed
shell: bash
env:
NEEDS: ${{ toJSON(needs) }}
run: |
set -euo pipefail
jq -e 'all(.[]; .result == "success")' <<<"${NEEDS}"

notify-release-failure:
name: Notify Release Failure
needs: [promote, build, release-notes, publish]
needs: [promote, build, release-notes, publish, publish-pypi, publish-fern-docs, check-publication]
if: failure()
runs-on: ubuntu-latest
timeout-minutes: 2
Expand Down
Loading
Loading