Skip to content

ci: use PYPI_TOKEN for publishing and fix infinite generate loop #67

ci: use PYPI_TOKEN for publishing and fix infinite generate loop

ci: use PYPI_TOKEN for publishing and fix infinite generate loop #67

Workflow file for this run

# Validate Speakeasy Generation (Dry Run) + Zero-Diff Check
#
# This workflow validates that Speakeasy generation can complete successfully
# and that the committed generated code matches what the generation pipeline produces.
#
# Jobs:
# 1. validate: Runs the full generation pipeline in dry-run mode
# 2. zero-diff: Compares the dry-run artifacts against the committed code to detect drift.
# If drift is detected, the check fails and posts a comment telling the author to run /generate.
#
# This workflow calls the main generation workflow with dry_run=true to ensure
# both workflows use the same generation logic.
#
# Note: paths-ignore is NOT used at the workflow level because GitHub treats a
# workflow that never runs as "expected" (pending), which blocks required checks.
# Instead, we filter paths at the job level so skipped jobs report as "skipped"
# (equivalent to "passed" for required checks).
name: Test (Full)
on:
pull_request:
workflow_dispatch:
permissions:
contents: write
pull-requests: write
actions: read
jobs:
check-paths:
name: Check Changed Paths
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Filter changed paths
uses: dorny/paths-filter@v4
id: filter
with:
filters: |
generation:
- '**'
- '!README.md'
- '!docs/**'
outputs:
should_run: ${{ github.event_name == 'workflow_dispatch' || steps.filter.outputs.generation == 'true' }}
validate:
name: Validate Generation (Dry Run)
needs: check-paths
if: needs.check-paths.outputs.should_run == 'true'
uses: ./.github/workflows/generate-command.yml
with:
dry_run: true
secrets: inherit
zero-diff:
name: Zero-Diff Check (Generated Code)
needs: [check-paths, validate]
if: needs.check-paths.outputs.should_run == 'true' && github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout PR branch
uses: actions/checkout@v4
- name: Download generated SDK artifact
uses: actions/download-artifact@v8
with:
name: generated_sdk_code
path: /tmp/generated/
- name: Compare generated code against committed code
id: diff-check
run: |
DIFF_SUMMARY=""
echo "=== Comparing generated SDK code ==="
# Compare src/ directory
if [ -d "src/" ] && [ -d "/tmp/generated/src/" ]; then
while IFS= read -r line; do
# Extract relative path from diff output
FILE=$(echo "$line" | sed 's|^Files ||; s| and /tmp/generated/.*||')
ADDED=$(diff -u "$FILE" "/tmp/generated/$FILE" 2>/dev/null | tail -n +3 | grep -c '^+' || echo "0")
REMOVED=$(diff -u "$FILE" "/tmp/generated/$FILE" 2>/dev/null | tail -n +3 | grep -c '^-' || echo "0")
DIFF_SUMMARY="${DIFF_SUMMARY}${FILE} (+${ADDED}/-${REMOVED})"$'\n'
done < <(diff -rq src/ /tmp/generated/src/ 2>&1 | grep "^Files" || true)
# Check for files only in one side
ONLY_LINES=$(diff -rq src/ /tmp/generated/src/ 2>&1 | grep "^Only" || true)
if [ -n "$ONLY_LINES" ]; then
while IFS= read -r line; do
DIR=$(echo "$line" | sed 's|^Only in /tmp/generated/||; s|^Only in ||; s|: |/|')
if echo "$line" | grep -q "^Only in /tmp/generated/"; then
DIFF_SUMMARY="${DIFF_SUMMARY}${DIR} (new file)"$'\n'
else
DIFF_SUMMARY="${DIFF_SUMMARY}${DIR} (deleted)"$'\n'
fi
done <<< "$ONLY_LINES"
fi
elif [ -d "/tmp/generated/src/" ]; then
DIFF_SUMMARY="src/ directory missing in committed code but present in generated output"$'\n'
fi
# Compare pyproject.toml
if [ -f "/tmp/generated/pyproject.toml" ]; then
TOML_DIFF=$(diff -q pyproject.toml /tmp/generated/pyproject.toml 2>&1 || true)
if [ -n "$TOML_DIFF" ]; then
ADDED=$(diff -u pyproject.toml /tmp/generated/pyproject.toml 2>/dev/null | tail -n +3 | grep -c '^+' || echo "0")
REMOVED=$(diff -u pyproject.toml /tmp/generated/pyproject.toml 2>/dev/null | tail -n +3 | grep -c '^-' || echo "0")
DIFF_SUMMARY="${DIFF_SUMMARY}pyproject.toml (+${ADDED}/-${REMOVED})"$'\n'
fi
fi
if [ -n "$DIFF_SUMMARY" ]; then
echo "has_diff=true" >> $GITHUB_OUTPUT
echo "::warning::Generated code drift detected. The committed code does not match what the generation pipeline produces."
echo "$DIFF_SUMMARY"
echo "$DIFF_SUMMARY" > /tmp/diff_summary.txt
else
echo "has_diff=false" >> $GITHUB_OUTPUT
echo "Zero-diff check passed. Committed code matches generation output."
fi
- name: Prepare diff summary
if: steps.diff-check.outputs.has_diff == 'true'
id: diff-summary
run: |
SUMMARY=$(cat /tmp/diff_summary.txt 2>/dev/null || echo "(see job logs for full details)")
EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
echo "content<<$EOF" >> $GITHUB_OUTPUT
echo "$SUMMARY" >> $GITHUB_OUTPUT
echo "$EOF" >> $GITHUB_OUTPUT
- name: Find existing drift comment
if: steps.diff-check.outputs.has_diff == 'true'
uses: peter-evans/find-comment@v3
id: find-drift-comment
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- zero-diff-check -->'
- name: Post drift comment on PR
if: steps.diff-check.outputs.has_diff == 'true'
uses: peter-evans/create-or-update-comment@v5
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find-drift-comment.outputs.comment-id || '' }}
edit-mode: replace
body: |
<!-- zero-diff-check -->
**Generated Code Drift Detected**
The committed code does not match what the generation pipeline produces.
**To fix:** Comment `/generate` on this PR to regenerate.
```
${{ steps.diff-summary.outputs.content }}
```
- name: Fail if drift detected
if: steps.diff-check.outputs.has_diff == 'true'
run: |
echo "::error::Generated code drift detected. Run /generate on this PR to fix."
exit 1