Skip to content
Merged
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
70 changes: 70 additions & 0 deletions .github/actions/fetch-release-asset/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Fetch release asset
description: Downloads a published release asset, retrying while GitHub finishes making it available, and returns its checksum

inputs:
repository:
description: Repository the release belongs to, as owner/name
required: true
tag:
description: Release tag the asset was published under
required: true
asset:
description: Asset file name
required: true
attempts:
description: How many times to retry the download
required: false
default: "5"

outputs:
url:
description: Public download URL of the asset
value: ${{ steps.fetch.outputs.url }}
path:
description: Local path of the downloaded asset
value: ${{ steps.fetch.outputs.path }}
sha256:
description: SHA-256 checksum of the downloaded asset
value: ${{ steps.fetch.outputs.sha256 }}

runs:
using: composite
steps:
- name: Download asset and compute checksum
id: fetch
shell: bash
env:
ASSET_REPOSITORY: ${{ inputs.repository }}
ASSET_TAG: ${{ inputs.tag }}
ASSET_NAME: ${{ inputs.asset }}
ASSET_ATTEMPTS: ${{ inputs.attempts }}
run: |
set -euo pipefail

url="https://github.com/${ASSET_REPOSITORY}/releases/download/${ASSET_TAG}/${ASSET_NAME}"
dest="${RUNNER_TEMP}/${ASSET_NAME}"

downloaded=""
for attempt in $(seq 1 "$ASSET_ATTEMPTS"); do
if curl -fSL -o "$dest" "$url"; then
downloaded=1
break
fi
echo "Asset not downloadable yet (attempt ${attempt}/${ASSET_ATTEMPTS}); retrying..."
sleep $((attempt * 10))
done

if [[ -z "$downloaded" || ! -s "$dest" ]]; then
echo "::error::Could not download ${url}"
exit 1
fi

sha256="$(sha256sum "$dest" | awk '{print $1}')"

{
echo "url=$url"
echo "path=$dest"
echo "sha256=$sha256"
} >> "$GITHUB_OUTPUT"

echo "Fetched ${ASSET_NAME} (sha256 ${sha256})"
98 changes: 98 additions & 0 deletions .github/actions/publish-manifest/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: Publish package manifest
description: Commits a generated package manifest to an external repository, rebasing onto releases published concurrently

inputs:
repository:
description: Target repository, as owner/name
required: true
token:
description: Token with contents write access on the target repository
required: true
path:
description: Manifest path inside the target repository
required: true
content:
description: Full manifest contents
required: true
message:
description: Commit message
required: true
branch:
description: Branch to push to
required: false
default: main
attempts:
description: How many times to retry a rejected push
required: false
default: "5"
validate-json:
description: Parse the manifest as JSON before committing
required: false
default: "false"

runs:
using: composite
steps:
- name: Commit manifest
shell: bash
env:
TARGET_REPOSITORY: ${{ inputs.repository }}
TARGET_TOKEN: ${{ inputs.token }}
TARGET_PATH: ${{ inputs.path }}
TARGET_BRANCH: ${{ inputs.branch }}
MANIFEST_CONTENT: ${{ inputs.content }}
COMMIT_MESSAGE: ${{ inputs.message }}
PUSH_ATTEMPTS: ${{ inputs.attempts }}
VALIDATE_JSON: ${{ inputs.validate-json }}
run: |
set -uo pipefail

if [[ -z "${MANIFEST_CONTENT//[[:space:]]/}" ]]; then
echo "::error::Manifest content is empty"
exit 1
fi

clone="${RUNNER_TEMP}/publish-manifest"
rm -rf "$clone"

if ! git clone "https://x-access-token:${TARGET_TOKEN}@github.com/${TARGET_REPOSITORY}.git" "$clone"; then
echo "::error::Could not clone ${TARGET_REPOSITORY}"
exit 1
fi

cd "$clone" || exit 1
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

mkdir -p "$(dirname "$TARGET_PATH")"
printf '%s\n' "${MANIFEST_CONTENT%$'\n'}" > "$TARGET_PATH"

if [[ "$VALIDATE_JSON" == "true" ]] && ! jq empty "$TARGET_PATH"; then
echo "::error::Generated manifest is not valid JSON"
cat "$TARGET_PATH"
exit 1
fi

git add "$TARGET_PATH"
if git diff --cached --quiet; then
echo "Manifest already up to date; nothing to publish"
exit 0
fi

if ! git commit -m "$COMMIT_MESSAGE"; then
echo "::error::Could not commit ${TARGET_PATH}"
exit 1
fi

