From b8bb205e6ed5d0f4075ba8727f600ab27e2c3c17 Mon Sep 17 00:00:00 2001 From: Jay S Date: Wed, 14 Oct 2026 15:50:48 +0100 Subject: [PATCH] ci: release via the shared dwc-plugin-workflows workflow Replace the copy-pasted release workflow with a thin caller of jaysuk/dwc-plugin-workflows (release.yml@v1). The footer script becomes .github/release-footer.md (plugin-specific notes only); the rest is generated from plugin.json by the shared workflow. scripts/release.mjs is now the shared launcher: `npm run release -- X.Y.Z` drafts release notes with Claude, pauses for review, and commits them with the version bump. ci.yml uses dwc-plugin-test-kit v0.2.9 (CI on Node 26). Co-Authored-By: Claude Sonnet 5.5 --- .github/release-footer.md | 5 ++ .github/workflows/ci.yml | 4 +- .github/workflows/release.yml | 141 ++++------------------------------ CHANGELOG.md | 2 +- scripts/changelog-section.mjs | 31 -------- scripts/release-footer.mjs | 55 ------------- scripts/release-titles.txt | 25 ++++++ scripts/release.mjs | 82 ++++++++------------ 8 files changed, 80 insertions(+), 265 deletions(-) create mode 100644 .github/release-footer.md delete mode 100644 scripts/changelog-section.mjs delete mode 100644 scripts/release-footer.mjs create mode 100644 scripts/release-titles.txt diff --git a/.github/release-footer.md b/.github/release-footer.md new file mode 100644 index 0000000..fd60242 --- /dev/null +++ b/.github/release-footer.md @@ -0,0 +1,5 @@ +> πŸ“ Find it under **Plugins β†’ Post-Processor**. +> πŸ“„ New here? The [usage guide](https://github.com/jaysuk/duet-gcode-postprocessor/blob/main/docs/usage.md) +> covers every step type, the safety rules and the scripting tiers. +> ⚠️ Preview before you apply. Overwriting always takes a backup, but reading the diff is what +> catches a pattern that matched more than you meant. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 207a7aa..e041fce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ on: jobs: test: - uses: jaysuk/dwc-plugin-test-kit/.github/workflows/dwc-plugin-ci.yml@v0.2.8 + uses: jaysuk/dwc-plugin-test-kit/.github/workflows/dwc-plugin-ci.yml@v0.2.9 with: dwc-ref: v3.7-dev - kit-ref: v0.2.8 + kit-ref: v0.2.9 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0a471d3..f36db55 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,20 +1,16 @@ name: Release -# Builds the installable plugin ZIP against DuetWebControl's source and creates a GitHub Release for -# it - no local build needed. Just cut a release: +# Cut a release: # -# npm run release -- 0.2.0 --push # bumps plugin.json+package.json, commits, tags, pushes -# (or manually: bump the versions, commit, then `git tag v0.2.0 && git push origin v0.2.0`) +# npm run release -- 1.0.1 --push # drafts release notes, bumps plugin.json+package.json, commits, tags, pushes # -# This runs automatically on the tag, builds GCodePostProcessor-.zip against DWC, and -# publishes a Release with the ZIP attached. The notes are the version's section of CHANGELOG.md (or, -# failing that, a generated Conventional-Commit changelog) plus an install footer; the tag name is -# the release title. +# The tag push runs the shared release workflow (jaysuk/dwc-plugin-workflows), which builds +# GCodePostProcessor-.zip against DWC and publishes a Release with a pun title and +# generated notes. Run it by hand (Actions -> Release -> Run workflow) for a dry run: it builds the +# ZIP as an artifact and shows the title and notes in the run summary, publishing nothing. # -# You can also run it manually (Actions -> Release -> Run workflow) to build a ZIP as an artifact, -# optionally against a different DWC ref. -# -# Adapted from duet-eddy-align's release.yml - same shape, minus its punny release-title step. +# Plugin-specific bits: scripts/release-titles.txt (the puns) and .github/release-footer.md (extra notes in +# the footer). on: push: @@ -27,121 +23,10 @@ on: default: v3.7-dev permissions: - contents: write # create the Release and upload the asset - -env: - # The Conventional-Commit changelog generator is shared across the Duet plugin family and lives in - # the runtime repo (single source of truth). Pinned to a ref; bump this one line to adopt changelog - # format changes. Source: jaysuk/dwc-plugin-runtime β†’ scripts/changelog.mjs. - RUNTIME_REF: d077f81a11bb5af9a35fe39f7b82d789eab19896 + contents: write # the shared workflow creates the Release and uploads the asset jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - with: - path: plugin - fetch-depth: 0 # full history + tags so the changelog generator can find the previous tag - fetch-tags: true - - - name: Checkout DuetWebControl - uses: actions/checkout@v5 - with: - repository: Duet3D/DuetWebControl - ref: ${{ github.event.inputs.dwc-ref || 'v3.7-dev' }} - path: DuetWebControl - - # Record exactly which DWC the ZIP is built against, for the release notes. - - name: Capture DuetWebControl build info - id: dwc - run: | - echo "version=$(node -p "require('./DuetWebControl/package.json').version")" >> "$GITHUB_OUTPUT" - echo "sha=$(git -C DuetWebControl rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" - echo "ref=${{ github.event.inputs.dwc-ref || 'v3.7-dev' }}" >> "$GITHUB_OUTPUT" - - - uses: actions/setup-node@v5 - with: { node-version: 20 } - - # The tag must match the version stamped into the ZIP/manifest, so a release can't ship a - # mismatched build. - - name: Verify tag matches plugin version - if: startsWith(github.ref, 'refs/tags/') - working-directory: plugin - run: | - VER=$(node -p "require('./plugin.json').version") - TAG="${GITHUB_REF_NAME#v}" - if [ "$VER" != "$TAG" ]; then - echo "::error::Tag '$TAG' != plugin.json version '$VER'. Bump plugin.json + package.json and re-tag." - exit 1 - fi - - - name: Install DuetWebControl - working-directory: DuetWebControl - run: npm install - - - name: Install plugin (and its bundled deps into DWC) - working-directory: plugin - run: | - npm install - # Install the plugin's runtime dependencies into DWC with their exact specs (github: specs - # used verbatim, registry specs as name@range) so the build/type-check can resolve them. - SPECS=$(node -p "Object.entries(require('./package.json').dependencies||{}).map(([k,v])=>/[:\\/]/.test(v)?v:k+'@'+v).join(' ')") - if [ -n "$SPECS" ]; then (cd ../DuetWebControl && npm install --no-save $SPECS); fi - - - name: Type-check against DWC - working-directory: plugin - env: - DWC_DIR: ${{ github.workspace }}/DuetWebControl - run: npm run typecheck - - # verify-build itself runs scripts/build-quickjs-asset.mjs first (see package.json) β€” the - # sandboxed script engine's plugin asset has to exist before dwc-plugin-verify-build packages - # the plugin's dwc/ folder into the ZIP. - - name: Build installable ZIP - working-directory: plugin - env: - DWC_DIR: ${{ github.workspace }}/DuetWebControl - run: npm run verify-build - - # Fetch the shared changelog generator from the runtime repo (single source of truth, pinned ref). - - name: Fetch shared release-notes generator - if: startsWith(github.ref, 'refs/tags/') - run: curl -fsSL "https://raw.githubusercontent.com/jaysuk/dwc-plugin-runtime/${RUNTIME_REF}/scripts/changelog.mjs" -o "$RUNNER_TEMP/changelog.mjs" - - # Release body = shared Conventional-Commit changelog + a footer with install instructions and - # the DWC version built against. - - name: Generate release notes - id: changelog - if: startsWith(github.ref, 'refs/tags/') - working-directory: plugin - env: - DWC_VERSION: ${{ steps.dwc.outputs.version }} - DWC_SHA: ${{ steps.dwc.outputs.sha }} - DWC_REF: ${{ steps.dwc.outputs.ref }} - run: | - { - echo 'body<<__CHANGELOG_EOF__' - # Prefer the hand-written CHANGELOG.md section for this version; fall back to the - # generated Conventional-Commit list when there is none. - node scripts/changelog-section.mjs "$GITHUB_REF_NAME" || node "$RUNNER_TEMP/changelog.mjs" - echo - node scripts/release-footer.mjs - echo - echo '__CHANGELOG_EOF__' - } >> "$GITHUB_OUTPUT" - - - name: Publish Release with the ZIP (tag push) - if: startsWith(github.ref, 'refs/tags/') - uses: softprops/action-gh-release@v3 - with: - draft: false # auto-publish: the notes are generated, the tag is the title - body: ${{ steps.changelog.outputs.body }} - files: plugin/GCodePostProcessor-*.zip - - - name: Upload ZIP as an artifact (manual runs) - if: github.event_name == 'workflow_dispatch' - uses: actions/upload-artifact@v5 - with: - name: GCodePostProcessor-plugin - path: plugin/GCodePostProcessor-*.zip + release: + uses: jaysuk/dwc-plugin-workflows/.github/workflows/release.yml@v1 + with: + dwc-ref: ${{ inputs.dwc-ref }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 296f5a0..e928386 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog Release notes for each version. The GitHub Release body for a version is taken from its section -here (see `scripts/changelog-section.mjs`), so write the section before tagging. Versions before +here, so write the section before tagging (`npm run release` drafts it for you). Versions before 1.3.0 are documented on the [Releases page](https://github.com/jaysuk/duet-gcode-postprocessor/releases). ## [1.3.0] - 2026-09-29 diff --git a/scripts/changelog-section.mjs b/scripts/changelog-section.mjs deleted file mode 100644 index 06d1032..0000000 --- a/scripts/changelog-section.mjs +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env node -/** - * Print one version's section of CHANGELOG.md (the text under `## [x.y.z]` up to the next `## `) - * for use as the GitHub Release body. Prints nothing and exits 1 when there is no such section, so - * the release workflow can fall back to the generated Conventional-Commit changelog. - * - * node scripts/changelog-section.mjs 1.3.0 - */ -import { readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; - -const version = (process.argv[2] ?? "").replace(/^v/, ""); -if (!version) { - console.error("Usage: node scripts/changelog-section.mjs "); - process.exit(2); -} - -const here = dirname(fileURLToPath(import.meta.url)); -const lines = readFileSync(join(here, "..", "CHANGELOG.md"), "utf8").split(/\r?\n/); -const heading = new RegExp(`^##\\s+\\[?${version.replace(/\./g, "\\.")}\\]?(\\s|$)`); - -const start = lines.findIndex((l) => heading.test(l)); -if (start < 0) process.exit(1); - -let end = lines.findIndex((l, i) => i > start && /^##\s/.test(l)); -if (end < 0) end = lines.length; - -const body = lines.slice(start + 1, end).join("\n").trim(); -if (!body) process.exit(1); -process.stdout.write(body + "\n"); diff --git a/scripts/release-footer.mjs b/scripts/release-footer.mjs deleted file mode 100644 index d0a9aa7..0000000 --- a/scripts/release-footer.mjs +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env node -/** - * Print the static footer appended to every GitHub Release body: install instructions and the - * DuetWebControl version the ZIP was built against. Ends with the machine-readable - * `dwc-plugin-update` marker that dwc-plugin-runtime's in-app update checker looks for. - * - * DWC details come from the CI build environment (the release workflow sets them after checking out - * DuetWebControl); they fall back sensibly when run locally. - * - * Adapted from duet-eddy-align's scripts/release-footer.mjs. - */ -import { readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; - -const here = dirname(fileURLToPath(import.meta.url)); -const manifest = JSON.parse(readFileSync(join(here, "..", "plugin.json"), "utf8")); -const pkgVersion = manifest.version; -const dwcVersion = process.env.DWC_VERSION || ""; - -// Resolve the manifest's dwcVersion the same way DWC's build does ("auto" -> full DWC version, -// "auto-major" -> major.minor), so the metadata matches what DWC enforces at install and what the -// update checker compares against. -function resolveDwcRequirement(value, reference) { - if (value === "auto") return reference; - if (value === "auto-major") return reference.split(".").slice(0, 2).join("."); - return value || ""; -} - -const requiredDwc = resolveDwcRequirement(manifest.dwcVersion, dwcVersion); -const dwcSha = process.env.DWC_SHA || ""; -const dwcRef = process.env.DWC_REF || "v3.7-dev"; -const dwcBuiltAgainst = dwcVersion - ? `**DuetWebControl ${dwcVersion}**${dwcSha ? ` (\`${dwcSha}\`, ref \`${dwcRef}\`)` : ` (ref \`${dwcRef}\`)`}` - : `DuetWebControl (ref \`${dwcRef}\`)`; - -const out = ` ---- - -### πŸ“¦ Install -1. Download \`GCodePostProcessor-${pkgVersion}.zip\` from the **Assets** below. -2. In DuetWebControl, go to **Settings β†’ General β†’ Plugins** and click **Install Plugin**. -3. Select the downloaded ZIP and accept the third-party-plugin prompt. -4. Reload DWC, then open **Plugins β†’ Post-Processor**. - -> πŸ”§ Built against ${dwcBuiltAgainst}. Use a DuetWebControl build at or near this version. -> πŸ“„ New here? The [usage guide](https://github.com/jaysuk/duet-gcode-postprocessor/blob/main/docs/usage.md) -> covers every step type, the safety rules and the scripting tiers. -> ⚠️ Preview before you apply. Overwriting always takes a backup, but reading the diff is what -> catches a pattern that matched more than you meant. - - -`; - -process.stdout.write(out.replace(/^\n/, "")); diff --git a/scripts/release-titles.txt b/scripts/release-titles.txt new file mode 100644 index 0000000..0ffa3e6 --- /dev/null +++ b/scripts/release-titles.txt @@ -0,0 +1,25 @@ +# GitHub Release titles for duet-gcode-postprocessor, one per line (blank lines and # comments ignored). +# Picked by version index (semver-sorted), wrapping if it runs out. G-code-editing puns - top up +# the list as it gets low (the release workflow warns when few unused titles remain). +Post Haste +Find and Replace +Regex Appeal +Line by Line +Good Post +Edit Distance +Diff Erence +Patch Notes +Dry Run +Macro Economics +Mind Your G-Code +Code Review +Postmark +Comma Chameleon +Preview Before You Apply +Capture Group Therapy +Search and Rescue +Step by Step +Backup Plan +Safe to Overwrite +Syntax Tax +Beyond the Pale Comments diff --git a/scripts/release.mjs b/scripts/release.mjs index 7aab3aa..9336bf0 100644 --- a/scripts/release.mjs +++ b/scripts/release.mjs @@ -1,57 +1,43 @@ #!/usr/bin/env node /** - * Cut a release: bump plugin.json + package.json, commit, and create the annotated version tag. The - * tag push is what triggers the CI release build (see .github/workflows/release.yml). + * Launcher for the shared release script (jaysuk/dwc-plugin-workflows). Identical in every plugin and never + * needs editing: it keeps a cached checkout of the shared repo at the floating `v1` tag, refreshes it on each + * run, and hands over to its scripts/release.mjs. So a change to the release flow reaches every plugin without + * touching the plugin repos. * - * node scripts/release.mjs 0.2.0 # bump + commit + tag (you push when ready) - * node scripts/release.mjs 0.2.0 --push # …and push the commit + tag (triggers the release) - * npm run release -- 0.2.0 [--push] + * npm run release -- 1.3.0 [--push] [--no-notes] [--yes] * - * Ported from duet-eddy-align's scripts/release.mjs, minus its release-title generator. + * Offline, the cached copy is used. DWC_PLUGIN_WORKFLOWS_DIR= runs a local checkout instead (to try a + * change before tagging it). */ -import { execFileSync } from "node:child_process"; -import { readFileSync, writeFileSync } from "node:fs"; +import { execFileSync, spawnSync } from "node:child_process"; +import { existsSync, mkdirSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; -const args = process.argv.slice(2); -const push = args.includes("--push"); -const version = args.find((a) => !a.startsWith("-")); +const REPO = "https://github.com/jaysuk/dwc-plugin-workflows.git"; +const REF = "v1"; -if (!version || !/^\d+\.\d+\.\d+(-[\w.]+)?$/.test(version)) { - console.error("Usage: node scripts/release.mjs [--push] e.g. 0.2.0"); - process.exit(1); +let dir = process.env.DWC_PLUGIN_WORKFLOWS_DIR; +if (!dir) { + dir = join(homedir(), ".cache", "dwc-plugin-workflows"); + const git = (args) => execFileSync("git", args, { stdio: "pipe", encoding: "utf8" }); + try { + if (existsSync(join(dir, ".git"))) { + git(["-C", dir, "fetch", "--quiet", "--depth", "1", "--force", "origin", `+refs/tags/${REF}:refs/tags/${REF}`]); + git(["-C", dir, "checkout", "--quiet", "--force", REF]); + } else { + mkdirSync(dir, { recursive: true }); + git(["clone", "--quiet", "--depth", "1", "--branch", REF, REPO, dir]); + } + } catch (e) { + if (!existsSync(join(dir, "scripts", "release.mjs"))) { + console.error(`Could not fetch ${REPO} (${REF}): ${e.message}`); + process.exit(1); + } + console.warn("Could not refresh the shared release scripts (offline?) - using the cached copy."); + } } -const tag = `v${version}`; -function git(argv) { - execFileSync("git", argv, { stdio: "inherit" }); -} - -// Refuse to release a dirty tree (other than the two files about to be bumped) -const dirty = execFileSync("git", ["status", "--porcelain"], { encoding: "utf8" }) - .split("\n") - .map((l) => l.slice(3)) - .filter((f) => f && f !== "plugin.json" && f !== "package.json"); -if (dirty.length) { - console.error(`Working tree has uncommitted changes; commit or stash them first:\n ${dirty.join("\n ")}`); - process.exit(1); -} - -for (const file of ["plugin.json", "package.json"]) { - const json = JSON.parse(readFileSync(file, "utf8")); - json.version = version; - writeFileSync(file, JSON.stringify(json, null, "\t") + "\n"); -} -console.log(`Bumped plugin.json + package.json -> ${version}`); - -git(["add", "plugin.json", "package.json"]); -git(["commit", "-m", `chore(release): ${tag}`]); -git(["tag", "-a", tag, "-m", `G-code Post-Processor ${tag}`]); -console.log(`Committed and tagged ${tag}`); - -if (push) { - git(["push"]); - git(["push", "origin", tag]); - console.log("Pushed. CI will build the ZIP and publish the GitHub Release."); -} else { - console.log(`\nReview, then push to trigger the release:\n git push && git push origin ${tag}`); -} +const r = spawnSync(process.execPath, [join(dir, "scripts", "release.mjs"), ...process.argv.slice(2)], { stdio: "inherit" }); +process.exit(r.status ?? 1);