# Task Recon: Create release workflow (.github/workflows/release.yml)

**Task ID:** 37.2
**Status:** pending
**Generated:** 2026-01-30
**Agent:** claude-opus-4-5-20251101

---

## Task Summary

Create a GitHub Actions workflow that triggers on version tags (v*) to build Python packages using `uv build`, publish to PyPI using trusted publishing (OIDC), and create a GitHub Release with attached artifacts and auto-generated release notes.

### Parent Task Context

**Parent Task:** Set Up CI/CD Pipeline (ID: 37)
**Parent Goal:** Create GitHub Actions workflows for CI (lint, test), releases, and daily contract tests to ensure code quality and automate the release process.
**This Subtask's Role:** Handles the release pipeline - when a version tag is pushed (e.g., `v0.1.0`), this workflow validates the version, builds the package, publishes to PyPI, and creates a GitHub Release.

### Dependencies
- Depends on: 37.1 (Create main CI workflow) - **DONE**

### Acceptance Criteria
- Workflow triggers on push of tags matching `v*` pattern (e.g., v0.1.0, v1.0.0)
- Build job extracts version from tag and validates it matches pyproject.toml version
- Build job uses `uv build` to create sdist and wheel in dist/
- Build artifacts uploaded for publish job
- Publish job uses `pypa/gh-action-pypi-publish@release/v1` with trusted publishing
- Publish job requires `permissions: id-token: write` and `environment: pypi`
- GitHub Release job creates release from tag with auto-generated notes and attached artifacts
- Prerequisites documented: PyPI trusted publisher configuration required

---

## Insights from Previous Tasks

### Dependency Task Notes
| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 37.1 | Create main CI workflow | Used `actions/checkout@v5`, `astral-sh/setup-uv@v7`, `codecov-action@v5` - use same versions for consistency |
| 37.1 | Create main CI workflow | Python versions must be quoted strings in YAML ("3.10" not 3.10) |
| 37.1 | Create main CI workflow | `fail_ci_if_error: false` pattern for non-critical upload steps |

### Reusable Patterns from Dependencies
- UV setup pattern: `astral-sh/setup-uv@v7` with `enable-cache: true` - see `.github/workflows/ci.yml:15-18`
- Checkout pattern: `actions/checkout@v5` - see `.github/workflows/ci.yml:13`
- Job dependencies: Use `needs:` keyword for sequential jobs

### Warnings and Gotchas
- **From Task 37.1**: Parallel jobs don't use `needs:`, sequential jobs must declare dependencies
- **PyPI Trusted Publishing**: Must configure trusted publisher in PyPI project settings BEFORE first release
- **Version sync**: Both `pyproject.toml` (line 3) and `src/teams_phone/__init__.py` (line 3) contain version - currently both `0.1.0`

### Open Items Inherited
- CODECOV_TOKEN secret must be configured (from 37.1) - not relevant for release workflow
- PyPI trusted publisher must be configured in PyPI project settings (new requirement for this task)

---

## Architecture Context

### Relevant Design Decisions
- **Package Distribution**: PyPI distribution as primary release mechanism (see architecture doc 10-infrastructure-and-deployment.md)
- **Build Backend**: Uses `hatchling` as build backend (pyproject.toml:33-35)
- **Package Name**: PyPI name `teams-phone-cli` (with dashes), import name `teams_phone` (underscores)
- **Version Location**: Single-sourced in `pyproject.toml` version field, also in `__init__.py`

### Module Structure
```
.github/
  workflows/
    ci.yml              # 37.1 - DONE - lint, typecheck, test
    release.yml         # 37.2 - THIS TASK - tag-triggered PyPI publish
    contract-tests.yml  # 37.3 - scheduled API contract validation
```

### Conventions to Follow
- Use `actions/checkout@v5` (consistent with ci.yml)
- Use `astral-sh/setup-uv@v7` with `enable-cache: true`
- Use `pypa/gh-action-pypi-publish@release/v1` for trusted publishing
- Use `softprops/action-gh-release@v2` for GitHub Release creation
- Use `actions/upload-artifact@v4` and `actions/download-artifact@v4` for artifact passing

---

## Related Code References

