|
2 | 2 | # |
3 | 3 | # 1. In repository Settings -> Actions -> General, ensure "Allow all actions and reusable workflows" is selected, and that under "Workflow permissions", "Read repository contents and packages permissions" is checked. |
4 | 4 | # |
5 | | -name: Release on Tag |
| 5 | +# Releases are driven by the version in pyproject.toml: when a commit lands on |
| 6 | +# main with a version higher than the latest GitHub release, this workflow |
| 7 | +# tags it, builds and pushes the Docker image to GHCR, publishes to PyPI, and |
| 8 | +# creates a GitHub release. No manual tagging required. |
| 9 | +name: Release |
6 | 10 | on: |
7 | 11 | push: |
8 | | - tags: |
9 | | - - '*' |
| 12 | + branches: |
| 13 | + - main |
| 14 | +permissions: |
| 15 | + contents: write |
| 16 | + packages: write |
10 | 17 | jobs: |
11 | 18 | release: |
12 | 19 | runs-on: ubuntu-latest |
13 | | - permissions: |
14 | | - contents: write |
15 | | - packages: write |
16 | 20 | steps: |
17 | 21 | - uses: actions/checkout@v6 |
18 | | - - name: Set up Python |
19 | | - uses: actions/setup-python@v6 |
20 | 22 | with: |
21 | | - python-version: "3.13" |
22 | | - - name: Install dependencies |
23 | | - run: python -m pip install --upgrade pip && pip install poetry && poetry install |
24 | | - - name: Get Version |
25 | | - id: get-version |
26 | | - run: echo "APP_VERSION=$(poetry version -s)" >> $GITHUB_OUTPUT |
27 | | - - name: Ensure tag matches version |
28 | | - if: github.ref_name != steps.get-version.outputs.APP_VERSION |
| 23 | + fetch-depth: 0 |
| 24 | + - name: Check version vs. latest release |
| 25 | + id: version_check |
| 26 | + env: |
| 27 | + GH_TOKEN: ${{ github.token }} |
29 | 28 | run: | |
30 | | - echo "ERROR: tag name (${{ github.ref_name }}) does not match current version in version.py (${{ steps.get-version.outputs.APP_VERSION }})" |
31 | | - exit 2 |
| 29 | + CURRENT_VERSION=$(python3 -c " |
| 30 | + import re, pathlib |
| 31 | + text = pathlib.Path('pyproject.toml').read_text() |
| 32 | + m = re.search(r'^version\s*=\s*\"([^\"]+)\"', text, re.MULTILINE) |
| 33 | + print(m.group(1)) |
| 34 | + ") |
| 35 | + echo "current_version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT" |
| 36 | +
|
| 37 | + LATEST_TAG=$(gh release list --limit 1 --json tagName --jq '.[0].tagName // empty' 2>/dev/null || true) |
| 38 | +
|
| 39 | + if [ -z "$LATEST_TAG" ]; then |
| 40 | + echo "No existing releases found — first release" |
| 41 | + echo "should_release=true" >> "$GITHUB_OUTPUT" |
| 42 | + else |
| 43 | + LATEST_VERSION="${LATEST_TAG#v}" |
| 44 | + SHOULD=$(python3 -c " |
| 45 | + current = tuple(int(x) for x in '$CURRENT_VERSION'.split('.')) |
| 46 | + latest = tuple(int(x) for x in '$LATEST_VERSION'.split('.')) |
| 47 | + print('true' if current > latest else 'false') |
| 48 | + ") |
| 49 | + echo "Latest release: $LATEST_TAG, current version: $CURRENT_VERSION, should_release: $SHOULD" |
| 50 | + echo "should_release=$SHOULD" >> "$GITHUB_OUTPUT" |
| 51 | + fi |
32 | 52 | - name: Set up Docker Buildx |
| 53 | + if: steps.version_check.outputs.should_release == 'true' |
33 | 54 | uses: docker/setup-buildx-action@v4 |
34 | 55 | - name: Login to GHCR |
| 56 | + if: steps.version_check.outputs.should_release == 'true' |
35 | 57 | run: echo "${{secrets.GITHUB_TOKEN}}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin |
36 | 58 | - name: Lowercase image repository name |
| 59 | + if: steps.version_check.outputs.should_release == 'true' |
37 | 60 | id: image |
38 | 61 | run: echo "repo=${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT" |
39 | 62 | - name: Docker Build and Push |
| 63 | + if: steps.version_check.outputs.should_release == 'true' |
40 | 64 | uses: docker/build-push-action@v7 |
41 | 65 | with: |
42 | 66 | push: true |
43 | 67 | sbom: true |
44 | 68 | labels: | |
45 | 69 | org.opencontainers.image.url=https://github.com/${{ github.repository }} |
46 | 70 | org.opencontainers.image.source=https://github.com/${{ github.repository }} |
47 | | - org.opencontainers.image.version=${{ github.ref_name }} |
| 71 | + org.opencontainers.image.version=${{ steps.version_check.outputs.current_version }} |
48 | 72 | org.opencontainers.image.revision=${{ github.sha }} |
49 | 73 | tags: | |
50 | | - ghcr.io/${{ steps.image.outputs.repo }}:${{ github.ref_name }} |
| 74 | + ghcr.io/${{ steps.image.outputs.repo }}:${{ steps.version_check.outputs.current_version }} |
51 | 75 | ghcr.io/${{ steps.image.outputs.repo }}:latest |
52 | 76 | - name: Build and publish to pypi |
| 77 | + if: steps.version_check.outputs.should_release == 'true' |
53 | 78 | uses: JRubics/poetry-publish@v2.1 |
54 | 79 | with: |
55 | 80 | pypi_token: ${{ secrets.PYPI_TOKEN }} |
56 | | - - name: Create Release |
57 | | - id: create_release |
58 | | - uses: softprops/action-gh-release@v3 |
| 81 | + - name: Create git tag |
| 82 | + if: steps.version_check.outputs.should_release == 'true' |
| 83 | + run: | |
| 84 | + VERSION="${{ steps.version_check.outputs.current_version }}" |
| 85 | + git tag "$VERSION" |
| 86 | + git push origin "$VERSION" |
| 87 | + - name: Create GitHub Release |
| 88 | + if: steps.version_check.outputs.should_release == 'true' |
59 | 89 | env: |
60 | | - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token |
61 | | - with: |
62 | | - tag_name: ${{ github.ref_name }} |
63 | | - name: Release ${{ github.ref_name }} |
64 | | - body: | |
65 | | - Release ${{ github.ref_name }}. |
66 | | - Docker images: https://github.com/orgs/Decaturmakers/packages/container/package/machine-access-control |
67 | | - Python packages: https://pypi.org/project/machine_access_control/ |
68 | | - draft: false |
69 | | - prerelease: false |
| 90 | + GH_TOKEN: ${{ github.token }} |
| 91 | + run: | |
| 92 | + VERSION="${{ steps.version_check.outputs.current_version }}" |
| 93 | + REPO="${{ github.repository }}" |
| 94 | + IMAGE="ghcr.io/${{ steps.image.outputs.repo }}" |
| 95 | +
|
| 96 | + # Generate changelog via GitHub API |
| 97 | + CHANGELOG=$(gh api \ |
| 98 | + "repos/$REPO/releases/generate-notes" \ |
| 99 | + -f tag_name="$VERSION" \ |
| 100 | + -f target_commitish="main" \ |
| 101 | + --jq '.body') |
| 102 | +
|
| 103 | + # Build release body in a file to avoid shell escaping issues |
| 104 | + { |
| 105 | + printf '%s\n' "$CHANGELOG" |
| 106 | + printf '\n---\n\n## Docker Image\n\n```bash\n' |
| 107 | + printf 'docker pull %s:%s\n' "$IMAGE" "$VERSION" |
| 108 | + printf 'docker pull %s:latest\n' "$IMAGE" |
| 109 | + printf '```\n' |
| 110 | + printf '\n## Python Package\n\nhttps://pypi.org/project/machine_access_control/%s/\n' "$VERSION" |
| 111 | + } > /tmp/release_body.md |
| 112 | +
|
| 113 | + gh release create "$VERSION" \ |
| 114 | + --title "Release $VERSION" \ |
| 115 | + --notes-file /tmp/release_body.md |
0 commit comments