for attempt in $(seq 1 "$PUSH_ATTEMPTS"); do
if git push origin "HEAD:${TARGET_BRANCH}"; then
echo "Published ${TARGET_PATH} to ${TARGET_REPOSITORY}"
exit 0
fi
echo "Push rejected (attempt ${attempt}/${PUSH_ATTEMPTS}); rebasing onto concurrent release..."
git fetch origin "$TARGET_BRANCH" && git rebase "origin/${TARGET_BRANCH}"
sleep $((RANDOM % 5 + 3))
done

echo "::error::Could not push ${TARGET_PATH} to ${TARGET_REPOSITORY} after ${PUSH_ATTEMPTS} attempts"
exit 1
67 changes: 67 additions & 0 deletions .github/actions/resolve-version/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Resolve version
description: Derives every version representation the release pipeline needs from a single semver string

inputs:
version:
description: Semver string, optionally with a prerelease suffix (e.g. 1.2.3 or 1.2.3-beta.1)
required: true

outputs:
version:
description: The version exactly as given
value: ${{ steps.derive.outputs.version }}
build-name:
description: Version stripped of its prerelease suffix
value: ${{ steps.derive.outputs.build-name }}
build-number:
description: Monotonic integer derived from build-name, used as CFBundleVersion
value: ${{ steps.derive.outputs.build-number }}
version4:
description: Four-component version required by MSIX manifests
value: ${{ steps.derive.outputs.version4 }}
is-prerelease:
description: "true when the version carries a prerelease suffix"
value: ${{ steps.derive.outputs.is-prerelease }}

runs:
using: composite
steps:
- name: Derive version representations
id: derive
shell: bash
env:
RAW_VERSION: ${{ inputs.version }}
run: |
set -euo pipefail

version="$RAW_VERSION"
if [[ ! "$version" =~ ^[0-9]+(\.[0-9]+){0,2}(-[0-9A-Za-z.-]+)?$ ]]; then
echo "::error::'$version' is not a supported version string (expected MAJOR[.MINOR[.PATCH]][-PRERELEASE])"
exit 1
fi

if [[ "$version" == *-* ]]; then
is_prerelease=true
else
is_prerelease=false
fi

build_name="${version%%-*}"
IFS=. read -r major minor patch <<< "$build_name"

major=$((10#${major:-0}))
minor=$((10#${minor:-0}))
patch=$((10#${patch:-0}))

build_number=$(printf '%d%03d%03d' "$major" "$minor" "$patch")
version4="${major}.${minor}.${patch}.0"

{
echo "version=$version"
echo "build-name=$build_name"
echo "build-number=$build_number"
echo "version4=$version4"
echo "is-prerelease=$is_prerelease"
} >> "$GITHUB_OUTPUT"

echo "version=$version build-name=$build_name build-number=$build_number version4=$version4 prerelease=$is_prerelease"
27 changes: 27 additions & 0 deletions .github/actions/set-app-version/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Set app version
description: Writes the release version into the app pubspec so the produced binaries carry it

inputs:
version:
description: Version to write into the pubspec
required: true
pubspec:
description: Path to the pubspec to rewrite
required: false
default: apps/linkunbound/pubspec.yaml

runs:
using: composite
steps:
- name: Write version into pubspec
shell: bash
env:
APP_VERSION: ${{ inputs.version }}
PUBSPEC: ${{ inputs.pubspec }}
run: |
set -euo pipefail

sed "s|^version:.*|version: ${APP_VERSION}|" "$PUBSPEC" > "${PUBSPEC}.tmp"
mv "${PUBSPEC}.tmp" "$PUBSPEC"

grep '^version:' "$PUBSPEC"
32 changes: 32 additions & 0 deletions .github/actions/setup-flutter/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Set up Flutter workspace
description: Installs the pinned Flutter SDK with SDK and pub caches, then bootstraps the melos workspace

inputs:
flutter-version:
description: Flutter SDK version to install
required: false
default: "3.44.1"
bootstrap:
description: Run melos bootstrap after installing the SDK
required: false
default: "true"

runs:
using: composite
steps:
- uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: ${{ inputs.flutter-version }}
cache: true

- name: Install melos
if: inputs.bootstrap == 'true'
shell: bash
run: dart pub global activate melos

- name: Bootstrap workspace
if: inputs.bootstrap == 'true'
shell: bash
# Invoked through pub because the Windows shim is melos.bat, which bash does not resolve.
run: dart pub global run melos bootstrap
Loading
Loading