From 0fd79e8540ef10d4d686484ae69e2c9defa4cb1f Mon Sep 17 00:00:00 2001 From: Snider Date: Wed, 29 Jul 2026 04:42:31 +0100 Subject: [PATCH] fix(wails3): drive the stack with the wails3 CLI, not a Taskfile target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task (taskfile.dev) is its own build orchestrator and no part of Wails. Threading a Taskfile target name through this action put another project's tool in the Wails stack's vocabulary, and made the caller name an internal detail — `darwin:package` — that Wails already has a verb for. The premise in the old description was wrong: "there is no `wails build` invocation to compose". There is. `wails3 build` and `wails3 package` are both real, and internally they route through the project's own build description with GOOS passed in, so the CLI already does the dispatch this action was reaching past it to do by hand. The input is now `wails3-command`, taking `build` or `package` and nothing else — an unknown verb fails here, naming the caller, rather than reaching wails3 as a subcommand it does not have. That also drops the step installing the Task binary: wails3 runs the build through its own embedded runner, so nothing here needs task on PATH. The contract check gains the case that let this ship broken. A composite action referencing ${{ inputs.X }} it does not itself declare gets the empty string, silently — and the forwarding check could not see it, because it only asks whether a name a file declares is passed on. Forwarding an input the file never declared satisfies it while passing nothing, which is how the previous attempt at this repaired one hop of the chain, reported sound, and stayed severed at the next. Co-Authored-By: Virgil --- action.yml | 11 ++-- actions/action.yml | 15 +++--- actions/build/wails3/action.yml | 8 +-- actions/build/wails3/build/action.yml | 73 ++++++++++++--------------- tests/action_contracts.py | 15 ++++++ 5 files changed, 67 insertions(+), 55 deletions(-) diff --git a/action.yml b/action.yml index 3d80f6e..e012c71 100644 --- a/action.yml +++ b/action.yml @@ -63,13 +63,13 @@ inputs: description: "Base name for release assets, before the -- suffix" required: false default: "" - task: + wails3-command: description: > - Wails v3 stack: the Taskfile target to run. Empty runs :build, which - produces a bare binary — a project wanting the bundle or an installer - names its package target here. + Wails v3 stack: which wails3 CLI verb to run — `build` for the bare + binary, `package` for the platform's installable artefact (.app, the NSIS + installer, the Linux packages). required: false - default: "" + default: "build" entry: description: > Deno stack: entry module to compile. Empty probes for run.ts, main.ts, @@ -139,6 +139,7 @@ runs: app-working-directory: ${{ inputs.app-working-directory }} go-directory: ${{ inputs.go-directory }} release-asset-prefix: ${{ inputs.release-asset-prefix }} + wails3-command: ${{ inputs.wails3-command }} entry: ${{ inputs.entry }} permissions: ${{ inputs.permissions }} deb: ${{ inputs.deb }} diff --git a/actions/action.yml b/actions/action.yml index 2b7f3a1..5db31f4 100644 --- a/actions/action.yml +++ b/actions/action.yml @@ -64,6 +64,10 @@ inputs: description: "Base name for release assets, before the -- suffix" required: false default: "" + wails3-command: + description: "Wails v3 stack: the wails3 CLI verb — build or package." + required: false + default: "build" entry: description: "Deno stack: entry module to compile. Empty probes for the usual names." required: false @@ -172,12 +176,11 @@ runs: build-name: ${{ inputs.build-name }} app-working-directory: ${{ inputs.app-working-directory }} release-asset-prefix: ${{ inputs.release-asset-prefix }} - # The v3 stack runs the project's own Taskfile target, so which target - # is the whole configuration. Unforwarded it defaulted to :build on - # every caller — a caller asking for a package target got a bare binary - # and no error, because Actions ignores a `with:` key the action does - # not declare. - task: ${{ inputs.task }} + # build gives the bare binary, package the installable artefact. A + # caller wanting the second and getting the first finds out at whatever + # step next expects a bundle, so this is the difference between a + # release that has installers and one that does not. + wails3-command: ${{ inputs.wails3-command }} - name: Call Go wrapper id: go diff --git a/actions/build/wails3/action.yml b/actions/build/wails3/action.yml index a6c2605..5c66901 100644 --- a/actions/build/wails3/action.yml +++ b/actions/build/wails3/action.yml @@ -38,10 +38,10 @@ inputs: description: "Root of the app being built" required: false default: "." - task: - description: "Taskfile target; defaults to :build from the runner" + wails3-command: + description: "The wails3 CLI verb — build for the binary, package for the installable artefact" required: false - default: "" + default: "build" wails3-tool: description: "How to invoke wails3; defaults to the module-pinned `go tool wails3`" required: false @@ -103,7 +103,7 @@ runs: build: ${{ inputs.build }} app-working-directory: ${{ inputs.app-working-directory }} build-name: ${{ inputs.build-name }} - task: ${{ inputs.task }} + wails3-command: ${{ inputs.wails3-command }} wails3-tool: ${{ inputs.wails3-tool }} - name: Sign artifacts (OS-conditional) diff --git a/actions/build/wails3/build/action.yml b/actions/build/wails3/build/action.yml index 29a9b99..1d66245 100644 --- a/actions/build/wails3/build/action.yml +++ b/actions/build/wails3/build/action.yml @@ -1,13 +1,14 @@ name: "Build Wails v3 App" description: > - Builds a Wails v3 application through its Taskfile and normalises the - executable bits per OS. + Builds or packages a Wails v3 application through the wails3 CLI and + normalises the executable bits per OS. - Wails v3 is not v2 with a new flag set. There is no `wails build` invocation - to compose: the project owns a Taskfile, wails3 runs targets in it, and the - per-platform targets are where the real build lives. So this action runs the - project's own target rather than reconstructing the command, which keeps CI - and a developer's local build the same path. + Wails v3 is not v2 with a new flag set — there are no build flags to compose, + because a v3 project describes its own build. But the CLI still owns the + verbs: `wails3 build` and `wails3 package`. Both dispatch to the project's + build description internally, which is Wails' business rather than this + action's, so this runs the verb and lets Wails decide what it means. CI and a + developer's machine then take the same path through the same tool. inputs: build: @@ -21,12 +22,13 @@ inputs: build-name: description: "Binary or bundle name, used to locate the output" required: true - task: + wails3-command: description: > - Taskfile target to run. Defaults to :build, resolved from the runner — - darwin:build, linux:build, windows:build — which is the wails3 convention. + Which wails3 CLI verb to run: `build` for the bare binary, `package` for + the platform's installable artefact — the .app bundle, the NSIS + installer, the Linux packages. required: false - default: "" + default: "build" wails3-tool: description: > How to invoke wails3. Defaults to `go tool wails3`, which resolves the @@ -37,33 +39,33 @@ inputs: default: "go tool wails3" outputs: - TASK_RUN: - description: "The Taskfile target that was executed" - value: ${{ steps.target.outputs.TASK }} + WAILS3_COMMAND: + description: "The wails3 verb that was executed" + value: ${{ steps.verb.outputs.COMMAND }} runs: using: "composite" steps: - - name: Resolve the Taskfile target - id: target + # Only the two verbs. Anything else would reach the CLI as a subcommand it + # does not have, and the failure would name wails3 rather than the caller + # that asked for it. + - name: Resolve the wails3 verb + id: verb shell: bash - working-directory: ${{ inputs.app-working-directory }} env: - REQUESTED: ${{ inputs.task }} + REQUESTED: ${{ inputs.wails3-command }} run: | set -euo pipefail - if [ -n "$REQUESTED" ]; then - TASK="$REQUESTED" - else - case "${RUNNER_OS}" in - macOS) TASK="darwin:build" ;; - Linux) TASK="linux:build" ;; - Windows) TASK="windows:build" ;; - *) echo "::error::unsupported runner OS '${RUNNER_OS}'"; exit 1 ;; - esac - fi - echo "TASK=$TASK" >> "$GITHUB_OUTPUT" - echo "[DEBUG_LOG] (wails3) target=$TASK" + COMMAND="${REQUESTED:-build}" + case "$COMMAND" in + build|package) ;; + *) + echo "::error::wails3-command must be 'build' or 'package', got '$COMMAND'" + exit 1 + ;; + esac + echo "COMMAND=$COMMAND" >> "$GITHUB_OUTPUT" + echo "[DEBUG_LOG] (wails3) command=$COMMAND" # A project that pins wails3 with a go.mod tool directive builds with the # version it was tested against. Falling back to an install is for projects @@ -86,20 +88,11 @@ runs: go install github.com/wailsapp/wails/v3/cmd/wails3@latest fi - - name: Install Task - shell: bash - run: | - set -euo pipefail - if ! command -v task >/dev/null 2>&1; then - go install github.com/go-task/task/v3/cmd/task@latest - echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" - fi - - name: Build if: inputs.build == 'true' shell: bash working-directory: ${{ inputs.app-working-directory }} - run: ${{ inputs.wails3-tool }} task ${{ steps.target.outputs.TASK }} + run: ${{ inputs.wails3-tool }} ${{ steps.verb.outputs.COMMAND }} # wails3 writes to bin/. The bundle layout differs per platform, so the # executable bit is set where each platform actually keeps the binary. diff --git a/tests/action_contracts.py b/tests/action_contracts.py index 4ffa68e..f4a6edc 100755 --- a/tests/action_contracts.py +++ b/tests/action_contracts.py @@ -108,6 +108,21 @@ def main() -> int: # Inputs need a description or the marketplace listing renders blanks. declared = (doc.get("inputs") or {}) + + # ${{ inputs.X }} where X is not declared in *this* file resolves to the + # empty string. Nothing warns — not the runner, not the caller, not the + # forwarding check above, which only asks whether a name a file declares + # gets passed on. Forwarding an input the file never declared satisfies + # it while passing nothing, so a chain can be repaired at one hop, report + # sound, and still be severed at the next. That is how `task` was added + # to the root action and forwarded from the orchestrator, and every + # Wails v3 build carried on running its default target. + for i, line in enumerate(path.read_text().split("\n"), 1): + for ref in re.findall(r"inputs\.([A-Za-z0-9_-]+)", line): + if ref not in declared: + problems.append( + f"{rel}:{i}: references inputs.{ref}, which this file does " + f"not declare — it will always be empty") for name, spec in declared.items(): if not isinstance(spec, dict) or not spec.get("description"): problems.append(f"{rel}: input {name} has no description")