From b5ba8dfa5c7ef8d393cff917071d252f836b79d2 Mon Sep 17 00:00:00 2001 From: mlischetti Date: Wed, 2 Sep 2026 09:35:08 -0300 Subject: [PATCH 1/9] docs: design release artifact publication --- ...-02-release-artifact-publication-design.md | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-02-release-artifact-publication-design.md diff --git a/docs/superpowers/specs/2026-09-02-release-artifact-publication-design.md b/docs/superpowers/specs/2026-09-02-release-artifact-publication-design.md new file mode 100644 index 00000000..1a3cc787 --- /dev/null +++ b/docs/superpowers/specs/2026-09-02-release-artifact-publication-design.md @@ -0,0 +1,100 @@ +# Release Artifact Publication Design + +**Status:** Approved +**Date:** 2026-09-02 +**Scope:** Move GitHub Release publication out of the cross-platform build jobs +in `.github/workflows/release.yml`. Preserve the existing Linux, Windows, and +macOS artifact matrix. + +## Goal + +Publish release assets from a MuleSoft-managed Ubuntu runner after all native +platform builds succeed, so the GitHub-hosted macOS runner never accesses the +GitHub Releases API and is not blocked by the organization IP allow list. + +## Background + +Release run `33542023719` built the macOS ARM64 native artifacts successfully, +then failed in the CLI composite action while `svenstaro/upload-release-action` +requested release `v1.0.37`. GitHub rejected the request because the +GitHub-hosted macOS runner IP is outside the `mulesoft` organization allow list. + +The existing `mulesoft-ubuntu` and `mulesoft-windows` jobs completed. The +normal CI workflow already demonstrates artifact-only output from every matrix +job. + +## Design + +The existing tag-triggered release workflow remains a single workflow with two +phases: + +1. A three-platform matrix builds native CLI, Python, Node, and native library + outputs. Each job uses `publish: artifact`; none uploads to a GitHub Release. +2. A non-matrix `publish-release` job runs on `mulesoft-ubuntu`, requires the + entire build matrix to succeed, downloads all produced artifacts, creates a + GitHub Release when its tag has no release, and uploads the downloaded files. + +The publishing job uses `GITHUB_TOKEN` and `permissions: contents: write`. +It runs only on a MuleSoft-managed runner, whose IP is accepted by the +organization allow list. + +## Artifact Contract + +All artifacts must have explicit, unique names that include the package version, +platform, and architecture where applicable. The artifact names must be stable +enough for the publishing job to download them without inspecting a prior job's +output. + +The publication job uploads only the packaged distribution files, not the +artifact archive wrappers. It preserves the release asset names currently +published by each composite action: + +- CLI: `dw-cli---.zip` +- Node: `dataweave-native--.tgz`, plus the Linux-produced + `dataweave-native-.tgz` meta package +- Python: the built wheel filename +- Native library: platform-specific `dwlib---` file, + plus the shared header only once + +The current Python artifact path is platform-independent. The implementation +must make Python artifact names platform-qualified so parallel matrix uploads do +not collide. The release publisher must upload each distinct asset exactly once. + +## Publish Semantics + +For a tag `v`, `publish-release` must: + +1. Download all release artifacts produced by the build matrix. +2. Test whether the matching GitHub Release exists. +3. Create it with generated notes only when absent. +4. Upload every asset with replacement enabled, making reruns idempotent. + +No platform build job may invoke `svenstaro/upload-release-action`, `gh release`, +`publish-release` from starting, so a partial release is never published. + +## Non-goals + +- Changing the set of supported operating systems or architectures. +- Replacing the GitHub-hosted macOS runner. +- Publishing to Homebrew, npm, PyPI, or any other registry. +- Modifying the manual Homebrew promotion workflow. +- Publishing a release before every platform build completes. + +## Testing + +- Validate workflow syntax with the available YAML parser and `actionlint` when + available. +- Inspect the generated workflow to verify build jobs use artifact publication + only and the sole release API consumer runs on `mulesoft-ubuntu`. +- Trigger a test tag in a controlled repository or perform a release dry run if + repository policy permits it; verify macOS build artifacts are downloaded and + attached from the Ubuntu publisher. + +## Success Criteria + +- A tag build produces artifacts on Linux, Windows, and macOS without calling + the GitHub Releases API from GitHub-hosted macOS. +- The `mulesoft-ubuntu` publisher creates the GitHub Release if it is missing. +- A rerun replaces assets on an existing release rather than failing. +- A failed build matrix prevents release creation or asset upload. +- Release assets retain their existing external filenames. From 6f41593b538e12c87c9200c2fbe7ec1bfdd95625 Mon Sep 17 00:00:00 2001 From: mlischetti Date: Wed, 2 Sep 2026 09:59:44 -0300 Subject: [PATCH 2/9] docs: clarify platform wheel release assets --- ...2026-09-02-release-artifact-publication.md | 342 ++++++++++++++++++ ...-02-release-artifact-publication-design.md | 9 +- 2 files changed, 347 insertions(+), 4 deletions(-) create mode 100644 docs/superpowers/plans/2026-09-02-release-artifact-publication.md diff --git a/docs/superpowers/plans/2026-09-02-release-artifact-publication.md b/docs/superpowers/plans/2026-09-02-release-artifact-publication.md new file mode 100644 index 00000000..01a859d8 --- /dev/null +++ b/docs/superpowers/plans/2026-09-02-release-artifact-publication.md @@ -0,0 +1,342 @@ +# Release Artifact Publication Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Publish complete tag-release assets from a MuleSoft-managed Ubuntu job after every platform build succeeds. + +**Architecture:** The existing tag-triggered matrix continues to build Linux, Windows, and macOS assets but publishes each output as an Actions artifact. A final `publish-release` job on `mulesoft-ubuntu` downloads every artifact, creates the release if absent, then uses `gh release upload --clobber` to publish assets. This is a single workflow, so failed matrix work prevents the publisher from running. + +**Tech Stack:** GitHub Actions, composite actions, GitHub CLI, bash, Python YAML parser. + +**Spec:** `docs/superpowers/specs/2026-09-02-release-artifact-publication-design.md` + +## Global Constraints + +- Keep the current Linux, Windows, and macOS build matrix; macOS remains required to build ARM64 artifacts. +- No platform build job may access the GitHub Releases API. +- The sole release publishing job runs on `mulesoft-ubuntu`. +- The publisher must create the GitHub Release only if it does not already exist. +- Reruns must replace identically named assets. +- A failed matrix build must prevent publishing a partial release. +- Preserve all externally published asset filenames. +- Keep the Homebrew promotion workflow and branch out of scope. + +--- + +### Task 1: Make composite action artifact outputs unique and release-neutral + +**Files:** +- Modify: `.github/actions/cli/action.yml:16-72` +- Modify: `.github/actions/python/action.yml:28-74` +- Modify: `.github/actions/node/action.yml:17-111` +- Modify: `.github/actions/native-lib/action.yml:12-88` + +**Interfaces:** +- Consumes: `publish` input values `none`, `artifact`, and legacy `release`. +- Produces when `publish: artifact`: one Actions artifact per release file, with a stable explicit `name`, while retaining the existing file paths and external filename inside each artifact. +- The release workflow will use only `publish: artifact`; the composite actions retain their legacy `publish: release` paths for callers outside this workflow. + +- [ ] **Step 1: Add failing workflow-contract coverage** by creating `scripts/release-artifacts.test.mjs`. + +```javascript +import { readFileSync } from "node:fs"; +import { test } from "node:test"; +import assert from "node:assert/strict"; + +const files = [ + ".github/actions/cli/action.yml", + ".github/actions/python/action.yml", + ".github/actions/node/action.yml", + ".github/actions/native-lib/action.yml", +]; + +test("artifact publishing names every uploaded release input", () => { + for (const file of files) { + const action = readFileSync(file, "utf8"); + assert.match(action, /uses: actions\/upload-artifact@v7\.0\.1/); + assert.match(action, /\n\s+name: .+/); + } +}); + +test("Python wheel artifact names retain the platform-qualified wheel filename", () => { + const action = readFileSync(".github/actions/python/action.yml", "utf8"); + assert.match(action, /path: native-lib\/python\/dist\/dataweave_native-0\.0\.1-py3-\*\.whl/); +}); +``` + +- [ ] **Step 2: Run the coverage to verify it fails.** + +Run: `node --test scripts/release-artifacts.test.mjs` + +Expected: FAIL because the CLI, Python, and Node artifact uploads lack explicit names. + +- [ ] **Step 3: Update the CLI artifact upload** so it uses an explicit Actions artifact name: + +```yaml + with: + name: cli-${{ inputs.native-version }}-${{ inputs.script-name }}-${{ inputs.arch }} + path: native-cli/build/distributions/dw-cli-${{ inputs.native-version }}-${{ inputs.script-name }}-${{ inputs.arch }}.zip + archive: false +``` + +Keep its existing staged filename unchanged. + +- [ ] **Step 4: Name the Python artifact explicitly without changing its platform-qualified wheel filename.** + +```yaml + with: + name: python-wheel-${{ inputs.native-version }}-${{ inputs.platform }} + path: native-lib/python/dist/dataweave_native-0.0.1-py3-*.whl + archive: false +``` + +Add an optional `platform` input to the Python action only to name the GitHub +Actions artifact. Do not modify `native-lib/python/setup.py`, its wheel tags, +or the wheel filename pattern; it already produces platform-specific wheel +filenames such as `manylinux2014_x86_64`, `win_amd64`, and `macosx_*_arm64`. + +- [ ] **Step 5: Name both Node artifact uploads explicitly.** + +```yaml + with: + name: node-platform-${{ inputs.native-version }}-${{ inputs.script-name }}-${{ inputs.arch }} + path: ${{ steps.node-package.outputs.platform }} + archive: false +``` + +```yaml + with: + name: node-meta-${{ inputs.native-version }} + path: ${{ steps.node-package.outputs.meta }} + archive: false +``` + +The `node-meta` upload remains Linux-only as it is today. + +- [ ] **Step 6: Verify the native-lib artifact contract remains sufficient.** Its artifact already uses `dwlib-${{ inputs.native-version }}-${{ inputs.script-name }}-${{ inputs.arch }}`. Do not add duplicate uploads. Its file payload includes `dwlib.h` on every platform, which Task 2 handles by uploading only one matching header. + +- [ ] **Step 7: Run coverage to verify it passes.** + +Run: `node --test scripts/release-artifacts.test.mjs` + +Expected: PASS. + +- [ ] **Step 8: Commit the focused artifact contract.** + +```bash +git add .github/actions/cli/action.yml .github/actions/python/action.yml .github/actions/node/action.yml .github/actions/native-lib/action.yml scripts/release-artifacts.test.mjs +git commit -m "refactor(release): publish build outputs as named artifacts" +``` + +--- + +### Task 2: Separate release creation and upload into an internal Ubuntu publisher + +**Files:** +- Modify: `.github/workflows/release.yml:1-87` +- Modify: `scripts/release-artifacts.test.mjs` + +**Interfaces:** +- Consumes: matrix artifacts named by Task 1. +- Produces: an existing or newly created GitHub Release for the pushed `v*` tag containing all external asset filenames. +- Publisher command: `gh release view "$TAG" || gh release create "$TAG" --generate-notes`, followed by `gh release upload "$TAG" ... --clobber`. + +- [ ] **Step 1: Extend the contract test with the desired workflow behavior.** + +```javascript +test("release publication happens only on internal Ubuntu after the matrix", () => { + const workflow = readFileSync(".github/workflows/release.yml", "utf8"); + assert.match(workflow, /publish: 'artifact'/); + assert.match(workflow, /publish-release:/); + assert.match(workflow, /needs: RELEASE_EXTENSION/); + assert.match(workflow, /runs-on: mulesoft-ubuntu/); + assert.match(workflow, /permissions:\n\s+contents: write/); + assert.match(workflow, /gh release view "\$TAG" \|\| gh release create "\$TAG" --generate-notes/); + assert.match(workflow, /gh release upload "\$TAG"/); + assert.doesNotMatch(workflow, /publish: 'release'/); +}); +``` + +- [ ] **Step 2: Run the contract test to verify it fails.** + +Run: `node --test scripts/release-artifacts.test.mjs` + +Expected: FAIL because `release.yml` currently uses `publish: 'release'` within matrix jobs and has no publisher job. + +- [ ] **Step 3: Grant release permission at workflow scope.** Immediately after the trigger declaration add: + +```yaml +permissions: + contents: write +``` + +- [ ] **Step 4: Change every matrix composite action invocation to `publish: 'artifact'`.** + +Remove `repo-token` and `tag` inputs from the CLI, Python, Node, and native-lib action invocations. Pass the matrix platform token to Python only for its Actions artifact name: + +```yaml + - name: Python + uses: ./.github/actions/python + with: + native-version: ${{ env.NATIVE_VERSION }} + break-system-packages: 'true' + platform: ${{ matrix.script_name }}-${{ env.ARCH }} + publish: 'artifact' +``` + +Keep all build/test steps and matrix platform fields otherwise unchanged. + +- [ ] **Step 5: Add the publisher job after the matrix job.** + +```yaml + publish-release: + needs: RELEASE_EXTENSION + runs-on: mulesoft-ubuntu + steps: + - name: Download release artifacts + uses: actions/download-artifact@v8 + with: + path: release-assets + merge-multiple: true + - name: Create release and upload assets + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + gh release view "$TAG" || gh release create "$TAG" --generate-notes + gh release upload "$TAG" release-assets/* --clobber +``` + +`needs: RELEASE_EXTENSION` makes GitHub Actions require all matrix children to succeed before this job starts. `merge-multiple: true` makes all payload files available under one directory for a single `gh release upload` call. + +- [ ] **Step 6: Add a duplicate-header guard before the upload command.** The three native library artifacts each contain `dwlib.h`, while the platform-qualified Python wheel filenames are already distinct and must all remain. Preserve exactly one header under its existing release name: + +```bash +HEADER=$(find release-assets -type f -name 'dwlib.h' -print -quit) +if [ -z "$HEADER" ]; then + echo "dwlib.h is missing" + exit 1 +fi +mv "$HEADER" "$RUNNER_TEMP/dwlib.h" +find release-assets -type f -name 'dwlib.h' -delete +mv "$RUNNER_TEMP/dwlib.h" "release-assets/dwlib-${TAG#v}.h" +``` + +This preserves the existing release filename `dwlib-.h` and leaves all +`.so`, `.dll`, `.dylib`, `.zip`, `.tgz`, and platform-qualified `.whl` files +unchanged. + +- [ ] **Step 7: Run the contract test to verify it passes.** + +Run: `node --test scripts/release-artifacts.test.mjs` + +Expected: PASS. + +- [ ] **Step 8: Validate YAML and inspect the release workflow.** + +Run: + +```bash +python3 -c "import yaml; yaml.safe_load(open('.github/workflows/release.yml')); print('YAML valid')" +git diff -- .github/workflows/release.yml +``` + +Expected: valid YAML; all matrix action invocations use `publish: 'artifact'`; the only `gh release` commands are in `publish-release` on `mulesoft-ubuntu`. + +- [ ] **Step 9: Commit the publisher job.** + +```bash +git add .github/workflows/release.yml scripts/release-artifacts.test.mjs +git commit -m "fix(release): publish assets from internal Ubuntu" +``` + +--- + +### Task 3: Final verification and release workflow review + +**Files:** +- Verify: `.github/workflows/release.yml` +- Verify: `.github/actions/cli/action.yml` +- Verify: `.github/actions/python/action.yml` +- Verify: `.github/actions/node/action.yml` +- Verify: `.github/actions/native-lib/action.yml` +- Verify: `scripts/release-artifacts.test.mjs` + +**Interfaces:** +- Verifies the complete release pipeline contract defined in the spec. + +- [ ] **Step 1: Run all release artifact contract coverage.** + +Run: `node --test scripts/release-artifacts.test.mjs` + +Expected: PASS with no failures. + +- [ ] **Step 2: Validate every modified Actions YAML file.** + +Run: + +```bash +python3 - <<'PY' +import yaml + +for path in [ + ".github/workflows/release.yml", + ".github/actions/cli/action.yml", + ".github/actions/python/action.yml", + ".github/actions/node/action.yml", + ".github/actions/native-lib/action.yml", +]: + with open(path) as source: + yaml.safe_load(source) + print(f"YAML valid: {path}") +PY +``` + +Expected: every listed file prints `YAML valid`. + +- [ ] **Step 3: Run `actionlint` if installed.** + +Run: + +```bash +if command -v actionlint >/dev/null 2>&1; then + actionlint .github/workflows/release.yml +else + echo "actionlint unavailable" +fi +``` + +Expected: no diagnostics if `actionlint` is installed; otherwise record that it is unavailable. + +- [ ] **Step 4: Review release API placement and artifact filenames.** + +Run: + +```bash +rg -n "publish: 'release'|upload-release-action|gh release|runs-on: mulesoft-ubuntu|publish-release" .github/workflows/release.yml .github/actions +git diff --check origin/master...HEAD +``` + +Expected: no `publish: 'release'` in `release.yml`; `gh release` commands only in the `publish-release` job; no whitespace errors. + +- [ ] **Step 5: Commit any verification-driven correction.** If all previous checks pass without source changes, do not create an empty commit. Otherwise: + +```bash +git add +git commit -m "fix(release): correct artifact publication" +``` + +--- + +## Spec Coverage + +| Spec requirement | Plan task | +|---|---| +| Matrix builds artifacts only | 1, 2 | +| Publisher runs on MuleSoft Ubuntu after successful matrix | 2 | +| Creates release if absent | 2 | +| Reruns replace assets | 2 | +| No GitHub Release API call from macOS | 2, 3 | +| Existing release asset filenames retained | 1, 2, 3 | +| YAML and contract verification | 3 | diff --git a/docs/superpowers/specs/2026-09-02-release-artifact-publication-design.md b/docs/superpowers/specs/2026-09-02-release-artifact-publication-design.md index 1a3cc787..ac6fcf78 100644 --- a/docs/superpowers/specs/2026-09-02-release-artifact-publication-design.md +++ b/docs/superpowers/specs/2026-09-02-release-artifact-publication-design.md @@ -52,13 +52,14 @@ published by each composite action: - CLI: `dw-cli---.zip` - Node: `dataweave-native--.tgz`, plus the Linux-produced `dataweave-native-.tgz` meta package -- Python: the built wheel filename +- Python: the existing platform-qualified built wheel filename - Native library: platform-specific `dwlib---` file, plus the shared header only once -The current Python artifact path is platform-independent. The implementation -must make Python artifact names platform-qualified so parallel matrix uploads do -not collide. The release publisher must upload each distinct asset exactly once. +The Python packaging already produces platform-qualified wheel filenames. The +publisher must retain and upload each wheel, so Linux, Windows, and macOS +customers receive their matching native-library build. The release publisher +must upload each distinct asset exactly once. ## Publish Semantics From 958a109af26073049a1ba7421590247bfded17d7 Mon Sep 17 00:00:00 2001 From: mlischetti Date: Wed, 2 Sep 2026 10:02:51 -0300 Subject: [PATCH 3/9] refactor(release): publish build outputs as named artifacts --- .github/actions/cli/action.yml | 1 + .github/actions/node/action.yml | 2 ++ .github/actions/python/action.yml | 5 +++++ scripts/release-artifacts.test.mjs | 23 +++++++++++++++++++++++ 4 files changed, 31 insertions(+) create mode 100644 scripts/release-artifacts.test.mjs diff --git a/.github/actions/cli/action.yml b/.github/actions/cli/action.yml index bccea892..2452b071 100644 --- a/.github/actions/cli/action.yml +++ b/.github/actions/cli/action.yml @@ -58,6 +58,7 @@ runs: if: inputs.publish == 'artifact' uses: actions/upload-artifact@v7.0.1 with: + name: cli-${{ inputs.native-version }}-${{ inputs.script-name }}-${{ inputs.arch }} path: native-cli/build/distributions/dw-cli-${{ inputs.native-version }}-${{ inputs.script-name }}-${{ inputs.arch }}.zip archive: false diff --git a/.github/actions/node/action.yml b/.github/actions/node/action.yml index 4d8bd007..ee48cebb 100644 --- a/.github/actions/node/action.yml +++ b/.github/actions/node/action.yml @@ -80,6 +80,7 @@ runs: if: inputs.publish == 'artifact' uses: actions/upload-artifact@v7.0.1 with: + name: node-platform-${{ inputs.native-version }}-${{ inputs.script-name }}-${{ inputs.arch }} path: ${{ steps.node-package.outputs.platform }} archive: false @@ -87,6 +88,7 @@ runs: if: inputs.publish == 'artifact' && inputs.script-name == 'linux' uses: actions/upload-artifact@v7.0.1 with: + name: node-meta-${{ inputs.native-version }} path: ${{ steps.node-package.outputs.meta }} archive: false diff --git a/.github/actions/python/action.yml b/.github/actions/python/action.yml index 345062ec..bb08e61b 100644 --- a/.github/actions/python/action.yml +++ b/.github/actions/python/action.yml @@ -29,6 +29,10 @@ inputs: description: "'none' | 'artifact' | 'release'." required: false default: 'none' + platform: + description: Platform token for artifact names. + required: false + default: '' repo-token: description: Token for svenstaro release upload (publish=release). required: false @@ -60,6 +64,7 @@ runs: if: inputs.publish == 'artifact' uses: actions/upload-artifact@v7.0.1 with: + name: python-wheel-${{ inputs.native-version }}-${{ inputs.platform }} path: native-lib/python/dist/dataweave_native-0.0.1-py3-*.whl archive: false diff --git a/scripts/release-artifacts.test.mjs b/scripts/release-artifacts.test.mjs new file mode 100644 index 00000000..0947dceb --- /dev/null +++ b/scripts/release-artifacts.test.mjs @@ -0,0 +1,23 @@ +import { readFileSync } from "node:fs"; +import assert from "node:assert/strict"; +import { test } from "node:test"; + +const files = [ + ".github/actions/cli/action.yml", + ".github/actions/python/action.yml", + ".github/actions/node/action.yml", + ".github/actions/native-lib/action.yml", +]; + +test("artifact publishing names every uploaded release input", () => { + for (const file of files) { + const action = readFileSync(file, "utf8"); + assert.match(action, /uses: actions\/upload-artifact@v7\.0\.1/); + assert.match(action, /\n\s+name: .+/); + } +}); + +test("Python wheel artifact retains its platform-qualified wheel filename", () => { + const action = readFileSync(".github/actions/python/action.yml", "utf8"); + assert.match(action, /path: native-lib\/python\/dist\/dataweave_native-0\.0\.1-py3-\*\.whl/); +}); From 5fba42cdcacfcf76eb021a2617191e0b53110779 Mon Sep 17 00:00:00 2001 From: mlischetti Date: Wed, 2 Sep 2026 10:04:15 -0300 Subject: [PATCH 4/9] fix(release): publish assets from internal Ubuntu --- .github/workflows/release.yml | 59 ++++++++++++++++++++++++------ scripts/release-artifacts.test.mjs | 12 ++++++ 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 729c3e2e..209eeeee 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,9 +50,7 @@ jobs: uses: ./.github/actions/cli with: native-version: ${{ env.NATIVE_VERSION }} - publish: 'release' - repo-token: ${{ secrets.GITHUB_TOKEN }} - tag: ${{ github.ref }} + publish: 'artifact' arch: ${{ env.ARCH }} script-name: ${{ matrix.script_name }} distro-os: ${{ matrix.distro_os }} @@ -62,17 +60,14 @@ jobs: with: native-version: ${{ env.NATIVE_VERSION }} break-system-packages: 'true' - publish: 'release' - repo-token: ${{ secrets.GITHUB_TOKEN }} - tag: ${{ github.ref }} + platform: ${{ matrix.script_name }}-${{ env.ARCH }} + publish: 'artifact' - name: Node uses: ./.github/actions/node with: native-version: ${{ env.NATIVE_VERSION }} - publish: 'release' - repo-token: ${{ secrets.GITHUB_TOKEN }} - tag: ${{ github.ref }} + publish: 'artifact' arch: ${{ env.ARCH }} script-name: ${{ matrix.script_name }} @@ -80,8 +75,48 @@ jobs: uses: ./.github/actions/native-lib with: native-version: ${{ env.NATIVE_VERSION }} - publish: 'release' - repo-token: ${{ secrets.GITHUB_TOKEN }} - tag: ${{ github.ref }} + publish: 'artifact' arch: ${{ env.ARCH }} script-name: ${{ matrix.script_name }} + + publish-release: + needs: RELEASE_EXTENSION + runs-on: mulesoft-ubuntu + permissions: + contents: write + steps: + - name: Download release artifacts + uses: actions/download-artifact@v8 + with: + path: release-assets + merge-multiple: true + - name: Create release and upload assets + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + HEADER=$(find release-assets -type f -name 'dwlib.h' -print -quit) + if [ -z "$HEADER" ]; then + echo "dwlib.h is missing" + exit 1 + fi + mv "$HEADER" "$RUNNER_TEMP/dwlib.h" + find release-assets -type f -name 'dwlib.h' -delete + mv "$RUNNER_TEMP/dwlib.h" "release-assets/dwlib-${TAG#v}.h" + gh release view "$TAG" || gh release create "$TAG" --generate-notes + shopt -s nullglob + assets=( + release-assets/*.zip + release-assets/*.whl + release-assets/*.tgz + release-assets/*.so + release-assets/*.dll + release-assets/*.dylib + release-assets/*.h + ) + if [ "${#assets[@]}" -eq 0 ]; then + echo "release artifacts are missing" + exit 1 + fi + gh release upload "$TAG" "${assets[@]}" --clobber diff --git a/scripts/release-artifacts.test.mjs b/scripts/release-artifacts.test.mjs index 0947dceb..89afa762 100644 --- a/scripts/release-artifacts.test.mjs +++ b/scripts/release-artifacts.test.mjs @@ -21,3 +21,15 @@ test("Python wheel artifact retains its platform-qualified wheel filename", () = const action = readFileSync(".github/actions/python/action.yml", "utf8"); assert.match(action, /path: native-lib\/python\/dist\/dataweave_native-0\.0\.1-py3-\*\.whl/); }); + +test("release publication happens only on internal Ubuntu after the matrix", () => { + const workflow = readFileSync(".github/workflows/release.yml", "utf8"); + assert.match(workflow, /publish: 'artifact'/); + assert.match(workflow, /publish-release:/); + assert.match(workflow, /needs: RELEASE_EXTENSION/); + assert.match(workflow, /runs-on: mulesoft-ubuntu/); + assert.match(workflow, /publish-release:\n(?:.|\n)*?permissions:\n\s+contents: write/); + assert.match(workflow, /gh release view "\$TAG" \|\| gh release create "\$TAG" --generate-notes/); + assert.match(workflow, /gh release upload "\$TAG"/); + assert.doesNotMatch(workflow, /publish: 'release'/); +}); From a1bae92a1ee03596310524b79b8caa9399a74d06 Mon Sep 17 00:00:00 2001 From: mlischetti Date: Wed, 2 Sep 2026 10:17:57 -0300 Subject: [PATCH 5/9] refactor(release): remove direct release publishing --- .github/actions/cli/action.yml | 23 +------- .github/actions/native-lib/action.yml | 53 +------------------ .github/actions/node/action.yml | 35 ++---------- .github/actions/python/action.yml | 30 ++--------- ...2026-09-02-release-artifact-publication.md | 4 +- ...-02-release-artifact-publication-design.md | 4 +- scripts/release-artifacts.test.mjs | 10 ++++ 7 files changed, 26 insertions(+), 133 deletions(-) diff --git a/.github/actions/cli/action.yml b/.github/actions/cli/action.yml index 2452b071..b8565bf2 100644 --- a/.github/actions/cli/action.yml +++ b/.github/actions/cli/action.yml @@ -2,8 +2,7 @@ name: CLI artifact description: >- Produces the DataWeave `dw` CLI distro zip; optionally runs the master-only native-cli regression/TCK suites; optionally publishes the distro as a CI - artifact (main) or a release asset (release). Requires build-foundation to - have run earlier in the same job. + artifact. Requires build-foundation to have run earlier in the same job. inputs: native-version: description: -PnativeVersion value; empty omits the flag. @@ -14,13 +13,9 @@ inputs: required: false default: 'false' publish: - description: "'none' | 'artifact' | 'release'." + description: "'none' | 'artifact'." required: false default: 'none' - repo-token: - description: Token for svenstaro release upload (publish=release). - required: false - default: '' arch: description: Runtime arch token for artifact/asset names. required: false @@ -33,10 +28,6 @@ inputs: description: Gradle distro classifier (linux/windows/osx) for the source zip name. required: false default: '' - tag: - description: Release tag (publish=release). - required: false - default: '' runs: using: composite steps: @@ -61,13 +52,3 @@ runs: name: cli-${{ inputs.native-version }}-${{ inputs.script-name }}-${{ inputs.arch }} path: native-cli/build/distributions/dw-cli-${{ inputs.native-version }}-${{ inputs.script-name }}-${{ inputs.arch }}.zip archive: false - - - name: Upload binaries to release - if: inputs.publish == 'release' - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ inputs.repo-token }} - file: native-cli/build/distributions/native-cli-${{ inputs.native-version }}-native-distro-${{ inputs.distro-os }}.zip - asset_name: dw-cli-${{ inputs.native-version }}-${{ inputs.script-name }}-${{ inputs.arch }}.zip - tag: ${{ inputs.tag }} - overwrite: true diff --git a/.github/actions/native-lib/action.yml b/.github/actions/native-lib/action.yml index e644da2b..7e42868d 100644 --- a/.github/actions/native-lib/action.yml +++ b/.github/actions/native-lib/action.yml @@ -2,21 +2,16 @@ name: Native shared library artifact description: >- Stages and publishes the raw dwlib shared library (dwlib.so/.dll/.dylib) and its C header (dwlib.h) — the FFI surface both the Python and Node bindings - embed, published here as standalone downloads. Requires build-foundation - earlier in the same job. + embed — as CI artifacts. Requires build-foundation earlier in the same job. inputs: native-version: description: -PnativeVersion value; empty omits the flag. required: false default: '' publish: - description: "'none' | 'artifact' | 'release'." + description: "'none' | 'artifact'." required: false default: 'none' - repo-token: - description: Token for svenstaro release upload (publish=release). - required: false - default: '' arch: description: Runtime arch token for artifact/asset names. required: false @@ -25,10 +20,6 @@ inputs: description: OS naming token (linux/windows/macos) for artifact/asset names. required: false default: '' - tag: - description: Release tag (publish=release). - required: false - default: '' runs: using: composite steps: @@ -46,43 +37,3 @@ runs: native-lib/python/src/dataweave/native/dwlib.so native-lib/python/src/dataweave/native/dwlib.dll native-lib/python/src/dataweave/native/dwlib.h - - - name: Upload native shared library to release (Linux) - if: inputs.publish == 'release' && runner.os == 'Linux' - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ inputs.repo-token }} - file: native-lib/python/src/dataweave/native/dwlib.so - asset_name: dwlib-${{ inputs.native-version }}-${{ inputs.script-name }}-${{ inputs.arch }}.so - tag: ${{ inputs.tag }} - overwrite: true - - - name: Upload native shared library to release (Windows) - if: inputs.publish == 'release' && runner.os == 'Windows' - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ inputs.repo-token }} - file: native-lib/python/src/dataweave/native/dwlib.dll - asset_name: dwlib-${{ inputs.native-version }}-${{ inputs.script-name }}-${{ inputs.arch }}.dll - tag: ${{ inputs.tag }} - overwrite: true - - - name: Upload native shared library to release (macOS) - if: inputs.publish == 'release' && runner.os == 'macOS' - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ inputs.repo-token }} - file: native-lib/python/src/dataweave/native/dwlib.dylib - asset_name: dwlib-${{ inputs.native-version }}-${{ inputs.script-name }}-${{ inputs.arch }}.dylib - tag: ${{ inputs.tag }} - overwrite: true - - - name: Upload native library header to release - if: inputs.publish == 'release' - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ inputs.repo-token }} - file: native-lib/python/src/dataweave/native/dwlib.h - asset_name: dwlib-${{ inputs.native-version }}.h - tag: ${{ inputs.tag }} - overwrite: true diff --git a/.github/actions/node/action.yml b/.github/actions/node/action.yml index ee48cebb..9780ef6e 100644 --- a/.github/actions/node/action.yml +++ b/.github/actions/node/action.yml @@ -2,9 +2,8 @@ name: Node artifact description: >- Sets up Node.js, builds the DataWeave Node package (which embeds dwlib), and runs the Node unit/integration tests (always). Optionally runs the master-only - Node TCK conformance lane, and optionally publishes the .tgz as a CI artifact - (main) or a release asset (release). Requires build-foundation earlier in the - same job. + Node TCK conformance lane and optionally publishes the .tgz as a CI artifact. + Requires build-foundation earlier in the same job. inputs: native-version: description: -PnativeVersion value; empty omits the flag. @@ -15,13 +14,9 @@ inputs: required: false default: 'false' publish: - description: "'none' | 'artifact' | 'release'." + description: "'none' | 'artifact'." required: false default: 'none' - repo-token: - description: Token for svenstaro release upload (publish=release). - required: false - default: '' arch: description: Runtime arch token for artifact/asset names. required: false @@ -30,10 +25,6 @@ inputs: description: OS naming token (linux/windows/macos) for artifact/asset names. required: false default: '' - tag: - description: Release tag (publish=release). - required: false - default: '' runs: using: composite steps: @@ -91,23 +82,3 @@ runs: name: node-meta-${{ inputs.native-version }} path: ${{ steps.node-package.outputs.meta }} archive: false - - - name: Upload platform Node package to release - if: inputs.publish == 'release' - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ inputs.repo-token }} - file: ${{ steps.node-package.outputs.platform }} - asset_name: ${{ steps.node-package.outputs.platform_name }} - tag: ${{ inputs.tag }} - overwrite: true - - - name: Upload meta Node package to release - if: inputs.publish == 'release' && inputs.script-name == 'linux' - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ inputs.repo-token }} - file: ${{ steps.node-package.outputs.meta }} - asset_name: ${{ steps.node-package.outputs.meta_name }} - tag: ${{ inputs.tag }} - overwrite: true diff --git a/.github/actions/python/action.yml b/.github/actions/python/action.yml index bb08e61b..73d4b7d6 100644 --- a/.github/actions/python/action.yml +++ b/.github/actions/python/action.yml @@ -3,8 +3,8 @@ description: >- Installs Python build dependencies and builds the DataWeave Python wheel (which embeds dwlib), runs the Python unit/integration tests, and optionally runs the master-only Python TCK conformance lane before publishing the wheel - as a CI artifact (main) or a release asset (release). Requires - build-foundation to have run earlier in the same job. + as a CI artifact. Requires build-foundation to have run earlier in the same + job. inputs: native-version: description: -PnativeVersion value; empty omits the flag. @@ -22,25 +22,13 @@ inputs: required: false default: 'false' platform: - description: Platform token for matrix-qualified TCK JUnit artifact names. + description: Platform token for CI artifact names. required: false default: '' publish: - description: "'none' | 'artifact' | 'release'." + description: "'none' | 'artifact'." required: false default: 'none' - platform: - description: Platform token for artifact names. - required: false - default: '' - repo-token: - description: Token for svenstaro release upload (publish=release). - required: false - default: '' - tag: - description: Release tag (publish=release). - required: false - default: '' runs: using: composite steps: @@ -68,16 +56,6 @@ runs: path: native-lib/python/dist/dataweave_native-0.0.1-py3-*.whl archive: false - - name: Upload Python wheel to release - if: inputs.publish == 'release' - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ inputs.repo-token }} - file: native-lib/python/dist/dataweave_native-0.0.1-py3-none-*.whl - file_glob: true - tag: ${{ inputs.tag }} - overwrite: true - - name: Run Python TCK Conformance if: always() && inputs.run-tck == 'true' run: ./gradlew --stacktrace --no-problems-report native-lib:pythonTck diff --git a/docs/superpowers/plans/2026-09-02-release-artifact-publication.md b/docs/superpowers/plans/2026-09-02-release-artifact-publication.md index 01a859d8..ff4024fc 100644 --- a/docs/superpowers/plans/2026-09-02-release-artifact-publication.md +++ b/docs/superpowers/plans/2026-09-02-release-artifact-publication.md @@ -32,9 +32,9 @@ - Modify: `.github/actions/native-lib/action.yml:12-88` **Interfaces:** -- Consumes: `publish` input values `none`, `artifact`, and legacy `release`. +- Consumes: `publish` input values `none` and `artifact`. - Produces when `publish: artifact`: one Actions artifact per release file, with a stable explicit `name`, while retaining the existing file paths and external filename inside each artifact. -- The release workflow will use only `publish: artifact`; the composite actions retain their legacy `publish: release` paths for callers outside this workflow. +- Removes `publish: release`, `repo-token`, and `tag` from all four composite actions because no workflow invokes them. - [ ] **Step 1: Add failing workflow-contract coverage** by creating `scripts/release-artifacts.test.mjs`. diff --git a/docs/superpowers/specs/2026-09-02-release-artifact-publication-design.md b/docs/superpowers/specs/2026-09-02-release-artifact-publication-design.md index ac6fcf78..e5cb9edb 100644 --- a/docs/superpowers/specs/2026-09-02-release-artifact-publication-design.md +++ b/docs/superpowers/specs/2026-09-02-release-artifact-publication-design.md @@ -70,7 +70,9 @@ For a tag `v`, `publish-release` must: 3. Create it with generated notes only when absent. 4. Upload every asset with replacement enabled, making reruns idempotent. -No platform build job may invoke `svenstaro/upload-release-action`, `gh release`, +The composite actions expose only `none` and `artifact` publication modes. No +platform build job may invoke `svenstaro/upload-release-action`, `gh release`, +or any GitHub Releases API endpoint. A failed matrix job prevents `publish-release` from starting, so a partial release is never published. ## Non-goals diff --git a/scripts/release-artifacts.test.mjs b/scripts/release-artifacts.test.mjs index 89afa762..3ebe668c 100644 --- a/scripts/release-artifacts.test.mjs +++ b/scripts/release-artifacts.test.mjs @@ -22,6 +22,16 @@ test("Python wheel artifact retains its platform-qualified wheel filename", () = assert.match(action, /path: native-lib\/python\/dist\/dataweave_native-0\.0\.1-py3-\*\.whl/); }); +test("composite actions do not expose direct release publishing", () => { + for (const file of files) { + const action = readFileSync(file, "utf8"); + assert.doesNotMatch(action, /'release'/); + assert.doesNotMatch(action, /repo-token:/); + assert.doesNotMatch(action, /tag:/); + assert.doesNotMatch(action, /svenstaro\/upload-release-action/); + } +}); + test("release publication happens only on internal Ubuntu after the matrix", () => { const workflow = readFileSync(".github/workflows/release.yml", "utf8"); assert.match(workflow, /publish: 'artifact'/); From c0da1824cdce814c14d4abc318d46791ac6907e9 Mon Sep 17 00:00:00 2001 From: mlischetti Date: Wed, 2 Sep 2026 10:25:45 -0300 Subject: [PATCH 6/9] docs: remove release publication plan --- ...2026-09-02-release-artifact-publication.md | 342 ------------------ 1 file changed, 342 deletions(-) delete mode 100644 docs/superpowers/plans/2026-09-02-release-artifact-publication.md diff --git a/docs/superpowers/plans/2026-09-02-release-artifact-publication.md b/docs/superpowers/plans/2026-09-02-release-artifact-publication.md deleted file mode 100644 index ff4024fc..00000000 --- a/docs/superpowers/plans/2026-09-02-release-artifact-publication.md +++ /dev/null @@ -1,342 +0,0 @@ -# Release Artifact Publication Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Publish complete tag-release assets from a MuleSoft-managed Ubuntu job after every platform build succeeds. - -**Architecture:** The existing tag-triggered matrix continues to build Linux, Windows, and macOS assets but publishes each output as an Actions artifact. A final `publish-release` job on `mulesoft-ubuntu` downloads every artifact, creates the release if absent, then uses `gh release upload --clobber` to publish assets. This is a single workflow, so failed matrix work prevents the publisher from running. - -**Tech Stack:** GitHub Actions, composite actions, GitHub CLI, bash, Python YAML parser. - -**Spec:** `docs/superpowers/specs/2026-09-02-release-artifact-publication-design.md` - -## Global Constraints - -- Keep the current Linux, Windows, and macOS build matrix; macOS remains required to build ARM64 artifacts. -- No platform build job may access the GitHub Releases API. -- The sole release publishing job runs on `mulesoft-ubuntu`. -- The publisher must create the GitHub Release only if it does not already exist. -- Reruns must replace identically named assets. -- A failed matrix build must prevent publishing a partial release. -- Preserve all externally published asset filenames. -- Keep the Homebrew promotion workflow and branch out of scope. - ---- - -### Task 1: Make composite action artifact outputs unique and release-neutral - -**Files:** -- Modify: `.github/actions/cli/action.yml:16-72` -- Modify: `.github/actions/python/action.yml:28-74` -- Modify: `.github/actions/node/action.yml:17-111` -- Modify: `.github/actions/native-lib/action.yml:12-88` - -**Interfaces:** -- Consumes: `publish` input values `none` and `artifact`. -- Produces when `publish: artifact`: one Actions artifact per release file, with a stable explicit `name`, while retaining the existing file paths and external filename inside each artifact. -- Removes `publish: release`, `repo-token`, and `tag` from all four composite actions because no workflow invokes them. - -- [ ] **Step 1: Add failing workflow-contract coverage** by creating `scripts/release-artifacts.test.mjs`. - -```javascript -import { readFileSync } from "node:fs"; -import { test } from "node:test"; -import assert from "node:assert/strict"; - -const files = [ - ".github/actions/cli/action.yml", - ".github/actions/python/action.yml", - ".github/actions/node/action.yml", - ".github/actions/native-lib/action.yml", -]; - -test("artifact publishing names every uploaded release input", () => { - for (const file of files) { - const action = readFileSync(file, "utf8"); - assert.match(action, /uses: actions\/upload-artifact@v7\.0\.1/); - assert.match(action, /\n\s+name: .+/); - } -}); - -test("Python wheel artifact names retain the platform-qualified wheel filename", () => { - const action = readFileSync(".github/actions/python/action.yml", "utf8"); - assert.match(action, /path: native-lib\/python\/dist\/dataweave_native-0\.0\.1-py3-\*\.whl/); -}); -``` - -- [ ] **Step 2: Run the coverage to verify it fails.** - -Run: `node --test scripts/release-artifacts.test.mjs` - -Expected: FAIL because the CLI, Python, and Node artifact uploads lack explicit names. - -- [ ] **Step 3: Update the CLI artifact upload** so it uses an explicit Actions artifact name: - -```yaml - with: - name: cli-${{ inputs.native-version }}-${{ inputs.script-name }}-${{ inputs.arch }} - path: native-cli/build/distributions/dw-cli-${{ inputs.native-version }}-${{ inputs.script-name }}-${{ inputs.arch }}.zip - archive: false -``` - -Keep its existing staged filename unchanged. - -- [ ] **Step 4: Name the Python artifact explicitly without changing its platform-qualified wheel filename.** - -```yaml - with: - name: python-wheel-${{ inputs.native-version }}-${{ inputs.platform }} - path: native-lib/python/dist/dataweave_native-0.0.1-py3-*.whl - archive: false -``` - -Add an optional `platform` input to the Python action only to name the GitHub -Actions artifact. Do not modify `native-lib/python/setup.py`, its wheel tags, -or the wheel filename pattern; it already produces platform-specific wheel -filenames such as `manylinux2014_x86_64`, `win_amd64`, and `macosx_*_arm64`. - -- [ ] **Step 5: Name both Node artifact uploads explicitly.** - -```yaml - with: - name: node-platform-${{ inputs.native-version }}-${{ inputs.script-name }}-${{ inputs.arch }} - path: ${{ steps.node-package.outputs.platform }} - archive: false -``` - -```yaml - with: - name: node-meta-${{ inputs.native-version }} - path: ${{ steps.node-package.outputs.meta }} - archive: false -``` - -The `node-meta` upload remains Linux-only as it is today. - -- [ ] **Step 6: Verify the native-lib artifact contract remains sufficient.** Its artifact already uses `dwlib-${{ inputs.native-version }}-${{ inputs.script-name }}-${{ inputs.arch }}`. Do not add duplicate uploads. Its file payload includes `dwlib.h` on every platform, which Task 2 handles by uploading only one matching header. - -- [ ] **Step 7: Run coverage to verify it passes.** - -Run: `node --test scripts/release-artifacts.test.mjs` - -Expected: PASS. - -- [ ] **Step 8: Commit the focused artifact contract.** - -```bash -git add .github/actions/cli/action.yml .github/actions/python/action.yml .github/actions/node/action.yml .github/actions/native-lib/action.yml scripts/release-artifacts.test.mjs -git commit -m "refactor(release): publish build outputs as named artifacts" -``` - ---- - -### Task 2: Separate release creation and upload into an internal Ubuntu publisher - -**Files:** -- Modify: `.github/workflows/release.yml:1-87` -- Modify: `scripts/release-artifacts.test.mjs` - -**Interfaces:** -- Consumes: matrix artifacts named by Task 1. -- Produces: an existing or newly created GitHub Release for the pushed `v*` tag containing all external asset filenames. -- Publisher command: `gh release view "$TAG" || gh release create "$TAG" --generate-notes`, followed by `gh release upload "$TAG" ... --clobber`. - -- [ ] **Step 1: Extend the contract test with the desired workflow behavior.** - -```javascript -test("release publication happens only on internal Ubuntu after the matrix", () => { - const workflow = readFileSync(".github/workflows/release.yml", "utf8"); - assert.match(workflow, /publish: 'artifact'/); - assert.match(workflow, /publish-release:/); - assert.match(workflow, /needs: RELEASE_EXTENSION/); - assert.match(workflow, /runs-on: mulesoft-ubuntu/); - assert.match(workflow, /permissions:\n\s+contents: write/); - assert.match(workflow, /gh release view "\$TAG" \|\| gh release create "\$TAG" --generate-notes/); - assert.match(workflow, /gh release upload "\$TAG"/); - assert.doesNotMatch(workflow, /publish: 'release'/); -}); -``` - -- [ ] **Step 2: Run the contract test to verify it fails.** - -Run: `node --test scripts/release-artifacts.test.mjs` - -Expected: FAIL because `release.yml` currently uses `publish: 'release'` within matrix jobs and has no publisher job. - -- [ ] **Step 3: Grant release permission at workflow scope.** Immediately after the trigger declaration add: - -```yaml -permissions: - contents: write -``` - -- [ ] **Step 4: Change every matrix composite action invocation to `publish: 'artifact'`.** - -Remove `repo-token` and `tag` inputs from the CLI, Python, Node, and native-lib action invocations. Pass the matrix platform token to Python only for its Actions artifact name: - -```yaml - - name: Python - uses: ./.github/actions/python - with: - native-version: ${{ env.NATIVE_VERSION }} - break-system-packages: 'true' - platform: ${{ matrix.script_name }}-${{ env.ARCH }} - publish: 'artifact' -``` - -Keep all build/test steps and matrix platform fields otherwise unchanged. - -- [ ] **Step 5: Add the publisher job after the matrix job.** - -```yaml - publish-release: - needs: RELEASE_EXTENSION - runs-on: mulesoft-ubuntu - steps: - - name: Download release artifacts - uses: actions/download-artifact@v8 - with: - path: release-assets - merge-multiple: true - - name: Create release and upload assets - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG: ${{ github.ref_name }} - run: | - set -euo pipefail - gh release view "$TAG" || gh release create "$TAG" --generate-notes - gh release upload "$TAG" release-assets/* --clobber -``` - -`needs: RELEASE_EXTENSION` makes GitHub Actions require all matrix children to succeed before this job starts. `merge-multiple: true` makes all payload files available under one directory for a single `gh release upload` call. - -- [ ] **Step 6: Add a duplicate-header guard before the upload command.** The three native library artifacts each contain `dwlib.h`, while the platform-qualified Python wheel filenames are already distinct and must all remain. Preserve exactly one header under its existing release name: - -```bash -HEADER=$(find release-assets -type f -name 'dwlib.h' -print -quit) -if [ -z "$HEADER" ]; then - echo "dwlib.h is missing" - exit 1 -fi -mv "$HEADER" "$RUNNER_TEMP/dwlib.h" -find release-assets -type f -name 'dwlib.h' -delete -mv "$RUNNER_TEMP/dwlib.h" "release-assets/dwlib-${TAG#v}.h" -``` - -This preserves the existing release filename `dwlib-.h` and leaves all -`.so`, `.dll`, `.dylib`, `.zip`, `.tgz`, and platform-qualified `.whl` files -unchanged. - -- [ ] **Step 7: Run the contract test to verify it passes.** - -Run: `node --test scripts/release-artifacts.test.mjs` - -Expected: PASS. - -- [ ] **Step 8: Validate YAML and inspect the release workflow.** - -Run: - -```bash -python3 -c "import yaml; yaml.safe_load(open('.github/workflows/release.yml')); print('YAML valid')" -git diff -- .github/workflows/release.yml -``` - -Expected: valid YAML; all matrix action invocations use `publish: 'artifact'`; the only `gh release` commands are in `publish-release` on `mulesoft-ubuntu`. - -- [ ] **Step 9: Commit the publisher job.** - -```bash -git add .github/workflows/release.yml scripts/release-artifacts.test.mjs -git commit -m "fix(release): publish assets from internal Ubuntu" -``` - ---- - -### Task 3: Final verification and release workflow review - -**Files:** -- Verify: `.github/workflows/release.yml` -- Verify: `.github/actions/cli/action.yml` -- Verify: `.github/actions/python/action.yml` -- Verify: `.github/actions/node/action.yml` -- Verify: `.github/actions/native-lib/action.yml` -- Verify: `scripts/release-artifacts.test.mjs` - -**Interfaces:** -- Verifies the complete release pipeline contract defined in the spec. - -- [ ] **Step 1: Run all release artifact contract coverage.** - -Run: `node --test scripts/release-artifacts.test.mjs` - -Expected: PASS with no failures. - -- [ ] **Step 2: Validate every modified Actions YAML file.** - -Run: - -```bash -python3 - <<'PY' -import yaml - -for path in [ - ".github/workflows/release.yml", - ".github/actions/cli/action.yml", - ".github/actions/python/action.yml", - ".github/actions/node/action.yml", - ".github/actions/native-lib/action.yml", -]: - with open(path) as source: - yaml.safe_load(source) - print(f"YAML valid: {path}") -PY -``` - -Expected: every listed file prints `YAML valid`. - -- [ ] **Step 3: Run `actionlint` if installed.** - -Run: - -```bash -if command -v actionlint >/dev/null 2>&1; then - actionlint .github/workflows/release.yml -else - echo "actionlint unavailable" -fi -``` - -Expected: no diagnostics if `actionlint` is installed; otherwise record that it is unavailable. - -- [ ] **Step 4: Review release API placement and artifact filenames.** - -Run: - -```bash -rg -n "publish: 'release'|upload-release-action|gh release|runs-on: mulesoft-ubuntu|publish-release" .github/workflows/release.yml .github/actions -git diff --check origin/master...HEAD -``` - -Expected: no `publish: 'release'` in `release.yml`; `gh release` commands only in the `publish-release` job; no whitespace errors. - -- [ ] **Step 5: Commit any verification-driven correction.** If all previous checks pass without source changes, do not create an empty commit. Otherwise: - -```bash -git add -git commit -m "fix(release): correct artifact publication" -``` - ---- - -## Spec Coverage - -| Spec requirement | Plan task | -|---|---| -| Matrix builds artifacts only | 1, 2 | -| Publisher runs on MuleSoft Ubuntu after successful matrix | 2 | -| Creates release if absent | 2 | -| Reruns replace assets | 2 | -| No GitHub Release API call from macOS | 2, 3 | -| Existing release asset filenames retained | 1, 2, 3 | -| YAML and contract verification | 3 | From a4c7821a70bd4eb607b69a3c248c4ce4eb40f512 Mon Sep 17 00:00:00 2001 From: mlischetti Date: Wed, 2 Sep 2026 11:08:22 -0300 Subject: [PATCH 7/9] test(python): remove direct release upload expectation --- native-lib/python/tests/unit/test_ci_structure.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/native-lib/python/tests/unit/test_ci_structure.py b/native-lib/python/tests/unit/test_ci_structure.py index c2d32dec..7d6eacab 100644 --- a/native-lib/python/tests/unit/test_ci_structure.py +++ b/native-lib/python/tests/unit/test_ci_structure.py @@ -67,7 +67,8 @@ def test_python_artifact_owns_test_dependencies_and_tck_junit_upload(): assert "python-tck-junit-${{ inputs.platform }}" in action assert action.index("Create Native Lib Python Wheel") < action.index("Run Python TCK Conformance") assert action.index("Upload Python wheel (artifact)") < action.index("Run Python TCK Conformance") - assert action.index("Upload Python wheel to release") < action.index("Run Python TCK Conformance") + assert "Upload Python wheel to release" not in action + assert "svenstaro/upload-release-action" not in action assert action.index("Run Python TCK Conformance") < action.index("Upload Python TCK JUnit") assert named_step_if(action, "Run Python TCK Conformance") == "always() && inputs.run-tck == 'true'" assert "native-lib/build/test-results/pythonTck.xml" in action From 227d66bc7a46a260a69dc21c8a1ad3f94f683eb1 Mon Sep 17 00:00:00 2001 From: mlischetti Date: Wed, 2 Sep 2026 12:02:54 -0300 Subject: [PATCH 8/9] fix(release): retain native library asset names --- .github/workflows/release.yml | 35 ++++++++++++++++++++---------- scripts/release-artifacts.test.mjs | 16 ++++++++++++-- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 209eeeee..448ea1a3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -89,31 +89,44 @@ jobs: uses: actions/download-artifact@v8 with: path: release-assets - merge-multiple: true - name: Create release and upload assets env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ github.ref_name }} run: | set -euo pipefail - HEADER=$(find release-assets -type f -name 'dwlib.h' -print -quit) + VERSION="${TAG#v}" + HEADER=$(find "release-assets/dwlib-${VERSION}-linux-x86_64" -maxdepth 1 -name 'dwlib.h' -print -quit) if [ -z "$HEADER" ]; then echo "dwlib.h is missing" exit 1 fi mv "$HEADER" "$RUNNER_TEMP/dwlib.h" find release-assets -type f -name 'dwlib.h' -delete - mv "$RUNNER_TEMP/dwlib.h" "release-assets/dwlib-${TAG#v}.h" + mv "$RUNNER_TEMP/dwlib.h" "release-assets/dwlib-${VERSION}.h" + for platform in linux-x86_64 windows-x86_64 macos-arm64; do + case "$platform" in + linux-x86_64) extension=so ;; + windows-x86_64) extension=dll ;; + macos-arm64) extension=dylib ;; + esac + source="release-assets/dwlib-${VERSION}-${platform}/dwlib.${extension}" + if [ ! -f "$source" ]; then + echo "native library is missing: $source" + exit 1 + fi + mv "$source" "release-assets/dwlib-${VERSION}-${platform}.${extension}" + done gh release view "$TAG" || gh release create "$TAG" --generate-notes - shopt -s nullglob + shopt -s globstar nullglob assets=( - release-assets/*.zip - release-assets/*.whl - release-assets/*.tgz - release-assets/*.so - release-assets/*.dll - release-assets/*.dylib - release-assets/*.h + release-assets/**/*.zip + release-assets/**/*.whl + release-assets/**/*.tgz + release-assets/dwlib-*.so + release-assets/dwlib-*.dll + release-assets/dwlib-*.dylib + release-assets/dwlib-*.h ) if [ "${#assets[@]}" -eq 0 ]; then echo "release artifacts are missing" diff --git a/scripts/release-artifacts.test.mjs b/scripts/release-artifacts.test.mjs index 3ebe668c..f81ebe4c 100644 --- a/scripts/release-artifacts.test.mjs +++ b/scripts/release-artifacts.test.mjs @@ -12,8 +12,12 @@ const files = [ test("artifact publishing names every uploaded release input", () => { for (const file of files) { const action = readFileSync(file, "utf8"); - assert.match(action, /uses: actions\/upload-artifact@v7\.0\.1/); - assert.match(action, /\n\s+name: .+/); + const uploads = action.matchAll( + /uses: actions\/upload-artifact@v7\.0\.1\n\s+with:\n(?(?:\s+.+\n?)+)/g, + ); + for (const upload of uploads) { + assert.match(upload.groups.options, /^\s+name: .+/m); + } } }); @@ -43,3 +47,11 @@ test("release publication happens only on internal Ubuntu after the matrix", () assert.match(workflow, /gh release upload "\$TAG"/); assert.doesNotMatch(workflow, /publish: 'release'/); }); + +test("publisher retains the versioned native library release filenames", () => { + const workflow = readFileSync(".github/workflows/release.yml", "utf8"); + assert.doesNotMatch(workflow, /merge-multiple: true/); + assert.match(workflow, /dwlib-\$\{VERSION\}-\$\{platform\}\.\$\{extension\}/); + assert.match(workflow, /release-assets\/dwlib-\$\{VERSION\}-linux-x86_64/); + assert.match(workflow, /shopt -s globstar nullglob/); +}); From bf8c5837c90a1126a7772391419c5ae57dc9b65d Mon Sep 17 00:00:00 2001 From: mlischetti Date: Wed, 2 Sep 2026 12:51:30 -0300 Subject: [PATCH 9/9] fix(release): target repository explicitly --- .github/workflows/release.yml | 1 + scripts/release-artifacts.test.mjs | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 448ea1a3..ca389f45 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -92,6 +92,7 @@ jobs: - name: Create release and upload assets env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} TAG: ${{ github.ref_name }} run: | set -euo pipefail diff --git a/scripts/release-artifacts.test.mjs b/scripts/release-artifacts.test.mjs index f81ebe4c..18627df2 100644 --- a/scripts/release-artifacts.test.mjs +++ b/scripts/release-artifacts.test.mjs @@ -43,6 +43,7 @@ test("release publication happens only on internal Ubuntu after the matrix", () assert.match(workflow, /needs: RELEASE_EXTENSION/); assert.match(workflow, /runs-on: mulesoft-ubuntu/); assert.match(workflow, /publish-release:\n(?:.|\n)*?permissions:\n\s+contents: write/); + assert.match(workflow, /GH_REPO: \$\{\{ github\.repository \}\}/); assert.match(workflow, /gh release view "\$TAG" \|\| gh release create "\$TAG" --generate-notes/); assert.match(workflow, /gh release upload "\$TAG"/); assert.doesNotMatch(workflow, /publish: 'release'/);