From c3bb920292f7d988d1da82ffab0c34949333b436 Mon Sep 17 00:00:00 2001 From: Pablo Ariel Di Loreto Date: Sat, 26 Sep 2026 17:54:48 +0000 Subject: [PATCH 1/2] feat(release): the dev build is one pre-release on a moving tag; triage knows current versions plugin-release-wp.yml publishes the development build of every push to main as the one GitHub pre-release "Development build -dev." on the moving tag `dev-tag` (default `dev`, empty publishes none): the tag moves to the commit, the release is created or updated with notes that say the version, the commit and the changelog that is coming, and the asset .zip is replaced, so its URL never changes. The run's artifact goes. There is no history of development builds; the commit in the notes and in the Build header rebuilds any of them. The moving tag is outside the X.Y.Z rulesets and the job needs contents: write and nothing else; "Latest" stays the last X.Y.Z. This is the nightly pattern WooCommerce and others use, chosen over one tag per build because it multiplies neither permanent tags nor releases and needs no publishing key. issue-triage.yml gets a dev-tag input and puts the current versions in the brief (the readme's Stable tag; the pre-release's version): a report made on a plugin version older than the released one, or a development build older than the current one, is needs-info, and the reply asks to update, or to install the current development build, and to say whether the problem is still there. README, the release template and the organisation contributing guide describe both. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/issue-triage.yml | 26 ++++++++- .github/workflows/plugin-release-wp.yml | 77 +++++++++++++++++++------ CONTRIBUTING.md | 2 +- README.md | 4 +- workflow-templates/release-wp.yml | 5 +- 5 files changed, 87 insertions(+), 27 deletions(-) diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml index 026b31d..edaf4a9 100644 --- a/.github/workflows/issue-triage.yml +++ b/.github/workflows/issue-triage.yml @@ -8,8 +8,11 @@ name: Issue triage # # bug:unconfirmed a bug report with enough to try to reproduce it # (issue-repro.yml picks it up from this label) -# needs-info a bug report missing versions, steps or logs: the -# reply asks for them +# needs-info a bug report missing versions, steps or logs, or +# made on a version that is not current (older than +# the released one, or a development build older than +# the current pre-release): the reply asks for them, +# or to update and say whether it is still there # by-design works as documented, or a known limitation # pro-feature needs the paid service or add-on the roadmap names # enhancement a feature request (+ exists-in-pro when the roadmap @@ -43,6 +46,10 @@ on: description: Where usage questions go (a forum, a help page). type: string required: true + dev-tag: + description: The moving tag of the repository's "Development build" pre-release (as in plugin-release-wp.yml), so the triage knows the current development version. Empty when the repository publishes none. + type: string + default: dev roadmap: description: The roadmap file that says what is free, paid, planned and not planned. type: string @@ -114,6 +121,7 @@ jobs: AUTHOR: ${{ github.event.issue.user.login }} ROADMAP: ${{ inputs.roadmap }} SUPPORT_URL: ${{ inputs.support-url }} + DEV_TAG: ${{ inputs.dev-tag }} run: | set -euo pipefail { @@ -129,6 +137,13 @@ jobs: for f in "$ROADMAP" README.md readme.txt; do if [ -f "$f" ]; then echo; echo "## Repository file: $f"; echo; head -c 60000 "$f"; fi done + echo; echo "## Current versions"; echo + released=$(grep -oP '^Stable tag:\s*\K\S+' readme.txt 2>/dev/null || true) + echo "- Released (wordpress.org, the readme's Stable tag): ${released:-unknown}" + if [ -n "$DEV_TAG" ]; then + dev=$(gh release view "$DEV_TAG" --repo "$GITHUB_REPOSITORY" --json name --jq .name 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+-dev\.[0-9]+' || true) + echo "- Development build (GitHub pre-release $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/tag/$DEV_TAG): ${dev:-none published}" + fi echo; echo "## Where usage questions go"; echo; echo "$SUPPORT_URL" } > .dx-central/brief.md echo "Brief: $(wc -c < .dx-central/brief.md) bytes." @@ -149,7 +164,12 @@ jobs: Pick exactly one category: - bug_unconfirmed: a defect report with steps, versions and what was expected. - - needs_info: a defect report missing what a maintainer needs to try it (say what). + - needs_info: a defect report missing what a maintainer needs to try it (say what), + or one made on a version that is not current: a plugin version older than the + released one, or a development build (X.Y.Z-dev.N) older than the current one (see + "Current versions"). Then ask, kindly, to update to the released version or to + install the current development build (give its link) and to say whether the + problem is still there; many reports come from sites that never updated. - by_design: works as documented, or a known limitation from the roadmap. - pro_feature: it needs the paid service or add-on the roadmap names. - enhancement: a request for something new (exists_in_pro when a paid product has it). diff --git a/.github/workflows/plugin-release-wp.yml b/.github/workflows/plugin-release-wp.yml index c588bf2..01240bd 100644 --- a/.github/workflows/plugin-release-wp.yml +++ b/.github/workflows/plugin-release-wp.yml @@ -40,11 +40,17 @@ name: WordPress plugin release # that removes the line is the decision to release. A tag pushed by hand is # refused the same way. # -# Every push to main also produces a development build, whatever the labels +# Every push to main also publishes a development build, whatever the labels # or the readme say: the tree that ships (minus .distignore) with the three -# markers (and a `= Unreleased =` heading) stamped `-dev.` and a `Build: ` header, as the -# run's artifact `--dev.`, kept 30 days, for anyone to install -# and try what is coming. Nothing is committed or tagged for it. +# markers (and a `= Unreleased =` heading) stamped `-dev.` and a +# `Build: ` header, as ONE GitHub pre-release named "Development +# build" on the moving tag `dev-tag` (default `dev`), replaced on every push: +# the tag moves to the commit, the release notes say the version, the commit +# and the changelog that is coming, and the asset `.zip` keeps its URL, +# `…/releases/download//.zip`. There is no history of +# development builds (the commit in the `Build:` header rebuilds any of +# them); releases X.Y.Z are untouched and "Latest" stays the last of them. +# The moving tag is outside the X.Y.Z rulesets and never publishes anything. # # Release tags are permanent (tag protection): a failed deploy is fixed and # re-run, never re-tagged. `dry-run: true` rehearses everything but the SVN @@ -106,6 +112,10 @@ on: description: The ref of DiluxOne/.github whose scripts and policy to use. type: string default: v2 + dev-tag: + description: The moving tag of the "Development build" pre-release that every push to main replaces. Empty publishes no development build. + type: string + default: dev outputs: version: description: The version released, or empty when nothing was. @@ -233,19 +243,25 @@ jobs: name: Development build # Every push to main, whatever the labels or the readme say, and # whatever happens to the version job: the tree that ships, stamped with - # the version that is coming and the commit it was built from, for - # anyone to install and try. It computes the development version itself - # so that it does not depend on the release's job. Never on a tag: a - # release is its own build. No secrets, no write: nothing is committed - # or tagged for it. - if: startsWith(github.ref, 'refs/heads/') + # the version that is coming and the commit it was built from, published + # as the one "Development build" pre-release for anyone to install and + # try. It computes the development version itself so that it does not + # depend on the release's job. Never on a tag: a release is its own + # build. No secrets; contents: write only to move the `dev` tag and + # replace that pre-release, which the X.Y.Z rulesets do not cover and + # which publishes nothing to wordpress.org. + if: startsWith(github.ref, 'refs/heads/') && inputs.dev-tag != '' runs-on: ubuntu-latest timeout-minutes: 5 permissions: - contents: read + contents: write pull-requests: read # the labels of what merged + concurrency: + group: development-build-${{ github.repository }} + cancel-in-progress: false env: SLUG: ${{ inputs.slug }} + DEV_TAG: ${{ inputs.dev-tag }} MAIN: ${{ inputs.main-file != '' && inputs.main-file || format('{0}.php', inputs.slug) }} CONSTANT: ${{ inputs.version-constant }} steps: @@ -289,15 +305,38 @@ jobs: exclude=(); [ -f .distignore ] && exclude=(--exclude-from=.distignore) mkdir -p "$RUNNER_TEMP/dist" rsync -rc "${exclude[@]}" --exclude=.git --exclude=.github ./ "$RUNNER_TEMP/dist/$SLUG/" --delete - # shellcheck disable=SC2016 # backticks in the Markdown of the summary, not commands. - printf '## Development build %s\n\nThe tree that ships, stamped `%s` from commit `%s`, is the artifact **%s** of this run (download it from the summary of the run, unzip, install the `%s` folder as a plugin). Not a release: nothing was committed or tagged.\n' "$DEV" "$DEV" "$build" "$SLUG-$DEV" "$SLUG" >> "$GITHUB_STEP_SUMMARY" + (cd "$RUNNER_TEMP/dist" && zip -qr "$RUNNER_TEMP/$SLUG.zip" "$SLUG") + # The notes: what this build is, and the changelog that is coming + # (the newest entry of the readme, without its `Unreleased.` line). + # shellcheck disable=SC2016 # backticks in the Markdown of the notes, not commands. + { + printf 'Development build **%s** of DiluxOne %s, from commit %s. Not a release: it is replaced on every push to `%s`, it is not on wordpress.org, and a site that installs it updates to the release when it is published.\n\n' "$DEV" "$SLUG" "[\`$build\`]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/commit/$GITHUB_SHA)" "${GITHUB_REF#refs/heads/}" + printf '**Install:** download `%s.zip` below, then in WordPress: Plugins → Add New → Upload Plugin. Status › System shows the version and the build.\n\n' "$SLUG" + notes=$(awk '/^== Changelog ==$/ { c = 1; next } c && /^= .* =$/ { if (h) exit; h = 1; next } h && !/^Unreleased\.?$/ && (p || NF) { p = 1; print }' readme.txt | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}') + if [ -n "$(printf '%s' "$notes" | tr -d '[:space:]')" ]; then printf '**Coming in the next version:**\n%s\n' "$notes"; fi + } > "$RUNNER_TEMP/dev-notes.md" - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: ${{ inputs.slug }}-${{ steps.dev.outputs.dev }} - path: ${{ runner.temp }}/dist - retention-days: 30 - if-no-files-found: error + - name: Publish the pre-release on the moving tag + env: + GH_TOKEN: ${{ github.token }} + DEV: ${{ steps.dev.outputs.dev }} + # The tag moves to this commit, the one pre-release is created or + # updated, and the asset is replaced; its URL never changes. + run: | + set -euo pipefail + title="Development build $DEV" + if gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$DEV_TAG" --jq .object.sha >/dev/null 2>&1; then + gh api -X PATCH "repos/$GITHUB_REPOSITORY/git/refs/tags/$DEV_TAG" -f sha="$GITHUB_SHA" -F force=true --jq .object.sha + fi + if gh release view "$DEV_TAG" --repo "$GITHUB_REPOSITORY" --json id --jq .id >/dev/null 2>&1; then + gh release edit "$DEV_TAG" --repo "$GITHUB_REPOSITORY" --title "$title" --notes-file "$RUNNER_TEMP/dev-notes.md" --prerelease --latest=false --target "$GITHUB_SHA" >/dev/null + else + gh release create "$DEV_TAG" --repo "$GITHUB_REPOSITORY" --title "$title" --notes-file "$RUNNER_TEMP/dev-notes.md" --prerelease --latest=false --target "$GITHUB_SHA" >/dev/null + fi + gh release upload "$DEV_TAG" "$RUNNER_TEMP/$SLUG.zip" --repo "$GITHUB_REPOSITORY" --clobber + url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/tag/$DEV_TAG" + # shellcheck disable=SC2016 # backticks in the Markdown of the summary, not commands. + printf '## Development build %s\n\nPublished as the pre-release [%s](%s), replacing the previous one: tag `%s` now points at `%s`, the asset is `%s.zip`. Not a release: nothing reached wordpress.org.\n' "$DEV" "$title" "$url" "$DEV_TAG" "$build" "$SLUG" >> "$GITHUB_STEP_SUMMARY" release: name: Deploy to wordpress.org diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2459851..58e9d49 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,7 +73,7 @@ Pull requests from **forks** are not reviewed automatically: the review runs wit Nobody types a version number. The `type:*` label the review sets on each merged pull request decides the next one (`type:breaking` → major, `type:feat` → minor, `type:fix` or `type:perf` → patch; a maintainer's `version:major|minor|patch` label wins), and `main` keeps the last released version in its files between releases. In a repository that publishes (a WordPress plugin): -- **Every push to `main` produces a development build**, the shipped tree stamped `-dev.`, as an artifact of the *Release* run in the Actions tab. Anyone can download it and try what is coming; it is not a release. +- **Every push to `main` publishes a development build**, the shipped tree stamped `-dev.`, as the one *Development build* pre-release in the repository's Releases, replaced each time (no history: the commit in its notes rebuilds any of them). Anyone can download it and try what is coming; it is not a release, and "Latest" stays the last published version. - **The changelog is written as the changes merge.** A pull request that changes what a user sees adds its bullet to the newest entry of `readme.txt` (`= X.Y.Z =`, first line `Unreleased.`), in the same pull request. - **The maintainer decides when it is ready** by removing the `Unreleased.` line in a pull request. That push to `main` waits for approval in the repository's `wordpress-org` environment; only its required reviewers can approve, and approving publishes (a repository's policy can set a kind of bump to `auto`, published without waiting, or `off`, never published; the organisation default is to wait). Until then, however many pull requests merge, nothing waits for anyone and nothing is published. - **Outside contributors** need nothing more than the pull request: your change ships in the next version with its bullet in the changelog. You cannot approve a release, and you do not need to. diff --git a/README.md b/README.md index 733474f..51ab838 100644 --- a/README.md +++ b/README.md @@ -155,9 +155,9 @@ Call them pinned to `@v2`; a breaking change ships as `v2`. A stack suffix | [`plugin-checks-wp.yml`](.github/workflows/plugin-checks-wp.yml) | Fast gates for a WordPress plugin: syntax and unit tests on every PHP from the minimum to the latest, PHPCS, PHPStan, Psalm taint, i18n, Plugin Check on the shipped tree, readme and versions. Needs the composer scripts `test:unit`, `lint`, `stan`, `psalm:taint`, a `.distignore` and a `readme.txt`. | `slug`, `main-file`, `version-constant`, `php-versions` | | [`plugin-tests-wp.yml`](.github/workflows/plugin-tests-wp.yml) | Slow suites on wp-env: PHPUnit integration (multisite) and Playwright E2E. Needs `.wp-env.json`, `phpunit-integration.xml` and a Playwright config that writes to `build/e2e-results`. | `integration`, `multisite`, `e2e` | | [`weekly-failure.yml`](.github/workflows/weekly-failure.yml) | When the scheduled full run fails: opens one `ci:weekly` issue with the run, or adds the run to the one already open. | none | -| [`issue-triage.yml`](.github/workflows/issue-triage.yml) | When an issue opens: classifies it with the roadmap and the docs (bug to reproduce, needs info, by design, pro feature, enhancement, question, duplicate, security), applies the label and posts one reply; never closes. On a schedule, closes `needs-info` issues nobody answered. Light model. | every repository | +| [`issue-triage.yml`](.github/workflows/issue-triage.yml) | When an issue opens: classifies it with the roadmap and the docs (bug to reproduce, needs info, by design, pro feature, enhancement, question, duplicate, security), applies the label and posts one reply; never closes. A report made on a version that is not current (older than the released one, or a development build older than the current pre-release) is `needs-info`: the reply asks to update or to try the current development build. On a schedule, closes `needs-info` issues nobody answered. Light model. | every repository (`dev-tag`) | | [`issue-repro.yml`](.github/workflows/issue-repro.yml) | When an issue gets `bug:unconfirmed` (or `repro:again`): Claude writes one unit test that fails if the bug exists (no shell), a second job with no secrets and a read-only token runs it, a third with the bot token pushes the file the first job produced (hash-checked) and reports. Fails: `bug:confirmed` plus a draft PR with the test. Passes: `could-not-reproduce` and a question to the reporter. At most 5 a day. | repositories with unit tests | -| [`plugin-release-wp.yml`](.github/workflows/plugin-release-wp.yml) | The release as a deployment. On a push to `main`: computes the next version from the `type:*` labels of what merged (`scripts/next-version.py`); nothing pending, the policy's `release.: off`, or a readme whose newest changelog entry still starts with the line `Unreleased.` (the version is not ready), ends there, green, and the summary says why. Otherwise waits for the reviewers of the repository's environment (the summary shows the version, the bump and the pull requests), then stamps the three version markers in the checkout, validates them and the changelog (`= X.Y.Z =` or `= Unreleased =` renamed), deploys to wordpress.org SVN, creates the tag with the release App's token and the GitHub release with the changelog and what was merged, grouped by type. On a tag `X.Y.Z` pushed by hand: the same, and the tag must be the version the labels say is next and the readme must be ready. Every push to `main` also uploads a development build, the shipped tree stamped `-dev.` with a `Build: ` header, as the artifact `--dev.` (30 days). `dry-run` rehearses everything but the SVN commit, the tag and the release (the notes go to the summary). The caller passes `secrets: inherit` and runs only on `main` and `X.Y.Z` tags. Outputs `version` and `dev`. | `slug`, `main-file`, `version-constant`, `dry-run`, `environment`, `auto-environment`, `central-ref` | +| [`plugin-release-wp.yml`](.github/workflows/plugin-release-wp.yml) | The release as a deployment. On a push to `main`: computes the next version from the `type:*` labels of what merged (`scripts/next-version.py`); nothing pending, the policy's `release.: off`, or a readme whose newest changelog entry still starts with the line `Unreleased.` (the version is not ready), ends there, green, and the summary says why. Otherwise waits for the reviewers of the repository's environment (the summary shows the version, the bump and the pull requests), then stamps the three version markers in the checkout, validates them and the changelog (`= X.Y.Z =` or `= Unreleased =` renamed), deploys to wordpress.org SVN, creates the tag with the release App's token and the GitHub release with the changelog and what was merged, grouped by type. On a tag `X.Y.Z` pushed by hand: the same, and the tag must be the version the labels say is next and the readme must be ready. Every push to `main` also publishes a development build, the shipped tree stamped `-dev.` with a `Build: ` header, as the one **Development build** pre-release on the moving tag `dev-tag` (default `dev`), replaced each time: fixed asset URL `…/releases/download/dev/.zip`, no history (the commit rebuilds any build), "Latest" stays the last `X.Y.Z`. `dry-run` rehearses everything but the SVN commit, the tag and the release (the notes go to the summary). The caller passes `secrets: inherit` and runs only on `main` and `X.Y.Z` tags. Outputs `version` and `dev`. | `slug`, `main-file`, `version-constant`, `dry-run`, `environment`, `auto-environment`, `central-ref`, `dev-tag` | Only here: [`review-learnings.yml`](.github/workflows/review-learnings.yml) (every Monday: finds with one search the pull requests the bot reviewed diff --git a/workflow-templates/release-wp.yml b/workflow-templates/release-wp.yml index bb447ee..d5f3623 100644 --- a/workflow-templates/release-wp.yml +++ b/workflow-templates/release-wp.yml @@ -9,8 +9,9 @@ name: Release # be the version the labels say is next. While the readme's newest changelog # entry starts with the line `Unreleased.` nothing waits for approval: the # version is not ready, and the pull request that removes that line releases -# it. Every push to main also uploads a development build (the shipped tree -# stamped -dev.) as the run's artifact. Fill in slug and +# it. Every push to main also publishes a development build (the shipped +# tree stamped -dev.) as the one "Development build" pre-release on +# the moving tag `dev`, replaced each time. Fill in slug and # version-constant. # # The environment holds the SVN credentials and the release App's key From 84938725d74e07da7a5cd34c4fd81b744986e2f7 Mon Sep 17 00:00:00 2001 From: Pablo Ariel Di Loreto Date: Sat, 26 Sep 2026 18:01:06 +0000 Subject: [PATCH 2/2] fix(release): a rehearsal publishes no development build; the tag moves last and only from the head The development build honours dry-run: in a rehearsal it is built and its notes go to the summary, but no tag moves and no pre-release is published, as the documented contract says. The job runs only from the default branch, publishes only when its commit is still the head of that branch (a re-run of an old run does not move the tag back), uploads the zip before it changes the notes and the title, and moves the tag last, so a failed upload leaves the previous build whole. The dev output's description, the header's claim about rulesets (the repository's tag rulesets must leave the moving tag free; the adoption guide's cover X.Y.Z only) and the plan's mention of the artifact are brought in line. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/plugin-release-wp.yml | 53 ++++++++++++++++++------- README.md | 3 ++ docs/plans/release-automation.md | 2 +- 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/.github/workflows/plugin-release-wp.yml b/.github/workflows/plugin-release-wp.yml index 01240bd..4904791 100644 --- a/.github/workflows/plugin-release-wp.yml +++ b/.github/workflows/plugin-release-wp.yml @@ -50,7 +50,11 @@ name: WordPress plugin release # `…/releases/download//.zip`. There is no history of # development builds (the commit in the `Build:` header rebuilds any of # them); releases X.Y.Z are untouched and "Latest" stays the last of them. -# The moving tag is outside the X.Y.Z rulesets and never publishes anything. +# The moving tag must be left free by the repository's tag rulesets (the +# adoption guide's cover X.Y.Z only) and never publishes anything to +# wordpress.org. In a rehearsal (`dry-run: true`) the build is made and its +# notes go to the summary, but no tag moves and no pre-release is published: +# a rehearsal publishes nothing, as documented. # # Release tags are permanent (tag protection): a failed deploy is fixed and # re-run, never re-tagged. `dry-run: true` rehearses everything but the SVN @@ -121,7 +125,7 @@ on: description: The version released, or empty when nothing was. value: ${{ jobs.release.outputs.version }} dev: - description: The development version of this commit, -dev.; the artifact of the development build is named -. + description: The development version of this commit, -dev., the one the "Development build" pre-release carries. value: ${{ jobs.version.outputs.dev }} permissions: @@ -247,10 +251,11 @@ jobs: # as the one "Development build" pre-release for anyone to install and # try. It computes the development version itself so that it does not # depend on the release's job. Never on a tag: a release is its own - # build. No secrets; contents: write only to move the `dev` tag and - # replace that pre-release, which the X.Y.Z rulesets do not cover and - # which publishes nothing to wordpress.org. - if: startsWith(github.ref, 'refs/heads/') && inputs.dev-tag != '' + # build, and only from the default branch: a push there replaces the + # public pre-release. No secrets; contents: write only to move the `dev` + # tag and replace that pre-release, which publishes nothing to + # wordpress.org. In a rehearsal the build is made and nothing is published. + if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && inputs.dev-tag != '' runs-on: ubuntu-latest timeout-minutes: 5 permissions: @@ -295,9 +300,11 @@ jobs: # Plugins and wherever the plugin reads its headers. The zip holds # the plugin folder, named after the slug, the way WordPress installs # it. + id: pack run: | set -euo pipefail build=${GITHUB_SHA:0:7} + echo "build=$build" >> "$GITHUB_OUTPUT" bash .dx-central/scripts/stamp-version.sh . "$DEV" "$MAIN" "$CONSTANT" "$build" # The central's checkout leaves before the tree is packed: nothing # that is not the plugin may end up in the build. @@ -316,27 +323,43 @@ jobs: if [ -n "$(printf '%s' "$notes" | tr -d '[:space:]')" ]; then printf '**Coming in the next version:**\n%s\n' "$notes"; fi } > "$RUNNER_TEMP/dev-notes.md" + - name: The notes, in a rehearsal + if: inputs.dry-run + env: + DEV: ${{ steps.dev.outputs.dev }} + run: | + # shellcheck disable=SC2016 # backticks in the Markdown of the summary, not commands. + printf '## Development build %s (rehearsal)\n\nBuilt, not published: `dry-run: true`. The pre-release would say:\n\n' "$DEV" >> "$GITHUB_STEP_SUMMARY" + cat "$RUNNER_TEMP/dev-notes.md" >> "$GITHUB_STEP_SUMMARY" + - name: Publish the pre-release on the moving tag + if: ${{ !inputs.dry-run }} env: GH_TOKEN: ${{ github.token }} DEV: ${{ steps.dev.outputs.dev }} - # The tag moves to this commit, the one pre-release is created or - # updated, and the asset is replaced; its URL never changes. + BUILD: ${{ steps.pack.outputs.build }} + BASE: ${{ github.event.repository.default_branch }} + # Only when this commit is still the head of the default branch (a + # re-run of an old run must not move the tag back). The zip is + # uploaded before the notes, the title and the tag change, so a + # failed upload leaves the previous build whole; the tag moves last. run: | set -euo pipefail - title="Development build $DEV" - if gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$DEV_TAG" --jq .object.sha >/dev/null 2>&1; then - gh api -X PATCH "repos/$GITHUB_REPOSITORY/git/refs/tags/$DEV_TAG" -f sha="$GITHUB_SHA" -F force=true --jq .object.sha + head=$(gh api "repos/$GITHUB_REPOSITORY/commits/$BASE" --jq .sha) + if [ "$head" != "$GITHUB_SHA" ]; then + printf '## Development build %s\n\nNot published: %s is no longer the head of %s (a newer push publishes its own build).\n' "$DEV" "$BUILD" "$BASE" >> "$GITHUB_STEP_SUMMARY" + exit 0 fi - if gh release view "$DEV_TAG" --repo "$GITHUB_REPOSITORY" --json id --jq .id >/dev/null 2>&1; then - gh release edit "$DEV_TAG" --repo "$GITHUB_REPOSITORY" --title "$title" --notes-file "$RUNNER_TEMP/dev-notes.md" --prerelease --latest=false --target "$GITHUB_SHA" >/dev/null - else + title="Development build $DEV" + if ! gh release view "$DEV_TAG" --repo "$GITHUB_REPOSITORY" --json id --jq .id >/dev/null 2>&1; then gh release create "$DEV_TAG" --repo "$GITHUB_REPOSITORY" --title "$title" --notes-file "$RUNNER_TEMP/dev-notes.md" --prerelease --latest=false --target "$GITHUB_SHA" >/dev/null fi gh release upload "$DEV_TAG" "$RUNNER_TEMP/$SLUG.zip" --repo "$GITHUB_REPOSITORY" --clobber + gh release edit "$DEV_TAG" --repo "$GITHUB_REPOSITORY" --title "$title" --notes-file "$RUNNER_TEMP/dev-notes.md" --prerelease --latest=false --target "$GITHUB_SHA" >/dev/null + gh api -X PATCH "repos/$GITHUB_REPOSITORY/git/refs/tags/$DEV_TAG" -f sha="$GITHUB_SHA" -F force=true --jq .object.sha url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/tag/$DEV_TAG" # shellcheck disable=SC2016 # backticks in the Markdown of the summary, not commands. - printf '## Development build %s\n\nPublished as the pre-release [%s](%s), replacing the previous one: tag `%s` now points at `%s`, the asset is `%s.zip`. Not a release: nothing reached wordpress.org.\n' "$DEV" "$title" "$url" "$DEV_TAG" "$build" "$SLUG" >> "$GITHUB_STEP_SUMMARY" + printf '## Development build %s\n\nPublished as the pre-release [%s](%s), replacing the previous one: tag `%s` now points at `%s`, the asset is `%s.zip`. Not a release: nothing reached wordpress.org.\n' "$DEV" "$title" "$url" "$DEV_TAG" "$BUILD" "$SLUG" >> "$GITHUB_STEP_SUMMARY" release: name: Deploy to wordpress.org diff --git a/README.md b/README.md index 51ab838..64da2e9 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,9 @@ workflow* runs everything by hand, and its failure is in the run alone. reporting. The `dilux-bot` App must be installed on the repository. The labels are created by the review itself. + The tag rulesets cover `X.Y.Z` only, so the moving tag `dev` of the + development build stays free for the workflow's own token. + Then the readme: the newest entry under `== Changelog ==` is headed `= X.Y.Z =` (or `= Unreleased =`) and its first line is `Unreleased.` while the version is not ready. Every pull request that changes what a diff --git a/docs/plans/release-automation.md b/docs/plans/release-automation.md index d69de88..fa43764 100644 --- a/docs/plans/release-automation.md +++ b/docs/plans/release-automation.md @@ -28,7 +28,7 @@ This design is release-please with two changes: the type comes from the AI's rea ## B. The next version, and the dev version - `scripts/next-version.py` (central) is deterministic: from the labels of the pull requests merged into `main` since the last tag, `breaking` → major, `feat` → minor, `fix`/`perf` → patch, everything else → nothing to release; a `version:*` label overrides. It prints the pending bump, the next version and the pull requests grouped by the bump each asks for. The AI does not pick the number; it picks the type. The number is auditable from the labels. -- **Dev builds.** Nothing about the version is stored on `main`: the three markers stay at the last released version, and the release job stamps them in the build it publishes, so there is no bump-back commit after a release. Every build that is not a release (`make dist`, `make deploy-test`, the CI artifact) stamps `Version: -dev.` where `` comes from `next-version.py` and `N` is the number of commits since the last tag, and writes the short commit into the plugin's Status › System screen. The maintainer who installs a build sees `2.1.0-dev.14` and knows a minor release is pending and which build it is. When the release ships, `2.1.0-dev.14 < 2.1.0` for PHP, so the site updates normally. +- **Dev builds.** Nothing about the version is stored on `main`: the three markers stay at the last released version, and the release job stamps them in the build it publishes, so there is no bump-back commit after a release. Every build that is not a release (`make dist`, `make deploy-test`, the one "Development build" pre-release on the moving tag `dev` (as built)) stamps `Version: -dev.` where `` comes from `next-version.py` and `N` is the number of commits since the last tag, and writes the short commit into the plugin's Status › System screen. The maintainer who installs a build sees `2.1.0-dev.14` and knows a minor release is pending and which build it is. When the release ships, `2.1.0-dev.14 < 2.1.0` for PHP, so the site updates normally. - Because the number is derived, there is no chicken and egg: the dev version and the release version come from the same labels, and an override label changes both. ## C. The release is a deployment