|
1 | 1 | name: Publish to PyPI |
2 | 2 |
|
3 | 3 | on: |
4 | | - push: |
5 | | - tags: |
6 | | - - "v*" |
| 4 | + workflow_run: |
| 5 | + workflows: ["Tests"] |
| 6 | + types: [completed] |
| 7 | + branches: [] |
7 | 8 |
|
8 | 9 | permissions: |
9 | 10 | contents: write |
10 | 11 |
|
11 | 12 | jobs: |
12 | | - get-version: |
| 13 | + setup: |
| 14 | + name: Pre-flight Check |
13 | 15 | runs-on: ubuntu-latest |
| 16 | + if: > |
| 17 | + github.event.workflow_run.conclusion == 'success' && |
| 18 | + github.event.workflow_run.event == 'push' && |
| 19 | + startsWith(github.event.workflow_run.head_branch, 'v') |
14 | 20 | outputs: |
| 21 | + is_release: ${{ steps.check.outputs.is_release }} |
15 | 22 | version: ${{ steps.version.outputs.version }} |
| 23 | + tag_name: ${{ steps.check.outputs.tag_name }} |
| 24 | + commit_sha: ${{ steps.check.outputs.commit_sha }} |
16 | 25 | steps: |
17 | | - - name: Extract version |
| 26 | + - name: Determine trigger context |
| 27 | + id: context |
| 28 | + run: | |
| 29 | + if [ "${{ github.event_name }}" == "push" ]; then |
| 30 | + echo "trigger_type=direct_tag" >> $GITHUB_OUTPUT |
| 31 | + echo "tag_name=${{ github.ref_name }}" >> $GITHUB_OUTPUT |
| 32 | + echo "commit_sha=${{ github.sha }}" >> $GITHUB_OUTPUT |
| 33 | + else |
| 34 | + echo "trigger_type=workflow_run" >> $GITHUB_OUTPUT |
| 35 | + echo "tag_name=${{ github.event.workflow_run.head_branch }}" >> $GITHUB_OUTPUT |
| 36 | + echo "commit_sha=${{ github.event.workflow_run.head_sha }}" >> $GITHUB_OUTPUT |
| 37 | + fi |
| 38 | +
|
| 39 | + - uses: actions/checkout@v4 |
| 40 | + with: |
| 41 | + ref: ${{ steps.context.outputs.commit_sha }} |
| 42 | + fetch-depth: 0 |
| 43 | + fetch-tags: true |
| 44 | + |
| 45 | + - name: Verify Tag |
| 46 | + id: check |
| 47 | + run: | |
| 48 | + TRIGGER_NAME="${{ steps.context.outputs.tag_name }}" |
| 49 | + TRIGGER_SHA="${{ steps.context.outputs.commit_sha }}" |
| 50 | + TRIGGER_TYPE="${{ steps.context.outputs.trigger_type }}" |
| 51 | +
|
| 52 | + echo "🔎 Analyzing trigger: $TRIGGER_NAME ($TRIGGER_SHA)" |
| 53 | + echo "Trigger type: $TRIGGER_TYPE" |
| 54 | +
|
| 55 | + # Version tag regex |
| 56 | + # Matches: v1.2.3, v1.2.3a1, v1.2.3b2, v1.2.3rc1, v1.2.3.post1, v1.2.3.dev0 |
| 57 | + if [[ ! "$TRIGGER_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(a|b|rc|\.post|\.dev)?[0-9]*$ ]]; then |
| 58 | + echo "::notice::Skipping: '$TRIGGER_NAME' is not a valid release tag." |
| 59 | + echo "Expected format: vX.Y.Z or vX.Y.Z{a,b,rc,.post,.dev}N" |
| 60 | + echo "is_release=false" >> $GITHUB_OUTPUT |
| 61 | + exit 0 |
| 62 | + fi |
| 63 | +
|
| 64 | + if ! git fetch origin "refs/tags/$TRIGGER_NAME:refs/tags/$TRIGGER_NAME" 2>&1; then |
| 65 | + echo "::warning::Failed to fetch tag $TRIGGER_NAME - may not exist yet" |
| 66 | + fi |
| 67 | +
|
| 68 | + # Check if this is actually a tag |
| 69 | + TAG_SHA=$(git rev-list -n 1 "refs/tags/$TRIGGER_NAME" 2>/dev/null || echo "NOT_FOUND") |
| 70 | +
|
| 71 | + if [ "$TAG_SHA" == "NOT_FOUND" ]; then |
| 72 | + echo "::notice::Skipping: Tag 'refs/tags/$TRIGGER_NAME' does not exist." |
| 73 | + echo "is_release=false" >> $GITHUB_OUTPUT |
| 74 | + exit 0 |
| 75 | + fi |
| 76 | +
|
| 77 | + if [ "$TAG_SHA" != "$TRIGGER_SHA" ]; then |
| 78 | + echo "::warning::SHA Mismatch - not a tag release!" |
| 79 | + echo " - Event SHA: $TRIGGER_SHA (The code that was tested)" |
| 80 | + echo " - Tag SHA: $TAG_SHA (The actual tag)" |
| 81 | + echo " This is likely a branch named '$TRIGGER_NAME', not the tag." |
| 82 | + echo "is_release=false" >> $GITHUB_OUTPUT |
| 83 | + exit 0 |
| 84 | + fi |
| 85 | +
|
| 86 | + # Check if this version already exists on PyPI |
| 87 | + VERSION="${TRIGGER_NAME#v}" |
| 88 | + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "https://pypi.org/pypi/canirun/$VERSION/json") |
| 89 | +
|
| 90 | + if [ "$HTTP_CODE" == "200" ]; then |
| 91 | + echo "::warning::Version $VERSION already exists on PyPI. Skipping republish." |
| 92 | + echo "is_release=false" >> $GITHUB_OUTPUT |
| 93 | + exit 0 |
| 94 | + elif [ "$HTTP_CODE" != "404" ]; then |
| 95 | + echo "::error::Unable to verify PyPI status (HTTP $HTTP_CODE). Aborting for safety." |
| 96 | + echo "Check https://pypi.org/pypi/canirun/$VERSION/json manually." |
| 97 | + echo "is_release=false" >> $GITHUB_OUTPUT |
| 98 | + exit 1 |
| 99 | + fi |
| 100 | +
|
| 101 | + echo "✅ Verified: Valid release tag with matching commit." |
| 102 | + echo "is_release=true" >> $GITHUB_OUTPUT |
| 103 | + echo "tag_name=$TRIGGER_NAME" >> $GITHUB_OUTPUT |
| 104 | + echo "commit_sha=$TRIGGER_SHA" >> $GITHUB_OUTPUT |
| 105 | +
|
| 106 | + - name: Extract Version |
18 | 107 | id: version |
| 108 | + if: steps.check.outputs.is_release == 'true' |
19 | 109 | run: | |
20 | | - VERSION=${GITHUB_REF_NAME#v} |
21 | | - echo "version=$VERSION" >> $GITHUB_OUTPUT |
| 110 | + VERSION="${{ steps.check.outputs.tag_name }}" |
| 111 | + echo "version=${VERSION#v}" >> $GITHUB_OUTPUT |
22 | 112 |
|
23 | | - build-and-release: |
| 113 | + - name: Verify package version matches tag |
| 114 | + if: steps.check.outputs.is_release == 'true' |
| 115 | + run: | |
| 116 | + TAG_VERSION="${{ steps.version.outputs.version }}" |
| 117 | + VERSION_MISMATCH=0 |
| 118 | +
|
| 119 | + # Check pyproject.toml |
| 120 | + if [ -f "pyproject.toml" ]; then |
| 121 | + PKG_VERSION=$(grep -E '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/' | tr -d ' ') |
| 122 | + if [ -n "$PKG_VERSION" ]; then |
| 123 | + if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then |
| 124 | + echo "::error::pyproject.toml version mismatch!" |
| 125 | + echo " Tag version: $TAG_VERSION" |
| 126 | + echo " pyproject.toml version: $PKG_VERSION" |
| 127 | + VERSION_MISMATCH=1 |
| 128 | + else |
| 129 | + echo "✅ pyproject.toml version matches: $TAG_VERSION" |
| 130 | + fi |
| 131 | + fi |
| 132 | + fi |
| 133 | +
|
| 134 | + # Check canirun/__init__.py |
| 135 | + if [ -f "canirun/__init__.py" ]; then |
| 136 | + INIT_VERSION=$(grep -E '^__version__ = ' canirun/__init__.py | sed 's/__version__ = "\(.*\)"/\1/' | tr -d ' ' || echo "") |
| 137 | + if [ -n "$INIT_VERSION" ]; then |
| 138 | + if [ "$INIT_VERSION" != "$TAG_VERSION" ]; then |
| 139 | + echo "::error::__init__.py version mismatch!" |
| 140 | + echo " Tag version: $TAG_VERSION" |
| 141 | + echo " __init__.py __version__: $INIT_VERSION" |
| 142 | + VERSION_MISMATCH=1 |
| 143 | + else |
| 144 | + echo "✅ __init__.py __version__ matches: $TAG_VERSION" |
| 145 | + fi |
| 146 | + fi |
| 147 | + fi |
| 148 | +
|
| 149 | + if [ $VERSION_MISMATCH -eq 1 ]; then |
| 150 | + echo "is_release=false" >> $GITHUB_OUTPUT |
| 151 | + exit 1 |
| 152 | + fi |
| 153 | +
|
| 154 | + publish: |
24 | 155 | name: Build and Release |
25 | | - needs: get-version |
| 156 | + needs: setup |
| 157 | + if: needs.setup.outputs.is_release == 'true' |
26 | 158 | runs-on: ubuntu-latest |
27 | 159 | permissions: |
28 | 160 | contents: write |
29 | 161 | id-token: write |
30 | 162 | environment: |
31 | 163 | name: pypi |
32 | | - url: https://pypi.org/project/canirun/${{ needs.get-version.outputs.version }}/ |
| 164 | + url: https://pypi.org/project/canirun/${{ needs.setup.outputs.version }}/ |
33 | 165 |
|
34 | 166 | steps: |
35 | 167 | - uses: actions/checkout@v4 |
| 168 | + with: |
| 169 | + ref: ${{ needs.setup.outputs.commit_sha }} |
36 | 170 |
|
37 | 171 | - name: Set up Python |
38 | 172 | uses: actions/setup-python@v5 |
|
49 | 183 |
|
50 | 184 | - name: Create GitHub Release |
51 | 185 | uses: softprops/action-gh-release@v2 |
52 | | - if: startsWith(github.ref, 'refs/tags/') |
53 | 186 | with: |
| 187 | + tag_name: ${{ needs.setup.outputs.tag_name }} |
54 | 188 | files: dist/* |
55 | 189 | generate_release_notes: true |
56 | 190 |
|
|
0 commit comments