Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
32 changes: 6 additions & 26 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 }}
Expand All @@ -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"
94 changes: 94 additions & 0 deletions scripts/release.sh
Original file line number Diff line number Diff line change
@@ -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"
102 changes: 102 additions & 0 deletions tests/release_test.sh
Original file line number Diff line number Diff line change
@@ -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"