Skip to content

Commit 7a1cd9a

Browse files
committed
Add auto-release (canary-guarded)
1 parent 51790b5 commit 7a1cd9a

2 files changed

Lines changed: 192 additions & 7 deletions

File tree

.github/workflows/gpu-ci.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
name: Pauli GPU Tests
22

33
on:
4-
workflow_dispatch
5-
#push:
6-
# branches: [ main, ci-fix ]
7-
#pull_request:
8-
# branches: [ main, ci-fix ]
9-
#merge_group:
10-
# branches: [ main, ci-fix ]
4+
workflow_dispatch:
5+
push:
6+
branches: [ main, ci-fix ]
7+
pull_request:
8+
branches: [ main, ci-fix ]
9+
merge_group:
10+
branches: [ main, ci-fix ]
1111

1212
env:
1313
CUDACXX: /usr/local/cuda/bin/nvcc

.github/workflows/release.yml

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# .github/workflows/release.yml
2+
#
3+
# Adding a new CI workflow? ONE edit: add its display `name:` to
4+
# `on.workflow_run.workflows` below. The gate derives its required-list
5+
# from that field at runtime — no second place to update.
6+
7+
name: Release
8+
9+
on:
10+
workflow_run:
11+
workflows: ["General Tests", "Code Quality", "Heterogeneous Tests", "Machine Learning and Autodiff Tests", "Pauli GPU Tests"]
12+
types: [completed]
13+
workflow_dispatch:
14+
inputs:
15+
sha:
16+
description: "Commit SHA (blank = main HEAD)"
17+
required: false
18+
default: ""
19+
dry_run:
20+
description: "Skip side-effects"
21+
type: boolean
22+
default: true
23+
24+
concurrency:
25+
group: release-main
26+
cancel-in-progress: false
27+
28+
jobs:
29+
release:
30+
# CANARY: `false &&` gates the workflow_run branch so the first merge of this
31+
# file is a no-op. Dry-run via `workflow_dispatch` on main to validate, then
32+
# ship a follow-up PR that removes the `false &&` to enable real releases.
33+
if: >
34+
github.event_name == 'workflow_dispatch' ||
35+
(false &&
36+
github.event.workflow_run.conclusion == 'success' &&
37+
github.event.workflow_run.head_branch == 'main' &&
38+
github.event.workflow_run.event == 'push')
39+
runs-on: ubuntu-latest
40+
environment:
41+
name: pypi
42+
url: https://pypi.org/p/dace
43+
permissions:
44+
contents: write
45+
actions: read
46+
issues: write
47+
id-token: write
48+
env:
49+
SHA: ${{ github.event.inputs.sha || github.event.workflow_run.head_sha || github.sha }}
50+
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
51+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
52+
53+
steps:
54+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
55+
with:
56+
ref: ${{ env.SHA }}
57+
fetch-depth: 0
58+
59+
- name: Refuse if commit edits dace/version.py
60+
run: |
61+
if git show --name-only --pretty=format: HEAD | grep -qx 'dace/version.py'; then
62+
echo "::error::commit touches dace/version.py — refusing to overwrite"
63+
exit 1
64+
fi
65+
66+
- id: gate
67+
name: Verify all sibling CI workflows passed for this SHA
68+
run: |
69+
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
70+
echo "Manual dispatch — skipping sibling-CI gate."
71+
echo "release=true" >> "$GITHUB_OUTPUT"
72+
exit 0
73+
fi
74+
# Derive required list from our own `on.workflow_run.workflows` —
75+
# single source of truth. Adding a new CI workflow = one edit above.
76+
mapfile -t REQUIRED < <(
77+
awk '/^ workflows:/{f=1;next}/^ types:/{f=0}f' .github/workflows/release.yml \
78+
| tr -d '[]"' | tr ',' '\n' | sed 's/^ *//;s/ *$//' | grep -v '^$'
79+
)
80+
echo "Gating on: ${REQUIRED[*]}"
81+
for wf in "${REQUIRED[@]}"; do
82+
C=$(gh run list --repo "$GITHUB_REPOSITORY" --commit "$SHA" --workflow "$wf" \
83+
--limit 1 --json conclusion --jq '.[0].conclusion // "pending"')
84+
echo "$wf -> $C"
85+
if [[ "$C" != "success" ]]; then
86+
echo "release=false" >> "$GITHUB_OUTPUT"
87+
exit 0
88+
fi
89+
done
90+
echo "release=true" >> "$GITHUB_OUTPUT"
91+
92+
- if: steps.gate.outputs.release == 'true'
93+
id: ver
94+
name: Compute next patch version
95+
run: |
96+
L=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1)
97+
L=${L:-v0.0.0}
98+
V=${L#v}
99+
IFS='.' read -r A B C <<< "$V"
100+
N="${A}.${B}.$((C+1))"
101+
echo "next=$N" >> "$GITHUB_OUTPUT"
102+
echo "tag=v$N" >> "$GITHUB_OUTPUT"
103+
echo "-> $L -> v$N"
104+
105+
- if: steps.gate.outputs.release == 'true'
106+
id: idem
107+
name: Existing-state probes (tag / release / PyPI)
108+
run: |
109+
if git ls-remote --tags origin "refs/tags/${{ steps.ver.outputs.tag }}" | grep -q .; then
110+
echo "tag=true" >> "$GITHUB_OUTPUT"
111+
else
112+
echo "tag=false" >> "$GITHUB_OUTPUT"
113+
fi
114+
if gh release view "${{ steps.ver.outputs.tag }}" >/dev/null 2>&1; then
115+
echo "release=true" >> "$GITHUB_OUTPUT"
116+
else
117+
echo "release=false" >> "$GITHUB_OUTPUT"
118+
fi
119+
python -m pip install --quiet --upgrade pip
120+
if python -m pip index versions dace 2>/dev/null | grep -qw "${{ steps.ver.outputs.next }}"; then
121+
echo "pypi=true" >> "$GITHUB_OUTPUT"
122+
else
123+
echo "pypi=false" >> "$GITHUB_OUTPUT"
124+
fi
125+
126+
- if: steps.gate.outputs.release == 'true' && steps.idem.outputs.pypi == 'false'
127+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
128+
with:
129+
python-version: '3.10'
130+
131+
- if: steps.gate.outputs.release == 'true' && steps.idem.outputs.pypi == 'false'
132+
name: Build sdist + wheel and offline metadata check
133+
run: |
134+
echo "__version__ = '${{ steps.ver.outputs.next }}'" > dace/version.py
135+
python -m pip install --upgrade build twine
136+
python -m build
137+
python -m twine check dist/*
138+
139+
- if: steps.gate.outputs.release == 'true' && steps.idem.outputs.pypi == 'false' && env.DRY_RUN != 'true'
140+
name: Publish to PyPI via OIDC Trusted Publishing
141+
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
142+
143+
- if: env.DRY_RUN == 'true'
144+
name: Dry-run notice
145+
run: echo "DRY-RUN: would publish dist/* via OIDC, push commit+tag, create GitHub Release"
146+
147+
- if: steps.gate.outputs.release == 'true' && steps.idem.outputs.tag == 'false' && env.DRY_RUN != 'true'
148+
name: Push bump commit + tag
149+
run: |
150+
retry(){ local n=0 d=5; until "$@"; do n=$((n+1)); ((n>=3)) && return 1; sleep $d; d=$((d*2)); done; }
151+
git config user.name "github-actions[bot]"
152+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
153+
git add dace/version.py
154+
git commit -m "chore: release ${{ steps.ver.outputs.tag }}"
155+
git tag -a "${{ steps.ver.outputs.tag }}" -m "Release ${{ steps.ver.outputs.tag }}"
156+
retry git push origin HEAD:main
157+
retry git push origin "${{ steps.ver.outputs.tag }}"
158+
159+
- if: steps.gate.outputs.release == 'true' && steps.idem.outputs.release == 'false' && env.DRY_RUN != 'true'
160+
name: Create GitHub Release
161+
run: |
162+
retry(){ local n=0 d=5; until "$@"; do n=$((n+1)); ((n>=3)) && return 1; sleep $d; d=$((d*2)); done; }
163+
retry gh release create "${{ steps.ver.outputs.tag }}" --generate-notes dist/*
164+
165+
- if: always() && steps.gate.outputs.release == 'true'
166+
name: Run summary
167+
run: |
168+
{
169+
echo "## Release"
170+
echo ""
171+
echo "| Field | Value |"
172+
echo "|---|---|"
173+
echo "| Tag | \`${{ steps.ver.outputs.tag }}\` |"
174+
echo "| Commit | \`$SHA\` |"
175+
echo "| Dry run | $DRY_RUN |"
176+
echo "| PyPI | https://pypi.org/project/dace/${{ steps.ver.outputs.next }}/ |"
177+
echo "| GitHub Release | https://github.com/$GITHUB_REPOSITORY/releases/tag/${{ steps.ver.outputs.tag }} |"
178+
} >> "$GITHUB_STEP_SUMMARY"
179+
180+
- if: failure() && github.event_name != 'workflow_dispatch'
181+
name: Open tracking issue on failure
182+
run: |
183+
gh issue create --label "release,ci-failure" \
184+
--title "Release workflow failed on $SHA" \
185+
--body "Logs: https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"

0 commit comments

Comments
 (0)