From 1e5a232044f19a51c9de6c079fce766b5fe1ee05 Mon Sep 17 00:00:00 2001 From: Kelindi Date: Mon, 20 Jul 2026 13:48:26 -0400 Subject: [PATCH] Add custom release artifact names --- README.md | 5 +++ action.yml | 32 +++---------- scripts/release.sh | 94 ++++++++++++++++++++++++++++++++++++++ tests/release_test.sh | 102 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 207 insertions(+), 26 deletions(-) create mode 100755 scripts/release.sh create mode 100755 tests/release_test.sh diff --git a/README.md b/README.md index 0dc457d..a87bf74 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,9 @@ jobs: channel: ${{ inputs.channel }} build-number: ${{ inputs.build-number || 'auto' }} marketing-version: ${{ inputs.marketing-version }} + # Optional: publish a versioned download filename instead of the + # product-derived default (for example, YourApp.dmg). + # artifact-name: YourApp-${{ inputs.marketing-version }}.dmg # Leave false to use your account default: Amore+ ships a clean DMG, # free accounts always ship the "Built with amore.computer" watermark. # Set true only if you want to keep the watermark. @@ -190,6 +193,8 @@ Self-hosting on S3 / R2 / MinIO instead of Amore hosting? Also set `AWS_ACCESS_K `scheme` is the Xcode scheme to build. `channel` picks the release channel (e.g. `alpha`, `beta`); empty or `stable` ships stable, so a `workflow_dispatch` choice input can pass straight through. Everything else has a sensible default; see [`action.yml`](action.yml) for the full list, including `path`, `release-notes`, `critical`, `draft`, `phased-rollout`, `watermark`, `no-dmg`, `provisioning-profile`, and the `s3-*` hosting inputs. +`artifact-name` optionally controls the published download filename. Include the extension (`.dmg`, or `.zip` with `no-dmg: true`) and do not include a directory. The action first produces the signed and notarized artifact locally, renames it, then publishes that existing artifact. When omitted, the action keeps the normal one-pass release flow. + `build-number` defaults to `auto`: one past the highest build number the destination already has published. Use `timestamp` if you'd rather not depend on the destination being reachable, or pass an explicit integer. `provisioning-profile` (base64 of a `.provisionprofile`) is only needed if your app's entitlements require one, e.g. Associated Domains. diff --git a/action.yml b/action.yml index 23b3fdc..2a0104a 100644 --- a/action.yml +++ b/action.yml @@ -45,6 +45,10 @@ inputs: description: Ship a ZIP instead of a DMG (skips code signing and notarization). required: false default: "false" + artifact-name: + description: Optional filename for the published DMG or ZIP, including its extension. When set, the action builds locally, renames the artifact, then publishes it. + required: false + default: "" cache: description: '"true" caches Swift package checkouts in .amore/SourcePackages between runs via actions/cache. "external" still pins the checkouts to that directory but leaves caching it to the workflow, for provider-specific cache actions. "false" disables both.' required: false @@ -199,6 +203,7 @@ runs: IN_PHASED_ROLLOUT: ${{ inputs.phased-rollout }} IN_WATERMARK: ${{ inputs.watermark }} IN_NO_DMG: ${{ inputs.no-dmg }} + IN_ARTIFACT_NAME: ${{ inputs.artifact-name }} IN_BUILD_NUMBER: ${{ inputs.build-number }} IN_MARKETING_VERSION: ${{ inputs.marketing-version }} IN_BUNDLE_ID: ${{ inputs.bundle-id }} @@ -218,29 +223,4 @@ runs: AMORE_TOKEN: ${{ inputs.amore-token }} AMORE_CLONED_SOURCE_PACKAGES_DIR: ${{ (inputs.cache == 'true' || inputs.cache == 'external') && format('{0}/.amore/SourcePackages', github.workspace) || '' }} run: | - set -euo pipefail - args=(release) - [ -n "$IN_PATH" ] && args+=("$IN_PATH") - [ -n "$IN_SCHEME" ] && args+=(--scheme "$IN_SCHEME") - [ -n "$IN_CODESIGN_IDENTITY" ] && args+=(--codesign-identity "$IN_CODESIGN_IDENTITY") - [ -n "$IN_RELEASE_NOTES" ] && args+=(--release-notes "$IN_RELEASE_NOTES") - [ -n "$IN_BUILD_NUMBER" ] && args+=(--build-number "$IN_BUILD_NUMBER") - [ -n "$IN_MARKETING_VERSION" ] && args+=(--marketing-version "$IN_MARKETING_VERSION") - [ -n "$IN_BUNDLE_ID" ] && args+=(--bundle-id "$IN_BUNDLE_ID") - [ "$IN_CHANNEL" = "stable" ] && IN_CHANNEL="" - [ -n "$IN_CHANNEL" ] && args+=(--channel "$IN_CHANNEL") - [ "$IN_CRITICAL" = "true" ] && args+=(--critical) - [ "$IN_DRAFT" = "true" ] && args+=(--draft) - [ "$IN_PHASED_ROLLOUT" = "true" ] && args+=(--phased-rollout) - [ "$IN_WATERMARK" = "true" ] && args+=(--watermark) - [ "$IN_NO_DMG" = "true" ] && args+=(--no-dmg) - - amore "${args[@]}" --format json | tee release.json - - { - echo "version=$(jq -r '.release.bundleVersionString // ""' release.json)" - echo "build-number=$(jq -r '.release.bundleVersion // ""' release.json)" - echo "bundle-id=$(jq -r '.app.bundleIdentifier // ""' release.json)" - echo "download-url=$(jq -r '.release.downloadURL // ""' release.json)" - echo "latest-url=$(jq -r '.latestDownloadURL // ""' release.json)" - } >> "$GITHUB_OUTPUT" + "$GITHUB_ACTION_PATH/scripts/release.sh" diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..dceb95c --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash + +set -euo pipefail + +add_source_args() { + [ -n "$IN_PATH" ] && args+=("$IN_PATH") + [ -n "$IN_SCHEME" ] && args+=(--scheme "$IN_SCHEME") + [ -n "$IN_CODESIGN_IDENTITY" ] && args+=(--codesign-identity "$IN_CODESIGN_IDENTITY") + [ -n "$IN_BUILD_NUMBER" ] && args+=(--build-number "$IN_BUILD_NUMBER") + [ -n "$IN_MARKETING_VERSION" ] && args+=(--marketing-version "$IN_MARKETING_VERSION") + [ -n "$IN_BUNDLE_ID" ] && args+=(--bundle-id "$IN_BUNDLE_ID") + [ "$IN_WATERMARK" = "true" ] && args+=(--watermark) + [ "$IN_NO_DMG" = "true" ] && args+=(--no-dmg) + return 0 +} + +add_publish_args() { + [ -n "$IN_RELEASE_NOTES" ] && args+=(--release-notes "$IN_RELEASE_NOTES") + [ -n "$IN_CHANNEL" ] && args+=(--channel "$IN_CHANNEL") + [ "$IN_CRITICAL" = "true" ] && args+=(--critical) + [ "$IN_DRAFT" = "true" ] && args+=(--draft) + [ "$IN_PHASED_ROLLOUT" = "true" ] && args+=(--phased-rollout) + return 0 +} + +publish_named_artifact() { + case "$IN_ARTIFACT_NAME" in + */*|*\\*|.|..) + echo "artifact-name must be a filename without directory components" >&2 + exit 1 + ;; + esac + + local extension="dmg" + [ "$IN_NO_DMG" = "true" ] && extension="zip" + case "$IN_ARTIFACT_NAME" in + *."$extension") ;; + *) + echo "artifact-name must end in .$extension" >&2 + exit 1 + ;; + esac + + local temp_root="${RUNNER_TEMP:-${TMPDIR:-/tmp}}" + local artifact_dir + artifact_dir=$(mktemp -d "$temp_root/amore-release.XXXXXX") + cleanup_artifact_dir="$artifact_dir" + trap 'rm -rf "$cleanup_artifact_dir"' EXIT + + args=(release) + add_source_args + args+=(--output "$artifact_dir") + amore "${args[@]}" --format json > "$artifact_dir/build.json" + + local artifact_path + artifact_path=$(find "$artifact_dir" -maxdepth 1 -type f -name "*.$extension" -print -quit) + if [ -z "$artifact_path" ]; then + echo "Amore did not produce a .$extension artifact in $artifact_dir" >&2 + exit 1 + fi + if [ "$(find "$artifact_dir" -maxdepth 1 -type f -name "*.$extension" | wc -l | tr -d ' ')" -ne 1 ]; then + echo "Amore produced more than one .$extension artifact in $artifact_dir" >&2 + exit 1 + fi + + local named_artifact="$artifact_dir/$IN_ARTIFACT_NAME" + if [ "$artifact_path" != "$named_artifact" ]; then + mv "$artifact_path" "$named_artifact" + fi + + args=(release "$named_artifact") + [ -n "$IN_CODESIGN_IDENTITY" ] && args+=(--codesign-identity "$IN_CODESIGN_IDENTITY") + add_publish_args + amore "${args[@]}" --format json | tee release.json +} + +[ "$IN_CHANNEL" = "stable" ] && IN_CHANNEL="" + +if [ -n "$IN_ARTIFACT_NAME" ]; then + publish_named_artifact +else + args=(release) + add_source_args + add_publish_args + amore "${args[@]}" --format json | tee release.json +fi + +{ + echo "version=$(jq -r '.release.bundleVersionString // ""' release.json)" + echo "build-number=$(jq -r '.release.bundleVersion // ""' release.json)" + echo "bundle-id=$(jq -r '.app.bundleIdentifier // ""' release.json)" + echo "download-url=$(jq -r '.release.downloadURL // ""' release.json)" + echo "latest-url=$(jq -r '.latestDownloadURL // ""' release.json)" +} >> "$GITHUB_OUTPUT" diff --git a/tests/release_test.sh b/tests/release_test.sh new file mode 100755 index 0000000..691abea --- /dev/null +++ b/tests/release_test.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root=$(cd "$(dirname "$0")/.." && pwd) +test_root=$(mktemp -d "${TMPDIR:-/tmp}/amore-action-test.XXXXXX") +trap 'rm -rf "$test_root"' EXIT + +mkdir -p "$test_root/bin" "$test_root/runner" +cat > "$test_root/bin/amore" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +{ + echo CALL + printf 'ARG=%s\n' "$@" +} >> "$AMORE_TEST_LOG" + +output="" +extension="dmg" +for arg in "$@"; do + [ "$arg" = "--no-dmg" ] && extension="zip" +done +while [ "$#" -gt 0 ]; do + if [ "$1" = "--output" ]; then + output="$2" + shift 2 + else + shift + fi +done + +if [ -n "$output" ]; then + mkdir -p "$output" + printf 'artifact' > "$output/Generated.$extension" +fi + +printf '%s\n' '{"release":{"bundleVersionString":"1.2.3","bundleVersion":"42","downloadURL":"https://example.com/download"},"app":{"bundleIdentifier":"com.example.app"},"latestDownloadURL":"https://example.com/latest"}' +EOF +chmod +x "$test_root/bin/amore" + +run_release() { + ( + cd "$test_root" + env \ + PATH="$test_root/bin:$PATH" \ + AMORE_TEST_LOG="$test_root/amore.log" \ + GITHUB_OUTPUT="$test_root/github-output" \ + RUNNER_TEMP="$test_root/runner" \ + IN_SCHEME="Example" \ + IN_PATH="Example.xcodeproj" \ + IN_CODESIGN_IDENTITY="Developer ID" \ + IN_RELEASE_NOTES="Fixed things" \ + IN_CHANNEL="beta" \ + IN_CRITICAL="false" \ + IN_DRAFT="false" \ + IN_PHASED_ROLLOUT="false" \ + IN_WATERMARK="false" \ + IN_NO_DMG="${2:-false}" \ + IN_BUILD_NUMBER="auto" \ + IN_MARKETING_VERSION="1.2.3" \ + IN_BUNDLE_ID="com.example.app" \ + IN_ARTIFACT_NAME="$1" \ + bash "$repo_root/scripts/release.sh" + ) +} + +run_release "Example-1.2.3.dmg" >/dev/null + +[ "$(grep -c '^CALL$' "$test_root/amore.log")" -eq 2 ] +[ "$(grep -c '^ARG=--build-number$' "$test_root/amore.log")" -eq 1 ] +[ "$(grep -c '^ARG=--release-notes$' "$test_root/amore.log")" -eq 1 ] +[ "$(grep -c '^ARG=--codesign-identity$' "$test_root/amore.log")" -eq 2 ] +grep -q '^ARG=--output$' "$test_root/amore.log" +grep -q '/Example-1.2.3.dmg$' "$test_root/amore.log" +grep -q '^version=1.2.3$' "$test_root/github-output" +grep -q '^build-number=42$' "$test_root/github-output" + +: > "$test_root/amore.log" +: > "$test_root/github-output" +run_release "" >/dev/null + +[ "$(grep -c '^CALL$' "$test_root/amore.log")" -eq 1 ] +[ "$(grep -c '^ARG=--build-number$' "$test_root/amore.log")" -eq 1 ] +[ "$(grep -c '^ARG=--release-notes$' "$test_root/amore.log")" -eq 1 ] +[ "$(grep -c '^ARG=--codesign-identity$' "$test_root/amore.log")" -eq 1 ] +! grep -q '^ARG=--output$' "$test_root/amore.log" + +: > "$test_root/amore.log" +: > "$test_root/github-output" +run_release "Example-1.2.3.zip" "true" >/dev/null + +[ "$(grep -c '^CALL$' "$test_root/amore.log")" -eq 2 ] +[ "$(grep -c '^ARG=--no-dmg$' "$test_root/amore.log")" -eq 1 ] +grep -q '/Example-1.2.3.zip$' "$test_root/amore.log" + +if run_release "nested/Example.dmg" >/dev/null 2>&1; then + echo "Expected a nested artifact-name to fail" >&2 + exit 1 +fi + +echo "release tests passed"