release: import CommonGround Kernel v3 public baseline #1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Publish Package and Release Artifacts | |
| on: | |
| push: | |
| branches: | |
| - main | |
| paths: | |
| - VERSION | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: Existing release tag to reconcile, for example v3.1.0. | |
| required: true | |
| type: string | |
| publish_to_pypi: | |
| description: Publish artifacts to PyPI before syncing the GitHub Release | |
| required: true | |
| default: false | |
| type: boolean | |
| sync_github_release: | |
| description: Create or repair the GitHub Release and release assets | |
| required: true | |
| default: true | |
| type: boolean | |
| concurrency: | |
| group: publish-${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.sha }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: read | |
| jobs: | |
| resolve: | |
| name: Resolve Release Target | |
| runs-on: ubuntu-latest | |
| outputs: | |
| release_version: ${{ steps.target.outputs.release_version }} | |
| release_tag: ${{ steps.target.outputs.release_tag }} | |
| target_commit: ${{ steps.target.outputs.target_commit }} | |
| checkout_ref: ${{ steps.target.outputs.checkout_ref }} | |
| create_tag: ${{ steps.target.outputs.create_tag }} | |
| publish_to_pypi: ${{ steps.target.outputs.publish_to_pypi }} | |
| sync_github_release: ${{ steps.target.outputs.sync_github_release }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Resolve release target | |
| id: target | |
| run: | | |
| set -euo pipefail | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| RAW_TAG="${{ inputs.tag }}" | |
| RELEASE_TAG="${RAW_TAG#refs/tags/}" | |
| RELEASE_TAG="v${RELEASE_TAG#v}" | |
| if ! git ls-remote --exit-code --refs --tags origin "refs/tags/$RELEASE_TAG" >/dev/null 2>&1; then | |
| echo "::error::Tag $RELEASE_TAG does not exist on origin." | |
| exit 1 | |
| fi | |
| git fetch --tags origin | |
| TARGET_COMMIT="$(git rev-list -n 1 "$RELEASE_TAG")" | |
| RELEASE_VERSION="${RELEASE_TAG#v}" | |
| CREATE_TAG="false" | |
| PUBLISH_TO_PYPI="${{ inputs.publish_to_pypi }}" | |
| SYNC_GITHUB_RELEASE="${{ inputs.sync_github_release }}" | |
| else | |
| RELEASE_VERSION="$(sed -n '1{s/[[:space:]]*$//;p;q}' VERSION)" | |
| RELEASE_TAG="v$RELEASE_VERSION" | |
| TARGET_COMMIT="${GITHUB_SHA}" | |
| EXISTING_SHA="$(git ls-remote --tags origin "refs/tags/$RELEASE_TAG^{}" | awk 'NR==1 {print $1}')" | |
| if [ -z "$EXISTING_SHA" ]; then | |
| EXISTING_SHA="$(git ls-remote --refs --tags origin "refs/tags/$RELEASE_TAG" | awk 'NR==1 {print $1}')" | |
| fi | |
| if [ -n "$EXISTING_SHA" ] && [ "$EXISTING_SHA" != "$TARGET_COMMIT" ]; then | |
| echo "::error::Tag $RELEASE_TAG already exists at $EXISTING_SHA, expected $TARGET_COMMIT." | |
| exit 1 | |
| fi | |
| if [ -n "$EXISTING_SHA" ]; then | |
| CREATE_TAG="false" | |
| else | |
| CREATE_TAG="true" | |
| fi | |
| PUBLISH_TO_PYPI="true" | |
| SYNC_GITHUB_RELEASE="true" | |
| fi | |
| if [ -z "$RELEASE_VERSION" ]; then | |
| echo "::error::Resolved release version is empty." | |
| exit 1 | |
| fi | |
| echo "release_version=$RELEASE_VERSION" >> "$GITHUB_OUTPUT" | |
| echo "release_tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT" | |
| echo "target_commit=$TARGET_COMMIT" >> "$GITHUB_OUTPUT" | |
| echo "checkout_ref=$TARGET_COMMIT" >> "$GITHUB_OUTPUT" | |
| echo "create_tag=$CREATE_TAG" >> "$GITHUB_OUTPUT" | |
| echo "publish_to_pypi=$PUBLISH_TO_PYPI" >> "$GITHUB_OUTPUT" | |
| echo "sync_github_release=$SYNC_GITHUB_RELEASE" >> "$GITHUB_OUTPUT" | |
| build: | |
| name: Build Release Artifacts | |
| runs-on: ubuntu-latest | |
| needs: resolve | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ needs.resolve.outputs.checkout_ref }} | |
| submodules: recursive | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.13" | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@v7 | |
| with: | |
| enable-cache: false | |
| - name: Check patch hygiene | |
| run: git diff --check | |
| - name: Validate release version metadata | |
| run: > | |
| uv run --with packaging python scripts/release/sync_release_version.py | |
| --version "${{ needs.resolve.outputs.release_version }}" | |
| --check | |
| - name: Compile Python sources | |
| run: uv run python -m compileall CommonGround Integrations tests scripts | |
| - name: Run lightweight package tests | |
| run: > | |
| uv run --extra server --with pytest --with packaging python -m pytest | |
| tests/test_doc_hygiene.py | |
| tests/test_package_version.py | |
| tests/test_release_tag_version.py | |
| tests/test_release_version_sync.py | |
| tests/test_project_setup_package_resources.py | |
| tests/test_contracts_cardbox_boundary.py | |
| -q | |
| - name: Clean previous build artifacts | |
| run: rm -rf build dist | |
| - name: Build package artifacts | |
| run: uv build --no-sources | |
| - name: Check artifact metadata | |
| run: uvx twine check dist/* | |
| - name: Verify built version matches release tag | |
| run: > | |
| uv run --with packaging python scripts/release/check_dist_tag_version.py | |
| --tag "${{ needs.resolve.outputs.release_tag }}" | |
| - name: Smoke test built wheel | |
| run: bash ./scripts/smoke_test_built_cli.sh dist/commonground_kernel-*.whl | |
| - name: Smoke test built sdist | |
| run: bash ./scripts/smoke_test_built_cli.sh dist/commonground_kernel-*.tar.gz | |
| - name: Upload release artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: release-artifacts | |
| path: dist/* | |
| if-no-files-found: error | |
| create_tag: | |
| name: Create Release Tag | |
| runs-on: ubuntu-latest | |
| needs: | |
| - resolve | |
| - build | |
| if: ${{ needs.resolve.outputs.create_tag == 'true' }} | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Create and push release tag | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git tag -a "${{ needs.resolve.outputs.release_tag }}" "${{ needs.resolve.outputs.target_commit }}" -m "Release ${{ needs.resolve.outputs.release_tag }}" | |
| git push origin "refs/tags/${{ needs.resolve.outputs.release_tag }}" | |
| publish: | |
| name: Publish to PyPI | |
| runs-on: ubuntu-latest | |
| needs: | |
| - resolve | |
| - build | |
| - create_tag | |
| if: >- | |
| ${{ | |
| needs.resolve.outputs.publish_to_pypi == 'true' | |
| && needs.build.result == 'success' | |
| && (needs.create_tag.result == 'success' || needs.create_tag.result == 'skipped') | |
| }} | |
| environment: | |
| name: pypi | |
| url: https://pypi.org/p/commonground-kernel | |
| steps: | |
| - name: Require PyPI API token | |
| env: | |
| PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }} | |
| run: | | |
| if [ -z "${PYPI_API_TOKEN}" ]; then | |
| echo "Missing environment secret PYPI_API_TOKEN for the pypi environment." >&2 | |
| exit 1 | |
| fi | |
| - name: Download release artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: release-artifacts | |
| path: dist | |
| - name: Publish to PyPI | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| password: ${{ secrets.PYPI_API_TOKEN }} | |
| sync_github_release: | |
| name: Sync GitHub Release | |
| runs-on: ubuntu-latest | |
| needs: | |
| - resolve | |
| - build | |
| - create_tag | |
| - publish | |
| if: >- | |
| ${{ | |
| needs.resolve.outputs.sync_github_release == 'true' | |
| && needs.build.result == 'success' | |
| && (needs.create_tag.result == 'success' || needs.create_tag.result == 'skipped') | |
| && (needs.publish.result == 'success' || needs.publish.result == 'skipped') | |
| }} | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Download release artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: release-artifacts | |
| path: dist | |
| - name: Sync GitHub Release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| RELEASE_TAG: ${{ needs.resolve.outputs.release_tag }} | |
| run: | | |
| set -euo pipefail | |
| retry() { | |
| local attempts="$1" | |
| local sleep_seconds="$2" | |
| shift 2 | |
| local attempt=1 | |
| while true; do | |
| if "$@"; then | |
| return 0 | |
| fi | |
| if [ "$attempt" -ge "$attempts" ]; then | |
| echo "Command failed after ${attempts} attempts: $*" >&2 | |
| return 1 | |
| fi | |
| echo "Retrying (${attempt}/${attempts}) after transient failure: $*" >&2 | |
| sleep "$sleep_seconds" | |
| attempt=$((attempt + 1)) | |
| done | |
| } | |
| ensure_release() { | |
| if retry 3 5 gh release view "$RELEASE_TAG" --json url >/dev/null; then | |
| echo "Release ${RELEASE_TAG} already exists." | |
| return 0 | |
| fi | |
| retry 3 5 gh release create "$RELEASE_TAG" --verify-tag --generate-notes | |
| } | |
| ensure_release | |
| mapfile -t existing_assets < <( | |
| retry 3 5 gh release view "$RELEASE_TAG" --json assets --jq '.assets[].name' | |
| ) | |
| for asset in dist/*.tar.gz dist/*.whl; do | |
| asset_name="$(basename "$asset")" | |
| if printf '%s\n' "${existing_assets[@]}" | grep -Fxq "$asset_name"; then | |
| echo "Release asset already present: $asset_name" | |
| continue | |
| fi | |
| retry 3 5 gh release upload "$RELEASE_TAG" "$asset" | |
| done |