Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 38 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,43 +21,77 @@ jobs:
contents: write
id-token: write
steps:
- name: Checkout release branch
- name: Checkout workflow SHA
uses: actions/checkout@v4
with:
ref: ${{ github.ref_name }}
ref: ${{ github.sha }}
fetch-depth: 0

- name: Pin checkout to workflow SHA
run: git reset --hard ${{ github.sha }}
- name: Restore release branch at workflow SHA
run: git checkout -B "$GITHUB_REF_NAME" "$GITHUB_SHA"

- name: Check workflow SHA is still current
id: release_head
run: |
set -o pipefail
remote_sha="$(git ls-remote --exit-code origin "refs/heads/${GITHUB_REF_NAME}" | cut -f1)"
if [[ "$remote_sha" == "$GITHUB_SHA" ]]; then
echo "current=true" >> "$GITHUB_OUTPUT"
else
echo "current=false" >> "$GITHUB_OUTPUT"
echo "::notice::Skipping stale release run for ${GITHUB_SHA}; ${GITHUB_REF_NAME} is now at ${remote_sha}."
fi

- name: Set up Python
if: steps.release_head.outputs.current == 'true'
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: pip
- name: Set up Node
if: steps.release_head.outputs.current == 'true'
uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
cache-dependency-path: dashboard/package-lock.json

- name: Install package with test dependencies
if: steps.release_head.outputs.current == 'true'
run: python -m pip install -e '.[test]'
- name: Install dashboard dependencies
if: steps.release_head.outputs.current == 'true'
run: npm ci
working-directory: dashboard
- name: Run dashboard tests
if: steps.release_head.outputs.current == 'true'
run: npm test
working-directory: dashboard
- name: Build dashboard
if: steps.release_head.outputs.current == 'true'
run: npm run build
working-directory: dashboard

- name: Run tests before release
if: steps.release_head.outputs.current == 'true'
run: pytest -q

- name: Recheck workflow SHA before release
id: pre_release_head
if: steps.release_head.outputs.current == 'true'
run: |
set -o pipefail
remote_sha="$(git ls-remote --exit-code origin "refs/heads/${GITHUB_REF_NAME}" | cut -f1)"
if [[ "$remote_sha" == "$GITHUB_SHA" ]]; then
echo "current=true" >> "$GITHUB_OUTPUT"
else
echo "current=false" >> "$GITHUB_OUTPUT"
echo "::notice::Skipping release because ${GITHUB_REF_NAME} advanced to ${remote_sha} during validation."
fi

- name: Semantic version release
id: release
if: steps.pre_release_head.outputs.current == 'true'
uses: python-semantic-release/python-semantic-release@v10.5.3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
Expand Down
4 changes: 4 additions & 0 deletions docs/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ When commits land on `main`, the `release` workflow:
8. creates a GitHub Release;
9. builds `sdist`/wheel artifacts and uploads them to the GitHub Release.

Release runs are serialized. If another commit reaches `main` before an older
run starts publishing, the stale run exits successfully and leaves the release
to the run for the current branch head.

No PyPI publish is configured yet.

## Commit convention
Expand Down
53 changes: 53 additions & 0 deletions tests/test_release_workflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from pathlib import Path


WORKFLOW = Path(__file__).parents[1] / ".github" / "workflows" / "release.yml"


def release_steps():
sections = WORKFLOW.read_text().split("\n - name: ")[1:]
return {
name: body
for section in sections
for name, _, body in [section.partition("\n")]
}


def test_release_checkout_can_pin_an_older_workflow_sha():
steps = release_steps()

checkout = steps["Checkout workflow SHA"]
assert "ref: ${{ github.sha }}" in checkout
assert "fetch-depth: 0" in checkout
assert 'git checkout -B "$GITHUB_REF_NAME" "$GITHUB_SHA"' in steps[
"Restore release branch at workflow SHA"
]


def test_stale_release_runs_skip_validation_and_publish_steps():
steps = release_steps()
initial_guard = steps["Check workflow SHA is still current"]

assert "id: release_head" in initial_guard
assert 'refs/heads/${GITHUB_REF_NAME}' in initial_guard
assert 'echo "current=false" >> "$GITHUB_OUTPUT"' in initial_guard

guarded_steps = [
"Set up Python",
"Set up Node",
"Install package with test dependencies",
"Install dashboard dependencies",
"Run dashboard tests",
"Build dashboard",
"Run tests before release",
"Recheck workflow SHA before release",
]
for name in guarded_steps:
assert "if: steps.release_head.outputs.current == 'true'" in steps[name]

final_guard = steps["Recheck workflow SHA before release"]
assert "id: pre_release_head" in final_guard
assert 'refs/heads/${GITHUB_REF_NAME}' in final_guard
assert "if: steps.pre_release_head.outputs.current == 'true'" in steps[
"Semantic version release"
]
Loading