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
9 changes: 7 additions & 2 deletions .github/PULL_REQUEST_TEMPLATE/promotion.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ The `Promotion gate` check directly depends on every constituent below and fails
| Exact-candidate clean install and startup | `Clean install and startup` | Pending |
| Real objects install, update, unload, delete | `Real objects install, update, unload, delete` | Pending |
| Aggregate | `Promotion gate` | Pending |
| Semantic version and notes | `Release plan` | Pending |

- [ ] The candidate SHA has not changed since every required check completed.
- [ ] The `Guard main branch source` and `Promotion gate` required contexts pass.
- [ ] The `Guard main branch source`, `Promotion gate`, and `Release plan` required contexts pass.
- [ ] The complete file and commit compare contains only reviewed work.
- [ ] PR conversations, review summaries, and all review threads have been read; actionable findings are addressed and required code-owner approvals exist.

Expand All @@ -47,6 +48,10 @@ List each unresolved issue and its disposition. Write `None` only after checking

Summarize behavioral changes. If no migration is required, state why.

### Release plan

Review the `Release Plan` workflow comment. Confirm the proposed semantic tag and deterministic notes describe the complete candidate, or confirm that the workflow reports a no-op. If the version is wrong, change the Conventional Commit history on `next` through a reviewed pull request before merging this promotion.

### Public-contract follow-ups

Link documentation and consumer follow-ups identified by the public-contract impact check. Write `None` only with a rationale.
Expand Down Expand Up @@ -102,4 +107,4 @@ Never force-push either persistent branch. Roll back with a reviewed revert comm

## Stable consumption boundary

Merging this promotion updates the Git-consumed stable `main` ref. It does not create a semantic tag or GitHub release. Any later tag is a separately approved action with its own release notes and exact-ref validation.
Merging this promotion updates the Git-consumed stable `main` ref and authorizes the reviewed release plan. When releasable commits exist, publication waits until every required workflow succeeds on the exact merge SHA, then creates the annotated tag and GitHub release automatically. A no-op plan creates neither. The signed manual tag path remains available for recovery.
15 changes: 12 additions & 3 deletions .github/workflows/promotion-readiness.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }}

- name: Install Zsh and archive tools
run: sudo apt-get update && sudo apt-get install -yq zsh zip unzip
- name: Install Zsh, jq, and archive tools
run: sudo apt-get update && sudo apt-get install -yq zsh jq zip unzip

- name: Check and compile Zsh sources
shell: bash
Expand Down Expand Up @@ -68,9 +68,18 @@ jobs:
if: ${{ hashFiles('tests/snippet-directory-mirror.zsh') != '' }}
run: zsh -f tests/snippet-directory-mirror.zsh

- name: Test release planning
run: zsh -f tests/release-plan.zsh

- name: Test promotion release verification
run: zsh -f tests/promotion-release-verification.zsh

- name: Test idempotent promotion publication
run: zsh -f tests/promotion-release-publication.zsh

zd:
name: ZD integration
uses: z-shell/zd/.github/workflows/test-native.yml@01c3477e48c0c31bb7225986bb1bac4270e151ff # z-shell/zd#121
uses: z-shell/zd/.github/workflows/test-native.yml@846591255b19558f4c61d9502982dc1ad6a049db # z-shell/zd#123
with:
zi_repo: ${{ github.event.pull_request.head.repo.full_name }}
zi_ref: ${{ github.event.pull_request.head.sha }}
Expand Down
75 changes: 75 additions & 0 deletions .github/workflows/release-plan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
name: Release Plan

on:
pull_request:
branches: [main]
types: [opened, reopened, synchronize, ready_for_review]
workflow_dispatch: {}

permissions:
contents: read
pull-requests: write

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true

jobs:
plan:
name: Release plan
runs-on: ubuntu-latest
steps:
- name: Check out the candidate
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
fetch-tags: true
persist-credentials: false

- name: Install Zsh
run: sudo apt-get update && sudo apt-get install -yq zsh

