Public Access
DEVX-65: refactor: consolidate publish.yml into post-merge.yml
Post-merge / detect-type (push) Successful in 30s
Post-merge / vikunja (push) Successful in 46s
Post-merge / sync-wiki (push) Successful in 48s
Post-merge / validate-commit-msg (push) Successful in 51s
Post-merge / badges (push) Successful in 59s
Post-merge / release (push) Successful in 1m30s
Post-merge / configure-repo (push) Successful in 1m34s
Post-merge / publish (push) Successful in 54s
Post-merge / detect-type (push) Successful in 30s
Post-merge / vikunja (push) Successful in 46s
Post-merge / sync-wiki (push) Successful in 48s
Post-merge / validate-commit-msg (push) Successful in 51s
Post-merge / badges (push) Successful in 59s
Post-merge / release (push) Successful in 1m30s
Post-merge / configure-repo (push) Successful in 1m34s
Post-merge / publish (push) Successful in 54s
This commit was merged in pull request #105.
This commit is contained in:
@@ -1,13 +1,13 @@
|
|||||||
name: Post-merge
|
name: Post-merge
|
||||||
|
|
||||||
# Runs on every push to master. A single workflow with conditional jobs
|
# Runs on every push to master. A single workflow with conditional jobs
|
||||||
# replaces separate workflows for release, wiki sync, badges, and
|
# for release, publish, wiki sync, badges, and Vikunja task updates.
|
||||||
# Vikunja task updates.
|
|
||||||
#
|
#
|
||||||
# Job dependency graph:
|
# Job dependency graph:
|
||||||
#
|
#
|
||||||
# detect-type ──┬── validate-commit-msg (skip if release commit)
|
# detect-type ──┬── validate-commit-msg (skip if release commit)
|
||||||
# ├── release (skip if release commit)
|
# ├── release (skip if release commit)
|
||||||
|
# │ └── publish (needs release — builds & publishes to PyPI)
|
||||||
# ├── badges (ALWAYS runs — even on release commits)
|
# ├── badges (ALWAYS runs — even on release commits)
|
||||||
# ├── configure-repo (independent — skip if release commit)
|
# ├── configure-repo (independent — skip if release commit)
|
||||||
# ├── sync-wiki (skip if release commit — runs for ALL merges)
|
# ├── sync-wiki (skip if release commit — runs for ALL merges)
|
||||||
@@ -21,9 +21,10 @@ name: Post-merge
|
|||||||
# runs on every push to master, including release commits. This ensures
|
# runs on every push to master, including release commits. This ensures
|
||||||
# badges (tests, coverage, version, etc.) are always current.
|
# badges (tests, coverage, version, etc.) are always current.
|
||||||
#
|
#
|
||||||
# When release creates a "release: vX.Y.Z" commit, the release
|
# When release creates a "release: vX.Y.Z" commit and tag, the publish
|
||||||
# commit's post-merge run still updates badges (version badge picks
|
# job (which depends on release) builds and publishes the package to the
|
||||||
# up the new version). Other jobs skip. The tag push triggers publish.yml.
|
# Gitea PyPI registry. The release commit's post-merge run still updates
|
||||||
|
# badges (version badge picks up the new version). Other jobs skip.
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -74,6 +75,8 @@ jobs:
|
|||||||
if: needs.detect-type.outputs.is-release == 'false'
|
if: needs.detect-type.outputs.is-release == 'false'
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
outputs:
|
||||||
|
tag: ${{ steps.release-tag.outputs.tag }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@@ -88,12 +91,20 @@ jobs:
|
|||||||
git config user.name "devx-ci-bot"
|
git config user.name "devx-ci-bot"
|
||||||
git config user.email "devx-ci-bot@oblachno.fyi"
|
git config user.email "devx-ci-bot@oblachno.fyi"
|
||||||
- name: Run release
|
- name: Run release
|
||||||
|
id: release-tag
|
||||||
env:
|
env:
|
||||||
PYTHONPATH: src
|
PYTHONPATH: src
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate
|
. .venv/bin/activate
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
python3 -m devx.ci.release
|
python3 -m devx.ci.release
|
||||||
|
- name: Extract tag (fallback if GITHUB_OUTPUT not set)
|
||||||
|
if: steps.release-tag.outputs.tag == ''
|
||||||
|
run: |
|
||||||
|
tag=$(git describe --tags --abbrev=0 2>/dev/null || true)
|
||||||
|
if [ -n "$tag" ]; then
|
||||||
|
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
- name: Notify on failure
|
- name: Notify on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
env:
|
env:
|
||||||
@@ -108,6 +119,41 @@ jobs:
|
|||||||
--workflow "post-merge/release" \
|
--workflow "post-merge/release" \
|
||||||
--commit "${{ github.sha }}"
|
--commit "${{ github.sha }}"
|
||||||
|
|
||||||
|
publish:
|
||||||
|
needs: [release]
|
||||||
|
if: needs.release.outputs.tag != ''
|
||||||
|
runs-on: docker
|
||||||
|
timeout-minutes: 10
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Set up environment
|
||||||
|
env:
|
||||||
|
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
||||||
|
run: make setup-release
|
||||||
|
- name: Build and publish release
|
||||||
|
env:
|
||||||
|
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
||||||
|
PYTHONPATH: src
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
python3 -m devx.ci.publish "${{ needs.release.outputs.tag }}" "${{ github.repository }}"
|
||||||
|
- name: Notify on failure
|
||||||
|
if: failure()
|
||||||
|
env:
|
||||||
|
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
||||||
|
PYTHONPATH: src
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
python3 -m devx.ci.notify_failure \
|
||||||
|
--repo "${{ github.repository }}" \
|
||||||
|
--run-id "${{ github.run_id }}" \
|
||||||
|
--workflow "post-merge/publish" \
|
||||||
|
--commit "${{ github.sha }}"
|
||||||
|
|
||||||
sync-wiki:
|
sync-wiki:
|
||||||
needs: [detect-type]
|
needs: [detect-type]
|
||||||
if: needs.detect-type.outputs.is-release == 'false'
|
if: needs.detect-type.outputs.is-release == 'false'
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
name: Publish Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
tag:
|
|
||||||
description: 'Tag to publish (e.g. v0.9.11)'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish:
|
|
||||||
runs-on: docker
|
|
||||||
timeout-minutes: 10
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Set up environment
|
|
||||||
env:
|
|
||||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
|
||||||
run: make setup-release
|
|
||||||
- name: Build and publish release
|
|
||||||
env:
|
|
||||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
|
||||||
. .venv/bin/activate
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
python3 -m devx.ci.publish "${{ github.event.inputs.tag || github.ref_name }}" "${{ github.repository }}"
|
|
||||||
- name: Notify on failure
|
|
||||||
if: failure()
|
|
||||||
env:
|
|
||||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
python3 -m devx.ci.notify_failure \
|
|
||||||
--repo "${{ github.repository }}" \
|
|
||||||
--run-id "${{ github.run_id }}" \
|
|
||||||
--workflow "publish" \
|
|
||||||
--commit "${{ github.sha }}"
|
|
||||||
@@ -204,8 +204,9 @@ After a PR is merged to master, the **post-merge workflow**
|
|||||||
non-release commits (not just when release succeeds), so infrastructure-only
|
non-release commits (not just when release succeeds), so infrastructure-only
|
||||||
changes still update the task tracker.
|
changes still update the task tracker.
|
||||||
|
|
||||||
The tag push triggers the **publish workflow** (`.gitea/workflows/publish.yml`)
|
6. **publish** — Runs after release succeeds (needs: release). Builds and
|
||||||
which builds and publishes the package to the Gitea PyPI registry.
|
publishes the package to the Gitea PyPI registry. Gets the tag from the
|
||||||
|
release job's `tag` output (written via `GITHUB_OUTPUT`).
|
||||||
|
|
||||||
### Smart CI: User-Facing vs Workflow-Only Changes
|
### Smart CI: User-Facing vs Workflow-Only Changes
|
||||||
|
|
||||||
|
|||||||
@@ -317,6 +317,21 @@ def run_tests() -> None:
|
|||||||
click.echo(_("Tests passed."))
|
click.echo(_("Tests passed."))
|
||||||
|
|
||||||
|
|
||||||
|
def _write_github_output(tag: str) -> None:
|
||||||
|
"""Write the release tag to GITHUB_OUTPUT for downstream jobs.
|
||||||
|
|
||||||
|
This allows a publish job (needs: release) to read the tag via
|
||||||
|
``${{ needs.release.outputs.tag }}`` instead of relying on
|
||||||
|
tag-push event triggering a separate workflow.
|
||||||
|
"""
|
||||||
|
github_output = os.environ.get("GITHUB_OUTPUT")
|
||||||
|
if not github_output:
|
||||||
|
return
|
||||||
|
with open(github_output, "a") as f: # noqa: PTH123
|
||||||
|
f.write(f"tag={tag}\n")
|
||||||
|
click.echo(_("Wrote tag {tag} to GITHUB_OUTPUT.", tag=tag))
|
||||||
|
|
||||||
|
|
||||||
def create_and_push_tag(new_version: str, changelog: str, dry_run: bool) -> bool:
|
def create_and_push_tag(new_version: str, changelog: str, dry_run: bool) -> bool:
|
||||||
"""Create an annotated tag with the changelog as message and push it.
|
"""Create an annotated tag with the changelog as message and push it.
|
||||||
|
|
||||||
@@ -345,6 +360,7 @@ def create_and_push_tag(new_version: str, changelog: str, dry_run: bool) -> bool
|
|||||||
if not dry_run:
|
if not dry_run:
|
||||||
# Ensure the existing tag is pushed
|
# Ensure the existing tag is pushed
|
||||||
run_cmd(["git", "push", "origin", f"refs/tags/{tag}"], check=False)
|
run_cmd(["git", "push", "origin", f"refs/tags/{tag}"], check=False)
|
||||||
|
_write_github_output(tag)
|
||||||
return False
|
return False
|
||||||
tag_msg = f"Release v{new_version}\n\n{changelog}"
|
tag_msg = f"Release v{new_version}\n\n{changelog}"
|
||||||
if dry_run:
|
if dry_run:
|
||||||
@@ -352,6 +368,7 @@ def create_and_push_tag(new_version: str, changelog: str, dry_run: bool) -> bool
|
|||||||
return True
|
return True
|
||||||
run_cmd(["git", "tag", "-a", tag, "-m", tag_msg])
|
run_cmd(["git", "tag", "-a", tag, "-m", tag_msg])
|
||||||
run_cmd(["git", "push", "origin", f"refs/tags/{tag}"])
|
run_cmd(["git", "push", "origin", f"refs/tags/{tag}"])
|
||||||
|
_write_github_output(tag)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -617,6 +634,7 @@ def main(dry_run: bool, skip_tests: bool, verify: bool) -> None:
|
|||||||
tag=release_tag,
|
tag=release_tag,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
_write_github_output(release_tag)
|
||||||
return
|
return
|
||||||
# Tag is missing — recover by creating and pushing it
|
# Tag is missing — recover by creating and pushing it
|
||||||
click.echo(
|
click.echo(
|
||||||
|
|||||||
@@ -1846,5 +1846,13 @@
|
|||||||
"pl": "[check-dep-docs] Passed: all dependencies are documented",
|
"pl": "[check-dep-docs] Passed: all dependencies are documented",
|
||||||
"ru": "[check-dep-docs] Passed: all dependencies are documented",
|
"ru": "[check-dep-docs] Passed: all dependencies are documented",
|
||||||
"zh": "[check-dep-docs] Passed: all dependencies are documented"
|
"zh": "[check-dep-docs] Passed: all dependencies are documented"
|
||||||
|
},
|
||||||
|
"Wrote tag {tag} to GITHUB_OUTPUT.": {
|
||||||
|
"bg": "Wrote tag {tag} to GITHUB_OUTPUT.",
|
||||||
|
"de": "Wrote tag {tag} to GITHUB_OUTPUT.",
|
||||||
|
"en": "Wrote tag {tag} to GITHUB_OUTPUT.",
|
||||||
|
"ru": "Wrote tag {tag} to GITHUB_OUTPUT.",
|
||||||
|
"zh": "Wrote tag {tag} to GITHUB_OUTPUT.",
|
||||||
|
"pl": "Wrote tag {tag} to GITHUB_OUTPUT."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"""Unit tests for scripts/ci/release.py."""
|
"""Unit tests for scripts/ci/release.py."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import click
|
import click
|
||||||
@@ -815,11 +817,23 @@ class TestCommitReleaseChanges:
|
|||||||
class TestCreateAndPushTag:
|
class TestCreateAndPushTag:
|
||||||
@patch("devx.ci.release.tag_exists", return_value=False)
|
@patch("devx.ci.release.tag_exists", return_value=False)
|
||||||
@patch("devx.ci.release.run_cmd")
|
@patch("devx.ci.release.run_cmd")
|
||||||
def test_creates_tag(self, mock_run_cmd: MagicMock, mock_tag_exists: MagicMock) -> None:
|
def test_creates_tag(self, mock_run_cmd: MagicMock, mock_tag_exists: MagicMock, tmp_path: Path) -> None:
|
||||||
create_and_push_tag("0.2.0", "changelog", dry_run=False)
|
github_output = tmp_path / "output.txt"
|
||||||
|
with patch.dict(os.environ, {"GITHUB_OUTPUT": str(github_output)}):
|
||||||
|
create_and_push_tag("0.2.0", "changelog", dry_run=False)
|
||||||
calls = [c.args[0] for c in mock_run_cmd.call_args_list]
|
calls = [c.args[0] for c in mock_run_cmd.call_args_list]
|
||||||
assert ["git", "tag", "-a", "v0.2.0", "-m", "Release v0.2.0\n\nchangelog"] in calls
|
assert ["git", "tag", "-a", "v0.2.0", "-m", "Release v0.2.0\n\nchangelog"] in calls
|
||||||
assert ["git", "push", "origin", "refs/tags/v0.2.0"] in calls
|
assert ["git", "push", "origin", "refs/tags/v0.2.0"] in calls
|
||||||
|
assert github_output.read_text() == "tag=v0.2.0\n"
|
||||||
|
|
||||||
|
@patch("devx.ci.release.tag_exists", return_value=False)
|
||||||
|
@patch("devx.ci.release.run_cmd")
|
||||||
|
def test_no_github_output_skips_write(self, mock_run_cmd: MagicMock, mock_tag_exists: MagicMock) -> None:
|
||||||
|
with patch.dict(os.environ, {}, clear=True):
|
||||||
|
create_and_push_tag("0.2.0", "changelog", dry_run=False)
|
||||||
|
# Should still create tag, just not write GITHUB_OUTPUT
|
||||||
|
calls = [c.args[0] for c in mock_run_cmd.call_args_list]
|
||||||
|
assert ["git", "tag", "-a", "v0.2.0", "-m", "Release v0.2.0\n\nchangelog"] in calls
|
||||||
|
|
||||||
@patch("devx.ci.release.tag_exists", return_value=False)
|
@patch("devx.ci.release.tag_exists", return_value=False)
|
||||||
@patch("devx.ci.release.run_cmd")
|
@patch("devx.ci.release.run_cmd")
|
||||||
|
|||||||
Reference in New Issue
Block a user