### Primary References (Must Read)
| File | Lines | Purpose |
|------|-------|---------|
| `.github/workflows/ci.yml` | 1-73 | Existing CI workflow - follow same action versions and patterns |
| `pyproject.toml` | 1-38 | Package metadata, build-system config, version location |
| `src/teams_phone/__init__.py` | 1-3 | Package version string (must match pyproject.toml) |

### Secondary References (Helpful Context)
| File | Lines | Purpose |
|------|-------|---------|
| `.taskmaster/docs/architecture/10-infrastructure-and-deployment.md` | 1-35 | CI/CD pipeline architecture and promotion flow |
| `.taskmaster/tasks/task_037.md` | 48-85 | 37.1 implementation notes - patterns to follow |

### Code Patterns to Follow

```yaml
# Pattern from ci.yml - UV setup (lines 15-18)
- name: Install uv
  uses: astral-sh/setup-uv@v7
  with:
    enable-cache: true

- name: Install dependencies
  run: uv sync
```

```yaml
# Pattern for trusted publishing (from PyPA documentation)
jobs:
  pypi-publish:
    runs-on: ubuntu-latest
    environment:
      name: pypi
      url: https://pypi.org/p/teams-phone-cli
    permissions:
      id-token: write  # Required for OIDC trusted publishing
    steps:
      - uses: pypa/gh-action-pypi-publish@release/v1
```

---

## Dependencies

### Internal Modules
- `teams_phone` - Main package in src/teams_phone/
- `hatchling` - Build backend configured in pyproject.toml

### External Packages
- `hatchling` - Build backend (in build-system.requires)
- All runtime dependencies in pyproject.toml:6-16

### New Dependencies Needed
- None required in pyproject.toml
- **GitHub Configuration Required:**
  - PyPI trusted publisher configuration (see Testing Guidance)
  - GitHub environment named "pypi" for deployment protection

### GitHub Actions Used
| Action | Version | Purpose |
|--------|---------|---------|
| `actions/checkout` | v5 | Checkout code |
| `astral-sh/setup-uv` | v7 | Install uv package manager |
| `actions/upload-artifact` | v4 | Upload build artifacts |
| `actions/download-artifact` | v4 | Download build artifacts |
| `pypa/gh-action-pypi-publish` | release/v1 | Trusted publishing to PyPI |
| `softprops/action-gh-release` | v2 | Create GitHub Release |

---

## Testing Guidance

### Test File Location
Workflow file will be at `.github/workflows/release.yml`

### Test Patterns in Use
```bash
# Build locally to verify package builds correctly
uv build

# Check dist/ contents after build
ls -la dist/
# Should contain:
# - teams_phone_cli-0.1.0.tar.gz (sdist)
# - teams_phone_cli-0.1.0-py3-none-any.whl (wheel)

# Verify version in pyproject.toml
grep "^version" pyproject.toml
# Should output: version = "0.1.0"

# Verify version in __init__.py
grep "__version__" src/teams_phone/__init__.py
# Should output: __version__ = "0.1.0"
```