- name: Compute release plan
id: release
if: "${{ github.event_name == 'workflow_dispatch' || (github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.head.ref == 'next') }}"
env:
RELEASE_NOTES_FILE: ${{ runner.temp }}/release-notes.md
RELEASE_PLAN_OUTPUT: ${{ runner.temp }}/release-plan.env
RELEASE_PLAN_BODY: ${{ runner.temp }}/release-plan.md
run: |
zsh -f scripts/release-plan.zsh HEAD > "$RELEASE_PLAN_BODY"
cat "$RELEASE_PLAN_BODY" >> "$GITHUB_STEP_SUMMARY"
cat "$RELEASE_PLAN_OUTPUT" >> "$GITHUB_OUTPUT"

- name: Record non-promotion result
if: "${{ github.event_name == 'pull_request' && (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.head.ref != 'next') }}"
run: echo 'This pull request is not an internal next-to-main promotion; automatic publication does not apply.' >> "$GITHUB_STEP_SUMMARY"

- name: Update promotion pull request
if: "${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.head.ref == 'next' }}"
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
RELEASE_PLAN_BODY: ${{ runner.temp }}/release-plan.md
run: |
set -euo pipefail
marker='<!-- zi-release-plan -->'
body="${RUNNER_TEMP}/release-plan-comment.md"
{
echo "$marker"
cat "$RELEASE_PLAN_BODY"
echo
echo '_Merging this reviewed promotion authorizes publication after every required workflow succeeds on the exact merge SHA._'
} > "$body"
comment_id="$(gh api --paginate \
"repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \
--jq ".[] | select(.user.login == \"github-actions[bot]\" and (.body | contains(\"${marker}\"))) | .id" | head -n 1)"
if [[ -n "$comment_id" ]]; then
gh api --method PATCH "repos/${GITHUB_REPOSITORY}/issues/comments/${comment_id}" \
-F body=@"$body" >/dev/null
else
gh api --method POST "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \
-F body=@"$body" >/dev/null
fi
21 changes: 0 additions & 21 deletions .github/workflows/release-prepare.yml

This file was deleted.

70 changes: 63 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,24 @@ name: Release
on:
push:
tags: ["v*.*.*"]
workflow_run:
workflows: [Zsh, ZD Integration, CodeQL, Trunk Code Quality]
types: [completed]

permissions:
actions: read
contents: write
permissions: {}

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: release-${{ github.event.workflow_run.head_sha || github.ref_name }}
cancel-in-progress: false

jobs:
publish:
name: Verify and publish
if: github.repository == 'z-shell/zi'
manual:
name: Verify and publish recovery tag
if: github.event_name == 'push' && github.repository == 'z-shell/zi'
runs-on: ubuntu-latest
permissions:
actions: read
contents: write
steps:
- name: Check out the tagged commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down Expand Up @@ -49,3 +53,55 @@ jobs:
--title "Zi $TAG" \
--generate-notes \
--latest

automatic:
name: Publish reviewed promotion
if: "${{ github.event_name == 'workflow_run' && github.repository == 'z-shell/zi' && github.event.workflow_run.head_branch == 'main' }}"
runs-on: ubuntu-latest
permissions:
actions: read
contents: write
pull-requests: read
steps:
- name: Check out the trusted main branch
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: refs/heads/main
fetch-depth: 0
fetch-tags: true

- name: Install Zsh
run: sudo apt-get update && sudo apt-get install -yq zsh

- name: Verify promotion and exact-SHA validation
id: verify
env:
GH_TOKEN: ${{ github.token }}
PROMOTION_SHA: ${{ github.event.workflow_run.head_sha }}
run: zsh -f scripts/verify-promotion-release.zsh

- name: Compute the authorized release plan
id: plan
if: steps.verify.outputs.ready == 'true'
env:
RELEASE_NOTES_FILE: ${{ runner.temp }}/release-notes.md
RELEASE_TARGET: ${{ github.event.workflow_run.head_sha }}
run: |
RELEASE_PLAN_OUTPUT="$GITHUB_OUTPUT" \
zsh -f scripts/release-plan.zsh \
"$RELEASE_TARGET" >> "$GITHUB_STEP_SUMMARY"

