Repository navigation
Prepare WinUI plugin for WinApp CLI 0.7 #211
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release policy | |
| # Mechanical gates (Tier 1) for the PR / staging / main release flow. | |
| # See CONTRIBUTING.md and RELEASING.md for the human-side flow. | |
| # | |
| # Job matrix: | |
| # pr-target-policy PR → main | head_ref must be staging, release/*, or hotfix/* | |
| # version-sync PR → staging | NO version field changed (skipped for backmerge/*) | |
| # version-bump PR → main | all 5 version fields bumped, in sync, valid semver, strictly greater | |
| # changelog-entry PR → main | top-most CHANGELOG section matches new version, has bullets | |
| # staging-up-to-date PR → staging | PR head contains every commit on main | |
| # (passes naturally for backmerge/* PRs) | |
| on: | |
| pull_request: | |
| branches: [main, staging] | |
| types: [opened, synchronize, reopened, edited] | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| jobs: | |
| pr-target-policy: | |
| name: PR target policy | |
| if: github.event.pull_request.base.ref == 'main' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Verify head branch is staging, release/*, or hotfix/* | |
| env: | |
| HEAD_REF: ${{ github.event.pull_request.head.ref }} | |
| HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} | |
| BASE_REPO: ${{ github.repository }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| # Lock down: only branches in the same repo (no fork PRs) may target main. | |
| # Reason: prevents a fork from naming its branch "staging" to bypass. | |
| if [[ "$HEAD_REPO" != "$BASE_REPO" ]]; then | |
| echo "::error::PRs targeting 'main' must originate from a branch in this repo, not a fork ($HEAD_REPO)." | |
| exit 1 | |
| fi | |
| if [[ "$HEAD_REF" == "staging" || "$HEAD_REF" == release/* || "$HEAD_REF" == hotfix/* ]]; then | |
| echo "::notice::Head branch '$HEAD_REF' is allowed to target main." | |
| exit 0 | |
| fi | |
| echo "::error::PRs targeting 'main' must come from 'staging', 'release/*', or 'hotfix/*'." | |
| echo "::error::This PR's head branch is '$HEAD_REF'. Please re-target the PR at 'staging'." | |
| echo "::error::See CONTRIBUTING.md for the branch model." | |
| exit 1 | |
| version-sync: | |
| name: No version bump in feature PR | |
| # Skip on backmerge/* PRs: those legitimately carry main's version-bump | |
| # commit forward into staging. See CONTRIBUTING.md § Back-merge path. | |
| if: > | |
| github.event.pull_request.base.ref == 'staging' && | |
| !startsWith(github.event.pull_request.head.ref, 'backmerge/') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout (full depth) | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ github.event.pull_request.head.sha }} | |
| - name: Diff version fields against staging | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| # Compare parsed version values rather than changed lines. This keeps | |
| # package-layout migrations from looking like version bumps when a | |
| # manifest moves without changing its version. | |
| read_base_plugin_version() { | |
| if git cat-file -e "$BASE_SHA:plugins/winui/agent-plugin/plugin.json" 2>/dev/null; then | |
| git show "$BASE_SHA:plugins/winui/agent-plugin/plugin.json" | jq -r '.version' | |
| else | |
| # One-time migration fallback for PRs whose base still uses the | |
| # pre-Agent-Plugins package root. | |
| git show "$BASE_SHA:plugins/winui/plugin.json" | jq -r '.version' | |
| fi | |
| } | |
| HEAD_VERSIONS=( | |
| "$(jq -r '.version' plugins/winui/agent-plugin/plugin.json)" | |
| "$(jq -r '.metadata.version' .github/plugin/marketplace.json)" | |
| "$(jq -r '.plugins[0].version' .github/plugin/marketplace.json)" | |
| "$(jq -r '.version' .claude-plugin/marketplace.json)" | |
| "$(jq -r '.plugins[0].version' .claude-plugin/marketplace.json)" | |
| "$(jq -r '.version' plugins/winui/.claude-plugin/plugin.json)" | |
| "$(jq -r '.version' plugins/winui/.codex-plugin/plugin.json)" | |
| ) | |
| BASE_VERSIONS=( | |
| "$(read_base_plugin_version)" | |
| "$(git show "$BASE_SHA":.github/plugin/marketplace.json | jq -r '.metadata.version')" | |
| "$(git show "$BASE_SHA":.github/plugin/marketplace.json | jq -r '.plugins[0].version')" | |
| "$(git show "$BASE_SHA":.claude-plugin/marketplace.json | jq -r '.version')" | |
| "$(git show "$BASE_SHA":.claude-plugin/marketplace.json | jq -r '.plugins[0].version')" | |
| "$(git show "$BASE_SHA":plugins/winui/.claude-plugin/plugin.json | jq -r '.version')" | |
| "$(git show "$BASE_SHA":plugins/winui/.codex-plugin/plugin.json | jq -r '.version')" | |
| ) | |
| LABELS=( | |
| "portable plugin" | |
| "GitHub marketplace metadata" | |
| "GitHub marketplace plugin" | |
| "Claude marketplace" | |
| "Claude marketplace plugin" | |
| "Claude adapter plugin" | |
| "Codex adapter plugin" | |
| ) | |
| FAILED=0 | |
| for i in "${!HEAD_VERSIONS[@]}"; do | |
| if [[ "${HEAD_VERSIONS[$i]}" != "${BASE_VERSIONS[$i]}" ]]; then | |
| echo "::error::${LABELS[$i]} version changed from '${BASE_VERSIONS[$i]}' to '${HEAD_VERSIONS[$i]}'. Versioning is release-only — only the staging → main promotion PR may bump versions." | |
| FAILED=1 | |
| fi | |
| done | |
| if [[ "$FAILED" -ne 0 ]]; then | |
| echo "::error::See CONTRIBUTING.md § 'Things you should NOT do in a feature PR'." | |
| exit 1 | |
| fi | |
| echo "::notice::No version fields modified — OK." | |
| staging-up-to-date: | |
| name: staging is caught up with main | |
| if: github.event.pull_request.base.ref == 'staging' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout (full depth) | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| - name: Verify PR head contains every commit on main | |
| env: | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| HEAD_REF: ${{ github.event.pull_request.head.ref }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| git fetch origin main --quiet | |
| # We check the PR head (not origin/staging) so back-merge PRs — which | |
| # bring main into staging — pass naturally: after they merge, staging | |
| # will contain main. Any other PR that doesn't already include main | |
| # will fail here exactly as it would have under the old "staging vs | |
| # main" check. | |
| MISSING=$(git rev-list --count origin/main ^"$HEAD_SHA") | |
| if [[ "$MISSING" -gt 0 ]]; then | |
| echo "::error::PR head ('$HEAD_REF') is missing $MISSING commit(s) from 'main'." | |
| echo "::error::This usually means a hotfix landed on main and was not back-merged into staging," | |
| echo "::error::and this PR was branched from a stale staging." | |
| echo "::error::Fix: open a back-merge PR first." | |
| echo "::error:: git checkout -b backmerge/<topic> origin/staging" | |
| echo "::error:: git merge origin/main" | |
| echo "::error:: git push -u origin backmerge/<topic>" | |
| echo "::error:: gh pr create --base staging --head backmerge/<topic>" | |
| echo "::error::Then rebase this PR on the updated staging. See CONTRIBUTING.md § Back-merge path." | |
| exit 1 | |
| fi | |
| echo "::notice::PR head contains every commit on 'main' — OK." | |
| version-bump: | |
| name: Version bump (5 fields in sync) | |
| if: github.event.pull_request.base.ref == 'main' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout (full depth) | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ github.event.pull_request.head.sha }} | |
| - name: Read PR-side and main-side versions and validate | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| # Extract every version field from PR head and from main. | |
| # PR-head versions: | |
| PR_PLUGIN=$(jq -r '.version' plugins/winui/agent-plugin/plugin.json) | |
| PR_GH_META=$(jq -r '.metadata.version' .github/plugin/marketplace.json) | |
| PR_GH_PLUGIN=$(jq -r '.plugins[0].version' .github/plugin/marketplace.json) | |
| PR_CLAUDE_TOP=$(jq -r '.version' .claude-plugin/marketplace.json) | |
| PR_CLAUDE_PLUGIN=$(jq -r '.plugins[0].version' .claude-plugin/marketplace.json) | |
| PR_CLAUDE_ADAPTER=$(jq -r '.version' plugins/winui/.claude-plugin/plugin.json) | |
| PR_CODEX_ADAPTER=$(jq -r '.version' plugins/winui/.codex-plugin/plugin.json) | |
| # main-side versions: read all 7 and require they're already in sync | |
| # (defends against a previously-bad release leaving main inconsistent). | |
| if git cat-file -e "$BASE_SHA:plugins/winui/agent-plugin/plugin.json" 2>/dev/null; then | |
| MAIN_PLUGIN=$(git show "$BASE_SHA":plugins/winui/agent-plugin/plugin.json | jq -r '.version') | |
| else | |
| # The first promotion after the package-layout migration still has | |
| # the portable manifest at its former path on main. | |
| MAIN_PLUGIN=$(git show "$BASE_SHA":plugins/winui/plugin.json | jq -r '.version') | |
| fi | |
| MAIN_GH_META=$(git show "$BASE_SHA":.github/plugin/marketplace.json | jq -r '.metadata.version') | |
| MAIN_GH_PLUGIN=$(git show "$BASE_SHA":.github/plugin/marketplace.json | jq -r '.plugins[0].version') | |
| MAIN_CLAUDE_TOP=$(git show "$BASE_SHA":.claude-plugin/marketplace.json | jq -r '.version') | |
| MAIN_CLAUDE_PLUGIN=$(git show "$BASE_SHA":.claude-plugin/marketplace.json | jq -r '.plugins[0].version') | |
| MAIN_CLAUDE_ADAPTER=$(git show "$BASE_SHA":plugins/winui/.claude-plugin/plugin.json | jq -r '.version') | |
| MAIN_CODEX_ADAPTER=$(git show "$BASE_SHA":plugins/winui/.codex-plugin/plugin.json | jq -r '.version') | |
| MAIN_ALL=("$MAIN_PLUGIN" "$MAIN_GH_META" "$MAIN_GH_PLUGIN" "$MAIN_CLAUDE_TOP" "$MAIN_CLAUDE_PLUGIN" "$MAIN_CLAUDE_ADAPTER" "$MAIN_CODEX_ADAPTER") | |
| # Advisory only, intentionally non-fatal: Rule 1 below already forces | |
| # every PR-head field into sync, so a corrective hotfix PR (which by | |
| # definition targets an already-inconsistent main) must still be able | |
| # to pass this job. Failing here would deadlock exactly the PR meant | |
| # to fix the drift it detects. | |
| for v in "${MAIN_ALL[@]}"; do | |
| if [[ "$v" != "$MAIN_PLUGIN" ]]; then | |
| echo "::warning::main-side version fields are out of sync: ${MAIN_ALL[*]}" | |
| echo "::warning::main was left in an inconsistent state by a previous release. This PR's Rule 1 check (below) still requires all 7 PR-head fields to match, so merging this PR will fix it." | |
| break | |
| fi | |
| done | |
| echo "PR head: plugin=$PR_PLUGIN gh.meta=$PR_GH_META gh.plugin=$PR_GH_PLUGIN claude.top=$PR_CLAUDE_TOP claude.plugin=$PR_CLAUDE_PLUGIN claude.adapter=$PR_CLAUDE_ADAPTER codex.adapter=$PR_CODEX_ADAPTER" | |
| echo "main: plugin=$MAIN_PLUGIN gh.meta=$MAIN_GH_META gh.plugin=$MAIN_GH_PLUGIN claude.top=$MAIN_CLAUDE_TOP claude.plugin=$MAIN_CLAUDE_PLUGIN claude.adapter=$MAIN_CLAUDE_ADAPTER codex.adapter=$MAIN_CODEX_ADAPTER" | |
| # Rule 1: all 7 PR-head versions identical. | |
| ALL=("$PR_PLUGIN" "$PR_GH_META" "$PR_GH_PLUGIN" "$PR_CLAUDE_TOP" "$PR_CLAUDE_PLUGIN" "$PR_CLAUDE_ADAPTER" "$PR_CODEX_ADAPTER") | |
| for v in "${ALL[@]}"; do | |
| if [[ "$v" != "$PR_PLUGIN" ]]; then | |
| echo "::error::Version fields are out of sync. All 7 fields must match. Got: ${ALL[*]}" | |
| echo "::error::Files to fix: plugins/winui/agent-plugin/plugin.json, .github/plugin/marketplace.json, .claude-plugin/marketplace.json, plugins/winui/.claude-plugin/plugin.json, plugins/winui/.codex-plugin/plugin.json" | |
| exit 1 | |
| fi | |
| done | |
| # Rule 2: valid semver (X.Y.Z). Prereleases are NOT supported in this | |
| # release flow — they require real SemVer precedence comparison and | |
| # we don't need them yet. If you need one, extend this check first. | |
| if [[ ! "$PR_PLUGIN" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "::error::Version '$PR_PLUGIN' is not valid semver (X.Y.Z). Prereleases are not supported by this flow." | |
| exit 1 | |
| fi | |
| # Rule 3: strictly greater than main. | |
| # Compare via sort -V; if the larger of the two equals the PR version | |
| # AND the two differ, the PR is greater. | |
| if [[ "$PR_PLUGIN" == "$MAIN_PLUGIN" ]]; then | |
| echo "::error::Version was not bumped: still '$PR_PLUGIN' (same as main)." | |
| echo "::error::Promotion PRs MUST bump the version. See RELEASING.md." | |
| exit 1 | |
| fi | |
| LARGER=$(printf '%s\n%s\n' "$PR_PLUGIN" "$MAIN_PLUGIN" | sort -V | tail -n1) | |
| if [[ "$LARGER" != "$PR_PLUGIN" ]]; then | |
| echo "::error::Version '$PR_PLUGIN' is not strictly greater than main's '$MAIN_PLUGIN'." | |
| exit 1 | |
| fi | |
| echo "VERSION=$PR_PLUGIN" >> "$GITHUB_ENV" | |
| echo "::notice::Version bump $MAIN_PLUGIN → $PR_PLUGIN, all 5 fields in sync." | |
| changelog-entry: | |
| name: CHANGELOG entry matches new version | |
| if: github.event.pull_request.base.ref == 'main' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha }} | |
| - name: Verify CHANGELOG has a top-most section for the bumped version | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [[ ! -f CHANGELOG.md ]]; then | |
| echo "::error file=CHANGELOG.md::CHANGELOG.md not found." | |
| exit 1 | |
| fi | |
| VERSION=$(jq -r '.version' plugins/winui/agent-plugin/plugin.json) | |
| # The first H2 that is NOT '## [Unreleased]' must match the bumped | |
| # version, and have at least one bullet line under it. | |
| # Extract sections in order. | |
| SECTION=$(awk ' | |
| /^## \[Unreleased\]/ { in_unreleased=1; next } | |
| /^## \[/ && !in_unreleased { print; exit } | |
| /^## \[/ && in_unreleased { in_unreleased=0; print; exit } | |
| ' CHANGELOG.md) | |
| if [[ -z "$SECTION" ]]; then | |
| echo "::error file=CHANGELOG.md::No '## [X.Y.Z]' section found below '## [Unreleased]'." | |
| echo "::error::Add: ## [$VERSION] — $(date -u +%Y-%m-%d)" | |
| exit 1 | |
| fi | |
| # SECTION looks like: ## [0.4.0] — 2026-05-13 | |
| # Accept hyphen, double-hyphen (helper script ASCII fallback), | |
| # en dash, or em dash as the separator. | |
| if ! grep -qE "^## \[$(echo "$VERSION" | sed 's/\./\\./g')\][[:space:]]+(--?|—|–)[[:space:]]+[0-9]{4}-[0-9]{2}-[0-9]{2}" <<< "$SECTION"; then | |
| echo "::error file=CHANGELOG.md::Top-most release section is '$SECTION' but expected '## [$VERSION] — YYYY-MM-DD' (or '-', '--', '–' as separator)." | |
| echo "::error::Either fix the CHANGELOG header or fix the version bump." | |
| exit 1 | |
| fi | |
| # Verify there's at least one non-empty bullet under the new version | |
| # before the next H2. | |
| BULLETS=$(awk -v ver="$VERSION" ' | |
| $0 == "## [" ver "]" || $0 ~ "^## \\[" ver "\\][[:space:]]" { in_v=1; next } | |
| in_v && /^## \[/ { exit } | |
| in_v && /^[[:space:]]*-[[:space:]]+\S/ { count++ } | |
| END { print count+0 } | |
| ' CHANGELOG.md) | |
| if [[ "$BULLETS" -lt 1 ]]; then | |
| echo "::error file=CHANGELOG.md::Section '## [$VERSION]' has no bullet entries." | |
| echo "::error::Add at least one '- ...' line describing what changed." | |
| exit 1 | |
| fi | |
| echo "::notice::CHANGELOG section '## [$VERSION]' present with $BULLETS bullet(s)." |