### PyPI Trusted Publisher Setup (REQUIRED before first release)
1. Log in to PyPI (https://pypi.org)
2. Go to project settings for `teams-phone-cli` (or create pending publisher if first release)
3. Add trusted publisher with:
   - Owner: `<github-org-or-user>`
   - Repository: `teams-phone`
   - Workflow name: `release.yml`
   - Environment: `pypi`

### GitHub Environment Setup (REQUIRED)
1. Go to repository Settings > Environments
2. Create environment named `pypi`
3. (Optional) Add protection rules like required reviewers for production deployments

### Workflow Syntax Validation
```bash
# Use act to test workflow locally (optional)
# Or push a test tag to a feature branch

# Test with a pre-release tag (won't publish to production PyPI)
git tag v0.0.1-test
git push origin v0.0.1-test
```

### TestPyPI Testing (Recommended before production)
- Configure separate trusted publisher for TestPyPI
- Use `repository-url: https://test.pypi.org/legacy/` in action
- Test full flow before production release

---

## Risks and Blockers

### Technical Risks
- [ ] **Version mismatch**: Tag version must match pyproject.toml - need validation step
- [ ] **Trusted publisher not configured**: Workflow will fail if PyPI trusted publisher not set up before first release
- [ ] **Environment not created**: GitHub environment "pypi" must exist for deployment to work
- [ ] **Attestations**: PyPI publish action generates attestations by default - may want to disable initially

### Dependency Risks
- [ ] **37.1 must be complete**: CI workflow should pass before release (already done)
- [ ] **Package name availability**: `teams-phone-cli` must be available or already owned on PyPI

### Open Questions
- [ ] Should build artifacts be tested (smoke test) before publishing? (Recommended but optional per uv docs)
- [ ] Should we use TestPyPI first for pre-release versions? (Recommended for safety)
- [ ] Do we want manual approval required for the pypi environment? (Recommended for production)
- [ ] Should CHANGELOG.md be created and auto-updated? (Not currently present in repo)

---

## Implementation Recommendations

### Suggested Approach
1. Create `.github/workflows/release.yml` with tag trigger `on: push: tags: ['v*']`
2. Add build job: checkout, setup-uv, validate version, uv build, upload artifacts
3. Add publish job: download artifacts, pypa/gh-action-pypi-publish with trusted publishing
4. Add release job: softprops/action-gh-release with auto-generated notes and artifact attachment
5. Document PyPI trusted publisher setup requirements in workflow comments

### Files to Create/Modify
| Action | File | Reason |
|--------|------|--------|
| Create | `.github/workflows/release.yml` | Tag-triggered release workflow |

### Quick Wins
- Start with build job only to validate `uv build` works in CI
- Add version validation as simple shell comparison
- Use `generate_release_notes: true` for automatic release notes from commits

### Version Validation Approach
```bash
# Extract version from tag (strip 'v' prefix)
TAG_VERSION=${GITHUB_REF_NAME#v}

# Extract version from pyproject.toml
PYPROJECT_VERSION=$(grep "^version" pyproject.toml | cut -d'"' -f2)

# Validate match
if [ "$TAG_VERSION" != "$PYPROJECT_VERSION" ]; then
  echo "Version mismatch: tag=$TAG_VERSION, pyproject.toml=$PYPROJECT_VERSION"
  exit 1
fi
```

---

## Cheat Sheet

### Key Workflow Structure
```yaml
name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5

      - name: Install uv
        uses: astral-sh/setup-uv@v7
        with:
          enable-cache: true

      - name: Validate version
        run: |
          TAG_VERSION=${GITHUB_REF_NAME#v}
          PYPROJECT_VERSION=$(grep "^version" pyproject.toml | cut -d'"' -f2)
          if [ "$TAG_VERSION" != "$PYPROJECT_VERSION" ]; then
            echo "::error::Version mismatch: tag=$TAG_VERSION, pyproject.toml=$PYPROJECT_VERSION"
            exit 1
          fi

      - name: Build package
        run: uv build

      - name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/

  publish:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: pypi
      url: https://pypi.org/p/teams-phone-cli
    permissions:
      id-token: write
    steps:
      - name: Download artifacts
        uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/

      - name: Publish to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1

  release:
    needs: publish
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v5

      - name: Download artifacts
        uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          files: dist/*
          generate_release_notes: true
```

### Key Commands
- `uv build` - Create sdist and wheel in dist/
- `${GITHUB_REF_NAME#v}` - Strip 'v' prefix from tag name
- `grep "^version" pyproject.toml | cut -d'"' -f2` - Extract version from pyproject.toml

### Permission Requirements
| Job | Permission | Reason |
|-----|------------|--------|
| publish | `id-token: write` | Required for OIDC trusted publishing |
| release | `contents: write` | Required to create GitHub Release |

### Common Patterns
- Sequential jobs: `needs: [job_name]` for dependencies
- Environment protection: `environment: name: pypi` for deployment isolation
- Auto-release notes: `generate_release_notes: true` in gh-release action
- Artifact passing: upload-artifact -> download-artifact between jobs

---

## External Resources

### Documentation
- [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/) - Setup guide
- [GitHub OIDC for PyPI](https://docs.github.com/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-pypi)
- [uv in GitHub Actions](https://docs.astral.sh/uv/guides/integration/github/) - Build and publish guide
- [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) - Action documentation
- [softprops/action-gh-release](https://github.com/softprops/action-gh-release) - Release action docs

### Example Repositories
- [astral-sh/trusted-publishing-examples](https://github.com/astral-sh/trusted-publishing-examples) - Official uv example

---

*Generated by task-recon (claude-opus-4-5-20251101) | Ready for implementation*