- name: Record no-op promotion
if: steps.verify.outputs.ready == 'true' && steps.plan.outputs.release != 'true'
run: echo 'The reviewed promotion contains no releasable Conventional Commits; no tag or release was created.' >> "$GITHUB_STEP_SUMMARY"

- name: Create annotated tag and release
if: steps.verify.outputs.ready == 'true' && steps.plan.outputs.release == 'true'
env:
GH_TOKEN: ${{ github.token }}
RELEASE_NOTES_FILE: ${{ runner.temp }}/release-notes.md
RELEASE_TAG: ${{ steps.plan.outputs.tag }}
RELEASE_TARGET: ${{ github.event.workflow_run.head_sha }}
run: |
zsh -f scripts/publish-promotion-release.zsh >> "$GITHUB_STEP_SUMMARY"
echo "Authorized by reviewed promotion #${{ steps.verify.outputs.promotion_pr }}." >> "$GITHUB_STEP_SUMMARY"
6 changes: 1 addition & 5 deletions .github/workflows/zd-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@ name: ZD Integration
on:
push:
branches: [main, next]
paths:
- ".github/workflows/zd-integration.yml"
- "zi.zsh"
- "lib/**"
pull_request:
paths:
- ".github/workflows/zd-integration.yml"
Expand All @@ -24,7 +20,7 @@ permissions:

jobs:
zd-test:
uses: z-shell/zd/.github/workflows/test-native.yml@01c3477e48c0c31bb7225986bb1bac4270e151ff # z-shell/zd#121
uses: z-shell/zd/.github/workflows/test-native.yml@846591255b19558f4c61d9502982dc1ad6a049db # z-shell/zd#123
with:
zi_repo: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
zi_ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
Expand Down
80 changes: 36 additions & 44 deletions .github/workflows/zsh-n.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,56 +6,15 @@ on:
branches:
- main
- next
paths:
- "zi.zsh"
- "lib/**"
- "contracts/package-manifest-v1.json"
- "scripts/validate-package-manifest.py"
- "tests/**"
- ".github/workflows/*.yml"
- "tests/annex-unregister.zsh"
- "tests/atinit-deferred-marker.zsh"
- "tests/benchmark-harness.zsh"
- "benchmarks/**"
- "tests/ci-registration.zsh"
- "tests/archive-extraction.zsh"
- "tests/completion-refresh.zsh"
- "tests/disk-ice-resolution.zsh"
- "tests/home-preparation.zsh"
- "tests/hook-ownership.zsh"
- "tests/ice-tokenizer.zsh"
- "tests/load-object-status.zsh"
- "tests/message-formatting.zsh"
- "tests/package-manifest-contract.zsh"
- "tests/package-manifest-fixtures.zsh"
- "tests/fixtures/package-manifests/**"
- "scripts/refresh-package-manifests.zsh"
- "tests/package-manifest-parsing.zsh"
- "tests/path-resolution.zsh"
- "tests/parallel-update.zsh"
- "tests/plugin-autoload-fpath-scope.zsh"
- "tests/plugin-autoload-ice.zsh"
- "tests/nested-load-state.zsh"
- "tests/pack-service-first-install.zsh"
- "tests/plugin-autoload-ownership.zsh"
- "tests/plugin-standard-callbacks.zsh"
- "tests/release-tag-verification.zsh"
- "tests/scheduler-idle.zsh"
- "tests/fixtures/plugin-standard-callbacks/**"
- "tests/self-update-reload.zsh"
- "tests/snippet-directory-mirror.zsh"
- "tests/snippet-update-status.zsh"
- "tests/source-hygiene.zsh"
- "tests/subst-nesting.zsh"
- "tests/unload-hook-dispatch.zsh"
- "tests/unload-ownership-contracts.zsh"
- "tests/version-reporting.zsh"
pull_request:
paths:
- "zi.zsh"
- "lib/**"
- "contracts/package-manifest-v1.json"
- "scripts/validate-package-manifest.py"
- "scripts/release-plan.zsh"
- "scripts/verify-promotion-release.zsh"
- "scripts/publish-promotion-release.zsh"
- "tests/**"
- ".github/workflows/*.yml"
- "tests/annex-unregister.zsh"
Expand Down Expand Up @@ -208,6 +167,39 @@ jobs:
- name: Test release tag verification
run: zsh -f tests/release-tag-verification.zsh

release-plan:
name: Release Plan
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Zsh
run: sudo apt update && sudo apt-get install -yq zsh
- name: Test release planning
run: zsh -f tests/release-plan.zsh

promotion-release-verification:
name: Promotion Release Verification
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Zsh and jq
run: sudo apt update && sudo apt-get install -yq zsh jq
- name: Test promotion release verification
run: zsh -f tests/promotion-release-verification.zsh

promotion-release-publication:
name: Promotion Release Publication
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Zsh
run: sudo apt update && sudo apt-get install -yq zsh
- name: Test idempotent promotion publication
run: zsh -f tests/promotion-release-publication.zsh

benchmark-harness:
name: Benchmark Harness
runs-on: ubuntu-latest
Expand Down
21 changes: 6 additions & 15 deletions docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,25 +49,16 @@ Repository rules intentionally omit linear-history requirements on both persiste

## Releases

A promotion to `main` publishes nothing. It updates the Git-consumed stable ref and stops there.
A same-repository `next` to `main` promotion is the normal publication authorization. Reviewers see the deterministic version and release-note plan on the promotion pull request before deciding whether to merge.

**A signed annotated tag is the sole publication authorization.** Nothing else creates a release: not a merge, not a green pipeline, not the automated proposal.

1. After a promotion reaches `main`, `Release Prepare` opens or updates a proposal issue with the next semantic version computed from Conventional Commits and a draft changelog. It never creates a tag.
2. A maintainer reviews the proposed version, adjusts it if the computed bump does not describe the change, and pushes a signed annotated tag:

```text
git switch main && git pull --ff-only
git tag -s vX.Y.Z -F <notes-file>
git push origin vX.Y.Z
```

3. `scripts/verify-release-tag.zsh` rejects the tag unless every one of the following holds: it matches `vX.Y.Z`, it is annotated rather than lightweight, GitHub reports its signature as verified, its target is the current `origin/main`, and the `Zsh`, `ZD Integration`, `CodeQL` and `Trunk Code Quality` workflows all succeeded on that exact commit.
4. Only then is a GitHub release published, idempotently, with generated notes.
1. `Release Plan` computes the next semantic version from Conventional Commits since the latest `vX.Y.Z` tag. A breaking change produces a major bump, `feat` produces a minor bump, and `fix` or `perf` produces a patch bump. A promotion with none of those commits is an explicit no-op.
2. Merging the reviewed promotion authorizes publication of that displayed plan. The merge still updates the Git-consumed stable `main` ref immediately.
3. The automatic publisher proves that the exact merge commit came from the reviewed same-repository `next` pull request and is still current `main`. It waits for `Zsh`, `ZD Integration`, `CodeQL`, and `Trunk Code Quality` to succeed on that exact SHA.
4. The publisher creates an annotated tag and the GitHub release in one idempotent workflow. It fails closed if `main` moves, the promotion identity cannot be proven, validation fails, or the proposed tag already targets another commit.

The repository stores no version file. `ZI[VERSION]` is derived at runtime from `git describe --tags --exact-match`, so the tag is the version and there is nothing to keep in step with it.

Closing a proposal issue without tagging skips that release; the next promotion opens a new proposal.
The signed manual-tag flow remains available for recovery or exceptional publication. A maintainer may push a signed annotated `vX.Y.Z` tag to the exact current `main`; `scripts/verify-release-tag.zsh` then requires a valid GitHub signature, the exact target, and the same four successful workflows before it creates the release. No personal signing key is stored in Actions.

## What not to add

Expand Down
Loading
Loading